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 ¶ms,
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 ¶m_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: ¶ms,
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: ¶ms,
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 ¶m_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 ¶m_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: ¶ms,
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: ¶ms,
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: ¶ms,
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, ¶m_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, ¶m_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, ¶m_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: ¶m_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(¶m_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(¶m_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(¶m_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}