Skip to main content

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 rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
18
19use gruel_rir::{InstData, InstRef, RirArgMode, RirCallArg, RirDirective, RirParamMode};
20use gruel_target::{Arch, Os};
21
22/// Maps a target [`Arch`] to its variant index in `ARCH_ENUM` from
23/// `gruel-builtins`. The order is the historical compatibility order:
24/// existing programs depend on `X86_64 = 0`, `Aarch64 = 1`, with new
25/// variants appended.
26pub(super) fn arch_variant_index(arch: Arch) -> u32 {
27    match arch {
28        Arch::X86_64 => 0,
29        Arch::Aarch64 => 1,
30        Arch::X86 => 2,
31        Arch::Arm => 3,
32        Arch::Riscv32 => 4,
33        Arch::Riscv64 => 5,
34        Arch::Wasm32 => 6,
35        Arch::Wasm64 => 7,
36        // Unknown architectures fall back to X86_64 so a stray triple
37        // doesn't ICE; users targeting an unrecognized arch should
38        // notice via the unblessed-target warning.
39        Arch::Unknown => 0,
40    }
41}
42
43/// Maps a target [`Os`] to its variant index in `OS_ENUM` from
44/// `gruel-builtins`. The order is the historical compatibility order:
45/// existing programs depend on `Linux = 0`, `Macos = 1`, with new
46/// variants appended.
47pub(super) fn os_variant_index(os: Os) -> u32 {
48    match os {
49        Os::Linux => 0,
50        Os::Macos => 1,
51        Os::Windows => 2,
52        Os::Freestanding => 3,
53        Os::Wasi => 4,
54        Os::Unknown => 0,
55    }
56}
57use gruel_util::{BinOp, Span};
58use gruel_util::{
59    CompileError, CompileErrors, CompileResult, CompileWarning, ErrorKind, MultiErrorResult,
60    OptionExt, PreviewFeature, WarningKind,
61};
62use lasso::Spur;
63
64use super::context::{AnalysisContext, AnalysisResult, ComptimeHeapItem, ConstValue, ParamInfo};
65use super::{AnalyzedFunction, InferenceContext, MethodInfo, Sema, SemaOutput};
66use crate::inference::{
67    Constraint, ConstraintContext, ConstraintGenerator, ParamVarInfo, Unifier, UnifyResult,
68};
69use crate::inst::{
70    Air, AirArgMode, AirCallArg, AirInst, AirInstData, AirPlaceBase, AirProjection, AirRef,
71};
72use crate::types::{EnumId, EnumVariantDef, StructField, StructId, Type, TypeKind};
73
74/// Data describing a method body for analysis.
75struct MethodBodySpec<'a> {
76    return_type: Spur,
77    params: &'a [gruel_rir::RirParam],
78    body: InstRef,
79    /// The host struct/enum type. Always set when analyzing a method or
80    /// associated function inside a `struct` / `enum` body, regardless of
81    /// whether the function has a `self` parameter. ADR-0076 uses this to
82    /// bind `Self` for the duration of method-body analysis so that
83    /// associated functions like `fn new() -> Self` resolve `Self` to the
84    /// host type. `None` when analyzing a free top-level function.
85    host_type: Option<Type>,
86    /// Whether the function has a `self` parameter (i.e., it is a method
87    /// rather than an associated function). When `true`, `self` is added
88    /// as the first parameter with `host_type` as its type.
89    has_self: bool,
90    /// Receiver shape (ADR-0076 sole form: `self` / `self : MutRef(Self)` /
91    /// `self : Ref(Self)`). Encoded as a byte: 0 = by-value, 1 =
92    /// `MutRef(Self)`, 2 = `Ref(Self)`. Ignored when `has_self` is `false`.
93    self_mode: u8,
94}
95
96/// Result of analyzing a function: analyzed function, warnings, local strings,
97/// local byte blobs, referenced functions, and referenced methods.
98type AnalyzedFnResult = CompileResult<(
99    AnalyzedFunction,
100    Vec<CompileWarning>,
101    Vec<String>,
102    Vec<Vec<u8>>,
103    HashSet<Spur>,
104    HashSet<(StructId, Spur)>,
105)>;
106
107/// Raw analysis output: air, local count, param slots, param modes, param slot types,
108/// warnings, local strings, local byte blobs, referenced functions, and referenced methods.
109type RawFnAnalysis = CompileResult<(
110    Air,
111    u32,
112    u32,
113    Vec<crate::inst::AirParamMode>,
114    Vec<Type>,
115    Vec<CompileWarning>,
116    Vec<String>,
117    Vec<Vec<u8>>,
118    HashSet<Spur>,
119    HashSet<(StructId, Spur)>,
120)>;
121
122/// Arguments for [`Sema::register_anon_struct_methods_for_comptime_with_subst`].
123pub(super) struct AnonStructSpec {
124    pub(super) struct_id: StructId,
125    pub(super) struct_type: crate::types::Type,
126    pub(super) methods_start: u32,
127    pub(super) methods_len: u32,
128}
129
130/// Main entry point for analyzing all function bodies (ADR-0026).
131///
132/// Implements "lazy semantic analysis": only functions reachable from the
133/// entry point (`main`) are analyzed. Unreferenced code is not analyzed,
134/// not codegen'd, and errors in unreferenced code are not reported.
135///
136/// This is the same trade-off Zig makes for faster builds and smaller binaries.
137pub(crate) fn analyze_all_function_bodies(mut sema: Sema<'_>) -> MultiErrorResult<SemaOutput> {
138    let sema = &mut sema;
139    // Build inference context once
140    let infer_ctx = sema.build_inference_context();
141
142    // Work queue: functions/methods to analyze. Seed it with main() — the
143    // entry point — and let reachability pull in the rest. The compiler
144    // driver enforces that main() exists at codegen time
145    // (`ErrorKind::NoMainFunction`), so sema doesn't need to. When no main
146    // exists (e.g. golden-CFG spec tests over a single fn), fall back to
147    // enqueueing every non-generic top-level function so analysis still
148    // covers the program.
149    let mut pending_functions: Vec<Spur> = match sema.interner.get("main") {
150        Some(sym) if sema.functions.contains_key(&sym) => vec![sym],
151        _ => sema
152            .functions
153            .iter()
154            .filter_map(|(name, info)| (!info.is_generic).then_some(*name))
155            .collect(),
156    };
157    let mut analyzed_functions: HashSet<Spur> = HashSet::default();
158    let mut pending_methods: Vec<(StructId, Spur)> = Vec::new();
159    let mut analyzed_methods: HashSet<(StructId, Spur)> = HashSet::default();
160
161    // Collect results
162    let mut functions_with_strings: Vec<(AnalyzedFunction, Vec<String>, Vec<Vec<u8>>)> = Vec::new();
163    let mut errors = CompileErrors::new();
164    let mut all_warnings = Vec::new();
165
166    // Collect method refs from struct declarations (for later lookup)
167    let mut method_refs: HashSet<InstRef> = HashSet::default();
168    for (_, inst) in sema.rir.iter() {
169        if let InstData::StructDecl {
170            methods_start,
171            methods_len,
172            ..
173        } = &inst.data
174        {
175            let methods = sema.rir.get_inst_refs(*methods_start, *methods_len);
176            for method_ref in methods {
177                method_refs.insert(method_ref);
178            }
179        }
180    }
181
182    // Drive analysis to a fixed point: lazy BFS of reachable
183    // functions/methods, one-shot post-processing (vtables, anon types,
184    // destructors, derives, inline drops), then specialization. Specialization
185    // synthesizes new bodies whose method/function references we feed back
186    // into the work queue, so we re-enter the loop until nothing new appears.
187    //
188    // The specialization name map persists across outer iterations so that a
189    // newly-analyzed body with a CallGeneric for an already-synthesized key
190    // (e.g. a method whose body re-calls a generic main has already
191    // specialized) doesn't trigger a duplicate body — `specialize` skips keys
192    // it has seen before but still rewrites the new CallGeneric to a Call.
193    let mut post_processed = false;
194    let mut spec_name_map: rustc_hash::FxHashMap<crate::specialize::SpecializationKey, Spur> =
195        rustc_hash::FxHashMap::default();
196    loop {
197        // Process work queue until empty
198        while !pending_functions.is_empty() || !pending_methods.is_empty() {
199            // Process pending functions
200            while let Some(fn_name) = pending_functions.pop() {
201                if analyzed_functions.contains(&fn_name) {
202                    continue;
203                }
204                analyzed_functions.insert(fn_name);
205
206                // Look up the function info
207                let fn_info = match sema.functions.get(&fn_name) {
208                    Some(info) => *info,
209                    None => continue, // Should not happen, but be defensive
210                };
211
212                // Skip generic functions - they're analyzed during specialization
213                if fn_info.is_generic {
214                    continue;
215                }
216
217                // Skip type-constructor functions (return `type`). Their bodies
218                // run only via the comptime evaluator at each call site (see
219                // `evaluate_type_ctor_body`); analyzing them as ordinary
220                // runtime code would mis-evaluate `comptime if` predicates that
221                // reference comptime *value* parameters (e.g. `comptime N: i32`)
222                // before the call binds them.
223                if fn_info.return_type == Type::COMPTIME_TYPE {
224                    continue;
225                }
226
227                let fn_name_str = sema.interner.resolve(&fn_name).to_string();
228
229                // Find the function declaration in RIR to get params
230                let mut found = false;
231                for (inst_ref, inst) in sema.rir.iter() {
232                    if let InstData::FnDecl {
233                        name,
234                        params_start,
235                        params_len,
236                        return_type,
237                        body,
238                        ..
239                    } = &inst.data
240                        && *name == fn_name
241                        && !method_refs.contains(&inst_ref)
242                    {
243                        found = true;
244                        let params = sema.rir.get_params(*params_start, *params_len);
245
246                        match sema.analyze_single_function(
247                            &infer_ctx,
248                            &fn_name_str,
249                            *return_type,
250                            &params,
251                            *body,
252                            inst.span,
253                        ) {
254                            Ok((
255                                analyzed,
256                                warnings,
257                                local_strings,
258                                local_bytes,
259                                referenced_fns,
260                                referenced_meths,
261                            )) => {
262                                functions_with_strings.push((analyzed, local_strings, local_bytes));
263                                all_warnings.extend(warnings);
264
265                                // Add newly referenced functions to the work queue
266                                for ref_fn in referenced_fns {
267                                    if !analyzed_functions.contains(&ref_fn) {
268                                        pending_functions.push(ref_fn);
269                                    }
270                                }
271                                for ref_meth in referenced_meths {
272                                    if !analyzed_methods.contains(&ref_meth) {
273                                        pending_methods.push(ref_meth);
274                                    }
275                                }
276                            }
277                            Err(e) => errors.push(e),
278                        }
279                        break;
280                    }
281                }
282
283                if !found {
284                    // This could be a builtin or otherwise non-existent function
285                    // Just skip it
286                }
287            }
288
289            // Process pending methods
290            while let Some((struct_id, method_name)) = pending_methods.pop() {
291                if analyzed_methods.contains(&(struct_id, method_name)) {
292                    continue;
293                }
294                analyzed_methods.insert((struct_id, method_name));
295
296                // Look up the method info — first in struct methods, then in
297                // enum methods. Enum methods are recorded with the same
298                // `(StructId, Spur)` key shape (StructId reusing the EnumId's
299                // raw u32) so the work queue can dispatch them uniformly.
300                let (method_info, is_enum_method) =
301                    if let Some(info) = sema.methods.get(&(struct_id, method_name)) {
302                        (*info, false)
303                    } else if let Some(info) = sema
304                        .enum_methods
305                        .get(&(crate::types::EnumId(struct_id.0), method_name))
306                    {
307                        (*info, true)
308                    } else {
309                        continue;
310                    };
311
312                // Method-level generic: defer body analysis to specialization.
313                if method_info.is_generic {
314                    continue;
315                }
316
317                // Get the type definition to find its name for impl block lookup
318                let type_name_str = if is_enum_method {
319                    sema.type_pool
320                        .enum_def(crate::types::EnumId(struct_id.0))
321                        .name
322                        .clone()
323                } else {
324                    sema.type_pool.struct_def(struct_id).name.clone()
325                };
326                let type_name_sym = sema.interner.get_or_intern(&type_name_str);
327                let method_name_str = sema.interner.resolve(&method_name).to_string();
328
329                // For anonymous structs, use the MethodInfo directly since there's no named StructDecl
330                if type_name_str.starts_with("__anon_struct_") {
331                    let full_name = if method_info.has_self {
332                        format!("{}.{}", type_name_str, method_name_str)
333                    } else {
334                        format!("{}::{}", type_name_str, method_name_str)
335                    };
336
337                    // Build param_info from MethodInfo's ParamRange
338                    let param_names = sema.param_arena.names(method_info.params);
339                    let param_types = sema.param_arena.types(method_info.params);
340                    let param_modes = sema.param_arena.modes(method_info.params);
341
342                    let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
343
344                    if method_info.has_self {
345                        // Add self parameter (Normal mode - passed by value)
346                        let self_sym = sema.interner.get_or_intern("self");
347                        let self_mode = match method_info.receiver {
348                            crate::types::ReceiverMode::ByValue => RirParamMode::Normal,
349                            crate::types::ReceiverMode::Ref => RirParamMode::Ref,
350                            crate::types::ReceiverMode::MutRef => RirParamMode::MutRef,
351                        };
352                        param_info.push((self_sym, method_info.struct_type, self_mode));
353                    }
354
355                    // Add regular parameters (convert from arena slices)
356                    for i in 0..param_names.len() {
357                        param_info.push((param_names[i], param_types[i], param_modes[i]));
358                    }
359
360                    // Retrieve captured comptime values from struct-level storage
361                    // Clone the HashMap to avoid borrowing issues with mutable analyze_method_body call
362                    let struct_id = method_info
363                        .struct_type
364                        .as_struct()
365                        .expect("method must belong to struct");
366                    let captured_values = sema
367                        .anon_struct_captured_values
368                        .get(&struct_id)
369                        .cloned()
370                        .unwrap_or_else(HashMap::default);
371                    // ADR-0082: outer comptime fn's type subst (T → I32 etc.).
372                    let outer_type_subst = sema.anon_struct_type_subst.get(&struct_id).cloned();
373
374                    match sema.analyze_method_body(
375                        &infer_ctx,
376                        method_info.return_type,
377                        &param_info,
378                        method_info.body,
379                        method_info.struct_type,
380                        &captured_values,
381                        outer_type_subst.as_ref(),
382                    ) {
383                        Ok((
384                            air,
385                            num_locals,
386                            num_param_slots,
387                            param_modes_result,
388                            param_slot_types,
389                            warnings,
390                            local_strings,
391                            local_bytes,
392                            referenced_fns,
393                            referenced_meths,
394                        )) => {
395                            let analyzed = AnalyzedFunction {
396                                name: full_name,
397                                air,
398                                num_locals,
399                                num_param_slots,
400                                param_modes: param_modes_result,
401                                param_slot_types,
402                                is_destructor: false,
403                            };
404                            functions_with_strings.push((analyzed, local_strings, local_bytes));
405                            all_warnings.extend(warnings);
406
407                            for ref_fn in referenced_fns {
408                                if !analyzed_functions.contains(&ref_fn) {
409                                    pending_functions.push(ref_fn);
410                                }
411                            }
412                            for ref_meth in referenced_meths {
413                                if !analyzed_methods.contains(&ref_meth) {
414                                    pending_methods.push(ref_meth);
415                                }
416                            }
417                        }
418                        Err(e) => errors.push(e),
419                    }
420                    continue;
421                }
422
423                // ADR-0078: enum methods (named enums declared in source — not
424                // anonymous, those are handled by the fixed-point loop below).
425                // Find the matching `fn` inside the enum's declaration and
426                // analyze its body, mirroring the struct branch below.
427                if is_enum_method {
428                    for (_, inst) in sema.rir.iter() {
429                        if let InstData::EnumDecl {
430                            name: enum_name,
431                            methods_start,
432                            methods_len,
433                            ..
434                        } = &inst.data
435                            && *enum_name == type_name_sym
436                        {
437                            let methods = sema.rir.get_inst_refs(*methods_start, *methods_len);
438                            for method_ref in methods {
439                                let method_inst = sema.rir.get(method_ref);
440                                if let InstData::FnDecl {
441                                    name: m_name,
442                                    params_start,
443                                    params_len,
444                                    return_type,
445                                    body,
446                                    has_self,
447                                    receiver_mode,
448                                    ..
449                                } = &method_inst.data
450                                    && *m_name == method_name
451                                {
452                                    let params = sema.rir.get_params(*params_start, *params_len);
453                                    let full_name = if *has_self {
454                                        format!("{}.{}", type_name_str, method_name_str)
455                                    } else {
456                                        format!("{}::{}", type_name_str, method_name_str)
457                                    };
458
459                                    match sema.analyze_method_function(
460                                        &infer_ctx,
461                                        &full_name,
462                                        MethodBodySpec {
463                                            return_type: *return_type,
464                                            params: &params,
465                                            body: *body,
466                                            host_type: Some(method_info.struct_type),
467                                            has_self: *has_self,
468                                            self_mode: *receiver_mode,
469                                        },
470                                        method_inst.span,
471                                    ) {
472                                        Ok((
473                                            analyzed,
474                                            warnings,
475                                            local_strings,
476                                            local_bytes,
477                                            referenced_fns,
478                                            referenced_meths,
479                                        )) => {
480                                            functions_with_strings.push((
481                                                analyzed,
482                                                local_strings,
483                                                local_bytes,
484                                            ));
485                                            all_warnings.extend(warnings);
486                                            for ref_fn in referenced_fns {
487                                                if !analyzed_functions.contains(&ref_fn) {
488                                                    pending_functions.push(ref_fn);
489                                                }
490                                            }
491                                            for ref_meth in referenced_meths {
492                                                if !analyzed_methods.contains(&ref_meth) {
493                                                    pending_methods.push(ref_meth);
494                                                }
495                                            }
496                                        }
497                                        Err(e) => errors.push(e),
498                                    }
499                                }
500                            }
501                        }
502                    }
503                    continue;
504                }
505
506                // Find the method in struct declarations (for named structs)
507                for (_, inst) in sema.rir.iter() {
508                    if let InstData::StructDecl {
509                        name: struct_name,
510                        methods_start,
511                        methods_len,
512                        ..
513                    } = &inst.data
514                    {
515                        if *struct_name != type_name_sym {
516                            continue;
517                        }
518
519                        let methods = sema.rir.get_inst_refs(*methods_start, *methods_len);
520                        for method_ref in methods {
521                            let method_inst = sema.rir.get(method_ref);
522                            if let InstData::FnDecl {
523                                name: m_name,
524                                params_start,
525                                params_len,
526                                return_type,
527                                body,
528                                has_self,
529                                receiver_mode,
530                                ..
531                            } = &method_inst.data
532                            {
533                                if *m_name != method_name {
534                                    continue;
535                                }
536
537                                let params = sema.rir.get_params(*params_start, *params_len);
538                                let full_name = if *has_self {
539                                    format!("{}.{}", type_name_str, method_name_str)
540                                } else {
541                                    format!("{}::{}", type_name_str, method_name_str)
542                                };
543
544                                match sema.analyze_method_function(
545                                    &infer_ctx,
546                                    &full_name,
547                                    MethodBodySpec {
548                                        return_type: *return_type,
549                                        params: &params,
550                                        body: *body,
551                                        host_type: Some(method_info.struct_type),
552                                        has_self: *has_self,
553                                        self_mode: *receiver_mode,
554                                    },
555                                    method_inst.span,
556                                ) {
557                                    Ok((
558                                        analyzed,
559                                        warnings,
560                                        local_strings,
561                                        local_bytes,
562                                        referenced_fns,
563                                        referenced_meths,
564                                    )) => {
565                                        functions_with_strings.push((
566                                            analyzed,
567                                            local_strings,
568                                            local_bytes,
569                                        ));
570                                        all_warnings.extend(warnings);
571
572                                        for ref_fn in referenced_fns {
573                                            if !analyzed_functions.contains(&ref_fn) {
574                                                pending_functions.push(ref_fn);
575                                            }
576                                        }
577                                        for ref_meth in referenced_meths {
578                                            if !analyzed_methods.contains(&ref_meth) {
579                                                pending_methods.push(ref_meth);
580                                            }
581                                        }
582                                    }
583                                    Err(e) => errors.push(e),
584                                }
585                            }
586                        }
587                    }
588                }
589            }
590        }
591
592        // Post-processing is one-shot — derives, destructors, vtables, and the
593        // anonymous-method fixed points each enumerate sema state directly and
594        // would re-emit duplicate analyzed bodies on a second pass. A labeled
595        // block lets later outer-loop iterations skip the whole section after
596        // the first round without re-indenting the existing code.
597        'post_processing: {
598            if post_processed {
599                break 'post_processing;
600            }
601
602            // ADR-0078: catch-all for anonymous struct methods registered after the
603            // work queue drained. Specialization (for `comptime F: type` parameters
604            // bound to anonymous-struct callable types) and other late-registration
605            // paths can leave `__call` methods unanalyzed if their references
606            // weren't tracked through `ctx.referenced_methods`. The fixed-point
607            // loop here mirrors the anonymous-enum fixed-point loop just below.
608            let mut analyzed_anon_struct_methods: HashSet<(StructId, Spur)> = HashSet::default();
609            // Pre-seed with the methods the work queue already analyzed so we don't
610            // double-emit.
611            for (struct_id, method_name) in &analyzed_methods {
612                analyzed_anon_struct_methods.insert((*struct_id, *method_name));
613            }
614            loop {
615                let pending_anon_struct_methods: Vec<(StructId, Spur, MethodInfo)> = sema
616                    .methods
617                    .iter()
618                    .filter_map(|((struct_id, method_name), method_info)| {
619                        let struct_def = sema.type_pool.struct_def(*struct_id);
620                        if struct_def.name.starts_with("__anon_struct_")
621                            && !analyzed_anon_struct_methods.contains(&(*struct_id, *method_name))
622                            && !method_info.is_generic
623                        {
624                            // ADR-0082: skip Copy-gated Vec methods when
625                            // the instance's element type is non-Copy.
626                            // Their bodies (e.g. `clone`, `eq`, `cmp`,
627                            // `contains`, …) read elements via
628                            // `ptr.offset(i).read()` which sema rejects
629                            // for non-Copy T. The dispatch path already
630                            // returns "T: Copy required" before we get
631                            // here, so the bodies are unreachable.
632                            let m_name = sema.interner.resolve(method_name);
633                            let copy_only = matches!(
634                                m_name,
635                                "clone"
636                                    | "eq"
637                                    | "cmp"
638                                    | "contains"
639                                    | "starts_with"
640                                    | "ends_with"
641                                    | "concat"
642                                    | "extend_from_slice"
643                            );
644                            if copy_only
645                                && let Some(subst) = sema.anon_struct_type_subst.get(struct_id)
646                                && let Some(t_sym) = sema.interner.get("T")
647                                && let Some(&elem_ty) = subst.get(&t_sym)
648                                && !sema.is_type_copy(elem_ty)
649                            {
650                                return None;
651                            }
652                            Some((*struct_id, *method_name, *method_info))
653                        } else {
654                            None
655                        }
656                    })
657                    .collect();
658                if pending_anon_struct_methods.is_empty() {
659                    break;
660                }
661                for (struct_id, method_name, method_info) in pending_anon_struct_methods {
662                    analyzed_anon_struct_methods.insert((struct_id, method_name));
663                    let struct_def = sema.type_pool.struct_def(struct_id);
664                    let type_name_str = struct_def.name.clone();
665                    let method_name_str = sema.interner.resolve(&method_name).to_string();
666                    let full_name = if method_info.has_self {
667                        format!("{}.{}", type_name_str, method_name_str)
668                    } else {
669                        format!("{}::{}", type_name_str, method_name_str)
670                    };
671
672                    let param_names = sema.param_arena.names(method_info.params);
673                    let param_types = sema.param_arena.types(method_info.params);
674                    let param_modes = sema.param_arena.modes(method_info.params);
675                    let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
676                    if method_info.has_self {
677                        let self_sym = sema.interner.get_or_intern("self");
678                        let self_mode = match method_info.receiver {
679                            crate::types::ReceiverMode::ByValue => RirParamMode::Normal,
680                            crate::types::ReceiverMode::Ref => RirParamMode::Ref,
681                            crate::types::ReceiverMode::MutRef => RirParamMode::MutRef,
682                        };
683                        param_info.push((self_sym, method_info.struct_type, self_mode));
684                    }
685                    for i in 0..param_names.len() {
686                        param_info.push((param_names[i], param_types[i], param_modes[i]));
687                    }
688                    let struct_id = method_info
689                        .struct_type
690                        .as_struct()
691                        .expect("method must belong to struct");
692                    let captured_values = sema
693                        .anon_struct_captured_values
694                        .get(&struct_id)
695                        .cloned()
696                        .unwrap_or_else(HashMap::default);
697                    let outer_type_subst = sema.anon_struct_type_subst.get(&struct_id).cloned();
698                    match sema.analyze_method_body(
699                        &infer_ctx,
700                        method_info.return_type,
701                        &param_info,
702                        method_info.body,
703                        method_info.struct_type,
704                        &captured_values,
705                        outer_type_subst.as_ref(),
706                    ) {
707                        Ok((
708                            air,
709                            num_locals,
710                            num_param_slots,
711                            param_modes_result,
712                            param_slot_types,
713                            warnings,
714                            local_strings,
715                            local_bytes,
716                            _ref_fns,
717                            _ref_meths,
718                        )) => {
719                            let analyzed = AnalyzedFunction {
720                                name: full_name,
721                                air,
722                                num_locals,
723                                num_param_slots,
724                                param_modes: param_modes_result,
725                                param_slot_types,
726                                is_destructor: false,
727                            };
728                            functions_with_strings.push((analyzed, local_strings, local_bytes));
729                            all_warnings.extend(warnings);
730                        }
731                        Err(e) => errors.push(e),
732                    }
733                }
734            }
735
736            // Analyze anonymous enum methods that were registered during comptime evaluation.
737            // These are not tracked by the work queue (which only handles struct methods),
738            // so we process them in a fixed-point loop similar to the eager path.
739            let mut analyzed_anon_enum_methods: HashSet<(EnumId, Spur)> = HashSet::default();
740            loop {
741                let pending_anon_enum_methods: Vec<(EnumId, Spur, MethodInfo)> = sema
742                    .enum_methods
743                    .iter()
744                    .filter_map(|((enum_id, method_name), method_info)| {
745                        let enum_def = sema.type_pool.enum_def(*enum_id);
746                        if enum_def.name.starts_with("__anon_enum_")
747                            && !analyzed_anon_enum_methods.contains(&(*enum_id, *method_name))
748                        {
749                            Some((*enum_id, *method_name, *method_info))
750                        } else {
751                            None
752                        }
753                    })
754                    .collect();
755
756                if pending_anon_enum_methods.is_empty() {
757                    break;
758                }
759
760                for (enum_id, method_name, method_info) in pending_anon_enum_methods {
761                    analyzed_anon_enum_methods.insert((enum_id, method_name));
762
763                    let enum_def = sema.type_pool.enum_def(enum_id);
764                    let type_name_str = enum_def.name.clone();
765                    let method_name_str = sema.interner.resolve(&method_name).to_string();
766
767                    let full_name = if method_info.has_self {
768                        format!("{}.{}", type_name_str, method_name_str)
769                    } else {
770                        format!("{}::{}", type_name_str, method_name_str)
771                    };
772
773                    let param_names = sema.param_arena.names(method_info.params);
774                    let param_types = sema.param_arena.types(method_info.params);
775                    let param_modes = sema.param_arena.modes(method_info.params);
776
777                    let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
778
779                    if method_info.has_self {
780                        let self_sym = sema.interner.get_or_intern("self");
781                        let self_mode = match method_info.receiver {
782                            crate::types::ReceiverMode::ByValue => RirParamMode::Normal,
783                            crate::types::ReceiverMode::Ref => RirParamMode::Ref,
784                            crate::types::ReceiverMode::MutRef => RirParamMode::MutRef,
785                        };
786                        param_info.push((self_sym, method_info.struct_type, self_mode));
787                    }
788
789                    for i in 0..param_names.len() {
790                        param_info.push((param_names[i], param_types[i], param_modes[i]));
791                    }
792
793                    let captured_values = sema
794                        .anon_enum_captured_values
795                        .get(&enum_id)
796                        .cloned()
797                        .unwrap_or_else(HashMap::default);
798                    let outer_type_subst = sema.anon_enum_type_subst.get(&enum_id).cloned();
799
800                    match sema.analyze_method_body(
801                        &infer_ctx,
802                        method_info.return_type,
803                        &param_info,
804                        method_info.body,
805                        method_info.struct_type,
806                        &captured_values,
807                        outer_type_subst.as_ref(),
808                    ) {
809                        Ok((
810                            air,
811                            num_locals,
812                            num_param_slots,
813                            param_modes_result,
814                            param_slot_types,
815                            warnings,
816                            local_strings,
817                            local_bytes,
818                            _ref_fns,
819                            _ref_meths,
820                        )) => {
821                            let analyzed = AnalyzedFunction {
822                                name: full_name,
823                                air,
824                                num_locals,
825                                num_param_slots,
826                                param_modes: param_modes_result,
827                                param_slot_types,
828                                is_destructor: false,
829                            };
830                            functions_with_strings.push((analyzed, local_strings, local_bytes));
831                            all_warnings.extend(warnings);
832                        }
833                        Err(e) => errors.push(e),
834                    }
835                }
836            }
837
838            // ADR-0053: destructors are inline `fn __drop(self)` methods
839            // declared inside the struct body. They flow through the
840            // regular method-analysis path above; no separate top-level
841            // `drop fn TypeName(self)` form exists.
842
843            // ADR-0056 vtable population: every (struct, interface) pair recorded
844            // in `interface_vtables_needed` references the conformer's slot
845            // methods. Codegen emits the vtable, so those methods must be in the
846            // analyzed functions list. Queue them for analysis if the work loop
847            // didn't already pick them up.
848            let vtable_methods: Vec<(StructId, Spur)> = sema
849                .interface_vtables_needed
850                .values()
851                .flat_map(|witness| witness.iter().copied())
852                .collect();
853            for (struct_id, method_name) in vtable_methods {
854                if !analyzed_methods.contains(&(struct_id, method_name)) {
855                    pending_methods.push((struct_id, method_name));
856                }
857            }
858            // Drain again now that vtable methods may have been queued.
859            while !pending_methods.is_empty() {
860                let queue = std::mem::take(&mut pending_methods);
861                let mut local_pending = queue;
862                while let Some((struct_id, method_name)) = local_pending.pop() {
863                    if analyzed_methods.contains(&(struct_id, method_name)) {
864                        continue;
865                    }
866                    analyzed_methods.insert((struct_id, method_name));
867
868                    let (method_info, is_enum_method) =
869                        if let Some(info) = sema.methods.get(&(struct_id, method_name)) {
870                            (*info, false)
871                        } else if let Some(info) = sema
872                            .enum_methods
873                            .get(&(crate::types::EnumId(struct_id.0), method_name))
874                        {
875                            (*info, true)
876                        } else {
877                            continue;
878                        };
879                    if method_info.is_generic {
880                        continue;
881                    }
882
883                    let type_name_str = if is_enum_method {
884                        sema.type_pool
885                            .enum_def(crate::types::EnumId(struct_id.0))
886                            .name
887                            .clone()
888                    } else {
889                        sema.type_pool.struct_def(struct_id).name.clone()
890                    };
891                    let type_name_sym = sema.interner.get_or_intern(&type_name_str);
892                    let method_name_str = sema.interner.resolve(&method_name).to_string();
893
894                    // Find the FnDecl in either struct or enum declarations and
895                    // analyze its body. (Anon types are handled by the fixed-point
896                    // loops below.)
897                    let decl_iter: Vec<_> = sema.rir.iter().collect();
898                    let mut handled = false;
899                    for (_, inst) in decl_iter {
900                        let (decl_name, methods_start, methods_len) = match &inst.data {
901                            InstData::StructDecl {
902                                name,
903                                methods_start,
904                                methods_len,
905                                ..
906                            } if !is_enum_method => (*name, *methods_start, *methods_len),
907                            InstData::EnumDecl {
908                                name,
909                                methods_start,
910                                methods_len,
911                                ..
912                            } if is_enum_method => (*name, *methods_start, *methods_len),
913                            _ => continue,
914                        };
915                        if decl_name != type_name_sym {
916                            continue;
917                        }
918                        let methods = sema.rir.get_inst_refs(methods_start, methods_len);
919                        for method_ref in methods {
920                            let method_inst = sema.rir.get(method_ref);
921                            let InstData::FnDecl {
922                                name: m_name,
923                                params_start,
924                                params_len,
925                                return_type,
926                                body,
927                                has_self,
928                                receiver_mode,
929                                ..
930                            } = &method_inst.data
931                            else {
932                                continue;
933                            };
934                            if *m_name != method_name {
935                                continue;
936                            }
937                            let params = sema.rir.get_params(*params_start, *params_len);
938                            let full_name = if *has_self {
939                                format!("{}.{}", type_name_str, method_name_str)
940                            } else {
941                                format!("{}::{}", type_name_str, method_name_str)
942                            };
943                            match sema.analyze_method_function(
944                                &infer_ctx,
945                                &full_name,
946                                MethodBodySpec {
947                                    return_type: *return_type,
948                                    params: &params,
949                                    body: *body,
950                                    host_type: Some(method_info.struct_type),
951                                    has_self: *has_self,
952                                    self_mode: *receiver_mode,
953                                },
954                                method_inst.span,
955                            ) {
956                                Ok((analyzed, warnings, local_strings, local_bytes, _, _)) => {
957                                    functions_with_strings.push((
958                                        analyzed,
959                                        local_strings,
960                                        local_bytes,
961                                    ));
962                                    all_warnings.extend(warnings);
963                                }
964                                Err(e) => errors.push(e),
965                            }
966                            handled = true;
967                            break;
968                        }
969                        if handled {
970                            break;
971                        }
972                    }
973                }
974            }
975
976            // ADR-0058: derive-bound methods spliced onto host types via
977            // `@derive(...)` directives. Same as the sequential path; the work
978            // queue doesn't reach these because they aren't discovered through
979            // direct call lookup.
980            let derive_jobs: Vec<(Spur, Spur, bool, super::DeriveBinding)> = sema
981                .derive_bindings
982                .iter()
983                .map(|b| (b.derive_name, b.host_name, b.host_is_enum, *b))
984                .collect();
985            for (derive_name, host_name, host_is_enum, _binding) in derive_jobs {
986                let dmethods: Vec<crate::sema::info::DeriveMethod> =
987                    match sema.derives.get(&derive_name) {
988                        Some(info) => info.methods.clone(),
989                        None => continue,
990                    };
991                if host_is_enum {
992                    let enum_id = match sema.enums.get(&host_name).copied() {
993                        Some(id) => id,
994                        None => continue,
995                    };
996                    let enum_type = Type::new_enum(enum_id);
997                    let host_str = sema.type_pool.enum_def(enum_id).name.clone();
998                    for dm in dmethods {
999                        let m = sema.rir.get(dm.method_ref);
1000                        let InstData::FnDecl {
1001                            name: method_name,
1002                            params_start,
1003                            params_len,
1004                            return_type,
1005                            body,
1006                            has_self,
1007                            receiver_mode,
1008                            ..
1009                        } = &m.data
1010                        else {
1011                            continue;
1012                        };
1013                        let method_str = sema.interner.resolve(method_name).to_string();
1014                        let params = sema.rir.get_params(*params_start, *params_len);
1015                        let full_name = if *has_self {
1016                            format!("{}.{}", host_str, method_str)
1017                        } else {
1018                            format!("{}::{}", host_str, method_str)
1019                        };
1020                        match sema.analyze_method_function(
1021                            &infer_ctx,
1022                            &full_name,
1023                            MethodBodySpec {
1024                                return_type: *return_type,
1025                                params: &params,
1026                                body: *body,
1027                                host_type: Some(enum_type),
1028                                has_self: *has_self,
1029                                self_mode: *receiver_mode,
1030                            },
1031                            m.span,
1032                        ) {
1033                            Ok((
1034                                analyzed,
1035                                warnings,
1036                                local_strings,
1037                                local_bytes,
1038                                referenced_fns,
1039                                referenced_meths,
1040                            )) => {
1041                                functions_with_strings.push((analyzed, local_strings, local_bytes));
1042                                all_warnings.extend(warnings);
1043                                // ADR-0081: feed references from the derived
1044                                // body back into the work queue so symbols
1045                                // it depends on (e.g. `String.clone` from a
1046                                // `@derive(Clone)` body's `@field(...).clone()`
1047                                // calls) get analyzed and emitted.
1048                                for ref_fn in referenced_fns {
1049                                    if !analyzed_functions.contains(&ref_fn) {
1050                                        pending_functions.push(ref_fn);
1051                                    }
1052                                }
1053                                for ref_meth in referenced_meths {
1054                                    if !analyzed_methods.contains(&ref_meth) {
1055                                        pending_methods.push(ref_meth);
1056                                    }
1057                                }
1058                            }
1059                            Err(e) => errors.push(e),
1060                        }
1061                    }
1062                } else {
1063                    let struct_id = match sema.structs.get(&host_name).copied() {
1064                        Some(id) => id,
1065                        None => continue,
1066                    };
1067                    let struct_type = Type::new_struct(struct_id);
1068                    let host_str = sema.type_pool.struct_def(struct_id).name.clone();
1069                    for dm in dmethods {
1070                        let m = sema.rir.get(dm.method_ref);
1071                        let InstData::FnDecl {
1072                            name: method_name,
1073                            params_start,
1074                            params_len,
1075                            return_type,
1076                            body,
1077                            has_self,
1078                            receiver_mode,
1079                            ..
1080                        } = &m.data
1081                        else {
1082                            continue;
1083                        };
1084                        let method_str = sema.interner.resolve(method_name).to_string();
1085                        let params = sema.rir.get_params(*params_start, *params_len);
1086                        let full_name = if *has_self {
1087                            format!("{}.{}", host_str, method_str)
1088                        } else {
1089                            format!("{}::{}", host_str, method_str)
1090                        };
1091                        match sema.analyze_method_function(
1092                            &infer_ctx,
1093                            &full_name,
1094                            MethodBodySpec {
1095                                return_type: *return_type,
1096                                params: &params,
1097                                body: *body,
1098                                host_type: Some(struct_type),
1099                                has_self: *has_self,
1100                                self_mode: *receiver_mode,
1101                            },
1102                            m.span,
1103                        ) {
1104                            Ok((
1105                                analyzed,
1106                                warnings,
1107                                local_strings,
1108                                local_bytes,
1109                                referenced_fns,
1110                                referenced_meths,
1111                            )) => {
1112                                functions_with_strings.push((analyzed, local_strings, local_bytes));
1113                                all_warnings.extend(warnings);
1114                                // ADR-0081: feed references from the derived
1115                                // body back into the work queue.
1116                                for ref_fn in referenced_fns {
1117                                    if !analyzed_functions.contains(&ref_fn) {
1118                                        pending_functions.push(ref_fn);
1119                                    }
1120                                }
1121                                for ref_meth in referenced_meths {
1122                                    if !analyzed_methods.contains(&ref_meth) {
1123                                        pending_methods.push(ref_meth);
1124                                    }
1125                                }
1126                            }
1127                            Err(e) => errors.push(e),
1128                        }
1129                    }
1130                }
1131            }
1132
1133            // ADR-0053 phase 3 / 3b: also analyze inline `fn __drop(self)` destructors
1134            // (struct- or enum-body declared) — same as the sequential path. The
1135            // lazy work queue doesn't reach these because the methods aren't
1136            // discovered through call dispatch.
1137            let inline_struct_drops: Vec<(StructId, InstRef, Span)> = sema
1138                .inline_struct_drops
1139                .iter()
1140                .map(|(sid, (body, span))| (*sid, *body, *span))
1141                .collect();
1142            for (struct_id, body, drop_span) in inline_struct_drops {
1143                let struct_def = sema.type_pool.struct_def(struct_id);
1144                let type_name_str = struct_def.name.clone();
1145                let full_name = format!("{}.__drop", type_name_str);
1146                let struct_type = Type::new_struct(struct_id);
1147                match sema.analyze_destructor_function(
1148                    &infer_ctx,
1149                    &full_name,
1150                    body,
1151                    drop_span,
1152                    struct_type,
1153                ) {
1154                    Ok((analyzed, warnings, local_strings, local_bytes, ref_fns, ref_meths)) => {
1155                        functions_with_strings.push((analyzed, local_strings, local_bytes));
1156                        all_warnings.extend(warnings);
1157                        // ADR-0087 Phase 2: destructor bodies may reference
1158                        // prelude wrappers (e.g. via `@dbg`'s lowered targets
1159                        // — `dbg_i64_noln`, `dbg_newline`, …). Feed the
1160                        // refs back into the work queue so the wrapper fns
1161                        // get analyzed and emitted.
1162                        for fname in ref_fns {
1163                            if !analyzed_functions.contains(&fname) {
1164                                pending_functions.push(fname);
1165                            }
1166                        }
1167                        for mref in ref_meths {
1168                            if !analyzed_methods.contains(&mref) {
1169                                pending_methods.push(mref);
1170                            }
1171                        }
1172                    }
1173                    Err(e) => errors.push(e),
1174                }
1175            }
1176            let inline_enum_drops_vec: Vec<(EnumId, InstRef, Span)> = sema
1177                .inline_enum_drops
1178                .iter()
1179                .map(|(eid, (body, span))| (*eid, *body, *span))
1180                .collect();
1181            for (enum_id, body, drop_span) in inline_enum_drops_vec {
1182                let enum_def = sema.type_pool.enum_def(enum_id);
1183                let type_name_str = enum_def.name.clone();
1184                let full_name = format!("{}.__drop", type_name_str);
1185                let enum_type = Type::new_enum(enum_id);
1186                match sema
1187                    .analyze_destructor_function(&infer_ctx, &full_name, body, drop_span, enum_type)
1188                {
1189                    Ok((analyzed, warnings, local_strings, local_bytes, ref_fns, ref_meths)) => {
1190                        functions_with_strings.push((analyzed, local_strings, local_bytes));
1191                        all_warnings.extend(warnings);
1192                        // ADR-0087 Phase 2: see comment on the struct-drops
1193                        // loop above.
1194                        for fname in ref_fns {
1195                            if !analyzed_functions.contains(&fname) {
1196                                pending_functions.push(fname);
1197                            }
1198                        }
1199                        for mref in ref_meths {
1200                            if !analyzed_methods.contains(&mref) {
1201                                pending_methods.push(mref);
1202                            }
1203                        }
1204                    }
1205                    Err(e) => errors.push(e),
1206                }
1207            }
1208
1209            post_processed = true;
1210        }
1211
1212        // Defensive: post-processing's inner loops drain their own work, but
1213        // re-enter the outer loop if anything is still pending so the BFS can
1214        // absorb it before specialization runs.
1215        if !pending_functions.is_empty() || !pending_methods.is_empty() {
1216            continue;
1217        }
1218
1219        // Run specialization. It collects every CallGeneric across the
1220        // analyzed bodies, rewrites them to direct Call instructions, and
1221        // synthesizes specialized bodies — re-running internally until no
1222        // new specialization keys appear. The references it returns are
1223        // method/function names the synthesized bodies depend on; we feed
1224        // them back into the work queue so reachability stays closed.
1225        let refs = match crate::specialize::specialize(
1226            &mut functions_with_strings,
1227            &mut spec_name_map,
1228            sema,
1229            &infer_ctx,
1230            sema.interner,
1231        ) {
1232            Ok(refs) => refs,
1233            Err(e) => {
1234                errors.push(e);
1235                crate::specialize::SpecializationRefs::default()
1236            }
1237        };
1238
1239        let mut had_new = false;
1240        for f in refs.fns {
1241            if !analyzed_functions.contains(&f) {
1242                pending_functions.push(f);
1243                had_new = true;
1244            }
1245        }
1246        for m in refs.meths {
1247            if !analyzed_methods.contains(&m) {
1248                pending_methods.push(m);
1249                had_new = true;
1250            }
1251        }
1252
1253        if !had_new {
1254            break;
1255        }
1256    }
1257
1258    // Merge strings from all functions into a global table with deduplication.
1259    // Bytes pools are concatenated (no dedup) — each `@embed_file` call gets
1260    // a fresh entry since these are rare and may legitimately repeat.
1261    let mut global_string_table: HashMap<String, u32> = HashMap::default();
1262    let mut global_strings: Vec<String> = Vec::new();
1263    let mut global_bytes: Vec<Vec<u8>> = Vec::new();
1264
1265    let mut functions: Vec<AnalyzedFunction> = Vec::new();
1266    for (mut analyzed, local_strings, local_bytes) in functions_with_strings {
1267        if !local_strings.is_empty() {
1268            let local_to_global: Vec<u32> = local_strings
1269                .into_iter()
1270                .map(|s| {
1271                    *global_string_table.entry(s.clone()).or_insert_with(|| {
1272                        let id = global_strings.len() as u32;
1273                        global_strings.push(s);
1274                        id
1275                    })
1276                })
1277                .collect();
1278
1279            analyzed
1280                .air
1281                .remap_string_ids(|local_id| local_to_global[local_id as usize]);
1282        }
1283        if !local_bytes.is_empty() {
1284            let bytes_offset = global_bytes.len() as u32;
1285            global_bytes.extend(local_bytes);
1286            analyzed
1287                .air
1288                .remap_bytes_ids(|local_id| local_id + bytes_offset);
1289        }
1290        functions.push(analyzed);
1291    }
1292
1293    // Emit warnings for any comptime @dbg calls that occurred during comptime evaluation.
1294    for (msg, span) in std::mem::take(&mut sema.comptime_log_output) {
1295        all_warnings.push(CompileWarning::new(
1296            WarningKind::ComptimeDbgPresent(msg),
1297            span,
1298        ));
1299    }
1300
1301    all_warnings.sort_by_key(|w| w.span().map(|s| s.start));
1302
1303    let output = SemaOutput {
1304        functions,
1305        strings: global_strings,
1306        bytes: global_bytes,
1307        warnings: all_warnings,
1308        type_pool: sema.type_pool.clone(),
1309        comptime_dbg_output: std::mem::take(&mut sema.comptime_dbg_output),
1310        interface_defs: sema.interface_defs.clone(),
1311        interface_vtables: sema.interface_vtables_needed.clone(),
1312    };
1313
1314    // Surface anonymous-host derive expansion errors (ADR-0058).
1315    for e in std::mem::take(&mut sema.pending_anon_derive_errors) {
1316        errors.push(e);
1317    }
1318    // Surface anon-struct/enum eval validation errors that weren't drained by
1319    // their evaluating call site (e.g. an unreached / dead type-constructor).
1320    for e in std::mem::take(&mut sema.pending_anon_eval_errors) {
1321        errors.push(e);
1322    }
1323
1324    errors.into_result_with(output)
1325}
1326
1327// ============================================================================
1328// Helper functions for parallel analysis (using SemaContext)
1329// ============================================================================
1330
1331impl<'a> Sema<'a> {
1332    /// Check that a preview feature is enabled.
1333    ///
1334    /// This is used to gate experimental features behind the `--preview` flag.
1335    /// Returns an error with a helpful message if the feature is not enabled.
1336    ///
1337    /// # Arguments
1338    /// - `feature`: The preview feature to check
1339    /// - `what`: Human-readable description of what requires this feature
1340    /// - `span`: The source location where the feature is used
1341    ///
1342    /// # Returns
1343    /// - `Ok(())` if the feature is enabled
1344    /// - `Err(CompileError)` with a helpful message if not enabled
1345    pub(crate) fn require_preview(
1346        &self,
1347        feature: PreviewFeature,
1348        what: &str,
1349        span: Span,
1350    ) -> CompileResult<()> {
1351        if self.preview_features.contains(&feature) {
1352            Ok(())
1353        } else {
1354            Err(CompileError::new(
1355                ErrorKind::PreviewFeatureRequired {
1356                    feature,
1357                    what: what.to_string(),
1358                },
1359                span,
1360            )
1361            .with_help(format!(
1362                "use `--preview {}` to enable this feature ({})",
1363                feature.name(),
1364                feature.adr()
1365            )))
1366        }
1367    }
1368
1369    /// ADR-0073: cross-module field visibility check.
1370    ///
1371    /// Non-`pub` fields are accessible only from inside the type's home
1372    /// module. Built-ins are homed in the synthetic `<builtin>` file, so
1373    /// their non-`pub` fields are unreachable from user code.
1374    pub(crate) fn check_field_visibility(
1375        &self,
1376        struct_def: &crate::types::StructDef,
1377        field: &crate::types::StructField,
1378        access_span: Span,
1379    ) -> CompileResult<()> {
1380        let accessing_file_id = access_span.file_id;
1381        let target_file_id = struct_def.file_id;
1382        if !self.is_accessible(accessing_file_id, target_file_id, field.is_pub) {
1383            return Err(CompileError::new(
1384                ErrorKind::PrivateField {
1385                    struct_name: struct_def.name.clone(),
1386                    field_name: field.name.clone(),
1387                },
1388                access_span,
1389            ));
1390        }
1391        Ok(())
1392    }
1393
1394    /// ADR-0073: cross-module method visibility check.
1395    ///
1396    /// Non-`pub` methods are callable only from inside the type's home
1397    /// module.
1398    pub(crate) fn check_method_visibility(
1399        &self,
1400        type_name: &str,
1401        _is_builtin: bool,
1402        method_is_pub: bool,
1403        method_file_id: gruel_util::FileId,
1404        method_name: &str,
1405        access_span: Span,
1406    ) -> CompileResult<()> {
1407        let accessing_file_id = access_span.file_id;
1408        if !self.is_accessible(accessing_file_id, method_file_id, method_is_pub) {
1409            return Err(CompileError::new(
1410                ErrorKind::PrivateMemberAccess {
1411                    item_kind: "method".to_string(),
1412                    name: format!("{}::{}", type_name, method_name),
1413                },
1414                access_span,
1415            ));
1416        }
1417        Ok(())
1418    }
1419
1420    /// Check that we are inside a `checked` block.
1421    /// Returns an error if `checked_depth` is zero.
1422    pub(crate) fn require_checked_for_intrinsic(
1423        ctx: &AnalysisContext,
1424        intrinsic_name: &str,
1425        span: Span,
1426    ) -> CompileResult<()> {
1427        if ctx.checked_depth > 0 {
1428            Ok(())
1429        } else {
1430            Err(CompileError::new(
1431                ErrorKind::IntrinsicRequiresChecked(intrinsic_name.to_string()),
1432                span,
1433            ))
1434        }
1435    }
1436
1437    fn analyze_single_function(
1438        &mut self,
1439        infer_ctx: &InferenceContext,
1440        fn_name: &str,
1441        return_type: Spur,
1442        params: &[gruel_rir::RirParam],
1443        body: InstRef,
1444        span: Span,
1445    ) -> AnalyzedFnResult {
1446        let ret_type = self.resolve_type(return_type, span)?;
1447
1448        // Resolve parameter types and modes. Use `resolve_param_type` so
1449        // interface-typed parameters (ADR-0056 Phase 4) and `Ref(I)` /
1450        // `MutRef(I)` interface refs (ADR-0076 Phase 2) resolve correctly.
1451        // The returned mode may differ from `p.mode` after normalization.
1452        let param_info: Vec<(Spur, Type, RirParamMode)> = params
1453            .iter()
1454            .map(|p| {
1455                let (ty, mode) = self.resolve_param_type(p.ty, p.mode, span)?;
1456                Ok((p.name, ty, mode))
1457            })
1458            .collect::<CompileResult<Vec<_>>>()?;
1459
1460        let (
1461            air,
1462            num_locals,
1463            num_param_slots,
1464            param_modes,
1465            param_slot_types,
1466            warnings,
1467            local_strings,
1468            local_bytes,
1469            ref_fns,
1470            ref_meths,
1471        ) = self.analyze_function(infer_ctx, ret_type, &param_info, body)?;
1472
1473        Ok((
1474            AnalyzedFunction {
1475                name: fn_name.to_string(),
1476                air,
1477                num_locals,
1478                num_param_slots,
1479                param_modes,
1480                param_slot_types,
1481                is_destructor: false,
1482            },
1483            warnings,
1484            local_strings,
1485            local_bytes,
1486            ref_fns,
1487            ref_meths,
1488        ))
1489    }
1490
1491    /// Analyze a method function from an impl block.
1492    ///
1493    /// The `infer_ctx` provides pre-computed type information for constraint generation.
1494    ///
1495    /// Returns the analyzed function, any warnings, and local strings collected during analysis.
1496    fn analyze_method_function(
1497        &mut self,
1498        infer_ctx: &InferenceContext,
1499        full_name: &str,
1500        spec: MethodBodySpec<'_>,
1501        span: Span,
1502    ) -> AnalyzedFnResult {
1503        // ADR-0076: bind `Self` for the duration of body analysis whenever
1504        // we are inside a struct/enum body — including associated functions
1505        // like `fn new() -> Self` that have no `self` parameter. The host
1506        // type is what `Self` refers to in `Self::Variant`, `Self { ... }`,
1507        // and any wrapped form (`Vec(Self)`, `Ref(Self)`, …).
1508        let saved_self = self.current_self;
1509        if let Some(host) = spec.host_type {
1510            self.current_self = Some(host);
1511        }
1512
1513        let ret_type = self.resolve_type(spec.return_type, span)?;
1514
1515        // Build parameter list, adding self as first parameter for methods
1516        let mut param_info: Vec<(Spur, Type, RirParamMode)> = Vec::new();
1517
1518        if spec.has_self {
1519            let host = spec
1520                .host_type
1521                .expect("MethodBodySpec.has_self=true requires host_type to be set");
1522            // ADR-0076: encode the receiver shape directly in the synthesized
1523            // self parameter's type — the byte-encoded receiver mode set by
1524            // the parser (1 = `MutRef(Self)`, 2 = `Ref(Self)`, 0 = by-value)
1525            // becomes a `MutRef(Self)` / `Ref(Self)` / `Self` type with a
1526            // `Normal` parameter mode. Body analysis, borrow tracking, and
1527            // codegen all key off the type pool from this point forward.
1528            let self_ty = match spec.self_mode {
1529                1 => Type::new_mut_ref(self.type_pool.intern_mut_ref_from_type(host)),
1530                2 => Type::new_ref(self.type_pool.intern_ref_from_type(host)),
1531                _ => host,
1532            };
1533            let self_sym = self.interner.get_or_intern("self");
1534            param_info.push((self_sym, self_ty, RirParamMode::Normal));
1535        }
1536
1537        // Add regular parameters with their modes. Use `resolve_param_type`
1538        // for ADR-0056 interface-typed parameters; ADR-0076 Phase 2 also
1539        // normalizes `Ref(I)` / `MutRef(I)` here, so the returned mode may
1540        // differ from `p.mode`.
1541        for p in spec.params.iter() {
1542            let (ty, mode) = self.resolve_param_type(p.ty, p.mode, span)?;
1543            param_info.push((p.name, ty, mode));
1544        }
1545
1546        let (
1547            air,
1548            num_locals,
1549            num_param_slots,
1550            param_modes,
1551            param_slot_types,
1552            warnings,
1553            local_strings,
1554            local_bytes,
1555            ref_fns,
1556            ref_meths,
1557        ) = self.analyze_function(infer_ctx, ret_type, &param_info, spec.body)?;
1558
1559        self.current_self = saved_self;
1560
1561        Ok((
1562            AnalyzedFunction {
1563                name: full_name.to_string(),
1564                air,
1565                num_locals,
1566                num_param_slots,
1567                param_modes,
1568                param_slot_types,
1569                is_destructor: false,
1570            },
1571            warnings,
1572            local_strings,
1573            local_bytes,
1574            ref_fns,
1575            ref_meths,
1576        ))
1577    }
1578
1579    /// Analyze a destructor function.
1580    ///
1581    /// The `infer_ctx` provides pre-computed type information for constraint generation.
1582    ///
1583    /// Returns the analyzed function, any warnings, and local strings collected during analysis.
1584    fn analyze_destructor_function(
1585        &mut self,
1586        infer_ctx: &InferenceContext,
1587        full_name: &str,
1588        body: InstRef,
1589        _span: Span,
1590        struct_type: Type,
1591    ) -> AnalyzedFnResult {
1592        // ADR-0076: bind `Self` to the host struct/enum while analyzing the
1593        // destructor body so `Self::Variant` / `Self { ... }` resolve.
1594        let saved_self = self.current_self.replace(struct_type);
1595
1596        // Destructors take self parameter and return unit
1597        let self_sym = self.interner.get_or_intern("self");
1598        let param_info: Vec<(Spur, Type, RirParamMode)> =
1599            vec![(self_sym, struct_type, RirParamMode::Normal)];
1600
1601        let (
1602            air,
1603            num_locals,
1604            num_param_slots,
1605            param_modes,
1606            param_slot_types,
1607            warnings,
1608            local_strings,
1609            local_bytes,
1610            ref_fns,
1611            ref_meths,
1612        ) = self.analyze_function(infer_ctx, Type::UNIT, &param_info, body)?;
1613
1614        self.current_self = saved_self;
1615
1616        Ok((
1617            AnalyzedFunction {
1618                name: full_name.to_string(),
1619                air,
1620                num_locals,
1621                num_param_slots,
1622                param_modes,
1623                param_slot_types,
1624                is_destructor: true,
1625            },
1626            warnings,
1627            local_strings,
1628            local_bytes,
1629            ref_fns,
1630            ref_meths,
1631        ))
1632    }
1633    /// Analyze a single function, producing AIR.
1634    ///
1635    /// The `infer_ctx` provides pre-computed type information for constraint generation,
1636    /// avoiding the cost of rebuilding maps for each function.
1637    ///
1638    /// Returns (air, num_locals, num_param_slots, param_modes, warnings).
1639    /// Warnings are collected per-function to enable future parallel analysis.
1640    fn analyze_function(
1641        &mut self,
1642        infer_ctx: &InferenceContext,
1643        return_type: Type,
1644        params: &[(Spur, Type, RirParamMode)], // (name, type, mode)
1645        body: InstRef,
1646    ) -> RawFnAnalysis {
1647        self.analyze_function_internal(infer_ctx, return_type, params, body, None, None)
1648    }
1649
1650    /// Internal function analysis with optional type substitutions.
1651    ///
1652    /// When `type_subst` is provided (for specialized generic functions), it populates
1653    /// `comptime_type_vars` so that type parameters can be resolved in struct initialization
1654    /// (e.g., `P { x: 1, y: 2 }` where `P` is a type parameter).
1655    fn analyze_function_internal(
1656        &mut self,
1657        infer_ctx: &InferenceContext,
1658        return_type: Type,
1659        params: &[(Spur, Type, RirParamMode)],
1660        body: InstRef,
1661        type_subst: Option<&rustc_hash::FxHashMap<Spur, Type>>,
1662        value_subst: Option<&rustc_hash::FxHashMap<Spur, ConstValue>>,
1663    ) -> RawFnAnalysis {
1664        // ADR-0076 internal collapse: bindings keep their surface
1665        // `Ref(T)` / `MutRef(T)` types end-to-end. Body-analysis sites
1666        // (HM, sema, CFG/codegen) read ref-ness off the type pool
1667        // (`TypeKind::Ref` / `TypeKind::MutRef`) instead of off a
1668        // parallel mode field. Auto-deref happens at the use site.
1669
1670        let mut air = Air::new(return_type);
1671        let mut param_vec: Vec<ParamInfo> = Vec::new();
1672        let mut param_modes: Vec<crate::inst::AirParamMode> = Vec::new();
1673        let mut param_slot_types: Vec<Type> = Vec::new();
1674
1675        // Add parameters to the param vec, tracking ABI slot offsets.
1676        // Each parameter starts at the next available ABI slot.
1677        // For struct parameters, the slot count is the number of fields.
1678        let mut next_abi_slot: u32 = 0;
1679        for (pname, ptype, mode) in params.iter() {
1680            param_vec.push(ParamInfo {
1681                name: *pname,
1682                abi_slot: next_abi_slot,
1683                ty: *ptype,
1684                mode: *mode,
1685            });
1686            // Inout and Borrow parameters are passed by reference.
1687            // Comptime parameters are VALUE params (like `comptime n: i32`), passed by value.
1688            // Normal parameters are passed by value.
1689            let air_mode: crate::inst::AirParamMode = (*mode).into();
1690            let is_by_ref = air_mode.is_by_ref();
1691            let slot_count = if is_by_ref {
1692                // By-ref parameters are always 1 slot (pointer)
1693                1
1694            } else {
1695                self.abi_slot_count(*ptype)
1696            };
1697            for _ in 0..slot_count {
1698                param_modes.push(air_mode);
1699                param_slot_types.push(*ptype);
1700            }
1701            next_abi_slot += slot_count;
1702        }
1703        let num_param_slots = next_abi_slot;
1704
1705        // ======================================================================
1706        // Phase 1-2: Hindley-Milner Type Inference
1707        // ======================================================================
1708        // Run constraint generation and unification to determine types
1709        // for all expressions BEFORE emitting AIR.
1710        let resolved_types = self.run_type_inference(
1711            infer_ctx,
1712            return_type,
1713            params,
1714            body,
1715            type_subst,
1716            value_subst,
1717        )?;
1718
1719        // Create analysis context with resolved types
1720        // If type_subst is provided, initialize comptime_type_vars with the substitutions
1721        // so that type parameters can be resolved during struct initialization.
1722        let mut comptime_type_vars = type_subst.cloned().unwrap_or_default();
1723        // ADR-0076: pervasive `Self`. When analyzing a method/associated-fn
1724        // body with a host type in scope, expose `Self` to the body's name
1725        // resolution machinery so struct literals (`Self { ... }`), enum
1726        // variant paths (`Self::Variant`), and pattern paths
1727        // (`Self::Variant(x)`) all resolve to the host type.
1728        if let Some(host) = self.current_self {
1729            let self_sym = self.interner.get_or_intern("Self");
1730            comptime_type_vars.entry(self_sym).or_insert(host);
1731        }
1732        let comptime_value_vars = value_subst.cloned().unwrap_or_default();
1733        let mut ctx = AnalysisContext {
1734            locals: HashMap::default(),
1735            params: &param_vec,
1736            next_slot: 0,
1737            loop_depth: 0,
1738            forbid_break: None,
1739            checked_depth: 0,
1740            used_locals: HashSet::default(),
1741            return_type,
1742            scope_stack: Vec::new(),
1743            resolved_types: &resolved_types,
1744            moved_vars: HashMap::default(),
1745            warnings: Vec::new(),
1746            local_string_table: HashMap::default(),
1747            local_strings: Vec::new(),
1748            local_bytes: Vec::new(),
1749            comptime_type_vars,
1750            comptime_value_vars,
1751            referenced_functions: HashSet::default(),
1752            referenced_methods: HashSet::default(),
1753            borrow_arg_skip_move: None,
1754            uninit_handles: HashMap::default(),
1755            unroll_arm_bindings: HashMap::default(),
1756        };
1757
1758        // ADR-0082: install the `type_subst` (and `Self`) into
1759        // `comptime_type_overrides` so `resolve_type` calls inside the
1760        // body see the right substitutions for `T`, `Self`, etc. Saved
1761        // and restored around body analysis so the override doesn't
1762        // leak across functions. (Mirrors the comptime-interpreter's
1763        // own use of `comptime_type_overrides` on line ~1778 in
1764        // `comptime.rs`.)
1765        let mut active_overrides: HashMap<Spur, Type> = HashMap::default();
1766        if let Some(subst) = type_subst {
1767            for (k, v) in subst.iter() {
1768                active_overrides.insert(*k, *v);
1769            }
1770        }
1771        if let Some(host) = self.current_self {
1772            let self_sym = self.interner.get_or_intern("Self");
1773            active_overrides.entry(self_sym).or_insert(host);
1774        }
1775        let saved_overrides =
1776            std::mem::replace(&mut self.comptime_type_overrides, active_overrides);
1777
1778        // ======================================================================
1779        // Phase 3: AIR Emission
1780        // ======================================================================
1781        // Analyze the body expression, emitting AIR with resolved types
1782        let body_analysis = self.analyze_inst(&mut air, body, &mut ctx);
1783
1784        // Restore the previous overrides regardless of analyze_inst's outcome.
1785        self.comptime_type_overrides = saved_overrides;
1786
1787        let body_result = body_analysis?;
1788
1789        // Add implicit return only if body doesn't already diverge (e.g., explicit return)
1790        if body_result.ty != Type::NEVER {
1791            air.add_inst(AirInst {
1792                data: AirInstData::Ret(Some(body_result.air_ref)),
1793                ty: return_type,
1794                span: self.rir.get(body).span,
1795            });
1796        }
1797
1798        Ok((
1799            air,
1800            ctx.next_slot,
1801            num_param_slots,
1802            param_modes,
1803            param_slot_types,
1804            ctx.warnings,
1805            ctx.local_strings,
1806            ctx.local_bytes,
1807            ctx.referenced_functions,
1808            ctx.referenced_methods,
1809        ))
1810    }
1811
1812    /// Analyze a specialized function body.
1813    ///
1814    /// This is similar to `analyze_function` but for generic function specialization.
1815    /// The `type_subst` map provides substitutions for type parameters to their
1816    /// concrete types.
1817    ///
1818    /// For example, when specializing `fn identity<T>(x: T) -> T { x }` with `T = i32`,
1819    /// the `params` will be `[(x, i32, Normal)]` and `return_type` will be `i32`.
1820    pub fn analyze_specialized_function(
1821        &mut self,
1822        infer_ctx: &InferenceContext,
1823        return_type: Type,
1824        params: &[(Spur, Type, RirParamMode)],
1825        body: InstRef,
1826        type_subst: &rustc_hash::FxHashMap<Spur, Type>,
1827        value_subst: Option<&rustc_hash::FxHashMap<Spur, ConstValue>>,
1828    ) -> RawFnAnalysis {
1829        // For specialized functions, we need to populate comptime_type_vars with the
1830        // type substitutions so that references to type parameters (like `P { ... }`)
1831        // can be resolved in the function body. The optional `value_subst` carries
1832        // bindings for `comptime n: i32`-style value parameters so the body's
1833        // `comptime if`/`@compile_error` checks see the call's concrete value.
1834        self.analyze_function_internal(
1835            infer_ctx,
1836            return_type,
1837            params,
1838            body,
1839            Some(type_subst),
1840            value_subst,
1841        )
1842    }
1843
1844    /// Analyze a method body with `Self` type resolution.
1845    ///
1846    /// This is used for anonymous struct methods where `Self` should resolve to the
1847    /// struct type. The `self_type` is added to the type substitution map under the
1848    /// symbol "Self", allowing `Self { ... }` struct literals to work correctly.
1849    ///
1850    /// ADR-0082: when the host type is an anonymous struct/enum produced by a
1851    /// parameterized comptime function (e.g. `Vec(I32)`), `outer_type_subst`
1852    /// carries the outer fn's type bindings (`T → I32`) into body analysis so
1853    /// references to `T` inside the method body resolve. Pass `None` for plain
1854    /// (non-parameterized) anonymous structs.
1855    fn analyze_method_body(
1856        &mut self,
1857        infer_ctx: &InferenceContext,
1858        return_type: Type,
1859        params: &[(Spur, Type, RirParamMode)],
1860        body: InstRef,
1861        self_type: Type,
1862        captured_comptime_values: &rustc_hash::FxHashMap<Spur, ConstValue>,
1863        outer_type_subst: Option<&rustc_hash::FxHashMap<Spur, Type>>,
1864    ) -> RawFnAnalysis {
1865        // Start with the outer comptime fn's type bindings (T → I32 etc.)
1866        // and overlay Self. Later we'd merge any method-level subst here too.
1867        let mut type_subst: HashMap<Spur, Type> = outer_type_subst
1868            .map(|s| s.iter().map(|(k, v)| (*k, *v)).collect())
1869            .unwrap_or_default();
1870        let self_sym = self.interner.get_or_intern("Self");
1871        type_subst.insert(self_sym, self_type);
1872
1873        // ADR-0076 follow-up: `Self` in *expression* position (e.g.
1874        // `helper(Self, T, self, value)` passing the host type as a
1875        // comptime argument) is resolved through `resolve_type` →
1876        // `current_self`, not the type-substitution map. Set it for
1877        // the duration of body analysis so the receiver and the body
1878        // see the same `Self`. The eager method analysis path at
1879        // `analyze_function_call` already does this; the
1880        // anon-struct/comptime-evaluator path lands here, which used
1881        // to leave `current_self` unset.
1882        let saved_self = self.current_self.replace(self_type);
1883        let result = self.analyze_function_internal(
1884            infer_ctx,
1885            return_type,
1886            params,
1887            body,
1888            Some(&type_subst),
1889            Some(captured_comptime_values),
1890        );
1891        self.current_self = saved_self;
1892        result
1893    }
1894
1895    /// Run Hindley-Milner type inference on a function body.
1896    ///
1897    /// This is Phases 1-2 of the HM algorithm:
1898    /// 1. Generate constraints by walking the RIR
1899    /// 2. Solve constraints via unification
1900    ///
1901    /// The `infer_ctx` parameter provides pre-computed type information (function
1902    /// signatures, struct/enum types, method signatures) converted to InferType format.
1903    /// This avoids rebuilding these maps for each function, reducing O(n²) to O(n).
1904    ///
1905    /// Returns a map from RIR instruction refs to their resolved concrete types.
1906    fn run_type_inference(
1907        &mut self,
1908        infer_ctx: &InferenceContext,
1909        return_type: Type,
1910        params: &[(Spur, Type, RirParamMode)],
1911        body: InstRef,
1912        type_subst: Option<&HashMap<Spur, Type>>,
1913        value_subst: Option<&HashMap<Spur, ConstValue>>,
1914    ) -> CompileResult<HashMap<InstRef, Type>> {
1915        // Create constraint generator using pre-computed inference context
1916        let mut cgen =
1917            ConstraintGenerator::new(self.rir, self.interner, infer_ctx, &self.type_pool)
1918                .with_type_subst(type_subst);
1919
1920        // Build parameter map for constraint context.
1921        // Convert Type to InferType so arrays are represented structurally.
1922        let mut param_vars: HashMap<Spur, ParamVarInfo> = params
1923            .iter()
1924            .map(|(name, ty, mode)| {
1925                (
1926                    *name,
1927                    ParamVarInfo {
1928                        ty: self.type_to_infer_type(*ty),
1929                        mode: *mode,
1930                    },
1931                )
1932            })
1933            .collect();
1934
1935        // Add comptime value variables as if they were parameters
1936        // This allows constraint generation to see captured comptime values
1937        if let Some(values) = value_subst {
1938            for (name, const_val) in values {
1939                let ty = match const_val {
1940                    ConstValue::Integer(_) => Type::COMPTIME_INT,
1941                    ConstValue::Bool(_) => Type::BOOL,
1942                    ConstValue::Type(t) => *t,
1943                    ConstValue::Unit => Type::UNIT,
1944                    ConstValue::ComptimeStr(_) => Type::COMPTIME_STR,
1945                    ConstValue::Struct(_)
1946                    | ConstValue::Array(_)
1947                    | ConstValue::EnumVariant { .. }
1948                    | ConstValue::EnumData { .. }
1949                    | ConstValue::EnumStruct { .. }
1950                    | ConstValue::BreakSignal
1951                    | ConstValue::ContinueSignal
1952                    | ConstValue::ReturnSignal => {
1953                        unreachable!(
1954                            "control-flow signal or composite value in comptime_value_vars"
1955                        )
1956                    }
1957                };
1958                param_vars.insert(
1959                    *name,
1960                    ParamVarInfo {
1961                        ty: self.type_to_infer_type(ty),
1962                        mode: RirParamMode::Comptime,
1963                    },
1964                );
1965            }
1966        }
1967
1968        // Create constraint context
1969        let mut cgen_ctx = ConstraintContext::new(&param_vars, return_type);
1970
1971        // Phase 1: Generate constraints
1972        let body_info = cgen.generate(body, &mut cgen_ctx);
1973
1974        // The function body's type must match the return type.
1975        // This handles implicit returns like `fn foo() -> i8 { 42 }`.
1976        // For arrays, we need to convert Type to InferType structurally.
1977        // ADR-0076: auto-deref `Ref(T)` / `MutRef(T)` body values so
1978        // implicit returns from a `Ref(T)`-typed binding constrain
1979        // against `T` (sema separately rejects moving out of a borrow).
1980        let body_ty = match &body_info.ty {
1981            crate::inference::InferType::Concrete(t) => match t.kind() {
1982                crate::types::TypeKind::Ref(id) => {
1983                    crate::inference::InferType::Concrete(self.type_pool.ref_def(id))
1984                }
1985                crate::types::TypeKind::MutRef(id) => {
1986                    crate::inference::InferType::Concrete(self.type_pool.mut_ref_def(id))
1987                }
1988                _ => body_info.ty.clone(),
1989            },
1990            _ => body_info.ty.clone(),
1991        };
1992        cgen.add_constraint(Constraint::equal(
1993            body_ty,
1994            self.type_to_infer_type(return_type),
1995            body_info.span,
1996        ));
1997
1998        // Consume the constraint generator to release borrows
1999        let (constraints, int_literal_vars, float_literal_vars, expr_types, type_var_count) =
2000            cgen.into_parts();
2001
2002        // Phase 2: Solve constraints via unification
2003        // Pre-size the substitution for better performance on large functions
2004        let mut unifier = Unifier::with_capacity(type_var_count);
2005        let errors = unifier.solve_constraints(&constraints);
2006
2007        // Convert unification errors to compile errors
2008        // For now, we collect the first error. In the future, we could
2009        // report multiple errors for better diagnostics.
2010        if let Some(err) = errors.first() {
2011            // Map each UnifyResult variant to the appropriate ErrorKind
2012            let error_kind = match &err.kind {
2013                UnifyResult::Ok => unreachable!("UnificationError should never contain Ok"),
2014                UnifyResult::TypeMismatch { expected, found } => ErrorKind::TypeMismatch {
2015                    expected: expected.to_string(),
2016                    found: found.to_string(),
2017                },
2018                UnifyResult::IntLiteralNonInteger { found } => ErrorKind::TypeMismatch {
2019                    expected: "integer type".to_string(),
2020                    found: found.name().to_string(),
2021                },
2022                UnifyResult::OccursCheck { var, ty } => ErrorKind::TypeMismatch {
2023                    expected: "non-recursive type".to_string(),
2024                    found: format!("{var} = {ty} (infinite type)"),
2025                },
2026                UnifyResult::NotSigned { ty } => {
2027                    ErrorKind::CannotNegateUnsigned(ty.name().to_string())
2028                }
2029                UnifyResult::NotInteger { ty } => ErrorKind::TypeMismatch {
2030                    expected: "integer type".to_string(),
2031                    found: ty.name().to_string(),
2032                },
2033                UnifyResult::NotUnsigned { ty } => ErrorKind::TypeMismatch {
2034                    expected: "unsigned integer type".to_string(),
2035                    found: ty.name().to_string(),
2036                },
2037                UnifyResult::NotNumeric { ty } => ErrorKind::TypeMismatch {
2038                    expected: "numeric type".to_string(),
2039                    found: ty.name().to_string(),
2040                },
2041                UnifyResult::ArrayLengthMismatch { expected, found } => {
2042                    ErrorKind::ArrayLengthMismatch {
2043                        expected: *expected,
2044                        found: *found,
2045                    }
2046                }
2047            };
2048
2049            let mut compile_error = CompileError::new(error_kind, err.span);
2050
2051            // Add note for unsigned negation errors
2052            if matches!(err.kind, UnifyResult::NotSigned { .. }) {
2053                compile_error = compile_error.with_note("unsigned values cannot be negated");
2054            }
2055
2056            return Err(compile_error);
2057        }
2058
2059        // Default any unconstrained integer literals to i32 and float literals to f64
2060        unifier.default_int_literal_vars(&int_literal_vars);
2061        unifier.default_float_literal_vars(&float_literal_vars);
2062
2063        // Pre-collect all array types from resolved InferTypes before converting them.
2064        // This ensures all array types are created before the conversion loop, which
2065        // enables parallelization of function analysis (mutation happens here, not in
2066        // infer_type_to_type).
2067        for infer_ty in expr_types.values() {
2068            let resolved = unifier.resolve_infer_type(infer_ty);
2069            self.pre_create_array_types_from_infer_type(&resolved);
2070        }
2071
2072        // Build the resolved types map, converting InferType to Type.
2073        // Since we pre-created all array types above, infer_type_to_type only
2074        // performs lookups (no mutation).
2075        let mut resolved_types = HashMap::default();
2076        for (inst_ref, infer_ty) in &expr_types {
2077            let resolved = unifier.resolve_infer_type(infer_ty);
2078            let concrete_ty = self.infer_type_to_type(&resolved);
2079            resolved_types.insert(*inst_ref, concrete_ty);
2080        }
2081
2082        Ok(resolved_types)
2083    }
2084    /// Analyze an RIR instruction for projection (field access).
2085    ///
2086    /// This is like `analyze_inst` but does NOT mark non-Copy values as moved.
2087    /// Used for field access where we're reading from a struct without consuming it.
2088    /// We still check that the variable hasn't already been moved (fully moved).
2089    /// Field-level move checking is done at the FieldGet level, not here.
2090    pub(crate) fn analyze_inst_for_projection(
2091        &mut self,
2092        air: &mut Air,
2093        inst_ref: InstRef,
2094        ctx: &mut AnalysisContext,
2095    ) -> CompileResult<AnalysisResult> {
2096        let inst = self.rir.get(inst_ref);
2097
2098        // For VarRef, we handle it specially: check for full moves but don't mark as moved
2099        if let InstData::VarRef { name } = &inst.data {
2100            // First check if it's a parameter
2101            if let Some(param_info) = ctx.params.iter().find(|p| p.name == *name) {
2102                let ty = param_info.ty;
2103
2104                // Check if this parameter has been fully moved
2105                // (Partial moves are checked at the FieldGet level)
2106                if let Some(move_state) = ctx.moved_vars.get(name)
2107                    && let Some(moved_span) = move_state.full_move
2108                {
2109                    let name_str = self.interner.resolve(name);
2110                    return Err(CompileError::use_after_move(
2111                        name_str, inst.span, moved_span,
2112                    ));
2113                }
2114
2115                // NOTE: We do NOT mark as moved here - this is a projection
2116
2117                let air_ref = air.add_inst(AirInst {
2118                    data: AirInstData::Param {
2119                        index: param_info.abi_slot,
2120                    },
2121                    ty,
2122                    span: inst.span,
2123                });
2124                return Ok(AnalysisResult::new(air_ref, ty));
2125            }
2126
2127            // Check if it's a local variable
2128            if let Some(local) = ctx.locals.get(name) {
2129                let ty = local.ty;
2130                let slot = local.slot;
2131
2132                // Check if this variable has been fully moved
2133                // (Partial moves are checked at the FieldGet level)
2134                if let Some(move_state) = ctx.moved_vars.get(name)
2135                    && let Some(moved_span) = move_state.full_move
2136                {
2137                    let name_str = self.interner.resolve(name);
2138                    return Err(CompileError::use_after_move(
2139                        name_str, inst.span, moved_span,
2140                    ));
2141                }
2142
2143                // NOTE: We do NOT mark as moved here - this is a projection
2144
2145                // Mark variable as used
2146                ctx.used_locals.insert(*name);
2147
2148                // Load the variable
2149                let air_ref = air.add_inst(AirInst {
2150                    data: AirInstData::Load { slot },
2151                    ty,
2152                    span: inst.span,
2153                });
2154                return Ok(AnalysisResult::new(air_ref, ty));
2155            }
2156
2157            // Check if it's a comptime type variable (e.g., `let P = Point();`)
2158            if let Some(&ty) = ctx.comptime_type_vars.get(name) {
2159                let air_ref = air.add_inst(AirInst {
2160                    data: AirInstData::TypeConst(ty),
2161                    ty: Type::COMPTIME_TYPE,
2162                    span: inst.span,
2163                });
2164                return Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE));
2165            }
2166
2167            // Check if it's a comptime value variable (e.g., captured `comptime N: i32`)
2168            if let Some(const_value) = ctx.comptime_value_vars.get(name) {
2169                match const_value {
2170                    ConstValue::Integer(val) => {
2171                        let ty = Self::get_resolved_type(
2172                            ctx,
2173                            inst_ref,
2174                            inst.span,
2175                            "comptime integer value",
2176                        )?;
2177                        let air_ref = air.add_inst(AirInst {
2178                            data: AirInstData::Const(*val as u64),
2179                            ty,
2180                            span: inst.span,
2181                        });
2182                        return Ok(AnalysisResult::new(air_ref, ty));
2183                    }
2184                    ConstValue::Bool(val) => {
2185                        let air_ref = air.add_inst(AirInst {
2186                            data: AirInstData::Const(*val as u64),
2187                            ty: Type::BOOL,
2188                            span: inst.span,
2189                        });
2190                        return Ok(AnalysisResult::new(air_ref, Type::BOOL));
2191                    }
2192                    ConstValue::Type(ty) => {
2193                        let air_ref = air.add_inst(AirInst {
2194                            data: AirInstData::TypeConst(*ty),
2195                            ty: Type::COMPTIME_TYPE,
2196                            span: inst.span,
2197                        });
2198                        return Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE));
2199                    }
2200                    ConstValue::ComptimeStr(_)
2201                    | ConstValue::Struct(_)
2202                    | ConstValue::Array(_)
2203                    | ConstValue::EnumVariant { .. }
2204                    | ConstValue::EnumData { .. }
2205                    | ConstValue::EnumStruct { .. } => {
2206                        return Err(CompileError::new(
2207                            ErrorKind::ComptimeEvaluationFailed {
2208                                reason: "comptime composite values cannot be used in runtime expressions; use @field to access fields".to_string(),
2209                            },
2210                            inst.span,
2211                        ));
2212                    }
2213                    ConstValue::Unit => {
2214                        return Err(CompileError::new(
2215                            ErrorKind::ComptimeEvaluationFailed {
2216                                reason:
2217                                    "comptime unit values cannot be used in runtime expressions"
2218                                        .to_string(),
2219                            },
2220                            inst.span,
2221                        ));
2222                    }
2223                    ConstValue::BreakSignal
2224                    | ConstValue::ContinueSignal
2225                    | ConstValue::ReturnSignal => {
2226                        unreachable!("control-flow signal in comptime_value_vars")
2227                    }
2228                }
2229            }
2230
2231            // Not found
2232            let name_str = self.interner.resolve(name);
2233            return Err(CompileError::new(
2234                ErrorKind::UndefinedVariable(name_str.to_string()),
2235                inst.span,
2236            ));
2237        }
2238
2239        // For nested field access (e.g., a.b.c), recursively use projection mode
2240        if let InstData::FieldGet { base, field } = &inst.data {
2241            let base_result = self.analyze_inst_for_projection(air, *base, ctx)?;
2242            // ADR-0076: auto-deref through `Ref(T)` / `MutRef(T)` so that
2243            // `r.field` works in projection contexts (e.g., comparison
2244            // operands) the same way it does in expression position.
2245            let base_type = crate::sema::analyze_ops::unwrap_ref_for_place(self, base_result.ty);
2246
2247            let struct_id = match base_type.kind() {
2248                TypeKind::Struct(id) => id,
2249                _ => {
2250                    return Err(CompileError::new(
2251                        ErrorKind::FieldAccessOnNonStruct {
2252                            found: base_type.name().to_string(),
2253                        },
2254                        inst.span,
2255                    ));
2256                }
2257            };
2258
2259            let struct_def = self.type_pool.struct_def(struct_id);
2260            let raw_field_name_str = self.interner.resolve(field).to_string();
2261            // Tuple-root match suffix marker `..end_N`: resolve to the
2262            // concrete tuple index now that we know the tuple's arity
2263            // (ADR-0049 Phase 6).
2264            let field_name_str = if let Some(rest) = raw_field_name_str.strip_prefix("..end_") {
2265                match rest.parse::<usize>() {
2266                    Ok(from_end) if from_end < struct_def.fields.len() => {
2267                        let idx = struct_def.fields.len() - 1 - from_end;
2268                        idx.to_string()
2269                    }
2270                    _ => raw_field_name_str.clone(),
2271                }
2272            } else {
2273                raw_field_name_str.clone()
2274            };
2275
2276            let (field_index, struct_field) =
2277                struct_def.find_field(&field_name_str).ok_or_compile_error(
2278                    ErrorKind::UnknownField {
2279                        struct_name: struct_def.name.clone(),
2280                        field_name: field_name_str.clone(),
2281                    },
2282                    inst.span,
2283                )?;
2284
2285            // ADR-0073: unified visibility check.
2286            self.check_field_visibility(&struct_def, struct_field, inst.span)?;
2287
2288            let field_type = struct_field.ty;
2289
2290            let air_ref = air.add_inst(AirInst {
2291                data: AirInstData::FieldGet {
2292                    base: base_result.air_ref,
2293                    struct_id,
2294                    field_index: field_index as u32,
2295                },
2296                ty: field_type,
2297                span: inst.span,
2298            });
2299            return Ok(AnalysisResult::new(air_ref, field_type));
2300        }
2301
2302        // For index access in projection mode (e.g., `arr[i].field`), we allow the
2303        // indexing without checking if the element type is Copy. This enables
2304        // accessing Copy fields of non-Copy array elements.
2305        if let InstData::IndexGet { base, index } = &inst.data {
2306            // Recursively analyze the base in projection mode
2307            let base_result = self.analyze_inst_for_projection(air, *base, ctx)?;
2308            // ADR-0076: auto-deref through `Ref(T)` / `MutRef(T)` so that
2309            // `arr[i]` works in projection contexts (e.g., comparison
2310            // operands) when `arr` is a reference parameter. The base's
2311            // air_ref still points at the param load (the pointer), and
2312            // codegen treats by-ref params as the base pointer for GEP.
2313            let base_type = crate::sema::analyze_ops::unwrap_ref_for_place(self, base_result.ty);
2314
2315            let array_type_id = match base_type.kind() {
2316                TypeKind::Array(id) => id,
2317                _ => {
2318                    return Err(CompileError::new(
2319                        ErrorKind::IndexOnNonArray {
2320                            found: base_type.name().to_string(),
2321                        },
2322                        inst.span,
2323                    ));
2324                }
2325            };
2326
2327            let (element_type, length) = self.type_pool.array_def(array_type_id);
2328
2329            // Index must be `usize` (ADR-0054).
2330            let index_result = self.analyze_inst(air, *index, ctx)?;
2331            if index_result.ty != Type::USIZE && !index_result.ty.is_error() {
2332                return Err(CompileError::type_mismatch(
2333                    "usize".to_string(),
2334                    index_result.ty.name().to_string(),
2335                    self.rir.get(*index).span,
2336                ));
2337            }
2338
2339            let array_length = length;
2340
2341            // Compile-time bounds check for constant indices
2342            if let Some(const_index) = self.try_get_const_index(*index)
2343                && (const_index < 0 || const_index as u64 >= array_length)
2344            {
2345                return Err(CompileError::new(
2346                    ErrorKind::IndexOutOfBounds {
2347                        index: const_index,
2348                        length: array_length,
2349                    },
2350                    self.rir.get(*index).span,
2351                ));
2352            }
2353
2354            // NOTE: We do NOT check if element_type is Copy here.
2355            // In projection mode, we allow accessing elements for further projection
2356            // (e.g., arr[i].field where field is Copy).
2357
2358            let air_ref = air.add_inst(AirInst {
2359                data: AirInstData::IndexGet {
2360                    base: base_result.air_ref,
2361                    array_type: base_type,
2362                    index: index_result.air_ref,
2363                },
2364                ty: element_type,
2365                span: inst.span,
2366            });
2367            return Ok(AnalysisResult::new(air_ref, element_type));
2368        }
2369
2370        // For other expressions, use the normal analyze_inst
2371        // (they will trigger move semantics as expected)
2372        self.analyze_inst(air, inst_ref, ctx)
2373    }
2374
2375    /// Look up the resolved type for an instruction from HM inference.
2376    ///
2377    /// Returns an `InternalError` if the type was not resolved. This should
2378    /// never happen in normal operation, but provides a better error message
2379    /// than a panic if there's a bug in type inference.
2380    pub(crate) fn get_resolved_type(
2381        ctx: &AnalysisContext,
2382        inst_ref: InstRef,
2383        span: Span,
2384        context: &str,
2385    ) -> CompileResult<Type> {
2386        ctx.resolved_types.get(&inst_ref).copied().ok_or_else(|| {
2387            CompileError::new(
2388                ErrorKind::InternalError(format!(
2389                    "type inference did not resolve type for {} (instruction {:?})",
2390                    context, inst_ref
2391                )),
2392                span,
2393            )
2394        })
2395    }
2396
2397    /// Analyze an RIR instruction, producing AIR instructions.
2398    ///
2399    /// Types are determined by Hindley-Milner inference (stored in `resolved_types`).
2400    /// Returns both the AIR reference and the synthesized type.
2401    /// Analyze a single RIR instruction and produce the corresponding AIR instruction.
2402    ///
2403    /// This method dispatches to category-specific methods in `analyze_ops.rs` for
2404    /// maintainability. Each category handles related instruction types together.
2405    ///
2406    /// # Categories
2407    ///
2408    /// - **Literals**: IntConst, BoolConst, StringConst, UnitConst
2409    /// - **Binary arithmetic**: Add, Sub, Mul, Div, Mod, BitAnd, BitOr, BitXor, Shl, Shr
2410    /// - **Comparison**: Eq, Ne, Lt, Gt, Le, Ge
2411    /// - **Logical**: And, Or
2412    /// - **Unary**: Neg, Not, BitNot
2413    /// - **Control flow**: Branch, Loop, InfiniteLoop, Match, Break, Continue, Ret, Block
2414    /// - **Variables**: Alloc, VarRef, ParamRef, Assign
2415    /// - **Structs**: StructDecl, StructInit, FieldGet, FieldSet
2416    /// - **Arrays**: ArrayInit, IndexGet, IndexSet
2417    /// - **Enums**: EnumDecl, EnumVariant
2418    /// - **Calls**: Call, MethodCall, AssocFnCall
2419    /// - **Intrinsics**: Intrinsic, TypeIntrinsic
2420    /// - **Declarations**: FnDecl
2421    pub(crate) fn analyze_inst(
2422        &mut self,
2423        air: &mut Air,
2424        inst_ref: InstRef,
2425        ctx: &mut AnalysisContext,
2426    ) -> CompileResult<AnalysisResult> {
2427        let inst = self.rir.get(inst_ref);
2428
2429        match &inst.data {
2430            // Literals
2431            InstData::IntConst(_)
2432            | InstData::FloatConst(_)
2433            | InstData::BoolConst(_)
2434            | InstData::CharConst(_)
2435            | InstData::StringConst(_)
2436            | InstData::UnitConst => self.analyze_literal(air, inst_ref, ctx),
2437
2438            InstData::Bin { op, lhs, rhs } => match op {
2439                BinOp::Add
2440                | BinOp::Sub
2441                | BinOp::Mul
2442                | BinOp::Div
2443                | BinOp::Mod
2444                | BinOp::BitAnd
2445                | BinOp::BitOr
2446                | BinOp::BitXor
2447                | BinOp::Shl
2448                | BinOp::Shr => self.analyze_binary_arith(air, *lhs, *rhs, *op, inst.span, ctx),
2449                BinOp::Eq | BinOp::Ne => {
2450                    self.analyze_comparison(air, (*lhs, *rhs), true, *op, inst.span, ctx)
2451                }
2452                BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge => {
2453                    self.analyze_comparison(air, (*lhs, *rhs), false, *op, inst.span, ctx)
2454                }
2455                BinOp::And | BinOp::Or => self.analyze_logical_op(air, inst_ref, ctx),
2456            },
2457
2458            InstData::Unary { .. } => self.analyze_unary_op(air, inst_ref, ctx),
2459
2460            // Reference construction (ADR-0062): `&x` / `&mut x`.
2461            InstData::MakeRef { .. } => self.analyze_make_ref(air, inst_ref, ctx),
2462
2463            // ADR-0064: slice construction by borrow over a range subscript
2464            // (`&arr[range]` / `&mut arr[range]`).
2465            InstData::MakeSlice { .. } => self.analyze_make_slice(air, inst_ref, ctx),
2466
2467            // ADR-0064: range subscript without `&` / `&mut`.
2468            InstData::BareRangeSubscript => Err(CompileError::new(
2469                ErrorKind::ParseError(
2470                    "range subscripts produce slices and must be borrowed with `&` or `&mut`"
2471                        .to_string(),
2472                ),
2473                inst.span,
2474            )),
2475
2476            // Control flow
2477            InstData::Branch { .. }
2478            | InstData::Loop { .. }
2479            | InstData::For { .. }
2480            | InstData::InfiniteLoop { .. }
2481            | InstData::Match { .. }
2482            | InstData::Break
2483            | InstData::Continue
2484            | InstData::Ret(_)
2485            | InstData::Block { .. } => self.analyze_control_flow(air, inst_ref, ctx),
2486
2487            // Variable operations
2488            InstData::Alloc { .. }
2489            | InstData::StructDestructure { .. }
2490            | InstData::VarRef { .. }
2491            | InstData::ParamRef { .. }
2492            | InstData::Assign { .. } => self.analyze_variable_ops(air, inst_ref, ctx),
2493
2494            // Struct operations
2495            InstData::StructDecl { .. }
2496            | InstData::StructInit { .. }
2497            | InstData::FieldGet { .. }
2498            | InstData::FieldSet { .. } => self.analyze_struct_ops(air, inst_ref, ctx),
2499
2500            // Array operations
2501            InstData::ArrayInit { .. } | InstData::IndexGet { .. } | InstData::IndexSet { .. } => {
2502                self.analyze_array_ops(air, inst_ref, ctx)
2503            }
2504
2505            // Enum operations
2506            InstData::EnumDecl { .. }
2507            | InstData::EnumVariant { .. }
2508            | InstData::EnumStructVariant { .. } => self.analyze_enum_ops(air, inst_ref, ctx),
2509
2510            // Call operations
2511            InstData::Call { .. } | InstData::MethodCall { .. } | InstData::AssocFnCall { .. } => {
2512                self.analyze_call_ops(air, inst_ref, ctx)
2513            }
2514
2515            // Intrinsic operations
2516            InstData::Intrinsic { .. }
2517            | InstData::TypeIntrinsic { .. }
2518            | InstData::TypeInterfaceIntrinsic { .. } => {
2519                self.analyze_intrinsic_ops(air, inst_ref, ctx)
2520            }
2521
2522            // Declaration no-ops (produce Unit in expression context)
2523            InstData::FnDecl { .. }
2524            | InstData::ConstDecl { .. }
2525            | InstData::InterfaceDecl { .. }
2526            | InstData::InterfaceMethodSig { .. }
2527            | InstData::DeriveDecl { .. } => self.analyze_decl_noop(air, inst_ref, ctx),
2528
2529            // Comptime block expression
2530            InstData::Comptime { expr } => {
2531                let span = inst.span;
2532                let expr = *expr;
2533                // Use the stateful comptime interpreter (Phase 1a).
2534                // This supports mutable let bindings, if/else, and blocks
2535                // in addition to pure arithmetic expressions.
2536                match self.evaluate_comptime_block(expr, ctx, span)? {
2537                    ConstValue::Integer(value) => {
2538                        let ty =
2539                            Self::get_resolved_type(ctx, inst_ref, span, "comptime block")?;
2540                        if value < 0 {
2541                            return Err(CompileError::new(
2542                                ErrorKind::ComptimeEvaluationFailed {
2543                                    reason: "negative values not yet supported in comptime"
2544                                        .to_string(),
2545                                },
2546                                span,
2547                            ));
2548                        }
2549                        let unsigned_value = value as u64;
2550                        if !ty.literal_fits(unsigned_value) {
2551                            return Err(CompileError::new(
2552                                ErrorKind::LiteralOutOfRange {
2553                                    value: unsigned_value,
2554                                    ty: ty.name().to_string(),
2555                                },
2556                                span,
2557                            ));
2558                        }
2559                        let air_ref = air.add_inst(AirInst {
2560                            data: AirInstData::Const(unsigned_value),
2561                            ty,
2562                            span,
2563                        });
2564                        Ok(AnalysisResult::new(air_ref, ty))
2565                    }
2566                    ConstValue::Bool(value) => {
2567                        let ty = Type::BOOL;
2568                        let air_ref = air.add_inst(AirInst {
2569                            data: AirInstData::BoolConst(value),
2570                            ty,
2571                            span,
2572                        });
2573                        Ok(AnalysisResult::new(air_ref, ty))
2574                    }
2575                    ConstValue::Type(_) => Err(CompileError::new(
2576                        ErrorKind::ComptimeEvaluationFailed {
2577                            reason: "type values cannot exist at runtime".to_string(),
2578                        },
2579                        span,
2580                    )),
2581                    ConstValue::ComptimeStr(str_idx) => {
2582                        // Materialize comptime string as a runtime String constant.
2583                        let content =
2584                            self.resolve_comptime_str(str_idx, span)?.to_string();
2585                        let ty = self.builtin_string_type();
2586                        let local_string_id = ctx.add_local_string(content);
2587                        let air_ref = air.add_inst(AirInst {
2588                            data: AirInstData::StringConst(local_string_id),
2589                            ty,
2590                            span,
2591                        });
2592                        Ok(AnalysisResult::new(air_ref, ty))
2593                    }
2594                    ConstValue::Unit => {
2595                        let air_ref = air.add_inst(AirInst {
2596                            data: AirInstData::UnitConst,
2597                            ty: Type::UNIT,
2598                            span,
2599                        });
2600                        Ok(AnalysisResult::new(air_ref, Type::UNIT))
2601                    }
2602                    // Composite comptime values (structs, arrays, enums) cannot be placed at
2603                    // runtime directly. The user must access individual fields/elements.
2604                    ConstValue::Struct(_)
2605                    | ConstValue::Array(_)
2606                    | ConstValue::EnumVariant { .. }
2607                    | ConstValue::EnumData { .. }
2608                    | ConstValue::EnumStruct { .. } => {
2609                        Err(CompileError::new(
2610                            ErrorKind::ComptimeEvaluationFailed {
2611                                reason: "comptime composite values cannot be used at runtime; access individual fields or elements instead".into(),
2612                            },
2613                            span,
2614                        ))
2615                    }
2616                    // These signals are consumed by loop/call handlers inside evaluate_comptime_block.
2617                    // If they escape here, it means break/continue outside a loop, or return outside
2618                    // a function, which evaluate_comptime_block converts to an error before returning.
2619                    ConstValue::BreakSignal
2620                    | ConstValue::ContinueSignal
2621                    | ConstValue::ReturnSignal => {
2622                        unreachable!("control-flow signal escaped evaluate_comptime_block")
2623                    }
2624                }
2625            }
2626
2627            // Comptime unroll for: evaluate iterable at comptime, unroll body N times
2628            InstData::ComptimeUnrollFor {
2629                binding,
2630                iterable,
2631                body,
2632            } => {
2633                let span = inst.span;
2634                let binding = *binding;
2635                let iterable = *iterable;
2636                let body = *body;
2637
2638                // Step 1: Evaluate the iterable expression at comptime.
2639                // ADR-0079 Phase 3: don't clear the heap — a nested
2640                // `comptime_unroll for` (e.g. one that iterates
2641                // `v.fields` inside an outer arm-template iteration
2642                // over `variants`) would otherwise invalidate the
2643                // outer loop's comptime binding (a `Struct(heap_idx)`
2644                // pointing at the now-cleared heap). Use the
2645                // heap-preserving evaluator instead.
2646                let iterable_val = {
2647                    let prev_steps = self.comptime_steps_used;
2648                    self.comptime_steps_used = 0;
2649                    let mut locals = ctx.comptime_value_vars.clone();
2650                    let v = self.evaluate_comptime_inst(iterable, &mut locals, ctx, span)?;
2651                    self.comptime_steps_used = prev_steps;
2652                    v
2653                };
2654
2655                // Step 2: Extract array elements from the comptime heap.
2656                // We clone the elements AND preserve the heap so that composite
2657                // ConstValues (e.g., Struct(heap_idx)) remain valid during iteration.
2658                let elements = match iterable_val {
2659                    ConstValue::Array(heap_idx) => match &self.comptime_heap[heap_idx as usize] {
2660                        ComptimeHeapItem::Array(elems) => elems.clone(),
2661                        _ => {
2662                            return Err(CompileError::new(
2663                                ErrorKind::ComptimeEvaluationFailed {
2664                                    reason: "comptime_unroll iterable is not an array".to_string(),
2665                                },
2666                                span,
2667                            ));
2668                        }
2669                    },
2670                    _ => {
2671                        return Err(CompileError::new(
2672                            ErrorKind::ComptimeEvaluationFailed {
2673                                reason: "comptime_unroll for requires an array iterable"
2674                                    .to_string(),
2675                            },
2676                            span,
2677                        ));
2678                    }
2679                };
2680
2681                // Step 3: For each element, bind the loop variable and analyze the body.
2682                // The loop variable is stored in comptime_value_vars so that @field
2683                // and other comptime expressions in the body can access it.
2684                let mut body_air_refs = Vec::with_capacity(elements.len());
2685                for element in &elements {
2686                    // Insert the loop variable as a comptime value
2687                    let prev_value = ctx.comptime_value_vars.insert(binding, *element);
2688
2689                    // Analyze the body block
2690                    let body_result = self.analyze_inst(air, body, ctx)?;
2691                    body_air_refs.push(body_result.air_ref);
2692
2693                    // Restore the previous value (or remove if there was none)
2694                    match prev_value {
2695                        Some(v) => {
2696                            ctx.comptime_value_vars.insert(binding, v);
2697                        }
2698                        None => {
2699                            ctx.comptime_value_vars.remove(&binding);
2700                        }
2701                    }
2702                }
2703
2704                // Step 4: Emit all unrolled body instructions.
2705                if body_air_refs.is_empty() {
2706                    // Empty loop — emit unit constant
2707                    let air_ref = air.add_inst(AirInst {
2708                        data: AirInstData::UnitConst,
2709                        ty: Type::UNIT,
2710                        span,
2711                    });
2712                    Ok(AnalysisResult::new(air_ref, Type::UNIT))
2713                } else if body_air_refs.len() == 1 {
2714                    Ok(AnalysisResult::new(body_air_refs[0], Type::UNIT))
2715                } else {
2716                    // Emit a block containing all unrolled body results.
2717                    // The last body is the block's "value"; the rest are statements.
2718                    let last = body_air_refs.pop().unwrap();
2719                    let stmts: Vec<u32> = body_air_refs.iter().map(|r| r.as_u32()).collect();
2720                    let stmts_start = air.add_extra(&stmts);
2721                    let block_ref = air.add_inst(AirInst {
2722                        data: AirInstData::Block {
2723                            stmts_start,
2724                            stmts_len: stmts.len() as u32,
2725                            value: last,
2726                        },
2727                        ty: Type::UNIT,
2728                        span,
2729                    });
2730                    Ok(AnalysisResult::new(block_ref, Type::UNIT))
2731                }
2732            }
2733
2734            // Type constant: a type used as a value (e.g., `i32` in `identity(i32, 42)`)
2735            InstData::TypeConst { type_name } => {
2736                // Resolve the type name to a concrete type
2737                let ty = self.resolve_type(*type_name, inst.span)?;
2738                let air_ref = air.add_inst(AirInst {
2739                    data: AirInstData::TypeConst(ty),
2740                    ty: Type::COMPTIME_TYPE,
2741                    span: inst.span,
2742                });
2743                Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE))
2744            }
2745
2746            // Anonymous struct type: a struct type constructed at comptime
2747            // (e.g., `struct { first: T, second: T, fn get(self) -> T { ... } }` in a comptime function)
2748            InstData::AnonStructType {
2749                fields_start,
2750                fields_len,
2751                methods_start,
2752                methods_len,
2753                directives_start,
2754                directives_len,
2755            } => {
2756                // Get the field declarations from the RIR
2757                let field_decls = self.rir.get_field_decls(*fields_start, *fields_len);
2758
2759                // Empty structs are not allowed (unless they have methods)
2760                if field_decls.is_empty() && *methods_len == 0 {
2761                    return Err(CompileError::new(ErrorKind::EmptyStruct, inst.span));
2762                }
2763
2764                // Methods are fully supported (anon_struct_methods stabilized)
2765
2766                // Resolve each field type and build the struct fields
2767                let mut struct_fields = Vec::with_capacity(field_decls.len());
2768                for (name_sym, type_sym) in field_decls {
2769                    let name_str = self.interner.resolve(&name_sym).to_string();
2770                    let field_ty = self.resolve_type(type_sym, inst.span)?;
2771                    struct_fields.push(StructField {
2772                        name: name_str,
2773                        ty: field_ty,
2774
2775                        is_pub: true,
2776                    });
2777                }
2778
2779                // Extract method signatures for structural equality comparison
2780                // (uses type symbols, not resolved Types, so Self matches Self)
2781                let method_sigs = self.extract_anon_method_sigs(*methods_start, *methods_len);
2782
2783                // Check if an equivalent anonymous struct already exists (structural equality)
2784                // This now compares fields, method signatures, AND captured comptime values
2785                let (struct_ty, is_new) = self.find_or_create_anon_struct(
2786                    &struct_fields,
2787                    &method_sigs,
2788                    &HashMap::default(),
2789                );
2790                // ADR-0083 / ADR-0084: apply `@mark(...)` directives on
2791                // freshly-built anonymous struct expressions, mirroring
2792                // the comptime-evaluator path. Reached for
2793                // anon-struct expressions analyzed outside a comptime
2794                // type-constructor body.
2795                if is_new && *directives_len > 0 {
2796                    let struct_id = struct_ty
2797                        .as_struct()
2798                        .expect("anon struct must have StructId");
2799                    self.apply_anon_struct_marks(
2800                        struct_id,
2801                        *directives_start,
2802                        *directives_len,
2803                        inst.span,
2804                    )?;
2805                }
2806
2807                // DON'T register methods here - they should be registered during const evaluation
2808                // (either try_evaluate_const for non-comptime, or try_evaluate_const_with_subst for comptime).
2809                // If we register here, we create a struct without captured comptime values, which is incorrect.
2810                //
2811                // if is_new && *methods_len > 0 {
2812                //     let struct_id = struct_ty
2813                //         .as_struct()
2814                //         .expect("anon struct should have StructId");
2815                //     self.register_anon_struct_methods(
2816                //         struct_id,
2817                //         struct_ty,
2818                //         *methods_start,
2819                //         *methods_len,
2820                //         inst.span,
2821                //     )?;
2822                // }
2823
2824                let air_ref = air.add_inst(AirInst {
2825                    data: AirInstData::TypeConst(struct_ty),
2826                    ty: Type::COMPTIME_TYPE,
2827                    span: inst.span,
2828                });
2829                Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE))
2830            }
2831
2832            // Anonymous interface type (ADR-0057): an interface type
2833            // constructed at comptime, e.g.
2834            // `interface { fn size(self) -> T }` inside a `fn ... -> type`
2835            // body. Resolves the method-signature types under the current
2836            // substitution map (none here — see the comptime evaluator path
2837            // for substituted resolution), then either dedupes against an
2838            // existing structurally-equal interface or registers a new
2839            // `InterfaceDef` and returns its id as a `Type::COMPTIME_TYPE`
2840            // value.
2841            InstData::AnonInterfaceType {
2842                methods_start,
2843                methods_len,
2844            } => {
2845                let req = self.build_anon_interface_def(
2846                    *methods_start,
2847                    *methods_len,
2848                    inst.span,
2849                    &rustc_hash::FxHashMap::default(),
2850                )?;
2851                let iface_id = self.find_or_create_anon_interface(req);
2852                let iface_ty = Type::new_interface(iface_id);
2853                let air_ref = air.add_inst(AirInst {
2854                    data: AirInstData::TypeConst(iface_ty),
2855                    ty: Type::COMPTIME_TYPE,
2856                    span: inst.span,
2857                });
2858                Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE))
2859            }
2860
2861            // Anonymous enum type: an enum type constructed at comptime
2862            // (e.g., `enum { Some(T), None, fn method(self) -> bool { ... } }` in a comptime function)
2863            InstData::AnonEnumType {
2864                variants_start,
2865                variants_len,
2866                methods_start,
2867                methods_len,
2868                directives_start,
2869                directives_len,
2870            } => {
2871                // Get the variant declarations from the RIR
2872                let variant_decls = self
2873                    .rir
2874                    .get_enum_variant_decls(*variants_start, *variants_len);
2875
2876                // Empty enums are not allowed
2877                if variant_decls.is_empty() {
2878                    return Err(CompileError::new(ErrorKind::EmptyAnonEnum, inst.span));
2879                }
2880
2881                // Resolve each variant and build the enum variants
2882                let mut enum_variants = Vec::with_capacity(variant_decls.len());
2883                for (name_sym, field_type_syms, field_name_syms) in &variant_decls {
2884                    let name_str = self.interner.resolve(name_sym).to_string();
2885                    let mut fields = Vec::with_capacity(field_type_syms.len());
2886                    for ty_sym in field_type_syms {
2887                        let field_ty = self.resolve_type(*ty_sym, inst.span)?;
2888                        fields.push(field_ty);
2889                    }
2890                    let field_names: Vec<String> = field_name_syms
2891                        .iter()
2892                        .map(|s| self.interner.resolve(s).to_string())
2893                        .collect();
2894                    enum_variants.push(EnumVariantDef {
2895                        name: name_str,
2896                        fields,
2897                        field_names,
2898                    });
2899                }
2900
2901                // Check for duplicate method names
2902                if *methods_len > 0 {
2903                    let method_refs = self.rir.get_inst_refs(*methods_start, *methods_len);
2904                    let mut seen_method_names: rustc_hash::FxHashSet<Spur> =
2905                        rustc_hash::FxHashSet::default();
2906                    for mref in method_refs {
2907                        let minst = self.rir.get(mref);
2908                        if let InstData::FnDecl {
2909                            name: method_name, ..
2910                        } = &minst.data
2911                            && !seen_method_names.insert(*method_name)
2912                        {
2913                            let method_name_str = self.interner.resolve(method_name).to_string();
2914                            return Err(CompileError::new(
2915                                ErrorKind::DuplicateMethod {
2916                                    type_name: "anonymous enum".to_string(),
2917                                    method_name: method_name_str,
2918                                },
2919                                minst.span,
2920                            ));
2921                        }
2922                    }
2923                }
2924
2925                // Extract method signatures for structural equality comparison
2926                let method_sigs = self.extract_anon_method_sigs(*methods_start, *methods_len);
2927
2928                // Check if an equivalent anonymous enum already exists (structural equality)
2929                let (enum_ty, is_new) = self.find_or_create_anon_enum(
2930                    &enum_variants,
2931                    &method_sigs,
2932                    &HashMap::default(),
2933                );
2934                // ADR-0083 / ADR-0084: apply `@mark(...)` on the
2935                // freshly-built anon enum, mirroring the comptime path.
2936                if is_new
2937                    && *directives_len > 0
2938                    && let TypeKind::Enum(enum_id) = enum_ty.kind()
2939                {
2940                    self.apply_anon_enum_marks(
2941                        enum_id,
2942                        *directives_start,
2943                        *directives_len,
2944                        inst.span,
2945                    )?;
2946                }
2947
2948                let air_ref = air.add_inst(AirInst {
2949                    data: AirInstData::TypeConst(enum_ty),
2950                    ty: Type::COMPTIME_TYPE,
2951                    span: inst.span,
2952                });
2953                Ok(AnalysisResult::new(air_ref, Type::COMPTIME_TYPE))
2954            }
2955
2956            // Checked block: enter checked context, evaluate inner expression, then exit
2957            InstData::Checked { expr } => {
2958                ctx.checked_depth += 1;
2959                let result = self.analyze_inst(air, *expr, ctx);
2960                ctx.checked_depth -= 1;
2961                result
2962            }
2963
2964            // Tuple literal (ADR-0048): lower to an anon struct with fields "0", "1", ...
2965            InstData::TupleInit {
2966                elems_start,
2967                elems_len,
2968            } => self.analyze_tuple_init(air, *elems_start, *elems_len, inst.span, ctx),
2969
2970            // Anonymous function value (ADR-0055): synthesize a fresh anon
2971            // struct with zero fields and one `__call` method, then emit an
2972            // empty StructInit against it. Phase 2 uses the normal structural-
2973            // dedup path; Phase 3 makes each lambda site unique.
2974            InstData::AnonFnValue { method } => {
2975                self.analyze_anon_fn_value(air, *method, inst.span, ctx)
2976            }
2977        }
2978    }
2979
2980    // ========================================================================
2981    // Implementation methods for complex operations
2982    // These are called by the category methods in analyze_ops.rs
2983    // ========================================================================
2984
2985    /// Implementation for FieldSet - handles both local and parameter field assignment.
2986    pub(crate) fn analyze_field_set_impl(
2987        &mut self,
2988        air: &mut Air,
2989        base: InstRef,
2990        field: Spur,
2991        value: InstRef,
2992        span: Span,
2993        ctx: &mut AnalysisContext,
2994    ) -> CompileResult<AnalysisResult> {
2995        use crate::sema::analyze_ops::ProjectionInfo;
2996
2997        // Try to trace the base to a place
2998        if let Some(mut trace) = self.try_trace_place(base, air, ctx)? {
2999            // Check if the root variable was fully moved
3000            if let Some(state) = ctx.moved_vars.get(&trace.root_var)
3001                && let Some(moved_span) = state.full_move
3002            {
3003                let root_name = self.interner.resolve(&trace.root_var);
3004                return Err(CompileError::use_after_move(root_name, span, moved_span));
3005            }
3006
3007            // Check mutability
3008            let root_name = self.interner.resolve(&trace.root_var).to_string();
3009            if !trace.is_root_mutable {
3010                // Check if this is a borrow parameter - special error message
3011                if trace.is_borrow_param {
3012                    return Err(CompileError::new(
3013                        ErrorKind::MutateBorrowedValue {
3014                            variable: root_name,
3015                        },
3016                        span,
3017                    ));
3018                }
3019
3020                let root_type = trace.base_type;
3021                // Provide more specific error based on whether it's a param or local
3022                match trace.base {
3023                    AirPlaceBase::Param(_) => {
3024                        return Err(CompileError::new(
3025                            ErrorKind::AssignToImmutable(root_name.clone()),
3026                            span,
3027                        )
3028                        .with_help(format!(
3029                            "consider making parameter `{}` inout: `inout {}: {}`",
3030                            root_name,
3031                            root_name,
3032                            root_type.name()
3033                        )));
3034                    }
3035                    AirPlaceBase::Local(_) => {
3036                        return Err(CompileError::new(
3037                            ErrorKind::AssignToImmutable(root_name),
3038                            span,
3039                        ));
3040                    }
3041                }
3042            }
3043
3044            // Add the final field projection
3045            let base_type = trace.result_type();
3046            let struct_id = match base_type.as_struct() {
3047                Some(id) => id,
3048                None => {
3049                    return Err(CompileError::new(
3050                        ErrorKind::FieldAccessOnNonStruct {
3051                            found: base_type.name().to_string(),
3052                        },
3053                        span,
3054                    ));
3055                }
3056            };
3057
3058            let struct_def = self.type_pool.struct_def(struct_id);
3059            let field_name_str = self.interner.resolve(&field).to_string();
3060
3061            let (field_index, struct_field) =
3062                struct_def.find_field(&field_name_str).ok_or_compile_error(
3063                    ErrorKind::UnknownField {
3064                        struct_name: struct_def.name.clone(),
3065                        field_name: field_name_str.clone(),
3066                    },
3067                    span,
3068                )?;
3069
3070            // ADR-0073: unified visibility check on field write.
3071            self.check_field_visibility(&struct_def, struct_field, span)?;
3072
3073            let field_type = struct_field.ty;
3074
3075            // Add the field projection to the trace
3076            trace.projections.push(ProjectionInfo {
3077                proj: AirProjection::Field {
3078                    struct_id,
3079                    field_index: field_index as u32,
3080                },
3081                result_type: field_type,
3082                field_name: Some(field),
3083            });
3084
3085            // Analyze the value
3086            let value_result = self.analyze_inst(air, value, ctx)?;
3087
3088            // Emit PlaceWrite instruction
3089            let place_ref = Self::build_place_ref(air, &trace);
3090            let air_ref = air.add_inst(AirInst {
3091                data: AirInstData::PlaceWrite {
3092                    place: place_ref,
3093                    value: value_result.air_ref,
3094                },
3095                ty: Type::UNIT,
3096                span,
3097            });
3098            return Ok(AnalysisResult::new(air_ref, Type::UNIT));
3099        }
3100
3101        // Fallback: base is not a place (e.g., function call result)
3102        // This shouldn't normally happen for valid assignment targets
3103        Err(CompileError::new(ErrorKind::InvalidAssignmentTarget, span))
3104    }
3105
3106    /// Implementation for IndexSet - handles both local and parameter array index assignment.
3107    pub(crate) fn analyze_index_set_impl(
3108        &mut self,
3109        air: &mut Air,
3110        base: InstRef,
3111        index: InstRef,
3112        value: InstRef,
3113        span: Span,
3114        ctx: &mut AnalysisContext,
3115    ) -> CompileResult<AnalysisResult> {
3116        use crate::sema::analyze_ops::ProjectionInfo;
3117
3118        // Try to trace the base to a place
3119        if let Some(mut trace) = self.try_trace_place(base, air, ctx)? {
3120            // Check if the root variable was fully moved
3121            if let Some(state) = ctx.moved_vars.get(&trace.root_var)
3122                && let Some(moved_span) = state.full_move
3123            {
3124                let root_name = self.interner.resolve(&trace.root_var);
3125                return Err(CompileError::use_after_move(root_name, span, moved_span));
3126            }
3127
3128            // Check mutability
3129            let root_name = self.interner.resolve(&trace.root_var).to_string();
3130            if !trace.is_root_mutable {
3131                // Check if this is a borrow parameter - special error message
3132                if trace.is_borrow_param {
3133                    return Err(CompileError::new(
3134                        ErrorKind::MutateBorrowedValue {
3135                            variable: root_name,
3136                        },
3137                        span,
3138                    ));
3139                }
3140
3141                let root_type = trace.base_type;
3142                match trace.base {
3143                    AirPlaceBase::Param(_) => {
3144                        return Err(CompileError::new(
3145                            ErrorKind::AssignToImmutable(root_name.clone()),
3146                            span,
3147                        )
3148                        .with_help(format!(
3149                            "consider making parameter `{}` inout: `inout {}: {}`",
3150                            root_name,
3151                            root_name,
3152                            root_type.name()
3153                        )));
3154                    }
3155                    AirPlaceBase::Local(_) => {
3156                        return Err(CompileError::new(
3157                            ErrorKind::AssignToImmutable(root_name),
3158                            span,
3159                        ));
3160                    }
3161                }
3162            }
3163
3164            // Get array type info from the trace
3165            let base_type = trace.result_type();
3166            let (_array_type_id, elem_type, array_len) = match base_type.as_array() {
3167                Some(id) => {
3168                    let (elem, len) = self.type_pool.array_def(id);
3169                    (id, elem, len)
3170                }
3171                None => {
3172                    return Err(CompileError::new(
3173                        ErrorKind::IndexOnNonArray {
3174                            found: base_type.name().to_string(),
3175                        },
3176                        span,
3177                    ));
3178                }
3179            };
3180
3181            // Analyze index. Must be `usize` (ADR-0054).
3182            let index_result = self.analyze_inst(air, index, ctx)?;
3183            if index_result.ty != Type::USIZE && !index_result.ty.is_error() {
3184                return Err(CompileError::type_mismatch(
3185                    "usize".to_string(),
3186                    index_result.ty.name().to_string(),
3187                    self.rir.get(index).span,
3188                ));
3189            }
3190
3191            // Compile-time bounds check for constant indices
3192            if let Some(const_index) = self.try_get_const_index(index)
3193                && (const_index < 0 || const_index as u64 >= array_len)
3194            {
3195                return Err(CompileError::new(
3196                    ErrorKind::IndexOutOfBounds {
3197                        index: const_index,
3198                        length: array_len,
3199                    },
3200                    self.rir.get(index).span,
3201                ));
3202            }
3203
3204            // Add the index projection
3205            trace.projections.push(ProjectionInfo {
3206                proj: AirProjection::Index {
3207                    array_type: base_type,
3208                    index: index_result.air_ref,
3209                },
3210                result_type: elem_type,
3211                field_name: None,
3212            });
3213
3214            // Analyze the value
3215            let value_result = self.analyze_inst(air, value, ctx)?;
3216
3217            // Emit PlaceWrite instruction
3218            let place_ref = Self::build_place_ref(air, &trace);
3219            let air_ref = air.add_inst(AirInst {
3220                data: AirInstData::PlaceWrite {
3221                    place: place_ref,
3222                    value: value_result.air_ref,
3223                },
3224                ty: Type::UNIT,
3225                span,
3226            });
3227            return Ok(AnalysisResult::new(air_ref, Type::UNIT));
3228        }
3229
3230        // Fallback: base is not a place
3231        Err(CompileError::new(ErrorKind::InvalidAssignmentTarget, span))
3232    }
3233
3234    /// Implementation for MethodCall.
3235    pub(crate) fn analyze_method_call_impl(
3236        &mut self,
3237        air: &mut Air,
3238        receiver: InstRef,
3239        method: Spur,
3240        args: Vec<RirCallArg>,
3241        span: Span,
3242        ctx: &mut AnalysisContext,
3243    ) -> CompileResult<AnalysisResult> {
3244        let receiver_var = self.extract_root_variable(receiver);
3245        let method_name_str = self.interner.resolve(&method).to_string();
3246
3247        // Analyze the receiver expression
3248        let receiver_result = self.analyze_inst(air, receiver, ctx)?;
3249        let receiver_type = receiver_result.ty;
3250
3251        // Handle module member access: module.function() becomes a direct function call
3252        if receiver_type.is_module() {
3253            return self.analyze_module_member_call_impl(air, method, args, span, ctx);
3254        }
3255
3256        // ADR-0079: `.clone()` on a Copy type is a bitwise copy. The
3257        // method dispatch falls through to "no method named clone"
3258        // for primitives like i32, but Copy types structurally
3259        // conform to `Clone` (lang-item-driven short-circuit), so a
3260        // `.clone()` call on them must succeed and just hand back the
3261        // receiver value. Used by the prelude `derive Clone` body to
3262        // clone Copy fields without requiring per-primitive method
3263        // declarations.
3264        if method_name_str == "clone"
3265            && args.is_empty()
3266            && self.is_type_copy(receiver_type)
3267            && self.lang_items.clone().is_some()
3268        {
3269            return Ok(AnalysisResult::new(receiver_result.air_ref, receiver_type));
3270        }
3271
3272        // Check that receiver is a struct or enum type
3273        // For enum methods, dispatch through enum_methods table
3274        if let TypeKind::Enum(enum_id) = receiver_type.kind() {
3275            let enum_def = self.type_pool.enum_def(enum_id);
3276            let enum_name_str = enum_def.name.clone();
3277
3278            let method_key = (enum_id, method);
3279            let method_info = self.enum_methods.get(&method_key).ok_or_compile_error(
3280                ErrorKind::UndefinedMethod {
3281                    type_name: enum_name_str.clone(),
3282                    method_name: method_name_str.clone(),
3283                },
3284                span,
3285            )?;
3286            // ADR-0026 lazy analysis: track this enum method as referenced
3287            // so its body is analyzed by the work queue. The same fix as
3288            // the struct-method path above.
3289            ctx.referenced_methods.insert((StructId(enum_id.0), method));
3290
3291            if !method_info.has_self {
3292                return Err(CompileError::new(
3293                    ErrorKind::AssocFnCalledAsMethod {
3294                        type_name: enum_name_str,
3295                        function_name: method_name_str,
3296                    },
3297                    span,
3298                ));
3299            }
3300
3301            // ADR-0073: gated cross-module visibility check on the enum
3302            // method. Enums aren't built-in, so the synthetic-builtin
3303            // exemption doesn't apply.
3304            self.check_method_visibility(
3305                &enum_def.name,
3306                false,
3307                method_info.is_pub,
3308                method_info.file_id,
3309                &method_name_str,
3310                span,
3311            )?;
3312
3313            let method_param_types = self.param_arena.types(method_info.params);
3314            if args.len() != method_param_types.len() {
3315                return Err(CompileError::new(
3316                    ErrorKind::WrongArgumentCount {
3317                        expected: method_param_types.len(),
3318                        found: args.len(),
3319                    },
3320                    span,
3321                ));
3322            }
3323
3324            self.check_exclusive_access(&args, span)?;
3325
3326            // ADR-0062: undo the receiver move and pass it by-pointer when the
3327            // method takes `&self` / `&mut self`. Mirrors the struct-method
3328            // path below.
3329            let recv_pass_mode = match method_info.receiver {
3330                crate::types::ReceiverMode::ByValue => AirArgMode::Normal,
3331                crate::types::ReceiverMode::Ref => AirArgMode::Ref,
3332                crate::types::ReceiverMode::MutRef => AirArgMode::MutRef,
3333            };
3334            if !matches!(method_info.receiver, crate::types::ReceiverMode::ByValue)
3335                && let Some(var) = receiver_var
3336            {
3337                ctx.moved_vars.remove(&var);
3338            }
3339
3340            let return_type = method_info.return_type;
3341
3342            let mut air_args = vec![AirCallArg {
3343                value: receiver_result.air_ref,
3344                mode: recv_pass_mode,
3345            }];
3346            air_args.extend(self.analyze_call_args(air, &args, ctx)?);
3347
3348            let call_name = format!("{}.{}", enum_name_str, method_name_str);
3349            let call_name_sym = self.interner.get_or_intern(&call_name);
3350
3351            let args_len = air_args.len() as u32;
3352            let mut extra_data = Vec::with_capacity(air_args.len() * 2);
3353            for arg in &air_args {
3354                extra_data.push(arg.value.as_u32());
3355                extra_data.push(arg.mode.as_u32());
3356            }
3357            let args_start = air.add_extra(&extra_data);
3358
3359            let air_ref = air.add_inst(AirInst {
3360                data: AirInstData::Call {
3361                    name: call_name_sym,
3362                    args_start,
3363                    args_len,
3364                },
3365                ty: return_type,
3366                span,
3367            });
3368            return Ok(AnalysisResult::new(air_ref, return_type));
3369        }
3370
3371        // ADR-0056 Phase 4d-extended: dispatch method calls on interface
3372        // receivers dynamically via the vtable. The body type-checks against
3373        // the interface's declared signature (not against any concrete
3374        // implementor).
3375        if let TypeKind::Interface(iface_id) = receiver_type.kind() {
3376            // Interface dispatch passes the data pointer to the dispatched
3377            // method without copying or moving the underlying value, so the
3378            // receiver behaves like a borrow at the move-checker level.
3379            // Undo any move that `analyze_inst` may have recorded for the
3380            // root variable.
3381            if let Some(var) = receiver_var {
3382                ctx.moved_vars.remove(&var);
3383            }
3384            let iface_def = self.interface_defs[iface_id.0 as usize].clone();
3385            let (slot, req) = iface_def
3386                .find_method(&method_name_str)
3387                .map(|(s, r)| (s, r.clone()))
3388                .ok_or_compile_error(
3389                    ErrorKind::UndefinedMethod {
3390                        type_name: format!("interface `{}`", iface_def.name),
3391                        method_name: method_name_str.clone(),
3392                    },
3393                    span,
3394                )?;
3395
3396            // ADR-0088: if the interface method signature is
3397            // `@mark(unchecked)`, the call site needs to sit inside a
3398            // `checked { }` block. Conformance enforces that every
3399            // implementor's `is_unchecked` matches the interface's, so
3400            // the gate decision is fully determined here.
3401            if req.is_unchecked && ctx.checked_depth == 0 {
3402                return Err(CompileError::new(
3403                    ErrorKind::UncheckedCallRequiresChecked(format!(
3404                        "{}.{}",
3405                        iface_def.name, req.name
3406                    )),
3407                    span,
3408                ));
3409            }
3410
3411            // Argument count check.
3412            if args.len() != req.param_types.len() {
3413                return Err(CompileError::new(
3414                    ErrorKind::WrongArgumentCount {
3415                        expected: req.param_types.len(),
3416                        found: args.len(),
3417                    },
3418                    span,
3419                ));
3420            }
3421
3422            self.check_exclusive_access(&args, span)?;
3423
3424            let air_args = self.analyze_call_args(air, &args, ctx)?;
3425
3426            // Type-check each arg against the interface's declared param type.
3427            // `Self` slots (ADR-0060) are substituted with the interface type
3428            // itself — at a dynamic dispatch site there is no concrete
3429            // candidate to bind to, so `Self` flows through as the receiver's
3430            // static type.
3431            let iface_ty = receiver_type;
3432            for (i, (arg_air, req_ty)) in air_args.iter().zip(req.param_types.iter()).enumerate() {
3433                let expected_ty = req_ty.substitute_self(iface_ty);
3434                let actual_ty = air.get(arg_air.value).ty;
3435                if actual_ty != expected_ty {
3436                    let arg_span = self.rir.get(args[i].value).span;
3437                    return Err(CompileError::type_mismatch(
3438                        expected_ty.name().to_string(),
3439                        actual_ty.name().to_string(),
3440                        arg_span,
3441                    ));
3442                }
3443            }
3444
3445            // Encode args (excluding the receiver) into the extra array.
3446            let mut extra_data = Vec::with_capacity(air_args.len() * 2);
3447            for arg in &air_args {
3448                extra_data.push(arg.value.as_u32());
3449                extra_data.push(arg.mode.as_u32());
3450            }
3451            let dyn_args_start = air.add_extra(&extra_data);
3452            let dyn_args_len = air_args.len() as u32;
3453
3454            let return_type = req.return_type.substitute_self(iface_ty);
3455            let air_ref = air.add_inst(AirInst {
3456                data: AirInstData::MethodCallDyn {
3457                    interface_id: iface_id,
3458                    slot: slot as u32,
3459                    recv: receiver_result.air_ref,
3460                    args_start: dyn_args_start,
3461                    args_len: dyn_args_len,
3462                },
3463                ty: return_type,
3464                span,
3465            });
3466            return Ok(AnalysisResult::new(air_ref, return_type));
3467        }
3468
3469        // ADR-0063: methods on `Ptr(T)` / `MutPtr(T)` values dispatch through
3470        // the POINTER_METHODS registry to existing pointer intrinsics.
3471        if matches!(
3472            receiver_type.kind(),
3473            TypeKind::PtrConst(_) | TypeKind::PtrMut(_)
3474        ) {
3475            return self.dispatch_pointer_method_call(
3476                air,
3477                receiver_result,
3478                &method_name_str,
3479                &args,
3480                span,
3481                ctx,
3482            );
3483        }
3484
3485        // ADR-0064: methods on `Slice(T)` / `MutSlice(T)` values dispatch
3486        // through the SLICE_METHODS registry.
3487        if matches!(
3488            receiver_type.kind(),
3489            TypeKind::Slice(_) | TypeKind::MutSlice(_)
3490        ) {
3491            return self.dispatch_slice_method_call(
3492                air,
3493                receiver_result,
3494                &method_name_str,
3495                &args,
3496                span,
3497                ctx,
3498            );
3499        }
3500
3501        // ADR-0071: methods on `char` values. `to_u32` lowers to a no-op
3502        // bitcast (the i32 storage already holds the codepoint). `len_utf8`,
3503        // `is_ascii`, `encode_utf8` are added in later phases.
3504        if receiver_type == Type::CHAR {
3505            return self.dispatch_char_method_call(
3506                air,
3507                receiver_result,
3508                &method_name_str,
3509                &args,
3510                span,
3511                ctx,
3512            );
3513        }
3514
3515        // ADR-0066: methods on `Vec(T)` values dispatch through the
3516        // vec_methods module which emits the appropriate intrinsic. Most
3517        // Vec methods take borrow/inout self — undo the move that
3518        // analyze_inst recorded. ADR-0067's `dispose(self)` consumes
3519        // self by-value, so the move recorded by `analyze_inst` is correct
3520        // and should be preserved.
3521        if matches!(receiver_type.kind(), TypeKind::Vec(_)) {
3522            let consumes_self = matches!(method_name_str.as_str(), "dispose");
3523            if !consumes_self && let Some(var) = receiver_var {
3524                ctx.moved_vars.remove(&var);
3525            }
3526            return self.dispatch_vec_method_call(
3527                air,
3528                receiver_result,
3529                &method_name_str,
3530                &args,
3531                span,
3532                ctx,
3533            );
3534        }
3535
3536        let struct_id = match receiver_type.kind() {
3537            TypeKind::Struct(id) => id,
3538            _ => {
3539                return Err(CompileError::new(
3540                    ErrorKind::MethodCallOnNonStruct {
3541                        found: receiver_type.name().to_string(),
3542                        method_name: method_name_str,
3543                    },
3544                    span,
3545                ));
3546            }
3547        };
3548
3549        // Look up the struct name by its ID (for error messages)
3550        let struct_def = self.type_pool.struct_def(struct_id);
3551        let struct_name_str = struct_def.name.clone();
3552
3553        // ADR-0065 Phase 2: `@derive(Clone)` structs have a synthesized
3554        // `<TypeName>.clone(borrow self) -> Self` emitted by `clone_glue`.
3555        // The synthesized function isn't registered in `self.methods`; emit
3556        // the Call directly when dispatching `.clone()` on such a struct,
3557        // and *only* if the user hasn't also written their own clone method
3558        // (which takes precedence via the regular methods.get path below).
3559        if struct_def.is_clone
3560            && method_name_str == "clone"
3561            && !self.methods.contains_key(&(struct_id, method))
3562            && args.is_empty()
3563        {
3564            // Receiver is `borrow self`; the AIR Call carries the receiver
3565            // as a Borrow-mode arg.
3566            if let Some(var) = receiver_var {
3567                ctx.moved_vars.remove(&var);
3568            }
3569            let extra = [receiver_result.air_ref.as_u32(), AirArgMode::Ref.as_u32()];
3570            let args_start = air.add_extra(&extra);
3571            let fn_name = self
3572                .interner
3573                .get_or_intern(format!("{}.clone", struct_name_str));
3574            let return_type = Type::new_struct(struct_id);
3575            let air_ref = air.add_inst(AirInst {
3576                data: AirInstData::Call {
3577                    name: fn_name,
3578                    args_start,
3579                    args_len: 1,
3580                },
3581                ty: return_type,
3582                span,
3583            });
3584            return Ok(AnalysisResult::new(air_ref, return_type));
3585        }
3586
3587        // Look up the method using StructId directly. Copy out so the
3588        // borrow on `self.methods` doesn't conflict with later mutable
3589        // borrows of `self` (e.g. `analyze_call_args`).
3590        let method_key = (struct_id, method);
3591        let method_info: MethodInfo = *self.methods.get(&method_key).ok_or_compile_error(
3592            ErrorKind::UndefinedMethod {
3593                type_name: struct_name_str.clone(),
3594                method_name: method_name_str.clone(),
3595            },
3596            span,
3597        )?;
3598        // ADR-0026 lazy analysis (and ADR-0078 prelude module loading):
3599        // record the dispatched method as referenced so the lazy work
3600        // queue analyzes its body. Without this, anonymous-struct
3601        // `__call` methods (and any other dispatched-by-method-call
3602        // method) are registered but their bodies never get codegen,
3603        // causing link errors.
3604        ctx.referenced_methods.insert(method_key);
3605        let method_info = &method_info;
3606
3607        // Check that this is a method (has self), not an associated function
3608        if !method_info.has_self {
3609            return Err(CompileError::new(
3610                ErrorKind::AssocFnCalledAsMethod {
3611                    type_name: struct_name_str,
3612                    function_name: method_name_str,
3613                },
3614                span,
3615            ));
3616        }
3617
3618        // ADR-0073: gated cross-module visibility check on the method.
3619        let struct_def = self.type_pool.struct_def(struct_id);
3620        self.check_method_visibility(
3621            &struct_def.name,
3622            struct_def.is_builtin,
3623            method_info.is_pub,
3624            method_info.file_id,
3625            &method_name_str,
3626            span,
3627        )?;
3628
3629        // ADR-0088: the previous ADR-0072 by-name gate
3630        // (`check_string_vec_bridge_method_gates`) retired in favour of
3631        // the per-method `@mark(unchecked)` directive carried by the
3632        // prelude declarations themselves. The unchecked gate fires via
3633        // the standard `MethodInfo::is_unchecked` check below.
3634
3635        // ADR-0062: a `&self` / `&mut self` receiver is sugar for a borrow
3636        // (immutable / mutable). The receiver expression's `analyze_inst`
3637        // already recorded a move on the root variable since it was
3638        // evaluated as a value; undo it so the caller can keep using the
3639        // value after the call. This mirrors the interface-dispatch and
3640        // builtin-method paths above.
3641        let recv_pass_mode = match method_info.receiver {
3642            crate::types::ReceiverMode::ByValue => AirArgMode::Normal,
3643            crate::types::ReceiverMode::Ref => AirArgMode::Ref,
3644            crate::types::ReceiverMode::MutRef => AirArgMode::MutRef,
3645        };
3646        if !matches!(method_info.receiver, crate::types::ReceiverMode::ByValue)
3647            && let Some(var) = receiver_var
3648        {
3649            ctx.moved_vars.remove(&var);
3650        }
3651
3652        // ADR-0081: a `MutRef(Self)` receiver requires the bound variable
3653        // to be `let mut` (or to come through a `MutRef(_)` parameter /
3654        // local). Without this, `let s = String::new(); s.push_str("...")`
3655        // would silently mutate `s` despite the `let`'s implicit-immutable
3656        // binding.
3657        if matches!(method_info.receiver, crate::types::ReceiverMode::MutRef)
3658            && let Some(var) = receiver_var
3659        {
3660            let is_mutable = ctx
3661                .params
3662                .iter()
3663                .find(|p| p.name == var)
3664                .map(|p| {
3665                    matches!(p.ty.kind(), TypeKind::MutRef(_))
3666                        || matches!(p.mode, RirParamMode::MutRef)
3667                })
3668                .or_else(|| {
3669                    ctx.locals
3670                        .get(&var)
3671                        .map(|local| local.is_mut || matches!(local.ty.kind(), TypeKind::MutRef(_)))
3672                })
3673                .unwrap_or(true);
3674            if !is_mutable {
3675                let name_str = self.interner.resolve(&var).to_string();
3676                return Err(CompileError::new(
3677                    ErrorKind::AssignToImmutable(name_str),
3678                    span,
3679                ));
3680            }
3681        }
3682
3683        // Check if calling an unchecked method requires a checked block
3684        if method_info.is_unchecked && ctx.checked_depth == 0 {
3685            return Err(CompileError::new(
3686                ErrorKind::UncheckedCallRequiresChecked(format!(
3687                    "{}.{}",
3688                    struct_name_str, method_name_str
3689                )),
3690                span,
3691            ));
3692        }
3693
3694        // Clone data needed before mutable borrow
3695        let is_method_generic = method_info.is_generic;
3696        let method_param_comptime = self.param_arena.comptime(method_info.params).to_vec();
3697        let method_param_names = self.param_arena.names(method_info.params).to_vec();
3698        let return_type_for_call = method_info.return_type;
3699        let method_return_type_sym = method_info.return_type_sym;
3700        let method_param_types = self.param_arena.types(method_info.params).to_vec();
3701
3702        // Argument count check is split between generic and non-generic methods.
3703        // Generic methods (ADR-0055) accept either the full arg list (explicit
3704        // mode) or just the runtime args (inference mode), so we skip the check
3705        // here for generics and validate later inside the inference branch.
3706        if !is_method_generic && args.len() != method_param_types.len() {
3707            return Err(CompileError::new(
3708                ErrorKind::WrongArgumentCount {
3709                    expected: method_param_types.len(),
3710                    found: args.len(),
3711                },
3712                span,
3713            ));
3714        }
3715
3716        // Check for exclusive access violation
3717        self.check_exclusive_access(&args, span)?;
3718
3719        // ADR-0055 / method-level generics: if this method has comptime type
3720        // params (e.g. `comptime F: type`), emit a CallGeneric instruction
3721        // with the type arguments — either extracted from the call args
3722        // (explicit) or inferred from the runtime arg types (when the user
3723        // omits the comptime type args entirely). The specialization pass
3724        // synthesizes a specialized method body using these type args.
3725        if is_method_generic {
3726            let total_params = method_param_types.len();
3727            let runtime_param_count = method_param_comptime.iter().filter(|c| !**c).count();
3728
3729            // Two valid call shapes:
3730            //   1. Explicit: every comptime + runtime param has an arg.
3731            //   2. Inferred: only runtime params have args; comptime types
3732            //      are recovered from the runtime arg types.
3733            // Anything else is a count mismatch.
3734            let infer_comptimes = if args.len() == total_params {
3735                false
3736            } else if args.len() == runtime_param_count && runtime_param_count < total_params {
3737                true
3738            } else {
3739                return Err(CompileError::new(
3740                    ErrorKind::WrongArgumentCount {
3741                        expected: total_params,
3742                        found: args.len(),
3743                    },
3744                    span,
3745                ));
3746            };
3747
3748            // Phase 1: extract or analyze, in two paths that converge on
3749            // (type_args, type_subst, air_runtime_args).
3750            let mut type_args: Vec<Type> = Vec::new();
3751            let mut type_subst: rustc_hash::FxHashMap<Spur, Type> =
3752                rustc_hash::FxHashMap::default();
3753            let air_runtime_args: Vec<AirCallArg>;
3754
3755            if !infer_comptimes {
3756                // Explicit mode: walk args in order, picking out comptime
3757                // type args and analyzing runtime args.
3758                let mut runtime_args: Vec<RirCallArg> = Vec::new();
3759                for (idx, arg) in args.iter().enumerate() {
3760                    if method_param_comptime[idx] {
3761                        let ty = self.resolve_method_generic_type_arg(
3762                            arg.value,
3763                            method_param_names[idx],
3764                            ctx,
3765                        )?;
3766                        type_args.push(ty);
3767                        type_subst.insert(method_param_names[idx], ty);
3768                    } else {
3769                        runtime_args.push(arg.clone());
3770                    }
3771                }
3772                air_runtime_args = self.analyze_call_args(air, &runtime_args, ctx)?;
3773            } else {
3774                // Inference mode: analyze all user-supplied args (they are
3775                // all runtime), then recover each comptime type param by
3776                // structural unification against a later runtime param's
3777                // declared type.
3778                air_runtime_args = self.analyze_call_args(air, &args, ctx)?;
3779
3780                let runtime_arg_tys: Vec<Type> = air_runtime_args
3781                    .iter()
3782                    .map(|a| air.get(a.value).ty)
3783                    .collect();
3784
3785                // Look up the declared type symbols of the original method
3786                // params from RIR (the resolved types are placeholders).
3787                let param_decl_tys =
3788                    self.method_param_type_syms(method_info.body)
3789                        .ok_or_else(|| {
3790                            CompileError::new(
3791                                ErrorKind::InternalError(
3792                                    "generic method has no FnDecl in RIR".to_string(),
3793                                ),
3794                                span,
3795                            )
3796                        })?;
3797
3798                for (i, is_comptime) in method_param_comptime.iter().enumerate() {
3799                    if !is_comptime {
3800                        continue;
3801                    }
3802                    let cp_name = method_param_names[i];
3803                    // Find a runtime param at full position j > i whose
3804                    // declared type symbol references this comptime type
3805                    // param (bare match `j: T` for now).
3806                    let mut runtime_pos = 0usize;
3807                    let mut inferred: Option<Type> = None;
3808                    for (j, j_is_comptime) in method_param_comptime.iter().enumerate() {
3809                        if *j_is_comptime {
3810                            continue;
3811                        }
3812                        if j > i && param_decl_tys[j] == cp_name {
3813                            inferred = Some(runtime_arg_tys[runtime_pos]);
3814                            break;
3815                        }
3816                        runtime_pos += 1;
3817                    }
3818                    let inferred_ty = inferred.ok_or_else(|| {
3819                        CompileError::new(
3820                            ErrorKind::ComptimeEvaluationFailed {
3821                                reason: format!(
3822                                    "cannot infer comptime type parameter `{}`; \
3823                                     pass it explicitly",
3824                                    self.interner.resolve(&cp_name)
3825                                ),
3826                            },
3827                            span,
3828                        )
3829                    })?;
3830                    type_args.push(inferred_ty);
3831                    type_subst.insert(cp_name, inferred_ty);
3832                }
3833            }
3834
3835            // Substitute the return type if it references any method-level
3836            // type params. Handles the simple case `-> U` (look up in the
3837            // substitution map) as well as compound cases like `-> [U; N]`
3838            // and `-> ptr const U` (recursive resolution via
3839            // `resolve_type_for_comptime_with_subst`).
3840            let return_type_sub = if let Some(&ty) = type_subst.get(&method_return_type_sym) {
3841                ty
3842            } else if return_type_for_call == Type::COMPTIME_TYPE {
3843                match self.resolve_type_for_comptime_with_subst(method_return_type_sym, &type_subst)
3844                {
3845                    Some(ty) => ty,
3846                    None => return_type_for_call,
3847                }
3848            } else {
3849                return_type_for_call
3850            };
3851
3852            // Build the AIR call args: receiver first, then runtime args.
3853            let mut air_args = vec![AirCallArg {
3854                value: receiver_result.air_ref,
3855                mode: recv_pass_mode,
3856            }];
3857            air_args.extend(air_runtime_args);
3858
3859            // Encode type args (raw Type discriminant values).
3860            let type_extra: Vec<u32> = type_args.iter().map(|t| t.as_u32()).collect();
3861            let type_args_start = air.add_extra(&type_extra);
3862            let type_args_len = type_args.len() as u32;
3863
3864            // Encode runtime args.
3865            let mut args_extra = Vec::with_capacity(air_args.len() * 2);
3866            for arg in &air_args {
3867                args_extra.push(arg.value.as_u32());
3868                args_extra.push(arg.mode.as_u32());
3869            }
3870            let args_start_air = air.add_extra(&args_extra);
3871            let args_len_air = air_args.len() as u32;
3872
3873            let call_name = format!("{}.{}", struct_name_str, method_name_str);
3874            let call_name_sym = self.interner.get_or_intern(&call_name);
3875
3876            let air_ref = air.add_inst(AirInst {
3877                data: AirInstData::CallGeneric {
3878                    name: call_name_sym,
3879                    type_args_start,
3880                    type_args_len,
3881                    args_start: args_start_air,
3882                    args_len: args_len_air,
3883                },
3884                ty: return_type_sub,
3885                span,
3886            });
3887            return Ok(AnalysisResult::new(air_ref, return_type_sub));
3888        }
3889
3890        let return_type = method_info.return_type;
3891
3892        // Analyze arguments - receiver first, then remaining args
3893        let mut air_args = vec![AirCallArg {
3894            value: receiver_result.air_ref,
3895            mode: recv_pass_mode,
3896        }];
3897        air_args.extend(self.analyze_call_args(air, &args, ctx)?);
3898
3899        // Generate a method call name: Type.method
3900        let call_name = format!("{}.{}", struct_name_str, method_name_str);
3901        let call_name_sym = self.interner.get_or_intern(&call_name);
3902
3903        // Encode call args into extra array
3904        let args_len = air_args.len() as u32;
3905        let mut extra_data = Vec::with_capacity(air_args.len() * 2);
3906        for arg in &air_args {
3907            extra_data.push(arg.value.as_u32());
3908            extra_data.push(arg.mode.as_u32());
3909        }
3910        let args_start = air.add_extra(&extra_data);
3911
3912        let air_ref = air.add_inst(AirInst {
3913            data: AirInstData::Call {
3914                name: call_name_sym,
3915                args_start,
3916                args_len,
3917            },
3918            ty: return_type,
3919            span,
3920        });
3921        Ok(AnalysisResult::new(air_ref, return_type))
3922    }
3923
3924    /// Analyze a module member call: `module.function(args)` becomes a direct function call.
3925    ///
3926    /// In Phase 1 of the module system, modules are virtual namespaces. When you import
3927    /// a module with `@import("foo.gruel")`, all of foo.gruel's functions are already in the
3928    /// global function table (via multi-file compilation). The module just provides a
3929    /// namespace at the source level.
3930    fn analyze_module_member_call_impl(
3931        &mut self,
3932        air: &mut Air,
3933        function_name: Spur,
3934        args: Vec<RirCallArg>,
3935        span: Span,
3936        ctx: &mut AnalysisContext,
3937    ) -> CompileResult<AnalysisResult> {
3938        // Look up the function in the global function table
3939        let fn_name_str = self.interner.resolve(&function_name).to_string();
3940        let fn_info = *self
3941            .functions
3942            .get(&function_name)
3943            .ok_or_compile_error(ErrorKind::UndefinedFunction(fn_name_str.clone()), span)?;
3944
3945        // Track this function as referenced (for lazy analysis)
3946        ctx.referenced_functions.insert(function_name);
3947
3948        // Check visibility: private functions are only accessible from the same directory
3949        let accessing_file_id = span.file_id;
3950        let target_file_id = fn_info.file_id;
3951        if !self.is_accessible(accessing_file_id, target_file_id, fn_info.is_pub) {
3952            return Err(CompileError::new(
3953                ErrorKind::PrivateMemberAccess {
3954                    item_kind: "function".to_string(),
3955                    name: fn_name_str,
3956                },
3957                span,
3958            ));
3959        }
3960
3961        // Get parameter data from the arena
3962        let param_types = self.param_arena.types(fn_info.params);
3963        let param_modes = self.param_arena.modes(fn_info.params);
3964
3965        // Check argument count
3966        if args.len() != param_types.len() {
3967            let expected = param_types.len();
3968            let found = args.len();
3969            return Err(CompileError::new(
3970                ErrorKind::WrongArgumentCount { expected, found },
3971                span,
3972            ));
3973        }
3974
3975        // Check that call-site argument modes match function parameter modes
3976        for (i, (arg, expected_mode)) in args.iter().zip(param_modes.iter()).enumerate() {
3977            match expected_mode {
3978                RirParamMode::MutRef => {
3979                    if arg.mode != RirArgMode::MutRef {
3980                        return Err(CompileError::new(
3981                            ErrorKind::InoutKeywordMissing,
3982                            self.rir.get(args[i].value).span,
3983                        ));
3984                    }
3985                }
3986                RirParamMode::Ref => {
3987                    if arg.mode != RirArgMode::Ref {
3988                        return Err(CompileError::new(
3989                            ErrorKind::BorrowKeywordMissing,
3990                            self.rir.get(args[i].value).span,
3991                        ));
3992                    }
3993                }
3994                RirParamMode::Normal => {
3995                    // Normal params accept any mode
3996                }
3997                RirParamMode::Comptime => {
3998                    // Comptime params - handled elsewhere
3999                }
4000            }
4001        }
4002
4003        // Analyze arguments
4004        let air_args = self.analyze_call_args(air, &args, ctx)?;
4005
4006        // ADR-0056 Phase 4c: for any argument whose corresponding parameter
4007        // has an interface type, run a structural conformance check against
4008        // the argument's concrete type. If conformance succeeds we currently
4009        // surface "Phase 4d not yet implemented" — emitting a real
4010        // `MakeInterfaceRef` is wired in Phase 4d alongside codegen.
4011        let param_types_owned: Vec<Type> = self.param_arena.types(fn_info.params).to_vec();
4012        for (i, (arg_air, param_ty)) in air_args.iter().zip(param_types_owned.iter()).enumerate() {
4013            if let crate::types::TypeKind::Interface(iface_id) = param_ty.kind() {
4014                let arg_ty = air.get(arg_air.value).ty;
4015                let arg_span = self.rir.get(args[i].value).span;
4016                self.check_conforms(arg_ty, iface_id, arg_span)?;
4017                return Err(CompileError::new(
4018                    ErrorKind::InternalError(
4019                        "interface runtime dispatch (fat-pointer codegen) is not yet \
4020                         implemented (ADR-0056 Phase 4d). The conformance check passes; \
4021                         use `comptime T: I` for a working alternative."
4022                            .to_string(),
4023                    ),
4024                    arg_span,
4025                ));
4026            }
4027        }
4028
4029        let return_type = fn_info.return_type;
4030
4031        // Encode call args
4032        let mut extra_data = Vec::with_capacity(air_args.len() * 2);
4033        for arg in &air_args {
4034            extra_data.push(arg.value.as_u32());
4035            extra_data.push(arg.mode.as_u32());
4036        }
4037        let call_args_start = air.add_extra(&extra_data);
4038        let call_args_len = air_args.len() as u32;
4039
4040        let air_ref = air.add_inst(AirInst {
4041            data: AirInstData::Call {
4042                name: function_name,
4043                args_start: call_args_start,
4044                args_len: call_args_len,
4045            },
4046            ty: return_type,
4047            span,
4048        });
4049        Ok(AnalysisResult::new(air_ref, return_type))
4050    }
4051
4052    /// Implementation for AssocFnCall.
4053    pub(crate) fn analyze_assoc_fn_call_impl(
4054        &mut self,
4055        air: &mut Air,
4056        type_name: Spur,
4057        function: Spur,
4058        args: Vec<RirCallArg>,
4059        span: Span,
4060        ctx: &mut AnalysisContext,
4061    ) -> CompileResult<AnalysisResult> {
4062        let type_name_str = self.interner.resolve(&type_name).to_string();
4063        let function_name_str = self.interner.resolve(&function).to_string();
4064
4065        // ADR-0071: associated functions on the `char` primitive type.
4066        // `char::from_u32(n) -> Result(char, u32)` and (in `checked` blocks)
4067        // `char::from_u32_unchecked(n) -> char`.
4068        if type_name_str == "char" {
4069            return self.dispatch_char_assoc_fn_call(air, &function_name_str, &args, span, ctx);
4070        }
4071
4072        // ADR-0063: `Ptr(T)::name(args)` / `MutPtr(T)::name(args)`. The RIR
4073        // path stores the LHS as the synthesized symbol `Ptr(T)`; sema's
4074        // resolve_type already handles type-call syntax via the
4075        // BuiltinTypeConstructor registry, so dispatch through there.
4076        //
4077        // Parameterized type calls fall into three buckets handled here:
4078        //   1. `Vec(T)::method(...)` — the lang-item Vec dispatcher.
4079        //   2. `Ptr(T)::method(...)` / `MutPtr(T)::method(...)` — pointer dispatcher.
4080        //   3. `UserType(T)::method(...)` — user-defined `pub fn UserType(...)
4081        //      -> type` returning a struct. Resolve the LHS to its synthetic
4082        //      `Type`, extract the StructId, and fall through to the regular
4083        //      struct method-lookup path below.
4084        if let Some((callee_name, _)) = crate::types::parse_type_call_syntax(&type_name_str) {
4085            if let Some(constructor) = gruel_builtins::get_builtin_type_constructor(&callee_name) {
4086                // ADR-0066: route Vec(T)::new()/with_capacity(n) through the
4087                // Vec dispatcher.
4088                if matches!(
4089                    constructor.kind,
4090                    gruel_builtins::BuiltinTypeConstructorKind::Vec
4091                ) {
4092                    let vec_ty = self.resolve_type(type_name, span)?;
4093                    if let Some(result) = self.try_dispatch_vec_static_call(
4094                        air,
4095                        ctx,
4096                        vec_ty,
4097                        &function_name_str,
4098                        &args,
4099                        span,
4100                    ) {
4101                        return result;
4102                    }
4103                }
4104                return self.dispatch_pointer_assoc_fn_call(
4105                    air,
4106                    type_name,
4107                    &function_name_str,
4108                    &args,
4109                    span,
4110                    ctx,
4111                );
4112            }
4113            // User-defined parameterized type. Resolve the LHS via the
4114            // standard type resolver — that drives the comptime evaluation
4115            // of the type-constructor function and produces a synthetic
4116            // struct type. From there we fall through to the regular
4117            // struct method dispatch below by replacing `struct_id`.
4118            if let Ok(resolved_ty) = self.resolve_type(type_name, span)
4119                && let TypeKind::Struct(struct_id) = resolved_ty.kind()
4120            {
4121                return self.analyze_struct_assoc_fn_call(
4122                    air,
4123                    struct_id,
4124                    type_name_str,
4125                    function,
4126                    function_name_str,
4127                    args,
4128                    span,
4129                    ctx,
4130                );
4131            }
4132        }
4133
4134        // Check if this is an enum data variant construction (e.g., IntOption::Some(42)).
4135        // This must be checked before the struct lookup because enums and structs share the
4136        // same AssocFnCall syntax.
4137        if let Some(&enum_id) = self.enums.get(&type_name) {
4138            let enum_def = self.type_pool.enum_def(enum_id);
4139            if let Some(variant_index) = enum_def.find_variant(&function_name_str) {
4140                let variant_def = &enum_def.variants[variant_index];
4141                let field_types: Vec<Type> = variant_def.fields.clone();
4142                if !field_types.is_empty() {
4143                    // If this is a struct variant, error: use { } instead of ( )
4144                    if variant_def.is_struct_variant() {
4145                        return Err(CompileError::type_mismatch(
4146                            format!(
4147                                "struct-style construction `{}::{} {{ ... }}`",
4148                                type_name_str, function_name_str
4149                            ),
4150                            format!(
4151                                "tuple-style construction `{}::{}(...)`",
4152                                type_name_str, function_name_str
4153                            ),
4154                            span,
4155                        ));
4156                    }
4157
4158                    // Check argument count
4159                    if args.len() != field_types.len() {
4160                        return Err(CompileError::new(
4161                            ErrorKind::WrongArgumentCount {
4162                                expected: field_types.len(),
4163                                found: args.len(),
4164                            },
4165                            span,
4166                        ));
4167                    }
4168
4169                    // Analyze each argument and type-check against the variant's field types
4170                    let mut field_air_refs = Vec::with_capacity(args.len());
4171                    for (i, arg) in args.iter().enumerate() {
4172                        let result = self.analyze_inst(air, arg.value, ctx)?;
4173                        if result.ty != field_types[i] {
4174                            return Err(CompileError::type_mismatch(
4175                                field_types[i].name().to_string(),
4176                                result.ty.name().to_string(),
4177                                span,
4178                            ));
4179                        }
4180                        field_air_refs.push(result.air_ref.as_u32());
4181                    }
4182
4183                    // Store field AirRefs in the extra array
4184                    let fields_len = field_air_refs.len() as u32;
4185                    let fields_start = air.add_extra(&field_air_refs);
4186
4187                    let enum_type = Type::new_enum(enum_id);
4188                    let air_ref = air.add_inst(AirInst {
4189                        data: AirInstData::EnumCreate {
4190                            enum_id,
4191                            variant_index: variant_index as u32,
4192                            fields_start,
4193                            fields_len,
4194                        },
4195                        ty: enum_type,
4196                        span,
4197                    });
4198                    return Ok(AnalysisResult::new(air_ref, enum_type));
4199                }
4200                // Unit variant called as a function: fall through to the error path.
4201            }
4202
4203            // Not a variant — look for an associated function on this named enum (ADR-0053).
4204            let method_key = (enum_id, function);
4205            if let Some(method_info) = self.enum_methods.get(&method_key).copied() {
4206                ctx.referenced_methods
4207                    .insert((StructId(enum_id.0), function));
4208
4209                if method_info.has_self {
4210                    return Err(CompileError::new(
4211                        ErrorKind::MethodCalledAsAssocFn {
4212                            type_name: type_name_str.clone(),
4213                            method_name: function_name_str.clone(),
4214                        },
4215                        span,
4216                    ));
4217                }
4218
4219                let method_param_types: Vec<Type> =
4220                    self.param_arena.types(method_info.params).to_vec();
4221                if args.len() != method_param_types.len() {
4222                    return Err(CompileError::new(
4223                        ErrorKind::WrongArgumentCount {
4224                            expected: method_param_types.len(),
4225                            found: args.len(),
4226                        },
4227                        span,
4228                    ));
4229                }
4230
4231                let mut extra_data = Vec::with_capacity(args.len() * 2);
4232                for (i, arg) in args.iter().enumerate() {
4233                    let result = self.analyze_inst(air, arg.value, ctx)?;
4234                    if result.ty != method_param_types[i] {
4235                        return Err(CompileError::type_mismatch(
4236                            method_param_types[i].name().to_string(),
4237                            result.ty.name().to_string(),
4238                            span,
4239                        ));
4240                    }
4241                    extra_data.push(result.air_ref.as_u32());
4242                    extra_data.push(AirArgMode::Normal.as_u32());
4243                }
4244
4245                let enum_def = self.type_pool.enum_def(enum_id);
4246                let full_name = format!("{}::{}", enum_def.name, function_name_str);
4247                let callee_sym = self.interner.get_or_intern(&full_name);
4248
4249                let args_start = air.add_extra(&extra_data);
4250                let args_len = args.len() as u32;
4251                let air_ref = air.add_inst(AirInst {
4252                    data: AirInstData::Call {
4253                        name: callee_sym,
4254                        args_start,
4255                        args_len,
4256                    },
4257                    ty: method_info.return_type,
4258                    span,
4259                });
4260                return Ok(AnalysisResult::new(air_ref, method_info.return_type));
4261            }
4262        }
4263
4264        // ADR-0066: Vec(T) static method calls via comptime type variable
4265        // (e.g., `let V = Vec(i32); V::new()`).
4266        if let Some(&ty) = ctx.comptime_type_vars.get(&type_name)
4267            && matches!(ty.kind(), TypeKind::Vec(_))
4268            && let Some(result) =
4269                self.try_dispatch_vec_static_call(air, ctx, ty, &function_name_str, &args, span)
4270        {
4271            return result;
4272        }
4273
4274        // Check if this is an enum data variant construction via a comptime type variable
4275        // (e.g., `let Opt = Option(i32); Opt::Some(42)`)
4276        if let Some(&ty) = ctx.comptime_type_vars.get(&type_name)
4277            && let TypeKind::Enum(enum_id) = ty.kind()
4278        {
4279            let enum_def = self.type_pool.enum_def(enum_id);
4280            if let Some(variant_index) = enum_def.find_variant(&function_name_str) {
4281                let variant_def = &enum_def.variants[variant_index];
4282                let field_types: Vec<Type> = variant_def.fields.clone();
4283                if !field_types.is_empty() {
4284                    if variant_def.is_struct_variant() {
4285                        return Err(CompileError::type_mismatch(
4286                            format!(
4287                                "struct-style construction `{}::{} {{ ... }}`",
4288                                type_name_str, function_name_str
4289                            ),
4290                            format!(
4291                                "tuple-style construction `{}::{}(...)`",
4292                                type_name_str, function_name_str
4293                            ),
4294                            span,
4295                        ));
4296                    }
4297                    if args.len() != field_types.len() {
4298                        return Err(CompileError::new(
4299                            ErrorKind::WrongArgumentCount {
4300                                expected: field_types.len(),
4301                                found: args.len(),
4302                            },
4303                            span,
4304                        ));
4305                    }
4306                    let mut field_air_refs = Vec::with_capacity(args.len());
4307                    for (i, arg) in args.iter().enumerate() {
4308                        let result = self.analyze_inst(air, arg.value, ctx)?;
4309                        if result.ty != field_types[i] {
4310                            return Err(CompileError::type_mismatch(
4311                                field_types[i].name().to_string(),
4312                                result.ty.name().to_string(),
4313                                span,
4314                            ));
4315                        }
4316                        field_air_refs.push(result.air_ref.as_u32());
4317                    }
4318                    let fields_len = field_air_refs.len() as u32;
4319                    let fields_start = air.add_extra(&field_air_refs);
4320                    let enum_type = Type::new_enum(enum_id);
4321                    let air_ref = air.add_inst(AirInst {
4322                        data: AirInstData::EnumCreate {
4323                            enum_id,
4324                            variant_index: variant_index as u32,
4325                            fields_start,
4326                            fields_len,
4327                        },
4328                        ty: enum_type,
4329                        span,
4330                    });
4331                    return Ok(AnalysisResult::new(air_ref, enum_type));
4332                }
4333                // Unit variant called as function — fall through to error
4334            }
4335
4336            // Not a variant — check for associated function on the enum
4337            let method_key = (enum_id, function);
4338            if let Some(method_info) = self.enum_methods.get(&method_key).copied() {
4339                ctx.referenced_methods
4340                    .insert((StructId(enum_id.0), function));
4341
4342                if method_info.has_self {
4343                    return Err(CompileError::new(
4344                        ErrorKind::MethodCalledAsAssocFn {
4345                            type_name: type_name_str,
4346                            method_name: function_name_str,
4347                        },
4348                        span,
4349                    ));
4350                }
4351
4352                let method_param_types: Vec<Type> =
4353                    self.param_arena.types(method_info.params).to_vec();
4354                if args.len() != method_param_types.len() {
4355                    return Err(CompileError::new(
4356                        ErrorKind::WrongArgumentCount {
4357                            expected: method_param_types.len(),
4358                            found: args.len(),
4359                        },
4360                        span,
4361                    ));
4362                }
4363
4364                let mut extra_data = Vec::with_capacity(args.len() * 2);
4365                for (i, arg) in args.iter().enumerate() {
4366                    let result = self.analyze_inst(air, arg.value, ctx)?;
4367                    if result.ty != method_param_types[i] {
4368                        return Err(CompileError::type_mismatch(
4369                            method_param_types[i].name().to_string(),
4370                            result.ty.name().to_string(),
4371                            span,
4372                        ));
4373                    }
4374                    extra_data.push(result.air_ref.as_u32());
4375                    extra_data.push(AirArgMode::Normal.as_u32());
4376                }
4377
4378                let enum_def = self.type_pool.enum_def(enum_id);
4379                let type_name_str2 = enum_def.name.clone();
4380                let full_name = format!("{}::{}", type_name_str2, function_name_str);
4381                let callee_sym = self.interner.get_or_intern(&full_name);
4382
4383                let args_start = air.add_extra(&extra_data);
4384                let args_len = args.len() as u32;
4385                let air_ref = air.add_inst(AirInst {
4386                    data: AirInstData::Call {
4387                        name: callee_sym,
4388                        args_start,
4389                        args_len,
4390                    },
4391                    ty: method_info.return_type,
4392                    span,
4393                });
4394                return Ok(AnalysisResult::new(air_ref, method_info.return_type));
4395            }
4396        }
4397
4398        // Check that the type exists and is a struct
4399        // First check if it's a comptime type variable (e.g., `let P = Point(); P::origin()`)
4400        let struct_id = if let Some(&ty) = ctx.comptime_type_vars.get(&type_name) {
4401            // Extract struct ID from the comptime type
4402            match ty.kind() {
4403                TypeKind::Struct(id) => id,
4404                _ => {
4405                    return Err(CompileError::type_mismatch(
4406                        "struct type".to_string(),
4407                        ty.name().to_string(),
4408                        span,
4409                    ));
4410                }
4411            }
4412        } else {
4413            *self
4414                .structs
4415                .get(&type_name)
4416                .ok_or_compile_error(ErrorKind::UnknownType(type_name_str.clone()), span)?
4417        };
4418
4419        self.analyze_struct_assoc_fn_call(
4420            air,
4421            struct_id,
4422            type_name_str,
4423            function,
4424            function_name_str,
4425            args,
4426            span,
4427            ctx,
4428        )
4429    }
4430
4431    /// Dispatch a `Type::method(args)` call once the receiver type has
4432    /// been resolved to a `StructId`. Shared by:
4433    ///
4434    /// - The named-struct path (`MyStruct::method(...)`).
4435    /// - The user-defined parameterized type path (`MyStruct(T)::method(...)`),
4436    ///   where the LHS is comptime-evaluated to a synthetic struct.
4437    #[allow(clippy::too_many_arguments)]
4438    pub(crate) fn analyze_struct_assoc_fn_call(
4439        &mut self,
4440        air: &mut Air,
4441        struct_id: StructId,
4442        type_name_str: String,
4443        function: Spur,
4444        function_name_str: String,
4445        args: Vec<RirCallArg>,
4446        span: Span,
4447        ctx: &mut AnalysisContext,
4448    ) -> CompileResult<AnalysisResult> {
4449        // ADR-0081: `String::from_utf8` / `String::from_c_str` are now
4450        // regular associated functions on the prelude struct; they reach
4451        // the user-method lookup below alongside any other static method.
4452        // The previous registry-driven path retired with `STRING_TYPE`.
4453
4454        // Look up the function using StructId
4455        let method_key = (struct_id, function);
4456        let method_info = self.methods.get(&method_key).ok_or_compile_error(
4457            ErrorKind::UndefinedAssocFn {
4458                type_name: type_name_str.clone(),
4459                function_name: function_name_str.clone(),
4460            },
4461            span,
4462        )?;
4463
4464        // Track this associated function/method as referenced (for lazy analysis)
4465        ctx.referenced_methods.insert(method_key);
4466
4467        // Check that this is an associated function (no self), not a method
4468        if method_info.has_self {
4469            return Err(CompileError::new(
4470                ErrorKind::MethodCalledAsAssocFn {
4471                    type_name: type_name_str,
4472                    method_name: function_name_str,
4473                },
4474                span,
4475            ));
4476        }
4477
4478        // ADR-0073: gated cross-module visibility check.
4479        let method_is_pub = method_info.is_pub;
4480        let method_file_id = method_info.file_id;
4481        let struct_def = self.type_pool.struct_def(struct_id);
4482        self.check_method_visibility(
4483            &struct_def.name,
4484            struct_def.is_builtin,
4485            method_is_pub,
4486            method_file_id,
4487            &function_name_str,
4488            span,
4489        )?;
4490
4491        // ADR-0088: the previous ADR-0072 by-name gate
4492        // (`check_string_vec_bridge_method_gates`) retired in favour of
4493        // the per-method `@mark(unchecked)` directive carried by the
4494        // prelude declarations themselves. The unchecked gate fires via
4495        // the standard `MethodInfo::is_unchecked` check below.
4496
4497        // Check if calling an unchecked associated function requires a checked block
4498        if method_info.is_unchecked && ctx.checked_depth == 0 {
4499            return Err(CompileError::new(
4500                ErrorKind::UncheckedCallRequiresChecked(format!(
4501                    "{}::{}",
4502                    type_name_str, function_name_str
4503                )),
4504                span,
4505            ));
4506        }
4507
4508        // Check argument count
4509        let method_param_types = self.param_arena.types(method_info.params);
4510        if args.len() != method_param_types.len() {
4511            return Err(CompileError::new(
4512                ErrorKind::WrongArgumentCount {
4513                    expected: method_param_types.len(),
4514                    found: args.len(),
4515                },
4516                span,
4517            ));
4518        }
4519
4520        // Check for exclusive access violation
4521        self.check_exclusive_access(&args, span)?;
4522
4523        // Clone data needed before mutable borrow
4524        let return_type = method_info.return_type;
4525
4526        // Analyze arguments
4527        let air_args = self.analyze_call_args(air, &args, ctx)?;
4528
4529        // Generate a function call name: Type::function
4530        // Use the internal struct name (e.g., "__anon_struct_0") for anonymous structs,
4531        // not the user-visible type variable name (e.g., "P")
4532        let struct_def = self.type_pool.struct_def(struct_id);
4533        let internal_type_name = &struct_def.name;
4534        let call_name = format!("{}::{}", internal_type_name, function_name_str);
4535        let call_name_sym = self.interner.get_or_intern(&call_name);
4536
4537        // Encode call args into extra array
4538        let args_len = air_args.len() as u32;
4539        let mut extra_data = Vec::with_capacity(air_args.len() * 2);
4540        for arg in &air_args {
4541            extra_data.push(arg.value.as_u32());
4542            extra_data.push(arg.mode.as_u32());
4543        }
4544        let args_start = air.add_extra(&extra_data);
4545
4546        let air_ref = air.add_inst(AirInst {
4547            data: AirInstData::Call {
4548                name: call_name_sym,
4549                args_start,
4550                args_len,
4551            },
4552            ty: return_type,
4553            span,
4554        });
4555        Ok(AnalysisResult::new(air_ref, return_type))
4556    }
4557
4558    // Note: The old analyze_inst body from here onwards is now handled by the
4559    // dispatcher above and the category methods in analyze_ops.rs
4560
4561    // ========================================================================
4562    // Helper methods for analysis
4563    // ========================================================================
4564
4565    /// Analyze a binary arithmetic operator (+, -, *, /, %).
4566    ///
4567    /// Follows Rust's type inference rules:
4568    /// Types are determined by HM inference. Both operands must have the same type.
4569    fn analyze_binary_arith(
4570        &mut self,
4571        air: &mut Air,
4572        lhs: InstRef,
4573        rhs: InstRef,
4574        op: BinOp,
4575        span: Span,
4576        ctx: &mut AnalysisContext,
4577    ) -> CompileResult<AnalysisResult> {
4578        let lhs_result = self.analyze_inst(air, lhs, ctx)?;
4579        let rhs_result = self.analyze_inst(air, rhs, ctx)?;
4580
4581        // Verify the type is numeric (HM should have enforced this, but check anyway)
4582        if !lhs_result.ty.is_numeric() && !lhs_result.ty.is_error() && !lhs_result.ty.is_never() {
4583            return Err(CompileError::type_mismatch(
4584                "numeric type".to_string(),
4585                lhs_result.ty.name().to_string(),
4586                span,
4587            ));
4588        }
4589
4590        let air_ref = air.add_inst(AirInst {
4591            data: AirInstData::Bin(op, lhs_result.air_ref, rhs_result.air_ref),
4592            ty: lhs_result.ty,
4593            span,
4594        });
4595        Ok(AnalysisResult::new(air_ref, lhs_result.ty))
4596    }
4597
4598    /// Analyze a comparison operator.
4599    ///
4600    /// Types are determined by HM inference. Both operands must have the same type.
4601    ///
4602    /// For equality operators (`==`, `!=`), both integers and booleans are allowed.
4603    /// For ordering operators (`<`, `>`, `<=`, `>=`), only integers are allowed.
4604    fn analyze_comparison(
4605        &mut self,
4606        air: &mut Air,
4607        (lhs, rhs): (InstRef, InstRef),
4608        allow_bool: bool,
4609        op: BinOp,
4610        span: Span,
4611        ctx: &mut AnalysisContext,
4612    ) -> CompileResult<AnalysisResult> {
4613        // Check for chained comparisons (e.g., `a < b < c`)
4614        // Since the parser is left-associative, `a < b < c` parses as `(a < b) < c`,
4615        // so we only need to check if the LHS is a comparison.
4616        if self.is_comparison(lhs) {
4617            return Err(CompileError::new(ErrorKind::ChainedComparison, span)
4618                .with_help("use `&&` to combine comparisons: `a < b && b < c`"));
4619        }
4620
4621        // Comparisons read values without consuming them (like projections).
4622        // This matches Rust's PartialEq trait which takes references.
4623        let lhs_result = self.analyze_inst_for_projection(air, lhs, ctx)?;
4624        let lhs_type = lhs_result.ty;
4625
4626        // Propagate Never/Error without additional type errors. Analyze rhs
4627        // as projection too and emit the regular `Bin` path; downstream
4628        // type checking is suppressed by the never/error propagation.
4629        if lhs_type.is_never() || lhs_type.is_error() {
4630            let rhs_result = self.analyze_inst_for_projection(air, rhs, ctx)?;
4631            let air_ref = air.add_inst(AirInst {
4632                data: AirInstData::Bin(op, lhs_result.air_ref, rhs_result.air_ref),
4633                ty: Type::BOOL,
4634                span,
4635            });
4636            return Ok(AnalysisResult::new(air_ref, Type::BOOL));
4637        }
4638
4639        // ADR-0081 Phase 1: route `Vec(T) ==` / `<` to the Vec-method
4640        // dispatch path (`vec_eq` / `vec_cmp` intrinsics). The Vec receiver
4641        // isn't a user-declared struct, so the regular `lookup_user_method`
4642        // path below would miss it; we recognize it explicitly here and
4643        // reuse `finish_operator_dispatch` for the Ne / Lt / Le / Gt / Ge
4644        // wrapping. Requires `T: Copy` (enforced inside the dispatch).
4645        if matches!(lhs_type.kind(), TypeKind::Vec(_)) {
4646            let method_name = if matches!(op, BinOp::Eq | BinOp::Ne) {
4647                "eq"
4648            } else {
4649                "cmp"
4650            };
4651            // Pass the rhs through as a normal RIR call argument — the
4652            // dispatch arm will project it (no move) and verify the type.
4653            let dispatch_result = self.dispatch_vec_method_call(
4654                air,
4655                lhs_result,
4656                method_name,
4657                &[RirCallArg {
4658                    value: rhs,
4659                    mode: RirArgMode::Normal,
4660                }],
4661                span,
4662                ctx,
4663            )?;
4664            return self.finish_operator_dispatch(
4665                air,
4666                op,
4667                method_name,
4668                dispatch_result.air_ref,
4669                dispatch_result.ty,
4670                span,
4671            );
4672        }
4673
4674        // ADR-0078 Phase 4: operator desugaring for non-primitive types.
4675        //
4676        // Dispatch order:
4677        //   1. Numeric / bool / char / unit primitives — fall through to the
4678        //      regular `Bin` path (existing behavior).
4679        //   2. User struct or enum with an `eq` (for `==` / `!=`) or `cmp`
4680        //      (for `<` / `<=` / `>` / `>=`) method — desugar to a method
4681        //      call. The conformer's signature must match `Eq::eq` /
4682        //      `Ord::cmp` from `prelude/cmp.gruel`. ADR-0081: the prelude
4683        //      `String` flows through this path; its `eq` / `cmp` methods
4684        //      delegate to `Vec(u8)` byte comparisons.
4685        //
4686        // This is the load-bearing piece of ADR-0078 Phase 4 — it's what
4687        // makes the `Eq` / `Ord` interfaces useful as overloading hooks.
4688        if !lhs_type.is_numeric()
4689            && lhs_type != Type::BOOL
4690            && lhs_type != Type::CHAR
4691            && lhs_type != Type::UNIT
4692        {
4693            // ADR-0079: read the method name out of the lang-item
4694            // interface declaration when available. The prelude-tagged
4695            // `Eq` / `Ord` interfaces each declare exactly one method,
4696            // so the first slot's name is the dispatch target. Falls
4697            // back to the historical hardcoded `"eq"` / `"cmp"` when
4698            // the lang item isn't bound (e.g. test fixtures that bypass
4699            // the prelude).
4700            let lang_iface_id = if matches!(op, BinOp::Eq | BinOp::Ne) {
4701                self.lang_items.op_eq()
4702            } else {
4703                self.lang_items.op_cmp()
4704            };
4705            let method_name_owned: String = lang_iface_id
4706                .and_then(|id| {
4707                    self.interface_defs[id.0 as usize]
4708                        .methods
4709                        .first()
4710                        .map(|m| m.name.clone())
4711                })
4712                .unwrap_or_else(|| {
4713                    if matches!(op, BinOp::Eq | BinOp::Ne) {
4714                        "eq".to_string()
4715                    } else {
4716                        "cmp".to_string()
4717                    }
4718                });
4719            let method_name: &str = &method_name_owned;
4720            let method_sym = self.interner.get(method_name);
4721            if let Some(method_sym) = method_sym
4722                && let Some(method_info) = self.lookup_user_method(lhs_type, method_sym)
4723            {
4724                let recv_pass_mode = match method_info.receiver {
4725                    crate::types::ReceiverMode::ByValue => AirArgMode::Normal,
4726                    crate::types::ReceiverMode::Ref => AirArgMode::Ref,
4727                    crate::types::ReceiverMode::MutRef => AirArgMode::MutRef,
4728                };
4729                let return_type = method_info.return_type;
4730                let type_name = self.format_type_name(lhs_type);
4731
4732                // ADR-0026 lazy analysis: register the dispatched method so
4733                // its body is analyzed and emitted by the work queue. The
4734                // regular method-call path does this via
4735                // `ctx.referenced_methods.insert(method_key)`; mirror that
4736                // here so prelude / user-defined `eq` / `cmp` aren't dropped
4737                // from codegen.
4738                if let TypeKind::Struct(struct_id) = lhs_type.kind() {
4739                    ctx.referenced_methods.insert((struct_id, method_sym));
4740                } else if let TypeKind::Enum(enum_id) = lhs_type.kind() {
4741                    ctx.referenced_methods
4742                        .insert((crate::types::StructId(enum_id.0), method_sym));
4743                }
4744
4745                // Analyze rhs through the regular call-arg path so it gets
4746                // proper move tracking against the method's `other: Self`
4747                // parameter. Borrow-on-projection of lhs above stays as-is.
4748                let rhs_args = self.analyze_call_args(
4749                    air,
4750                    &[RirCallArg {
4751                        value: rhs,
4752                        mode: RirArgMode::Normal,
4753                    }],
4754                    ctx,
4755                )?;
4756                let rhs_air_ref = rhs_args[0].value;
4757                let rhs_type = air.get(rhs_air_ref).ty;
4758                if rhs_type != lhs_type && !rhs_type.is_never() && !rhs_type.is_error() {
4759                    return Err(CompileError::type_mismatch(
4760                        type_name.clone(),
4761                        rhs_type.name().to_string(),
4762                        self.rir.get(rhs).span,
4763                    ));
4764                }
4765
4766                let mut air_args = vec![AirCallArg {
4767                    value: lhs_result.air_ref,
4768                    mode: recv_pass_mode,
4769                }];
4770                air_args.extend(rhs_args);
4771
4772                let call_name_str = format!("{}.{}", type_name, method_name);
4773                let call_name_sym = self.interner.get_or_intern(&call_name_str);
4774
4775                let mut extra_data = Vec::with_capacity(air_args.len() * 2);
4776                for arg in &air_args {
4777                    extra_data.push(arg.value.as_u32());
4778                    extra_data.push(arg.mode.as_u32());
4779                }
4780                let args_start = air.add_extra(&extra_data);
4781
4782                let call_air_ref = air.add_inst(AirInst {
4783                    data: AirInstData::Call {
4784                        name: call_name_sym,
4785                        args_start,
4786                        args_len: air_args.len() as u32,
4787                    },
4788                    ty: return_type,
4789                    span,
4790                });
4791
4792                return self.finish_operator_dispatch(
4793                    air,
4794                    op,
4795                    method_name,
4796                    call_air_ref,
4797                    return_type,
4798                    span,
4799                );
4800            }
4801            // No `eq` / `cmp` method on this type. For ordering ops, emit a
4802            // helpful error naming `Ord`. For equality, fall through to the
4803            // existing path: structs get bitwise equality via
4804            // `build_value_eq`; types that aren't structs get rejected by
4805            // the type-validation check below.
4806            if !allow_bool {
4807                let type_name = self.format_type_name(lhs_type);
4808                return Err(CompileError::new(
4809                    ErrorKind::TypeMismatch {
4810                        expected:
4811                            "a type that conforms to `Ord` (with `fn cmp(self: Ref(Self), other: Self) -> Ordering`)"
4812                                .to_string(),
4813                        found: type_name,
4814                    },
4815                    self.rir.get(lhs).span,
4816                )
4817                .with_help(format!(
4818                    "implement `fn cmp(self: Ref(Self), other: Self) -> Ordering` on the type to enable `{}`",
4819                    op.symbol()
4820                )));
4821            }
4822        }
4823
4824        // Fall-through: analyze rhs and emit the regular `Bin` instruction.
4825        let rhs_result = self.analyze_inst_for_projection(air, rhs, ctx)?;
4826
4827        // Validate the type is appropriate for this comparison
4828        if allow_bool {
4829            // Equality operators (==, !=) work on integers, floats, booleans, chars,
4830            // unit, and structs. (ADR-0071: char comparison is by codepoint
4831            // value.) ADR-0081: String is a regular struct in the prelude,
4832            // covered by `is_struct()`.
4833            if !lhs_type.is_numeric()
4834                && lhs_type != Type::BOOL
4835                && lhs_type != Type::CHAR
4836                && lhs_type != Type::UNIT
4837                && !lhs_type.is_struct()
4838            {
4839                return Err(CompileError::type_mismatch(
4840                    "numeric, bool, char, unit, or struct".to_string(),
4841                    lhs_type.name().to_string(),
4842                    self.rir.get(lhs).span,
4843                ));
4844            }
4845        } else if !lhs_type.is_numeric() && lhs_type != Type::CHAR {
4846            return Err(CompileError::type_mismatch(
4847                "numeric or char".to_string(),
4848                lhs_type.name().to_string(),
4849                self.rir.get(lhs).span,
4850            ));
4851        }
4852
4853        let air_ref = air.add_inst(AirInst {
4854            data: AirInstData::Bin(op, lhs_result.air_ref, rhs_result.air_ref),
4855            ty: Type::BOOL,
4856            span,
4857        });
4858        Ok(AnalysisResult::new(air_ref, Type::BOOL))
4859    }
4860
4861    /// ADR-0078 Phase 4: look up a user-defined method by name on a struct
4862    /// or enum type. Returns the method info if found, regardless of
4863    /// signature — the caller's responsibility to validate the shape.
4864    fn lookup_user_method(&self, ty: Type, method_sym: Spur) -> Option<MethodInfo> {
4865        match ty.kind() {
4866            TypeKind::Struct(struct_id) => self.methods.get(&(struct_id, method_sym)).cloned(),
4867            TypeKind::Enum(enum_id) => self.enum_methods.get(&(enum_id, method_sym)).cloned(),
4868            _ => None,
4869        }
4870    }
4871
4872    /// ADR-0078 Phase 4: finish operator-dispatch lowering after the
4873    /// dispatched method call has been emitted. For `==` / `!=`, the call
4874    /// returned a `bool`; `!=` wraps in `Bin(Ne, call, true)`. For
4875    /// `<` / `<=` / `>` / `>=`, the call returned an `Ordering`; build a
4876    /// comparison against `Ordering::Less` or `Ordering::Greater`.
4877    fn finish_operator_dispatch(
4878        &mut self,
4879        air: &mut Air,
4880        op: BinOp,
4881        method_name: &str,
4882        call_air_ref: AirRef,
4883        return_type: Type,
4884        span: Span,
4885    ) -> CompileResult<AnalysisResult> {
4886        match op {
4887            BinOp::Eq => {
4888                if return_type != Type::BOOL {
4889                    return Err(CompileError::type_mismatch(
4890                        "bool".to_string(),
4891                        return_type.name().to_string(),
4892                        span,
4893                    )
4894                    .with_help(format!(
4895                        "`fn {}(...) -> bool` is required for `Eq` conformance",
4896                        method_name
4897                    )));
4898                }
4899                Ok(AnalysisResult::new(call_air_ref, Type::BOOL))
4900            }
4901            BinOp::Ne => {
4902                if return_type != Type::BOOL {
4903                    return Err(CompileError::type_mismatch(
4904                        "bool".to_string(),
4905                        return_type.name().to_string(),
4906                        span,
4907                    )
4908                    .with_help(format!(
4909                        "`fn {}(...) -> bool` is required for `Eq` conformance",
4910                        method_name
4911                    )));
4912                }
4913                let true_ref = air.add_inst(AirInst {
4914                    data: AirInstData::BoolConst(true),
4915                    ty: Type::BOOL,
4916                    span,
4917                });
4918                let result = air.add_inst(AirInst {
4919                    data: AirInstData::Bin(BinOp::Ne, call_air_ref, true_ref),
4920                    ty: Type::BOOL,
4921                    span,
4922                });
4923                Ok(AnalysisResult::new(result, Type::BOOL))
4924            }
4925            BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
4926                // ADR-0079: prefer the lang-item binding; fall back to
4927                // the legacy name cache for compilations that bypass
4928                // the prelude entirely.
4929                let ordering_id = self
4930                    .lang_items
4931                    .ordering()
4932                    .or(self.builtin_ordering_id)
4933                    .ok_or_else(|| {
4934                        CompileError::new(
4935                            ErrorKind::InternalError(
4936                                "Ordering enum not found (prelude not loaded?)".into(),
4937                            ),
4938                            span,
4939                        )
4940                    })?;
4941                let expected_ty = Type::new_enum(ordering_id);
4942                if return_type != expected_ty {
4943                    return Err(CompileError::type_mismatch(
4944                        "Ordering".to_string(),
4945                        return_type.name().to_string(),
4946                        span,
4947                    )
4948                    .with_help(format!(
4949                        "`fn {}(...) -> Ordering` is required for `Ord` conformance",
4950                        method_name
4951                    )));
4952                }
4953                // Variant indices match `prelude/cmp.gruel`:
4954                // Less = 0, Equal = 1, Greater = 2.
4955                let (variant_index, cmp_op) = match op {
4956                    BinOp::Lt => (0u32, BinOp::Eq), // result == Less
4957                    BinOp::Ge => (0u32, BinOp::Ne), // result != Less
4958                    BinOp::Gt => (2u32, BinOp::Eq), // result == Greater
4959                    BinOp::Le => (2u32, BinOp::Ne), // result != Greater
4960                    _ => unreachable!(),
4961                };
4962                let variant_air = air.add_inst(AirInst {
4963                    data: AirInstData::EnumVariant {
4964                        enum_id: ordering_id,
4965                        variant_index,
4966                    },
4967                    ty: expected_ty,
4968                    span,
4969                });
4970                let result = air.add_inst(AirInst {
4971                    data: AirInstData::Bin(cmp_op, call_air_ref, variant_air),
4972                    ty: Type::BOOL,
4973                    span,
4974                });
4975                Ok(AnalysisResult::new(result, Type::BOOL))
4976            }
4977            _ => unreachable!("operator dispatch only handles comparison ops"),
4978        }
4979    }
4980
4981    /// Check if an RIR instruction is a comparison operation.
4982    ///
4983    /// This is used to detect chained comparisons (e.g., `a < b < c`) which are
4984    /// not allowed in Gruel.
4985    fn is_comparison(&self, inst_ref: InstRef) -> bool {
4986        matches!(
4987            self.rir.get(inst_ref).data,
4988            InstData::Bin {
4989                op: BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge | BinOp::Eq | BinOp::Ne,
4990                ..
4991            }
4992        )
4993    }
4994
4995    /// Check if directives contain @allow for a specific warning name.
4996    pub(crate) fn has_allow_directive(
4997        &self,
4998        directives: &[RirDirective],
4999        warning_name: &str,
5000    ) -> bool {
5001        let allow_sym = self.interner.get("allow");
5002        let warning_sym = self.interner.get(warning_name);
5003
5004        for directive in directives {
5005            if Some(directive.name) == allow_sym {
5006                for arg in &directive.args {
5007                    if Some(*arg) == warning_sym {
5008                        return true;
5009                    }
5010                }
5011            }
5012        }
5013        false
5014    }
5015
5016    /// Check for unused local variables in the current scope (before popping it).
5017    /// Uses the scope stack to determine which variables were added in the current scope.
5018    pub(crate) fn check_unused_locals_in_current_scope(&self, ctx: &mut AnalysisContext) {
5019        // Get the current scope entries (variables added in this scope)
5020        let Some(current_scope) = ctx.scope_stack.last() else {
5021            return;
5022        };
5023
5024        for (symbol, _old_value) in current_scope {
5025            // Skip if variable was used
5026            if ctx.used_locals.contains(symbol) {
5027                continue;
5028            }
5029
5030            // Get the local var info (it should still be in ctx.locals before pop)
5031            let Some(local) = ctx.locals.get(symbol) else {
5032                continue;
5033            };
5034
5035            // Get variable name
5036            let name = self.interner.resolve(symbol);
5037
5038            // Skip variables starting with underscore (convention for intentionally unused)
5039            if name.starts_with('_') {
5040                continue;
5041            }
5042
5043            // Skip if @allow(unused_variable) was applied
5044            if local.allow_unused {
5045                continue;
5046            }
5047
5048            // Emit warning with help suggestion (to ctx.warnings for parallel safety)
5049            ctx.warnings.push(
5050                CompileWarning::new(WarningKind::UnusedVariable(name.to_string()), local.span)
5051                    .with_help(format!(
5052                        "if this is intentional, prefix it with an underscore: `_{}`",
5053                        name
5054                    )),
5055            );
5056        }
5057    }
5058
5059    /// Check for unconsumed linear values in the current scope (before popping it).
5060    /// Linear values MUST be consumed (moved) - it's an error to let them drop implicitly.
5061    /// Returns an error if any linear value was not consumed.
5062    pub(crate) fn check_unconsumed_linear_values(
5063        &self,
5064        ctx: &AnalysisContext,
5065    ) -> CompileResult<()> {
5066        // Get the current scope entries (variables added in this scope)
5067        let Some(current_scope) = ctx.scope_stack.last() else {
5068            return Ok(());
5069        };
5070
5071        for (symbol, _old_value) in current_scope {
5072            // Get the local var info (it should still be in ctx.locals before pop)
5073            let Some(local) = ctx.locals.get(symbol) else {
5074                continue;
5075            };
5076
5077            // Only check linear types
5078            if !self.is_type_linear(local.ty) {
5079                continue;
5080            }
5081
5082            // Check if this variable was moved (consumed)
5083            let was_consumed = ctx
5084                .moved_vars
5085                .get(symbol)
5086                .is_some_and(|state| state.full_move.is_some());
5087
5088            if !was_consumed {
5089                let name = self.interner.resolve(symbol);
5090                return Err(CompileError::new(
5091                    ErrorKind::LinearValueNotConsumed(name.to_string()),
5092                    local.span,
5093                ));
5094            }
5095        }
5096
5097        Ok(())
5098    }
5099
5100    /// Extract the root variable symbol from an expression, if it refers to a variable.
5101    ///
5102    /// For inout arguments, we need to track which variable is being passed to detect
5103    /// when the same variable is passed to multiple inout parameters.
5104    ///
5105    /// Returns Some(symbol) for:
5106    /// - VarRef { name } -> the variable symbol
5107    /// - ParamRef { name, .. } -> the parameter symbol
5108    /// - FieldGet { base, .. } -> recursively extract from base
5109    /// - IndexGet { base, .. } -> recursively extract from base
5110    ///
5111    /// Returns None for expressions that don't refer to a variable (literals, calls, etc.)
5112    pub(crate) fn extract_root_variable(&self, inst_ref: InstRef) -> Option<Spur> {
5113        let inst = self.rir.get(inst_ref);
5114        match &inst.data {
5115            InstData::VarRef { name } => Some(*name),
5116            InstData::ParamRef { name, .. } => Some(*name),
5117            InstData::FieldGet { base, .. } => self.extract_root_variable(*base),
5118            InstData::IndexGet { base, .. } => self.extract_root_variable(*base),
5119            _ => None,
5120        }
5121    }
5122
5123    /// Check exclusivity rules for inout and borrow parameters in a call.
5124    ///
5125    /// This enforces two rules:
5126    /// 1. Same variable cannot be passed to multiple inout parameters (prevents aliasing)
5127    /// 2. Same variable cannot be passed to both inout and borrow (law of exclusivity)
5128    ///
5129    /// The law of exclusivity: either one mutable (inout) access OR any number of
5130    /// immutable (borrow) accesses, never both simultaneously.
5131    pub(crate) fn check_exclusive_access(
5132        &self,
5133        args: &[RirCallArg],
5134        call_span: Span,
5135    ) -> CompileResult<()> {
5136        use rustc_hash::FxHashSet as HashSet;
5137        let mut inout_vars: HashSet<Spur> = HashSet::default();
5138        let mut borrow_vars: HashSet<Spur> = HashSet::default();
5139
5140        for arg in args {
5141            // Classify the borrow/inout shape of this arg, considering both
5142            // the legacy `borrow`/`inout` modes (ADR-0013) and the new `&x` /
5143            // `&mut x` MakeRef expressions (ADR-0062). Both produce the same
5144            // exclusivity obligations.
5145            let (is_inout_like, is_borrow_like) = self.classify_borrowing_arg(arg);
5146
5147            // Lvalue check for legacy modes is here; for MakeRef the lvalue
5148            // check happens in `analyze_make_ref`.
5149            if arg.mode == RirArgMode::MutRef {
5150                if self.extract_root_variable(arg.value).is_none() {
5151                    return Err(CompileError::new(
5152                        ErrorKind::InoutNonLvalue,
5153                        self.rir.get(arg.value).span,
5154                    ));
5155                }
5156            } else if arg.mode == RirArgMode::Ref && self.extract_root_variable(arg.value).is_none()
5157            {
5158                return Err(CompileError::new(
5159                    ErrorKind::BorrowNonLvalue,
5160                    self.rir.get(arg.value).span,
5161                ));
5162            }
5163
5164            let maybe_var_symbol = self.extract_borrowing_root_variable(arg.value);
5165
5166            if let Some(var_symbol) = maybe_var_symbol {
5167                if is_inout_like {
5168                    // Check for duplicate inout access
5169                    if !inout_vars.insert(var_symbol) {
5170                        let var_name = self.interner.resolve(&var_symbol).to_string();
5171                        return Err(CompileError::new(
5172                            ErrorKind::InoutExclusiveAccess { variable: var_name },
5173                            call_span,
5174                        ));
5175                    }
5176                    // Check for borrow/inout conflict
5177                    if borrow_vars.contains(&var_symbol) {
5178                        let var_name = self.interner.resolve(&var_symbol).to_string();
5179                        return Err(CompileError::new(
5180                            ErrorKind::BorrowInoutConflict { variable: var_name },
5181                            call_span,
5182                        ));
5183                    }
5184                } else if is_borrow_like {
5185                    borrow_vars.insert(var_symbol);
5186                    // Check for borrow/inout conflict
5187                    if inout_vars.contains(&var_symbol) {
5188                        let var_name = self.interner.resolve(&var_symbol).to_string();
5189                        return Err(CompileError::new(
5190                            ErrorKind::BorrowInoutConflict { variable: var_name },
5191                            call_span,
5192                        ));
5193                    }
5194                }
5195            }
5196        }
5197        Ok(())
5198    }
5199
5200    /// Classify a call argument's borrowing shape (ADR-0013 + ADR-0062).
5201    ///
5202    /// Returns `(is_inout_like, is_borrow_like)`. `inout`/`&mut x` count as
5203    /// inout-like; `borrow`/`&x` count as borrow-like. Normal-mode arguments
5204    /// where the value is not a MakeRef return `(false, false)`.
5205    pub(crate) fn classify_borrowing_arg(&self, arg: &RirCallArg) -> (bool, bool) {
5206        if arg.mode == RirArgMode::MutRef {
5207            return (true, false);
5208        }
5209        if arg.mode == RirArgMode::Ref {
5210            return (false, true);
5211        }
5212        if let InstData::MakeRef { is_mut, .. } = self.rir.get(arg.value).data {
5213            return (is_mut, !is_mut);
5214        }
5215        (false, false)
5216    }
5217
5218    /// Like `extract_root_variable`, but transparently descends through a
5219    /// `MakeRef` wrapper so that `&x` / `&mut x` resolve to the same root as
5220    /// `borrow x` / `inout x`.
5221    pub(crate) fn extract_borrowing_root_variable(&self, inst_ref: InstRef) -> Option<Spur> {
5222        if let InstData::MakeRef { operand, .. } = &self.rir.get(inst_ref).data {
5223            return self.extract_root_variable(*operand);
5224        }
5225        self.extract_root_variable(inst_ref)
5226    }
5227
5228    /// Analyze a list of call arguments, handling inout unmove logic.
5229    ///
5230    /// For inout arguments, the variable is "unmoving" after analysis - this is because
5231    /// inout is a mutable borrow, not a move. The value stays valid after the call.
5232    pub(crate) fn analyze_call_args(
5233        &mut self,
5234        air: &mut Air,
5235        args: &[RirCallArg],
5236        ctx: &mut AnalysisContext,
5237    ) -> CompileResult<Vec<AirCallArg>> {
5238        let mut air_args = Vec::new();
5239        for arg in args.iter() {
5240            // For inout/borrow arguments and `&x` / `&mut x` constructions
5241            // (ADR-0062), extract the underlying variable name so we can
5242            // "unmove" it after analysis — these are borrows, not moves.
5243            let (is_inout_like, is_borrow_like) = self.classify_borrowing_arg(arg);
5244            let borrowed_var = if is_inout_like || is_borrow_like {
5245                self.extract_borrowing_root_variable(arg.value)
5246            } else {
5247                None
5248            };
5249
5250            let arg_result = self.analyze_inst(air, arg.value, ctx)?;
5251
5252            // If this was an inout/borrow/ref argument, the variable shouldn't
5253            // be marked as moved — the value stays valid after the call.
5254            if let Some(var_symbol) = borrowed_var {
5255                ctx.moved_vars.remove(&var_symbol);
5256            }
5257
5258            air_args.push(AirCallArg {
5259                value: arg_result.air_ref,
5260                mode: AirArgMode::from(arg.mode),
5261            });
5262        }
5263        Ok(air_args)
5264    }
5265
5266    /// Register methods from an anonymous struct type with type substitution (comptime-safe).
5267    ///
5268    /// This variant supports comptime parameter capture by using `resolve_type_for_comptime_with_subst`
5269    /// to resolve type parameters like `T` to their concrete types from the enclosing function's
5270    /// comptime arguments.
5271    pub(super) fn register_anon_struct_methods_for_comptime_with_subst(
5272        &mut self,
5273        spec: AnonStructSpec,
5274        _span: Span,
5275        type_subst: &rustc_hash::FxHashMap<Spur, Type>,
5276        _value_subst: &rustc_hash::FxHashMap<Spur, ConstValue>,
5277    ) -> Option<()> {
5278        let AnonStructSpec {
5279            struct_id,
5280            struct_type,
5281            methods_start,
5282            methods_len,
5283        } = spec;
5284        let method_refs = self.rir.get_inst_refs(methods_start, methods_len);
5285
5286        let mut seen_methods: rustc_hash::FxHashSet<Spur> = rustc_hash::FxHashSet::default();
5287
5288        // ADR-0082: capture the outer comptime fn's type substitution
5289        // (e.g. `T → I32` for a `Vec(I32)` instance) so method body
5290        // analysis can resolve `T` references inside the body. Stored
5291        // per-struct and merged into the body analyzer's type_subst by
5292        // `analyze_method_body`. Empty subst is recorded too (cheap, and
5293        // makes lookup-without-result mean "don't carry anything").
5294        if !type_subst.is_empty() {
5295            let owned: rustc_hash::FxHashMap<Spur, Type> =
5296                type_subst.iter().map(|(k, v)| (*k, *v)).collect();
5297            self.anon_struct_type_subst.insert(struct_id, owned);
5298        }
5299
5300        // ADR-0076: bind `Self` to the anonymous struct's `Type` while
5301        // resolving method signatures.
5302        let saved_self = self.current_self.replace(struct_type);
5303
5304        for method_ref in method_refs {
5305            let method_inst = self.rir.get(method_ref);
5306            if let InstData::FnDecl {
5307                name: method_name,
5308                is_pub: method_is_pub,
5309                is_unchecked,
5310                params_start,
5311                params_len,
5312                return_type,
5313                body,
5314                has_self,
5315                receiver_mode,
5316                ..
5317            } = &method_inst.data
5318            {
5319                let receiver = crate::sema::anon_interfaces::decode_receiver_mode(*receiver_mode);
5320                let key = (struct_id, *method_name);
5321
5322                if seen_methods.contains(method_name) {
5323                    return None;
5324                }
5325                seen_methods.insert(*method_name);
5326
5327                if self.methods.contains_key(&key) {
5328                    return None;
5329                }
5330
5331                let params = self.rir.get_params(*params_start, *params_len);
5332                let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
5333                let param_modes: Vec<RirParamMode> = params.iter().map(|p| p.mode).collect();
5334                let param_comptime: Vec<bool> = params.iter().map(|p| p.is_comptime).collect();
5335                let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
5336
5337                // Method-level comptime type parameters (e.g. `comptime F: type`)
5338                // and any later param whose declared type is one of those names
5339                // (e.g. `f: F`) cannot be resolved here — their concrete types
5340                // are only known at the method's call site. Match the
5341                // top-level-generic-fn treatment in declarations.rs: store
5342                // COMPTIME_TYPE as a placeholder and let specialization fill
5343                // the concrete type in when the method is monomorphized.
5344                let type_sym = self.interner.get_or_intern("type");
5345                let method_type_param_names: Vec<Spur> = params
5346                    .iter()
5347                    .filter(|p| p.is_comptime && p.ty == type_sym)
5348                    .map(|p| p.name)
5349                    .collect();
5350
5351                // Build a "sentinel" map that assigns a concrete type to each
5352                // method-level type param, used to detect whether a given
5353                // type symbol references any of them (including through
5354                // array / pointer / tuple wrappers).
5355                let sentinel_subst: rustc_hash::FxHashMap<Spur, Type> = method_type_param_names
5356                    .iter()
5357                    .map(|&n| (n, Type::I32))
5358                    .collect();
5359                let references_method_type_param = |sema: &mut Self, ty_sym: Spur| -> bool {
5360                    if method_type_param_names.contains(&ty_sym) {
5361                        return true;
5362                    }
5363                    let with = sema.resolve_type_for_comptime_with_subst(ty_sym, &sentinel_subst);
5364                    // Also include the caller's type_subst so `T` from the
5365                    // outer generic still resolves in the "without" baseline.
5366                    let without = sema.resolve_type_for_comptime_with_subst(ty_sym, type_subst);
5367                    with.is_some() && without.is_none()
5368                };
5369
5370                for p in params {
5371                    let resolved_ty = if p.is_comptime && p.ty == type_sym {
5372                        // `comptime X: type` param — placeholder until call.
5373                        Type::COMPTIME_TYPE
5374                    } else if references_method_type_param(self, p.ty) {
5375                        // Declared type is (or contains) a method-level
5376                        // comptime type param.
5377                        Type::COMPTIME_TYPE
5378                    } else {
5379                        self.resolve_type_for_comptime_with_subst(p.ty, type_subst)?
5380                    };
5381                    param_types.push(resolved_ty);
5382                }
5383
5384                let ret_type = if references_method_type_param(self, *return_type) {
5385                    Type::COMPTIME_TYPE
5386                } else {
5387                    self.resolve_type_for_comptime_with_subst(*return_type, type_subst)?
5388                };
5389
5390                // Preserve mode and comptime flags so specialization can see
5391                // method-level comptime type parameters.
5392                let any_comptime_param = param_comptime.iter().any(|c| *c);
5393                let param_range =
5394                    self.param_arena
5395                        .alloc(param_names, param_types, param_modes, param_comptime);
5396
5397                self.methods.insert(
5398                    key,
5399                    MethodInfo {
5400                        struct_type,
5401                        has_self: *has_self,
5402                        receiver,
5403                        params: param_range,
5404                        return_type: ret_type,
5405                        body: *body,
5406                        span: method_inst.span,
5407                        is_unchecked: *is_unchecked,
5408                        // Any comptime parameter — type or value — makes the
5409                        // method generic so per-call specialization can bind
5410                        // the values for `comptime if`/`@compile_error`
5411                        // checks in the body. (Mirrors the top-level rule in
5412                        // `declarations.rs`.)
5413                        is_generic: any_comptime_param,
5414                        return_type_sym: *return_type,
5415                        is_pub: *method_is_pub,
5416                        file_id: method_inst.span.file_id,
5417                    },
5418                );
5419            }
5420        }
5421        self.current_self = saved_self;
5422        Some(())
5423    }
5424
5425    /// Register the single synthesized `__call` method on a lambda-origin
5426    /// anonymous struct (ADR-0055).
5427    ///
5428    /// Unlike `register_anon_struct_methods_for_comptime_with_subst`, we take
5429    /// Resolve a single comptime type argument supplied at a generic method
5430    /// call site (ADR-0055). Accepts: a `type` literal (e.g. `i32`), an
5431    /// anon-struct-type expression evaluated at comptime, a comptime type
5432    /// variable bound earlier in the function (`let W = Wrap(i32)`), or a
5433    /// bare struct/enum name.
5434    pub(crate) fn resolve_method_generic_type_arg(
5435        &mut self,
5436        arg: InstRef,
5437        param_name: Spur,
5438        ctx: &super::context::AnalysisContext,
5439    ) -> CompileResult<Type> {
5440        let arg_inst = self.rir.get(arg);
5441        match &arg_inst.data {
5442            InstData::TypeConst { type_name } => self.resolve_type(*type_name, arg_inst.span),
5443            InstData::AnonStructType { .. } => {
5444                let empty_type_subst: rustc_hash::FxHashMap<Spur, Type> =
5445                    rustc_hash::FxHashMap::default();
5446                let empty_value_subst: rustc_hash::FxHashMap<Spur, ConstValue> =
5447                    rustc_hash::FxHashMap::default();
5448                match self.try_evaluate_const_with_subst(arg, &empty_type_subst, &empty_value_subst)
5449                {
5450                    Some(ConstValue::Type(ty)) => Ok(ty),
5451                    _ => Err(CompileError::new(
5452                        ErrorKind::ComptimeEvaluationFailed {
5453                            reason: format!(
5454                                "method-generic argument for `{}` must be a type value",
5455                                self.interner.resolve(&param_name)
5456                            ),
5457                        },
5458                        arg_inst.span,
5459                    )),
5460                }
5461            }
5462            _ => {
5463                let resolved_ty = if let InstData::VarRef { name } = &arg_inst.data {
5464                    if let Some(&ty) = ctx.comptime_type_vars.get(name) {
5465                        Some(ty)
5466                    } else if let Some(&sid) = self.structs.get(name) {
5467                        Some(Type::new_struct(sid))
5468                    } else if let Some(&eid) = self.enums.get(name) {
5469                        Some(Type::new_enum(eid))
5470                    } else {
5471                        None
5472                    }
5473                } else {
5474                    None
5475                };
5476                resolved_ty.ok_or_else(|| {
5477                    CompileError::new(
5478                        ErrorKind::ComptimeEvaluationFailed {
5479                            reason: format!(
5480                                "method-generic argument for `{}` must be a type literal, \
5481                                 struct/enum name, or comptime type variable",
5482                                self.interner.resolve(&param_name)
5483                            ),
5484                        },
5485                        arg_inst.span,
5486                    )
5487                })
5488            }
5489        }
5490    }
5491
5492    /// Look up the declared parameter type symbols for a method body. Walks
5493    /// RIR to find the FnDecl whose `body` matches and returns its params'
5494    /// declared `ty` Spurs in order. Used by ADR-0055 comptime type-arg
5495    /// inference, which needs the as-written type names (e.g. `F`, `T`,
5496    /// `[U; 3]`) rather than the registered types (which are placeholders
5497    /// for method-level generics).
5498    pub(crate) fn method_param_type_syms(&self, method_body: InstRef) -> Option<Vec<Spur>> {
5499        for (_, inst) in self.rir.iter() {
5500            if let InstData::FnDecl {
5501                body,
5502                params_start,
5503                params_len,
5504                ..
5505            } = &inst.data
5506                && *body == method_body
5507            {
5508                let params = self.rir.get_params(*params_start, *params_len);
5509                return Some(params.iter().map(|p| p.ty).collect());
5510            }
5511        }
5512        None
5513    }
5514
5515    /// the method InstRef directly rather than reading it from the RIR extra
5516    /// array, and there is no comptime-parameter substitution to apply — the
5517    /// parent function's comptime substitutions have already baked into the
5518    /// method body during astgen.
5519    pub(crate) fn register_anon_fn_call_method(
5520        &mut self,
5521        struct_id: StructId,
5522        struct_type: Type,
5523        method_ref: InstRef,
5524        span: Span,
5525    ) -> CompileResult<()> {
5526        let method_inst = self.rir.get(method_ref);
5527        let (
5528            method_name,
5529            is_unchecked,
5530            params_start,
5531            params_len,
5532            return_type,
5533            body,
5534            has_self,
5535            receiver,
5536        ) = match &method_inst.data {
5537            InstData::FnDecl {
5538                name,
5539                is_unchecked,
5540                params_start,
5541                params_len,
5542                return_type,
5543                body,
5544                has_self,
5545                receiver_mode,
5546                ..
5547            } => (
5548                *name,
5549                *is_unchecked,
5550                *params_start,
5551                *params_len,
5552                *return_type,
5553                *body,
5554                *has_self,
5555                crate::sema::anon_interfaces::decode_receiver_mode(*receiver_mode),
5556            ),
5557            _ => unreachable!("AnonFnValue method must be a FnDecl"),
5558        };
5559
5560        let key = (struct_id, method_name);
5561        if self.methods.contains_key(&key) {
5562            return Ok(());
5563        }
5564
5565        // ADR-0076: bind `Self` to the anonymous struct's `Type` while
5566        // resolving the synthesized `__call` method signature.
5567        let saved_self = self.current_self.replace(struct_type);
5568
5569        let params = self.rir.get_params(params_start, params_len);
5570        let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
5571        let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
5572        for p in params {
5573            let resolved = self.resolve_type(p.ty, span)?;
5574            param_types.push(resolved);
5575        }
5576
5577        let ret_ty = self.resolve_type(return_type, span)?;
5578
5579        self.current_self = saved_self;
5580
5581        let param_range = self.param_arena.alloc_method(param_names, param_types);
5582
5583        self.methods.insert(
5584            key,
5585            MethodInfo {
5586                struct_type,
5587                has_self,
5588                receiver,
5589                params: param_range,
5590                return_type: ret_ty,
5591                body,
5592                span: method_inst.span,
5593                is_unchecked,
5594                // Synthesized __call methods (ADR-0055) don't have their
5595                // own method-level comptime type params.
5596                is_generic: false,
5597                return_type_sym: return_type,
5598                // Synthetic `__call` is part of the lambda's surface — pub
5599                // so it can be invoked at any call site.
5600                is_pub: true,
5601                file_id: method_inst.span.file_id,
5602            },
5603        );
5604        Ok(())
5605    }
5606
5607    /// Register methods for an anonymous enum created via comptime with type substitution.
5608    ///
5609    /// Analogous to `register_anon_struct_methods_for_comptime_with_subst`, but for enums.
5610    /// Resolves method parameter/return types with `Self` mapped to the anonymous enum type.
5611    pub(super) fn register_anon_enum_methods_for_comptime_with_subst(
5612        &mut self,
5613        enum_id: EnumId,
5614        enum_type: crate::types::Type,
5615        methods_start: u32,
5616        methods_len: u32,
5617        type_subst: &rustc_hash::FxHashMap<Spur, Type>,
5618    ) -> Option<()> {
5619        let method_refs = self.rir.get_inst_refs(methods_start, methods_len);
5620
5621        let mut seen_methods: rustc_hash::FxHashSet<Spur> = rustc_hash::FxHashSet::default();
5622
5623        // ADR-0082: capture the outer comptime fn's type substitution
5624        // (parallel to the anon-struct path above). Lets enum method
5625        // bodies resolve `T` etc. when analyzed.
5626        if !type_subst.is_empty() {
5627            let owned: rustc_hash::FxHashMap<Spur, Type> =
5628                type_subst.iter().map(|(k, v)| (*k, *v)).collect();
5629            self.anon_enum_type_subst.insert(enum_id, owned);
5630        }
5631
5632        // ADR-0076: bind `Self` to the anonymous enum's `Type` while
5633        // resolving method signatures.
5634        let saved_self = self.current_self.replace(enum_type);
5635
5636        for method_ref in method_refs {
5637            let method_inst = self.rir.get(method_ref);
5638            if let InstData::FnDecl {
5639                name: method_name,
5640                is_pub: method_is_pub,
5641                is_unchecked,
5642                params_start,
5643                params_len,
5644                return_type,
5645                body,
5646                has_self,
5647                receiver_mode,
5648                ..
5649            } = &method_inst.data
5650            {
5651                let receiver = crate::sema::anon_interfaces::decode_receiver_mode(*receiver_mode);
5652                let key = (enum_id, *method_name);
5653
5654                if seen_methods.contains(method_name) {
5655                    return None;
5656                }
5657                seen_methods.insert(*method_name);
5658
5659                if self.enum_methods.contains_key(&key) {
5660                    return None;
5661                }
5662
5663                let params = self.rir.get_params(*params_start, *params_len);
5664                let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
5665                let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
5666
5667                for p in params {
5668                    let resolved_ty =
5669                        self.resolve_type_for_comptime_with_subst(p.ty, type_subst)?;
5670                    param_types.push(resolved_ty);
5671                }
5672
5673                let ret_type =
5674                    self.resolve_type_for_comptime_with_subst(*return_type, type_subst)?;
5675
5676                let param_range = self
5677                    .param_arena
5678                    .alloc_method(param_names.into_iter(), param_types.into_iter());
5679
5680                self.enum_methods.insert(
5681                    key,
5682                    MethodInfo {
5683                        struct_type: enum_type,
5684                        has_self: *has_self,
5685                        receiver,
5686                        params: param_range,
5687                        return_type: ret_type,
5688                        body: *body,
5689                        span: method_inst.span,
5690                        is_unchecked: *is_unchecked,
5691                        // Method-level comptime type params on enum methods
5692                        // are not yet supported; set to false.
5693                        is_generic: false,
5694                        return_type_sym: *return_type,
5695                        is_pub: *method_is_pub,
5696                        file_id: method_inst.span.file_id,
5697                    },
5698                );
5699            }
5700        }
5701        self.current_self = saved_self;
5702        Some(())
5703    }
5704
5705    /// Extract method signatures from RIR for structural equality comparison.
5706    ///
5707    /// This extracts method signatures as type symbols (Spur), not resolved Types.
5708    /// This is intentional: for structural equality, we compare type symbols directly
5709    /// so that `Self` matches `Self` even before we know the concrete StructId.
5710    pub(super) fn extract_anon_method_sigs(
5711        &self,
5712        methods_start: u32,
5713        methods_len: u32,
5714    ) -> Vec<super::AnonMethodSig> {
5715        let method_refs = self.rir.get_inst_refs(methods_start, methods_len);
5716        let mut sigs = Vec::with_capacity(method_refs.len());
5717
5718        for method_ref in method_refs {
5719            let method_inst = self.rir.get(method_ref);
5720            if let InstData::FnDecl {
5721                name,
5722                params_start,
5723                params_len,
5724                return_type,
5725                has_self,
5726                ..
5727            } = &method_inst.data
5728            {
5729                // Extract parameter types as symbols (excluding self)
5730                let params = self.rir.get_params(*params_start, *params_len);
5731                let param_types: Vec<Spur> = params.iter().map(|p| p.ty).collect();
5732
5733                sigs.push(super::AnonMethodSig {
5734                    name: *name,
5735                    has_self: *has_self,
5736                    param_types,
5737                    return_type: *return_type,
5738                });
5739            }
5740        }
5741
5742        sigs
5743    }
5744}