Skip to main content

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