gruel_air/
sema_context.rs

1//! Immutable semantic analysis context.
2//!
3//! This module contains `SemaContext`, which holds all type information and
4//! declarations that are immutable after the declaration gathering phase.
5//! `SemaContext` is designed to be `Send + Sync` for parallel function analysis.
6//!
7//! # Architecture
8//!
9//! The semantic analysis pipeline is split into two phases:
10//!
11//! 1. **Declaration gathering** (sequential): Builds the `SemaContext` with all
12//!    type definitions, function signatures, and method signatures.
13//!
14//! 2. **Function body analysis** (parallelizable): Each function is analyzed
15//!    using a `FunctionAnalyzer` that holds a reference to the shared `SemaContext`.
16//!
17//! This separation enables:
18//! - Parallel type checking (each function can be analyzed independently)
19//! - Better cache locality (context can be shared across threads)
20//! - Foundation for incremental compilation (can cache `SemaContext` across compilations)
21//!
22//! # Array Type Registry
23//!
24//! The array type registry is thread-safe to support parallel function analysis.
25//! Array types can be created during function body analysis when type inference
26//! resolves array literals like `[1, 2, 3]` without explicit type annotations.
27//! The registry uses `RwLock` for concurrent access with the following pattern:
28//! - Read lock for lookups (most common case)
29//! - Write lock for insertions (rare, only for new array types)
30
31use std::collections::HashMap;
32use std::sync::{PoisonError, RwLock};
33
34use gruel_error::PreviewFeatures;
35use gruel_rir::Rir;
36use lasso::{Spur, ThreadedRodeo};
37
38use crate::inference::{FunctionSig, InferType, MethodSig};
39use crate::intern_pool::TypeInternPool;
40use crate::param_arena::ParamArena;
41// Import FunctionInfo, MethodInfo, and KnownSymbols from sema module to avoid duplication.
42// FunctionInfo and MethodInfo are the canonical definitions; we re-export them for convenience.
43pub use crate::sema::{FunctionInfo, KnownSymbols, MethodInfo};
44use crate::types::{
45    ArrayTypeId, EnumDef, EnumId, ModuleDef, ModuleId, StructDef, StructId, Type, TypeKind,
46};
47
48/// Thread-safe registry for modules.
49///
50/// This registry allows concurrent lookups and insertions of imported modules during
51/// parallel function analysis. It uses double-checked locking to minimize contention.
52#[derive(Debug)]
53pub struct ModuleRegistry {
54    /// Maps import path (e.g., "math.gruel") to ModuleId.
55    paths: RwLock<HashMap<String, ModuleId>>,
56    /// Module definitions indexed by ModuleId.
57    defs: RwLock<Vec<ModuleDef>>,
58}
59
60impl ModuleRegistry {
61    /// Create a new empty registry.
62    pub fn new() -> Self {
63        Self {
64            paths: RwLock::new(HashMap::new()),
65            defs: RwLock::new(Vec::new()),
66        }
67    }
68
69    /// Look up a module by import path.
70    pub fn get(&self, import_path: &str) -> Option<ModuleId> {
71        self.paths
72            .read()
73            .unwrap_or_else(PoisonError::into_inner)
74            .get(import_path)
75            .copied()
76    }
77
78    /// Get or create a module for the given import path and resolved file path.
79    ///
80    /// Returns the ModuleId and whether it was newly created.
81    pub fn get_or_create(&self, import_path: String, file_path: String) -> (ModuleId, bool) {
82        // Fast path: check if already exists
83        {
84            let paths = self.paths.read().unwrap_or_else(PoisonError::into_inner);
85            if let Some(id) = paths.get(&import_path) {
86                return (*id, false);
87            }
88        }
89
90        // Slow path: acquire write lock and insert
91        let mut paths = self.paths.write().unwrap_or_else(PoisonError::into_inner);
92        // Double-check after acquiring write lock
93        if let Some(id) = paths.get(&import_path) {
94            return (*id, false);
95        }
96
97        let mut defs = self.defs.write().unwrap_or_else(PoisonError::into_inner);
98        let id = ModuleId::new(defs.len() as u32);
99        defs.push(ModuleDef::new(import_path.clone(), file_path));
100        paths.insert(import_path, id);
101        (id, true)
102    }
103
104    /// Get a module definition by ID.
105    pub fn get_def(&self, id: ModuleId) -> ModuleDef {
106        self.defs
107            .read()
108            .unwrap_or_else(PoisonError::into_inner)
109            .get(id.index() as usize)
110            .cloned()
111            .expect("Invalid ModuleId")
112    }
113
114    /// Update a module definition.
115    pub fn update_def(&self, id: ModuleId, def: ModuleDef) {
116        let mut defs = self.defs.write().unwrap_or_else(PoisonError::into_inner);
117        defs[id.index() as usize] = def;
118    }
119
120    /// Get the number of modules in the registry.
121    pub fn len(&self) -> usize {
122        self.defs
123            .read()
124            .unwrap_or_else(PoisonError::into_inner)
125            .len()
126    }
127
128    /// Check if the registry is empty.
129    pub fn is_empty(&self) -> bool {
130        self.len() == 0
131    }
132
133    /// Extract the module definitions (consumes the registry).
134    pub fn into_defs(self) -> Vec<ModuleDef> {
135        self.defs
136            .into_inner()
137            .unwrap_or_else(PoisonError::into_inner)
138    }
139}
140
141impl Default for ModuleRegistry {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147/// Pre-computed type information for constraint generation.
148///
149/// This struct holds the function, struct, enum, and method signature maps
150/// converted to `InferType` format for use in Hindley-Milner type inference.
151/// Building this once and reusing it for all function analyses avoids the
152/// O(n²) cost of rebuilding these maps for each function.
153#[derive(Debug)]
154pub struct InferenceContext {
155    /// Function signatures with InferType (for constraint generation).
156    pub func_sigs: HashMap<Spur, FunctionSig>,
157    /// Struct types: name -> Type::new_struct(id).
158    pub struct_types: HashMap<Spur, Type>,
159    /// Enum types: name -> Type::new_enum(id).
160    pub enum_types: HashMap<Spur, Type>,
161    /// Method signatures with InferType: (struct_id, method_name) -> MethodSig.
162    pub method_sigs: HashMap<(StructId, Spur), MethodSig>,
163}
164
165/// Context for semantic analysis, designed for parallel function analysis.
166///
167/// This struct contains all type information and declarations needed during
168/// function body analysis. It is designed to be `Send + Sync` so it can be
169/// shared across threads during parallel function analysis.
170///
171/// # Contents
172///
173/// - Struct and enum definitions (immutable)
174/// - Function and method signatures (references to immutable data in Sema)
175/// - Type intern pool (thread-safe, allows concurrent array interning)
176/// - Pre-computed inference context (immutable)
177/// - Built-in type IDs (immutable)
178/// - Parameter arena for function/method parameter data (immutable after declaration gathering)
179///
180/// # Thread Safety
181///
182/// `SemaContext` is `Send + Sync` because:
183/// - Most fields are immutable after construction
184/// - The type intern pool uses `RwLock` for thread-safe mutations
185/// - References to RIR and interner are shared immutably
186/// - References to functions/methods HashMaps are immutable after declaration gathering
187/// - Reference to param_arena is immutable after declaration gathering
188/// - ThreadedRodeo is designed to be thread-safe
189#[derive(Debug)]
190pub struct SemaContext<'a> {
191    /// Reference to the RIR being analyzed.
192    pub rir: &'a Rir,
193    /// Reference to the string interner.
194    pub interner: &'a ThreadedRodeo,
195    /// Struct lookup: maps struct name symbol to StructId.
196    pub structs: HashMap<Spur, StructId>,
197    /// Enum lookup: maps enum name symbol to EnumId.
198    pub enums: HashMap<Spur, EnumId>,
199    /// Function lookup: reference to Sema's function map (immutable after declaration gathering).
200    pub functions: &'a HashMap<Spur, FunctionInfo>,
201    /// Method lookup: reference to Sema's method map (immutable after declaration gathering).
202    /// Uses (StructId, method_name) key to support anonymous struct methods.
203    pub methods: &'a HashMap<(StructId, Spur), MethodInfo>,
204    /// Enabled preview features.
205    pub preview_features: PreviewFeatures,
206    /// StructId of the synthetic String type.
207    pub builtin_string_id: Option<StructId>,
208    /// EnumId of the synthetic Arch enum (for @target_arch intrinsic).
209    pub builtin_arch_id: Option<EnumId>,
210    /// EnumId of the synthetic Os enum (for @target_os intrinsic).
211    pub builtin_os_id: Option<EnumId>,
212    /// EnumId of the synthetic TypeKind enum (for @typeInfo intrinsic).
213    pub builtin_typekind_id: Option<EnumId>,
214    /// Compilation target (architecture + OS).
215    pub target: gruel_target::Target,
216    /// Pre-computed inference context for HM type inference.
217    pub inference_ctx: InferenceContext,
218    /// Pre-interned known symbols for fast comparison.
219    pub known: KnownSymbols,
220    /// Type intern pool for unified type representation (ADR-0024 Phase 1).
221    ///
222    /// During Phase 1, the pool coexists with the existing type registries.
223    /// It can be used for lookups but the canonical type representation
224    /// remains the old `Type` enum. Later phases will migrate to using
225    /// the pool exclusively.
226    pub type_pool: TypeInternPool,
227    /// Thread-safe module registry.
228    /// Supports concurrent lookups and insertions during parallel analysis.
229    pub module_registry: ModuleRegistry,
230    /// Path to the current source file being compiled (single-file mode).
231    /// Used for resolving relative imports when only one file is compiled.
232    pub source_file_path: Option<String>,
233    /// Maps FileId to source file paths (multi-file mode).
234    /// Used for resolving relative imports when multiple files are compiled.
235    pub file_paths: HashMap<gruel_span::FileId, String>,
236    /// Reference to the parameter arena for accessing function/method parameter data.
237    /// Use `param_arena.types(fn_info.params)` to get parameter types, etc.
238    pub param_arena: &'a ParamArena,
239    /// Constant lookup: reference to Sema's constant map (immutable after declaration gathering).
240    /// Used for looking up const declarations like `const x = @import("...")`.
241    pub constants: &'a HashMap<Spur, crate::sema::ConstInfo>,
242}
243
244// SAFETY: SemaContext is Send + Sync because:
245// - Immutable fields (structs, enums, etc.) are trivially thread-safe
246// - ModuleRegistry uses RwLock for interior mutability
247// - TypeInternPool uses RwLock for interior mutability (including array interning)
248// - References to RIR and ThreadedRodeo are shared immutably
249// - References to functions/methods HashMaps are shared immutably (read-only after declaration gathering)
250// - ThreadedRodeo is designed to be thread-safe
251// - &HashMap<K, V> is Send + Sync when the HashMap is (immutable references are always safe)
252unsafe impl<'a> Send for SemaContext<'a> {}
253unsafe impl<'a> Sync for SemaContext<'a> {}
254
255impl<'a> SemaContext<'a> {
256    /// Get the builtin String type as a Type::Struct.
257    pub fn builtin_string_type(&self) -> Type {
258        self.builtin_string_id
259            .map(Type::new_struct)
260            .expect("String type should be registered during builtin injection")
261    }
262
263    /// Look up a struct by name.
264    pub fn get_struct(&self, name: Spur) -> Option<StructId> {
265        self.structs.get(&name).copied()
266    }
267
268    /// Get a struct definition by ID.
269    pub fn get_struct_def(&self, id: StructId) -> StructDef {
270        self.type_pool.struct_def(id)
271    }
272
273    /// Look up an enum by name.
274    pub fn get_enum(&self, name: Spur) -> Option<EnumId> {
275        self.enums.get(&name).copied()
276    }
277
278    /// Get an enum definition by ID.
279    pub fn get_enum_def(&self, id: EnumId) -> EnumDef {
280        self.type_pool.enum_def(id)
281    }
282
283    /// Look up a function by name.
284    pub fn get_function(&self, name: Spur) -> Option<&FunctionInfo> {
285        self.functions.get(&name)
286    }
287
288    /// Look up a method by struct ID and method name.
289    pub fn get_method(&self, struct_id: StructId, method_name: Spur) -> Option<&MethodInfo> {
290        self.methods.get(&(struct_id, method_name))
291    }
292
293    /// Look up a constant by name.
294    pub fn get_constant(&self, name: Spur) -> Option<&crate::sema::ConstInfo> {
295        self.constants.get(&name)
296    }
297
298    /// Get an array type definition by ID.
299    ///
300    /// Returns `(element_type, length)` for the array.
301    pub fn get_array_type_def(&self, id: ArrayTypeId) -> (Type, u64) {
302        self.type_pool.array_def(id)
303    }
304
305    /// Look up an array type by element type and length.
306    pub fn get_array_type(&self, element_type: Type, length: u64) -> Option<ArrayTypeId> {
307        self.type_pool.get_array_by_type(element_type, length)
308    }
309
310    /// Get or create an array type. Thread-safe.
311    pub fn get_or_create_array_type(&self, element_type: Type, length: u64) -> ArrayTypeId {
312        self.type_pool.intern_array_from_type(element_type, length)
313    }
314
315    /// Get or create a ptr const type. Thread-safe.
316    pub fn get_or_create_ptr_const_type(&self, pointee_type: Type) -> crate::types::PtrConstTypeId {
317        self.type_pool.intern_ptr_const_from_type(pointee_type)
318    }
319
320    /// Get or create a ptr mut type. Thread-safe.
321    pub fn get_or_create_ptr_mut_type(&self, pointee_type: Type) -> crate::types::PtrMutTypeId {
322        self.type_pool.intern_ptr_mut_from_type(pointee_type)
323    }
324
325    /// Look up a module by import path.
326    pub fn get_module(&self, import_path: &str) -> Option<ModuleId> {
327        self.module_registry.get(import_path)
328    }
329
330    /// Get a module definition by ID.
331    pub fn get_module_def(&self, id: ModuleId) -> ModuleDef {
332        self.module_registry.get_def(id)
333    }
334
335    /// Get or create a module for the given import path and file path. Thread-safe.
336    ///
337    /// Returns the ModuleId and whether it was newly created.
338    pub fn get_or_create_module(&self, import_path: String, file_path: String) -> (ModuleId, bool) {
339        self.module_registry.get_or_create(import_path, file_path)
340    }
341
342    /// Update a module definition with populated declarations.
343    pub fn update_module_def(&self, id: ModuleId, def: ModuleDef) {
344        self.module_registry.update_def(id, def);
345    }
346
347    /// Get the source file path for a span.
348    ///
349    /// Looks up the file path using the span's file_id. Falls back to
350    /// `source_file_path` for single-file compilation mode.
351    pub fn get_source_path(&self, span: gruel_span::Span) -> Option<&str> {
352        // First, try the file_paths map (multi-file mode)
353        if let Some(path) = self.file_paths.get(&span.file_id) {
354            return Some(path.as_str());
355        }
356        // Fall back to source_file_path (single-file mode)
357        self.source_file_path.as_deref()
358    }
359
360    /// Get the file path for a given FileId.
361    pub fn get_file_path(&self, file_id: gruel_span::FileId) -> Option<&str> {
362        self.file_paths.get(&file_id).map(|s| s.as_str())
363    }
364
365    /// Check if the accessing file can see a private item from the target file.
366    ///
367    /// Visibility rules (per ADR-0026):
368    /// - `pub` items are always accessible
369    /// - Private items are accessible if the files are in the same directory module
370    ///
371    /// Directory module membership includes:
372    /// - Files directly in the directory (e.g., `utils/strings.gruel` is in `utils`)
373    /// - Facade files for the directory (e.g., `_utils.gruel` is in `utils` module)
374    ///
375    /// Returns true if the item is accessible.
376    pub fn is_accessible(
377        &self,
378        accessing_file_id: gruel_span::FileId,
379        target_file_id: gruel_span::FileId,
380        is_pub: bool,
381    ) -> bool {
382        // Public items are always accessible
383        if is_pub {
384            return true;
385        }
386
387        // Get paths for both files
388        let accessing_path = self.get_file_path(accessing_file_id);
389        let target_path = self.get_file_path(target_file_id);
390
391        // If we can't determine the paths, be permissive (for single-file mode or tests)
392        match (accessing_path, target_path) {
393            (Some(acc), Some(tgt)) => {
394                use std::path::Path;
395
396                // Get the "module identity" for each file.
397                // For a regular file like `utils/strings.gruel`, the module is `utils/`
398                // For a facade file like `_utils.gruel`, the module is `utils/` (the directory it represents)
399                let acc_module = Self::get_module_identity(Path::new(acc));
400                let tgt_module = Self::get_module_identity(Path::new(tgt));
401
402                acc_module == tgt_module
403            }
404            // If either path is unknown, allow access (e.g., synthetic types, single-file mode)
405            _ => true,
406        }
407    }
408
409    /// Get the module identity for a file path.
410    ///
411    /// - For regular files: returns the parent directory
412    /// - For facade files (`_foo.gruel`): returns the corresponding directory (`foo/`)
413    ///
414    /// This allows facade files to be treated as part of their corresponding directory module.
415    fn get_module_identity(path: &std::path::Path) -> Option<std::path::PathBuf> {
416        let parent = path.parent()?;
417        let file_stem = path.file_stem()?.to_str()?;
418
419        // Check if this is a facade file (starts with underscore)
420        if let Some(module_name) = file_stem.strip_prefix('_') {
421            // Facade file: _utils.gruel -> parent/utils
422            // Strip the leading underscore
423            Some(parent.join(module_name))
424        } else {
425            // Regular file: the module is just the parent directory
426            Some(parent.to_path_buf())
427        }
428    }
429
430    /// Get a human-readable name for a type.
431    pub fn format_type_name(&self, ty: Type) -> String {
432        self.type_pool.format_type_name(ty)
433    }
434
435    /// Check if a type is a Copy type.
436    pub fn is_type_copy(&self, ty: Type) -> bool {
437        match ty.kind() {
438            // Primitive Copy types
439            TypeKind::I8
440            | TypeKind::I16
441            | TypeKind::I32
442            | TypeKind::I64
443            | TypeKind::U8
444            | TypeKind::U16
445            | TypeKind::U32
446            | TypeKind::U64
447            | TypeKind::Isize
448            | TypeKind::Usize
449            | TypeKind::F16
450            | TypeKind::F32
451            | TypeKind::F64
452            | TypeKind::Bool
453            | TypeKind::Unit => true,
454            // Enum types are Copy (they're small discriminant values)
455            TypeKind::Enum(_) => true,
456            // Never, Error, ComptimeType, ComptimeStr, and ComptimeInt are Copy for convenience
457            TypeKind::Never
458            | TypeKind::Error
459            | TypeKind::ComptimeType
460            | TypeKind::ComptimeStr
461            | TypeKind::ComptimeInt => true,
462            // Struct types: check if marked with @copy
463            TypeKind::Struct(struct_id) => {
464                let struct_def = self.type_pool.struct_def(struct_id);
465                struct_def.is_copy
466            }
467            // Arrays are Copy if their element type is Copy
468            TypeKind::Array(array_id) => {
469                let (element_type, _length) = self.type_pool.array_def(array_id);
470                self.is_type_copy(element_type)
471            }
472            // Module types are Copy (they're just compile-time namespace references)
473            TypeKind::Module(_) => true,
474            // Pointer types are Copy (they're just addresses)
475            TypeKind::PtrConst(_) | TypeKind::PtrMut(_) => true,
476        }
477    }
478
479    /// Get the number of ABI slots required for a type.
480    pub fn abi_slot_count(&self, ty: Type) -> u32 {
481        self.type_pool.abi_slot_count(ty)
482    }
483
484    /// Get the slot offset of a field within a struct.
485    pub fn field_slot_offset(&self, struct_id: StructId, field_index: usize) -> u32 {
486        let struct_def = self.type_pool.struct_def(struct_id);
487        struct_def.fields[..field_index]
488            .iter()
489            .map(|f| self.abi_slot_count(f.ty))
490            .sum()
491    }
492
493    /// Convert a concrete Type to InferType for use in constraint generation.
494    pub fn type_to_infer_type(&self, ty: Type) -> InferType {
495        match ty.kind() {
496            TypeKind::Array(array_id) => {
497                let (element_type, length) = self.type_pool.array_def(array_id);
498                let element_infer = self.type_to_infer_type(element_type);
499                InferType::Array {
500                    element: Box::new(element_infer),
501                    length,
502                }
503            }
504            // ComptimeInt coerces to any integer type (like an integer literal)
505            TypeKind::ComptimeInt => InferType::IntLiteral,
506            _ => InferType::Concrete(ty),
507        }
508    }
509
510    // ========================================================================
511    // Builtin type helpers (duplicated from Sema for parallel analysis)
512    // ========================================================================
513
514    /// Check if a type is the builtin String type.
515    pub fn is_builtin_string(&self, ty: Type) -> bool {
516        match ty.kind() {
517            TypeKind::Struct(struct_id) => Some(struct_id) == self.builtin_string_id,
518            _ => false,
519        }
520    }
521
522    /// Get the builtin type definition for a struct if it's a builtin type.
523    pub fn get_builtin_type_def(
524        &self,
525        struct_id: StructId,
526    ) -> Option<&'static gruel_builtins::BuiltinTypeDef> {
527        let struct_def = self.type_pool.struct_def(struct_id);
528        if struct_def.is_builtin {
529            gruel_builtins::get_builtin_type(&struct_def.name)
530        } else {
531            None
532        }
533    }
534
535    /// Check if a method name is a builtin mutation method.
536    pub fn is_builtin_mutation_method(&self, method_name: &str) -> bool {
537        use gruel_builtins::{BUILTIN_TYPES, ReceiverMode};
538
539        for builtin in BUILTIN_TYPES {
540            if let Some(method) = builtin.find_method(method_name)
541                && method.receiver_mode == ReceiverMode::ByMutRef
542            {
543                return true;
544            }
545        }
546        false
547    }
548
549    /// Get the AIR output type for a builtin struct.
550    pub fn builtin_air_type(&self, struct_id: StructId) -> Type {
551        Type::new_struct(struct_id)
552    }
553
554    /// Check if a type is a linear type.
555    pub fn is_type_linear(&self, ty: Type) -> bool {
556        match ty.kind() {
557            TypeKind::Struct(struct_id) => {
558                let struct_def = self.type_pool.struct_def(struct_id);
559                struct_def.is_linear
560            }
561            _ => false,
562        }
563    }
564
565    /// Check that a preview feature is enabled.
566    ///
567    /// This is used to gate experimental features behind the `--preview` flag.
568    /// Returns an error with a helpful message if the feature is not enabled.
569    pub fn require_preview(
570        &self,
571        feature: gruel_error::PreviewFeature,
572        what: &str,
573        span: gruel_span::Span,
574    ) -> gruel_error::CompileResult<()> {
575        if self.preview_features.contains(&feature) {
576            Ok(())
577        } else {
578            Err(gruel_error::CompileError::new(
579                gruel_error::ErrorKind::PreviewFeatureRequired {
580                    feature,
581                    what: what.to_string(),
582                },
583                span,
584            )
585            .with_help(format!(
586                "use `--preview {}` to enable this feature ({})",
587                feature.name(),
588                feature.adr()
589            )))
590        }
591    }
592
593    // ========================================================================
594    // Module-qualified type resolution
595    // ========================================================================
596
597    /// Resolve a struct type through a module reference.
598    ///
599    /// Used for qualified struct literals like `module.StructName { ... }`.
600    /// The `module_ref` is an InstRef pointing to the result of an @import.
601    /// Checks visibility: private structs are only accessible from the same directory.
602    pub fn resolve_struct_through_module(
603        &self,
604        _module_ref: gruel_rir::InstRef,
605        type_name: lasso::Spur,
606        span: gruel_span::Span,
607    ) -> gruel_error::CompileResult<StructId> {
608        use gruel_error::{CompileError, ErrorKind};
609
610        // Get the module type from the inst - we need to look up the AIR result
611        // For now, use a simplified approach: look up the type name in the global scope
612        // but require it to be exported from the module.
613        //
614        // A full implementation would:
615        // 1. Resolve module_ref to get the ModuleId
616        // 2. Look up the struct in that module's exports
617        //
618        // For now, we just look it up globally (works for single module imports)
619        let type_name_str = self.interner.resolve(&type_name);
620
621        // Try to find the struct globally
622        let struct_id = self.get_struct(type_name).ok_or_else(|| {
623            CompileError::new(ErrorKind::UnknownType(type_name_str.to_string()), span)
624        })?;
625
626        // Check visibility
627        let struct_def = self.get_struct_def(struct_id);
628        let accessing_file_id = span.file_id;
629        let target_file_id = struct_def.file_id;
630
631        if !self.is_accessible(accessing_file_id, target_file_id, struct_def.is_pub) {
632            return Err(CompileError::new(
633                ErrorKind::PrivateMemberAccess {
634                    item_kind: "struct".to_string(),
635                    name: type_name_str.to_string(),
636                },
637                span,
638            ));
639        }
640
641        Ok(struct_id)
642    }
643
644    /// Resolve an enum type through a module reference.
645    ///
646    /// Used for qualified enum paths like `module.EnumName::Variant`.
647    /// The `module_ref` is an InstRef pointing to the result of an @import.
648    /// Checks visibility: private enums are only accessible from the same directory.
649    pub fn resolve_enum_through_module(
650        &self,
651        _module_ref: gruel_rir::InstRef,
652        type_name: lasso::Spur,
653        span: gruel_span::Span,
654    ) -> gruel_error::CompileResult<EnumId> {
655        use gruel_error::{CompileError, ErrorKind};
656
657        let type_name_str = self.interner.resolve(&type_name);
658
659        // Try to find the enum globally
660        let enum_id = self.get_enum(type_name).ok_or_else(|| {
661            CompileError::new(ErrorKind::UnknownEnumType(type_name_str.to_string()), span)
662        })?;
663
664        // Check visibility
665        let enum_def = self.get_enum_def(enum_id);
666        let accessing_file_id = span.file_id;
667        let target_file_id = enum_def.file_id;
668
669        if !self.is_accessible(accessing_file_id, target_file_id, enum_def.is_pub) {
670            return Err(CompileError::new(
671                ErrorKind::PrivateMemberAccess {
672                    item_kind: "enum".to_string(),
673                    name: type_name_str.to_string(),
674                },
675                span,
676            ));
677        }
678
679        Ok(enum_id)
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    /// Compile-time assertion that SemaContext is Send + Sync.
688    /// This is critical for parallel function body analysis.
689    fn assert_send_sync<T: Send + Sync>() {}
690
691    #[test]
692    fn test_sema_context_is_send_sync() {
693        assert_send_sync::<SemaContext<'_>>();
694    }
695}