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}