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}