gruel_air/sema/
analysis.rs

1//! Function body analysis and AIR generation.
2//!
3//! This module contains the core semantic analysis functionality:
4//! - Function analysis (analyze_single_function, analyze_method_function, analyze_destructor_function)
5//! - Hindley-Milner type inference (run_type_inference)
6//! - RIR to AIR instruction lowering (analyze_inst)
7//! - Helper functions for expression analysis
8//!
9//! # Parallel Analysis
10//!
11//! Function body analysis is parallelized using rayon. The architecture:
12//! 1. Declaration gathering (sequential) builds an immutable `SemaContext`
13//! 2. Function jobs are collected describing each function to analyze
14//! 3. Jobs are processed in parallel using `par_iter`
15//! 4. Results are merged (strings deduplicated, warnings collected)
16
17use std::collections::{HashMap, HashSet};
18
19use gruel_builtins::BuiltinTypeDef;
20use gruel_error::{
21    CompileError, CompileErrors, CompileResult, CompileWarning, ErrorKind,
22    IntrinsicTypeMismatchError, MultiErrorResult, OptionExt, PreviewFeature, WarningKind,
23};
24use gruel_rir::{
25    InstData, InstRef, RirArgMode, RirCallArg, RirDirective, RirParamMode, RirPattern,
26};
27use gruel_span::Span;
28use gruel_target::{Arch, Os};
29use lasso::Spur;
30use tracing::info_span;
31
32use super::context::{
33    AnalysisContext, AnalysisResult, BuiltinMethodContext, ComptimeHeapItem, ConstValue, ParamInfo,
34    ReceiverInfo, StringReceiverStorage,
35};
36use super::{AnalyzedFunction, InferenceContext, MethodInfo, Sema, SemaOutput};
37use crate::inference::{
38    Constraint, ConstraintContext, ConstraintGenerator, ParamVarInfo, Unifier, UnifyResult,
39};
40use crate::inst::{
41    Air, AirArgMode, AirCallArg, AirInst, AirInstData, AirPlaceBase, AirProjection, AirRef,
42};
43use crate::types::{EnumId, EnumVariantDef, StructField, StructId, Type, TypeKind};
44
45/// Data describing a method body for analysis.
46struct MethodBodySpec<'a> {
47    return_type: Spur,
48    params: &'a [gruel_rir::RirParam],
49    body: InstRef,
50    /// The type of `self`, or `None` if this is a static/associated function.
51    self_type: Option<Type>,
52}
53
54/// Result of analyzing a function: analyzed function, warnings, local strings,
55/// referenced functions, and referenced methods.
56type AnalyzedFnResult = CompileResult<(
57    AnalyzedFunction,
58    Vec<CompileWarning>,
59    Vec<String>,
60    HashSet<Spur>,
61    HashSet<(StructId, Spur)>,
62)>;
63
64/// Raw analysis output: air, local count, param slots, param modes, param slot types,
65/// warnings, local strings, referenced functions, and referenced methods.
66type RawFnAnalysis = CompileResult<(
67    Air,
68    u32,
69    u32,
70    Vec<bool>,
71    Vec<Type>,
72    Vec<CompileWarning>,
73    Vec<String>,
74    HashSet<Spur>,
75    HashSet<(StructId, Spur)>,
76)>;
77
78/// Arguments for [`Sema::register_anon_struct_methods_for_comptime_with_subst`].
79struct AnonStructSpec {
80    struct_id: StructId,
81    struct_type: crate::types::Type,
82    methods_start: u32,
83    methods_len: u32,
84}
85
86/// Main entry point for analyzing all function bodies.
87///
88/// Called from Sema::analyze_all after declarations are collected.
89/// Currently uses the sequential analysis path while the parallel infrastructure
90/// is being completed.
91///
92/// # Parallel Analysis Infrastructure
93///
94/// The parallel analysis infrastructure is ready but not all instruction types
95/// are implemented in `analyze_inst_with_context` yet. Once complete:
96/// 1. Build `SemaContext` from `Sema`
97/// 2. Collect function jobs with `collect_function_jobs`
98/// 3. Process with `par_iter` using `analyze_function_job`
99/// 4. Merge with `merge_function_results`
100pub(crate) fn analyze_all_function_bodies(mut sema: Sema<'_>) -> MultiErrorResult<SemaOutput> {
101    // Use lazy analysis when imports are present (multi-file compilation)
102    // This ensures only reachable code is analyzed, per ADR-0026
103    if sema.has_imports() {
104        analyze_function_bodies_lazy(&mut sema)
105    } else {
106        // Use eager analysis for single-file compilation (backwards compatibility)
107        analyze_all_function_bodies_sequential(&mut sema)
108    }
109}
110
111/// Sequential analysis path (current implementation).
112fn analyze_all_function_bodies_sequential(sema: &mut Sema<'_>) -> MultiErrorResult<SemaOutput> {
113    // Build inference context once
114    let infer_ctx = sema.build_inference_context();
115
116    // Collect analyzed functions with their local strings.
117    let mut functions_with_strings: Vec<(AnalyzedFunction, Vec<String>)> = Vec::new();
118    let mut errors = CompileErrors::new();
119    let mut all_warnings = Vec::new();
120
121    // Collect method refs from struct declarations to skip them when analyzing regular functions
122    let mut method_refs: HashSet<InstRef> = HashSet::new();
123    for (_, inst) in sema.rir.iter() {
124        match &inst.data {
125            InstData::StructDecl {
126                methods_start,
127                methods_len,
128                ..
129            } => {
130                let methods = sema.rir.get_inst_refs(*methods_start, *methods_len);
131                for method_ref in methods {
132                    method_refs.insert(method_ref);
133                }
134            }
135            // Also collect methods from anonymous structs/enums (inside comptime functions)
136            InstData::AnonStructType {
137                methods_start,
138                methods_len,
139                ..
140            }
141            | InstData::AnonEnumType {
142                methods_start,
143                methods_len,
144                ..
145            } => {
146                if *methods_len > 0 {
147                    let methods = sema.rir.get_inst_refs(*methods_start, *methods_len);
148                    for method_ref in methods {
149                        method_refs.insert(method_ref);
150                    }
151                }
152            }
153            _ => {}
154        }
155    }
156
157    // Analyze regular functions (skip generic functions - they're analyzed during specialization)
158    for (inst_ref, inst) in sema.rir.iter() {
159        if let InstData::FnDecl {
160            name,
161            params_start,
162            params_len,
163            return_type,
164            body,
165            has_self,
166            ..
167        } = &inst.data
168        {
169            if method_refs.contains(&inst_ref) {
170                continue;
171            }
172
173            // Skip methods (has_self = true) - these are handled elsewhere:
174            // - Named struct methods are collected below via StructDecl
175            // - Anonymous struct methods are analyzed in the fixed-point loop later
176            if *has_self {
177                continue;
178            }
179
180            // Skip FnDecls that are not in the functions table.
181            // These are anonymous struct methods which are analyzed separately.
182            if !sema.functions.contains_key(name) {
183                continue;
184            }
185
186            // Skip generic functions - they'll be analyzed during specialization
187            if let Some(fn_info) = sema.functions.get(name)
188                && fn_info.is_generic
189            {
190                continue;
191            }
192
193            let fn_name = sema.interner.resolve(name).to_string();
194            let params = sema.rir.get_params(*params_start, *params_len);
195
196            match sema.analyze_single_function(
197                &infer_ctx,
198                &fn_name,
199                *return_type,
200                &params,
201                *body,
202                inst.span,
203            ) {
204                Ok((analyzed, warnings, local_strings, _ref_fns, _ref_meths)) => {
205                    functions_with_strings.push((analyzed, local_strings));
206                    all_warnings.extend(warnings);
207                }
208                Err(e) => errors.push(e),
209            }
210        }
211    }
212
213    // Analyze method bodies from struct declarations
214    for (_, inst) in sema.rir.iter() {
215        if let InstData::StructDecl {
216            name: type_name,
217            methods_start,
218            methods_len,
219            ..
220        } = &inst.data
221        {
222            let type_name_str = sema.interner.resolve(type_name).to_string();
223            let struct_id = match sema.structs.get(type_name) {
224                Some(id) => *id,
225                None => {
226                    errors.push(CompileError::new(
227                        ErrorKind::InternalError(format!(
228                            "struct '{}' not found in struct map during method analysis",
229                            type_name_str
230                        )),
231                        inst.span,
232                    ));
233                    continue;
234                }
235            };
236            let struct_type = Type::new_struct(struct_id);
237
238            let methods = sema.rir.get_inst_refs(*methods_start, *methods_len);
239            for method_ref in methods {
240                let method_inst = sema.rir.get(method_ref);
241                if let InstData::FnDecl {
242                    name: method_name,
243                    params_start,
244                    params_len,
245                    return_type,
246                    body,
247                    has_self,
248                    ..
249                } = &method_inst.data
250                {
251                    let method_name_str = sema.interner.resolve(method_name).to_string();
252                    let params = sema.rir.get_params(*params_start, *params_len);
253
254                    let full_name = if *has_self {
255                        format!("{}.{}", type_name_str, method_name_str)
256                    } else {
257                        format!("{}::{}", type_name_str, method_name_str)
258                    };
259
260                    match sema.analyze_method_function(
261                        &infer_ctx,
262                        &full_name,
263                        MethodBodySpec {
264                            return_type: *return_type,
265                            params: &params,
266                            body: *body,
267                            self_type: has_self.then_some(struct_type),
268                        },
269                        method_inst.span,
270                    ) {
271                        Ok((analyzed, warnings, local_strings, _ref_fns, _ref_meths)) => {
272                            functions_with_strings.push((analyzed, local_strings));
273                            all_warnings.extend(warnings);
274                        }
275                        Err(e) => errors.push(e),
276                    }
277                }
278            }
279        }
280    }
281
282    // Analyze destructor bodies
283    for (_, inst) in sema.rir.iter() {
284        if let InstData::DropFnDecl { type_name, body } = &inst.data {
285            let type_name_str = sema.interner.resolve(type_name).to_string();
286            let struct_id = match sema.structs.get(type_name) {
287                Some(id) => *id,
288                None => {
289                    errors.push(CompileError::new(
290                        ErrorKind::InternalError(format!(
291                            "destructor for undefined type '{}' survived validation",
292                            type_name_str
293                        )),
294                        inst.span,
295                    ));
296                    continue;
297                }
298            };
299            let struct_type = Type::new_struct(struct_id);
300            let full_name = format!("{}.__drop", type_name_str);
301
302            match sema.analyze_destructor_function(
303                &infer_ctx,
304                &full_name,
305                *body,
306                inst.span,
307                struct_type,
308            ) {
309                Ok((analyzed, warnings, local_strings, _ref_fns, _ref_meths)) => {
310                    functions_with_strings.push((analyzed, local_strings));
311                    all_warnings.extend(warnings);
312                }
313                Err(e) => errors.push(e),
314            }
315        }
316    }
317
318    // Analyze methods for anonymous structs.
319    // These are registered during comptime evaluation of function bodies, so they
320    // aren't in any named StructDecl. We use a fixed-point loop since analyzing one
321    // method may create new anonymous struct types with their own methods.
322    let mut analyzed_anon_methods: HashSet<(StructId, Spur)> = HashSet::new();
323    let mut analyzed_anon_enum_methods: HashSet<(EnumId, Spur)> = HashSet::new();
324    loop {
325        // Collect anonymous struct methods that haven't been analyzed yet
326        let pending_anon_methods: Vec<(StructId, Spur, MethodInfo)> = sema
327            .methods
328            .iter()
329            .filter_map(|((struct_id, method_name), method_info)| {
330                // Check if this is an anonymous struct
331                let struct_def = sema.type_pool.struct_def(*struct_id);
332                if struct_def.name.starts_with("__anon_struct_")
333                    && !analyzed_anon_methods.contains(&(*struct_id, *method_name))
334                {
335                    Some((*struct_id, *method_name, *method_info))
336                } else {
337                    None
338                }
339            })
340            .collect();
341
342        // Collect anonymous enum methods that haven't been analyzed yet
343        let pending_anon_enum_methods: Vec<(EnumId, Spur, MethodInfo)> = sema
344            .enum_methods
345            .iter()
346            .filter_map(|((enum_id, method_name), method_info)| {
347                let enum_def = sema.type_pool.enum_def(*enum_id);
348                if enum_def.name.starts_with("__anon_enum_")
349                    && !analyzed_anon_enum_methods.contains(&(*enum_id, *method_name))
350                {
351                    Some((*enum_id, *method_name, *method_info))
352                } else {
353                    None
354                }
355            })
356            .collect();
357
358        if pending_anon_methods.is_empty() && pending_anon_enum_methods.is_empty() {
359            break;
360        }
361
362        for (struct_id, method_name, method_info) in pending_anon_methods {
363            analyzed_anon_methods.insert((struct_id, method_name));
364
365            let struct_def = sema.type_pool.struct_def(struct_id);
366            let type_name_str = struct_def.name.clone();
367            let method_name_str = sema.interner.resolve(&method_name).to_string();
368
369            let full_name = if method_info.has_self {
370                format!("{}.{}", type_name_str, method_name_str)
371            } else {
372                format!("{}::{}", type_name_str, method_name_str)
373            };
374
375            // Build param_info from MethodInfo's ParamRange
376            let param_names = sema.param_arena.names(method_info.params);
377            let param_types = sema.param_arena.types(method_info.params);
378            let param_modes = sema.param_arena.modes(method_info.params);
379
380            let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
381
382            if method_info.has_self {
383                // Add self parameter (Normal mode - passed by value)
384                let self_sym = sema.interner.get_or_intern("self");
385                param_info.push((self_sym, method_info.struct_type, RirParamMode::Normal));
386            }
387
388            // Add regular parameters (convert from arena slices)
389            for i in 0..param_names.len() {
390                param_info.push((param_names[i], param_types[i], param_modes[i]));
391            }
392
393            // Retrieve captured comptime values from struct-level storage
394            // Clone the HashMap to avoid borrowing issues with mutable analyze_method_body call
395            let struct_id = method_info
396                .struct_type
397                .as_struct()
398                .expect("method must belong to struct");
399            let captured_values = sema
400                .anon_struct_captured_values
401                .get(&struct_id)
402                .cloned()
403                .unwrap_or_else(HashMap::new);
404
405            match sema.analyze_method_body(
406                &infer_ctx,
407                method_info.return_type,
408                &param_info,
409                method_info.body,
410                method_info.struct_type,
411                &captured_values,
412            ) {
413                Ok((
414                    air,
415                    num_locals,
416                    num_param_slots,
417                    param_modes_result,
418                    param_slot_types,
419                    warnings,
420                    local_strings,
421                    _ref_fns,
422                    _ref_meths,
423                )) => {
424                    let analyzed = AnalyzedFunction {
425                        name: full_name,
426                        air,
427                        num_locals,
428                        num_param_slots,
429                        param_modes: param_modes_result,
430                        param_slot_types,
431                        is_destructor: false,
432                    };
433                    functions_with_strings.push((analyzed, local_strings));
434                    all_warnings.extend(warnings);
435                }
436                Err(e) => errors.push(e),
437            }
438        }
439
440        // Process anonymous enum methods in the same fixed-point loop
441        for (enum_id, method_name, method_info) in pending_anon_enum_methods {
442            analyzed_anon_enum_methods.insert((enum_id, method_name));
443
444            let enum_def = sema.type_pool.enum_def(enum_id);
445            let type_name_str = enum_def.name.clone();
446            let method_name_str = sema.interner.resolve(&method_name).to_string();
447
448            let full_name = if method_info.has_self {
449                format!("{}.{}", type_name_str, method_name_str)
450            } else {
451                format!("{}::{}", type_name_str, method_name_str)
452            };
453
454            let param_names = sema.param_arena.names(method_info.params);
455            let param_types = sema.param_arena.types(method_info.params);
456            let param_modes = sema.param_arena.modes(method_info.params);
457
458            let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
459
460            if method_info.has_self {
461                let self_sym = sema.interner.get_or_intern("self");
462                param_info.push((self_sym, method_info.struct_type, RirParamMode::Normal));
463            }
464
465            for i in 0..param_names.len() {
466                param_info.push((param_names[i], param_types[i], param_modes[i]));
467            }
468
469            let captured_values = sema
470                .anon_enum_captured_values
471                .get(&enum_id)
472                .cloned()
473                .unwrap_or_else(HashMap::new);
474
475            match sema.analyze_method_body(
476                &infer_ctx,
477                method_info.return_type,
478                &param_info,
479                method_info.body,
480                method_info.struct_type,
481                &captured_values,
482            ) {
483                Ok((
484                    air,
485                    num_locals,
486                    num_param_slots,
487                    param_modes_result,
488                    param_slot_types,
489                    warnings,
490                    local_strings,
491                    _ref_fns,
492                    _ref_meths,
493                )) => {
494                    let analyzed = AnalyzedFunction {
495                        name: full_name,
496                        air,
497                        num_locals,
498                        num_param_slots,
499                        param_modes: param_modes_result,
500                        param_slot_types,
501                        is_destructor: false,
502                    };
503                    functions_with_strings.push((analyzed, local_strings));
504                    all_warnings.extend(warnings);
505                }
506                Err(e) => errors.push(e),
507            }
508        }
509    }
510
511    // Merge strings from all functions into a global table with deduplication.
512    let mut global_string_table: HashMap<String, u32> = HashMap::new();
513    let mut global_strings: Vec<String> = Vec::new();
514
515    let mut functions: Vec<AnalyzedFunction> = Vec::new();
516    for (mut analyzed, local_strings) in functions_with_strings {
517        if !local_strings.is_empty() {
518            let local_to_global: Vec<u32> = local_strings
519                .into_iter()
520                .map(|s| {
521                    *global_string_table.entry(s.clone()).or_insert_with(|| {
522                        let id = global_strings.len() as u32;
523                        global_strings.push(s);
524                        id
525                    })
526                })
527                .collect();
528
529            analyzed
530                .air
531                .remap_string_ids(|local_id| local_to_global[local_id as usize]);
532        }
533        functions.push(analyzed);
534    }
535
536    // Emit warnings for any comptime @dbg calls that occurred during comptime evaluation.
537    for (msg, span) in std::mem::take(&mut sema.comptime_log_output) {
538        all_warnings.push(CompileWarning::new(
539            WarningKind::ComptimeDbgPresent(msg),
540            span,
541        ));
542    }
543
544    all_warnings.sort_by_key(|w| w.span().map(|s| s.start));
545
546    let mut output = SemaOutput {
547        functions,
548        strings: global_strings,
549        warnings: all_warnings,
550        type_pool: sema.type_pool.clone(),
551        comptime_dbg_output: std::mem::take(&mut sema.comptime_dbg_output),
552    };
553
554    // Run specialization pass to rewrite CallGeneric instructions to Call
555    // and create specialized function bodies
556    if let Err(e) = crate::specialize::specialize(&mut output, sema, &infer_ctx, sema.interner) {
557        errors.push(e);
558    }
559
560    errors.into_result_with(output)
561}
562
563/// Lazy analysis path (Phase 3 of module system, ADR-0026).
564///
565/// This implements "lazy semantic analysis" where only functions reachable from
566/// the entry point (main) are analyzed. Unreferenced code is not analyzed,
567/// not codegen'd, and errors in unreferenced code are not reported.
568///
569/// This is the same trade-off Zig makes for faster builds and smaller binaries.
570fn analyze_function_bodies_lazy(sema: &mut Sema<'_>) -> MultiErrorResult<SemaOutput> {
571    // Build inference context once
572    let infer_ctx = sema.build_inference_context();
573
574    // Find main() function - this is the entry point for lazy analysis
575    let main_sym = match sema.interner.get("main") {
576        Some(sym) if sema.functions.contains_key(&sym) => sym,
577        _ => {
578            // No main function found - this is an error
579            return Err(CompileErrors::from(CompileError::without_span(
580                ErrorKind::NoMainFunction,
581            )));
582        }
583    };
584
585    // Work queue: functions/methods to analyze
586    // Start with main()
587    let mut pending_functions: Vec<Spur> = vec![main_sym];
588    let mut analyzed_functions: HashSet<Spur> = HashSet::new();
589    let mut pending_methods: Vec<(StructId, Spur)> = Vec::new();
590    let mut analyzed_methods: HashSet<(StructId, Spur)> = HashSet::new();
591
592    // Collect results
593    let mut functions_with_strings: Vec<(AnalyzedFunction, Vec<String>)> = Vec::new();
594    let mut errors = CompileErrors::new();
595    let mut all_warnings = Vec::new();
596
597    // Collect method refs from struct declarations (for later lookup)
598    let mut method_refs: HashSet<InstRef> = HashSet::new();
599    for (_, inst) in sema.rir.iter() {
600        if let InstData::StructDecl {
601            methods_start,
602            methods_len,
603            ..
604        } = &inst.data
605        {
606            let methods = sema.rir.get_inst_refs(*methods_start, *methods_len);
607            for method_ref in methods {
608                method_refs.insert(method_ref);
609            }
610        }
611    }
612
613    // Process work queue until empty
614    while !pending_functions.is_empty() || !pending_methods.is_empty() {
615        // Process pending functions
616        while let Some(fn_name) = pending_functions.pop() {
617            if analyzed_functions.contains(&fn_name) {
618                continue;
619            }
620            analyzed_functions.insert(fn_name);
621
622            // Look up the function info
623            let fn_info = match sema.functions.get(&fn_name) {
624                Some(info) => *info,
625                None => continue, // Should not happen, but be defensive
626            };
627
628            // Skip generic functions - they're analyzed during specialization
629            if fn_info.is_generic {
630                continue;
631            }
632
633            let fn_name_str = sema.interner.resolve(&fn_name).to_string();
634
635            // Find the function declaration in RIR to get params
636            let mut found = false;
637            for (inst_ref, inst) in sema.rir.iter() {
638                if let InstData::FnDecl {
639                    name,
640                    params_start,
641                    params_len,
642                    return_type,
643                    body,
644                    ..
645                } = &inst.data
646                    && *name == fn_name
647                    && !method_refs.contains(&inst_ref)
648                {
649                    found = true;
650                    let params = sema.rir.get_params(*params_start, *params_len);
651
652                    match sema.analyze_single_function(
653                        &infer_ctx,
654                        &fn_name_str,
655                        *return_type,
656                        &params,
657                        *body,
658                        inst.span,
659                    ) {
660                        Ok((
661                            analyzed,
662                            warnings,
663                            local_strings,
664                            referenced_fns,
665                            referenced_meths,
666                        )) => {
667                            functions_with_strings.push((analyzed, local_strings));
668                            all_warnings.extend(warnings);
669
670                            // Add newly referenced functions to the work queue
671                            for ref_fn in referenced_fns {
672                                if !analyzed_functions.contains(&ref_fn) {
673                                    pending_functions.push(ref_fn);
674                                }
675                            }
676                            for ref_meth in referenced_meths {
677                                if !analyzed_methods.contains(&ref_meth) {
678                                    pending_methods.push(ref_meth);
679                                }
680                            }
681                        }
682                        Err(e) => errors.push(e),
683                    }
684                    break;
685                }
686            }
687
688            if !found {
689                // This could be a builtin or otherwise non-existent function
690                // Just skip it
691            }
692        }
693
694        // Process pending methods
695        while let Some((struct_id, method_name)) = pending_methods.pop() {
696            if analyzed_methods.contains(&(struct_id, method_name)) {
697                continue;
698            }
699            analyzed_methods.insert((struct_id, method_name));
700
701            // Look up the method info
702            let method_info = match sema.methods.get(&(struct_id, method_name)) {
703                Some(info) => *info,
704                None => continue,
705            };
706
707            // Get the struct definition to find its name for impl block lookup
708            let struct_def = sema.type_pool.struct_def(struct_id);
709            let type_name_str = struct_def.name.clone();
710            let type_name_sym = sema.interner.get_or_intern(&type_name_str);
711            let method_name_str = sema.interner.resolve(&method_name).to_string();
712
713            // For anonymous structs, use the MethodInfo directly since there's no named StructDecl
714            if type_name_str.starts_with("__anon_struct_") {
715                let full_name = if method_info.has_self {
716                    format!("{}.{}", type_name_str, method_name_str)
717                } else {
718                    format!("{}::{}", type_name_str, method_name_str)
719                };
720
721                // Build param_info from MethodInfo's ParamRange
722                let param_names = sema.param_arena.names(method_info.params);
723                let param_types = sema.param_arena.types(method_info.params);
724                let param_modes = sema.param_arena.modes(method_info.params);
725
726                let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
727
728                if method_info.has_self {
729                    // Add self parameter (Normal mode - passed by value)
730                    let self_sym = sema.interner.get_or_intern("self");
731                    param_info.push((self_sym, method_info.struct_type, RirParamMode::Normal));
732                }
733
734                // Add regular parameters (convert from arena slices)
735                for i in 0..param_names.len() {
736                    param_info.push((param_names[i], param_types[i], param_modes[i]));
737                }
738
739                // Retrieve captured comptime values from struct-level storage
740                // Clone the HashMap to avoid borrowing issues with mutable analyze_method_body call
741                let struct_id = method_info
742                    .struct_type
743                    .as_struct()
744                    .expect("method must belong to struct");
745                let captured_values = sema
746                    .anon_struct_captured_values
747                    .get(&struct_id)
748                    .cloned()
749                    .unwrap_or_else(HashMap::new);
750
751                match sema.analyze_method_body(
752                    &infer_ctx,
753                    method_info.return_type,
754                    &param_info,
755                    method_info.body,
756                    method_info.struct_type,
757                    &captured_values,
758                ) {
759                    Ok((
760                        air,
761                        num_locals,
762                        num_param_slots,
763                        param_modes_result,
764                        param_slot_types,
765                        warnings,
766                        local_strings,
767                        referenced_fns,
768                        referenced_meths,
769                    )) => {
770                        let analyzed = AnalyzedFunction {
771                            name: full_name,
772                            air,
773                            num_locals,
774                            num_param_slots,
775                            param_modes: param_modes_result,
776                            param_slot_types,
777                            is_destructor: false,
778                        };
779                        functions_with_strings.push((analyzed, local_strings));
780                        all_warnings.extend(warnings);
781
782                        for ref_fn in referenced_fns {
783                            if !analyzed_functions.contains(&ref_fn) {
784                                pending_functions.push(ref_fn);
785                            }
786                        }
787                        for ref_meth in referenced_meths {
788                            if !analyzed_methods.contains(&ref_meth) {
789                                pending_methods.push(ref_meth);
790                            }
791                        }
792                    }
793                    Err(e) => errors.push(e),
794                }
795                continue;
796            }
797
798            // Find the method in struct declarations (for named structs)
799            for (_, inst) in sema.rir.iter() {
800                if let InstData::StructDecl {
801                    name: struct_name,
802                    methods_start,
803                    methods_len,
804                    ..
805                } = &inst.data
806                {
807                    if *struct_name != type_name_sym {
808                        continue;
809                    }
810
811                    let methods = sema.rir.get_inst_refs(*methods_start, *methods_len);
812                    for method_ref in methods {
813                        let method_inst = sema.rir.get(method_ref);
814                        if let InstData::FnDecl {
815                            name: m_name,
816                            params_start,
817                            params_len,
818                            return_type,
819                            body,
820                            has_self,
821                            ..
822                        } = &method_inst.data
823                        {
824                            if *m_name != method_name {
825                                continue;
826                            }
827
828                            let params = sema.rir.get_params(*params_start, *params_len);
829                            let full_name = if *has_self {
830                                format!("{}.{}", type_name_str, method_name_str)
831                            } else {
832                                format!("{}::{}", type_name_str, method_name_str)
833                            };
834
835                            match sema.analyze_method_function(
836                                &infer_ctx,
837                                &full_name,
838                                MethodBodySpec {
839                                    return_type: *return_type,
840                                    params: &params,
841                                    body: *body,
842                                    self_type: has_self.then_some(method_info.struct_type),
843                                },
844                                method_inst.span,
845                            ) {
846                                Ok((
847                                    analyzed,
848                                    warnings,
849                                    local_strings,
850                                    referenced_fns,
851                                    referenced_meths,
852                                )) => {
853                                    functions_with_strings.push((analyzed, local_strings));
854                                    all_warnings.extend(warnings);
855
856                                    for ref_fn in referenced_fns {
857                                        if !analyzed_functions.contains(&ref_fn) {
858                                            pending_functions.push(ref_fn);
859                                        }
860                                    }
861                                    for ref_meth in referenced_meths {
862                                        if !analyzed_methods.contains(&ref_meth) {
863                                            pending_methods.push(ref_meth);
864                                        }
865                                    }
866                                }
867                                Err(e) => errors.push(e),
868                            }
869                        }
870                    }
871                }
872            }
873        }
874    }
875
876    // Analyze anonymous enum methods that were registered during comptime evaluation.
877    // These are not tracked by the work queue (which only handles struct methods),
878    // so we process them in a fixed-point loop similar to the eager path.
879    let mut analyzed_anon_enum_methods: HashSet<(EnumId, Spur)> = HashSet::new();
880    loop {
881        let pending_anon_enum_methods: Vec<(EnumId, Spur, MethodInfo)> = sema
882            .enum_methods
883            .iter()
884            .filter_map(|((enum_id, method_name), method_info)| {
885                let enum_def = sema.type_pool.enum_def(*enum_id);
886                if enum_def.name.starts_with("__anon_enum_")
887                    && !analyzed_anon_enum_methods.contains(&(*enum_id, *method_name))
888                {
889                    Some((*enum_id, *method_name, *method_info))
890                } else {
891                    None
892                }
893            })
894            .collect();
895
896        if pending_anon_enum_methods.is_empty() {
897            break;
898        }
899
900        for (enum_id, method_name, method_info) in pending_anon_enum_methods {
901            analyzed_anon_enum_methods.insert((enum_id, method_name));
902
903            let enum_def = sema.type_pool.enum_def(enum_id);
904            let type_name_str = enum_def.name.clone();
905            let method_name_str = sema.interner.resolve(&method_name).to_string();
906
907            let full_name = if method_info.has_self {
908                format!("{}.{}", type_name_str, method_name_str)
909            } else {
910                format!("{}::{}", type_name_str, method_name_str)
911            };
912
913            let param_names = sema.param_arena.names(method_info.params);
914            let param_types = sema.param_arena.types(method_info.params);
915            let param_modes = sema.param_arena.modes(method_info.params);
916
917            let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
918
919            if method_info.has_self {
920                let self_sym = sema.interner.get_or_intern("self");
921                param_info.push((self_sym, method_info.struct_type, RirParamMode::Normal));
922            }
923
924            for i in 0..param_names.len() {
925                param_info.push((param_names[i], param_types[i], param_modes[i]));
926            }
927
928            let captured_values = sema
929                .anon_enum_captured_values
930                .get(&enum_id)
931                .cloned()
932                .unwrap_or_else(HashMap::new);
933
934            match sema.analyze_method_body(
935                &infer_ctx,
936                method_info.return_type,
937                &param_info,
938                method_info.body,
939                method_info.struct_type,
940                &captured_values,
941            ) {
942                Ok((
943                    air,
944                    num_locals,
945                    num_param_slots,
946                    param_modes_result,
947                    param_slot_types,
948                    warnings,
949                    local_strings,
950                    _ref_fns,
951                    _ref_meths,
952                )) => {
953                    let analyzed = AnalyzedFunction {
954                        name: full_name,
955                        air,
956                        num_locals,
957                        num_param_slots,
958                        param_modes: param_modes_result,
959                        param_slot_types,
960                        is_destructor: false,
961                    };
962                    functions_with_strings.push((analyzed, local_strings));
963                    all_warnings.extend(warnings);
964                }
965                Err(e) => errors.push(e),
966            }
967        }
968    }
969
970    // Also analyze destructors for any structs whose types we've used
971    // (This is necessary because drop is implicitly called)
972    for (_, inst) in sema.rir.iter() {
973        if let InstData::DropFnDecl { type_name, body } = &inst.data {
974            let type_name_str = sema.interner.resolve(type_name).to_string();
975            let struct_id = match sema.structs.get(type_name) {
976                Some(id) => *id,
977                None => continue,
978            };
979            let struct_type = Type::new_struct(struct_id);
980            let full_name = format!("{}.__drop", type_name_str);
981
982            match sema.analyze_destructor_function(
983                &infer_ctx,
984                &full_name,
985                *body,
986                inst.span,
987                struct_type,
988            ) {
989                Ok((analyzed, warnings, local_strings, _, _)) => {
990                    functions_with_strings.push((analyzed, local_strings));
991                    all_warnings.extend(warnings);
992                }
993                Err(e) => errors.push(e),
994            }
995        }
996    }
997
998    // Merge strings from all functions into a global table with deduplication.
999    let mut global_string_table: HashMap<String, u32> = HashMap::new();
1000    let mut global_strings: Vec<String> = Vec::new();
1001
1002    let mut functions: Vec<AnalyzedFunction> = Vec::new();
1003    for (mut analyzed, local_strings) in functions_with_strings {
1004        if !local_strings.is_empty() {
1005            let local_to_global: Vec<u32> = local_strings
1006                .into_iter()
1007                .map(|s| {
1008                    *global_string_table.entry(s.clone()).or_insert_with(|| {
1009                        let id = global_strings.len() as u32;
1010                        global_strings.push(s);
1011                        id
1012                    })
1013                })
1014                .collect();
1015
1016            analyzed
1017                .air
1018                .remap_string_ids(|local_id| local_to_global[local_id as usize]);
1019        }
1020        functions.push(analyzed);
1021    }
1022
1023    // Emit warnings for any comptime @dbg calls that occurred during comptime evaluation.
1024    for (msg, span) in std::mem::take(&mut sema.comptime_log_output) {
1025        all_warnings.push(CompileWarning::new(
1026            WarningKind::ComptimeDbgPresent(msg),
1027            span,
1028        ));
1029    }
1030
1031    all_warnings.sort_by_key(|w| w.span().map(|s| s.start));
1032
1033    let mut output = SemaOutput {
1034        functions,
1035        strings: global_strings,
1036        warnings: all_warnings,
1037        type_pool: sema.type_pool.clone(),
1038        comptime_dbg_output: std::mem::take(&mut sema.comptime_dbg_output),
1039    };
1040
1041    // Run specialization pass to rewrite CallGeneric instructions to Call
1042    // and create specialized function bodies
1043    if let Err(e) = crate::specialize::specialize(&mut output, sema, &infer_ctx, sema.interner) {
1044        errors.push(e);
1045    }
1046
1047    errors.into_result_with(output)
1048}
1049
1050// ============================================================================
1051// Helper functions for parallel analysis (using SemaContext)
1052// ============================================================================
1053
1054impl<'a> Sema<'a> {
1055    /// Check that a preview feature is enabled.
1056    ///
1057    /// This is used to gate experimental features behind the `--preview` flag.
1058    /// Returns an error with a helpful message if the feature is not enabled.
1059    ///
1060    /// # Arguments
1061    /// - `feature`: The preview feature to check
1062    /// - `what`: Human-readable description of what requires this feature
1063    /// - `span`: The source location where the feature is used
1064    ///
1065    /// # Returns
1066    /// - `Ok(())` if the feature is enabled
1067    /// - `Err(CompileError)` with a helpful message if not enabled
1068    pub(crate) fn require_preview(
1069        &self,
1070        feature: PreviewFeature,
1071        what: &str,
1072        span: Span,
1073    ) -> CompileResult<()> {
1074        if self.preview_features.contains(&feature) {
1075            Ok(())
1076        } else {
1077            Err(CompileError::new(
1078                ErrorKind::PreviewFeatureRequired {
1079                    feature,
1080                    what: what.to_string(),
1081                },
1082                span,
1083            )
1084            .with_help(format!(
1085                "use `--preview {}` to enable this feature ({})",
1086                feature.name(),
1087                feature.adr()
1088            )))
1089        }
1090    }
1091
1092    /// Check that we are inside a `checked` block.
1093    /// Returns an error if `checked_depth` is zero.
1094    fn require_checked_for_intrinsic(
1095        ctx: &AnalysisContext,
1096        intrinsic_name: &str,
1097        span: Span,
1098    ) -> CompileResult<()> {
1099        if ctx.checked_depth > 0 {
1100            Ok(())
1101        } else {
1102            Err(CompileError::new(
1103                ErrorKind::IntrinsicRequiresChecked(intrinsic_name.to_string()),
1104                span,
1105            ))
1106        }
1107    }
1108
1109    fn analyze_single_function(
1110        &mut self,
1111        infer_ctx: &InferenceContext,
1112        fn_name: &str,
1113        return_type: Spur,
1114        params: &[gruel_rir::RirParam],
1115        body: InstRef,
1116        span: Span,
1117    ) -> AnalyzedFnResult {
1118        let ret_type = self.resolve_type(return_type, span)?;
1119
1120        // Resolve parameter types and modes
1121        let param_info: Vec<(Spur, Type, RirParamMode)> = params
1122            .iter()
1123            .map(|p| {
1124                let ty = self.resolve_type(p.ty, span)?;
1125                Ok((p.name, ty, p.mode))
1126            })
1127            .collect::<CompileResult<Vec<_>>>()?;
1128
1129        let (
1130            air,
1131            num_locals,
1132            num_param_slots,
1133            param_modes,
1134            param_slot_types,
1135            warnings,
1136            local_strings,
1137            ref_fns,
1138            ref_meths,
1139        ) = self.analyze_function(infer_ctx, ret_type, &param_info, body)?;
1140
1141        Ok((
1142            AnalyzedFunction {
1143                name: fn_name.to_string(),
1144                air,
1145                num_locals,
1146                num_param_slots,
1147                param_modes,
1148                param_slot_types,
1149                is_destructor: false,
1150            },
1151            warnings,
1152            local_strings,
1153            ref_fns,
1154            ref_meths,
1155        ))
1156    }
1157
1158    /// Analyze a method function from an impl block.
1159    ///
1160    /// The `infer_ctx` provides pre-computed type information for constraint generation.
1161    ///
1162    /// Returns the analyzed function, any warnings, and local strings collected during analysis.
1163    fn analyze_method_function(
1164        &mut self,
1165        infer_ctx: &InferenceContext,
1166        full_name: &str,
1167        spec: MethodBodySpec<'_>,
1168        span: Span,
1169    ) -> AnalyzedFnResult {
1170        let ret_type = self.resolve_type(spec.return_type, span)?;
1171
1172        // Build parameter list, adding self as first parameter for methods
1173        let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
1174
1175        if let Some(struct_type) = spec.self_type {
1176            // Add self parameter (Normal mode - passed by value)
1177            let self_sym = self.interner.get_or_intern("self");
1178            param_info.push((self_sym, struct_type, RirParamMode::Normal));
1179        }
1180
1181        // Add regular parameters with their modes
1182        for p in spec.params.iter() {
1183            let ty = self.resolve_type(p.ty, span)?;
1184            param_info.push((p.name, ty, p.mode));
1185        }
1186
1187        let (
1188            air,
1189            num_locals,
1190            num_param_slots,
1191            param_modes,
1192            param_slot_types,
1193            warnings,
1194            local_strings,
1195            ref_fns,
1196            ref_meths,
1197        ) = self.analyze_function(infer_ctx, ret_type, &param_info, spec.body)?;
1198
1199        Ok((
1200            AnalyzedFunction {
1201                name: full_name.to_string(),
1202                air,
1203                num_locals,
1204                num_param_slots,
1205                param_modes,
1206                param_slot_types,
1207                is_destructor: false,
1208            },
1209            warnings,
1210            local_strings,
1211            ref_fns,
1212            ref_meths,
1213        ))
1214    }
1215
1216    /// Analyze a destructor function.
1217    ///
1218    /// The `infer_ctx` provides pre-computed type information for constraint generation.
1219    ///
1220    /// Returns the analyzed function, any warnings, and local strings collected during analysis.
1221    fn analyze_destructor_function(
1222        &mut self,
1223        infer_ctx: &InferenceContext,
1224        full_name: &str,
1225        body: InstRef,
1226        _span: Span,
1227        struct_type: Type,
1228    ) -> AnalyzedFnResult {
1229        // Destructors take self parameter and return unit
1230        let self_sym = self.interner.get_or_intern("self");
1231        let param_info: Vec<(Spur, Type, RirParamMode)> =
1232            vec![(self_sym, struct_type, RirParamMode::Normal)];
1233
1234        let (
1235            air,
1236            num_locals,
1237            num_param_slots,
1238            param_modes,
1239            param_slot_types,
1240            warnings,
1241            local_strings,
1242            ref_fns,
1243            ref_meths,
1244        ) = self.analyze_function(infer_ctx, Type::UNIT, &param_info, body)?;
1245
1246        Ok((
1247            AnalyzedFunction {
1248                name: full_name.to_string(),
1249                air,
1250                num_locals,
1251                num_param_slots,
1252                param_modes,
1253                param_slot_types,
1254                is_destructor: true,
1255            },
1256            warnings,
1257            local_strings,
1258            ref_fns,
1259            ref_meths,
1260        ))
1261    }
1262    /// Analyze a single function, producing AIR.
1263    ///
1264    /// The `infer_ctx` provides pre-computed type information for constraint generation,
1265    /// avoiding the cost of rebuilding maps for each function.
1266    ///
1267    /// Returns (air, num_locals, num_param_slots, param_modes, warnings).
1268    /// Warnings are collected per-function to enable future parallel analysis.
1269    fn analyze_function(
1270        &mut self,
1271        infer_ctx: &InferenceContext,
1272        return_type: Type,
1273        params: &[(Spur, Type, RirParamMode)], // (name, type, mode)
1274        body: InstRef,
1275    ) -> RawFnAnalysis {
1276        self.analyze_function_internal(infer_ctx, return_type, params, body, None, None)
1277    }
1278
1279    /// Internal function analysis with optional type substitutions.
1280    ///
1281    /// When `type_subst` is provided (for specialized generic functions), it populates
1282    /// `comptime_type_vars` so that type parameters can be resolved in struct initialization
1283    /// (e.g., `P { x: 1, y: 2 }` where `P` is a type parameter).
1284    fn analyze_function_internal(
1285        &mut self,
1286        infer_ctx: &InferenceContext,
1287        return_type: Type,
1288        params: &[(Spur, Type, RirParamMode)],
1289        body: InstRef,
1290        type_subst: Option<&std::collections::HashMap<Spur, Type>>,
1291        value_subst: Option<&std::collections::HashMap<Spur, ConstValue>>,
1292    ) -> RawFnAnalysis {
1293        let mut air = Air::new(return_type);
1294        let mut param_vec: Vec<ParamInfo> = Vec::new();
1295        let mut param_modes: Vec<bool> = Vec::new();
1296        let mut param_slot_types: Vec<Type> = Vec::new();
1297
1298        // Add parameters to the param vec, tracking ABI slot offsets.
1299        // Each parameter starts at the next available ABI slot.
1300        // For struct parameters, the slot count is the number of fields.
1301        let mut next_abi_slot: u32 = 0;
1302        for (pname, ptype, mode) in params.iter() {
1303            param_vec.push(ParamInfo {
1304                name: *pname,
1305                abi_slot: next_abi_slot,
1306                ty: *ptype,
1307                mode: *mode,
1308            });
1309            // Inout and Borrow parameters are passed by reference.
1310            // Comptime parameters are VALUE params (like `comptime n: i32`), passed by value.
1311            // Normal parameters are passed by value.
1312            let is_by_ref = *mode == RirParamMode::Inout || *mode == RirParamMode::Borrow;
1313            let slot_count = if is_by_ref {
1314                // By-ref parameters are always 1 slot (pointer)
1315                1
1316            } else {
1317                self.abi_slot_count(*ptype)
1318            };
1319            for _ in 0..slot_count {
1320                param_modes.push(is_by_ref);
1321                param_slot_types.push(*ptype);
1322            }
1323            next_abi_slot += slot_count;
1324        }
1325        let num_param_slots = next_abi_slot;
1326
1327        // ======================================================================
1328        // Phase 1-2: Hindley-Milner Type Inference
1329        // ======================================================================
1330        // Run constraint generation and unification to determine types
1331        // for all expressions BEFORE emitting AIR.
1332        let resolved_types = self.run_type_inference(
1333            infer_ctx,
1334            return_type,
1335            params,
1336            body,
1337            type_subst,
1338            value_subst,
1339        )?;
1340
1341        // Create analysis context with resolved types
1342        // If type_subst is provided, initialize comptime_type_vars with the substitutions
1343        // so that type parameters can be resolved during struct initialization.
1344        let comptime_type_vars = type_subst.cloned().unwrap_or_default();
1345        let comptime_value_vars = value_subst.cloned().unwrap_or_default();
1346        let mut ctx = AnalysisContext {
1347            locals: HashMap::new(),
1348            params: &param_vec,
1349            next_slot: 0,
1350            loop_depth: 0,
1351            forbid_break: None,
1352            checked_depth: 0,
1353            used_locals: HashSet::new(),
1354            return_type,
1355            scope_stack: Vec::new(),
1356            resolved_types: &resolved_types,
1357            moved_vars: HashMap::new(),
1358            warnings: Vec::new(),
1359            local_string_table: HashMap::new(),
1360            local_strings: Vec::new(),
1361            comptime_type_vars,
1362            comptime_value_vars,
1363            referenced_functions: HashSet::new(),
1364            referenced_methods: HashSet::new(),
1365        };
1366
1367        // ======================================================================
1368        // Phase 3: AIR Emission
1369        // ======================================================================
1370        // Analyze the body expression, emitting AIR with resolved types
1371        let body_result = self.analyze_inst(&mut air, body, &mut ctx)?;
1372
1373        // Add implicit return only if body doesn't already diverge (e.g., explicit return)
1374        if body_result.ty != Type::NEVER {
1375            air.add_inst(AirInst {
1376                data: AirInstData::Ret(Some(body_result.air_ref)),
1377                ty: return_type,
1378                span: self.rir.get(body).span,
1379            });
1380        }
1381
1382        Ok((
1383            air,
1384            ctx.next_slot,
1385            num_param_slots,
1386            param_modes,
1387            param_slot_types,
1388            ctx.warnings,
1389            ctx.local_strings,
1390            ctx.referenced_functions,
1391            ctx.referenced_methods,
1392        ))
1393    }
1394
1395    /// Analyze a specialized function body.
1396    ///
1397    /// This is similar to `analyze_function` but for generic function specialization.
1398    /// The `type_subst` map provides substitutions for type parameters to their
1399    /// concrete types.
1400    ///
1401    /// For example, when specializing `fn identity<T>(x: T) -> T { x }` with `T = i32`,
1402    /// the `params` will be `[(x, i32, Normal)]` and `return_type` will be `i32`.
1403    pub fn analyze_specialized_function(
1404        &mut self,
1405        infer_ctx: &InferenceContext,
1406        return_type: Type,
1407        params: &[(Spur, Type, RirParamMode)],
1408        body: InstRef,
1409        type_subst: &std::collections::HashMap<Spur, Type>,
1410    ) -> RawFnAnalysis {
1411        // For specialized functions, we need to populate comptime_type_vars with the
1412        // type substitutions so that references to type parameters (like `P { ... }`)
1413        // can be resolved in the function body.
1414        self.analyze_function_internal(infer_ctx, return_type, params, body, Some(type_subst), None)
1415    }
1416
1417    /// Analyze a method body with `Self` type resolution.
1418    ///
1419    /// This is used for anonymous struct methods where `Self` should resolve to the
1420    /// struct type. The `self_type` is added to the type substitution map under the
1421    /// symbol "Self", allowing `Self { ... }` struct literals to work correctly.
1422    fn analyze_method_body(
1423        &mut self,
1424        infer_ctx: &InferenceContext,
1425        return_type: Type,
1426        params: &[(Spur, Type, RirParamMode)],
1427        body: InstRef,
1428        self_type: Type,
1429        captured_comptime_values: &std::collections::HashMap<Spur, ConstValue>,
1430    ) -> RawFnAnalysis {
1431        // Create a type substitution map with Self -> the struct type
1432        let self_sym = self.interner.get_or_intern("Self");
1433        let mut type_subst = HashMap::new();
1434        type_subst.insert(self_sym, self_type);
1435
1436        self.analyze_function_internal(
1437            infer_ctx,
1438            return_type,
1439            params,
1440            body,
1441            Some(&type_subst),
1442            Some(captured_comptime_values),
1443        )
1444    }
1445
1446    /// Run Hindley-Milner type inference on a function body.
1447    ///
1448    /// This is Phases 1-2 of the HM algorithm:
1449    /// 1. Generate constraints by walking the RIR
1450    /// 2. Solve constraints via unification
1451    ///
1452    /// The `infer_ctx` parameter provides pre-computed type information (function
1453    /// signatures, struct/enum types, method signatures) converted to InferType format.
1454    /// This avoids rebuilding these maps for each function, reducing O(n²) to O(n).
1455    ///
1456    /// Returns a map from RIR instruction refs to their resolved concrete types.
1457    fn run_type_inference(
1458        &mut self,
1459        infer_ctx: &InferenceContext,
1460        return_type: Type,
1461        params: &[(Spur, Type, RirParamMode)],
1462        body: InstRef,
1463        type_subst: Option<&HashMap<Spur, Type>>,
1464        value_subst: Option<&HashMap<Spur, ConstValue>>,
1465    ) -> CompileResult<HashMap<InstRef, Type>> {
1466        // Create constraint generator using pre-computed inference context
1467        let mut cgen = ConstraintGenerator::new(
1468            self.rir,
1469            self.interner,
1470            &infer_ctx.func_sigs,
1471            &infer_ctx.struct_types,
1472            &infer_ctx.enum_types,
1473            &infer_ctx.method_sigs,
1474            &infer_ctx.enum_method_sigs,
1475            &self.type_pool,
1476        )
1477        .with_type_subst(type_subst);
1478
1479        // Build parameter map for constraint context.
1480        // Convert Type to InferType so arrays are represented structurally.
1481        let mut param_vars: HashMap<Spur, ParamVarInfo> = params
1482            .iter()
1483            .map(|(name, ty, _mode)| {
1484                (
1485                    *name,
1486                    ParamVarInfo {
1487                        ty: self.type_to_infer_type(*ty),
1488                    },
1489                )
1490            })
1491            .collect();
1492
1493        // Add comptime value variables as if they were parameters
1494        // This allows constraint generation to see captured comptime values
1495        if let Some(values) = value_subst {
1496            for (name, const_val) in values {
1497                let ty = match const_val {
1498                    ConstValue::Integer(_) => Type::COMPTIME_INT,
1499                    ConstValue::Bool(_) => Type::BOOL,
1500                    ConstValue::Type(t) => *t,
1501                    ConstValue::Unit => Type::UNIT,
1502                    ConstValue::ComptimeStr(_) => Type::COMPTIME_STR,
1503                    ConstValue::Struct(_)
1504                    | ConstValue::Array(_)
1505                    | ConstValue::EnumVariant { .. }
1506                    | ConstValue::EnumData { .. }
1507                    | ConstValue::EnumStruct { .. }
1508                    | ConstValue::BreakSignal
1509                    | ConstValue::ContinueSignal
1510                    | ConstValue::ReturnSignal => {
1511                        unreachable!(
1512                            "control-flow signal or composite value in comptime_value_vars"
1513                        )
1514                    }
1515                };
1516                param_vars.insert(
1517                    *name,
1518                    ParamVarInfo {
1519                        ty: self.type_to_infer_type(ty),
1520                    },
1521                );
1522            }
1523        }
1524
1525        // Create constraint context
1526        let mut cgen_ctx = ConstraintContext::new(&param_vars, return_type);
1527
1528        // Phase 1: Generate constraints
1529        let body_info = cgen.generate(body, &mut cgen_ctx);
1530
1531        // The function body's type must match the return type.
1532        // This handles implicit returns like `fn foo() -> i8 { 42 }`.
1533        // For arrays, we need to convert Type to InferType structurally.
1534        cgen.add_constraint(Constraint::equal(
1535            body_info.ty,
1536            self.type_to_infer_type(return_type),
1537            body_info.span,
1538        ));
1539
1540        // Consume the constraint generator to release borrows
1541        let (constraints, int_literal_vars, float_literal_vars, expr_types, type_var_count) =
1542            cgen.into_parts();
1543
1544        // Phase 2: Solve constraints via unification
1545        // Pre-size the substitution for better performance on large functions
1546        let mut unifier = Unifier::with_capacity(type_var_count);
1547        let errors = unifier.solve_constraints(&constraints);
1548
1549        // Convert unification errors to compile errors
1550        // For now, we collect the first error. In the future, we could
1551        // report multiple errors for better diagnostics.
1552        if let Some(err) = errors.first() {
1553            // Map each UnifyResult variant to the appropriate ErrorKind
1554            let error_kind = match &err.kind {
1555                UnifyResult::Ok => unreachable!("UnificationError should never contain Ok"),
1556                UnifyResult::TypeMismatch { expected, found } => ErrorKind::TypeMismatch {
1557                    expected: expected.to_string(),
1558                    found: found.to_string(),
1559                },
1560                UnifyResult::IntLiteralNonInteger { found } => ErrorKind::TypeMismatch {
1561                    expected: "integer type".to_string(),
1562                    found: found.name().to_string(),
1563                },
1564                UnifyResult::OccursCheck { var, ty } => ErrorKind::TypeMismatch {
1565                    expected: "non-recursive type".to_string(),
1566                    found: format!("{var} = {ty} (infinite type)"),
1567                },
1568                UnifyResult::NotSigned { ty } => {
1569                    ErrorKind::CannotNegateUnsigned(ty.name().to_string())
1570                }
1571                UnifyResult::NotInteger { ty } => ErrorKind::TypeMismatch {
1572                    expected: "integer type".to_string(),
1573                    found: ty.name().to_string(),
1574                },
1575                UnifyResult::NotUnsigned { ty } => ErrorKind::TypeMismatch {
1576                    expected: "unsigned integer type".to_string(),
1577                    found: ty.name().to_string(),
1578                },
1579                UnifyResult::NotNumeric { ty } => ErrorKind::TypeMismatch {
1580                    expected: "numeric type".to_string(),
1581                    found: ty.name().to_string(),
1582                },
1583                UnifyResult::ArrayLengthMismatch { expected, found } => {
1584                    ErrorKind::ArrayLengthMismatch {
1585                        expected: *expected,
1586                        found: *found,
1587                    }
1588                }
1589            };
1590
1591            let mut compile_error = CompileError::new(error_kind, err.span);
1592
1593            // Add note for unsigned negation errors
1594            if matches!(err.kind, UnifyResult::NotSigned { .. }) {
1595                compile_error = compile_error.with_note("unsigned values cannot be negated");
1596            }
1597
1598            return Err(compile_error);
1599        }
1600
1601        // Default any unconstrained integer literals to i32 and float literals to f64
1602        unifier.default_int_literal_vars(&int_literal_vars);
1603        unifier.default_float_literal_vars(&float_literal_vars);
1604
1605        // Pre-collect all array types from resolved InferTypes before converting them.
1606        // This ensures all array types are created before the conversion loop, which
1607        // enables parallelization of function analysis (mutation happens here, not in
1608        // infer_type_to_type).
1609        for infer_ty in expr_types.values() {
1610            let resolved = unifier.resolve_infer_type(infer_ty);
1611            self.pre_create_array_types_from_infer_type(&resolved);
1612        }
1613
1614        // Build the resolved types map, converting InferType to Type.
1615        // Since we pre-created all array types above, infer_type_to_type only
1616        // performs lookups (no mutation).
1617        let mut resolved_types = HashMap::new();
1618        for (inst_ref, infer_ty) in &expr_types {
1619            let resolved = unifier.resolve_infer_type(infer_ty);
1620            let concrete_ty = self.infer_type_to_type(&resolved);
1621            resolved_types.insert(*inst_ref, concrete_ty);
1622        }
1623
1624        Ok(resolved_types)
1625    }
1626    /// Analyze an RIR instruction for projection (field access).
1627    ///
1628    /// This is like `analyze_inst` but does NOT mark non-Copy values as moved.
1629    /// Used for field access where we're reading from a struct without consuming it.
1630    /// We still check that the variable hasn't already been moved (fully moved).
1631    /// Field-level move checking is done at the FieldGet level, not here.
1632    pub(crate) fn analyze_inst_for_projection(
1633        &mut self,
1634        air: &mut Air,
1635        inst_ref: InstRef,
1636        ctx: &mut AnalysisContext,
1637    ) -> CompileResult<AnalysisResult> {
1638        let inst = self.rir.get(inst_ref);
1639
1640        // For VarRef, we handle it specially: check for full moves but don't mark as moved
1641        if let InstData::VarRef { name } = &inst.data {
1642            // First check if it's a parameter
1643            if let Some(param_info) = ctx.params.iter().find(|p| p.name == *name) {
1644                let ty = param_info.ty;
1645
1646                // Check if this parameter has been fully moved
1647                // (Partial moves are checked at the FieldGet level)
1648                if let Some(move_state) = ctx.moved_vars.get(name)
1649                    && let Some(moved_span) = move_state.full_move
1650                {
1651                    let name_str = self.interner.resolve(name);
1652                    return Err(CompileError::new(
1653                        ErrorKind::UseAfterMove(name_str.to_string()),
1654                        inst.span,
1655                    )
1656                    .with_label("value moved here", moved_span));
1657                }
1658
1659                // NOTE: We do NOT mark as moved here - this is a projection
1660
1661                let air_ref = air.add_inst(AirInst {
1662                    data: AirInstData::Param {
1663                        index: param_info.abi_slot,
1664                    },
1665                    ty,
1666                    span: inst.span,
1667                });
1668                return Ok(AnalysisResult::new(air_ref, ty));
1669            }
1670
1671            // Check if it's a local variable
1672            if let Some(local) = ctx.locals.get(name) {
1673                let ty = local.ty;
1674                let slot = local.slot;
1675
1676                // Check if this variable has been fully moved
1677                // (Partial moves are checked at the FieldGet level)
1678                if let Some(move_state) = ctx.moved_vars.get(name)
1679                    && let Some(moved_span) = move_state.full_move
1680                {
1681                    let name_str = self.interner.resolve(name);
1682                    return Err(CompileError::new(
1683                        ErrorKind::UseAfterMove(name_str.to_string()),
1684                        inst.span,
1685                    )
1686                    .with_label("value moved here", moved_span));
1687                }
1688
1689                // NOTE: We do NOT mark as moved here - this is a projection
1690
1691                // Mark variable as used
1692                ctx.used_locals.insert(*name);
1693
1694                // Load the variable
1695                let air_ref = air.add_inst(AirInst {
1696                    data: AirInstData::Load { slot },
1697                    ty,
1698                    span: inst.span,
1699                });
1700                return Ok(AnalysisResult::new(air_ref, ty));
1701            }
1702
1703            // Check if it's a comptime type variable (e.g., `let P = Point();`)
1704            if let Some(&ty) = ctx.comptime_type_vars.get(name) {
1705                let air_ref = air.add_inst(AirInst {
1706                    data: AirInstData::TypeConst(ty),
1707                    ty: Type::COMPTIME_TYPE,
1708                    span: inst.span,
1709                });
1710                return Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE));
1711            }
1712
1713            // Check if it's a comptime value variable (e.g., captured `comptime N: i32`)
1714            if let Some(const_value) = ctx.comptime_value_vars.get(name) {
1715                match const_value {
1716                    ConstValue::Integer(val) => {
1717                        let ty = Self::get_resolved_type(
1718                            ctx,
1719                            inst_ref,
1720                            inst.span,
1721                            "comptime integer value",
1722                        )?;
1723                        let air_ref = air.add_inst(AirInst {
1724                            data: AirInstData::Const(*val as u64),
1725                            ty,
1726                            span: inst.span,
1727                        });
1728                        return Ok(AnalysisResult::new(air_ref, ty));
1729                    }
1730                    ConstValue::Bool(val) => {
1731                        let air_ref = air.add_inst(AirInst {
1732                            data: AirInstData::Const(*val as u64),
1733                            ty: Type::BOOL,
1734                            span: inst.span,
1735                        });
1736                        return Ok(AnalysisResult::new(air_ref, Type::BOOL));
1737                    }
1738                    ConstValue::Type(ty) => {
1739                        let air_ref = air.add_inst(AirInst {
1740                            data: AirInstData::TypeConst(*ty),
1741                            ty: Type::COMPTIME_TYPE,
1742                            span: inst.span,
1743                        });
1744                        return Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE));
1745                    }
1746                    ConstValue::ComptimeStr(_)
1747                    | ConstValue::Struct(_)
1748                    | ConstValue::Array(_)
1749                    | ConstValue::EnumVariant { .. }
1750                    | ConstValue::EnumData { .. }
1751                    | ConstValue::EnumStruct { .. } => {
1752                        return Err(CompileError::new(
1753                            ErrorKind::ComptimeEvaluationFailed {
1754                                reason: "comptime composite values cannot be used in runtime expressions; use @field to access fields".to_string(),
1755                            },
1756                            inst.span,
1757                        ));
1758                    }
1759                    ConstValue::Unit => {
1760                        return Err(CompileError::new(
1761                            ErrorKind::ComptimeEvaluationFailed {
1762                                reason:
1763                                    "comptime unit values cannot be used in runtime expressions"
1764                                        .to_string(),
1765                            },
1766                            inst.span,
1767                        ));
1768                    }
1769                    ConstValue::BreakSignal
1770                    | ConstValue::ContinueSignal
1771                    | ConstValue::ReturnSignal => {
1772                        unreachable!("control-flow signal in comptime_value_vars")
1773                    }
1774                }
1775            }
1776
1777            // Not found
1778            let name_str = self.interner.resolve(name);
1779            return Err(CompileError::new(
1780                ErrorKind::UndefinedVariable(name_str.to_string()),
1781                inst.span,
1782            ));
1783        }
1784
1785        // For nested field access (e.g., a.b.c), recursively use projection mode
1786        if let InstData::FieldGet { base, field } = &inst.data {
1787            let base_result = self.analyze_inst_for_projection(air, *base, ctx)?;
1788            let base_type = base_result.ty;
1789
1790            let struct_id = match base_type.kind() {
1791                TypeKind::Struct(id) => id,
1792                _ => {
1793                    return Err(CompileError::new(
1794                        ErrorKind::FieldAccessOnNonStruct {
1795                            found: base_type.name().to_string(),
1796                        },
1797                        inst.span,
1798                    ));
1799                }
1800            };
1801
1802            let struct_def = self.type_pool.struct_def(struct_id);
1803            let field_name_str = self.interner.resolve(field).to_string();
1804
1805            let (field_index, struct_field) =
1806                struct_def.find_field(&field_name_str).ok_or_compile_error(
1807                    ErrorKind::UnknownField {
1808                        struct_name: struct_def.name.clone(),
1809                        field_name: field_name_str.clone(),
1810                    },
1811                    inst.span,
1812                )?;
1813
1814            let field_type = struct_field.ty;
1815
1816            let air_ref = air.add_inst(AirInst {
1817                data: AirInstData::FieldGet {
1818                    base: base_result.air_ref,
1819                    struct_id,
1820                    field_index: field_index as u32,
1821                },
1822                ty: field_type,
1823                span: inst.span,
1824            });
1825            return Ok(AnalysisResult::new(air_ref, field_type));
1826        }
1827
1828        // For index access in projection mode (e.g., `arr[i].field`), we allow the
1829        // indexing without checking if the element type is Copy. This enables
1830        // accessing Copy fields of non-Copy array elements.
1831        if let InstData::IndexGet { base, index } = &inst.data {
1832            // Recursively analyze the base in projection mode
1833            let base_result = self.analyze_inst_for_projection(air, *base, ctx)?;
1834            let base_type = base_result.ty;
1835
1836            let array_type_id = match base_type.kind() {
1837                TypeKind::Array(id) => id,
1838                _ => {
1839                    return Err(CompileError::new(
1840                        ErrorKind::IndexOnNonArray {
1841                            found: base_type.name().to_string(),
1842                        },
1843                        inst.span,
1844                    ));
1845                }
1846            };
1847
1848            let (element_type, length) = self.type_pool.array_def(array_type_id);
1849
1850            // Index must be an unsigned integer
1851            let index_result = self.analyze_inst(air, *index, ctx)?;
1852            if !index_result.ty.is_unsigned() && !index_result.ty.is_error() {
1853                return Err(CompileError::new(
1854                    ErrorKind::TypeMismatch {
1855                        expected: "unsigned integer type".to_string(),
1856                        found: index_result.ty.name().to_string(),
1857                    },
1858                    self.rir.get(*index).span,
1859                ));
1860            }
1861
1862            let array_length = length;
1863
1864            // Compile-time bounds check for constant indices
1865            if let Some(const_index) = self.try_get_const_index(*index)
1866                && (const_index < 0 || const_index as u64 >= array_length)
1867            {
1868                return Err(CompileError::new(
1869                    ErrorKind::IndexOutOfBounds {
1870                        index: const_index,
1871                        length: array_length,
1872                    },
1873                    self.rir.get(*index).span,
1874                ));
1875            }
1876
1877            // NOTE: We do NOT check if element_type is Copy here.
1878            // In projection mode, we allow accessing elements for further projection
1879            // (e.g., arr[i].field where field is Copy).
1880
1881            let air_ref = air.add_inst(AirInst {
1882                data: AirInstData::IndexGet {
1883                    base: base_result.air_ref,
1884                    array_type: base_type,
1885                    index: index_result.air_ref,
1886                },
1887                ty: element_type,
1888                span: inst.span,
1889            });
1890            return Ok(AnalysisResult::new(air_ref, element_type));
1891        }
1892
1893        // For other expressions, use the normal analyze_inst
1894        // (they will trigger move semantics as expected)
1895        self.analyze_inst(air, inst_ref, ctx)
1896    }
1897
1898    /// Look up the resolved type for an instruction from HM inference.
1899    ///
1900    /// Returns an `InternalError` if the type was not resolved. This should
1901    /// never happen in normal operation, but provides a better error message
1902    /// than a panic if there's a bug in type inference.
1903    pub(crate) fn get_resolved_type(
1904        ctx: &AnalysisContext,
1905        inst_ref: InstRef,
1906        span: Span,
1907        context: &str,
1908    ) -> CompileResult<Type> {
1909        ctx.resolved_types.get(&inst_ref).copied().ok_or_else(|| {
1910            CompileError::new(
1911                ErrorKind::InternalError(format!(
1912                    "type inference did not resolve type for {} (instruction {:?})",
1913                    context, inst_ref
1914                )),
1915                span,
1916            )
1917        })
1918    }
1919
1920    /// Analyze an RIR instruction, producing AIR instructions.
1921    ///
1922    /// Types are determined by Hindley-Milner inference (stored in `resolved_types`).
1923    /// Returns both the AIR reference and the synthesized type.
1924    /// Analyze a single RIR instruction and produce the corresponding AIR instruction.
1925    ///
1926    /// This method dispatches to category-specific methods in `analyze_ops.rs` for
1927    /// maintainability. Each category handles related instruction types together.
1928    ///
1929    /// # Categories
1930    ///
1931    /// - **Literals**: IntConst, BoolConst, StringConst, UnitConst
1932    /// - **Binary arithmetic**: Add, Sub, Mul, Div, Mod, BitAnd, BitOr, BitXor, Shl, Shr
1933    /// - **Comparison**: Eq, Ne, Lt, Gt, Le, Ge
1934    /// - **Logical**: And, Or
1935    /// - **Unary**: Neg, Not, BitNot
1936    /// - **Control flow**: Branch, Loop, InfiniteLoop, Match, Break, Continue, Ret, Block
1937    /// - **Variables**: Alloc, VarRef, ParamRef, Assign
1938    /// - **Structs**: StructDecl, StructInit, FieldGet, FieldSet
1939    /// - **Arrays**: ArrayInit, IndexGet, IndexSet
1940    /// - **Enums**: EnumDecl, EnumVariant
1941    /// - **Calls**: Call, MethodCall, AssocFnCall
1942    /// - **Intrinsics**: Intrinsic, TypeIntrinsic
1943    /// - **Declarations**: DropFnDecl, FnDecl
1944    pub(crate) fn analyze_inst(
1945        &mut self,
1946        air: &mut Air,
1947        inst_ref: InstRef,
1948        ctx: &mut AnalysisContext,
1949    ) -> CompileResult<AnalysisResult> {
1950        let inst = self.rir.get(inst_ref);
1951
1952        match &inst.data {
1953            // Literals
1954            InstData::IntConst(_)
1955            | InstData::FloatConst(_)
1956            | InstData::BoolConst(_)
1957            | InstData::StringConst(_)
1958            | InstData::UnitConst => self.analyze_literal(air, inst_ref, ctx),
1959
1960            // Binary arithmetic operations
1961            InstData::Add { lhs, rhs } => {
1962                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::Add, inst.span, ctx)
1963            }
1964            InstData::Sub { lhs, rhs } => {
1965                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::Sub, inst.span, ctx)
1966            }
1967            InstData::Mul { lhs, rhs } => {
1968                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::Mul, inst.span, ctx)
1969            }
1970            InstData::Div { lhs, rhs } => {
1971                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::Div, inst.span, ctx)
1972            }
1973            InstData::Mod { lhs, rhs } => {
1974                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::Mod, inst.span, ctx)
1975            }
1976
1977            // Bitwise binary operations
1978            InstData::BitAnd { lhs, rhs } => {
1979                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::BitAnd, inst.span, ctx)
1980            }
1981            InstData::BitOr { lhs, rhs } => {
1982                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::BitOr, inst.span, ctx)
1983            }
1984            InstData::BitXor { lhs, rhs } => {
1985                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::BitXor, inst.span, ctx)
1986            }
1987            InstData::Shl { lhs, rhs } => {
1988                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::Shl, inst.span, ctx)
1989            }
1990            InstData::Shr { lhs, rhs } => {
1991                self.analyze_binary_arith(air, *lhs, *rhs, AirInstData::Shr, inst.span, ctx)
1992            }
1993
1994            // Comparison operations
1995            InstData::Eq { lhs, rhs } => {
1996                self.analyze_comparison(air, (*lhs, *rhs), true, AirInstData::Eq, inst.span, ctx)
1997            }
1998            InstData::Ne { lhs, rhs } => {
1999                self.analyze_comparison(air, (*lhs, *rhs), true, AirInstData::Ne, inst.span, ctx)
2000            }
2001            InstData::Lt { lhs, rhs } => {
2002                self.analyze_comparison(air, (*lhs, *rhs), false, AirInstData::Lt, inst.span, ctx)
2003            }
2004            InstData::Gt { lhs, rhs } => {
2005                self.analyze_comparison(air, (*lhs, *rhs), false, AirInstData::Gt, inst.span, ctx)
2006            }
2007            InstData::Le { lhs, rhs } => {
2008                self.analyze_comparison(air, (*lhs, *rhs), false, AirInstData::Le, inst.span, ctx)
2009            }
2010            InstData::Ge { lhs, rhs } => {
2011                self.analyze_comparison(air, (*lhs, *rhs), false, AirInstData::Ge, inst.span, ctx)
2012            }
2013
2014            // Logical operations
2015            InstData::And { .. } | InstData::Or { .. } => {
2016                self.analyze_logical_op(air, inst_ref, ctx)
2017            }
2018
2019            // Unary operations
2020            InstData::Neg { .. } | InstData::Not { .. } | InstData::BitNot { .. } => {
2021                self.analyze_unary_op(air, inst_ref, ctx)
2022            }
2023
2024            // Control flow
2025            InstData::Branch { .. }
2026            | InstData::Loop { .. }
2027            | InstData::For { .. }
2028            | InstData::InfiniteLoop { .. }
2029            | InstData::Match { .. }
2030            | InstData::Break
2031            | InstData::Continue
2032            | InstData::Ret(_)
2033            | InstData::Block { .. } => self.analyze_control_flow(air, inst_ref, ctx),
2034
2035            // Variable operations
2036            InstData::Alloc { .. }
2037            | InstData::StructDestructure { .. }
2038            | InstData::VarRef { .. }
2039            | InstData::ParamRef { .. }
2040            | InstData::Assign { .. } => self.analyze_variable_ops(air, inst_ref, ctx),
2041
2042            // Struct operations
2043            InstData::StructDecl { .. }
2044            | InstData::StructInit { .. }
2045            | InstData::FieldGet { .. }
2046            | InstData::FieldSet { .. } => self.analyze_struct_ops(air, inst_ref, ctx),
2047
2048            // Array operations
2049            InstData::ArrayInit { .. } | InstData::IndexGet { .. } | InstData::IndexSet { .. } => {
2050                self.analyze_array_ops(air, inst_ref, ctx)
2051            }
2052
2053            // Enum operations
2054            InstData::EnumDecl { .. }
2055            | InstData::EnumVariant { .. }
2056            | InstData::EnumStructVariant { .. } => self.analyze_enum_ops(air, inst_ref, ctx),
2057
2058            // Call operations
2059            InstData::Call { .. } | InstData::MethodCall { .. } | InstData::AssocFnCall { .. } => {
2060                self.analyze_call_ops(air, inst_ref, ctx)
2061            }
2062
2063            // Intrinsic operations
2064            InstData::Intrinsic { .. } | InstData::TypeIntrinsic { .. } => {
2065                self.analyze_intrinsic_ops(air, inst_ref, ctx)
2066            }
2067
2068            // Declaration no-ops (produce Unit in expression context)
2069            InstData::DropFnDecl { .. } | InstData::FnDecl { .. } | InstData::ConstDecl { .. } => {
2070                self.analyze_decl_noop(air, inst_ref, ctx)
2071            }
2072
2073            // Comptime block expression
2074            InstData::Comptime { expr } => {
2075                let span = inst.span;
2076                let expr = *expr;
2077                // Use the stateful comptime interpreter (Phase 1a).
2078                // This supports mutable let bindings, if/else, and blocks
2079                // in addition to pure arithmetic expressions.
2080                match self.evaluate_comptime_block(expr, ctx, span)? {
2081                    ConstValue::Integer(value) => {
2082                        let ty =
2083                            Self::get_resolved_type(ctx, inst_ref, span, "comptime block")?;
2084                        if value < 0 {
2085                            return Err(CompileError::new(
2086                                ErrorKind::ComptimeEvaluationFailed {
2087                                    reason: "negative values not yet supported in comptime"
2088                                        .to_string(),
2089                                },
2090                                span,
2091                            ));
2092                        }
2093                        let unsigned_value = value as u64;
2094                        if !ty.literal_fits(unsigned_value) {
2095                            return Err(CompileError::new(
2096                                ErrorKind::LiteralOutOfRange {
2097                                    value: unsigned_value,
2098                                    ty: ty.name().to_string(),
2099                                },
2100                                span,
2101                            ));
2102                        }
2103                        let air_ref = air.add_inst(AirInst {
2104                            data: AirInstData::Const(unsigned_value),
2105                            ty,
2106                            span,
2107                        });
2108                        Ok(AnalysisResult::new(air_ref, ty))
2109                    }
2110                    ConstValue::Bool(value) => {
2111                        let ty = Type::BOOL;
2112                        let air_ref = air.add_inst(AirInst {
2113                            data: AirInstData::BoolConst(value),
2114                            ty,
2115                            span,
2116                        });
2117                        Ok(AnalysisResult::new(air_ref, ty))
2118                    }
2119                    ConstValue::Type(_) => Err(CompileError::new(
2120                        ErrorKind::ComptimeEvaluationFailed {
2121                            reason: "type values cannot exist at runtime".to_string(),
2122                        },
2123                        span,
2124                    )),
2125                    ConstValue::ComptimeStr(str_idx) => {
2126                        // Materialize comptime string as a runtime String constant.
2127                        let content =
2128                            self.resolve_comptime_str(str_idx, span)?.to_string();
2129                        let ty = self.builtin_string_type();
2130                        let local_string_id = ctx.add_local_string(content);
2131                        let air_ref = air.add_inst(AirInst {
2132                            data: AirInstData::StringConst(local_string_id),
2133                            ty,
2134                            span,
2135                        });
2136                        Ok(AnalysisResult::new(air_ref, ty))
2137                    }
2138                    ConstValue::Unit => {
2139                        let air_ref = air.add_inst(AirInst {
2140                            data: AirInstData::UnitConst,
2141                            ty: Type::UNIT,
2142                            span,
2143                        });
2144                        Ok(AnalysisResult::new(air_ref, Type::UNIT))
2145                    }
2146                    // Composite comptime values (structs, arrays, enums) cannot be placed at
2147                    // runtime directly. The user must access individual fields/elements.
2148                    ConstValue::Struct(_)
2149                    | ConstValue::Array(_)
2150                    | ConstValue::EnumVariant { .. }
2151                    | ConstValue::EnumData { .. }
2152                    | ConstValue::EnumStruct { .. } => {
2153                        Err(CompileError::new(
2154                            ErrorKind::ComptimeEvaluationFailed {
2155                                reason: "comptime composite values cannot be used at runtime; access individual fields or elements instead".into(),
2156                            },
2157                            span,
2158                        ))
2159                    }
2160                    // These signals are consumed by loop/call handlers inside evaluate_comptime_block.
2161                    // If they escape here, it means break/continue outside a loop, or return outside
2162                    // a function, which evaluate_comptime_block converts to an error before returning.
2163                    ConstValue::BreakSignal
2164                    | ConstValue::ContinueSignal
2165                    | ConstValue::ReturnSignal => {
2166                        unreachable!("control-flow signal escaped evaluate_comptime_block")
2167                    }
2168                }
2169            }
2170
2171            // Comptime unroll for: evaluate iterable at comptime, unroll body N times
2172            InstData::ComptimeUnrollFor {
2173                binding,
2174                iterable,
2175                body,
2176            } => {
2177                let span = inst.span;
2178                let binding = *binding;
2179                let iterable = *iterable;
2180                let body = *body;
2181
2182                // Step 1: Evaluate the iterable expression at comptime.
2183                // We use evaluate_comptime_block which clears and rebuilds the heap.
2184                let iterable_val = self.evaluate_comptime_block(iterable, ctx, span)?;
2185
2186                // Step 2: Extract array elements from the comptime heap.
2187                // We clone the elements AND preserve the heap so that composite
2188                // ConstValues (e.g., Struct(heap_idx)) remain valid during iteration.
2189                let elements = match iterable_val {
2190                    ConstValue::Array(heap_idx) => match &self.comptime_heap[heap_idx as usize] {
2191                        ComptimeHeapItem::Array(elems) => elems.clone(),
2192                        _ => {
2193                            return Err(CompileError::new(
2194                                ErrorKind::ComptimeEvaluationFailed {
2195                                    reason: "comptime_unroll iterable is not an array".to_string(),
2196                                },
2197                                span,
2198                            ));
2199                        }
2200                    },
2201                    _ => {
2202                        return Err(CompileError::new(
2203                            ErrorKind::ComptimeEvaluationFailed {
2204                                reason: "comptime_unroll for requires an array iterable"
2205                                    .to_string(),
2206                            },
2207                            span,
2208                        ));
2209                    }
2210                };
2211
2212                // Step 3: For each element, bind the loop variable and analyze the body.
2213                // The loop variable is stored in comptime_value_vars so that @field
2214                // and other comptime expressions in the body can access it.
2215                let mut body_air_refs = Vec::with_capacity(elements.len());
2216                for element in &elements {
2217                    // Insert the loop variable as a comptime value
2218                    let prev_value = ctx.comptime_value_vars.insert(binding, *element);
2219
2220                    // Analyze the body block
2221                    let body_result = self.analyze_inst(air, body, ctx)?;
2222                    body_air_refs.push(body_result.air_ref);
2223
2224                    // Restore the previous value (or remove if there was none)
2225                    match prev_value {
2226                        Some(v) => {
2227                            ctx.comptime_value_vars.insert(binding, v);
2228                        }
2229                        None => {
2230                            ctx.comptime_value_vars.remove(&binding);
2231                        }
2232                    }
2233                }
2234
2235                // Step 4: Emit all unrolled body instructions.
2236                if body_air_refs.is_empty() {
2237                    // Empty loop — emit unit constant
2238                    let air_ref = air.add_inst(AirInst {
2239                        data: AirInstData::UnitConst,
2240                        ty: Type::UNIT,
2241                        span,
2242                    });
2243                    Ok(AnalysisResult::new(air_ref, Type::UNIT))
2244                } else if body_air_refs.len() == 1 {
2245                    Ok(AnalysisResult::new(body_air_refs[0], Type::UNIT))
2246                } else {
2247                    // Emit a block containing all unrolled body results.
2248                    // The last body is the block's "value"; the rest are statements.
2249                    let last = body_air_refs.pop().unwrap();
2250                    let stmts: Vec<u32> = body_air_refs.iter().map(|r| r.as_u32()).collect();
2251                    let stmts_start = air.add_extra(&stmts);
2252                    let block_ref = air.add_inst(AirInst {
2253                        data: AirInstData::Block {
2254                            stmts_start,
2255                            stmts_len: stmts.len() as u32,
2256                            value: last,
2257                        },
2258                        ty: Type::UNIT,
2259                        span,
2260                    });
2261                    Ok(AnalysisResult::new(block_ref, Type::UNIT))
2262                }
2263            }
2264
2265            // Type constant: a type used as a value (e.g., `i32` in `identity(i32, 42)`)
2266            InstData::TypeConst { type_name } => {
2267                // Resolve the type name to a concrete type
2268                let ty = self.resolve_type(*type_name, inst.span)?;
2269                let air_ref = air.add_inst(AirInst {
2270                    data: AirInstData::TypeConst(ty),
2271                    ty: Type::COMPTIME_TYPE,
2272                    span: inst.span,
2273                });
2274                Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE))
2275            }
2276
2277            // Anonymous struct type: a struct type constructed at comptime
2278            // (e.g., `struct { first: T, second: T, fn get(self) -> T { ... } }` in a comptime function)
2279            InstData::AnonStructType {
2280                fields_start,
2281                fields_len,
2282                methods_start,
2283                methods_len,
2284            } => {
2285                // Get the field declarations from the RIR
2286                let field_decls = self.rir.get_field_decls(*fields_start, *fields_len);
2287
2288                // Empty structs are not allowed (unless they have methods)
2289                if field_decls.is_empty() && *methods_len == 0 {
2290                    return Err(CompileError::new(ErrorKind::EmptyStruct, inst.span));
2291                }
2292
2293                // Methods are fully supported (anon_struct_methods stabilized)
2294
2295                // Resolve each field type and build the struct fields
2296                let mut struct_fields = Vec::with_capacity(field_decls.len());
2297                for (name_sym, type_sym) in field_decls {
2298                    let name_str = self.interner.resolve(&name_sym).to_string();
2299                    let field_ty = self.resolve_type(type_sym, inst.span)?;
2300                    struct_fields.push(StructField {
2301                        name: name_str,
2302                        ty: field_ty,
2303                    });
2304                }
2305
2306                // Extract method signatures for structural equality comparison
2307                // (uses type symbols, not resolved Types, so Self matches Self)
2308                let method_sigs = self.extract_anon_method_sigs(*methods_start, *methods_len);
2309
2310                // Check if an equivalent anonymous struct already exists (structural equality)
2311                // This now compares fields, method signatures, AND captured comptime values
2312                let (struct_ty, _is_new) =
2313                    self.find_or_create_anon_struct(&struct_fields, &method_sigs, &HashMap::new());
2314
2315                // DON'T register methods here - they should be registered during const evaluation
2316                // (either try_evaluate_const for non-comptime, or try_evaluate_const_with_subst for comptime).
2317                // If we register here, we create a struct without captured comptime values, which is incorrect.
2318                //
2319                // if is_new && *methods_len > 0 {
2320                //     let struct_id = struct_ty
2321                //         .as_struct()
2322                //         .expect("anon struct should have StructId");
2323                //     self.register_anon_struct_methods(
2324                //         struct_id,
2325                //         struct_ty,
2326                //         *methods_start,
2327                //         *methods_len,
2328                //         inst.span,
2329                //     )?;
2330                // }
2331
2332                let air_ref = air.add_inst(AirInst {
2333                    data: AirInstData::TypeConst(struct_ty),
2334                    ty: Type::COMPTIME_TYPE,
2335                    span: inst.span,
2336                });
2337                Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE))
2338            }
2339
2340            // Anonymous enum type: an enum type constructed at comptime
2341            // (e.g., `enum { Some(T), None, fn method(self) -> bool { ... } }` in a comptime function)
2342            InstData::AnonEnumType {
2343                variants_start,
2344                variants_len,
2345                methods_start,
2346                methods_len,
2347            } => {
2348                // Get the variant declarations from the RIR
2349                let variant_decls = self
2350                    .rir
2351                    .get_enum_variant_decls(*variants_start, *variants_len);
2352
2353                // Empty enums are not allowed
2354                if variant_decls.is_empty() {
2355                    return Err(CompileError::new(ErrorKind::EmptyAnonEnum, inst.span));
2356                }
2357
2358                // Resolve each variant and build the enum variants
2359                let mut enum_variants = Vec::with_capacity(variant_decls.len());
2360                for (name_sym, field_type_syms, field_name_syms) in &variant_decls {
2361                    let name_str = self.interner.resolve(name_sym).to_string();
2362                    let mut fields = Vec::with_capacity(field_type_syms.len());
2363                    for ty_sym in field_type_syms {
2364                        let field_ty = self.resolve_type(*ty_sym, inst.span)?;
2365                        fields.push(field_ty);
2366                    }
2367                    let field_names: Vec<String> = field_name_syms
2368                        .iter()
2369                        .map(|s| self.interner.resolve(s).to_string())
2370                        .collect();
2371                    enum_variants.push(EnumVariantDef {
2372                        name: name_str,
2373                        fields,
2374                        field_names,
2375                    });
2376                }
2377
2378                // Check for duplicate method names
2379                if *methods_len > 0 {
2380                    let method_refs = self.rir.get_inst_refs(*methods_start, *methods_len);
2381                    let mut seen_method_names: std::collections::HashSet<Spur> =
2382                        std::collections::HashSet::new();
2383                    for mref in method_refs {
2384                        let minst = self.rir.get(mref);
2385                        if let InstData::FnDecl {
2386                            name: method_name, ..
2387                        } = &minst.data
2388                            && !seen_method_names.insert(*method_name)
2389                        {
2390                            let method_name_str = self.interner.resolve(method_name).to_string();
2391                            return Err(CompileError::new(
2392                                ErrorKind::DuplicateMethod {
2393                                    type_name: "anonymous enum".to_string(),
2394                                    method_name: method_name_str,
2395                                },
2396                                minst.span,
2397                            ));
2398                        }
2399                    }
2400                }
2401
2402                // Extract method signatures for structural equality comparison
2403                let method_sigs = self.extract_anon_method_sigs(*methods_start, *methods_len);
2404
2405                // Check if an equivalent anonymous enum already exists (structural equality)
2406                let (enum_ty, _is_new) =
2407                    self.find_or_create_anon_enum(&enum_variants, &method_sigs, &HashMap::new());
2408
2409                let air_ref = air.add_inst(AirInst {
2410                    data: AirInstData::TypeConst(enum_ty),
2411                    ty: Type::COMPTIME_TYPE,
2412                    span: inst.span,
2413                });
2414                Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE))
2415            }
2416
2417            // Checked block: enter checked context, evaluate inner expression, then exit
2418            InstData::Checked { expr } => {
2419                ctx.checked_depth += 1;
2420                let result = self.analyze_inst(air, *expr, ctx);
2421                ctx.checked_depth -= 1;
2422                result
2423            }
2424        }
2425    }
2426
2427    // ========================================================================
2428    // Implementation methods for complex operations
2429    // These are called by the category methods in analyze_ops.rs
2430    // ========================================================================
2431
2432    /// Implementation for FieldSet - handles both local and parameter field assignment.
2433    pub(crate) fn analyze_field_set_impl(
2434        &mut self,
2435        air: &mut Air,
2436        base: InstRef,
2437        field: Spur,
2438        value: InstRef,
2439        span: Span,
2440        ctx: &mut AnalysisContext,
2441    ) -> CompileResult<AnalysisResult> {
2442        use crate::sema::analyze_ops::ProjectionInfo;
2443
2444        // Try to trace the base to a place
2445        if let Some(mut trace) = self.try_trace_place(base, air, ctx)? {
2446            // Check if the root variable was fully moved
2447            if let Some(state) = ctx.moved_vars.get(&trace.root_var)
2448                && let Some(moved_span) = state.full_move
2449            {
2450                let root_name = self.interner.resolve(&trace.root_var);
2451                return Err(CompileError::new(
2452                    ErrorKind::UseAfterMove(root_name.to_string()),
2453                    span,
2454                )
2455                .with_label("value moved here", moved_span));
2456            }
2457
2458            // Check mutability
2459            let root_name = self.interner.resolve(&trace.root_var).to_string();
2460            if !trace.is_root_mutable {
2461                // Check if this is a borrow parameter - special error message
2462                if trace.is_borrow_param {
2463                    return Err(CompileError::new(
2464                        ErrorKind::MutateBorrowedValue {
2465                            variable: root_name,
2466                        },
2467                        span,
2468                    ));
2469                }
2470
2471                let root_type = trace.base_type;
2472                // Provide more specific error based on whether it's a param or local
2473                match trace.base {
2474                    AirPlaceBase::Param(_) => {
2475                        return Err(CompileError::new(
2476                            ErrorKind::AssignToImmutable(root_name.clone()),
2477                            span,
2478                        )
2479                        .with_help(format!(
2480                            "consider making parameter `{}` inout: `inout {}: {}`",
2481                            root_name,
2482                            root_name,
2483                            root_type.name()
2484                        )));
2485                    }
2486                    AirPlaceBase::Local(_) => {
2487                        return Err(CompileError::new(
2488                            ErrorKind::AssignToImmutable(root_name),
2489                            span,
2490                        ));
2491                    }
2492                }
2493            }
2494
2495            // Add the final field projection
2496            let base_type = trace.result_type();
2497            let struct_id = match base_type.as_struct() {
2498                Some(id) => id,
2499                None => {
2500                    return Err(CompileError::new(
2501                        ErrorKind::FieldAccessOnNonStruct {
2502                            found: base_type.name().to_string(),
2503                        },
2504                        span,
2505                    ));
2506                }
2507            };
2508
2509            let struct_def = self.type_pool.struct_def(struct_id);
2510            let field_name_str = self.interner.resolve(&field).to_string();
2511
2512            let (field_index, struct_field) =
2513                struct_def.find_field(&field_name_str).ok_or_compile_error(
2514                    ErrorKind::UnknownField {
2515                        struct_name: struct_def.name.clone(),
2516                        field_name: field_name_str.clone(),
2517                    },
2518                    span,
2519                )?;
2520
2521            let field_type = struct_field.ty;
2522
2523            // Add the field projection to the trace
2524            trace.projections.push(ProjectionInfo {
2525                proj: AirProjection::Field {
2526                    struct_id,
2527                    field_index: field_index as u32,
2528                },
2529                result_type: field_type,
2530                field_name: Some(field),
2531            });
2532
2533            // Analyze the value
2534            let value_result = self.analyze_inst(air, value, ctx)?;
2535
2536            // Emit PlaceWrite instruction
2537            let place_ref = Self::build_place_ref(air, &trace);
2538            let air_ref = air.add_inst(AirInst {
2539                data: AirInstData::PlaceWrite {
2540                    place: place_ref,
2541                    value: value_result.air_ref,
2542                },
2543                ty: Type::UNIT,
2544                span,
2545            });
2546            return Ok(AnalysisResult::new(air_ref, Type::UNIT));
2547        }
2548
2549        // Fallback: base is not a place (e.g., function call result)
2550        // This shouldn't normally happen for valid assignment targets
2551        Err(CompileError::new(ErrorKind::InvalidAssignmentTarget, span))
2552    }
2553
2554    /// Implementation for IndexSet - handles both local and parameter array index assignment.
2555    pub(crate) fn analyze_index_set_impl(
2556        &mut self,
2557        air: &mut Air,
2558        base: InstRef,
2559        index: InstRef,
2560        value: InstRef,
2561        span: Span,
2562        ctx: &mut AnalysisContext,
2563    ) -> CompileResult<AnalysisResult> {
2564        use crate::sema::analyze_ops::ProjectionInfo;
2565
2566        // Try to trace the base to a place
2567        if let Some(mut trace) = self.try_trace_place(base, air, ctx)? {
2568            // Check if the root variable was fully moved
2569            if let Some(state) = ctx.moved_vars.get(&trace.root_var)
2570                && let Some(moved_span) = state.full_move
2571            {
2572                let root_name = self.interner.resolve(&trace.root_var);
2573                return Err(CompileError::new(
2574                    ErrorKind::UseAfterMove(root_name.to_string()),
2575                    span,
2576                )
2577                .with_label("value moved here", moved_span));
2578            }
2579
2580            // Check mutability
2581            let root_name = self.interner.resolve(&trace.root_var).to_string();
2582            if !trace.is_root_mutable {
2583                // Check if this is a borrow parameter - special error message
2584                if trace.is_borrow_param {
2585                    return Err(CompileError::new(
2586                        ErrorKind::MutateBorrowedValue {
2587                            variable: root_name,
2588                        },
2589                        span,
2590                    ));
2591                }
2592
2593                let root_type = trace.base_type;
2594                match trace.base {
2595                    AirPlaceBase::Param(_) => {
2596                        return Err(CompileError::new(
2597                            ErrorKind::AssignToImmutable(root_name.clone()),
2598                            span,
2599                        )
2600                        .with_help(format!(
2601                            "consider making parameter `{}` inout: `inout {}: {}`",
2602                            root_name,
2603                            root_name,
2604                            root_type.name()
2605                        )));
2606                    }
2607                    AirPlaceBase::Local(_) => {
2608                        return Err(CompileError::new(
2609                            ErrorKind::AssignToImmutable(root_name),
2610                            span,
2611                        ));
2612                    }
2613                }
2614            }
2615
2616            // Get array type info from the trace
2617            let base_type = trace.result_type();
2618            let (_array_type_id, elem_type, array_len) = match base_type.as_array() {
2619                Some(id) => {
2620                    let (elem, len) = self.type_pool.array_def(id);
2621                    (id, elem, len)
2622                }
2623                None => {
2624                    return Err(CompileError::new(
2625                        ErrorKind::IndexOnNonArray {
2626                            found: base_type.name().to_string(),
2627                        },
2628                        span,
2629                    ));
2630                }
2631            };
2632
2633            // Analyze index
2634            let index_result = self.analyze_inst(air, index, ctx)?;
2635            if !index_result.ty.is_unsigned() && !index_result.ty.is_error() {
2636                return Err(CompileError::new(
2637                    ErrorKind::TypeMismatch {
2638                        expected: "unsigned integer type".to_string(),
2639                        found: index_result.ty.name().to_string(),
2640                    },
2641                    self.rir.get(index).span,
2642                ));
2643            }
2644
2645            // Compile-time bounds check for constant indices
2646            if let Some(const_index) = self.try_get_const_index(index)
2647                && (const_index < 0 || const_index as u64 >= array_len)
2648            {
2649                return Err(CompileError::new(
2650                    ErrorKind::IndexOutOfBounds {
2651                        index: const_index,
2652                        length: array_len,
2653                    },
2654                    self.rir.get(index).span,
2655                ));
2656            }
2657
2658            // Add the index projection
2659            trace.projections.push(ProjectionInfo {
2660                proj: AirProjection::Index {
2661                    array_type: base_type,
2662                    index: index_result.air_ref,
2663                },
2664                result_type: elem_type,
2665                field_name: None,
2666            });
2667
2668            // Analyze the value
2669            let value_result = self.analyze_inst(air, value, ctx)?;
2670
2671            // Emit PlaceWrite instruction
2672            let place_ref = Self::build_place_ref(air, &trace);
2673            let air_ref = air.add_inst(AirInst {
2674                data: AirInstData::PlaceWrite {
2675                    place: place_ref,
2676                    value: value_result.air_ref,
2677                },
2678                ty: Type::UNIT,
2679                span,
2680            });
2681            return Ok(AnalysisResult::new(air_ref, Type::UNIT));
2682        }
2683
2684        // Fallback: base is not a place
2685        Err(CompileError::new(ErrorKind::InvalidAssignmentTarget, span))
2686    }
2687
2688    /// Implementation for MethodCall.
2689    pub(crate) fn analyze_method_call_impl(
2690        &mut self,
2691        air: &mut Air,
2692        receiver: InstRef,
2693        method: Spur,
2694        args: Vec<RirCallArg>,
2695        span: Span,
2696        ctx: &mut AnalysisContext,
2697    ) -> CompileResult<AnalysisResult> {
2698        let receiver_var = self.extract_root_variable(receiver);
2699        let method_name_str = self.interner.resolve(&method).to_string();
2700
2701        // Check if this is a builtin mutation method
2702        let is_builtin_mutation_method = self.is_builtin_mutation_method(&method_name_str);
2703
2704        // Get storage location for mutation methods before analyzing receiver
2705        let receiver_storage = if is_builtin_mutation_method {
2706            self.get_string_receiver_storage(receiver, ctx, span)?
2707        } else {
2708            None
2709        };
2710
2711        // Analyze the receiver expression
2712        let receiver_result = self.analyze_inst(air, receiver, ctx)?;
2713        let receiver_type = receiver_result.ty;
2714
2715        // Handle module member access: module.function() becomes a direct function call
2716        if receiver_type.is_module() {
2717            return self.analyze_module_member_call_impl(air, method, args, span, ctx);
2718        }
2719
2720        // Check that receiver is a struct or enum type
2721        // For enum methods, dispatch through enum_methods table
2722        if let TypeKind::Enum(enum_id) = receiver_type.kind() {
2723            let enum_def = self.type_pool.enum_def(enum_id);
2724            let enum_name_str = enum_def.name.clone();
2725
2726            let method_key = (enum_id, method);
2727            let method_info = self.enum_methods.get(&method_key).ok_or_compile_error(
2728                ErrorKind::UndefinedMethod {
2729                    type_name: enum_name_str.clone(),
2730                    method_name: method_name_str.clone(),
2731                },
2732                span,
2733            )?;
2734
2735            if !method_info.has_self {
2736                return Err(CompileError::new(
2737                    ErrorKind::AssocFnCalledAsMethod {
2738                        type_name: enum_name_str,
2739                        function_name: method_name_str,
2740                    },
2741                    span,
2742                ));
2743            }
2744
2745            let method_param_types = self.param_arena.types(method_info.params);
2746            if args.len() != method_param_types.len() {
2747                return Err(CompileError::new(
2748                    ErrorKind::WrongArgumentCount {
2749                        expected: method_param_types.len(),
2750                        found: args.len(),
2751                    },
2752                    span,
2753                ));
2754            }
2755
2756            self.check_exclusive_access(&args, span)?;
2757
2758            let return_type = method_info.return_type;
2759
2760            let mut air_args = vec![AirCallArg {
2761                value: receiver_result.air_ref,
2762                mode: AirArgMode::Normal,
2763            }];
2764            air_args.extend(self.analyze_call_args(air, &args, ctx)?);
2765
2766            let call_name = format!("{}.{}", enum_name_str, method_name_str);
2767            let call_name_sym = self.interner.get_or_intern(&call_name);
2768
2769            let args_len = air_args.len() as u32;
2770            let mut extra_data = Vec::with_capacity(air_args.len() * 2);
2771            for arg in &air_args {
2772                extra_data.push(arg.value.as_u32());
2773                extra_data.push(arg.mode.as_u32());
2774            }
2775            let args_start = air.add_extra(&extra_data);
2776
2777            let air_ref = air.add_inst(AirInst {
2778                data: AirInstData::Call {
2779                    name: call_name_sym,
2780                    args_start,
2781                    args_len,
2782                },
2783                ty: return_type,
2784                span,
2785            });
2786            return Ok(AnalysisResult::new(air_ref, return_type));
2787        }
2788
2789        let struct_id = match receiver_type.kind() {
2790            TypeKind::Struct(id) => id,
2791            _ => {
2792                return Err(CompileError::new(
2793                    ErrorKind::MethodCallOnNonStruct {
2794                        found: receiver_type.name().to_string(),
2795                        method_name: method_name_str,
2796                    },
2797                    span,
2798                ));
2799            }
2800        };
2801
2802        // Check if this is a builtin type and handle its methods
2803        if let Some(builtin_def) = self.get_builtin_type_def(struct_id) {
2804            let method_ctx = BuiltinMethodContext {
2805                struct_id,
2806                builtin_def,
2807                method_name: &method_name_str,
2808                span,
2809            };
2810            let receiver_info = ReceiverInfo {
2811                result: receiver_result,
2812                var: receiver_var,
2813                storage: receiver_storage,
2814            };
2815            return self.analyze_builtin_method(air, ctx, &method_ctx, receiver_info, &args);
2816        }
2817
2818        // Look up the struct name by its ID (for error messages)
2819        let struct_def = self.type_pool.struct_def(struct_id);
2820        let struct_name_str = struct_def.name.clone();
2821
2822        // Look up the method using StructId directly
2823        let method_key = (struct_id, method);
2824        let method_info = self.methods.get(&method_key).ok_or_compile_error(
2825            ErrorKind::UndefinedMethod {
2826                type_name: struct_name_str.clone(),
2827                method_name: method_name_str.clone(),
2828            },
2829            span,
2830        )?;
2831
2832        // Check that this is a method (has self), not an associated function
2833        if !method_info.has_self {
2834            return Err(CompileError::new(
2835                ErrorKind::AssocFnCalledAsMethod {
2836                    type_name: struct_name_str,
2837                    function_name: method_name_str,
2838                },
2839                span,
2840            ));
2841        }
2842
2843        // Check if calling an unchecked method requires a checked block
2844        if method_info.is_unchecked && ctx.checked_depth == 0 {
2845            return Err(CompileError::new(
2846                ErrorKind::UncheckedCallRequiresChecked(format!(
2847                    "{}.{}",
2848                    struct_name_str, method_name_str
2849                )),
2850                span,
2851            ));
2852        }
2853
2854        // Check argument count (method_info.params excludes self)
2855        let method_param_types = self.param_arena.types(method_info.params);
2856        if args.len() != method_param_types.len() {
2857            return Err(CompileError::new(
2858                ErrorKind::WrongArgumentCount {
2859                    expected: method_param_types.len(),
2860                    found: args.len(),
2861                },
2862                span,
2863            ));
2864        }
2865
2866        // Check for exclusive access violation
2867        self.check_exclusive_access(&args, span)?;
2868
2869        // Clone data needed before mutable borrow
2870        let return_type = method_info.return_type;
2871
2872        // Analyze arguments - receiver first, then remaining args
2873        let mut air_args = vec![AirCallArg {
2874            value: receiver_result.air_ref,
2875            mode: AirArgMode::Normal,
2876        }];
2877        air_args.extend(self.analyze_call_args(air, &args, ctx)?);
2878
2879        // Generate a method call name: Type.method
2880        let call_name = format!("{}.{}", struct_name_str, method_name_str);
2881        let call_name_sym = self.interner.get_or_intern(&call_name);
2882
2883        // Encode call args into extra array
2884        let args_len = air_args.len() as u32;
2885        let mut extra_data = Vec::with_capacity(air_args.len() * 2);
2886        for arg in &air_args {
2887            extra_data.push(arg.value.as_u32());
2888            extra_data.push(arg.mode.as_u32());
2889        }
2890        let args_start = air.add_extra(&extra_data);
2891
2892        let air_ref = air.add_inst(AirInst {
2893            data: AirInstData::Call {
2894                name: call_name_sym,
2895                args_start,
2896                args_len,
2897            },
2898            ty: return_type,
2899            span,
2900        });
2901        Ok(AnalysisResult::new(air_ref, return_type))
2902    }
2903
2904    /// Analyze a module member call: `module.function(args)` becomes a direct function call.
2905    ///
2906    /// In Phase 1 of the module system, modules are virtual namespaces. When you import
2907    /// a module with `@import("foo.gruel")`, all of foo.gruel's functions are already in the
2908    /// global function table (via multi-file compilation). The module just provides a
2909    /// namespace at the source level.
2910    fn analyze_module_member_call_impl(
2911        &mut self,
2912        air: &mut Air,
2913        function_name: Spur,
2914        args: Vec<RirCallArg>,
2915        span: Span,
2916        ctx: &mut AnalysisContext,
2917    ) -> CompileResult<AnalysisResult> {
2918        // Look up the function in the global function table
2919        let fn_name_str = self.interner.resolve(&function_name).to_string();
2920        let fn_info = *self
2921            .functions
2922            .get(&function_name)
2923            .ok_or_compile_error(ErrorKind::UndefinedFunction(fn_name_str.clone()), span)?;
2924
2925        // Track this function as referenced (for lazy analysis)
2926        ctx.referenced_functions.insert(function_name);
2927
2928        // Check visibility: private functions are only accessible from the same directory
2929        let accessing_file_id = span.file_id;
2930        let target_file_id = fn_info.file_id;
2931        if !self.is_accessible(accessing_file_id, target_file_id, fn_info.is_pub) {
2932            return Err(CompileError::new(
2933                ErrorKind::PrivateMemberAccess {
2934                    item_kind: "function".to_string(),
2935                    name: fn_name_str,
2936                },
2937                span,
2938            ));
2939        }
2940
2941        // Get parameter data from the arena
2942        let param_types = self.param_arena.types(fn_info.params);
2943        let param_modes = self.param_arena.modes(fn_info.params);
2944
2945        // Check argument count
2946        if args.len() != param_types.len() {
2947            let expected = param_types.len();
2948            let found = args.len();
2949            return Err(CompileError::new(
2950                ErrorKind::WrongArgumentCount { expected, found },
2951                span,
2952            ));
2953        }
2954
2955        // Check that call-site argument modes match function parameter modes
2956        for (i, (arg, expected_mode)) in args.iter().zip(param_modes.iter()).enumerate() {
2957            match expected_mode {
2958                RirParamMode::Inout => {
2959                    if arg.mode != RirArgMode::Inout {
2960                        return Err(CompileError::new(
2961                            ErrorKind::InoutKeywordMissing,
2962                            self.rir.get(args[i].value).span,
2963                        ));
2964                    }
2965                }
2966                RirParamMode::Borrow => {
2967                    if arg.mode != RirArgMode::Borrow {
2968                        return Err(CompileError::new(
2969                            ErrorKind::BorrowKeywordMissing,
2970                            self.rir.get(args[i].value).span,
2971                        ));
2972                    }
2973                }
2974                RirParamMode::Normal => {
2975                    // Normal params accept any mode
2976                }
2977                RirParamMode::Comptime => {
2978                    // Comptime params - handled elsewhere
2979                }
2980            }
2981        }
2982
2983        // Analyze arguments
2984        let air_args = self.analyze_call_args(air, &args, ctx)?;
2985
2986        let return_type = fn_info.return_type;
2987
2988        // Encode call args
2989        let mut extra_data = Vec::with_capacity(air_args.len() * 2);
2990        for arg in &air_args {
2991            extra_data.push(arg.value.as_u32());
2992            extra_data.push(arg.mode.as_u32());
2993        }
2994        let call_args_start = air.add_extra(&extra_data);
2995        let call_args_len = air_args.len() as u32;
2996
2997        let air_ref = air.add_inst(AirInst {
2998            data: AirInstData::Call {
2999                name: function_name,
3000                args_start: call_args_start,
3001                args_len: call_args_len,
3002            },
3003            ty: return_type,
3004            span,
3005        });
3006        Ok(AnalysisResult::new(air_ref, return_type))
3007    }
3008
3009    /// Implementation for AssocFnCall.
3010    pub(crate) fn analyze_assoc_fn_call_impl(
3011        &mut self,
3012        air: &mut Air,
3013        type_name: Spur,
3014        function: Spur,
3015        args: Vec<RirCallArg>,
3016        span: Span,
3017        ctx: &mut AnalysisContext,
3018    ) -> CompileResult<AnalysisResult> {
3019        let type_name_str = self.interner.resolve(&type_name).to_string();
3020        let function_name_str = self.interner.resolve(&function).to_string();
3021
3022        // Check if this is an enum data variant construction (e.g., IntOption::Some(42)).
3023        // This must be checked before the struct lookup because enums and structs share the
3024        // same AssocFnCall syntax.
3025        if let Some(&enum_id) = self.enums.get(&type_name) {
3026            let enum_def = self.type_pool.enum_def(enum_id);
3027            if let Some(variant_index) = enum_def.find_variant(&function_name_str) {
3028                let variant_def = &enum_def.variants[variant_index];
3029                let field_types: Vec<Type> = variant_def.fields.clone();
3030                if !field_types.is_empty() {
3031                    // If this is a struct variant, error: use { } instead of ( )
3032                    if variant_def.is_struct_variant() {
3033                        return Err(CompileError::new(
3034                            ErrorKind::TypeMismatch {
3035                                expected: format!(
3036                                    "struct-style construction `{}::{} {{ ... }}`",
3037                                    type_name_str, function_name_str
3038                                ),
3039                                found: format!(
3040                                    "tuple-style construction `{}::{}(...)`",
3041                                    type_name_str, function_name_str
3042                                ),
3043                            },
3044                            span,
3045                        ));
3046                    }
3047
3048                    // Check argument count
3049                    if args.len() != field_types.len() {
3050                        return Err(CompileError::new(
3051                            ErrorKind::WrongArgumentCount {
3052                                expected: field_types.len(),
3053                                found: args.len(),
3054                            },
3055                            span,
3056                        ));
3057                    }
3058
3059                    // Analyze each argument and type-check against the variant's field types
3060                    let mut field_air_refs = Vec::with_capacity(args.len());
3061                    for (i, arg) in args.iter().enumerate() {
3062                        let result = self.analyze_inst(air, arg.value, ctx)?;
3063                        if result.ty != field_types[i] {
3064                            return Err(CompileError::new(
3065                                ErrorKind::TypeMismatch {
3066                                    expected: field_types[i].name().to_string(),
3067                                    found: result.ty.name().to_string(),
3068                                },
3069                                span,
3070                            ));
3071                        }
3072                        field_air_refs.push(result.air_ref.as_u32());
3073                    }
3074
3075                    // Store field AirRefs in the extra array
3076                    let fields_len = field_air_refs.len() as u32;
3077                    let fields_start = air.add_extra(&field_air_refs);
3078
3079                    let enum_type = Type::new_enum(enum_id);
3080                    let air_ref = air.add_inst(AirInst {
3081                        data: AirInstData::EnumCreate {
3082                            enum_id,
3083                            variant_index: variant_index as u32,
3084                            fields_start,
3085                            fields_len,
3086                        },
3087                        ty: enum_type,
3088                        span,
3089                    });
3090                    return Ok(AnalysisResult::new(air_ref, enum_type));
3091                }
3092                // Unit variant called as a function: fall through to the error path.
3093            }
3094        }
3095
3096        // Check if this is an enum data variant construction via a comptime type variable
3097        // (e.g., `let Opt = Option(i32); Opt::Some(42)`)
3098        if let Some(&ty) = ctx.comptime_type_vars.get(&type_name)
3099            && let TypeKind::Enum(enum_id) = ty.kind()
3100        {
3101            let enum_def = self.type_pool.enum_def(enum_id);
3102            if let Some(variant_index) = enum_def.find_variant(&function_name_str) {
3103                let variant_def = &enum_def.variants[variant_index];
3104                let field_types: Vec<Type> = variant_def.fields.clone();
3105                if !field_types.is_empty() {
3106                    if variant_def.is_struct_variant() {
3107                        return Err(CompileError::new(
3108                            ErrorKind::TypeMismatch {
3109                                expected: format!(
3110                                    "struct-style construction `{}::{} {{ ... }}`",
3111                                    type_name_str, function_name_str
3112                                ),
3113                                found: format!(
3114                                    "tuple-style construction `{}::{}(...)`",
3115                                    type_name_str, function_name_str
3116                                ),
3117                            },
3118                            span,
3119                        ));
3120                    }
3121                    if args.len() != field_types.len() {
3122                        return Err(CompileError::new(
3123                            ErrorKind::WrongArgumentCount {
3124                                expected: field_types.len(),
3125                                found: args.len(),
3126                            },
3127                            span,
3128                        ));
3129                    }
3130                    let mut field_air_refs = Vec::with_capacity(args.len());
3131                    for (i, arg) in args.iter().enumerate() {
3132                        let result = self.analyze_inst(air, arg.value, ctx)?;
3133                        if result.ty != field_types[i] {
3134                            return Err(CompileError::new(
3135                                ErrorKind::TypeMismatch {
3136                                    expected: field_types[i].name().to_string(),
3137                                    found: result.ty.name().to_string(),
3138                                },
3139                                span,
3140                            ));
3141                        }
3142                        field_air_refs.push(result.air_ref.as_u32());
3143                    }
3144                    let fields_len = field_air_refs.len() as u32;
3145                    let fields_start = air.add_extra(&field_air_refs);
3146                    let enum_type = Type::new_enum(enum_id);
3147                    let air_ref = air.add_inst(AirInst {
3148                        data: AirInstData::EnumCreate {
3149                            enum_id,
3150                            variant_index: variant_index as u32,
3151                            fields_start,
3152                            fields_len,
3153                        },
3154                        ty: enum_type,
3155                        span,
3156                    });
3157                    return Ok(AnalysisResult::new(air_ref, enum_type));
3158                }
3159                // Unit variant called as function — fall through to error
3160            }
3161
3162            // Not a variant — check for associated function on the enum
3163            let method_key = (enum_id, function);
3164            if let Some(method_info) = self.enum_methods.get(&method_key).copied() {
3165                ctx.referenced_methods
3166                    .insert((StructId(enum_id.0), function));
3167
3168                if method_info.has_self {
3169                    return Err(CompileError::new(
3170                        ErrorKind::MethodCalledAsAssocFn {
3171                            type_name: type_name_str,
3172                            method_name: function_name_str,
3173                        },
3174                        span,
3175                    ));
3176                }
3177
3178                let method_param_types: Vec<Type> =
3179                    self.param_arena.types(method_info.params).to_vec();
3180                if args.len() != method_param_types.len() {
3181                    return Err(CompileError::new(
3182                        ErrorKind::WrongArgumentCount {
3183                            expected: method_param_types.len(),
3184                            found: args.len(),
3185                        },
3186                        span,
3187                    ));
3188                }
3189
3190                let mut extra_data = Vec::with_capacity(args.len() * 2);
3191                for (i, arg) in args.iter().enumerate() {
3192                    let result = self.analyze_inst(air, arg.value, ctx)?;
3193                    if result.ty != method_param_types[i] {
3194                        return Err(CompileError::new(
3195                            ErrorKind::TypeMismatch {
3196                                expected: method_param_types[i].name().to_string(),
3197                                found: result.ty.name().to_string(),
3198                            },
3199                            span,
3200                        ));
3201                    }
3202                    extra_data.push(result.air_ref.as_u32());
3203                    extra_data.push(AirArgMode::Normal.as_u32());
3204                }
3205
3206                let enum_def = self.type_pool.enum_def(enum_id);
3207                let type_name_str2 = enum_def.name.clone();
3208                let full_name = format!("{}::{}", type_name_str2, function_name_str);
3209                let callee_sym = self.interner.get_or_intern(&full_name);
3210
3211                let args_start = air.add_extra(&extra_data);
3212                let args_len = args.len() as u32;
3213                let air_ref = air.add_inst(AirInst {
3214                    data: AirInstData::Call {
3215                        name: callee_sym,
3216                        args_start,
3217                        args_len,
3218                    },
3219                    ty: method_info.return_type,
3220                    span,
3221                });
3222                return Ok(AnalysisResult::new(air_ref, method_info.return_type));
3223            }
3224        }
3225
3226        // Check that the type exists and is a struct
3227        // First check if it's a comptime type variable (e.g., `let P = Point(); P::origin()`)
3228        let struct_id = if let Some(&ty) = ctx.comptime_type_vars.get(&type_name) {
3229            // Extract struct ID from the comptime type
3230            match ty.kind() {
3231                TypeKind::Struct(id) => id,
3232                _ => {
3233                    return Err(CompileError::new(
3234                        ErrorKind::TypeMismatch {
3235                            expected: "struct type".to_string(),
3236                            found: ty.name().to_string(),
3237                        },
3238                        span,
3239                    ));
3240                }
3241            }
3242        } else {
3243            *self
3244                .structs
3245                .get(&type_name)
3246                .ok_or_compile_error(ErrorKind::UnknownType(type_name_str.clone()), span)?
3247        };
3248
3249        // Handle builtin type associated functions
3250        if let Some(builtin_def) = self.get_builtin_type_def(struct_id) {
3251            return self.analyze_builtin_assoc_fn(
3252                air,
3253                ctx,
3254                (struct_id, builtin_def),
3255                &function_name_str,
3256                &args,
3257                span,
3258            );
3259        }
3260
3261        // Look up the function using StructId
3262        let method_key = (struct_id, function);
3263        let method_info = self.methods.get(&method_key).ok_or_compile_error(
3264            ErrorKind::UndefinedAssocFn {
3265                type_name: type_name_str.clone(),
3266                function_name: function_name_str.clone(),
3267            },
3268            span,
3269        )?;
3270
3271        // Track this associated function/method as referenced (for lazy analysis)
3272        ctx.referenced_methods.insert(method_key);
3273
3274        // Check that this is an associated function (no self), not a method
3275        if method_info.has_self {
3276            return Err(CompileError::new(
3277                ErrorKind::MethodCalledAsAssocFn {
3278                    type_name: type_name_str,
3279                    method_name: function_name_str,
3280                },
3281                span,
3282            ));
3283        }
3284
3285        // Check if calling an unchecked associated function requires a checked block
3286        if method_info.is_unchecked && ctx.checked_depth == 0 {
3287            return Err(CompileError::new(
3288                ErrorKind::UncheckedCallRequiresChecked(format!(
3289                    "{}::{}",
3290                    type_name_str, function_name_str
3291                )),
3292                span,
3293            ));
3294        }
3295
3296        // Check argument count
3297        let method_param_types = self.param_arena.types(method_info.params);
3298        if args.len() != method_param_types.len() {
3299            return Err(CompileError::new(
3300                ErrorKind::WrongArgumentCount {
3301                    expected: method_param_types.len(),
3302                    found: args.len(),
3303                },
3304                span,
3305            ));
3306        }
3307
3308        // Check for exclusive access violation
3309        self.check_exclusive_access(&args, span)?;
3310
3311        // Clone data needed before mutable borrow
3312        let return_type = method_info.return_type;
3313
3314        // Analyze arguments
3315        let air_args = self.analyze_call_args(air, &args, ctx)?;
3316
3317        // Generate a function call name: Type::function
3318        // Use the internal struct name (e.g., "__anon_struct_0") for anonymous structs,
3319        // not the user-visible type variable name (e.g., "P")
3320        let struct_def = self.type_pool.struct_def(struct_id);
3321        let internal_type_name = &struct_def.name;
3322        let call_name = format!("{}::{}", internal_type_name, function_name_str);
3323        let call_name_sym = self.interner.get_or_intern(&call_name);
3324
3325        // Encode call args into extra array
3326        let args_len = air_args.len() as u32;
3327        let mut extra_data = Vec::with_capacity(air_args.len() * 2);
3328        for arg in &air_args {
3329            extra_data.push(arg.value.as_u32());
3330            extra_data.push(arg.mode.as_u32());
3331        }
3332        let args_start = air.add_extra(&extra_data);
3333
3334        let air_ref = air.add_inst(AirInst {
3335            data: AirInstData::Call {
3336                name: call_name_sym,
3337                args_start,
3338                args_len,
3339            },
3340            ty: return_type,
3341            span,
3342        });
3343        Ok(AnalysisResult::new(air_ref, return_type))
3344    }
3345
3346    /// Implementation for Intrinsic calls.
3347    pub(crate) fn analyze_intrinsic_impl(
3348        &mut self,
3349        air: &mut Air,
3350        inst_ref: InstRef,
3351        name: Spur,
3352        args: Vec<RirCallArg>,
3353        span: Span,
3354        ctx: &mut AnalysisContext,
3355    ) -> CompileResult<AnalysisResult> {
3356        let known = &self.known;
3357
3358        // Use pre-interned symbol comparison instead of string comparison
3359        if name == known.dbg {
3360            self.analyze_dbg_intrinsic(air, inst_ref, &args, span, ctx)
3361        } else if name == known.int_cast {
3362            self.analyze_intcast_intrinsic(air, inst_ref, &args, span, ctx)
3363        } else if name == known.test_preview_gate {
3364            self.analyze_test_preview_gate_intrinsic(air, &args, span)
3365        } else if name == known.read_line {
3366            self.analyze_read_line_intrinsic(air, name, &args, span)
3367        } else if let Some(intrinsic_name_str) = known.get_parse_intrinsic_name(name) {
3368            self.analyze_parse_intrinsic(air, name, intrinsic_name_str, &args, span, ctx)
3369        } else if name == known.cast {
3370            self.analyze_cast_intrinsic(air, inst_ref, &args, span, ctx)
3371        } else if name == known.panic {
3372            self.analyze_panic_intrinsic(air, &args, span, ctx)
3373        } else if name == known.assert {
3374            self.analyze_assert_intrinsic(air, &args, span, ctx)
3375        } else if name == known.import {
3376            self.analyze_import_intrinsic(air, &args, span)
3377        } else if name == known.random_u32 {
3378            self.analyze_random_u32_intrinsic(air, name, &args, span)
3379        } else if name == known.random_u64 {
3380            self.analyze_random_u64_intrinsic(air, name, &args, span)
3381        } else if name == known.ptr_read {
3382            Self::require_checked_for_intrinsic(ctx, "ptr_read", span)?;
3383            self.analyze_ptr_read_intrinsic(air, name, &args, span, ctx)
3384        } else if name == known.ptr_write {
3385            Self::require_checked_for_intrinsic(ctx, "ptr_write", span)?;
3386            self.analyze_ptr_write_intrinsic(air, name, &args, span, ctx)
3387        } else if name == known.ptr_offset {
3388            Self::require_checked_for_intrinsic(ctx, "ptr_offset", span)?;
3389            self.analyze_ptr_offset_intrinsic(air, name, &args, span, ctx)
3390        } else if name == known.ptr_to_int {
3391            Self::require_checked_for_intrinsic(ctx, "ptr_to_int", span)?;
3392            self.analyze_ptr_to_int_intrinsic(air, name, &args, span, ctx)
3393        } else if name == known.int_to_ptr {
3394            Self::require_checked_for_intrinsic(ctx, "int_to_ptr", span)?;
3395            self.analyze_int_to_ptr_intrinsic(air, name, inst_ref, &args, span, ctx)
3396        } else if name == known.null_ptr {
3397            Self::require_checked_for_intrinsic(ctx, "null_ptr", span)?;
3398            self.analyze_null_ptr_intrinsic(air, name, inst_ref, &args, span, ctx)
3399        } else if name == known.is_null {
3400            Self::require_checked_for_intrinsic(ctx, "is_null", span)?;
3401            self.analyze_is_null_intrinsic(air, name, &args, span, ctx)
3402        } else if name == known.ptr_copy {
3403            Self::require_checked_for_intrinsic(ctx, "ptr_copy", span)?;
3404            self.analyze_ptr_copy_intrinsic(air, name, &args, span, ctx)
3405        } else if name == known.raw {
3406            Self::require_checked_for_intrinsic(ctx, "raw", span)?;
3407            self.analyze_addr_of_intrinsic(air, &args, span, ctx, false)
3408        } else if name == known.raw_mut {
3409            Self::require_checked_for_intrinsic(ctx, "raw_mut", span)?;
3410            self.analyze_addr_of_intrinsic(air, &args, span, ctx, true)
3411        } else if name == known.syscall {
3412            Self::require_checked_for_intrinsic(ctx, "syscall", span)?;
3413            self.analyze_syscall_intrinsic(air, name, &args, span, ctx)
3414        } else if name == known.target_arch {
3415            self.analyze_target_arch_intrinsic(air, &args, span)
3416        } else if name == known.target_os {
3417            self.analyze_target_os_intrinsic(air, &args, span)
3418        } else if name == known.compile_error {
3419            self.analyze_compile_error_intrinsic(air, &args, span)
3420        } else if name == known.field {
3421            self.analyze_field_intrinsic(air, &args, span, ctx)
3422        } else {
3423            // Unknown intrinsic - resolve name for error message
3424            let intrinsic_name_str = self.interner.resolve(&name);
3425            Err(CompileError::new(
3426                ErrorKind::UnknownIntrinsic(intrinsic_name_str.to_string()),
3427                span,
3428            ))
3429        }
3430    }
3431
3432    // Helper methods for intrinsic analysis (delegated from analyze_intrinsic_impl)
3433
3434    fn analyze_dbg_intrinsic(
3435        &mut self,
3436        air: &mut Air,
3437        _inst_ref: InstRef,
3438        args: &[RirCallArg],
3439        span: Span,
3440        ctx: &mut AnalysisContext,
3441    ) -> CompileResult<AnalysisResult> {
3442        let mut arg_air_refs = Vec::with_capacity(args.len());
3443        for arg in args {
3444            let arg_result = self.analyze_inst(air, arg.value, ctx)?;
3445            let arg_type = arg_result.ty;
3446
3447            // Validate type. At runtime, @dbg accepts integers, booleans, and
3448            // strings. Structs/enums/arrays are rejected (except errors/never,
3449            // which propagate); String itself is a builtin struct and is
3450            // recognized via is_builtin_string in codegen — at the sema level
3451            // we allow structs here and let codegen handle the String case.
3452            if !arg_type.is_integer()
3453                && arg_type != Type::BOOL
3454                && !arg_type.is_struct()
3455                && !arg_type.is_enum()
3456                && !arg_type.is_array()
3457                && !arg_type.is_error()
3458                && !arg_type.is_never()
3459            {
3460                return Err(CompileError::new(
3461                    ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
3462                        name: "dbg".to_string(),
3463                        expected: "integer, bool, or string".to_string(),
3464                        found: arg_type.name().to_string(),
3465                    })),
3466                    span,
3467                ));
3468            }
3469
3470            arg_air_refs.push(arg_result.air_ref.as_u32());
3471        }
3472
3473        let args_len = arg_air_refs.len() as u32;
3474        let args_start = air.add_extra(&arg_air_refs);
3475        let air_ref = air.add_inst(AirInst {
3476            data: AirInstData::Intrinsic {
3477                name: self.known.dbg,
3478                args_start,
3479                args_len,
3480            },
3481            ty: Type::UNIT,
3482            span,
3483        });
3484        Ok(AnalysisResult::new(air_ref, Type::UNIT))
3485    }
3486
3487    fn analyze_cast_intrinsic(
3488        &mut self,
3489        air: &mut Air,
3490        inst_ref: InstRef,
3491        args: &[RirCallArg],
3492        span: Span,
3493        ctx: &mut AnalysisContext,
3494    ) -> CompileResult<AnalysisResult> {
3495        if args.len() != 1 {
3496            return Err(CompileError::new(
3497                ErrorKind::IntrinsicWrongArgCount {
3498                    name: "cast".to_string(),
3499                    expected: 1,
3500                    found: args.len(),
3501                },
3502                span,
3503            ));
3504        }
3505
3506        // Get target type from HM inference
3507        let target_type = Self::get_resolved_type(ctx, inst_ref, span, "@cast intrinsic")?;
3508
3509        let arg_result = self.analyze_inst(air, args[0].value, ctx)?;
3510        let source_type = arg_result.ty;
3511
3512        // Validate source type: must be numeric (integer or float)
3513        if !source_type.is_numeric() && !source_type.is_error() && !source_type.is_never() {
3514            return Err(CompileError::new(
3515                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
3516                    name: "cast".to_string(),
3517                    expected: "numeric type".to_string(),
3518                    found: source_type.name().to_string(),
3519                })),
3520                span,
3521            ));
3522        }
3523        // Validate target type: must be numeric (integer or float)
3524        if !target_type.is_numeric() && !target_type.is_error() && !target_type.is_never() {
3525            return Err(CompileError::new(
3526                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
3527                    name: "cast".to_string(),
3528                    expected: "numeric target type".to_string(),
3529                    found: target_type.name().to_string(),
3530                })),
3531                span,
3532            ));
3533        }
3534
3535        // Skip cast if types are the same
3536        if source_type == target_type || source_type.is_error() || source_type.is_never() {
3537            return Ok(arg_result);
3538        }
3539
3540        // Choose the right instruction based on source/target type categories
3541        let data = match (source_type.is_integer(), target_type.is_integer()) {
3542            (true, true) => AirInstData::IntCast {
3543                value: arg_result.air_ref,
3544                from_ty: source_type,
3545            },
3546            (true, false) => AirInstData::IntToFloat {
3547                value: arg_result.air_ref,
3548                from_ty: source_type,
3549            },
3550            (false, true) => AirInstData::FloatToInt {
3551                value: arg_result.air_ref,
3552                from_ty: source_type,
3553            },
3554            (false, false) => AirInstData::FloatCast {
3555                value: arg_result.air_ref,
3556                from_ty: source_type,
3557            },
3558        };
3559
3560        let air_ref = air.add_inst(AirInst {
3561            data,
3562            ty: target_type,
3563            span,
3564        });
3565        Ok(AnalysisResult::new(air_ref, target_type))
3566    }
3567
3568    fn analyze_panic_intrinsic(
3569        &mut self,
3570        air: &mut Air,
3571        args: &[RirCallArg],
3572        span: Span,
3573        ctx: &mut AnalysisContext,
3574    ) -> CompileResult<AnalysisResult> {
3575        // @panic takes an optional string message
3576        if args.len() > 1 {
3577            return Err(CompileError::new(
3578                ErrorKind::IntrinsicWrongArgCount {
3579                    name: "panic".to_string(),
3580                    expected: 1,
3581                    found: args.len(),
3582                },
3583                span,
3584            ));
3585        }
3586
3587        if args.is_empty() {
3588            // Panic with no message
3589            let air_ref = air.add_inst(AirInst {
3590                data: AirInstData::UnitConst,
3591                ty: Type::NEVER,
3592                span,
3593            });
3594            return Ok(AnalysisResult::new(air_ref, Type::NEVER));
3595        }
3596
3597        // Analyze the message argument
3598        let arg_result = self.analyze_inst(air, args[0].value, ctx)?;
3599
3600        let args_start = air.add_extra(&[arg_result.air_ref.as_u32()]);
3601        let air_ref = air.add_inst(AirInst {
3602            data: AirInstData::Intrinsic {
3603                name: self.known.panic,
3604                args_start,
3605                args_len: 1,
3606            },
3607            ty: Type::NEVER,
3608            span,
3609        });
3610        Ok(AnalysisResult::new(air_ref, Type::NEVER))
3611    }
3612
3613    fn analyze_assert_intrinsic(
3614        &mut self,
3615        air: &mut Air,
3616        args: &[RirCallArg],
3617        span: Span,
3618        ctx: &mut AnalysisContext,
3619    ) -> CompileResult<AnalysisResult> {
3620        // @assert takes a bool condition and optional message
3621        if args.is_empty() || args.len() > 2 {
3622            return Err(CompileError::new(
3623                ErrorKind::IntrinsicWrongArgCount {
3624                    name: "assert".to_string(),
3625                    expected: 1,
3626                    found: args.len(),
3627                },
3628                span,
3629            ));
3630        }
3631
3632        let cond_result = self.analyze_inst(air, args[0].value, ctx)?;
3633
3634        // Build args for AIR
3635        let mut extra_data = vec![cond_result.air_ref.as_u32()];
3636        if args.len() > 1 {
3637            let msg_result = self.analyze_inst(air, args[1].value, ctx)?;
3638            extra_data.push(msg_result.air_ref.as_u32());
3639        }
3640
3641        let args_len = extra_data.len() as u32;
3642        let args_start = air.add_extra(&extra_data);
3643        let air_ref = air.add_inst(AirInst {
3644            data: AirInstData::Intrinsic {
3645                name: self.known.assert,
3646                args_start,
3647                args_len,
3648            },
3649            ty: Type::UNIT,
3650            span,
3651        });
3652        Ok(AnalysisResult::new(air_ref, Type::UNIT))
3653    }
3654
3655    /// Analyze @intCast intrinsic.
3656    fn analyze_intcast_intrinsic(
3657        &mut self,
3658        air: &mut Air,
3659        inst_ref: InstRef,
3660        args: &[RirCallArg],
3661        span: Span,
3662        ctx: &mut AnalysisContext,
3663    ) -> CompileResult<AnalysisResult> {
3664        let intrinsic_name = "intCast";
3665
3666        // @intCast expects exactly one argument
3667        if args.len() != 1 {
3668            return Err(CompileError::new(
3669                ErrorKind::IntrinsicWrongArgCount {
3670                    name: intrinsic_name.to_string(),
3671                    expected: 1,
3672                    found: args.len(),
3673                },
3674                span,
3675            ));
3676        }
3677
3678        // Analyze the argument
3679        let arg_result = self.analyze_inst(air, args[0].value, ctx)?;
3680        let from_ty = arg_result.ty;
3681
3682        // Argument must be an integer type
3683        if !from_ty.is_integer() {
3684            return Err(CompileError::new(
3685                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
3686                    name: intrinsic_name.to_string(),
3687                    expected: "integer".to_string(),
3688                    found: from_ty.name().to_string(),
3689                })),
3690                span,
3691            ));
3692        }
3693
3694        // Get the target type from HM inference
3695        let target_ty = match ctx.resolved_types.get(&inst_ref).copied() {
3696            Some(ty) if ty.is_integer() => ty,
3697            Some(Type::ERROR) => {
3698                // Error already reported during type inference
3699                return Err(CompileError::new(ErrorKind::TypeAnnotationRequired, span));
3700            }
3701            Some(ty) => {
3702                return Err(CompileError::new(
3703                    ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
3704                        name: intrinsic_name.to_string(),
3705                        expected: "integer".to_string(),
3706                        found: ty.name().to_string(),
3707                    })),
3708                    span,
3709                ));
3710            }
3711            None => {
3712                // Type inference couldn't determine the target type
3713                return Err(CompileError::new(ErrorKind::TypeAnnotationRequired, span));
3714            }
3715        };
3716
3717        let air_ref = air.add_inst(AirInst {
3718            data: AirInstData::IntCast {
3719                value: arg_result.air_ref,
3720                from_ty,
3721            },
3722            ty: target_ty,
3723            span,
3724        });
3725        Ok(AnalysisResult::new(air_ref, target_ty))
3726    }
3727
3728    /// Analyze @test_preview_gate intrinsic.
3729    fn analyze_test_preview_gate_intrinsic(
3730        &mut self,
3731        air: &mut Air,
3732        args: &[RirCallArg],
3733        span: Span,
3734    ) -> CompileResult<AnalysisResult> {
3735        // @test_preview_gate() - no-op intrinsic gated by test_infra preview feature.
3736        self.require_preview(
3737            PreviewFeature::TestInfra,
3738            "@test_preview_gate() intrinsic",
3739            span,
3740        )?;
3741
3742        // Takes no arguments
3743        if !args.is_empty() {
3744            return Err(CompileError::new(
3745                ErrorKind::IntrinsicWrongArgCount {
3746                    name: "test_preview_gate".to_string(),
3747                    expected: 0,
3748                    found: args.len(),
3749                },
3750                span,
3751            ));
3752        }
3753
3754        // No-op: just return a unit constant
3755        let air_ref = air.add_inst(AirInst {
3756            data: AirInstData::UnitConst,
3757            ty: Type::UNIT,
3758            span,
3759        });
3760        Ok(AnalysisResult::new(air_ref, Type::UNIT))
3761    }
3762
3763    /// Analyze @compileError intrinsic.
3764    ///
3765    /// In the runtime analysis path, @compileError is a comptime-only intrinsic
3766    /// that has type `!` (never). It takes exactly one string literal argument.
3767    /// The actual error emission happens in the comptime interpreter.
3768    fn analyze_compile_error_intrinsic(
3769        &mut self,
3770        air: &mut Air,
3771        args: &[RirCallArg],
3772        span: Span,
3773    ) -> CompileResult<AnalysisResult> {
3774        if args.len() != 1 {
3775            return Err(CompileError::new(
3776                ErrorKind::IntrinsicWrongArgCount {
3777                    name: "compileError".to_string(),
3778                    expected: 1,
3779                    found: args.len(),
3780                },
3781                span,
3782            ));
3783        }
3784
3785        // Verify the argument is a string literal
3786        let arg_inst = self.rir.get(args[0].value);
3787        if !matches!(&arg_inst.data, gruel_rir::InstData::StringConst(_)) {
3788            return Err(CompileError::new(
3789                ErrorKind::ComptimeEvaluationFailed {
3790                    reason: "@compileError requires a string literal argument".into(),
3791                },
3792                arg_inst.span,
3793            ));
3794        }
3795
3796        // Type is `!` (never) — @compileError always terminates compilation
3797        let air_ref = air.add_inst(AirInst {
3798            data: AirInstData::UnitConst,
3799            ty: Type::NEVER,
3800            span,
3801        });
3802        Ok(AnalysisResult::new(air_ref, Type::NEVER))
3803    }
3804
3805    /// Analyze @field(value, field_name) intrinsic.
3806    ///
3807    /// Accesses a struct field by comptime-known name. The first argument is a
3808    /// runtime value of struct type, the second is a comptime_str naming the field.
3809    /// Resolves at compile time to a FieldGet instruction.
3810    fn analyze_field_intrinsic(
3811        &mut self,
3812        air: &mut Air,
3813        args: &[RirCallArg],
3814        span: Span,
3815        ctx: &mut AnalysisContext,
3816    ) -> CompileResult<AnalysisResult> {
3817        if args.len() != 2 {
3818            return Err(CompileError::new(
3819                ErrorKind::IntrinsicWrongArgCount {
3820                    name: "field".to_string(),
3821                    expected: 2,
3822                    found: args.len(),
3823                },
3824                span,
3825            ));
3826        }
3827
3828        // Arg 1: runtime value of struct type — analyze as a projection base
3829        // (does not mark the variable as moved, like regular field access)
3830        let value_result = self.analyze_inst_for_projection(air, args[0].value, ctx)?;
3831        let struct_ty = value_result.ty;
3832
3833        // Verify the value is a struct type
3834        let struct_id = match struct_ty.kind() {
3835            TypeKind::Struct(id) => id,
3836            _ => {
3837                return Err(CompileError::new(
3838                    ErrorKind::ComptimeEvaluationFailed {
3839                        reason: format!("@field requires a struct value, got {}", struct_ty.name()),
3840                    },
3841                    span,
3842                ));
3843            }
3844        };
3845
3846        // Arg 2: field name — evaluate at comptime to get a comptime_str.
3847        // Uses evaluate_comptime_expr (not evaluate_comptime_block) to preserve
3848        // the heap, which may contain data from a comptime_unroll iteration.
3849        let field_name_val = self.evaluate_comptime_expr(args[1].value, ctx, span)?;
3850        let field_name = match field_name_val {
3851            ConstValue::ComptimeStr(str_idx) => match &self.comptime_heap[str_idx as usize] {
3852                ComptimeHeapItem::String(s) => s.clone(),
3853                _ => {
3854                    return Err(CompileError::new(
3855                        ErrorKind::ComptimeEvaluationFailed {
3856                            reason: "@field second argument must be a comptime_str".to_string(),
3857                        },
3858                        span,
3859                    ));
3860                }
3861            },
3862            _ => {
3863                return Err(CompileError::new(
3864                    ErrorKind::ComptimeEvaluationFailed {
3865                        reason: "@field second argument must be a comptime_str".to_string(),
3866                    },
3867                    span,
3868                ));
3869            }
3870        };
3871
3872        // Resolve the field name to a field index
3873        let struct_def = self.type_pool.struct_def(struct_id);
3874        let (field_idx, field_def) = struct_def.find_field(&field_name).ok_or_else(|| {
3875            CompileError::new(
3876                ErrorKind::ComptimeEvaluationFailed {
3877                    reason: format!("struct '{}' has no field '{}'", struct_def.name, field_name),
3878                },
3879                span,
3880            )
3881        })?;
3882        let field_ty = field_def.ty;
3883
3884        // Emit a FieldGet instruction
3885        let air_ref = air.add_inst(AirInst {
3886            data: AirInstData::FieldGet {
3887                base: value_result.air_ref,
3888                struct_id,
3889                field_index: field_idx as u32,
3890            },
3891            ty: field_ty,
3892            span,
3893        });
3894        Ok(AnalysisResult::new(air_ref, field_ty))
3895    }
3896
3897    /// Analyze @read_line intrinsic.
3898    fn analyze_read_line_intrinsic(
3899        &mut self,
3900        air: &mut Air,
3901        name: Spur,
3902        args: &[RirCallArg],
3903        span: Span,
3904    ) -> CompileResult<AnalysisResult> {
3905        // @read_line() - reads a line from stdin and returns it as a String.
3906        // Takes no arguments, returns String.
3907        if !args.is_empty() {
3908            return Err(CompileError::new(
3909                ErrorKind::IntrinsicWrongArgCount {
3910                    name: "read_line".to_string(),
3911                    expected: 0,
3912                    found: args.len(),
3913                },
3914                span,
3915            ));
3916        }
3917
3918        // Get the String type
3919        let string_type = self.builtin_string_type();
3920
3921        // Create the intrinsic instruction that returns String
3922        let air_ref = air.add_inst(AirInst {
3923            data: AirInstData::Intrinsic {
3924                name,
3925                args_start: 0, // No args
3926                args_len: 0,
3927            },
3928            ty: string_type,
3929            span,
3930        });
3931        Ok(AnalysisResult::new(air_ref, string_type))
3932    }
3933
3934    /// Analyze @parse_i32, @parse_i64, @parse_u32, @parse_u64 intrinsics.
3935    fn analyze_parse_intrinsic(
3936        &mut self,
3937        air: &mut Air,
3938        name: Spur,
3939        intrinsic_name_str: &str,
3940        args: &[RirCallArg],
3941        span: Span,
3942        ctx: &mut AnalysisContext,
3943    ) -> CompileResult<AnalysisResult> {
3944        // Expects exactly one argument
3945        if args.len() != 1 {
3946            return Err(CompileError::new(
3947                ErrorKind::IntrinsicWrongArgCount {
3948                    name: intrinsic_name_str.to_string(),
3949                    expected: 1,
3950                    found: args.len(),
3951                },
3952                span,
3953            ));
3954        }
3955
3956        // Analyze the argument - String borrows are handled by
3957        // analyze_inst_for_projection to avoid consuming the String
3958        let arg_result = self.analyze_inst_for_projection(air, args[0].value, ctx)?;
3959        let arg_type = arg_result.ty;
3960
3961        // Argument must be a String
3962        if !self.is_builtin_string(arg_type) {
3963            return Err(CompileError::new(
3964                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
3965                    name: format!("@{}", intrinsic_name_str),
3966                    expected: "String".to_string(),
3967                    found: arg_type.name().to_string(),
3968                })),
3969                span,
3970            ));
3971        }
3972
3973        // Determine the return type based on the intrinsic name
3974        let return_type = match intrinsic_name_str {
3975            "parse_i32" => Type::I32,
3976            "parse_i64" => Type::I64,
3977            "parse_u32" => Type::U32,
3978            "parse_u64" => Type::U64,
3979            _ => unreachable!(),
3980        };
3981
3982        // Encode args into extra array
3983        let args_start = air.add_extra(&[arg_result.air_ref.as_u32()]);
3984        let air_ref = air.add_inst(AirInst {
3985            data: AirInstData::Intrinsic {
3986                name,
3987                args_start,
3988                args_len: 1,
3989            },
3990            ty: return_type,
3991            span,
3992        });
3993        Ok(AnalysisResult::new(air_ref, return_type))
3994    }
3995
3996    /// Analyze @random_u32 intrinsic.
3997    fn analyze_random_u32_intrinsic(
3998        &mut self,
3999        air: &mut Air,
4000        name: Spur,
4001        args: &[RirCallArg],
4002        span: Span,
4003    ) -> CompileResult<AnalysisResult> {
4004        // @random_u32() - takes no arguments, returns u32
4005        if !args.is_empty() {
4006            return Err(CompileError::new(
4007                ErrorKind::IntrinsicWrongArgCount {
4008                    name: "random_u32".to_string(),
4009                    expected: 0,
4010                    found: args.len(),
4011                },
4012                span,
4013            ));
4014        }
4015
4016        // Create the intrinsic instruction that returns u32
4017        let air_ref = air.add_inst(AirInst {
4018            data: AirInstData::Intrinsic {
4019                name,
4020                args_start: 0, // No args
4021                args_len: 0,
4022            },
4023            ty: Type::U32,
4024            span,
4025        });
4026        Ok(AnalysisResult::new(air_ref, Type::U32))
4027    }
4028
4029    /// Analyze @random_u64 intrinsic.
4030    fn analyze_random_u64_intrinsic(
4031        &mut self,
4032        air: &mut Air,
4033        name: Spur,
4034        args: &[RirCallArg],
4035        span: Span,
4036    ) -> CompileResult<AnalysisResult> {
4037        // @random_u64() - takes no arguments, returns u64
4038        if !args.is_empty() {
4039            return Err(CompileError::new(
4040                ErrorKind::IntrinsicWrongArgCount {
4041                    name: "random_u64".to_string(),
4042                    expected: 0,
4043                    found: args.len(),
4044                },
4045                span,
4046            ));
4047        }
4048
4049        // Create the intrinsic instruction that returns u64
4050        let air_ref = air.add_inst(AirInst {
4051            data: AirInstData::Intrinsic {
4052                name,
4053                args_start: 0, // No args
4054                args_len: 0,
4055            },
4056            ty: Type::U64,
4057            span,
4058        });
4059        Ok(AnalysisResult::new(air_ref, Type::U64))
4060    }
4061
4062    /// Analyze @import intrinsic.
4063    ///
4064    /// This requires the `modules` preview feature and takes a single string literal
4065    /// argument specifying the module path to import.
4066    fn analyze_import_intrinsic(
4067        &mut self,
4068        air: &mut Air,
4069        args: &[RirCallArg],
4070        span: Span,
4071    ) -> CompileResult<AnalysisResult> {
4072        // @import takes exactly one argument
4073        if args.len() != 1 {
4074            return Err(CompileError::new(
4075                ErrorKind::IntrinsicWrongArgCount {
4076                    name: "import".to_string(),
4077                    expected: 1,
4078                    found: args.len(),
4079                },
4080                span,
4081            ));
4082        }
4083
4084        // Get the argument instruction - it must be a string literal
4085        let arg_inst = self.rir.get(args[0].value);
4086        let import_path = match &arg_inst.data {
4087            gruel_rir::InstData::StringConst(path_spur) => {
4088                self.interner.resolve(path_spur).to_string()
4089            }
4090            _ => {
4091                return Err(CompileError::new(
4092                    ErrorKind::ImportRequiresStringLiteral,
4093                    arg_inst.span,
4094                ));
4095            }
4096        };
4097
4098        // Resolve the import path relative to the current source file
4099        // Resolution order (per ADR-0026):
4100        // 1. foo.gruel (simple file module)
4101        // 2. _foo.gruel with foo/ directory (directory module)
4102        // 3. (Future) Dependency from gruel.toml
4103        let resolved_path = self.resolve_import_path(&import_path, span)?;
4104
4105        // Get or create the module in the registry
4106        // The module will be populated lazily when member access is performed
4107        let (module_id, _is_new) = self
4108            .module_registry
4109            .get_or_create(import_path.clone(), resolved_path);
4110
4111        // Return a module type
4112        // AIR doesn't have a ModuleConst instruction, so we use UnitConst as a placeholder
4113        // The type is what matters for subsequent member access resolution
4114        let air_ref = air.add_inst(AirInst {
4115            data: AirInstData::UnitConst, // Placeholder - module values are compile-time only
4116            ty: Type::new_module(module_id),
4117            span,
4118        });
4119        Ok(AnalysisResult::new(air_ref, Type::new_module(module_id)))
4120    }
4121
4122    /// Resolve an import path to an absolute file path.
4123    ///
4124    /// Resolution order (per ADR-0026):
4125    /// 1. Standard library: `@import("std")` resolves to the bundled std library
4126    /// 2. Pre-loaded files (multi-file compilation)
4127    /// 3. `foo.gruel` (simple file module)
4128    /// 4. `_foo.gruel` with `foo/` directory (directory module)
4129    /// 5. (Future) Dependency from gruel.toml
4130    pub(crate) fn resolve_import_path(
4131        &self,
4132        import_path: &str,
4133        span: Span,
4134    ) -> CompileResult<String> {
4135        use std::path::Path;
4136
4137        // Phase 0: Check for standard library import
4138        // @import("std") resolves to the compiler's bundled standard library
4139        if import_path == "std" {
4140            return self.resolve_std_import(span);
4141        }
4142
4143        // Phase 1: Check if the import path matches an already-loaded file
4144        // This handles unit tests and multi-file compilation where all files are pre-loaded
4145        for path in self.file_paths.values() {
4146            // Check for exact match
4147            if path == import_path {
4148                return Ok(path.clone());
4149            }
4150            // Check if the file path ends with the import path (handles relative imports)
4151            if path.ends_with(import_path) {
4152                return Ok(path.clone());
4153            }
4154            // For imports like "math" or "math.gruel", check if the file is named accordingly
4155            let import_base = import_path.strip_suffix(".gruel").unwrap_or(import_path);
4156            let file_name = Path::new(path).file_stem().and_then(|s| s.to_str());
4157            if let Some(name) = file_name
4158                && name == import_base
4159            {
4160                return Ok(path.clone());
4161            }
4162        }
4163
4164        // Phase 2: Try to find the file on disk (for directory modules and actual file imports)
4165        // Get the directory of the current source file
4166        let source_path = self.get_source_path(span);
4167        let source_dir = source_path
4168            .and_then(|p| Path::new(p).parent())
4169            .unwrap_or(Path::new("."));
4170
4171        let mut candidates = Vec::new();
4172
4173        // Strip .gruel extension if present for base name calculation
4174        let base_name = import_path.strip_suffix(".gruel").unwrap_or(import_path);
4175
4176        // Resolution order:
4177        // 1. Try foo.gruel (simple file module)
4178        let file_candidate = source_dir.join(format!("{}.gruel", base_name));
4179        candidates.push(file_candidate.display().to_string());
4180        if file_candidate.exists() {
4181            return Ok(file_candidate.to_string_lossy().to_string());
4182        }
4183
4184        // 2. If the path already ends in .gruel, also try it directly
4185        if import_path.ends_with(".gruel") {
4186            let candidate = source_dir.join(import_path);
4187            if !candidates.contains(&candidate.display().to_string()) {
4188                candidates.push(candidate.display().to_string());
4189            }
4190            if candidate.exists() {
4191                return Ok(candidate.to_string_lossy().to_string());
4192            }
4193        }
4194
4195        // 3. Try _foo.gruel + foo/ directory (directory module)
4196        let dir_module_root = source_dir.join(format!("_{}.gruel", base_name));
4197        let dir_path = source_dir.join(base_name);
4198        candidates.push(format!("{} + {}/", dir_module_root.display(), base_name));
4199        if dir_module_root.exists() && dir_path.is_dir() {
4200            return Ok(dir_module_root.to_string_lossy().to_string());
4201        }
4202
4203        // 3b. Also try just _foo.gruel without requiring foo/ directory
4204        // (This allows directory modules where all submodules are re-exported)
4205        if dir_module_root.exists() {
4206            return Ok(dir_module_root.to_string_lossy().to_string());
4207        }
4208
4209        // Module not found - report error with candidates tried
4210        Err(CompileError::new(
4211            ErrorKind::ModuleNotFound {
4212                path: import_path.to_string(),
4213                candidates,
4214            },
4215            span,
4216        ))
4217    }
4218
4219    /// Resolve the standard library import.
4220    ///
4221    /// The standard library is located using the following resolution order:
4222    /// 1. `GRUEL_STD_PATH` environment variable (if set)
4223    /// 2. `std/` directory relative to the source file
4224    /// 3. Known installation paths
4225    ///
4226    /// Returns the path to `_std.gruel`, the standard library root module.
4227    fn resolve_std_import(&self, span: Span) -> CompileResult<String> {
4228        use std::path::Path;
4229
4230        // Check if we have a pre-loaded std library in file_paths
4231        for path in self.file_paths.values() {
4232            // Check for _std.gruel
4233            if path.ends_with("_std.gruel") || path.ends_with("std/_std.gruel") {
4234                return Ok(path.clone());
4235            }
4236        }
4237
4238        // 1. Check GRUEL_STD_PATH environment variable
4239        if let Ok(std_path) = std::env::var("GRUEL_STD_PATH") {
4240            let std_root = Path::new(&std_path).join("_std.gruel");
4241            if std_root.exists() {
4242                return Ok(std_root.to_string_lossy().to_string());
4243            }
4244        }
4245
4246        // 2. Look for std/ relative to the source file
4247        if let Some(source_path) = self.get_source_path(span) {
4248            let source_dir = Path::new(source_path).parent().unwrap_or(Path::new("."));
4249
4250            // Try std/_std.gruel relative to source
4251            let std_root = source_dir.join("std").join("_std.gruel");
4252            if std_root.exists() {
4253                return Ok(std_root.to_string_lossy().to_string());
4254            }
4255        }
4256
4257        // Note: We intentionally do NOT check the current working directory
4258        // because it's unreliable and may find the wrong std library.
4259        // Users should either:
4260        // 1. Set GRUEL_STD_PATH environment variable
4261        // 2. Have std/ in the same directory as their source files
4262        // 3. Use aux_files in tests to provide std
4263
4264        // Standard library not found
4265        Err(CompileError::new(ErrorKind::StdLibNotFound, span))
4266    }
4267
4268    // Note: The old analyze_inst body from here onwards is now handled by the
4269    // dispatcher above and the category methods in analyze_ops.rs
4270
4271    // ========================================================================
4272    // Helper methods for analysis
4273    // ========================================================================
4274
4275    /// Analyze a binary arithmetic operator (+, -, *, /, %).
4276    ///
4277    /// Follows Rust's type inference rules:
4278    /// Types are determined by HM inference. Both operands must have the same type.
4279    fn analyze_binary_arith<F>(
4280        &mut self,
4281        air: &mut Air,
4282        lhs: InstRef,
4283        rhs: InstRef,
4284        make_data: F,
4285        span: Span,
4286        ctx: &mut AnalysisContext,
4287    ) -> CompileResult<AnalysisResult>
4288    where
4289        F: FnOnce(AirRef, AirRef) -> AirInstData,
4290    {
4291        let lhs_result = self.analyze_inst(air, lhs, ctx)?;
4292        let rhs_result = self.analyze_inst(air, rhs, ctx)?;
4293
4294        // Verify the type is numeric (HM should have enforced this, but check anyway)
4295        if !lhs_result.ty.is_numeric() && !lhs_result.ty.is_error() && !lhs_result.ty.is_never() {
4296            return Err(CompileError::new(
4297                ErrorKind::TypeMismatch {
4298                    expected: "numeric type".to_string(),
4299                    found: lhs_result.ty.name().to_string(),
4300                },
4301                span,
4302            ));
4303        }
4304
4305        let air_ref = air.add_inst(AirInst {
4306            data: make_data(lhs_result.air_ref, rhs_result.air_ref),
4307            ty: lhs_result.ty,
4308            span,
4309        });
4310        Ok(AnalysisResult::new(air_ref, lhs_result.ty))
4311    }
4312
4313    /// Analyze a comparison operator.
4314    ///
4315    /// Types are determined by HM inference. Both operands must have the same type.
4316    ///
4317    /// For equality operators (`==`, `!=`), both integers and booleans are allowed.
4318    /// For ordering operators (`<`, `>`, `<=`, `>=`), only integers are allowed.
4319    fn analyze_comparison<F>(
4320        &mut self,
4321        air: &mut Air,
4322        (lhs, rhs): (InstRef, InstRef),
4323        allow_bool: bool,
4324        make_data: F,
4325        span: Span,
4326        ctx: &mut AnalysisContext,
4327    ) -> CompileResult<AnalysisResult>
4328    where
4329        F: FnOnce(AirRef, AirRef) -> AirInstData,
4330    {
4331        // Check for chained comparisons (e.g., `a < b < c`)
4332        // Since the parser is left-associative, `a < b < c` parses as `(a < b) < c`,
4333        // so we only need to check if the LHS is a comparison.
4334        if self.is_comparison(lhs) {
4335            return Err(CompileError::new(ErrorKind::ChainedComparison, span)
4336                .with_help("use `&&` to combine comparisons: `a < b && b < c`"));
4337        }
4338
4339        // Comparisons read values without consuming them (like projections).
4340        // This matches Rust's PartialEq trait which takes references.
4341        let lhs_result = self.analyze_inst_for_projection(air, lhs, ctx)?;
4342        let rhs_result = self.analyze_inst_for_projection(air, rhs, ctx)?;
4343        let lhs_type = lhs_result.ty;
4344
4345        // Propagate Never/Error without additional type errors
4346        if lhs_type.is_never() || lhs_type.is_error() {
4347            let air_ref = air.add_inst(AirInst {
4348                data: make_data(lhs_result.air_ref, rhs_result.air_ref),
4349                ty: Type::BOOL,
4350                span,
4351            });
4352            return Ok(AnalysisResult::new(air_ref, Type::BOOL));
4353        }
4354
4355        // Validate the type is appropriate for this comparison
4356        if allow_bool {
4357            // Equality operators (==, !=) work on integers, floats, booleans, strings, unit, and structs
4358            // Note: String is now a struct, so is_struct() covers it
4359            if !lhs_type.is_numeric()
4360                && lhs_type != Type::BOOL
4361                && lhs_type != Type::UNIT
4362                && !lhs_type.is_struct()
4363                && !self.is_builtin_string(lhs_type)
4364            {
4365                return Err(CompileError::new(
4366                    ErrorKind::TypeMismatch {
4367                        expected: "numeric, bool, string, unit, or struct".to_string(),
4368                        found: lhs_type.name().to_string(),
4369                    },
4370                    self.rir.get(lhs).span,
4371                ));
4372            }
4373        } else if !lhs_type.is_numeric() && !self.is_builtin_string(lhs_type) {
4374            return Err(CompileError::new(
4375                ErrorKind::TypeMismatch {
4376                    expected: "numeric or string".to_string(),
4377                    found: lhs_type.name().to_string(),
4378                },
4379                self.rir.get(lhs).span,
4380            ));
4381        }
4382
4383        let air_ref = air.add_inst(AirInst {
4384            data: make_data(lhs_result.air_ref, rhs_result.air_ref),
4385            ty: Type::BOOL,
4386            span,
4387        });
4388        Ok(AnalysisResult::new(air_ref, Type::BOOL))
4389    }
4390
4391    /// Try to evaluate an RIR expression as a compile-time constant.
4392    ///
4393    /// Returns `Some(value)` if the expression can be fully evaluated at compile time,
4394    /// or `None` if evaluation requires runtime information (e.g., variable values,
4395    /// function calls) or would cause overflow/panic.
4396    ///
4397    /// This is the foundation for compile-time bounds checking and can be extended
4398    /// for future `comptime` features.
4399    pub(crate) fn try_evaluate_const(&mut self, inst_ref: InstRef) -> Option<ConstValue> {
4400        let inst = self.rir.get(inst_ref);
4401        match &inst.data {
4402            // Integer literals
4403            InstData::IntConst(value) => i64::try_from(*value).ok().map(ConstValue::Integer),
4404
4405            // Boolean literals
4406            InstData::BoolConst(value) => Some(ConstValue::Bool(*value)),
4407
4408            // Unary negation: -expr
4409            InstData::Neg { operand } => match self.try_evaluate_const(*operand)? {
4410                ConstValue::Integer(n) => n.checked_neg().map(ConstValue::Integer),
4411                _ => None,
4412            },
4413
4414            // Logical NOT: !expr
4415            InstData::Not { operand } => match self.try_evaluate_const(*operand)? {
4416                ConstValue::Bool(b) => Some(ConstValue::Bool(!b)),
4417                _ => None,
4418            },
4419
4420            // Binary arithmetic operations
4421            InstData::Add { lhs, rhs } => {
4422                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4423                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4424                l.checked_add(r).map(ConstValue::Integer)
4425            }
4426            InstData::Sub { lhs, rhs } => {
4427                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4428                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4429                l.checked_sub(r).map(ConstValue::Integer)
4430            }
4431            InstData::Mul { lhs, rhs } => {
4432                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4433                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4434                l.checked_mul(r).map(ConstValue::Integer)
4435            }
4436            InstData::Div { lhs, rhs } => {
4437                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4438                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4439                if r == 0 {
4440                    None // Division by zero - defer to runtime
4441                } else {
4442                    l.checked_div(r).map(ConstValue::Integer)
4443                }
4444            }
4445            InstData::Mod { lhs, rhs } => {
4446                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4447                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4448                if r == 0 {
4449                    None // Modulo by zero - defer to runtime
4450                } else {
4451                    l.checked_rem(r).map(ConstValue::Integer)
4452                }
4453            }
4454
4455            // Comparison operations
4456            InstData::Eq { lhs, rhs } => {
4457                let l = self.try_evaluate_const(*lhs)?;
4458                let r = self.try_evaluate_const(*rhs)?;
4459                match (l, r) {
4460                    (ConstValue::Integer(a), ConstValue::Integer(b)) => {
4461                        Some(ConstValue::Bool(a == b))
4462                    }
4463                    (ConstValue::Bool(a), ConstValue::Bool(b)) => Some(ConstValue::Bool(a == b)),
4464                    _ => None, // Mixed types
4465                }
4466            }
4467            InstData::Ne { lhs, rhs } => {
4468                let l = self.try_evaluate_const(*lhs)?;
4469                let r = self.try_evaluate_const(*rhs)?;
4470                match (l, r) {
4471                    (ConstValue::Integer(a), ConstValue::Integer(b)) => {
4472                        Some(ConstValue::Bool(a != b))
4473                    }
4474                    (ConstValue::Bool(a), ConstValue::Bool(b)) => Some(ConstValue::Bool(a != b)),
4475                    _ => None,
4476                }
4477            }
4478            InstData::Lt { lhs, rhs } => {
4479                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4480                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4481                Some(ConstValue::Bool(l < r))
4482            }
4483            InstData::Gt { lhs, rhs } => {
4484                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4485                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4486                Some(ConstValue::Bool(l > r))
4487            }
4488            InstData::Le { lhs, rhs } => {
4489                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4490                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4491                Some(ConstValue::Bool(l <= r))
4492            }
4493            InstData::Ge { lhs, rhs } => {
4494                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4495                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4496                Some(ConstValue::Bool(l >= r))
4497            }
4498
4499            // Logical operations
4500            InstData::And { lhs, rhs } => {
4501                let l = self.try_evaluate_const(*lhs)?.as_bool()?;
4502                let r = self.try_evaluate_const(*rhs)?.as_bool()?;
4503                Some(ConstValue::Bool(l && r))
4504            }
4505            InstData::Or { lhs, rhs } => {
4506                let l = self.try_evaluate_const(*lhs)?.as_bool()?;
4507                let r = self.try_evaluate_const(*rhs)?.as_bool()?;
4508                Some(ConstValue::Bool(l || r))
4509            }
4510
4511            // Bitwise operations
4512            InstData::BitAnd { lhs, rhs } => {
4513                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4514                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4515                Some(ConstValue::Integer(l & r))
4516            }
4517            InstData::BitOr { lhs, rhs } => {
4518                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4519                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4520                Some(ConstValue::Integer(l | r))
4521            }
4522            InstData::BitXor { lhs, rhs } => {
4523                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4524                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4525                Some(ConstValue::Integer(l ^ r))
4526            }
4527            InstData::Shl { lhs, rhs } => {
4528                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4529                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4530                // Only constant-fold small shift amounts to avoid type-width issues.
4531                // For shifts >= 8, defer to runtime where hardware handles masking correctly.
4532                // This is conservative but safe - we don't know the operand type here.
4533                if !(0..8).contains(&r) {
4534                    return None;
4535                }
4536                Some(ConstValue::Integer(l << r))
4537            }
4538            InstData::Shr { lhs, rhs } => {
4539                let l = self.try_evaluate_const(*lhs)?.as_integer()?;
4540                let r = self.try_evaluate_const(*rhs)?.as_integer()?;
4541                // Only constant-fold small shift amounts to avoid type-width issues.
4542                // For shifts >= 8, defer to runtime where hardware handles masking correctly.
4543                if !(0..8).contains(&r) {
4544                    return None;
4545                }
4546                Some(ConstValue::Integer(l >> r))
4547            }
4548            InstData::BitNot { operand } => {
4549                let n = self.try_evaluate_const(*operand)?.as_integer()?;
4550                Some(ConstValue::Integer(!n))
4551            }
4552
4553            // Comptime block: comptime { expr } is compile-time evaluable if its inner expr is
4554            InstData::Comptime { expr } => self.try_evaluate_const(*expr),
4555
4556            // Block: evaluate the result expression (last expression in the block)
4557            InstData::Block { extra_start, len } => {
4558                // A block is comptime-evaluable if it has a single instruction
4559                // (which is the result expression) OR if all statements are
4560                // side-effect-free and the result is comptime-evaluable.
4561                // For now, only handle the single-instruction case (common for
4562                // simple type-returning functions like `fn make_type() -> type { i32 }`).
4563                if *len == 1 {
4564                    let inst_refs = self.rir.get_extra(*extra_start, *len);
4565                    let result_ref = InstRef::from_raw(inst_refs[0]);
4566                    self.try_evaluate_const(result_ref)
4567                } else {
4568                    None // Blocks with multiple instructions need full interpreter support
4569                }
4570            }
4571
4572            // Anonymous struct type: evaluate to a comptime type value
4573            InstData::AnonStructType {
4574                fields_start,
4575                fields_len,
4576                methods_start,
4577                methods_len,
4578            } => {
4579                // Get the field declarations from the RIR
4580                let field_decls = self.rir.get_field_decls(*fields_start, *fields_len);
4581
4582                // Resolve each field type and build the struct fields
4583                let mut struct_fields = Vec::with_capacity(field_decls.len());
4584                for (name_sym, type_sym) in field_decls {
4585                    let name_str = self.interner.resolve(&name_sym).to_string();
4586                    // Try to resolve the type - for anonymous structs in comptime context,
4587                    // we need to be able to resolve the field types
4588                    let field_ty = self.resolve_type_for_comptime(type_sym)?;
4589                    struct_fields.push(StructField {
4590                        name: name_str,
4591                        ty: field_ty,
4592                    });
4593                }
4594
4595                // Extract method signatures for structural equality comparison
4596                let method_sigs = self.extract_anon_method_sigs(*methods_start, *methods_len);
4597
4598                // Find or create the anonymous struct type
4599                let (struct_ty, is_new) =
4600                    self.find_or_create_anon_struct(&struct_fields, &method_sigs, &HashMap::new());
4601
4602                // Register methods if present and struct is new
4603                // This handles non-comptime functions like `fn Counter() -> type { struct { fn get() {} } }`
4604                // For comptime functions with captured values, use try_evaluate_const_with_subst instead
4605                if is_new && *methods_len > 0 {
4606                    let struct_id = struct_ty.as_struct()?;
4607                    // Use comptime-safe method registration (no type subst, no value subst for non-comptime)
4608                    self.register_anon_struct_methods_for_comptime_with_subst(
4609                        AnonStructSpec {
4610                            struct_id,
4611                            struct_type: struct_ty,
4612                            methods_start: *methods_start,
4613                            methods_len: *methods_len,
4614                        },
4615                        inst.span,
4616                        &HashMap::new(), // Empty type substitution
4617                        &HashMap::new(), // Empty value substitution (non-comptime)
4618                    )?;
4619                }
4620                Some(ConstValue::Type(struct_ty))
4621            }
4622
4623            // Anonymous enum type: evaluate to a comptime type value
4624            InstData::AnonEnumType {
4625                variants_start,
4626                variants_len,
4627                methods_start,
4628                methods_len,
4629            } => {
4630                let variant_decls = self
4631                    .rir
4632                    .get_enum_variant_decls(*variants_start, *variants_len);
4633
4634                let mut enum_variants = Vec::with_capacity(variant_decls.len());
4635                for (name_sym, field_type_syms, field_name_syms) in &variant_decls {
4636                    let name_str = self.interner.resolve(name_sym).to_string();
4637                    let mut fields = Vec::with_capacity(field_type_syms.len());
4638                    for ty_sym in field_type_syms {
4639                        let field_ty = self.resolve_type_for_comptime(*ty_sym)?;
4640                        fields.push(field_ty);
4641                    }
4642                    let field_names: Vec<String> = field_name_syms
4643                        .iter()
4644                        .map(|s| self.interner.resolve(s).to_string())
4645                        .collect();
4646                    enum_variants.push(EnumVariantDef {
4647                        name: name_str,
4648                        fields,
4649                        field_names,
4650                    });
4651                }
4652
4653                let method_sigs = self.extract_anon_method_sigs(*methods_start, *methods_len);
4654
4655                let (enum_ty, is_new) =
4656                    self.find_or_create_anon_enum(&enum_variants, &method_sigs, &HashMap::new());
4657
4658                // Register methods for newly created anonymous enums
4659                if is_new
4660                    && *methods_len > 0
4661                    && let TypeKind::Enum(enum_id) = enum_ty.kind()
4662                {
4663                    self.register_anon_enum_methods_for_comptime_with_subst(
4664                        enum_id,
4665                        enum_ty,
4666                        *methods_start,
4667                        *methods_len,
4668                        &HashMap::new(),
4669                    );
4670                }
4671
4672                Some(ConstValue::Type(enum_ty))
4673            }
4674
4675            // TypeConst: a type used as a value (e.g., `i32` in `identity(i32, 42)`)
4676            InstData::TypeConst { type_name } => {
4677                let type_name_str = self.interner.resolve(type_name);
4678                let ty = match type_name_str {
4679                    "i8" => Type::I8,
4680                    "i16" => Type::I16,
4681                    "i32" => Type::I32,
4682                    "i64" => Type::I64,
4683                    "u8" => Type::U8,
4684                    "u16" => Type::U16,
4685                    "u32" => Type::U32,
4686                    "u64" => Type::U64,
4687                    "bool" => Type::BOOL,
4688                    "()" => Type::UNIT,
4689                    "!" => Type::NEVER,
4690                    _ => {
4691                        // Check for struct types
4692                        if let Some(&struct_id) = self.structs.get(type_name) {
4693                            Type::new_struct(struct_id)
4694                        } else if let Some(&enum_id) = self.enums.get(type_name) {
4695                            Type::new_enum(enum_id)
4696                        } else {
4697                            return None; // Unknown type
4698                        }
4699                    }
4700                };
4701                Some(ConstValue::Type(ty))
4702            }
4703
4704            // VarRef: when a variable reference is actually a type name (e.g., `Point` in `fn make_type() -> type { Point }`)
4705            InstData::VarRef { name } => {
4706                // Try to resolve as a type - if it's a type name, return the type
4707                let name_str = self.interner.resolve(name);
4708                let ty = match name_str {
4709                    "i8" => Type::I8,
4710                    "i16" => Type::I16,
4711                    "i32" => Type::I32,
4712                    "i64" => Type::I64,
4713                    "u8" => Type::U8,
4714                    "u16" => Type::U16,
4715                    "u32" => Type::U32,
4716                    "u64" => Type::U64,
4717                    "bool" => Type::BOOL,
4718                    "()" => Type::UNIT,
4719                    "!" => Type::NEVER,
4720                    _ => {
4721                        // Check for struct types
4722                        if let Some(&struct_id) = self.structs.get(name) {
4723                            Type::new_struct(struct_id)
4724                        } else if let Some(&enum_id) = self.enums.get(name) {
4725                            Type::new_enum(enum_id)
4726                        } else {
4727                            return None; // Not a type name - can't evaluate at compile time
4728                        }
4729                    }
4730                };
4731                Some(ConstValue::Type(ty))
4732            }
4733
4734            // Everything else requires runtime evaluation
4735            _ => None,
4736        }
4737    }
4738
4739    /// Try to extract a constant integer value from an RIR index expression.
4740    ///
4741    /// This is used for compile-time bounds checking. Returns `Some(value)` if
4742    /// the index can be evaluated to an integer constant at compile time.
4743    pub(crate) fn try_get_const_index(&mut self, inst_ref: InstRef) -> Option<i64> {
4744        self.try_evaluate_const(inst_ref)?.as_integer()
4745    }
4746
4747    /// Try to evaluate an RIR instruction to a compile-time constant value with type substitution.
4748    ///
4749    /// This is used when evaluating generic functions that return `type`. For example,
4750    /// when calling `fn Pair(comptime T: type) -> type { struct { first: T, second: T } }`
4751    /// with `Pair(i32)`, we need to substitute `T -> i32` when evaluating the body.
4752    ///
4753    /// The `type_subst` map contains mappings from type parameter names to concrete types.
4754    pub(crate) fn try_evaluate_const_with_subst(
4755        &mut self,
4756        inst_ref: InstRef,
4757        type_subst: &std::collections::HashMap<Spur, Type>,
4758        value_subst: &std::collections::HashMap<Spur, ConstValue>,
4759    ) -> Option<ConstValue> {
4760        let inst = self.rir.get(inst_ref);
4761        match &inst.data {
4762            // Integer literals
4763            InstData::IntConst(value) => i64::try_from(*value).ok().map(ConstValue::Integer),
4764
4765            // Boolean literals
4766            InstData::BoolConst(value) => Some(ConstValue::Bool(*value)),
4767
4768            // Unary negation: -expr
4769            InstData::Neg { operand } => {
4770                match self.try_evaluate_const_with_subst(*operand, type_subst, value_subst)? {
4771                    ConstValue::Integer(n) => n.checked_neg().map(ConstValue::Integer),
4772                    _ => None,
4773                }
4774            }
4775
4776            // Logical NOT: !expr
4777            InstData::Not { operand } => {
4778                match self.try_evaluate_const_with_subst(*operand, type_subst, value_subst)? {
4779                    ConstValue::Bool(b) => Some(ConstValue::Bool(!b)),
4780                    _ => None,
4781                }
4782            }
4783
4784            // Binary arithmetic operations
4785            InstData::Add { lhs, rhs } => {
4786                let l = self
4787                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4788                    .as_integer()?;
4789                let r = self
4790                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4791                    .as_integer()?;
4792                l.checked_add(r).map(ConstValue::Integer)
4793            }
4794            InstData::Sub { lhs, rhs } => {
4795                let l = self
4796                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4797                    .as_integer()?;
4798                let r = self
4799                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4800                    .as_integer()?;
4801                l.checked_sub(r).map(ConstValue::Integer)
4802            }
4803            InstData::Mul { lhs, rhs } => {
4804                let l = self
4805                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4806                    .as_integer()?;
4807                let r = self
4808                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4809                    .as_integer()?;
4810                l.checked_mul(r).map(ConstValue::Integer)
4811            }
4812            InstData::Div { lhs, rhs } => {
4813                let l = self
4814                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4815                    .as_integer()?;
4816                let r = self
4817                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4818                    .as_integer()?;
4819                if r == 0 {
4820                    None
4821                } else {
4822                    l.checked_div(r).map(ConstValue::Integer)
4823                }
4824            }
4825            InstData::Mod { lhs, rhs } => {
4826                let l = self
4827                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4828                    .as_integer()?;
4829                let r = self
4830                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4831                    .as_integer()?;
4832                if r == 0 {
4833                    None
4834                } else {
4835                    l.checked_rem(r).map(ConstValue::Integer)
4836                }
4837            }
4838
4839            // Comparison operations
4840            InstData::Eq { lhs, rhs } => {
4841                let l = self.try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?;
4842                let r = self.try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?;
4843                match (l, r) {
4844                    (ConstValue::Integer(a), ConstValue::Integer(b)) => {
4845                        Some(ConstValue::Bool(a == b))
4846                    }
4847                    (ConstValue::Bool(a), ConstValue::Bool(b)) => Some(ConstValue::Bool(a == b)),
4848                    _ => None,
4849                }
4850            }
4851            InstData::Ne { lhs, rhs } => {
4852                let l = self.try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?;
4853                let r = self.try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?;
4854                match (l, r) {
4855                    (ConstValue::Integer(a), ConstValue::Integer(b)) => {
4856                        Some(ConstValue::Bool(a != b))
4857                    }
4858                    (ConstValue::Bool(a), ConstValue::Bool(b)) => Some(ConstValue::Bool(a != b)),
4859                    _ => None,
4860                }
4861            }
4862            InstData::Lt { lhs, rhs } => {
4863                let l = self
4864                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4865                    .as_integer()?;
4866                let r = self
4867                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4868                    .as_integer()?;
4869                Some(ConstValue::Bool(l < r))
4870            }
4871            InstData::Gt { lhs, rhs } => {
4872                let l = self
4873                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4874                    .as_integer()?;
4875                let r = self
4876                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4877                    .as_integer()?;
4878                Some(ConstValue::Bool(l > r))
4879            }
4880            InstData::Le { lhs, rhs } => {
4881                let l = self
4882                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4883                    .as_integer()?;
4884                let r = self
4885                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4886                    .as_integer()?;
4887                Some(ConstValue::Bool(l <= r))
4888            }
4889            InstData::Ge { lhs, rhs } => {
4890                let l = self
4891                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4892                    .as_integer()?;
4893                let r = self
4894                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4895                    .as_integer()?;
4896                Some(ConstValue::Bool(l >= r))
4897            }
4898
4899            // Logical operations
4900            InstData::And { lhs, rhs } => {
4901                let l = self
4902                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4903                    .as_bool()?;
4904                let r = self
4905                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4906                    .as_bool()?;
4907                Some(ConstValue::Bool(l && r))
4908            }
4909            InstData::Or { lhs, rhs } => {
4910                let l = self
4911                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4912                    .as_bool()?;
4913                let r = self
4914                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4915                    .as_bool()?;
4916                Some(ConstValue::Bool(l || r))
4917            }
4918
4919            // Bitwise operations
4920            InstData::BitAnd { lhs, rhs } => {
4921                let l = self
4922                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4923                    .as_integer()?;
4924                let r = self
4925                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4926                    .as_integer()?;
4927                Some(ConstValue::Integer(l & r))
4928            }
4929            InstData::BitOr { lhs, rhs } => {
4930                let l = self
4931                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4932                    .as_integer()?;
4933                let r = self
4934                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4935                    .as_integer()?;
4936                Some(ConstValue::Integer(l | r))
4937            }
4938            InstData::BitXor { lhs, rhs } => {
4939                let l = self
4940                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4941                    .as_integer()?;
4942                let r = self
4943                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4944                    .as_integer()?;
4945                Some(ConstValue::Integer(l ^ r))
4946            }
4947            InstData::Shl { lhs, rhs } => {
4948                let l = self
4949                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4950                    .as_integer()?;
4951                let r = self
4952                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4953                    .as_integer()?;
4954                if !(0..8).contains(&r) {
4955                    return None;
4956                }
4957                Some(ConstValue::Integer(l << r))
4958            }
4959            InstData::Shr { lhs, rhs } => {
4960                let l = self
4961                    .try_evaluate_const_with_subst(*lhs, type_subst, value_subst)?
4962                    .as_integer()?;
4963                let r = self
4964                    .try_evaluate_const_with_subst(*rhs, type_subst, value_subst)?
4965                    .as_integer()?;
4966                if !(0..8).contains(&r) {
4967                    return None;
4968                }
4969                Some(ConstValue::Integer(l >> r))
4970            }
4971            InstData::BitNot { operand } => {
4972                let n = self
4973                    .try_evaluate_const_with_subst(*operand, type_subst, value_subst)?
4974                    .as_integer()?;
4975                Some(ConstValue::Integer(!n))
4976            }
4977
4978            // Comptime block: comptime { expr } is compile-time evaluable if its inner expr is
4979            InstData::Comptime { expr } => {
4980                self.try_evaluate_const_with_subst(*expr, type_subst, value_subst)
4981            }
4982
4983            // Block: evaluate the result expression (last expression in the block)
4984            InstData::Block { extra_start, len } => {
4985                if *len == 1 {
4986                    let inst_refs = self.rir.get_extra(*extra_start, *len);
4987                    let result_ref = InstRef::from_raw(inst_refs[0]);
4988                    self.try_evaluate_const_with_subst(result_ref, type_subst, value_subst)
4989                } else {
4990                    None
4991                }
4992            }
4993
4994            // Anonymous struct type: evaluate to a comptime type value with substitution
4995            InstData::AnonStructType {
4996                fields_start,
4997                fields_len,
4998                methods_start,
4999                methods_len,
5000            } => {
5001                let field_decls = self.rir.get_field_decls(*fields_start, *fields_len);
5002
5003                let mut struct_fields = Vec::with_capacity(field_decls.len());
5004                for (name_sym, type_sym) in field_decls {
5005                    let name_str = self.interner.resolve(&name_sym).to_string();
5006                    // Use the substitution-aware type resolution
5007                    let field_ty =
5008                        self.resolve_type_for_comptime_with_subst(type_sym, type_subst)?;
5009                    struct_fields.push(StructField {
5010                        name: name_str,
5011                        ty: field_ty,
5012                    });
5013                }
5014
5015                // Extract method signatures for structural equality comparison
5016                let method_sigs = self.extract_anon_method_sigs(*methods_start, *methods_len);
5017
5018                let (struct_ty, _is_new) =
5019                    self.find_or_create_anon_struct(&struct_fields, &method_sigs, value_subst);
5020
5021                // Register methods if present (requires preview feature)
5022                // Register if either:
5023                // 1. This is a newly created struct (is_new=true), OR
5024                // 2. The struct exists but has no methods registered yet
5025                if *methods_len > 0 {
5026                    let struct_id = struct_ty.as_struct()?;
5027
5028                    // Check if methods are already registered for this struct
5029                    let method_refs = self.rir.get_inst_refs(*methods_start, *methods_len);
5030                    let first_method_ref = method_refs[0];
5031                    let first_method_inst = self.rir.get(first_method_ref);
5032                    if let InstData::FnDecl {
5033                        name: method_name, ..
5034                    } = &first_method_inst.data
5035                    {
5036                        let needs_registration =
5037                            !self.methods.contains_key(&(struct_id, *method_name));
5038
5039                        if needs_registration {
5040                            // Use comptime-safe method registration with type substitution
5041                            self.register_anon_struct_methods_for_comptime_with_subst(
5042                                AnonStructSpec {
5043                                    struct_id,
5044                                    struct_type: struct_ty,
5045                                    methods_start: *methods_start,
5046                                    methods_len: *methods_len,
5047                                },
5048                                inst.span,
5049                                type_subst,
5050                                value_subst,
5051                            )?;
5052                        }
5053                    }
5054                }
5055                Some(ConstValue::Type(struct_ty))
5056            }
5057
5058            // Anonymous enum type: evaluate to a comptime type value with substitution
5059            InstData::AnonEnumType {
5060                variants_start,
5061                variants_len,
5062                methods_start,
5063                methods_len,
5064            } => {
5065                let variant_decls = self
5066                    .rir
5067                    .get_enum_variant_decls(*variants_start, *variants_len);
5068
5069                let mut enum_variants = Vec::with_capacity(variant_decls.len());
5070                for (name_sym, field_type_syms, field_name_syms) in &variant_decls {
5071                    let name_str = self.interner.resolve(name_sym).to_string();
5072                    let mut fields = Vec::with_capacity(field_type_syms.len());
5073                    for ty_sym in field_type_syms {
5074                        let field_ty =
5075                            self.resolve_type_for_comptime_with_subst(*ty_sym, type_subst)?;
5076                        fields.push(field_ty);
5077                    }
5078                    let field_names: Vec<String> = field_name_syms
5079                        .iter()
5080                        .map(|s| self.interner.resolve(s).to_string())
5081                        .collect();
5082                    enum_variants.push(EnumVariantDef {
5083                        name: name_str,
5084                        fields,
5085                        field_names,
5086                    });
5087                }
5088
5089                let method_sigs = self.extract_anon_method_sigs(*methods_start, *methods_len);
5090
5091                let (enum_ty, is_new) =
5092                    self.find_or_create_anon_enum(&enum_variants, &method_sigs, value_subst);
5093
5094                // Register methods for newly created anonymous enums with captured values
5095                if is_new
5096                    && *methods_len > 0
5097                    && let TypeKind::Enum(enum_id) = enum_ty.kind()
5098                {
5099                    self.register_anon_enum_methods_for_comptime_with_subst(
5100                        enum_id,
5101                        enum_ty,
5102                        *methods_start,
5103                        *methods_len,
5104                        type_subst,
5105                    );
5106                }
5107
5108                Some(ConstValue::Type(enum_ty))
5109            }
5110
5111            // TypeConst: a type used as a value
5112            InstData::TypeConst { type_name } => {
5113                // First check the substitution map
5114                if let Some(&ty) = type_subst.get(type_name) {
5115                    return Some(ConstValue::Type(ty));
5116                }
5117
5118                let type_name_str = self.interner.resolve(type_name);
5119                let ty = match type_name_str {
5120                    "i8" => Type::I8,
5121                    "i16" => Type::I16,
5122                    "i32" => Type::I32,
5123                    "i64" => Type::I64,
5124                    "u8" => Type::U8,
5125                    "u16" => Type::U16,
5126                    "u32" => Type::U32,
5127                    "u64" => Type::U64,
5128                    "bool" => Type::BOOL,
5129                    "()" => Type::UNIT,
5130                    "!" => Type::NEVER,
5131                    _ => {
5132                        if let Some(&struct_id) = self.structs.get(type_name) {
5133                            Type::new_struct(struct_id)
5134                        } else if let Some(&enum_id) = self.enums.get(type_name) {
5135                            Type::new_enum(enum_id)
5136                        } else {
5137                            return None;
5138                        }
5139                    }
5140                };
5141                Some(ConstValue::Type(ty))
5142            }
5143
5144            // VarRef: check substitution maps first, then try as a type name
5145            InstData::VarRef { name } => {
5146                // Check if this is a type parameter in the type substitution map
5147                if let Some(&ty) = type_subst.get(name) {
5148                    return Some(ConstValue::Type(ty));
5149                }
5150
5151                // Check if this is a comptime value variable in the value substitution map
5152                if let Some(value) = value_subst.get(name) {
5153                    return Some(*value);
5154                }
5155
5156                // Try to resolve as a type name
5157                let name_str = self.interner.resolve(name);
5158                let ty = match name_str {
5159                    "i8" => Type::I8,
5160                    "i16" => Type::I16,
5161                    "i32" => Type::I32,
5162                    "i64" => Type::I64,
5163                    "u8" => Type::U8,
5164                    "u16" => Type::U16,
5165                    "u32" => Type::U32,
5166                    "u64" => Type::U64,
5167                    "bool" => Type::BOOL,
5168                    "()" => Type::UNIT,
5169                    "!" => Type::NEVER,
5170                    _ => {
5171                        if let Some(&struct_id) = self.structs.get(name) {
5172                            Type::new_struct(struct_id)
5173                        } else if let Some(&enum_id) = self.enums.get(name) {
5174                            Type::new_enum(enum_id)
5175                        } else {
5176                            return None;
5177                        }
5178                    }
5179                };
5180                Some(ConstValue::Type(ty))
5181            }
5182
5183            // Everything else requires runtime evaluation
5184            _ => None,
5185        }
5186    }
5187
5188    // =========================================================================
5189    // Phase 1a: Stateful Comptime Interpreter
5190    // =========================================================================
5191    //
5192    // The interpreter extends `try_evaluate_const` with mutable local variable
5193    // state, enabling:
5194    //   - Multi-statement `comptime { ... }` blocks
5195    //   - `let` bindings within comptime blocks
5196    //   - Variable assignment within comptime blocks
5197    //   - `if`/`else` with comptime-evaluable conditions
5198    //   - `while`, `loop`, `break`, `continue` (Phase 1b)
5199    //   - Function calls, push/pop call frames (Phase 1c)
5200    //   - `ConstValue::Struct`, `ConstValue::Array` (Phase 1d)
5201    //   - Comptime arg evaluation via full interpreter (Phase 1e)
5202
5203    /// Try to evaluate a single expression as a comptime argument value.
5204    ///
5205    /// Used when validating and extracting values for `comptime` parameters at
5206    /// call sites (Phase 1e). First tries the lightweight non-stateful evaluator
5207    /// (fast for literals and arithmetic), then falls back to the full stateful
5208    /// interpreter which supports function calls and composite operations.
5209    ///
5210    /// Returns `Some(value)` if evaluable at compile time, `None` otherwise.
5211    /// Never returns control-flow signals (`BreakSignal`, `ContinueSignal`,
5212    /// `ReturnSignal`).
5213    pub(crate) fn try_evaluate_comptime_arg(
5214        &mut self,
5215        inst_ref: InstRef,
5216        ctx: &AnalysisContext,
5217        outer_span: Span,
5218    ) -> Option<ConstValue> {
5219        // Fast path: lightweight evaluator handles literals and arithmetic.
5220        if let Some(val) = self.try_evaluate_const(inst_ref) {
5221            return Some(val);
5222        }
5223        // Full stateful interpreter: supports function calls, let bindings, etc.
5224        // Save and restore step counter so arg evaluation doesn't consume the
5225        // budget of any outer comptime block that may be in progress.
5226        let prev_steps = self.comptime_steps_used;
5227        self.comptime_steps_used = 0;
5228        let mut locals = ctx.comptime_value_vars.clone();
5229        let result = self
5230            .evaluate_comptime_inst(inst_ref, &mut locals, ctx, outer_span)
5231            .ok();
5232        self.comptime_steps_used = prev_steps;
5233        // Filter out control-flow signals — they cannot be meaningful here.
5234        result.filter(|v| {
5235            !matches!(
5236                v,
5237                ConstValue::BreakSignal | ConstValue::ContinueSignal | ConstValue::ReturnSignal
5238            )
5239        })
5240    }
5241
5242    /// Format a comptime value as a human-readable string.
5243    ///
5244    /// Used by `@dbg` and `@compileLog` to render comptime values during
5245    /// compile-time evaluation.
5246    fn format_const_value(&self, val: ConstValue, span: Span) -> CompileResult<String> {
5247        match val {
5248            ConstValue::Bool(b) => Ok(if b {
5249                "true".to_string()
5250            } else {
5251                "false".to_string()
5252            }),
5253            ConstValue::Integer(v) => Ok(format!("{v}")),
5254            ConstValue::Unit => Ok("()".to_string()),
5255            ConstValue::ComptimeStr(idx) => match &self.comptime_heap[idx as usize] {
5256                ComptimeHeapItem::String(s) => Ok(s.clone()),
5257                _ => Err(CompileError::new(
5258                    ErrorKind::ComptimeEvaluationFailed {
5259                        reason: "invalid comptime_str heap reference".into(),
5260                    },
5261                    span,
5262                )),
5263            },
5264            _ => Err(CompileError::new(
5265                ErrorKind::ComptimeEvaluationFailed {
5266                    reason: "expression contains values that cannot be known at compile time"
5267                        .into(),
5268                },
5269                span,
5270            )),
5271        }
5272    }
5273
5274    /// Resolve a `ConstValue::ComptimeStr` to its Rust string content.
5275    fn resolve_comptime_str(&self, idx: u32, span: Span) -> CompileResult<&str> {
5276        match &self.comptime_heap[idx as usize] {
5277            ComptimeHeapItem::String(s) => Ok(s.as_str()),
5278            _ => Err(CompileError::new(
5279                ErrorKind::ComptimeEvaluationFailed {
5280                    reason: "invalid comptime_str heap reference".into(),
5281                },
5282                span,
5283            )),
5284        }
5285    }
5286
5287    /// Evaluate a `comptime_str` method call in the comptime interpreter.
5288    ///
5289    /// Dispatches methods like `len`, `is_empty`, `contains`, `starts_with`,
5290    /// `ends_with`, `eq`, `ne`, `lt`, `le`, `gt`, `ge`, and `concat`.
5291    #[allow(clippy::too_many_arguments)]
5292    fn evaluate_comptime_str_method(
5293        &mut self,
5294        str_idx: u32,
5295        method_name: &str,
5296        call_args: &[gruel_rir::RirCallArg],
5297        locals: &mut HashMap<Spur, ConstValue>,
5298        ctx: &AnalysisContext,
5299        outer_span: Span,
5300        inst_span: Span,
5301    ) -> CompileResult<ConstValue> {
5302        let s = self.resolve_comptime_str(str_idx, inst_span)?.to_string();
5303
5304        match method_name {
5305            "len" => {
5306                if !call_args.is_empty() {
5307                    return Err(CompileError::new(
5308                        ErrorKind::IntrinsicWrongArgCount {
5309                            name: "len".to_string(),
5310                            expected: 0,
5311                            found: call_args.len(),
5312                        },
5313                        inst_span,
5314                    ));
5315                }
5316                Ok(ConstValue::Integer(s.len() as i64))
5317            }
5318            "is_empty" => {
5319                if !call_args.is_empty() {
5320                    return Err(CompileError::new(
5321                        ErrorKind::IntrinsicWrongArgCount {
5322                            name: "is_empty".to_string(),
5323                            expected: 0,
5324                            found: call_args.len(),
5325                        },
5326                        inst_span,
5327                    ));
5328                }
5329                Ok(ConstValue::Bool(s.is_empty()))
5330            }
5331            "contains" | "starts_with" | "ends_with" | "eq" | "ne" | "lt" | "le" | "gt" | "ge" => {
5332                if call_args.len() != 1 {
5333                    return Err(CompileError::new(
5334                        ErrorKind::IntrinsicWrongArgCount {
5335                            name: method_name.to_string(),
5336                            expected: 1,
5337                            found: call_args.len(),
5338                        },
5339                        inst_span,
5340                    ));
5341                }
5342                let arg_val =
5343                    self.evaluate_comptime_inst(call_args[0].value, locals, ctx, outer_span)?;
5344                let other_idx = match arg_val {
5345                    ConstValue::ComptimeStr(idx) => idx,
5346                    _ => {
5347                        return Err(CompileError::new(
5348                            ErrorKind::ComptimeEvaluationFailed {
5349                                reason: format!(
5350                                    "comptime_str.{method_name} expects a comptime_str argument"
5351                                ),
5352                            },
5353                            inst_span,
5354                        ));
5355                    }
5356                };
5357                let other = self.resolve_comptime_str(other_idx, inst_span)?.to_string();
5358                let result = match method_name {
5359                    "contains" => s.contains(other.as_str()),
5360                    "starts_with" => s.starts_with(other.as_str()),
5361                    "ends_with" => s.ends_with(other.as_str()),
5362                    "eq" => s == other,
5363                    "ne" => s != other,
5364                    "lt" => s < other,
5365                    "le" => s <= other,
5366                    "gt" => s > other,
5367                    "ge" => s >= other,
5368                    _ => unreachable!(),
5369                };
5370                Ok(ConstValue::Bool(result))
5371            }
5372            "concat" => {
5373                if call_args.len() != 1 {
5374                    return Err(CompileError::new(
5375                        ErrorKind::IntrinsicWrongArgCount {
5376                            name: "concat".to_string(),
5377                            expected: 1,
5378                            found: call_args.len(),
5379                        },
5380                        inst_span,
5381                    ));
5382                }
5383                let arg_val =
5384                    self.evaluate_comptime_inst(call_args[0].value, locals, ctx, outer_span)?;
5385                let other_idx = match arg_val {
5386                    ConstValue::ComptimeStr(idx) => idx,
5387                    _ => {
5388                        return Err(CompileError::new(
5389                            ErrorKind::ComptimeEvaluationFailed {
5390                                reason: "comptime_str.concat expects a comptime_str argument"
5391                                    .into(),
5392                            },
5393                            inst_span,
5394                        ));
5395                    }
5396                };
5397                let other = self.resolve_comptime_str(other_idx, inst_span)?.to_string();
5398                let result = format!("{s}{other}");
5399                let idx = self.comptime_heap.len() as u32;
5400                self.comptime_heap.push(ComptimeHeapItem::String(result));
5401                Ok(ConstValue::ComptimeStr(idx))
5402            }
5403            "clone" => {
5404                if !call_args.is_empty() {
5405                    return Err(CompileError::new(
5406                        ErrorKind::IntrinsicWrongArgCount {
5407                            name: "clone".to_string(),
5408                            expected: 0,
5409                            found: call_args.len(),
5410                        },
5411                        inst_span,
5412                    ));
5413                }
5414                Ok(self.alloc_comptime_str(s))
5415            }
5416            "push_str" | "push" | "clear" | "reserve" => Err(CompileError::new(
5417                ErrorKind::ComptimeEvaluationFailed {
5418                    reason: format!(
5419                        "cannot call .{method_name}() on a compile-time string; use .concat() to produce a new string"
5420                    ),
5421                },
5422                inst_span,
5423            )),
5424            "capacity" => Err(CompileError::new(
5425                ErrorKind::ComptimeEvaluationFailed {
5426                    reason: "capacity is not available for compile-time strings".into(),
5427                },
5428                inst_span,
5429            )),
5430            _ => Err(CompileError::new(
5431                ErrorKind::ComptimeEvaluationFailed {
5432                    reason: format!("unknown comptime_str method '{method_name}'"),
5433                },
5434                inst_span,
5435            )),
5436        }
5437    }
5438
5439    /// Evaluate a comptime intrinsic argument as a string.
5440    ///
5441    /// Accepts both `StringConst` instructions (string literals) and
5442    /// `ConstValue::ComptimeStr` values from comptime evaluation.
5443    fn evaluate_comptime_string_arg(
5444        &mut self,
5445        arg_ref: InstRef,
5446        locals: &mut HashMap<Spur, ConstValue>,
5447        ctx: &AnalysisContext,
5448        outer_span: Span,
5449    ) -> CompileResult<String> {
5450        let arg_inst = self.rir.get(arg_ref);
5451        // Try string literal first
5452        if let gruel_rir::InstData::StringConst(spur) = &arg_inst.data {
5453            return Ok(self.interner.resolve(spur).to_string());
5454        }
5455        // Otherwise evaluate as a comptime expression
5456        let val = self.evaluate_comptime_inst(arg_ref, locals, ctx, outer_span)?;
5457        match val {
5458            ConstValue::ComptimeStr(idx) => self
5459                .resolve_comptime_str(idx, arg_inst.span)
5460                .map(|s| s.to_string()),
5461            _ => Err(CompileError::new(
5462                ErrorKind::ComptimeEvaluationFailed {
5463                    reason: "@compileError requires a string literal or comptime_str argument"
5464                        .into(),
5465                },
5466                arg_inst.span,
5467            )),
5468        }
5469    }
5470
5471    /// Allocate a `comptime_str` on the comptime heap and return a `ConstValue::ComptimeStr`.
5472    fn alloc_comptime_str(&mut self, s: String) -> ConstValue {
5473        let idx = self.comptime_heap.len() as u32;
5474        self.comptime_heap.push(ComptimeHeapItem::String(s));
5475        ConstValue::ComptimeStr(idx)
5476    }
5477
5478    /// Allocate a comptime struct on the heap and return a `ConstValue::Struct`.
5479    fn alloc_comptime_struct(
5480        &mut self,
5481        struct_id: StructId,
5482        fields: Vec<ConstValue>,
5483    ) -> ConstValue {
5484        let idx = self.comptime_heap.len() as u32;
5485        self.comptime_heap
5486            .push(ComptimeHeapItem::Struct { struct_id, fields });
5487        ConstValue::Struct(idx)
5488    }
5489
5490    /// Allocate a comptime array on the heap and return a `ConstValue::Array`.
5491    fn alloc_comptime_array(&mut self, elements: Vec<ConstValue>) -> ConstValue {
5492        let idx = self.comptime_heap.len() as u32;
5493        self.comptime_heap.push(ComptimeHeapItem::Array(elements));
5494        ConstValue::Array(idx)
5495    }
5496
5497    /// Resolve a `TypeKind` variant name to its discriminant index.
5498    fn typekind_variant_idx(&self, variant_name: &str) -> u32 {
5499        let enum_id = self
5500            .builtin_typekind_id
5501            .expect("TypeKind enum not injected");
5502        let enum_def = self.type_pool.enum_def(enum_id);
5503        enum_def
5504            .find_variant(variant_name)
5505            .unwrap_or_else(|| panic!("TypeKind variant '{variant_name}' not found")) as u32
5506    }
5507
5508    /// Evaluate `@typeName(T)` — returns the type's name as a `comptime_str`.
5509    fn evaluate_comptime_type_name(&mut self, ty: Type, _span: Span) -> CompileResult<ConstValue> {
5510        let name = self.type_pool.format_type_name(ty);
5511        Ok(self.alloc_comptime_str(name))
5512    }
5513
5514    /// Evaluate `@typeInfo(T)` — returns a comptime struct describing the type.
5515    fn evaluate_comptime_type_info(&mut self, ty: Type, span: Span) -> CompileResult<ConstValue> {
5516        let typekind_enum_id = self
5517            .builtin_typekind_id
5518            .expect("TypeKind enum not injected");
5519        let typekind_type = Type::new_enum(typekind_enum_id);
5520
5521        match ty.kind() {
5522            TypeKind::Struct(struct_id) => {
5523                self.build_struct_type_info(struct_id, typekind_enum_id, typekind_type)
5524            }
5525            TypeKind::Enum(enum_id) => {
5526                self.build_enum_type_info(enum_id, typekind_enum_id, typekind_type)
5527            }
5528            TypeKind::I8 => {
5529                self.build_int_type_info("i8", 8, true, typekind_enum_id, typekind_type)
5530            }
5531            TypeKind::I16 => {
5532                self.build_int_type_info("i16", 16, true, typekind_enum_id, typekind_type)
5533            }
5534            TypeKind::I32 => {
5535                self.build_int_type_info("i32", 32, true, typekind_enum_id, typekind_type)
5536            }
5537            TypeKind::I64 => {
5538                self.build_int_type_info("i64", 64, true, typekind_enum_id, typekind_type)
5539            }
5540            TypeKind::U8 => {
5541                self.build_int_type_info("u8", 8, false, typekind_enum_id, typekind_type)
5542            }
5543            TypeKind::U16 => {
5544                self.build_int_type_info("u16", 16, false, typekind_enum_id, typekind_type)
5545            }
5546            TypeKind::U32 => {
5547                self.build_int_type_info("u32", 32, false, typekind_enum_id, typekind_type)
5548            }
5549            TypeKind::U64 => {
5550                self.build_int_type_info("u64", 64, false, typekind_enum_id, typekind_type)
5551            }
5552            TypeKind::Bool => {
5553                self.build_simple_type_info("bool", "Bool", typekind_enum_id, typekind_type)
5554            }
5555            TypeKind::Unit => {
5556                self.build_simple_type_info("()", "Unit", typekind_enum_id, typekind_type)
5557            }
5558            TypeKind::Never => {
5559                self.build_simple_type_info("!", "Never", typekind_enum_id, typekind_type)
5560            }
5561            TypeKind::Array(array_type_id) => {
5562                let (elem_ty, len) = self.type_pool.array_def(array_type_id);
5563                let elem_name = self.type_pool.format_type_name(elem_ty);
5564                let name = format!("[{elem_name}; {len}]");
5565                self.build_simple_type_info(&name, "Array", typekind_enum_id, typekind_type)
5566            }
5567            _ => Err(CompileError::new(
5568                ErrorKind::ComptimeEvaluationFailed {
5569                    reason: format!("@typeInfo not supported for type '{}'", ty.name()),
5570                },
5571                span,
5572            )),
5573        }
5574    }
5575
5576    /// Build a simple type info struct with just `kind` and `name` fields.
5577    fn build_simple_type_info(
5578        &mut self,
5579        type_name: &str,
5580        kind_variant_name: &str,
5581        typekind_enum_id: EnumId,
5582        typekind_type: Type,
5583    ) -> CompileResult<ConstValue> {
5584        let kind_val = ConstValue::EnumVariant {
5585            enum_id: typekind_enum_id,
5586            variant_idx: self.typekind_variant_idx(kind_variant_name),
5587        };
5588        let name_val = self.alloc_comptime_str(type_name.to_string());
5589
5590        let fields = vec![
5591            StructField {
5592                name: "kind".to_string(),
5593                ty: typekind_type,
5594            },
5595            StructField {
5596                name: "name".to_string(),
5597                ty: Type::COMPTIME_STR,
5598            },
5599        ];
5600        let (info_type, _) = self.find_or_create_anon_struct(&fields, &[], &HashMap::new());
5601        let info_struct_id = match info_type.kind() {
5602            TypeKind::Struct(id) => id,
5603            _ => unreachable!(),
5604        };
5605
5606        Ok(self.alloc_comptime_struct(info_struct_id, vec![kind_val, name_val]))
5607    }
5608
5609    /// Build type info for an integer type (includes `bits` and `is_signed`).
5610    #[allow(clippy::too_many_arguments)]
5611    fn build_int_type_info(
5612        &mut self,
5613        type_name: &str,
5614        bits: i32,
5615        is_signed: bool,
5616        typekind_enum_id: EnumId,
5617        typekind_type: Type,
5618    ) -> CompileResult<ConstValue> {
5619        let kind_val = ConstValue::EnumVariant {
5620            enum_id: typekind_enum_id,
5621            variant_idx: self.typekind_variant_idx("Int"),
5622        };
5623        let name_val = self.alloc_comptime_str(type_name.to_string());
5624
5625        let fields = vec![
5626            StructField {
5627                name: "kind".to_string(),
5628                ty: typekind_type,
5629            },
5630            StructField {
5631                name: "name".to_string(),
5632                ty: Type::COMPTIME_STR,
5633            },
5634            StructField {
5635                name: "bits".to_string(),
5636                ty: Type::I32,
5637            },
5638            StructField {
5639                name: "is_signed".to_string(),
5640                ty: Type::BOOL,
5641            },
5642        ];
5643        let (info_type, _) = self.find_or_create_anon_struct(&fields, &[], &HashMap::new());
5644        let info_struct_id = match info_type.kind() {
5645            TypeKind::Struct(id) => id,
5646            _ => unreachable!(),
5647        };
5648
5649        Ok(self.alloc_comptime_struct(
5650            info_struct_id,
5651            vec![
5652                kind_val,
5653                name_val,
5654                ConstValue::Integer(bits as i64),
5655                ConstValue::Bool(is_signed),
5656            ],
5657        ))
5658    }
5659
5660    /// Build type info for a struct type (includes `field_count` and `fields` array).
5661    fn build_struct_type_info(
5662        &mut self,
5663        struct_id: StructId,
5664        typekind_enum_id: EnumId,
5665        typekind_type: Type,
5666    ) -> CompileResult<ConstValue> {
5667        let kind_val = ConstValue::EnumVariant {
5668            enum_id: typekind_enum_id,
5669            variant_idx: self.typekind_variant_idx("Struct"),
5670        };
5671
5672        // Get struct info
5673        let struct_def = self.type_pool.struct_def(struct_id);
5674        let struct_name = struct_def.name.clone();
5675        let field_defs: Vec<(String, Type)> = struct_def
5676            .fields
5677            .iter()
5678            .map(|f| (f.name.clone(), f.ty))
5679            .collect();
5680        let field_count = field_defs.len() as i32;
5681
5682        let name_val = self.alloc_comptime_str(struct_name);
5683
5684        // Create FieldInfo struct type
5685        let field_info_fields = vec![
5686            StructField {
5687                name: "name".to_string(),
5688                ty: Type::COMPTIME_STR,
5689            },
5690            StructField {
5691                name: "field_type".to_string(),
5692                ty: Type::COMPTIME_TYPE,
5693            },
5694        ];
5695        let (field_info_type, _) =
5696            self.find_or_create_anon_struct(&field_info_fields, &[], &HashMap::new());
5697        let field_info_struct_id = match field_info_type.kind() {
5698            TypeKind::Struct(id) => id,
5699            _ => unreachable!(),
5700        };
5701
5702        // Create FieldInfo instances for each field
5703        let mut field_values = Vec::with_capacity(field_defs.len());
5704        for (fname, ftype) in &field_defs {
5705            let fname_val = self.alloc_comptime_str(fname.clone());
5706            let ftype_val = ConstValue::Type(*ftype);
5707            let field_info =
5708                self.alloc_comptime_struct(field_info_struct_id, vec![fname_val, ftype_val]);
5709            field_values.push(field_info);
5710        }
5711
5712        // Create the fields array
5713        let fields_array = self.alloc_comptime_array(field_values);
5714
5715        // Create the array type for fields: [FieldInfo; N]
5716        let fields_array_type =
5717            Type::new_array(self.get_or_create_array_type(field_info_type, field_count as u64));
5718
5719        // Create the info struct type
5720        let info_fields = vec![
5721            StructField {
5722                name: "kind".to_string(),
5723                ty: typekind_type,
5724            },
5725            StructField {
5726                name: "name".to_string(),
5727                ty: Type::COMPTIME_STR,
5728            },
5729            StructField {
5730                name: "field_count".to_string(),
5731                ty: Type::I32,
5732            },
5733            StructField {
5734                name: "fields".to_string(),
5735                ty: fields_array_type,
5736            },
5737        ];
5738        let (info_type, _) = self.find_or_create_anon_struct(&info_fields, &[], &HashMap::new());
5739        let info_struct_id = match info_type.kind() {
5740            TypeKind::Struct(id) => id,
5741            _ => unreachable!(),
5742        };
5743
5744        Ok(self.alloc_comptime_struct(
5745            info_struct_id,
5746            vec![
5747                kind_val,
5748                name_val,
5749                ConstValue::Integer(field_count as i64),
5750                fields_array,
5751            ],
5752        ))
5753    }
5754
5755    /// Build type info for an enum type (includes `variant_count` and `variants` array).
5756    fn build_enum_type_info(
5757        &mut self,
5758        enum_id: EnumId,
5759        typekind_enum_id: EnumId,
5760        typekind_type: Type,
5761    ) -> CompileResult<ConstValue> {
5762        let kind_val = ConstValue::EnumVariant {
5763            enum_id: typekind_enum_id,
5764            variant_idx: self.typekind_variant_idx("Enum"),
5765        };
5766
5767        // Get enum info
5768        let enum_def = self.type_pool.enum_def(enum_id);
5769        let enum_name = enum_def.name.clone();
5770        let variant_defs: Vec<(String, Vec<(String, Type)>)> = enum_def
5771            .variants
5772            .iter()
5773            .map(|v| {
5774                let vfields: Vec<(String, Type)> = if v.is_struct_variant() {
5775                    // Struct variant: field names + types
5776                    v.field_names
5777                        .iter()
5778                        .zip(v.fields.iter())
5779                        .map(|(name, ty)| (name.clone(), *ty))
5780                        .collect()
5781                } else {
5782                    // Unit or tuple variant: just types with positional names
5783                    v.fields
5784                        .iter()
5785                        .enumerate()
5786                        .map(|(i, ty)| (format!("{i}"), *ty))
5787                        .collect()
5788                };
5789                (v.name.clone(), vfields)
5790            })
5791            .collect();
5792        let variant_count = variant_defs.len() as i32;
5793
5794        let name_val = self.alloc_comptime_str(enum_name);
5795
5796        // Create FieldInfo struct type (reuse if already exists)
5797        let field_info_fields = vec![
5798            StructField {
5799                name: "name".to_string(),
5800                ty: Type::COMPTIME_STR,
5801            },
5802            StructField {
5803                name: "field_type".to_string(),
5804                ty: Type::COMPTIME_TYPE,
5805            },
5806        ];
5807        let (field_info_type, _) =
5808            self.find_or_create_anon_struct(&field_info_fields, &[], &HashMap::new());
5809        let field_info_struct_id = match field_info_type.kind() {
5810            TypeKind::Struct(id) => id,
5811            _ => unreachable!(),
5812        };
5813
5814        // Create VariantInfo instances
5815        let mut variant_values = Vec::with_capacity(variant_defs.len());
5816        for (vname, vfields) in &variant_defs {
5817            let vname_val = self.alloc_comptime_str(vname.clone());
5818
5819            // Create FieldInfo array for this variant's fields
5820            let mut vfield_values = Vec::new();
5821            for (fname, ftype) in vfields {
5822                let fname_val = self.alloc_comptime_str(fname.clone());
5823                let ftype_val = ConstValue::Type(*ftype);
5824                let field_info =
5825                    self.alloc_comptime_struct(field_info_struct_id, vec![fname_val, ftype_val]);
5826                vfield_values.push(field_info);
5827            }
5828            let vfields_array = self.alloc_comptime_array(vfield_values);
5829            let vfield_count = vfields.len() as i32;
5830
5831            // Create VariantInfo struct type with fields array
5832            let vfields_array_type = Type::new_array(
5833                self.get_or_create_array_type(field_info_type, vfields.len() as u64),
5834            );
5835            let variant_info_fields = vec![
5836                StructField {
5837                    name: "name".to_string(),
5838                    ty: Type::COMPTIME_STR,
5839                },
5840                StructField {
5841                    name: "field_count".to_string(),
5842                    ty: Type::I32,
5843                },
5844                StructField {
5845                    name: "fields".to_string(),
5846                    ty: vfields_array_type,
5847                },
5848            ];
5849            let (variant_info_type, _) =
5850                self.find_or_create_anon_struct(&variant_info_fields, &[], &HashMap::new());
5851            let variant_info_struct_id = match variant_info_type.kind() {
5852                TypeKind::Struct(id) => id,
5853                _ => unreachable!(),
5854            };
5855
5856            let variant_info = self.alloc_comptime_struct(
5857                variant_info_struct_id,
5858                vec![
5859                    vname_val,
5860                    ConstValue::Integer(vfield_count as i64),
5861                    vfields_array,
5862                ],
5863            );
5864            variant_values.push(variant_info);
5865        }
5866
5867        // Create the variants array
5868        let variants_array = self.alloc_comptime_array(variant_values);
5869
5870        // For the array type, we need a VariantInfo type — use one for unit variants (0 fields)
5871        let empty_fields_array_type =
5872            Type::new_array(self.get_or_create_array_type(field_info_type, 0));
5873        let variant_info_fields = vec![
5874            StructField {
5875                name: "name".to_string(),
5876                ty: Type::COMPTIME_STR,
5877            },
5878            StructField {
5879                name: "field_count".to_string(),
5880                ty: Type::I32,
5881            },
5882            StructField {
5883                name: "fields".to_string(),
5884                ty: empty_fields_array_type,
5885            },
5886        ];
5887        let (variant_info_type, _) =
5888            self.find_or_create_anon_struct(&variant_info_fields, &[], &HashMap::new());
5889        let variants_array_type =
5890            Type::new_array(self.get_or_create_array_type(variant_info_type, variant_count as u64));
5891
5892        // Create the info struct type
5893        let info_fields = vec![
5894            StructField {
5895                name: "kind".to_string(),
5896                ty: typekind_type,
5897            },
5898            StructField {
5899                name: "name".to_string(),
5900                ty: Type::COMPTIME_STR,
5901            },
5902            StructField {
5903                name: "variant_count".to_string(),
5904                ty: Type::I32,
5905            },
5906            StructField {
5907                name: "variants".to_string(),
5908                ty: variants_array_type,
5909            },
5910        ];
5911        let (info_type, _) = self.find_or_create_anon_struct(&info_fields, &[], &HashMap::new());
5912        let info_struct_id = match info_type.kind() {
5913            TypeKind::Struct(id) => id,
5914            _ => unreachable!(),
5915        };
5916
5917        Ok(self.alloc_comptime_struct(
5918            info_struct_id,
5919            vec![
5920                kind_val,
5921                name_val,
5922                ConstValue::Integer(variant_count as i64),
5923                variants_array,
5924            ],
5925        ))
5926    }
5927
5928    /// Evaluate a comptime block expression using the stateful interpreter.
5929    ///
5930    /// Seeds the local scope from `ctx.comptime_value_vars` (captured comptime
5931    /// parameters, e.g. `N` in `FixedBuffer(comptime N: i32)`), then delegates
5932    /// to `evaluate_comptime_inst`.
5933    fn evaluate_comptime_block(
5934        &mut self,
5935        inst_ref: InstRef,
5936        ctx: &AnalysisContext,
5937        span: Span,
5938    ) -> CompileResult<ConstValue> {
5939        let _span = info_span!("comptime").entered();
5940        // Reset the step counter and heap for this comptime block evaluation.
5941        self.comptime_steps_used = 0;
5942        self.comptime_heap.clear();
5943        // Seed interpreter locals with any comptime-captured values from the
5944        // outer analysis context (e.g. `N` in a method of `FixedBuffer(N)`).
5945        let mut locals = ctx.comptime_value_vars.clone();
5946        let result = self.evaluate_comptime_inst(inst_ref, &mut locals, ctx, span)?;
5947        // Control-flow signals escaping the top level are errors.
5948        // BreakSignal/ContinueSignal mean break/continue outside a loop.
5949        // ReturnSignal means return outside a function (comptime block is not a function).
5950        match result {
5951            ConstValue::BreakSignal | ConstValue::ContinueSignal => Err(CompileError::new(
5952                ErrorKind::ComptimeEvaluationFailed {
5953                    reason: "break/continue outside a loop in comptime block".into(),
5954                },
5955                span,
5956            )),
5957            ConstValue::ReturnSignal => Err(CompileError::new(
5958                ErrorKind::ComptimeEvaluationFailed {
5959                    reason: "return outside a function in comptime block".into(),
5960                },
5961                span,
5962            )),
5963            val => Ok(val),
5964        }
5965    }
5966
5967    /// Evaluate a comptime expression without clearing the heap.
5968    ///
5969    /// This is used by `@field` and other intrinsics inside `comptime_unroll for` bodies
5970    /// where the heap contains data from the iterable evaluation that must be preserved.
5971    fn evaluate_comptime_expr(
5972        &mut self,
5973        inst_ref: InstRef,
5974        ctx: &AnalysisContext,
5975        span: Span,
5976    ) -> CompileResult<ConstValue> {
5977        let prev_steps = self.comptime_steps_used;
5978        self.comptime_steps_used = 0;
5979        let mut locals = ctx.comptime_value_vars.clone();
5980        let result = self.evaluate_comptime_inst(inst_ref, &mut locals, ctx, span)?;
5981        self.comptime_steps_used = prev_steps;
5982        match result {
5983            ConstValue::BreakSignal | ConstValue::ContinueSignal => Err(CompileError::new(
5984                ErrorKind::ComptimeEvaluationFailed {
5985                    reason: "break/continue outside a loop in comptime expression".into(),
5986                },
5987                span,
5988            )),
5989            ConstValue::ReturnSignal => Err(CompileError::new(
5990                ErrorKind::ComptimeEvaluationFailed {
5991                    reason: "return outside a function in comptime expression".into(),
5992                },
5993                span,
5994            )),
5995            val => Ok(val),
5996        }
5997    }
5998
5999    /// Recursively evaluate one RIR instruction in a comptime context.
6000    ///
6001    /// `locals` holds variables declared within the current comptime block.
6002    /// Returns the evaluated `ConstValue`, or a `CompileError` if the
6003    /// instruction is not compile-time evaluable.
6004    #[allow(clippy::only_used_in_recursion)]
6005    fn evaluate_comptime_inst(
6006        &mut self,
6007        inst_ref: InstRef,
6008        locals: &mut HashMap<Spur, ConstValue>,
6009        ctx: &AnalysisContext,
6010        outer_span: Span,
6011    ) -> CompileResult<ConstValue> {
6012        // Clone the instruction data up-front to release the `self.rir` borrow
6013        // before any recursive calls to `evaluate_comptime_inst`.
6014        let (inst_span, inst_data) = {
6015            let inst = self.rir.get(inst_ref);
6016            (inst.span, inst.data.clone())
6017        };
6018
6019        /// Return a "cannot be known at compile time" error at `span`.
6020        #[inline]
6021        fn not_const(span: Span) -> CompileError {
6022            CompileError::new(
6023                ErrorKind::ComptimeEvaluationFailed {
6024                    reason: "expression contains values that cannot be known at compile time"
6025                        .into(),
6026                },
6027                span,
6028            )
6029        }
6030
6031        /// Return an arithmetic overflow error at `span`.
6032        #[inline]
6033        fn overflow(span: Span) -> CompileError {
6034            CompileError::new(
6035                ErrorKind::ComptimeEvaluationFailed {
6036                    reason: "arithmetic overflow in comptime evaluation".into(),
6037                },
6038                span,
6039            )
6040        }
6041
6042        /// Extract integer from ConstValue, or return not_const error.
6043        #[inline]
6044        fn int(v: ConstValue, span: Span) -> CompileResult<i64> {
6045            v.as_integer().ok_or_else(|| not_const(span))
6046        }
6047
6048        /// Extract bool from ConstValue, or return not_const error.
6049        #[inline]
6050        fn bool_val(v: ConstValue, span: Span) -> CompileResult<bool> {
6051            v.as_bool().ok_or_else(|| not_const(span))
6052        }
6053
6054        match inst_data {
6055            // ── Literals ──────────────────────────────────────────────────────
6056            InstData::IntConst(value) => {
6057                i64::try_from(value).map(ConstValue::Integer).map_err(|_| {
6058                    CompileError::new(
6059                        ErrorKind::ComptimeEvaluationFailed {
6060                            reason: "integer constant too large for comptime evaluation".into(),
6061                        },
6062                        inst_span,
6063                    )
6064                })
6065            }
6066
6067            InstData::BoolConst(value) => Ok(ConstValue::Bool(value)),
6068
6069            InstData::UnitConst => Ok(ConstValue::Unit),
6070
6071            InstData::StringConst(spur) => {
6072                let s = self.interner.resolve(&spur).to_string();
6073                let idx = self.comptime_heap.len() as u32;
6074                self.comptime_heap.push(ComptimeHeapItem::String(s));
6075                Ok(ConstValue::ComptimeStr(idx))
6076            }
6077
6078            // ── Unary operations ─────────────────────────────────────────────
6079            InstData::Neg { operand } => {
6080                let n = int(
6081                    self.evaluate_comptime_inst(operand, locals, ctx, outer_span)?,
6082                    inst_span,
6083                )?;
6084                n.checked_neg()
6085                    .map(ConstValue::Integer)
6086                    .ok_or_else(|| overflow(inst_span))
6087            }
6088
6089            InstData::Not { operand } => {
6090                let b = bool_val(
6091                    self.evaluate_comptime_inst(operand, locals, ctx, outer_span)?,
6092                    inst_span,
6093                )?;
6094                Ok(ConstValue::Bool(!b))
6095            }
6096
6097            InstData::BitNot { operand } => {
6098                let n = int(
6099                    self.evaluate_comptime_inst(operand, locals, ctx, outer_span)?,
6100                    inst_span,
6101                )?;
6102                Ok(ConstValue::Integer(!n))
6103            }
6104
6105            // ── Binary arithmetic ─────────────────────────────────────────────
6106            InstData::Add { lhs, rhs } => {
6107                let l = int(
6108                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6109                    inst_span,
6110                )?;
6111                let r = int(
6112                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6113                    inst_span,
6114                )?;
6115                l.checked_add(r)
6116                    .map(ConstValue::Integer)
6117                    .ok_or_else(|| overflow(inst_span))
6118            }
6119            InstData::Sub { lhs, rhs } => {
6120                let l = int(
6121                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6122                    inst_span,
6123                )?;
6124                let r = int(
6125                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6126                    inst_span,
6127                )?;
6128                l.checked_sub(r)
6129                    .map(ConstValue::Integer)
6130                    .ok_or_else(|| overflow(inst_span))
6131            }
6132            InstData::Mul { lhs, rhs } => {
6133                let l = int(
6134                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6135                    inst_span,
6136                )?;
6137                let r = int(
6138                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6139                    inst_span,
6140                )?;
6141                l.checked_mul(r)
6142                    .map(ConstValue::Integer)
6143                    .ok_or_else(|| overflow(inst_span))
6144            }
6145            InstData::Div { lhs, rhs } => {
6146                let l = int(
6147                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6148                    inst_span,
6149                )?;
6150                let r = int(
6151                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6152                    inst_span,
6153                )?;
6154                if r == 0 {
6155                    return Err(CompileError::new(
6156                        ErrorKind::ComptimeEvaluationFailed {
6157                            reason: "division by zero in comptime evaluation".into(),
6158                        },
6159                        inst_span,
6160                    ));
6161                }
6162                l.checked_div(r)
6163                    .map(ConstValue::Integer)
6164                    .ok_or_else(|| overflow(inst_span))
6165            }
6166            InstData::Mod { lhs, rhs } => {
6167                let l = int(
6168                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6169                    inst_span,
6170                )?;
6171                let r = int(
6172                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6173                    inst_span,
6174                )?;
6175                if r == 0 {
6176                    return Err(CompileError::new(
6177                        ErrorKind::ComptimeEvaluationFailed {
6178                            reason: "modulo by zero in comptime evaluation".into(),
6179                        },
6180                        inst_span,
6181                    ));
6182                }
6183                l.checked_rem(r)
6184                    .map(ConstValue::Integer)
6185                    .ok_or_else(|| overflow(inst_span))
6186            }
6187
6188            // ── Comparisons ───────────────────────────────────────────────────
6189            InstData::Eq { lhs, rhs } => {
6190                let l = self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?;
6191                let r = self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?;
6192                match (l, r) {
6193                    (ConstValue::Integer(a), ConstValue::Integer(b)) => {
6194                        Ok(ConstValue::Bool(a == b))
6195                    }
6196                    (ConstValue::Bool(a), ConstValue::Bool(b)) => Ok(ConstValue::Bool(a == b)),
6197                    (ConstValue::ComptimeStr(a), ConstValue::ComptimeStr(b)) => {
6198                        let sa = self.resolve_comptime_str(a, inst_span)?;
6199                        let sb = self.resolve_comptime_str(b, inst_span)?;
6200                        Ok(ConstValue::Bool(sa == sb))
6201                    }
6202                    _ => Err(not_const(inst_span)),
6203                }
6204            }
6205            InstData::Ne { lhs, rhs } => {
6206                let l = self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?;
6207                let r = self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?;
6208                match (l, r) {
6209                    (ConstValue::Integer(a), ConstValue::Integer(b)) => {
6210                        Ok(ConstValue::Bool(a != b))
6211                    }
6212                    (ConstValue::Bool(a), ConstValue::Bool(b)) => Ok(ConstValue::Bool(a != b)),
6213                    (ConstValue::ComptimeStr(a), ConstValue::ComptimeStr(b)) => {
6214                        let sa = self.resolve_comptime_str(a, inst_span)?;
6215                        let sb = self.resolve_comptime_str(b, inst_span)?;
6216                        Ok(ConstValue::Bool(sa != sb))
6217                    }
6218                    _ => Err(not_const(inst_span)),
6219                }
6220            }
6221            InstData::Lt { lhs, rhs } => {
6222                let l = self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?;
6223                let r = self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?;
6224                match (l, r) {
6225                    (ConstValue::Integer(a), ConstValue::Integer(b)) => Ok(ConstValue::Bool(a < b)),
6226                    (ConstValue::ComptimeStr(a), ConstValue::ComptimeStr(b)) => {
6227                        let sa = self.resolve_comptime_str(a, inst_span)?;
6228                        let sb = self.resolve_comptime_str(b, inst_span)?;
6229                        Ok(ConstValue::Bool(sa < sb))
6230                    }
6231                    _ => Err(not_const(inst_span)),
6232                }
6233            }
6234            InstData::Gt { lhs, rhs } => {
6235                let l = self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?;
6236                let r = self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?;
6237                match (l, r) {
6238                    (ConstValue::Integer(a), ConstValue::Integer(b)) => Ok(ConstValue::Bool(a > b)),
6239                    (ConstValue::ComptimeStr(a), ConstValue::ComptimeStr(b)) => {
6240                        let sa = self.resolve_comptime_str(a, inst_span)?;
6241                        let sb = self.resolve_comptime_str(b, inst_span)?;
6242                        Ok(ConstValue::Bool(sa > sb))
6243                    }
6244                    _ => Err(not_const(inst_span)),
6245                }
6246            }
6247            InstData::Le { lhs, rhs } => {
6248                let l = self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?;
6249                let r = self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?;
6250                match (l, r) {
6251                    (ConstValue::Integer(a), ConstValue::Integer(b)) => {
6252                        Ok(ConstValue::Bool(a <= b))
6253                    }
6254                    (ConstValue::ComptimeStr(a), ConstValue::ComptimeStr(b)) => {
6255                        let sa = self.resolve_comptime_str(a, inst_span)?;
6256                        let sb = self.resolve_comptime_str(b, inst_span)?;
6257                        Ok(ConstValue::Bool(sa <= sb))
6258                    }
6259                    _ => Err(not_const(inst_span)),
6260                }
6261            }
6262            InstData::Ge { lhs, rhs } => {
6263                let l = self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?;
6264                let r = self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?;
6265                match (l, r) {
6266                    (ConstValue::Integer(a), ConstValue::Integer(b)) => {
6267                        Ok(ConstValue::Bool(a >= b))
6268                    }
6269                    (ConstValue::ComptimeStr(a), ConstValue::ComptimeStr(b)) => {
6270                        let sa = self.resolve_comptime_str(a, inst_span)?;
6271                        let sb = self.resolve_comptime_str(b, inst_span)?;
6272                        Ok(ConstValue::Bool(sa >= sb))
6273                    }
6274                    _ => Err(not_const(inst_span)),
6275                }
6276            }
6277
6278            // ── Logical ───────────────────────────────────────────────────────
6279            InstData::And { lhs, rhs } => {
6280                let l = bool_val(
6281                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6282                    inst_span,
6283                )?;
6284                let r = bool_val(
6285                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6286                    inst_span,
6287                )?;
6288                Ok(ConstValue::Bool(l && r))
6289            }
6290            InstData::Or { lhs, rhs } => {
6291                let l = bool_val(
6292                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6293                    inst_span,
6294                )?;
6295                let r = bool_val(
6296                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6297                    inst_span,
6298                )?;
6299                Ok(ConstValue::Bool(l || r))
6300            }
6301
6302            // ── Bitwise ───────────────────────────────────────────────────────
6303            InstData::BitAnd { lhs, rhs } => {
6304                let l = int(
6305                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6306                    inst_span,
6307                )?;
6308                let r = int(
6309                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6310                    inst_span,
6311                )?;
6312                Ok(ConstValue::Integer(l & r))
6313            }
6314            InstData::BitOr { lhs, rhs } => {
6315                let l = int(
6316                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6317                    inst_span,
6318                )?;
6319                let r = int(
6320                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6321                    inst_span,
6322                )?;
6323                Ok(ConstValue::Integer(l | r))
6324            }
6325            InstData::BitXor { lhs, rhs } => {
6326                let l = int(
6327                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6328                    inst_span,
6329                )?;
6330                let r = int(
6331                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6332                    inst_span,
6333                )?;
6334                Ok(ConstValue::Integer(l ^ r))
6335            }
6336            InstData::Shl { lhs, rhs } => {
6337                let l = int(
6338                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6339                    inst_span,
6340                )?;
6341                let r = int(
6342                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6343                    inst_span,
6344                )?;
6345                if !(0..64).contains(&r) {
6346                    return Err(CompileError::new(
6347                        ErrorKind::ComptimeEvaluationFailed {
6348                            reason: "shift amount out of range in comptime evaluation".into(),
6349                        },
6350                        inst_span,
6351                    ));
6352                }
6353                Ok(ConstValue::Integer(l << r))
6354            }
6355            InstData::Shr { lhs, rhs } => {
6356                let l = int(
6357                    self.evaluate_comptime_inst(lhs, locals, ctx, outer_span)?,
6358                    inst_span,
6359                )?;
6360                let r = int(
6361                    self.evaluate_comptime_inst(rhs, locals, ctx, outer_span)?,
6362                    inst_span,
6363                )?;
6364                if !(0..64).contains(&r) {
6365                    return Err(CompileError::new(
6366                        ErrorKind::ComptimeEvaluationFailed {
6367                            reason: "shift amount out of range in comptime evaluation".into(),
6368                        },
6369                        inst_span,
6370                    ));
6371                }
6372                Ok(ConstValue::Integer(l >> r))
6373            }
6374
6375            // ── Block: iterate instructions, return last value ─────────────────
6376            InstData::Block { extra_start, len } => {
6377                // Collect into owned Vec to release the `self.rir` borrow before
6378                // the loop body calls `evaluate_comptime_inst` recursively.
6379                let raw_refs: Vec<u32> = self.rir.get_extra(extra_start, len).to_vec();
6380                let mut last_val = ConstValue::Unit;
6381                for raw_ref in raw_refs {
6382                    last_val = self.evaluate_comptime_inst(
6383                        InstRef::from_raw(raw_ref),
6384                        locals,
6385                        ctx,
6386                        outer_span,
6387                    )?;
6388                    // Propagate control-flow signals immediately — don't execute
6389                    // remaining statements after a break, continue, or return.
6390                    if matches!(
6391                        last_val,
6392                        ConstValue::BreakSignal
6393                            | ConstValue::ContinueSignal
6394                            | ConstValue::ReturnSignal
6395                    ) {
6396                        return Ok(last_val);
6397                    }
6398                }
6399                Ok(last_val)
6400            }
6401
6402            // ── Variable declaration ──────────────────────────────────────────
6403            InstData::Alloc { name, init, .. } => {
6404                let val = self.evaluate_comptime_inst(init, locals, ctx, outer_span)?;
6405                if let Some(name_sym) = name {
6406                    locals.insert(name_sym, val);
6407                }
6408                Ok(ConstValue::Unit)
6409            }
6410
6411            // ── Variable reference ────────────────────────────────────────────
6412            InstData::VarRef { name } => {
6413                // 1. Locals declared within this comptime block (or seeded from outer captures).
6414                if let Some(&val) = locals.get(&name) {
6415                    return Ok(val);
6416                }
6417                // 2. Comptime type overrides (type params bound during generic function calls).
6418                if let Some(&ty) = self.comptime_type_overrides.get(&name) {
6419                    return Ok(ConstValue::Type(ty));
6420                }
6421                // 3. Comptime type variables from the outer analysis context
6422                //    (e.g. `let P = make_point()` in the enclosing function).
6423                if let Some(&ty) = ctx.comptime_type_vars.get(&name) {
6424                    return Ok(ConstValue::Type(ty));
6425                }
6426                // 4. Built-in type names used as values (e.g. `i32` in `identity(i32, 42)`).
6427                let name_str = self.interner.resolve(&name).to_string();
6428                let builtin_ty = match name_str.as_str() {
6429                    "i8" => Some(Type::I8),
6430                    "i16" => Some(Type::I16),
6431                    "i32" => Some(Type::I32),
6432                    "i64" => Some(Type::I64),
6433                    "u8" => Some(Type::U8),
6434                    "u16" => Some(Type::U16),
6435                    "u32" => Some(Type::U32),
6436                    "u64" => Some(Type::U64),
6437                    "bool" => Some(Type::BOOL),
6438                    "()" => Some(Type::UNIT),
6439                    "!" => Some(Type::NEVER),
6440                    _ => None,
6441                };
6442                if let Some(ty) = builtin_ty {
6443                    return Ok(ConstValue::Type(ty));
6444                }
6445                // 5. User-defined struct/enum types used as values.
6446                if let Some(&struct_id) = self.structs.get(&name) {
6447                    return Ok(ConstValue::Type(Type::new_struct(struct_id)));
6448                }
6449                if let Some(&enum_id) = self.enums.get(&name) {
6450                    return Ok(ConstValue::Type(Type::new_enum(enum_id)));
6451                }
6452                // 6. Not a known comptime value — must be a runtime variable.
6453                Err(not_const(inst_span))
6454            }
6455
6456            // ── Assignment ────────────────────────────────────────────────────
6457            InstData::Assign { name, value } => {
6458                let val = self.evaluate_comptime_inst(value, locals, ctx, outer_span)?;
6459                locals.insert(name, val);
6460                Ok(ConstValue::Unit)
6461            }
6462
6463            // ── Branch (if/else) ──────────────────────────────────────────────
6464            InstData::Branch {
6465                cond,
6466                then_block,
6467                else_block,
6468            } => {
6469                let cond_val = self.evaluate_comptime_inst(cond, locals, ctx, outer_span)?;
6470                match cond_val {
6471                    ConstValue::Bool(true) => {
6472                        self.evaluate_comptime_inst(then_block, locals, ctx, outer_span)
6473                    }
6474                    ConstValue::Bool(false) => {
6475                        if let Some(else_ref) = else_block {
6476                            self.evaluate_comptime_inst(else_ref, locals, ctx, outer_span)
6477                        } else {
6478                            Ok(ConstValue::Unit)
6479                        }
6480                    }
6481                    _ => Err(not_const(inst_span)),
6482                }
6483            }
6484
6485            // ── Nested comptime ───────────────────────────────────────────────
6486            InstData::Comptime { expr } => {
6487                self.evaluate_comptime_inst(expr, locals, ctx, outer_span)
6488            }
6489
6490            // ── Declarations are no-ops in comptime context ───────────────────
6491            InstData::FnDecl { .. }
6492            | InstData::DropFnDecl { .. }
6493            | InstData::ConstDecl { .. }
6494            | InstData::StructDecl { .. }
6495            | InstData::EnumDecl { .. } => Ok(ConstValue::Unit),
6496
6497            // ── Type-related: delegate to existing evaluator ──────────────────
6498            // AnonStructType and TypeConst need the full try_evaluate_const
6499            // logic (type registry lookups, structural equality, etc.).
6500            InstData::AnonStructType { .. }
6501            | InstData::AnonEnumType { .. }
6502            | InstData::TypeConst { .. } => self
6503                .try_evaluate_const(inst_ref)
6504                .ok_or_else(|| not_const(inst_span)),
6505
6506            // ── While loop ────────────────────────────────────────────────────
6507            // `while cond { body }` — evaluates until condition is false.
6508            InstData::Loop { cond, body } => {
6509                const COMPTIME_MAX_STEPS: u64 = 1_000_000;
6510                loop {
6511                    let cond_val = self.evaluate_comptime_inst(cond, locals, ctx, outer_span)?;
6512                    if !bool_val(cond_val, inst_span)? {
6513                        break;
6514                    }
6515                    self.comptime_steps_used += 1;
6516                    if self.comptime_steps_used > COMPTIME_MAX_STEPS {
6517                        return Err(CompileError::new(
6518                            ErrorKind::ComptimeEvaluationFailed {
6519                                reason: format!(
6520                                    "comptime evaluation exceeded step budget of {} iterations",
6521                                    COMPTIME_MAX_STEPS
6522                                ),
6523                            },
6524                            inst_span,
6525                        ));
6526                    }
6527                    match self.evaluate_comptime_inst(body, locals, ctx, outer_span)? {
6528                        ConstValue::BreakSignal => break,
6529                        ConstValue::ContinueSignal => continue,
6530                        _ => {}
6531                    }
6532                }
6533                Ok(ConstValue::Unit)
6534            }
6535
6536            // ── For-in loop ──────────────────────────────────────────────────
6537            // Not supported in comptime context (desugared to while at runtime).
6538            InstData::For { .. } => Err(not_const(inst_span)),
6539
6540            // ── Infinite loop ─────────────────────────────────────────────────
6541            // `loop { body }` — runs until a break (or step budget exceeded).
6542            InstData::InfiniteLoop { body } => {
6543                const COMPTIME_MAX_STEPS: u64 = 1_000_000;
6544                loop {
6545                    self.comptime_steps_used += 1;
6546                    if self.comptime_steps_used > COMPTIME_MAX_STEPS {
6547                        return Err(CompileError::new(
6548                            ErrorKind::ComptimeEvaluationFailed {
6549                                reason: format!(
6550                                    "comptime evaluation exceeded step budget of {} iterations",
6551                                    COMPTIME_MAX_STEPS
6552                                ),
6553                            },
6554                            inst_span,
6555                        ));
6556                    }
6557                    match self.evaluate_comptime_inst(body, locals, ctx, outer_span)? {
6558                        ConstValue::BreakSignal => break,
6559                        ConstValue::ContinueSignal => continue,
6560                        _ => {}
6561                    }
6562                }
6563                Ok(ConstValue::Unit)
6564            }
6565
6566            // ── Break / Continue ──────────────────────────────────────────────
6567            InstData::Break => Ok(ConstValue::BreakSignal),
6568            InstData::Continue => Ok(ConstValue::ContinueSignal),
6569
6570            // ── Return ────────────────────────────────────────────────────────
6571            // `return expr` or bare `return` inside a comptime function.
6572            // Stores the return value in a side channel then signals the Call handler.
6573            InstData::Ret(opt_ref) => {
6574                let return_val = match opt_ref {
6575                    Some(val_ref) => {
6576                        self.evaluate_comptime_inst(val_ref, locals, ctx, outer_span)?
6577                    }
6578                    None => ConstValue::Unit,
6579                };
6580                self.comptime_return_value = Some(return_val);
6581                Ok(ConstValue::ReturnSignal)
6582            }
6583
6584            // ── Function call ─────────────────────────────────────────────────
6585            // Evaluate the callee's body with the arguments bound as locals.
6586            InstData::Call {
6587                name,
6588                args_start,
6589                args_len,
6590            } => {
6591                const COMPTIME_CALL_DEPTH_LIMIT: u32 = 64;
6592
6593                // Look up the function in the function table.
6594                let fn_info = match self.functions.get(&name) {
6595                    Some(info) => *info,
6596                    None => return Err(not_const(inst_span)),
6597                };
6598
6599                // Evaluate all arguments before entering the callee frame.
6600                let call_args = self.rir.get_call_args(args_start, args_len);
6601                let mut arg_values = Vec::with_capacity(call_args.len());
6602                for call_arg in &call_args {
6603                    let val =
6604                        self.evaluate_comptime_inst(call_arg.value, locals, ctx, outer_span)?;
6605                    arg_values.push(val);
6606                }
6607
6608                // For generic functions, extract type parameter bindings from
6609                // comptime arguments and set them as type overrides so that
6610                // struct/enum resolution inside the callee body can find them.
6611                let param_comptime = self.param_arena.comptime(fn_info.params).to_vec();
6612                let param_names = self.param_arena.names(fn_info.params).to_vec();
6613                let mut type_overrides: HashMap<Spur, Type> = HashMap::new();
6614                if fn_info.is_generic {
6615                    for (i, is_comptime) in param_comptime.iter().enumerate() {
6616                        if *is_comptime && let Some(ConstValue::Type(ty)) = arg_values.get(i) {
6617                            type_overrides.insert(param_names[i], *ty);
6618                        }
6619                    }
6620                }
6621
6622                // Enforce call stack depth limit to prevent infinite recursion.
6623                if self.comptime_call_depth >= COMPTIME_CALL_DEPTH_LIMIT {
6624                    return Err(CompileError::new(
6625                        ErrorKind::ComptimeEvaluationFailed {
6626                            reason: format!(
6627                                "comptime call stack depth exceeded {} levels (possible infinite recursion)",
6628                                COMPTIME_CALL_DEPTH_LIMIT
6629                            ),
6630                        },
6631                        inst_span,
6632                    ));
6633                }
6634
6635                // Bind non-comptime parameters to argument values in a fresh call frame.
6636                // Comptime (type) parameters are not bound as locals — they are
6637                // available through comptime_type_overrides.
6638                let mut call_locals: HashMap<Spur, ConstValue> =
6639                    HashMap::with_capacity(param_names.len());
6640                for (i, (param_name, arg_val)) in
6641                    param_names.iter().zip(arg_values.iter()).enumerate()
6642                {
6643                    if !param_comptime.get(i).copied().unwrap_or(false) {
6644                        call_locals.insert(*param_name, *arg_val);
6645                    }
6646                }
6647
6648                // Push type overrides for the duration of this call.
6649                let saved_overrides =
6650                    std::mem::replace(&mut self.comptime_type_overrides, type_overrides);
6651
6652                // Execute the callee body.
6653                self.comptime_call_depth += 1;
6654                let body_result =
6655                    self.evaluate_comptime_inst(fn_info.body, &mut call_locals, ctx, outer_span);
6656                self.comptime_call_depth -= 1;
6657
6658                // Restore previous type overrides.
6659                self.comptime_type_overrides = saved_overrides;
6660
6661                let body_result = body_result?;
6662
6663                // Consume any return signal; fall through on plain values.
6664                match body_result {
6665                    ConstValue::ReturnSignal => {
6666                        // `return val` was executed — take the stored value.
6667                        self.comptime_return_value.take().ok_or_else(|| {
6668                            CompileError::new(
6669                                ErrorKind::ComptimeEvaluationFailed {
6670                                    reason: "comptime return signal missing its value".into(),
6671                                },
6672                                inst_span,
6673                            )
6674                        })
6675                    }
6676                    ConstValue::BreakSignal | ConstValue::ContinueSignal => {
6677                        // break/continue escaped a function body — syntax error in Gruel.
6678                        Err(CompileError::new(
6679                            ErrorKind::ComptimeEvaluationFailed {
6680                                reason: "break/continue outside a loop in comptime function".into(),
6681                            },
6682                            inst_span,
6683                        ))
6684                    }
6685                    val => Ok(val),
6686                }
6687            }
6688
6689            // ── Struct construction ───────────────────────────────────────────
6690            InstData::StructInit {
6691                module,
6692                type_name,
6693                fields_start,
6694                fields_len,
6695            } => {
6696                // Module-qualified struct literals are not supported in comptime.
6697                if module.is_some() {
6698                    return Err(not_const(inst_span));
6699                }
6700
6701                // Resolve the struct type by name (also checks comptime type overrides).
6702                let struct_id = match self.resolve_comptime_struct(type_name, ctx) {
6703                    Some(id) => id,
6704                    None => return Err(not_const(inst_span)),
6705                };
6706
6707                // Get the struct definition to know field declaration order.
6708                let struct_def = self.type_pool.struct_def(struct_id);
6709                let field_count = struct_def.fields.len();
6710
6711                // Build a map from field name string to declaration index.
6712                let field_index_map: std::collections::HashMap<String, usize> = struct_def
6713                    .fields
6714                    .iter()
6715                    .enumerate()
6716                    .map(|(i, f)| (f.name.clone(), i))
6717                    .collect();
6718
6719                // Retrieve field initializers from the RIR (may be in any source order).
6720                let field_inits = self.rir.get_field_inits(fields_start, fields_len);
6721
6722                // Evaluate each field expression and place it in declaration order.
6723                let mut field_values = vec![ConstValue::Unit; field_count];
6724                for (field_name_sym, field_value_ref) in &field_inits {
6725                    let field_name = self.interner.resolve(field_name_sym).to_string();
6726                    let idx = match field_index_map.get(&field_name) {
6727                        Some(&i) => i,
6728                        None => return Err(not_const(inst_span)),
6729                    };
6730                    let val =
6731                        self.evaluate_comptime_inst(*field_value_ref, locals, ctx, outer_span)?;
6732                    field_values[idx] = val;
6733                }
6734
6735                // Allocate a new heap item and return its index.
6736                let heap_idx = self.comptime_heap.len() as u32;
6737                self.comptime_heap.push(ComptimeHeapItem::Struct {
6738                    struct_id,
6739                    fields: field_values,
6740                });
6741                Ok(ConstValue::Struct(heap_idx))
6742            }
6743
6744            // ── Field access ──────────────────────────────────────────────────
6745            InstData::FieldGet { base, field } => {
6746                let base_val = self.evaluate_comptime_inst(base, locals, ctx, outer_span)?;
6747                match base_val {
6748                    ConstValue::Struct(heap_idx) => {
6749                        // Clone data out to release the heap borrow before calling struct_def.
6750                        let (struct_id, fields) = match &self.comptime_heap[heap_idx as usize] {
6751                            ComptimeHeapItem::Struct { struct_id, fields } => {
6752                                (*struct_id, fields.clone())
6753                            }
6754                            _ => return Err(not_const(inst_span)),
6755                        };
6756                        let struct_def = self.type_pool.struct_def(struct_id);
6757                        let field_name = self.interner.resolve(&field);
6758                        let (field_idx, _) =
6759                            struct_def.find_field(field_name).ok_or_else(|| {
6760                                CompileError::new(
6761                                    ErrorKind::ComptimeEvaluationFailed {
6762                                        reason: format!(
6763                                            "unknown field '{}' in comptime struct",
6764                                            field_name
6765                                        ),
6766                                    },
6767                                    inst_span,
6768                                )
6769                            })?;
6770                        Ok(fields[field_idx])
6771                    }
6772                    _ => Err(not_const(inst_span)),
6773                }
6774            }
6775
6776            // ── Array construction ────────────────────────────────────────────
6777            InstData::ArrayInit {
6778                elems_start,
6779                elems_len,
6780            } => {
6781                let elem_refs = self.rir.get_inst_refs(elems_start, elems_len);
6782                let mut elem_values = Vec::with_capacity(elem_refs.len());
6783                for elem_ref in &elem_refs {
6784                    let val = self.evaluate_comptime_inst(*elem_ref, locals, ctx, outer_span)?;
6785                    elem_values.push(val);
6786                }
6787                let heap_idx = self.comptime_heap.len() as u32;
6788                self.comptime_heap
6789                    .push(ComptimeHeapItem::Array(elem_values));
6790                Ok(ConstValue::Array(heap_idx))
6791            }
6792
6793            // ── Array index read ──────────────────────────────────────────────
6794            InstData::IndexGet { base, index } => {
6795                let base_val = self.evaluate_comptime_inst(base, locals, ctx, outer_span)?;
6796                let index_val = self.evaluate_comptime_inst(index, locals, ctx, outer_span)?;
6797                match base_val {
6798                    ConstValue::Array(heap_idx) => {
6799                        let idx = int(index_val, inst_span)?;
6800                        // Clone elements to release heap borrow before error construction.
6801                        let elems = match &self.comptime_heap[heap_idx as usize] {
6802                            ComptimeHeapItem::Array(elems) => elems.clone(),
6803                            _ => return Err(not_const(inst_span)),
6804                        };
6805                        if idx < 0 || idx as usize >= elems.len() {
6806                            return Err(CompileError::new(
6807                                ErrorKind::ComptimeEvaluationFailed {
6808                                    reason: format!(
6809                                        "array index {} out of bounds (length {})",
6810                                        idx,
6811                                        elems.len()
6812                                    ),
6813                                },
6814                                inst_span,
6815                            ));
6816                        }
6817                        Ok(elems[idx as usize])
6818                    }
6819                    _ => Err(not_const(inst_span)),
6820                }
6821            }
6822
6823            // ── Field mutation ────────────────────────────────────────────────
6824            InstData::FieldSet { base, field, value } => {
6825                // base must be a VarRef to a local holding a ConstValue::Struct(heap_idx).
6826                let var_name = match &self.rir.get(base).data {
6827                    InstData::VarRef { name } => *name,
6828                    _ => return Err(not_const(inst_span)),
6829                };
6830                let heap_idx = match locals.get(&var_name) {
6831                    Some(ConstValue::Struct(idx)) => *idx,
6832                    _ => return Err(not_const(inst_span)),
6833                };
6834                let val = self.evaluate_comptime_inst(value, locals, ctx, outer_span)?;
6835                // Resolve field index from struct definition.
6836                let struct_id = match &self.comptime_heap[heap_idx as usize] {
6837                    ComptimeHeapItem::Struct { struct_id, .. } => *struct_id,
6838                    _ => return Err(not_const(inst_span)),
6839                };
6840                let struct_def = self.type_pool.struct_def(struct_id);
6841                let field_name = self.interner.resolve(&field);
6842                let (field_idx, _) = struct_def.find_field(field_name).ok_or_else(|| {
6843                    CompileError::new(
6844                        ErrorKind::ComptimeEvaluationFailed {
6845                            reason: format!("unknown field '{}' in comptime struct", field_name),
6846                        },
6847                        inst_span,
6848                    )
6849                })?;
6850                // Mutate the heap item in place.
6851                match &mut self.comptime_heap[heap_idx as usize] {
6852                    ComptimeHeapItem::Struct { fields, .. } => {
6853                        fields[field_idx] = val;
6854                    }
6855                    _ => return Err(not_const(inst_span)),
6856                }
6857                Ok(ConstValue::Unit)
6858            }
6859
6860            // ── Array element mutation ───────────────────────────────────────────
6861            InstData::IndexSet { base, index, value } => {
6862                // base must be a VarRef to a local holding a ConstValue::Array(heap_idx).
6863                let var_name = match &self.rir.get(base).data {
6864                    InstData::VarRef { name } => *name,
6865                    _ => return Err(not_const(inst_span)),
6866                };
6867                let heap_idx = match locals.get(&var_name) {
6868                    Some(ConstValue::Array(idx)) => *idx,
6869                    _ => return Err(not_const(inst_span)),
6870                };
6871                let idx = int(
6872                    self.evaluate_comptime_inst(index, locals, ctx, outer_span)?,
6873                    inst_span,
6874                )?;
6875                let val = self.evaluate_comptime_inst(value, locals, ctx, outer_span)?;
6876                // Bounds check and mutate.
6877                let len = match &self.comptime_heap[heap_idx as usize] {
6878                    ComptimeHeapItem::Array(elems) => elems.len(),
6879                    _ => return Err(not_const(inst_span)),
6880                };
6881                if idx < 0 || idx as usize >= len {
6882                    return Err(CompileError::new(
6883                        ErrorKind::ComptimeEvaluationFailed {
6884                            reason: format!("array index {} out of bounds (length {})", idx, len),
6885                        },
6886                        inst_span,
6887                    ));
6888                }
6889                match &mut self.comptime_heap[heap_idx as usize] {
6890                    ComptimeHeapItem::Array(elems) => {
6891                        elems[idx as usize] = val;
6892                    }
6893                    _ => return Err(not_const(inst_span)),
6894                }
6895                Ok(ConstValue::Unit)
6896            }
6897
6898            // ── Unit enum variant ──────────────────────────────────────────────
6899            InstData::EnumVariant {
6900                module: _,
6901                type_name,
6902                variant,
6903            } => {
6904                // Resolve enum ID — check direct enums, then comptime type vars.
6905                let enum_id = if let Some(&id) = self.enums.get(&type_name) {
6906                    id
6907                } else if let Some(&ty) = ctx.comptime_type_vars.get(&type_name) {
6908                    match ty.kind() {
6909                        TypeKind::Enum(id) => id,
6910                        _ => return Err(not_const(inst_span)),
6911                    }
6912                } else {
6913                    return Err(not_const(inst_span));
6914                };
6915                let enum_def = self.type_pool.enum_def(enum_id);
6916                let variant_name = self.interner.resolve(&variant);
6917                let variant_idx = enum_def
6918                    .find_variant(variant_name)
6919                    .ok_or_else(|| not_const(inst_span))? as u32;
6920                Ok(ConstValue::EnumVariant {
6921                    enum_id,
6922                    variant_idx,
6923                })
6924            }
6925
6926            // ── Struct-style enum variant ─────────────────────────────────────────
6927            InstData::EnumStructVariant {
6928                module: _,
6929                type_name,
6930                variant,
6931                fields_start,
6932                fields_len,
6933            } => {
6934                // Resolve enum ID.
6935                let enum_id = if let Some(&id) = self.enums.get(&type_name) {
6936                    id
6937                } else if let Some(&ty) = ctx.comptime_type_vars.get(&type_name) {
6938                    match ty.kind() {
6939                        TypeKind::Enum(id) => id,
6940                        _ => return Err(not_const(inst_span)),
6941                    }
6942                } else {
6943                    return Err(not_const(inst_span));
6944                };
6945                let enum_def = self.type_pool.enum_def(enum_id);
6946                let variant_name = self.interner.resolve(&variant);
6947                let variant_idx = enum_def
6948                    .find_variant(variant_name)
6949                    .ok_or_else(|| not_const(inst_span))? as u32;
6950                let variant_def = &enum_def.variants[variant_idx as usize];
6951
6952                // Get field initializers and resolve to declaration order.
6953                let field_inits = self.rir.get_field_inits(fields_start, fields_len);
6954                let mut field_values = vec![ConstValue::Unit; variant_def.fields.len()];
6955                for (init_field_name, field_value_ref) in &field_inits {
6956                    let field_name_str = self.interner.resolve(init_field_name);
6957                    let field_idx = variant_def
6958                        .find_field(field_name_str)
6959                        .ok_or_else(|| not_const(inst_span))?;
6960                    let val =
6961                        self.evaluate_comptime_inst(*field_value_ref, locals, ctx, outer_span)?;
6962                    field_values[field_idx] = val;
6963                }
6964
6965                let heap_idx = self.comptime_heap.len() as u32;
6966                self.comptime_heap
6967                    .push(ComptimeHeapItem::EnumStruct(field_values));
6968                Ok(ConstValue::EnumStruct {
6969                    enum_id,
6970                    variant_idx,
6971                    heap_idx,
6972                })
6973            }
6974
6975            // ── Tuple data enum variant (via AssocFnCall) ─────────────────────────
6976            InstData::AssocFnCall {
6977                type_name,
6978                function,
6979                args_start,
6980                args_len,
6981            } => {
6982                // Check if this is an enum data variant construction.
6983                let enum_id = if let Some(&id) = self.enums.get(&type_name) {
6984                    Some(id)
6985                } else if let Some(&ty) = ctx.comptime_type_vars.get(&type_name) {
6986                    match ty.kind() {
6987                        TypeKind::Enum(id) => Some(id),
6988                        _ => None,
6989                    }
6990                } else {
6991                    None
6992                };
6993
6994                if let Some(enum_id) = enum_id {
6995                    let enum_def = self.type_pool.enum_def(enum_id);
6996                    let variant_name = self.interner.resolve(&function);
6997                    if let Some(variant_idx) = enum_def.find_variant(variant_name) {
6998                        let variant_def = &enum_def.variants[variant_idx];
6999                        if variant_def.has_data() && !variant_def.is_struct_variant() {
7000                            // Tuple data variant: evaluate arguments.
7001                            let call_args = self.rir.get_call_args(args_start, args_len);
7002                            let mut field_values = Vec::with_capacity(variant_def.fields.len());
7003                            for arg in &call_args {
7004                                let val = self
7005                                    .evaluate_comptime_inst(arg.value, locals, ctx, outer_span)?;
7006                                field_values.push(val);
7007                            }
7008                            let heap_idx = self.comptime_heap.len() as u32;
7009                            self.comptime_heap
7010                                .push(ComptimeHeapItem::EnumData(field_values));
7011                            return Ok(ConstValue::EnumData {
7012                                enum_id,
7013                                variant_idx: variant_idx as u32,
7014                                heap_idx,
7015                            });
7016                        }
7017                    }
7018                }
7019
7020                // Not an enum data variant — unsupported in comptime.
7021                Err(not_const(inst_span))
7022            }
7023
7024            // ── Pattern matching ───────────────────────────────────────────────
7025            InstData::Match {
7026                scrutinee,
7027                arms_start,
7028                arms_len,
7029            } => {
7030                let scrut_val = self.evaluate_comptime_inst(scrutinee, locals, ctx, outer_span)?;
7031                let arms = self.rir.get_match_arms(arms_start, arms_len);
7032
7033                for (pattern, body) in &arms {
7034                    match pattern {
7035                        RirPattern::Wildcard(_) => {
7036                            // Always matches — evaluate body directly.
7037                            return self.evaluate_comptime_inst(*body, locals, ctx, outer_span);
7038                        }
7039                        RirPattern::Int(n, _) => {
7040                            if let ConstValue::Integer(val) = scrut_val
7041                                && val == *n
7042                            {
7043                                return self.evaluate_comptime_inst(*body, locals, ctx, outer_span);
7044                            }
7045                        }
7046                        RirPattern::Bool(b, _) => {
7047                            if let ConstValue::Bool(val) = scrut_val
7048                                && val == *b
7049                            {
7050                                return self.evaluate_comptime_inst(*body, locals, ctx, outer_span);
7051                            }
7052                        }
7053                        RirPattern::Path {
7054                            type_name, variant, ..
7055                        } => {
7056                            // Match unit enum variant by name.
7057                            let pat_enum_id = self.resolve_comptime_enum(*type_name, ctx);
7058                            if let Some(pat_enum_id) = pat_enum_id {
7059                                let enum_def = self.type_pool.enum_def(pat_enum_id);
7060                                let variant_name = self.interner.resolve(variant);
7061                                if let Some(pat_variant_idx) = enum_def.find_variant(variant_name) {
7062                                    let matches = match scrut_val {
7063                                        ConstValue::EnumVariant {
7064                                            enum_id,
7065                                            variant_idx,
7066                                        } => {
7067                                            enum_id == pat_enum_id
7068                                                && variant_idx == pat_variant_idx as u32
7069                                        }
7070                                        ConstValue::EnumData {
7071                                            enum_id,
7072                                            variant_idx,
7073                                            ..
7074                                        } => {
7075                                            enum_id == pat_enum_id
7076                                                && variant_idx == pat_variant_idx as u32
7077                                        }
7078                                        ConstValue::EnumStruct {
7079                                            enum_id,
7080                                            variant_idx,
7081                                            ..
7082                                        } => {
7083                                            enum_id == pat_enum_id
7084                                                && variant_idx == pat_variant_idx as u32
7085                                        }
7086                                        _ => false,
7087                                    };
7088                                    if matches {
7089                                        return self.evaluate_comptime_inst(
7090                                            *body, locals, ctx, outer_span,
7091                                        );
7092                                    }
7093                                }
7094                            }
7095                        }
7096                        RirPattern::DataVariant {
7097                            type_name,
7098                            variant,
7099                            bindings,
7100                            ..
7101                        } => {
7102                            // Match tuple data variant and bind fields.
7103                            let pat_enum_id = self.resolve_comptime_enum(*type_name, ctx);
7104                            if let Some(pat_enum_id) = pat_enum_id {
7105                                let enum_def = self.type_pool.enum_def(pat_enum_id);
7106                                let variant_name = self.interner.resolve(variant);
7107                                if let Some(pat_variant_idx) = enum_def.find_variant(variant_name) {
7108                                    let (matches, heap_idx_opt) = match scrut_val {
7109                                        ConstValue::EnumData {
7110                                            enum_id,
7111                                            variant_idx,
7112                                            heap_idx,
7113                                        } if enum_id == pat_enum_id
7114                                            && variant_idx == pat_variant_idx as u32 =>
7115                                        {
7116                                            (true, Some(heap_idx))
7117                                        }
7118                                        _ => (false, None),
7119                                    };
7120                                    if matches {
7121                                        // Bind fields into locals.
7122                                        if let Some(heap_idx) = heap_idx_opt {
7123                                            let field_values =
7124                                                match &self.comptime_heap[heap_idx as usize] {
7125                                                    ComptimeHeapItem::EnumData(fields) => {
7126                                                        fields.clone()
7127                                                    }
7128                                                    _ => return Err(not_const(inst_span)),
7129                                                };
7130                                            for (i, binding) in bindings.iter().enumerate() {
7131                                                if !binding.is_wildcard
7132                                                    && let Some(name) = binding.name
7133                                                {
7134                                                    locals.insert(name, field_values[i]);
7135                                                }
7136                                            }
7137                                        }
7138                                        return self.evaluate_comptime_inst(
7139                                            *body, locals, ctx, outer_span,
7140                                        );
7141                                    }
7142                                }
7143                            }
7144                        }
7145                        RirPattern::StructVariant {
7146                            type_name,
7147                            variant,
7148                            field_bindings,
7149                            ..
7150                        } => {
7151                            // Match struct variant and bind named fields.
7152                            let pat_enum_id = self.resolve_comptime_enum(*type_name, ctx);
7153                            if let Some(pat_enum_id) = pat_enum_id {
7154                                let enum_def = self.type_pool.enum_def(pat_enum_id);
7155                                let variant_name = self.interner.resolve(variant);
7156                                if let Some(pat_variant_idx) = enum_def.find_variant(variant_name) {
7157                                    let (matches, heap_idx_opt) = match scrut_val {
7158                                        ConstValue::EnumStruct {
7159                                            enum_id,
7160                                            variant_idx,
7161                                            heap_idx,
7162                                        } if enum_id == pat_enum_id
7163                                            && variant_idx == pat_variant_idx as u32 =>
7164                                        {
7165                                            (true, Some(heap_idx))
7166                                        }
7167                                        _ => (false, None),
7168                                    };
7169                                    if matches {
7170                                        if let Some(heap_idx) = heap_idx_opt {
7171                                            let field_values =
7172                                                match &self.comptime_heap[heap_idx as usize] {
7173                                                    ComptimeHeapItem::EnumStruct(fields) => {
7174                                                        fields.clone()
7175                                                    }
7176                                                    _ => return Err(not_const(inst_span)),
7177                                                };
7178                                            let variant_def = &enum_def.variants[pat_variant_idx];
7179                                            for fb in field_bindings {
7180                                                if !fb.binding.is_wildcard
7181                                                    && let Some(name) = fb.binding.name
7182                                                {
7183                                                    let field_name_str =
7184                                                        self.interner.resolve(&fb.field_name);
7185                                                    let field_idx = match variant_def
7186                                                        .find_field(field_name_str)
7187                                                    {
7188                                                        Some(idx) => idx,
7189                                                        None => return Err(not_const(inst_span)),
7190                                                    };
7191                                                    locals.insert(name, field_values[field_idx]);
7192                                                }
7193                                            }
7194                                        }
7195                                        return self.evaluate_comptime_inst(
7196                                            *body, locals, ctx, outer_span,
7197                                        );
7198                                    }
7199                                }
7200                            }
7201                        }
7202                    }
7203                }
7204
7205                // No arm matched — should not happen after exhaustiveness checking.
7206                Err(CompileError::new(
7207                    ErrorKind::ComptimeEvaluationFailed {
7208                        reason: "no match arm matched in comptime evaluation".into(),
7209                    },
7210                    inst_span,
7211                ))
7212            }
7213
7214            // ── Struct destructuring ─────────────────────────────────────────
7215            InstData::StructDestructure {
7216                type_name,
7217                fields_start,
7218                fields_len,
7219                init,
7220            } => {
7221                // Evaluate the initializer to a struct value.
7222                let init_val = self.evaluate_comptime_inst(init, locals, ctx, outer_span)?;
7223                let heap_idx = match init_val {
7224                    ConstValue::Struct(idx) => idx,
7225                    _ => return Err(not_const(inst_span)),
7226                };
7227
7228                // Resolve the struct type.
7229                let struct_id = match self.resolve_comptime_struct(type_name, ctx) {
7230                    Some(id) => id,
7231                    None => return Err(not_const(inst_span)),
7232                };
7233
7234                // Get field values from the heap.
7235                let field_values = match &self.comptime_heap[heap_idx as usize] {
7236                    ComptimeHeapItem::Struct { fields, .. } => fields.clone(),
7237                    _ => return Err(not_const(inst_span)),
7238                };
7239
7240                // Get the struct definition for field name lookup.
7241                let struct_def = self.type_pool.struct_def(struct_id);
7242                let field_name_to_idx: HashMap<String, usize> = struct_def
7243                    .fields
7244                    .iter()
7245                    .enumerate()
7246                    .map(|(i, f)| (f.name.clone(), i))
7247                    .collect();
7248
7249                // Bind each field into locals.
7250                let destr_fields = self.rir.get_destructure_fields(fields_start, fields_len);
7251                for field in &destr_fields {
7252                    if field.is_wildcard {
7253                        continue;
7254                    }
7255                    let field_name = self.interner.resolve(&field.field_name).to_string();
7256                    let field_idx = match field_name_to_idx.get(&field_name) {
7257                        Some(&idx) => idx,
7258                        None => return Err(not_const(inst_span)),
7259                    };
7260                    let binding_name = field.binding_name.unwrap_or(field.field_name);
7261                    locals.insert(binding_name, field_values[field_idx]);
7262                }
7263                Ok(ConstValue::Unit)
7264            }
7265
7266            // ── Method call ─────────────────────────────────────────────────────
7267            InstData::MethodCall {
7268                receiver,
7269                method,
7270                args_start,
7271                args_len,
7272            } => {
7273                const COMPTIME_CALL_DEPTH_LIMIT: u32 = 64;
7274
7275                // Evaluate the receiver to get the value.
7276                let receiver_val =
7277                    self.evaluate_comptime_inst(receiver, locals, ctx, outer_span)?;
7278
7279                // Handle comptime_str method dispatch.
7280                if let ConstValue::ComptimeStr(str_idx) = receiver_val {
7281                    let method_name = self.interner.resolve(&method).to_string();
7282                    let call_args = self.rir.get_call_args(args_start, args_len);
7283                    return self.evaluate_comptime_str_method(
7284                        str_idx,
7285                        &method_name,
7286                        &call_args,
7287                        locals,
7288                        ctx,
7289                        outer_span,
7290                        inst_span,
7291                    );
7292                }
7293
7294                // Determine the struct type from the receiver value.
7295                let struct_id = match receiver_val {
7296                    ConstValue::Struct(heap_idx) => match &self.comptime_heap[heap_idx as usize] {
7297                        ComptimeHeapItem::Struct { struct_id, .. } => *struct_id,
7298                        _ => return Err(not_const(inst_span)),
7299                    },
7300                    _ => return Err(not_const(inst_span)),
7301                };
7302
7303                // Look up the method.
7304                let method_key = (struct_id, method);
7305                let method_info = match self.methods.get(&method_key) {
7306                    Some(info) => *info,
7307                    None => return Err(not_const(inst_span)),
7308                };
7309
7310                // Evaluate all explicit arguments.
7311                let call_args = self.rir.get_call_args(args_start, args_len);
7312                let mut arg_values = Vec::with_capacity(call_args.len());
7313                for call_arg in &call_args {
7314                    let val =
7315                        self.evaluate_comptime_inst(call_arg.value, locals, ctx, outer_span)?;
7316                    arg_values.push(val);
7317                }
7318
7319                // Enforce call stack depth limit.
7320                if self.comptime_call_depth >= COMPTIME_CALL_DEPTH_LIMIT {
7321                    return Err(CompileError::new(
7322                        ErrorKind::ComptimeEvaluationFailed {
7323                            reason: format!(
7324                                "comptime call stack depth exceeded {} levels",
7325                                COMPTIME_CALL_DEPTH_LIMIT
7326                            ),
7327                        },
7328                        inst_span,
7329                    ));
7330                }
7331
7332                // Bind self and parameters.
7333                let param_names = self.param_arena.names(method_info.params).to_vec();
7334                let mut call_locals: HashMap<Spur, ConstValue> =
7335                    HashMap::with_capacity(param_names.len() + 1);
7336                // Bind `self`.
7337                let self_sym = self.interner.get_or_intern("self");
7338                call_locals.insert(self_sym, receiver_val);
7339                for (param_name, arg_val) in param_names.iter().zip(arg_values.iter()) {
7340                    call_locals.insert(*param_name, *arg_val);
7341                }
7342
7343                // Execute the method body.
7344                self.comptime_call_depth += 1;
7345                let body_result = self.evaluate_comptime_inst(
7346                    method_info.body,
7347                    &mut call_locals,
7348                    ctx,
7349                    outer_span,
7350                );
7351                self.comptime_call_depth -= 1;
7352                let body_result = body_result?;
7353
7354                match body_result {
7355                    ConstValue::ReturnSignal => {
7356                        self.comptime_return_value.take().ok_or_else(|| {
7357                            CompileError::new(
7358                                ErrorKind::ComptimeEvaluationFailed {
7359                                    reason: "comptime return signal missing its value".into(),
7360                                },
7361                                inst_span,
7362                            )
7363                        })
7364                    }
7365                    ConstValue::BreakSignal | ConstValue::ContinueSignal => Err(CompileError::new(
7366                        ErrorKind::ComptimeEvaluationFailed {
7367                            reason: "break/continue outside a loop in comptime method".into(),
7368                        },
7369                        inst_span,
7370                    )),
7371                    val => Ok(val),
7372                }
7373            }
7374
7375            // ── Intrinsic ────────────────────────────────────────────────────────
7376            InstData::Intrinsic {
7377                name,
7378                args_start,
7379                args_len,
7380            } => {
7381                // @intCast/@cast are no-ops in comptime since all integers are i64.
7382                if name == self.known.int_cast || name == self.known.cast {
7383                    let arg_refs = self.rir.get_inst_refs(args_start, args_len);
7384                    if arg_refs.len() != 1 {
7385                        return Err(not_const(inst_span));
7386                    }
7387                    return self.evaluate_comptime_inst(arg_refs[0], locals, ctx, outer_span);
7388                }
7389                // @dbg formats values, prints to stderr on-the-fly (unless
7390                // suppressed), appends to the comptime dbg buffer, and queues a
7391                // warning to be emitted after sema completes.
7392                if name == self.known.dbg {
7393                    let arg_refs = self.rir.get_inst_refs(args_start, args_len);
7394                    let mut parts = Vec::with_capacity(arg_refs.len());
7395                    for &arg_ref in &arg_refs {
7396                        let val = self.evaluate_comptime_inst(arg_ref, locals, ctx, outer_span)?;
7397                        parts.push(self.format_const_value(val, inst_span)?);
7398                    }
7399                    let msg = parts.join(" ");
7400                    if !self.suppress_comptime_dbg_print {
7401                        eprintln!("comptime dbg: {msg}");
7402                    }
7403                    self.comptime_dbg_output.push(msg.clone());
7404                    self.comptime_log_output.push((msg, inst_span));
7405                    return Ok(ConstValue::Unit);
7406                }
7407                // @compileError emits a user-defined compile error.
7408                if name == self.known.compile_error {
7409                    let arg_refs = self.rir.get_inst_refs(args_start, args_len);
7410                    if arg_refs.len() != 1 {
7411                        return Err(CompileError::new(
7412                            ErrorKind::IntrinsicWrongArgCount {
7413                                name: "compileError".to_string(),
7414                                expected: 1,
7415                                found: arg_refs.len(),
7416                            },
7417                            inst_span,
7418                        ));
7419                    }
7420                    let msg =
7421                        self.evaluate_comptime_string_arg(arg_refs[0], locals, ctx, outer_span)?;
7422                    return Err(CompileError::new(
7423                        ErrorKind::ComptimeUserError(msg),
7424                        inst_span,
7425                    ));
7426                }
7427                // @range produces a comptime array of integers.
7428                if name == self.known.range {
7429                    let arg_refs = self.rir.get_inst_refs(args_start, args_len);
7430                    let (start, end, stride) = match arg_refs.len() {
7431                        1 => {
7432                            let end = int(
7433                                self.evaluate_comptime_inst(arg_refs[0], locals, ctx, outer_span)?,
7434                                inst_span,
7435                            )?;
7436                            (0i64, end, 1i64)
7437                        }
7438                        2 => {
7439                            let s = int(
7440                                self.evaluate_comptime_inst(arg_refs[0], locals, ctx, outer_span)?,
7441                                inst_span,
7442                            )?;
7443                            let e = int(
7444                                self.evaluate_comptime_inst(arg_refs[1], locals, ctx, outer_span)?,
7445                                inst_span,
7446                            )?;
7447                            (s, e, 1i64)
7448                        }
7449                        3 => {
7450                            let s = int(
7451                                self.evaluate_comptime_inst(arg_refs[0], locals, ctx, outer_span)?,
7452                                inst_span,
7453                            )?;
7454                            let e = int(
7455                                self.evaluate_comptime_inst(arg_refs[1], locals, ctx, outer_span)?,
7456                                inst_span,
7457                            )?;
7458                            let st = int(
7459                                self.evaluate_comptime_inst(arg_refs[2], locals, ctx, outer_span)?,
7460                                inst_span,
7461                            )?;
7462                            (s, e, st)
7463                        }
7464                        _ => {
7465                            return Err(CompileError::new(
7466                                ErrorKind::IntrinsicWrongArgCount {
7467                                    name: "range".to_string(),
7468                                    expected: 1,
7469                                    found: arg_refs.len(),
7470                                },
7471                                inst_span,
7472                            ));
7473                        }
7474                    };
7475                    if stride == 0 {
7476                        return Err(CompileError::new(
7477                            ErrorKind::ComptimeEvaluationFailed {
7478                                reason: "@range stride must not be zero".into(),
7479                            },
7480                            inst_span,
7481                        ));
7482                    }
7483                    // Cap the element count to prevent OOM from e.g. @range(i64::MAX)
7484                    const MAX_RANGE_ELEMENTS: usize = 1_000_000;
7485                    let mut elements = Vec::new();
7486                    let mut i = start;
7487                    if stride > 0 {
7488                        while i < end {
7489                            if elements.len() >= MAX_RANGE_ELEMENTS {
7490                                return Err(CompileError::new(
7491                                    ErrorKind::ComptimeEvaluationFailed {
7492                                        reason: format!(
7493                                            "@range produces too many elements (limit is {})",
7494                                            MAX_RANGE_ELEMENTS
7495                                        ),
7496                                    },
7497                                    inst_span,
7498                                ));
7499                            }
7500                            elements.push(ConstValue::Integer(i));
7501                            i = i.checked_add(stride).ok_or_else(|| overflow(inst_span))?;
7502                        }
7503                    } else {
7504                        while i > end {
7505                            if elements.len() >= MAX_RANGE_ELEMENTS {
7506                                return Err(CompileError::new(
7507                                    ErrorKind::ComptimeEvaluationFailed {
7508                                        reason: format!(
7509                                            "@range produces too many elements (limit is {})",
7510                                            MAX_RANGE_ELEMENTS
7511                                        ),
7512                                    },
7513                                    inst_span,
7514                                ));
7515                            }
7516                            elements.push(ConstValue::Integer(i));
7517                            i = i.checked_add(stride).ok_or_else(|| overflow(inst_span))?;
7518                        }
7519                    }
7520                    let idx = self.comptime_heap.len() as u32;
7521                    self.comptime_heap.push(ComptimeHeapItem::Array(elements));
7522                    return Ok(ConstValue::Array(idx));
7523                }
7524                // Unrecognized intrinsic: surface the name in the diagnostic
7525                // rather than the generic "cannot be known at compile time"
7526                // message. In particular, `@compileLog` was removed in favor of
7527                // `@dbg` — reporting the name guides users to the replacement.
7528                let intrinsic_name = self.interner.resolve(&name).to_string();
7529                Err(CompileError::new(
7530                    ErrorKind::UnknownIntrinsic(intrinsic_name),
7531                    inst_span,
7532                ))
7533            }
7534
7535            // ── Type intrinsic (@size_of, @align_of, @typeName, @typeInfo) ──────
7536            InstData::TypeIntrinsic { name, type_arg } => {
7537                let intrinsic_name = self.interner.resolve(&name).to_string();
7538                // Resolve the type argument.
7539                // Check comptime_type_overrides first (for generic type params like T),
7540                // then comptime_type_vars from the analysis context, then fall back
7541                // to the normal type resolver.
7542                let ty = if let Some(&override_ty) = self.comptime_type_overrides.get(&type_arg) {
7543                    override_ty
7544                } else if let Some(&ctx_ty) = ctx.comptime_type_vars.get(&type_arg) {
7545                    ctx_ty
7546                } else if let Some(&var_ty) = locals.iter().find_map(|(k, v)| {
7547                    if *k == type_arg {
7548                        if let ConstValue::Type(t) = v {
7549                            Some(t)
7550                        } else {
7551                            None
7552                        }
7553                    } else {
7554                        None
7555                    }
7556                }) {
7557                    var_ty
7558                } else {
7559                    self.resolve_type(type_arg, inst_span)
7560                        .map_err(|_| not_const(inst_span))?
7561                };
7562                match intrinsic_name.as_str() {
7563                    "size_of" => {
7564                        let slot_count = self.abi_slot_count(ty);
7565                        Ok(ConstValue::Integer((slot_count as i64) * 8))
7566                    }
7567                    "align_of" => {
7568                        let slot_count = self.abi_slot_count(ty);
7569                        Ok(ConstValue::Integer(if slot_count == 0 { 1 } else { 8 }))
7570                    }
7571                    "typeName" => {
7572                        self.evaluate_comptime_type_name(ty, inst_span)
7573                    }
7574                    "typeInfo" => {
7575                        self.evaluate_comptime_type_info(ty, inst_span)
7576                    }
7577                    _ => Err(not_const(inst_span)),
7578                }
7579            }
7580
7581            // ── Not yet supported ─────────────────────────────────────────────
7582            _ => Err(not_const(inst_span)),
7583        }
7584    }
7585
7586    /// Check if an RIR instruction is a VarRef to a comptime type variable.
7587    ///
7588    /// This is used when validating comptime arguments to detect variables
7589    /// that hold comptime type values (e.g., `let P = Point(); ... Line(P)`).
7590    pub(crate) fn is_comptime_type_var(&self, inst_ref: InstRef, ctx: &AnalysisContext) -> bool {
7591        if let InstData::VarRef { name } = &self.rir.get(inst_ref).data {
7592            ctx.comptime_type_vars.contains_key(name)
7593        } else {
7594            false
7595        }
7596    }
7597
7598    /// Resolve an enum type by name during comptime evaluation.
7599    /// Checks direct enums first, then comptime type variables.
7600    fn resolve_comptime_enum(&self, type_name: Spur, ctx: &AnalysisContext) -> Option<EnumId> {
7601        if let Some(&id) = self.enums.get(&type_name) {
7602            Some(id)
7603        } else if let Some(&ty) = self.comptime_type_overrides.get(&type_name) {
7604            match ty.kind() {
7605                TypeKind::Enum(id) => Some(id),
7606                _ => None,
7607            }
7608        } else if let Some(&ty) = ctx.comptime_type_vars.get(&type_name) {
7609            match ty.kind() {
7610                TypeKind::Enum(id) => Some(id),
7611                _ => None,
7612            }
7613        } else {
7614            None
7615        }
7616    }
7617
7618    /// Resolve a struct type by name during comptime evaluation.
7619    /// Checks direct structs first, then comptime type overrides, then comptime type variables.
7620    fn resolve_comptime_struct(&self, type_name: Spur, ctx: &AnalysisContext) -> Option<StructId> {
7621        if let Some(&id) = self.structs.get(&type_name) {
7622            Some(id)
7623        } else if let Some(&ty) = self.comptime_type_overrides.get(&type_name) {
7624            match ty.kind() {
7625                TypeKind::Struct(id) => Some(id),
7626                _ => None,
7627            }
7628        } else if let Some(&ty) = ctx.comptime_type_vars.get(&type_name) {
7629            match ty.kind() {
7630                TypeKind::Struct(id) => Some(id),
7631                _ => None,
7632            }
7633        } else {
7634            None
7635        }
7636    }
7637
7638    /// Check if an RIR instruction is a comparison operation.
7639    ///
7640    /// This is used to detect chained comparisons (e.g., `a < b < c`) which are
7641    /// not allowed in Gruel.
7642    fn is_comparison(&self, inst_ref: InstRef) -> bool {
7643        matches!(
7644            self.rir.get(inst_ref).data,
7645            InstData::Lt { .. }
7646                | InstData::Gt { .. }
7647                | InstData::Le { .. }
7648                | InstData::Ge { .. }
7649                | InstData::Eq { .. }
7650                | InstData::Ne { .. }
7651        )
7652    }
7653
7654    /// Analyze a builtin type associated function call.
7655    ///
7656    /// Dispatches to the appropriate runtime function based on the builtin registry.
7657    fn analyze_builtin_assoc_fn(
7658        &mut self,
7659        air: &mut Air,
7660        ctx: &mut AnalysisContext,
7661        (struct_id, builtin_def): (StructId, &'static BuiltinTypeDef),
7662        function_name: &str,
7663        args: &[RirCallArg],
7664        span: Span,
7665    ) -> CompileResult<AnalysisResult> {
7666        use gruel_builtins::{BuiltinParamType, BuiltinReturnType};
7667
7668        // Look up the associated function in the registry
7669        let assoc_fn = builtin_def
7670            .find_associated_fn(function_name)
7671            .ok_or_else(|| {
7672                CompileError::new(
7673                    ErrorKind::UndefinedAssocFn {
7674                        type_name: builtin_def.name.to_string(),
7675                        function_name: function_name.to_string(),
7676                    },
7677                    span,
7678                )
7679            })?;
7680
7681        // Check argument count
7682        if args.len() != assoc_fn.params.len() {
7683            return Err(CompileError::new(
7684                ErrorKind::WrongArgumentCount {
7685                    expected: assoc_fn.params.len(),
7686                    found: args.len(),
7687                },
7688                span,
7689            ));
7690        }
7691
7692        // Analyze arguments and check types
7693        let mut air_args: Vec<(AirRef, AirArgMode)> = Vec::with_capacity(args.len());
7694        for (i, arg) in args.iter().enumerate() {
7695            let arg_result = self.analyze_inst(air, arg.value, ctx)?;
7696
7697            // Get expected type from param
7698            let expected_ty = match assoc_fn.params[i].ty {
7699                BuiltinParamType::U64 => Type::U64,
7700                BuiltinParamType::U8 => Type::U8,
7701                BuiltinParamType::Bool => Type::BOOL,
7702                BuiltinParamType::SelfType => Type::new_struct(struct_id),
7703            };
7704
7705            // Type check
7706            if arg_result.ty != expected_ty && !arg_result.ty.is_error() {
7707                return Err(CompileError::new(
7708                    ErrorKind::TypeMismatch {
7709                        expected: expected_ty.name().to_string(),
7710                        found: arg_result.ty.name().to_string(),
7711                    },
7712                    span,
7713                ));
7714            }
7715
7716            air_args.push((arg_result.air_ref, AirArgMode::Normal));
7717        }
7718
7719        // Determine return type
7720        // Use builtin_air_type for SelfType to get correct AIR output type
7721        let return_ty = match assoc_fn.return_ty {
7722            BuiltinReturnType::Unit => Type::UNIT,
7723            BuiltinReturnType::U64 => Type::U64,
7724            BuiltinReturnType::U8 => Type::U8,
7725            BuiltinReturnType::Bool => Type::BOOL,
7726            BuiltinReturnType::SelfType => self.builtin_air_type(struct_id),
7727        };
7728
7729        // Generate runtime function call
7730        let call_name = self.interner.get_or_intern(assoc_fn.runtime_fn);
7731
7732        // Encode args into extra array
7733        let mut extra_data: Vec<u32> = Vec::with_capacity(air_args.len() * 2);
7734        for (air_ref, mode) in &air_args {
7735            extra_data.push(air_ref.as_u32());
7736            extra_data.push(mode.as_u32());
7737        }
7738        let args_start = air.add_extra(&extra_data);
7739
7740        let air_ref = air.add_inst(AirInst {
7741            data: AirInstData::Call {
7742                name: call_name,
7743                args_start,
7744                args_len: air_args.len() as u32,
7745            },
7746            ty: return_ty,
7747            span,
7748        });
7749
7750        Ok(AnalysisResult::new(air_ref, return_ty))
7751    }
7752
7753    /// Analyze a builtin type method call.
7754    ///
7755    /// Dispatches to the appropriate runtime function based on the builtin registry.
7756    /// Handles borrow semantics (for query methods) and mutation semantics (for
7757    /// methods that modify the receiver).
7758    fn analyze_builtin_method(
7759        &mut self,
7760        air: &mut Air,
7761        ctx: &mut AnalysisContext,
7762        method_ctx: &BuiltinMethodContext<'_>,
7763        receiver: ReceiverInfo,
7764        args: &[RirCallArg],
7765    ) -> CompileResult<AnalysisResult> {
7766        use gruel_builtins::{BuiltinParamType, BuiltinReturnType, ReceiverMode};
7767
7768        // Look up the method in the registry
7769        let method = method_ctx
7770            .builtin_def
7771            .find_method(method_ctx.method_name)
7772            .ok_or_else(|| {
7773                CompileError::new(
7774                    ErrorKind::UndefinedMethod {
7775                        type_name: method_ctx.builtin_def.name.to_string(),
7776                        method_name: method_ctx.method_name.to_string(),
7777                    },
7778                    method_ctx.span,
7779                )
7780            })?;
7781
7782        // Handle receiver mode (borrow vs mutation vs consume)
7783        match method.receiver_mode {
7784            ReceiverMode::ByRef => {
7785                // Borrow semantics - "unmove" the variable since it's not consumed
7786                if let Some(var_symbol) = receiver.var {
7787                    ctx.moved_vars.remove(&var_symbol);
7788                }
7789            }
7790            ReceiverMode::ByMutRef => {
7791                // Mutation semantics - variable remains valid after
7792                if let Some(var_symbol) = receiver.var {
7793                    ctx.moved_vars.remove(&var_symbol);
7794                }
7795            }
7796            ReceiverMode::ByValue => {
7797                // Consume semantics - variable is moved (already handled by analyze_inst)
7798            }
7799        }
7800
7801        // Check argument count
7802        if args.len() != method.params.len() {
7803            return Err(CompileError::new(
7804                ErrorKind::WrongArgumentCount {
7805                    expected: method.params.len(),
7806                    found: args.len(),
7807                },
7808                method_ctx.span,
7809            ));
7810        }
7811
7812        // Analyze arguments and check types
7813        let mut air_args: Vec<(AirRef, AirArgMode)> = Vec::with_capacity(args.len() + 1);
7814
7815        // Add receiver as first argument
7816        air_args.push((receiver.result.air_ref, AirArgMode::Normal));
7817
7818        // Analyze and add other arguments
7819        for (i, arg) in args.iter().enumerate() {
7820            let arg_result = self.analyze_inst(air, arg.value, ctx)?;
7821
7822            // Get expected type from param
7823            let expected_ty = match method.params[i].ty {
7824                BuiltinParamType::U64 => Type::U64,
7825                BuiltinParamType::U8 => Type::U8,
7826                BuiltinParamType::Bool => Type::BOOL,
7827                BuiltinParamType::SelfType => Type::new_struct(method_ctx.struct_id),
7828            };
7829
7830            // Type check
7831            if arg_result.ty != expected_ty
7832                && !arg_result.ty.is_error()
7833                && !(self.is_builtin_string(arg_result.ty)
7834                    && matches!(method.params[i].ty, BuiltinParamType::SelfType))
7835            {
7836                return Err(CompileError::new(
7837                    ErrorKind::TypeMismatch {
7838                        expected: expected_ty.name().to_string(),
7839                        found: arg_result.ty.name().to_string(),
7840                    },
7841                    method_ctx.span,
7842                ));
7843            }
7844
7845            air_args.push((arg_result.air_ref, AirArgMode::Normal));
7846        }
7847
7848        // Determine return type
7849        // Use builtin_air_type for SelfType to get correct AIR output type
7850        let return_ty = match method.return_ty {
7851            BuiltinReturnType::Unit => Type::UNIT,
7852            BuiltinReturnType::U64 => Type::U64,
7853            BuiltinReturnType::U8 => Type::U8,
7854            BuiltinReturnType::Bool => Type::BOOL,
7855            BuiltinReturnType::SelfType => self.builtin_air_type(method_ctx.struct_id),
7856        };
7857
7858        // Generate runtime function call
7859        let call_name = self.interner.get_or_intern(method.runtime_fn);
7860
7861        // Encode args into extra array
7862        let mut extra_data: Vec<u32> = Vec::with_capacity(air_args.len() * 2);
7863        for (air_ref, mode) in &air_args {
7864            extra_data.push(air_ref.as_u32());
7865            extra_data.push(mode.as_u32());
7866        }
7867        let args_start = air.add_extra(&extra_data);
7868
7869        let call_ref = air.add_inst(AirInst {
7870            data: AirInstData::Call {
7871                name: call_name,
7872                args_start,
7873                args_len: air_args.len() as u32,
7874            },
7875            ty: return_ty,
7876            span: method_ctx.span,
7877        });
7878
7879        // For mutation methods, store the result back to the receiver
7880        if method.receiver_mode == ReceiverMode::ByMutRef {
7881            let storage = receiver.storage.ok_or_else(|| {
7882                CompileError::new(ErrorKind::InvalidAssignmentTarget, method_ctx.span)
7883            })?;
7884            return self.store_string_result(air, call_ref, storage, method_ctx.span);
7885        }
7886
7887        Ok(AnalysisResult::new(call_ref, return_ty))
7888    }
7889
7890    /// Get the storage location for a String receiver in a mutation method call.
7891    ///
7892    /// For mutation methods like `push_str`, `push`, `clear`, `reserve`, we need
7893    /// to know where to store the updated String after the runtime function returns.
7894    ///
7895    /// Returns `Some(storage)` if the receiver is a mutable local or inout parameter.
7896    /// Returns an error if the receiver is:
7897    /// - An immutable binding (`let` instead of `var`)
7898    /// - A borrow parameter (can't mutate borrowed values)
7899    /// - Not an lvalue (e.g., a function call result)
7900    fn get_string_receiver_storage(
7901        &self,
7902        receiver_ref: InstRef,
7903        ctx: &AnalysisContext,
7904        span: Span,
7905    ) -> CompileResult<Option<StringReceiverStorage>> {
7906        let receiver_inst = self.rir.get(receiver_ref);
7907
7908        match &receiver_inst.data {
7909            InstData::VarRef { name } => {
7910                // Check if this is a parameter
7911                if let Some(param_info) = ctx.params.iter().find(|p| p.name == *name) {
7912                    // Check parameter mode
7913                    match param_info.mode {
7914                        RirParamMode::Inout => {
7915                            return Ok(Some(StringReceiverStorage::Param {
7916                                abi_slot: param_info.abi_slot,
7917                            }));
7918                        }
7919                        RirParamMode::Borrow => {
7920                            let name_str = self.interner.resolve(name);
7921                            return Err(CompileError::new(
7922                                ErrorKind::MutateBorrowedValue {
7923                                    variable: name_str.to_string(),
7924                                },
7925                                span,
7926                            ));
7927                        }
7928                        RirParamMode::Normal | RirParamMode::Comptime => {
7929                            // Normal and comptime parameters are immutable
7930                            let name_str = self.interner.resolve(name);
7931                            return Err(CompileError::new(
7932                                ErrorKind::AssignToImmutable(name_str.to_string()),
7933                                span,
7934                            ));
7935                        }
7936                    }
7937                }
7938
7939                // Check if it's a local variable
7940                if let Some(local) = ctx.locals.get(name) {
7941                    if !local.is_mut {
7942                        let name_str = self.interner.resolve(name);
7943                        return Err(CompileError::new(
7944                            ErrorKind::AssignToImmutable(name_str.to_string()),
7945                            span,
7946                        ));
7947                    }
7948                    return Ok(Some(StringReceiverStorage::Local { slot: local.slot }));
7949                }
7950
7951                // Variable not found
7952                let name_str = self.interner.resolve(name);
7953                Err(CompileError::new(
7954                    ErrorKind::UndefinedVariable(name_str.to_string()),
7955                    span,
7956                ))
7957            }
7958
7959            // For other receiver types (field access, function calls, etc.),
7960            // we don't support mutation for now
7961            _ => Err(CompileError::new(ErrorKind::InvalidAssignmentTarget, span)),
7962        }
7963    }
7964
7965    /// Store the result of a String mutation method back to the receiver's storage.
7966    ///
7967    /// Returns a Unit-typed result since mutation methods don't return a value.
7968    fn store_string_result(
7969        &self,
7970        air: &mut Air,
7971        call_ref: AirRef,
7972        storage: StringReceiverStorage,
7973        span: Span,
7974    ) -> CompileResult<AnalysisResult> {
7975        let store_ref = match storage {
7976            StringReceiverStorage::Local { slot } => air.add_inst(AirInst {
7977                data: AirInstData::Store {
7978                    slot,
7979                    value: call_ref,
7980                    // The old string value was consumed by the mutation function call
7981                    // (passed as an argument). No drop is needed here.
7982                    had_live_value: false,
7983                },
7984                ty: Type::UNIT,
7985                span,
7986            }),
7987            StringReceiverStorage::Param { abi_slot } => air.add_inst(AirInst {
7988                data: AirInstData::ParamStore {
7989                    param_slot: abi_slot,
7990                    value: call_ref,
7991                },
7992                ty: Type::UNIT,
7993                span,
7994            }),
7995        };
7996
7997        Ok(AnalysisResult::new(store_ref, Type::UNIT))
7998    }
7999
8000    /// Check if directives contain @allow for a specific warning name.
8001    pub(crate) fn has_allow_directive(
8002        &self,
8003        directives: &[RirDirective],
8004        warning_name: &str,
8005    ) -> bool {
8006        let allow_sym = self.interner.get("allow");
8007        let warning_sym = self.interner.get(warning_name);
8008
8009        for directive in directives {
8010            if Some(directive.name) == allow_sym {
8011                for arg in &directive.args {
8012                    if Some(*arg) == warning_sym {
8013                        return true;
8014                    }
8015                }
8016            }
8017        }
8018        false
8019    }
8020
8021    /// Check for unused local variables in the current scope (before popping it).
8022    /// Uses the scope stack to determine which variables were added in the current scope.
8023    pub(crate) fn check_unused_locals_in_current_scope(&self, ctx: &mut AnalysisContext) {
8024        // Get the current scope entries (variables added in this scope)
8025        let Some(current_scope) = ctx.scope_stack.last() else {
8026            return;
8027        };
8028
8029        for (symbol, _old_value) in current_scope {
8030            // Skip if variable was used
8031            if ctx.used_locals.contains(symbol) {
8032                continue;
8033            }
8034
8035            // Get the local var info (it should still be in ctx.locals before pop)
8036            let Some(local) = ctx.locals.get(symbol) else {
8037                continue;
8038            };
8039
8040            // Get variable name
8041            let name = self.interner.resolve(symbol);
8042
8043            // Skip variables starting with underscore (convention for intentionally unused)
8044            if name.starts_with('_') {
8045                continue;
8046            }
8047
8048            // Skip if @allow(unused_variable) was applied
8049            if local.allow_unused {
8050                continue;
8051            }
8052
8053            // Emit warning with help suggestion (to ctx.warnings for parallel safety)
8054            ctx.warnings.push(
8055                CompileWarning::new(WarningKind::UnusedVariable(name.to_string()), local.span)
8056                    .with_help(format!(
8057                        "if this is intentional, prefix it with an underscore: `_{}`",
8058                        name
8059                    )),
8060            );
8061        }
8062    }
8063
8064    /// Check for unconsumed linear values in the current scope (before popping it).
8065    /// Linear values MUST be consumed (moved) - it's an error to let them drop implicitly.
8066    /// Returns an error if any linear value was not consumed.
8067    pub(crate) fn check_unconsumed_linear_values(
8068        &self,
8069        ctx: &AnalysisContext,
8070    ) -> CompileResult<()> {
8071        // Get the current scope entries (variables added in this scope)
8072        let Some(current_scope) = ctx.scope_stack.last() else {
8073            return Ok(());
8074        };
8075
8076        for (symbol, _old_value) in current_scope {
8077            // Get the local var info (it should still be in ctx.locals before pop)
8078            let Some(local) = ctx.locals.get(symbol) else {
8079                continue;
8080            };
8081
8082            // Only check linear types
8083            if !self.is_type_linear(local.ty) {
8084                continue;
8085            }
8086
8087            // Check if this variable was moved (consumed)
8088            let was_consumed = ctx
8089                .moved_vars
8090                .get(symbol)
8091                .is_some_and(|state| state.full_move.is_some());
8092
8093            if !was_consumed {
8094                let name = self.interner.resolve(symbol);
8095                return Err(CompileError::new(
8096                    ErrorKind::LinearValueNotConsumed(name.to_string()),
8097                    local.span,
8098                ));
8099            }
8100        }
8101
8102        Ok(())
8103    }
8104
8105    /// Extract the root variable symbol from an expression, if it refers to a variable.
8106    ///
8107    /// For inout arguments, we need to track which variable is being passed to detect
8108    /// when the same variable is passed to multiple inout parameters.
8109    ///
8110    /// Returns Some(symbol) for:
8111    /// - VarRef { name } -> the variable symbol
8112    /// - ParamRef { name, .. } -> the parameter symbol
8113    /// - FieldGet { base, .. } -> recursively extract from base
8114    /// - IndexGet { base, .. } -> recursively extract from base
8115    ///
8116    /// Returns None for expressions that don't refer to a variable (literals, calls, etc.)
8117    pub(crate) fn extract_root_variable(&self, inst_ref: InstRef) -> Option<Spur> {
8118        let inst = self.rir.get(inst_ref);
8119        match &inst.data {
8120            InstData::VarRef { name } => Some(*name),
8121            InstData::ParamRef { name, .. } => Some(*name),
8122            InstData::FieldGet { base, .. } => self.extract_root_variable(*base),
8123            InstData::IndexGet { base, .. } => self.extract_root_variable(*base),
8124            _ => None,
8125        }
8126    }
8127
8128    /// Check exclusivity rules for inout and borrow parameters in a call.
8129    ///
8130    /// This enforces two rules:
8131    /// 1. Same variable cannot be passed to multiple inout parameters (prevents aliasing)
8132    /// 2. Same variable cannot be passed to both inout and borrow (law of exclusivity)
8133    ///
8134    /// The law of exclusivity: either one mutable (inout) access OR any number of
8135    /// immutable (borrow) accesses, never both simultaneously.
8136    pub(crate) fn check_exclusive_access(
8137        &self,
8138        args: &[RirCallArg],
8139        call_span: Span,
8140    ) -> CompileResult<()> {
8141        use std::collections::HashSet;
8142        let mut inout_vars: HashSet<Spur> = HashSet::new();
8143        let mut borrow_vars: HashSet<Spur> = HashSet::new();
8144
8145        for arg in args {
8146            let maybe_var_symbol = self.extract_root_variable(arg.value);
8147
8148            // Check that inout/borrow arguments are lvalues
8149            if arg.is_inout() && maybe_var_symbol.is_none() {
8150                return Err(CompileError::new(
8151                    ErrorKind::InoutNonLvalue,
8152                    self.rir.get(arg.value).span,
8153                ));
8154            }
8155            if arg.is_borrow() && maybe_var_symbol.is_none() {
8156                return Err(CompileError::new(
8157                    ErrorKind::BorrowNonLvalue,
8158                    self.rir.get(arg.value).span,
8159                ));
8160            }
8161
8162            if let Some(var_symbol) = maybe_var_symbol {
8163                if arg.is_inout() {
8164                    // Check for duplicate inout access
8165                    if !inout_vars.insert(var_symbol) {
8166                        let var_name = self.interner.resolve(&var_symbol).to_string();
8167                        return Err(CompileError::new(
8168                            ErrorKind::InoutExclusiveAccess { variable: var_name },
8169                            call_span,
8170                        ));
8171                    }
8172                    // Check for borrow/inout conflict
8173                    if borrow_vars.contains(&var_symbol) {
8174                        let var_name = self.interner.resolve(&var_symbol).to_string();
8175                        return Err(CompileError::new(
8176                            ErrorKind::BorrowInoutConflict { variable: var_name },
8177                            call_span,
8178                        ));
8179                    }
8180                } else if arg.is_borrow() {
8181                    borrow_vars.insert(var_symbol);
8182                    // Check for borrow/inout conflict
8183                    if inout_vars.contains(&var_symbol) {
8184                        let var_name = self.interner.resolve(&var_symbol).to_string();
8185                        return Err(CompileError::new(
8186                            ErrorKind::BorrowInoutConflict { variable: var_name },
8187                            call_span,
8188                        ));
8189                    }
8190                }
8191            }
8192        }
8193        Ok(())
8194    }
8195
8196    /// Analyze a list of call arguments, handling inout unmove logic.
8197    ///
8198    /// For inout arguments, the variable is "unmoving" after analysis - this is because
8199    /// inout is a mutable borrow, not a move. The value stays valid after the call.
8200    pub(crate) fn analyze_call_args(
8201        &mut self,
8202        air: &mut Air,
8203        args: &[RirCallArg],
8204        ctx: &mut AnalysisContext,
8205    ) -> CompileResult<Vec<AirCallArg>> {
8206        let mut air_args = Vec::new();
8207        for arg in args.iter() {
8208            // For inout/borrow arguments, extract the variable name before analysis
8209            // so we can "unmove" it after - these are borrows, not moves
8210            let borrowed_var = if arg.is_inout() || arg.is_borrow() {
8211                self.extract_root_variable(arg.value)
8212            } else {
8213                None
8214            };
8215
8216            let arg_result = self.analyze_inst(air, arg.value, ctx)?;
8217
8218            // If this was an inout/borrow argument, the variable shouldn't be marked as moved
8219            // because these are borrows - the value stays valid after the call
8220            if let Some(var_symbol) = borrowed_var {
8221                ctx.moved_vars.remove(&var_symbol);
8222            }
8223
8224            air_args.push(AirCallArg {
8225                value: arg_result.air_ref,
8226                mode: AirArgMode::from(arg.mode),
8227            });
8228        }
8229        Ok(air_args)
8230    }
8231
8232    /// Register methods from an anonymous struct type with type substitution (comptime-safe).
8233    ///
8234    /// This variant supports comptime parameter capture by using `resolve_type_for_comptime_with_subst`
8235    /// to resolve type parameters like `T` to their concrete types from the enclosing function's
8236    /// comptime arguments.
8237    fn register_anon_struct_methods_for_comptime_with_subst(
8238        &mut self,
8239        spec: AnonStructSpec,
8240        _span: Span,
8241        type_subst: &std::collections::HashMap<Spur, Type>,
8242        _value_subst: &std::collections::HashMap<Spur, ConstValue>,
8243    ) -> Option<()> {
8244        let AnonStructSpec {
8245            struct_id,
8246            struct_type,
8247            methods_start,
8248            methods_len,
8249        } = spec;
8250        let method_refs = self.rir.get_inst_refs(methods_start, methods_len);
8251
8252        let mut seen_methods: std::collections::HashSet<Spur> = std::collections::HashSet::new();
8253
8254        for method_ref in method_refs {
8255            let method_inst = self.rir.get(method_ref);
8256            if let InstData::FnDecl {
8257                name: method_name,
8258                is_unchecked,
8259                params_start,
8260                params_len,
8261                return_type,
8262                body,
8263                has_self,
8264                ..
8265            } = &method_inst.data
8266            {
8267                let key = (struct_id, *method_name);
8268
8269                if seen_methods.contains(method_name) {
8270                    return None;
8271                }
8272                seen_methods.insert(*method_name);
8273
8274                if self.methods.contains_key(&key) {
8275                    return None;
8276                }
8277
8278                let params = self.rir.get_params(*params_start, *params_len);
8279                let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
8280                let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
8281
8282                for p in params {
8283                    let type_str = self.interner.resolve(&p.ty);
8284                    let resolved_ty = if type_str == "Self" {
8285                        struct_type
8286                    } else {
8287                        self.resolve_type_for_comptime_with_subst(p.ty, type_subst)?
8288                    };
8289                    param_types.push(resolved_ty);
8290                }
8291
8292                let ret_type_str = self.interner.resolve(return_type);
8293                let ret_type = if ret_type_str == "Self" {
8294                    struct_type
8295                } else {
8296                    self.resolve_type_for_comptime_with_subst(*return_type, type_subst)?
8297                };
8298
8299                let param_range = self
8300                    .param_arena
8301                    .alloc_method(param_names.into_iter(), param_types.into_iter());
8302
8303                self.methods.insert(
8304                    key,
8305                    MethodInfo {
8306                        struct_type,
8307                        has_self: *has_self,
8308                        params: param_range,
8309                        return_type: ret_type,
8310                        body: *body,
8311                        span: method_inst.span,
8312                        is_unchecked: *is_unchecked,
8313                    },
8314                );
8315            }
8316        }
8317        Some(())
8318    }
8319
8320    /// Register methods for an anonymous enum created via comptime with type substitution.
8321    ///
8322    /// Analogous to `register_anon_struct_methods_for_comptime_with_subst`, but for enums.
8323    /// Resolves method parameter/return types with `Self` mapped to the anonymous enum type.
8324    fn register_anon_enum_methods_for_comptime_with_subst(
8325        &mut self,
8326        enum_id: EnumId,
8327        enum_type: crate::types::Type,
8328        methods_start: u32,
8329        methods_len: u32,
8330        type_subst: &std::collections::HashMap<Spur, Type>,
8331    ) -> Option<()> {
8332        let method_refs = self.rir.get_inst_refs(methods_start, methods_len);
8333
8334        let mut seen_methods: std::collections::HashSet<Spur> = std::collections::HashSet::new();
8335
8336        for method_ref in method_refs {
8337            let method_inst = self.rir.get(method_ref);
8338            if let InstData::FnDecl {
8339                name: method_name,
8340                is_unchecked,
8341                params_start,
8342                params_len,
8343                return_type,
8344                body,
8345                has_self,
8346                ..
8347            } = &method_inst.data
8348            {
8349                let key = (enum_id, *method_name);
8350
8351                if seen_methods.contains(method_name) {
8352                    return None;
8353                }
8354                seen_methods.insert(*method_name);
8355
8356                if self.enum_methods.contains_key(&key) {
8357                    return None;
8358                }
8359
8360                let params = self.rir.get_params(*params_start, *params_len);
8361                let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
8362                let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
8363
8364                for p in params {
8365                    let type_str = self.interner.resolve(&p.ty);
8366                    let resolved_ty = if type_str == "Self" {
8367                        enum_type
8368                    } else {
8369                        self.resolve_type_for_comptime_with_subst(p.ty, type_subst)?
8370                    };
8371                    param_types.push(resolved_ty);
8372                }
8373
8374                let ret_type_str = self.interner.resolve(return_type);
8375                let ret_type = if ret_type_str == "Self" {
8376                    enum_type
8377                } else {
8378                    self.resolve_type_for_comptime_with_subst(*return_type, type_subst)?
8379                };
8380
8381                let param_range = self
8382                    .param_arena
8383                    .alloc_method(param_names.into_iter(), param_types.into_iter());
8384
8385                self.enum_methods.insert(
8386                    key,
8387                    MethodInfo {
8388                        struct_type: enum_type,
8389                        has_self: *has_self,
8390                        params: param_range,
8391                        return_type: ret_type,
8392                        body: *body,
8393                        span: method_inst.span,
8394                        is_unchecked: *is_unchecked,
8395                    },
8396                );
8397            }
8398        }
8399        Some(())
8400    }
8401
8402    /// Extract method signatures from RIR for structural equality comparison.
8403    ///
8404    /// This extracts method signatures as type symbols (Spur), not resolved Types.
8405    /// This is intentional: for structural equality, we compare type symbols directly
8406    /// so that `Self` matches `Self` even before we know the concrete StructId.
8407    fn extract_anon_method_sigs(
8408        &self,
8409        methods_start: u32,
8410        methods_len: u32,
8411    ) -> Vec<super::AnonMethodSig> {
8412        let method_refs = self.rir.get_inst_refs(methods_start, methods_len);
8413        let mut sigs = Vec::with_capacity(method_refs.len());
8414
8415        for method_ref in method_refs {
8416            let method_inst = self.rir.get(method_ref);
8417            if let InstData::FnDecl {
8418                name,
8419                params_start,
8420                params_len,
8421                return_type,
8422                has_self,
8423                ..
8424            } = &method_inst.data
8425            {
8426                // Extract parameter types as symbols (excluding self)
8427                let params = self.rir.get_params(*params_start, *params_len);
8428                let param_types: Vec<Spur> = params.iter().map(|p| p.ty).collect();
8429
8430                sigs.push(super::AnonMethodSig {
8431                    name: *name,
8432                    has_self: *has_self,
8433                    param_types,
8434                    return_type: *return_type,
8435                });
8436            }
8437        }
8438
8439        sigs
8440    }
8441
8442    // ========================================================================
8443    // Pointer intrinsics (require unchecked context)
8444    // ========================================================================
8445
8446    /// Analyze @ptr_read intrinsic: reads value through pointer.
8447    /// Signature: @ptr_read(ptr: ptr const T) -> T
8448    fn analyze_ptr_read_intrinsic(
8449        &mut self,
8450        air: &mut Air,
8451        name: Spur,
8452        args: &[RirCallArg],
8453        span: Span,
8454        ctx: &mut AnalysisContext,
8455    ) -> CompileResult<AnalysisResult> {
8456        if args.len() != 1 {
8457            return Err(CompileError::new(
8458                ErrorKind::IntrinsicWrongArgCount {
8459                    name: "ptr_read".to_string(),
8460                    expected: 1,
8461                    found: args.len(),
8462                },
8463                span,
8464            ));
8465        }
8466
8467        let ptr_result = self.analyze_inst(air, args[0].value, ctx)?;
8468        let ptr_type = ptr_result.ty;
8469
8470        // Get the pointee type from the pointer type
8471        let pointee_type = match ptr_type.kind() {
8472            TypeKind::PtrConst(ptr_id) => self.type_pool.ptr_const_def(ptr_id),
8473            TypeKind::PtrMut(ptr_id) => self.type_pool.ptr_mut_def(ptr_id),
8474            _ => {
8475                return Err(CompileError::new(
8476                    ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8477                        name: "ptr_read".to_string(),
8478                        expected: "ptr const T or ptr mut T".to_string(),
8479                        found: self.format_type_name(ptr_type),
8480                    })),
8481                    span,
8482                ));
8483            }
8484        };
8485
8486        // Create the intrinsic call instruction
8487        let args_start = air.add_extra(&[ptr_result.air_ref.as_u32()]);
8488        let air_ref = air.add_inst(AirInst {
8489            data: AirInstData::Intrinsic {
8490                name,
8491                args_start,
8492                args_len: 1,
8493            },
8494            ty: pointee_type,
8495            span,
8496        });
8497        Ok(AnalysisResult::new(air_ref, pointee_type))
8498    }
8499
8500    /// Analyze @ptr_write intrinsic: writes value through pointer.
8501    /// Signature: @ptr_write(ptr: ptr mut T, value: T) -> ()
8502    fn analyze_ptr_write_intrinsic(
8503        &mut self,
8504        air: &mut Air,
8505        name: Spur,
8506        args: &[RirCallArg],
8507        span: Span,
8508        ctx: &mut AnalysisContext,
8509    ) -> CompileResult<AnalysisResult> {
8510        if args.len() != 2 {
8511            return Err(CompileError::new(
8512                ErrorKind::IntrinsicWrongArgCount {
8513                    name: "ptr_write".to_string(),
8514                    expected: 2,
8515                    found: args.len(),
8516                },
8517                span,
8518            ));
8519        }
8520
8521        let ptr_result = self.analyze_inst(air, args[0].value, ctx)?;
8522        let value_result = self.analyze_inst(air, args[1].value, ctx)?;
8523        let ptr_type = ptr_result.ty;
8524        let value_type = value_result.ty;
8525
8526        // Pointer must be ptr mut T
8527        let pointee_type = match ptr_type.kind() {
8528            TypeKind::PtrMut(ptr_id) => self.type_pool.ptr_mut_def(ptr_id),
8529            TypeKind::PtrConst(_) => {
8530                return Err(CompileError::new(
8531                    ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8532                        name: "ptr_write".to_string(),
8533                        expected: "ptr mut T (cannot write through ptr const)".to_string(),
8534                        found: self.format_type_name(ptr_type),
8535                    })),
8536                    span,
8537                ));
8538            }
8539            _ => {
8540                return Err(CompileError::new(
8541                    ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8542                        name: "ptr_write".to_string(),
8543                        expected: "ptr mut T".to_string(),
8544                        found: self.format_type_name(ptr_type),
8545                    })),
8546                    span,
8547                ));
8548            }
8549        };
8550
8551        // Check that value type matches pointee type
8552        if value_type != pointee_type && !value_type.is_error() && !value_type.is_never() {
8553            return Err(CompileError::new(
8554                ErrorKind::TypeMismatch {
8555                    expected: self.format_type_name(pointee_type),
8556                    found: self.format_type_name(value_type),
8557                },
8558                span,
8559            ));
8560        }
8561
8562        // Create the intrinsic call instruction
8563        let args_start =
8564            air.add_extra(&[ptr_result.air_ref.as_u32(), value_result.air_ref.as_u32()]);
8565        let air_ref = air.add_inst(AirInst {
8566            data: AirInstData::Intrinsic {
8567                name,
8568                args_start,
8569                args_len: 2,
8570            },
8571            ty: Type::UNIT,
8572            span,
8573        });
8574        Ok(AnalysisResult::new(air_ref, Type::UNIT))
8575    }
8576
8577    /// Analyze @ptr_offset intrinsic: pointer arithmetic.
8578    /// Signature: @ptr_offset(ptr: ptr T, offset: i64) -> ptr T
8579    fn analyze_ptr_offset_intrinsic(
8580        &mut self,
8581        air: &mut Air,
8582        name: Spur,
8583        args: &[RirCallArg],
8584        span: Span,
8585        ctx: &mut AnalysisContext,
8586    ) -> CompileResult<AnalysisResult> {
8587        if args.len() != 2 {
8588            return Err(CompileError::new(
8589                ErrorKind::IntrinsicWrongArgCount {
8590                    name: "ptr_offset".to_string(),
8591                    expected: 2,
8592                    found: args.len(),
8593                },
8594                span,
8595            ));
8596        }
8597
8598        let ptr_result = self.analyze_inst(air, args[0].value, ctx)?;
8599        let offset_result = self.analyze_inst(air, args[1].value, ctx)?;
8600        let ptr_type = ptr_result.ty;
8601        let offset_type = offset_result.ty;
8602
8603        // Validate pointer type
8604        if !ptr_type.is_ptr() && !ptr_type.is_error() && !ptr_type.is_never() {
8605            return Err(CompileError::new(
8606                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8607                    name: "ptr_offset".to_string(),
8608                    expected: "ptr const T or ptr mut T".to_string(),
8609                    found: self.format_type_name(ptr_type),
8610                })),
8611                span,
8612            ));
8613        }
8614
8615        // Validate offset type (must be integer)
8616        if !offset_type.is_integer() && !offset_type.is_error() && !offset_type.is_never() {
8617            return Err(CompileError::new(
8618                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8619                    name: "ptr_offset".to_string(),
8620                    expected: "integer offset".to_string(),
8621                    found: self.format_type_name(offset_type),
8622                })),
8623                span,
8624            ));
8625        }
8626
8627        // Create the intrinsic call instruction (returns same pointer type)
8628        let args_start =
8629            air.add_extra(&[ptr_result.air_ref.as_u32(), offset_result.air_ref.as_u32()]);
8630        let air_ref = air.add_inst(AirInst {
8631            data: AirInstData::Intrinsic {
8632                name,
8633                args_start,
8634                args_len: 2,
8635            },
8636            ty: ptr_type,
8637            span,
8638        });
8639        Ok(AnalysisResult::new(air_ref, ptr_type))
8640    }
8641
8642    /// Analyze @ptr_to_int intrinsic: converts pointer to u64.
8643    /// Signature: @ptr_to_int(ptr: ptr T) -> u64
8644    fn analyze_ptr_to_int_intrinsic(
8645        &mut self,
8646        air: &mut Air,
8647        name: Spur,
8648        args: &[RirCallArg],
8649        span: Span,
8650        ctx: &mut AnalysisContext,
8651    ) -> CompileResult<AnalysisResult> {
8652        if args.len() != 1 {
8653            return Err(CompileError::new(
8654                ErrorKind::IntrinsicWrongArgCount {
8655                    name: "ptr_to_int".to_string(),
8656                    expected: 1,
8657                    found: args.len(),
8658                },
8659                span,
8660            ));
8661        }
8662
8663        let ptr_result = self.analyze_inst(air, args[0].value, ctx)?;
8664        let ptr_type = ptr_result.ty;
8665
8666        // Validate pointer type
8667        if !ptr_type.is_ptr() && !ptr_type.is_error() && !ptr_type.is_never() {
8668            return Err(CompileError::new(
8669                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8670                    name: "ptr_to_int".to_string(),
8671                    expected: "ptr const T or ptr mut T".to_string(),
8672                    found: self.format_type_name(ptr_type),
8673                })),
8674                span,
8675            ));
8676        }
8677
8678        // Create the intrinsic call instruction (returns u64)
8679        let args_start = air.add_extra(&[ptr_result.air_ref.as_u32()]);
8680        let air_ref = air.add_inst(AirInst {
8681            data: AirInstData::Intrinsic {
8682                name,
8683                args_start,
8684                args_len: 1,
8685            },
8686            ty: Type::U64,
8687            span,
8688        });
8689        Ok(AnalysisResult::new(air_ref, Type::U64))
8690    }
8691
8692    /// Analyze @int_to_ptr intrinsic: converts u64 to pointer.
8693    /// Signature: @int_to_ptr(addr: u64) -> ptr mut T
8694    /// The result type T is inferred from context (e.g., `let p: ptr mut i32 = @int_to_ptr(addr)`)
8695    fn analyze_int_to_ptr_intrinsic(
8696        &mut self,
8697        air: &mut Air,
8698        name: Spur,
8699        inst_ref: InstRef,
8700        args: &[RirCallArg],
8701        span: Span,
8702        ctx: &mut AnalysisContext,
8703    ) -> CompileResult<AnalysisResult> {
8704        if args.len() != 1 {
8705            return Err(CompileError::new(
8706                ErrorKind::IntrinsicWrongArgCount {
8707                    name: "int_to_ptr".to_string(),
8708                    expected: 1,
8709                    found: args.len(),
8710                },
8711                span,
8712            ));
8713        }
8714
8715        let addr_result = self.analyze_inst(air, args[0].value, ctx)?;
8716        let addr_type = addr_result.ty;
8717
8718        // Validate address type (must be u64)
8719        if addr_type != Type::U64 && !addr_type.is_error() && !addr_type.is_never() {
8720            return Err(CompileError::new(
8721                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8722                    name: "int_to_ptr".to_string(),
8723                    expected: "u64".to_string(),
8724                    found: self.format_type_name(addr_type),
8725                })),
8726                span,
8727            ));
8728        }
8729
8730        // Get the result type from HM inference (must be a ptr mut T)
8731        let result_type = Self::get_resolved_type(ctx, inst_ref, span, "@int_to_ptr intrinsic")?;
8732
8733        // Validate that the inferred type is a mutable pointer
8734        if !result_type.is_ptr_mut() && !result_type.is_error() && !result_type.is_never() {
8735            return Err(CompileError::new(
8736                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8737                    name: "int_to_ptr".to_string(),
8738                    expected: "ptr mut T".to_string(),
8739                    found: self.format_type_name(result_type),
8740                })),
8741                span,
8742            ));
8743        }
8744
8745        // Create the intrinsic call instruction
8746        let args_start = air.add_extra(&[addr_result.air_ref.as_u32()]);
8747        let air_ref = air.add_inst(AirInst {
8748            data: AirInstData::Intrinsic {
8749                name,
8750                args_start,
8751                args_len: 1,
8752            },
8753            ty: result_type,
8754            span,
8755        });
8756        Ok(AnalysisResult::new(air_ref, result_type))
8757    }
8758
8759    /// Analyze @null_ptr intrinsic: creates a typed null pointer.
8760    /// Signature: @null_ptr() -> ptr const T
8761    /// The result type T is inferred from context (e.g., `let p: ptr const i32 = @null_ptr()`)
8762    fn analyze_null_ptr_intrinsic(
8763        &mut self,
8764        air: &mut Air,
8765        name: Spur,
8766        inst_ref: InstRef,
8767        args: &[RirCallArg],
8768        span: Span,
8769        ctx: &mut AnalysisContext,
8770    ) -> CompileResult<AnalysisResult> {
8771        if !args.is_empty() {
8772            return Err(CompileError::new(
8773                ErrorKind::IntrinsicWrongArgCount {
8774                    name: "null_ptr".to_string(),
8775                    expected: 0,
8776                    found: args.len(),
8777                },
8778                span,
8779            ));
8780        }
8781
8782        // Get the result type from HM inference (must be a pointer type)
8783        let result_type = Self::get_resolved_type(ctx, inst_ref, span, "@null_ptr intrinsic")?;
8784
8785        // Validate that the inferred type is a pointer
8786        if !result_type.is_ptr() && !result_type.is_error() && !result_type.is_never() {
8787            return Err(CompileError::new(
8788                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8789                    name: "null_ptr".to_string(),
8790                    expected: "ptr const T or ptr mut T".to_string(),
8791                    found: self.format_type_name(result_type),
8792                })),
8793                span,
8794            ));
8795        }
8796
8797        // Create the intrinsic call instruction (no args)
8798        let args_start = air.add_extra(&[]);
8799        let air_ref = air.add_inst(AirInst {
8800            data: AirInstData::Intrinsic {
8801                name,
8802                args_start,
8803                args_len: 0,
8804            },
8805            ty: result_type,
8806            span,
8807        });
8808        Ok(AnalysisResult::new(air_ref, result_type))
8809    }
8810
8811    /// Analyze @is_null intrinsic: checks if a pointer is null.
8812    /// Signature: @is_null(ptr: ptr T) -> bool
8813    fn analyze_is_null_intrinsic(
8814        &mut self,
8815        air: &mut Air,
8816        name: Spur,
8817        args: &[RirCallArg],
8818        span: Span,
8819        ctx: &mut AnalysisContext,
8820    ) -> CompileResult<AnalysisResult> {
8821        if args.len() != 1 {
8822            return Err(CompileError::new(
8823                ErrorKind::IntrinsicWrongArgCount {
8824                    name: "is_null".to_string(),
8825                    expected: 1,
8826                    found: args.len(),
8827                },
8828                span,
8829            ));
8830        }
8831
8832        let ptr_result = self.analyze_inst(air, args[0].value, ctx)?;
8833        let ptr_type = ptr_result.ty;
8834
8835        // Validate pointer type
8836        if !ptr_type.is_ptr() && !ptr_type.is_error() && !ptr_type.is_never() {
8837            return Err(CompileError::new(
8838                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8839                    name: "is_null".to_string(),
8840                    expected: "ptr const T or ptr mut T".to_string(),
8841                    found: self.format_type_name(ptr_type),
8842                })),
8843                span,
8844            ));
8845        }
8846
8847        // Create the intrinsic call instruction (returns bool)
8848        let args_start = air.add_extra(&[ptr_result.air_ref.as_u32()]);
8849        let air_ref = air.add_inst(AirInst {
8850            data: AirInstData::Intrinsic {
8851                name,
8852                args_start,
8853                args_len: 1,
8854            },
8855            ty: Type::BOOL,
8856            span,
8857        });
8858        Ok(AnalysisResult::new(air_ref, Type::BOOL))
8859    }
8860
8861    /// Analyze @ptr_copy intrinsic: copies n elements from src to dst.
8862    /// Signature: @ptr_copy(dst: ptr mut T, src: ptr const T, count: u64) -> ()
8863    fn analyze_ptr_copy_intrinsic(
8864        &mut self,
8865        air: &mut Air,
8866        name: Spur,
8867        args: &[RirCallArg],
8868        span: Span,
8869        ctx: &mut AnalysisContext,
8870    ) -> CompileResult<AnalysisResult> {
8871        if args.len() != 3 {
8872            return Err(CompileError::new(
8873                ErrorKind::IntrinsicWrongArgCount {
8874                    name: "ptr_copy".to_string(),
8875                    expected: 3,
8876                    found: args.len(),
8877                },
8878                span,
8879            ));
8880        }
8881
8882        let dst_result = self.analyze_inst(air, args[0].value, ctx)?;
8883        let src_result = self.analyze_inst(air, args[1].value, ctx)?;
8884        let count_result = self.analyze_inst(air, args[2].value, ctx)?;
8885        let dst_type = dst_result.ty;
8886        let src_type = src_result.ty;
8887        let count_type = count_result.ty;
8888
8889        // dst must be ptr mut T
8890        let dst_pointee = match dst_type.kind() {
8891            TypeKind::PtrMut(ptr_id) => self.type_pool.ptr_mut_def(ptr_id),
8892            TypeKind::PtrConst(_) => {
8893                return Err(CompileError::new(
8894                    ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8895                        name: "ptr_copy".to_string(),
8896                        expected: "ptr mut T (cannot copy into ptr const)".to_string(),
8897                        found: self.format_type_name(dst_type),
8898                    })),
8899                    span,
8900                ));
8901            }
8902            _ => {
8903                if !dst_type.is_error() && !dst_type.is_never() {
8904                    return Err(CompileError::new(
8905                        ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8906                            name: "ptr_copy".to_string(),
8907                            expected: "ptr mut T".to_string(),
8908                            found: self.format_type_name(dst_type),
8909                        })),
8910                        span,
8911                    ));
8912                }
8913                Type::ERROR
8914            }
8915        };
8916
8917        // src must be ptr const T or ptr mut T
8918        let src_pointee = match src_type.kind() {
8919            TypeKind::PtrConst(ptr_id) => self.type_pool.ptr_const_def(ptr_id),
8920            TypeKind::PtrMut(ptr_id) => self.type_pool.ptr_mut_def(ptr_id),
8921            _ => {
8922                if !src_type.is_error() && !src_type.is_never() {
8923                    return Err(CompileError::new(
8924                        ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8925                            name: "ptr_copy".to_string(),
8926                            expected: "ptr const T or ptr mut T".to_string(),
8927                            found: self.format_type_name(src_type),
8928                        })),
8929                        span,
8930                    ));
8931                }
8932                Type::ERROR
8933            }
8934        };
8935
8936        // Pointee types must match
8937        if dst_pointee != src_pointee
8938            && !dst_pointee.is_error()
8939            && !src_pointee.is_error()
8940            && !dst_pointee.is_never()
8941            && !src_pointee.is_never()
8942        {
8943            return Err(CompileError::new(
8944                ErrorKind::TypeMismatch {
8945                    expected: self.format_type_name(dst_pointee),
8946                    found: self.format_type_name(src_pointee),
8947                },
8948                span,
8949            ));
8950        }
8951
8952        // count must be u64
8953        if count_type != Type::U64 && !count_type.is_error() && !count_type.is_never() {
8954            return Err(CompileError::new(
8955                ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
8956                    name: "ptr_copy".to_string(),
8957                    expected: "u64".to_string(),
8958                    found: self.format_type_name(count_type),
8959                })),
8960                span,
8961            ));
8962        }
8963
8964        // Create the intrinsic call instruction
8965        let args_start = air.add_extra(&[
8966            dst_result.air_ref.as_u32(),
8967            src_result.air_ref.as_u32(),
8968            count_result.air_ref.as_u32(),
8969        ]);
8970        let air_ref = air.add_inst(AirInst {
8971            data: AirInstData::Intrinsic {
8972                name,
8973                args_start,
8974                args_len: 3,
8975            },
8976            ty: Type::UNIT,
8977            span,
8978        });
8979        Ok(AnalysisResult::new(air_ref, Type::UNIT))
8980    }
8981
8982    /// Analyze @addr_of / @addr_of_mut intrinsics: takes address of lvalue.
8983    /// Signature: @addr_of(lvalue) -> ptr const T
8984    /// Signature: @addr_of_mut(lvalue) -> ptr mut T
8985    fn analyze_addr_of_intrinsic(
8986        &mut self,
8987        air: &mut Air,
8988        args: &[RirCallArg],
8989        span: Span,
8990        ctx: &mut AnalysisContext,
8991        is_mut: bool,
8992    ) -> CompileResult<AnalysisResult> {
8993        let intrinsic_name = if is_mut { "addr_of_mut" } else { "addr_of" };
8994
8995        if args.len() != 1 {
8996            return Err(CompileError::new(
8997                ErrorKind::IntrinsicWrongArgCount {
8998                    name: intrinsic_name.to_string(),
8999                    expected: 1,
9000                    found: args.len(),
9001                },
9002                span,
9003            ));
9004        }
9005
9006        let arg_result = self.analyze_inst(air, args[0].value, ctx)?;
9007        let pointee_type = arg_result.ty;
9008
9009        // For addr_of, we need the argument to be an lvalue (addressable)
9010        // This is validated at the RIR level - here we just compute the result type
9011
9012        // Create the pointer type
9013        let result_type = if is_mut {
9014            let ptr_type_id = self.type_pool.intern_ptr_mut_from_type(pointee_type);
9015            Type::new_ptr_mut(ptr_type_id)
9016        } else {
9017            let ptr_type_id = self.type_pool.intern_ptr_const_from_type(pointee_type);
9018            Type::new_ptr_const(ptr_type_id)
9019        };
9020
9021        // Create the intrinsic call instruction
9022        let name = if is_mut {
9023            self.known.raw_mut
9024        } else {
9025            self.known.raw
9026        };
9027        let args_start = air.add_extra(&[arg_result.air_ref.as_u32()]);
9028        let air_ref = air.add_inst(AirInst {
9029            data: AirInstData::Intrinsic {
9030                name,
9031                args_start,
9032                args_len: 1,
9033            },
9034            ty: result_type,
9035            span,
9036        });
9037        Ok(AnalysisResult::new(air_ref, result_type))
9038    }
9039
9040    /// Analyze @syscall intrinsic: perform a raw OS syscall.
9041    /// Signature: @syscall(syscall_num: u64, arg0?: u64, ..., arg5?: u64) -> i64
9042    ///
9043    /// Takes a syscall number and up to 6 arguments, all of which must be u64.
9044    /// Returns i64 (the syscall return value, which may be negative for errors).
9045    /// Requires a checked block.
9046    fn analyze_syscall_intrinsic(
9047        &mut self,
9048        air: &mut Air,
9049        name: Spur,
9050        args: &[RirCallArg],
9051        span: Span,
9052        ctx: &mut AnalysisContext,
9053    ) -> CompileResult<AnalysisResult> {
9054        // Syscall takes 1-7 arguments: syscall number + up to 6 arguments
9055        if args.is_empty() || args.len() > 7 {
9056            return Err(CompileError::new(
9057                ErrorKind::IntrinsicWrongArgCount {
9058                    name: "syscall".to_string(),
9059                    expected: 7, // Show max expected for "at least 1, at most 7"
9060                    found: args.len(),
9061                },
9062                span,
9063            ));
9064        }
9065
9066        // Analyze all arguments and verify they are u64
9067        let mut arg_refs = Vec::with_capacity(args.len());
9068        for (i, arg) in args.iter().enumerate() {
9069            let arg_result = self.analyze_inst(air, arg.value, ctx)?;
9070            let arg_type = arg_result.ty;
9071
9072            // All syscall arguments must be u64
9073            if arg_type != Type::U64 && !arg_type.is_error() && !arg_type.is_never() {
9074                return Err(CompileError::new(
9075                    ErrorKind::IntrinsicTypeMismatch(Box::new(IntrinsicTypeMismatchError {
9076                        name: "syscall".to_string(),
9077                        expected: format!("u64 for argument {}", i),
9078                        found: self.format_type_name(arg_type),
9079                    })),
9080                    span,
9081                ));
9082            }
9083
9084            arg_refs.push(arg_result.air_ref.as_u32());
9085        }
9086
9087        // Create the intrinsic call instruction
9088        let args_start = air.add_extra(&arg_refs);
9089        let air_ref = air.add_inst(AirInst {
9090            data: AirInstData::Intrinsic {
9091                name,
9092                args_start,
9093                args_len: args.len() as u32,
9094            },
9095            ty: Type::I64,
9096            span,
9097        });
9098        Ok(AnalysisResult::new(air_ref, Type::I64))
9099    }
9100
9101    /// Analyze @target_arch() intrinsic - returns target CPU architecture enum.
9102    ///
9103    /// This intrinsic takes no arguments and returns an Arch enum value
9104    /// representing the target CPU architecture (X86_64 or Aarch64).
9105    fn analyze_target_arch_intrinsic(
9106        &self,
9107        air: &mut Air,
9108        args: &[RirCallArg],
9109        span: Span,
9110    ) -> CompileResult<AnalysisResult> {
9111        // Validate: no arguments
9112        if !args.is_empty() {
9113            return Err(CompileError::new(
9114                ErrorKind::IntrinsicWrongArgCount {
9115                    name: "target_arch".to_string(),
9116                    expected: 0,
9117                    found: args.len(),
9118                },
9119                span,
9120            ));
9121        }
9122
9123        let arch_enum_id = self
9124            .builtin_arch_id
9125            .expect("Arch enum not injected - internal compiler error");
9126
9127        // Determine variant index based on host architecture (compile-time evaluation)
9128        // Currently we always compile for the host architecture
9129        let variant_index = match gruel_target::Target::host().arch() {
9130            Arch::X86_64 => 0,
9131            Arch::Aarch64 => 1,
9132        };
9133
9134        let result_type = Type::new_enum(arch_enum_id);
9135        let air_ref = air.add_inst(AirInst {
9136            data: AirInstData::EnumVariant {
9137                enum_id: arch_enum_id,
9138                variant_index,
9139            },
9140            ty: result_type,
9141            span,
9142        });
9143        Ok(AnalysisResult::new(air_ref, result_type))
9144    }
9145
9146    /// Analyze @target_os() intrinsic - returns target operating system enum.
9147    ///
9148    /// This intrinsic takes no arguments and returns an Os enum value
9149    /// representing the target operating system (Linux or Macos).
9150    fn analyze_target_os_intrinsic(
9151        &self,
9152        air: &mut Air,
9153        args: &[RirCallArg],
9154        span: Span,
9155    ) -> CompileResult<AnalysisResult> {
9156        // Validate: no arguments
9157        if !args.is_empty() {
9158            return Err(CompileError::new(
9159                ErrorKind::IntrinsicWrongArgCount {
9160                    name: "target_os".to_string(),
9161                    expected: 0,
9162                    found: args.len(),
9163                },
9164                span,
9165            ));
9166        }
9167
9168        let os_enum_id = self
9169            .builtin_os_id
9170            .expect("Os enum not injected - internal compiler error");
9171
9172        // Determine variant index based on host OS (compile-time evaluation)
9173        // Currently we always compile for the host OS
9174        let variant_index = match gruel_target::Target::host().os() {
9175            Os::Linux => 0,
9176            Os::Macos => 1,
9177        };
9178
9179        let result_type = Type::new_enum(os_enum_id);
9180        let air_ref = air.add_inst(AirInst {
9181            data: AirInstData::EnumVariant {
9182                enum_id: os_enum_id,
9183                variant_index,
9184            },
9185            ty: result_type,
9186            span,
9187        });
9188        Ok(AnalysisResult::new(air_ref, result_type))
9189    }
9190}