gruel_air/sema/imports.rs
1//! Import resolution and const initializer evaluation.
2//!
3//! This module handles:
4//! - Evaluating const initializers (e.g., `const x = @import(...)`)
5//! - Resolving import paths to actual file paths
6//!
7//! Import path resolution uses the structured [`ModulePath`] type for clear,
8//! testable resolution logic with explicit priority order. See the module_path
9//! module for details on how different import forms are resolved.
10
11use gruel_rir::InstRef;
12use gruel_util::Span;
13use gruel_util::{CompileError, CompileResult, ErrorKind};
14
15use crate::types::Type;
16
17use super::Sema;
18use super::context::ConstValue;
19use super::module_path::ModulePath;
20
21impl Sema<'_> {
22 /// Evaluate const initializers to determine their types.
23 ///
24 /// This is Phase 2.5 of semantic analysis, called after declaration gathering
25 /// but before function body analysis. It handles:
26 ///
27 /// - `const x = @import("module")` - evaluates to Type::Module
28 /// - Other const initializers are left with placeholder types for now
29 ///
30 /// This enables module re-exports where a const holds an imported module
31 /// that can be accessed via dot notation.
32 pub fn evaluate_const_initializers(&mut self) -> CompileResult<()> {
33 // Collect const names to iterate (avoid borrowing issues)
34 let const_names: Vec<lasso::Spur> = self.constants.keys().copied().collect();
35
36 for name in const_names {
37 let const_info = self.constants.get(&name).unwrap();
38 let init_ref = const_info.init;
39 let span = const_info.span;
40
41 // Check if the init expression is an @import intrinsic
42 let inst = self.rir.get(init_ref);
43 if let gruel_rir::InstData::Intrinsic {
44 name: intrinsic_name,
45 args_start,
46 args_len,
47 } = &inst.data
48 {
49 let intrinsic_name_str = self.interner.resolve(intrinsic_name);
50 if intrinsic_name_str == "import" {
51 // This is an @import - evaluate it at compile time
52 let result = self.evaluate_import_intrinsic(*args_start, *args_len, span)?;
53
54 // Update the const type to the module type
55 if let Some(const_info_mut) = self.constants.get_mut(&name) {
56 const_info_mut.ty = result;
57 }
58 }
59 }
60 }
61
62 Ok(())
63 }
64
65 /// Evaluate an @import intrinsic call at compile time.
66 ///
67 /// This is used during const initializer evaluation to resolve module imports.
68 pub(crate) fn evaluate_import_intrinsic(
69 &mut self,
70 args_start: u32,
71 args_len: u32,
72 span: Span,
73 ) -> CompileResult<Type> {
74 // @import takes exactly one argument
75 if args_len != 1 {
76 return Err(CompileError::new(
77 ErrorKind::IntrinsicWrongArgCount {
78 name: "import".to_string(),
79 expected: 1,
80 found: args_len as usize,
81 },
82 span,
83 ));
84 }
85
86 // Get the argument from extra data (intrinsics use inst_refs, not call_args)
87 let arg_refs = self.rir.get_inst_refs(args_start, args_len);
88 let import_path = self.resolve_import_path_arg(arg_refs[0])?;
89
90 // Resolve the import path
91 let resolved_path = self.resolve_import_path_for_const(&import_path, span)?;
92
93 // Register the module
94 let (module_id, _is_new) = self
95 .module_registry
96 .get_or_create(import_path, resolved_path);
97
98 Ok(Type::new_module(module_id))
99 }
100
101 /// Resolve the argument of `@import` to a concrete module-path string.
102 ///
103 /// Accepts either a bare string literal (fast path, keeps diagnostics
104 /// anchored on the literal) or any expression of type `comptime_str`, such
105 /// as a `comptime { ... }` block that selects a path based on
106 /// `@target_os()`. The comptime interpreter runs with a top-level stub
107 /// context: `@import` arguments never reference enclosing comptime type or
108 /// value parameters.
109 pub(crate) fn resolve_import_path_arg(&mut self, arg_ref: InstRef) -> CompileResult<String> {
110 let arg_inst = self.rir.get(arg_ref);
111 let arg_span = arg_inst.span;
112
113 if let gruel_rir::InstData::StringConst(path_spur) = &arg_inst.data {
114 return Ok(self.interner.resolve(path_spur).to_string());
115 }
116
117 match self.evaluate_comptime_top_level(arg_ref, arg_span)? {
118 ConstValue::ComptimeStr(idx) => {
119 Ok(self.resolve_comptime_str(idx, arg_span)?.to_string())
120 }
121 _ => Err(CompileError::new(
122 ErrorKind::ImportRequiresStringLiteral,
123 arg_span,
124 )),
125 }
126 }
127
128 /// Resolve an import path for const evaluation.
129 ///
130 /// This uses the structured `ModulePath` type for clear resolution logic.
131 /// See the module_path module for the resolution order and rules.
132 ///
133 /// # Resolution Order
134 ///
135 /// 1. Standard library (`"std"`) - currently not supported
136 /// 2. For explicit `.gruel` paths - exact match, then suffix match
137 /// 3. For simple paths - `{path}.gruel`, then suffix match, then basename match
138 /// 4. Facade files (`_foo.gruel`) for directory modules
139 pub(crate) fn resolve_import_path_for_const(
140 &self,
141 import_path: &str,
142 span: Span,
143 ) -> CompileResult<String> {
144 let module_path = ModulePath::parse(import_path);
145
146 // Try to resolve against loaded file paths
147 let loaded_paths = self.file_paths.values();
148 if let Some(resolved) = module_path.resolve(loaded_paths) {
149 return Ok(resolved);
150 }
151
152 // Module not found - collect candidates for error message
153 let candidates: Vec<String> = self.file_paths.values().cloned().collect();
154 Err(CompileError::new(
155 ErrorKind::ModuleNotFound {
156 path: import_path.to_string(),
157 candidates,
158 },
159 span,
160 ))
161 }
162}