gruel_air/sema/lang_items.rs
1//! Lang-item registry (ADR-0079).
2//!
3//! Stores the resolved interface/enum IDs for the compiler-recognized
4//! lang items (`drop`, `clone`, `handle`, `op_eq`, `op_cmp`,
5//! `ordering`). Populated from `@lang("…")` directives on prelude
6//! declarations during `resolve_declarations`. Every compiler-side
7//! behavior that historically matched on a hardcoded type name (the
8//! `Drop`/`Clone`/`Eq`/`Ord` strings) consults this registry instead.
9//!
10//! ADR-0080 retired the `copy` lang item: posture is declared on the
11//! type (`copy struct`/`copy enum`) and queried via `@ownership(T)`,
12//! never dispatched, so it no longer needs an interface binding.
13
14use gruel_builtins::{LangEnumItem, LangFnItem, LangInterfaceItem, LangItemKind};
15use gruel_rir::InstData;
16use gruel_util::{CompileError, CompileResult, ErrorKind, Span};
17use lasso::Spur;
18
19use super::Sema;
20use super::file_paths::is_prelude_path;
21use crate::types::{EnumId, InterfaceId};
22
23/// Lang items resolved against the prelude. Each option is `Some` once
24/// the prelude tags the corresponding declaration with `@lang("…")`.
25/// Missing entries surface lazily — every call site that needs a lang
26/// item logs through `Sema::lang_items()` and tolerates an absent entry
27/// in the same way it tolerated a missing interface name pre-ADR-0079.
28#[derive(Debug, Default, Clone)]
29pub struct LangItems {
30 pub(crate) drop: Option<InterfaceId>,
31 pub(crate) clone: Option<InterfaceId>,
32 pub(crate) handle: Option<InterfaceId>,
33 pub(crate) op_eq: Option<InterfaceId>,
34 pub(crate) op_cmp: Option<InterfaceId>,
35 pub(crate) ordering: Option<EnumId>,
36 /// `@lang("vec")` — the prelude `Vec(comptime T: type) -> type`
37 /// function whose instantiations are recognized as the canonical
38 /// owned-buffer vector (ADR-0082).
39 pub(crate) vec_fn: Option<Spur>,
40}
41
42impl LangItems {
43 pub fn drop(&self) -> Option<InterfaceId> {
44 self.drop
45 }
46 pub fn clone(&self) -> Option<InterfaceId> {
47 self.clone
48 }
49 pub fn handle(&self) -> Option<InterfaceId> {
50 self.handle
51 }
52 pub fn op_eq(&self) -> Option<InterfaceId> {
53 self.op_eq
54 }
55 pub fn op_cmp(&self) -> Option<InterfaceId> {
56 self.op_cmp
57 }
58 pub fn ordering(&self) -> Option<EnumId> {
59 self.ordering
60 }
61 pub fn vec_fn(&self) -> Option<Spur> {
62 self.vec_fn
63 }
64}
65
66impl<'a> Sema<'a> {
67 /// Public read access to the lang-item registry.
68 pub fn lang_items(&self) -> &LangItems {
69 &self.lang_items
70 }
71
72 /// ADR-0082: reverse lookup — given an element type, return the
73 /// `StructId` of the `@lang("vec")` instance for that element.
74 /// Returns `None` if `populate_vec_instance` has not yet been
75 /// called for `elem_ty` (or the lang-item Vec is unbound).
76 pub(crate) fn vec_instance_for_elem(
77 &self,
78 elem_ty: crate::types::Type,
79 ) -> Option<crate::types::StructId> {
80 self.vec_instance_registry
81 .iter()
82 .find_map(|(sid, t)| if *t == elem_ty { Some(*sid) } else { None })
83 }
84
85 /// ADR-0082: ensure the prelude `@lang("vec")` function's
86 /// instantiation for `elem_ty` has been evaluated and its
87 /// `StructId` registered. Idempotent — early-returns if the
88 /// element type is already represented in the registry. Errors
89 /// are swallowed: the legacy `TypeKind::Vec(_)` path stays valid,
90 /// so a failed prelude evaluation only means the registry stays
91 /// empty for that element type. Phase 3's bridge consumers fall
92 /// back to the legacy path when the registry has no entry.
93 ///
94 /// Uses the rich comptime interpreter (`evaluate_type_ctor_body`)
95 /// because the prelude `Vec` body is allowed to wrap its struct
96 /// literal in a `comptime if` (ADR-0084 follow-up: thread-safety
97 /// markers per element-type classification). The simpler
98 /// `try_evaluate_const_with_subst` path doesn't handle
99 /// `InstData::Branch`, so it can't see through the conditional to
100 /// the struct underneath.
101 pub(crate) fn populate_vec_instance(&mut self, elem_ty: crate::types::Type) {
102 // Skip if any existing entry already maps to this element.
103 if self.vec_instance_registry.values().any(|t| *t == elem_ty) {
104 return;
105 }
106 let Some(vec_fn_sym) = self.lang_items.vec_fn() else {
107 return;
108 };
109 let Some(fn_info) = self.functions.get(&vec_fn_sym).copied() else {
110 return;
111 };
112 let param_names = self.param_arena.names(fn_info.params).to_vec();
113 if param_names.is_empty() {
114 return;
115 }
116 let mut type_subst: rustc_hash::FxHashMap<lasso::Spur, crate::types::Type> =
117 rustc_hash::FxHashMap::default();
118 type_subst.insert(param_names[0], elem_ty);
119 let value_subst: rustc_hash::FxHashMap<lasso::Spur, super::ConstValue> =
120 rustc_hash::FxHashMap::default();
121
122 // Build a stub `AnalysisContext` so we can drive the rich
123 // evaluator without an enclosing analysis. Mirrors the stub
124 // built by `evaluate_comptime_top_level` for Phase 2.5
125 // const-initializer evaluation.
126 let empty_params: Vec<crate::sema::context::ParamInfo> = Vec::new();
127 let empty_resolved: rustc_hash::FxHashMap<gruel_rir::InstRef, crate::types::Type> =
128 rustc_hash::FxHashMap::default();
129 let stub = crate::sema::context::AnalysisContext {
130 locals: rustc_hash::FxHashMap::default(),
131 params: &empty_params,
132 next_slot: 0,
133 loop_depth: 0,
134 forbid_break: None,
135 checked_depth: 0,
136 used_locals: rustc_hash::FxHashSet::default(),
137 return_type: crate::types::Type::UNIT,
138 scope_stack: Vec::new(),
139 resolved_types: &empty_resolved,
140 moved_vars: rustc_hash::FxHashMap::default(),
141 warnings: Vec::new(),
142 local_string_table: rustc_hash::FxHashMap::default(),
143 local_strings: Vec::new(),
144 local_bytes: Vec::new(),
145 comptime_type_vars: rustc_hash::FxHashMap::default(),
146 comptime_value_vars: rustc_hash::FxHashMap::default(),
147 referenced_functions: rustc_hash::FxHashSet::default(),
148 referenced_methods: rustc_hash::FxHashSet::default(),
149 borrow_arg_skip_move: None,
150 uninit_handles: rustc_hash::FxHashMap::default(),
151 unroll_arm_bindings: rustc_hash::FxHashMap::default(),
152 };
153
154 // Evaluation may push diagnostics into `pending_anon_eval_errors`
155 // when the body has a sema-level problem (e.g. an empty struct
156 // body). Snapshot the buffer length and rewind on failure so we
157 // don't surface the prelude's evaluation errors to user code —
158 // the legacy `TypeKind::Vec(_)` path is the safety net.
159 let pending_before = self.pending_anon_eval_errors.len();
160 let saved_ctor = self.comptime_ctor_fn.replace(vec_fn_sym);
161 // Use the function's declaration span as a fallback diagnostic
162 // anchor; the rich evaluator only consults this on unrecoverable
163 // failures, which we discard.
164 let span = self
165 .functions
166 .get(&vec_fn_sym)
167 .map(|f| self.rir.get(f.body).span)
168 .unwrap_or(gruel_util::Span::new(0, 0));
169 let _ = self.evaluate_type_ctor_body(fn_info.body, &type_subst, &value_subst, &stub, span);
170 self.comptime_ctor_fn = saved_ctor;
171 self.pending_anon_eval_errors.truncate(pending_before);
172 }
173
174 /// ADR-0079: walk every interface and enum declaration in the RIR,
175 /// resolve `@lang("…")` directives against the closed lang-item set,
176 /// and record the binding on `self.lang_items`.
177 ///
178 /// Errors:
179 /// - `@lang(...)` outside a prelude file (`InvalidLangItem`).
180 /// - Wrong arg shape (zero, multiple, or non-string) at the
181 /// directive site.
182 /// - Unknown lang-item name.
183 /// - Lang-item kind mismatched with declaration kind (e.g.
184 /// `@lang("ordering")` on an interface).
185 /// - Two declarations both claim the same lang item.
186 pub(crate) fn populate_lang_items(&mut self) -> CompileResult<()> {
187 // Collect raw bindings first; we look up interface/enum IDs from
188 // already-populated maps and only mutate `self.lang_items` after
189 // all directives validate.
190 struct Pending {
191 kind: PendingKind,
192 site: Span,
193 }
194 enum PendingKind {
195 Interface(LangInterfaceItem, InterfaceId),
196 Enum(LangEnumItem, EnumId),
197 Fn(LangFnItem, Spur),
198 }
199 let mut pending: Vec<Pending> = Vec::new();
200
201 for (_, inst) in self.rir.iter() {
202 let (name, directives_start, directives_len, kind) = match &inst.data {
203 InstData::InterfaceDecl {
204 name,
205 directives_start,
206 directives_len,
207 ..
208 } => (
209 *name,
210 *directives_start,
211 *directives_len,
212 DeclKind::Interface,
213 ),
214 InstData::EnumDecl {
215 name,
216 directives_start,
217 directives_len,
218 ..
219 } => (*name, *directives_start, *directives_len, DeclKind::Enum),
220 InstData::FnDecl {
221 name,
222 directives_start,
223 directives_len,
224 ..
225 } => (*name, *directives_start, *directives_len, DeclKind::Fn),
226 _ => continue,
227 };
228 if directives_len == 0 {
229 continue;
230 }
231 // Use the host inst's span for privilege/diagnostic
232 // reporting. RIR directive storage drops the directive's
233 // file_id (its `span` carries `(start, len)` only) so the
234 // host span is the reliable file-of-origin signal.
235 let host_span = inst.span;
236 let directives = self.rir.get_directives(directives_start, directives_len);
237 for d in &directives {
238 if self.interner.resolve(&d.name) != "lang" {
239 continue;
240 }
241 // Privilege gate: `@lang(...)` is only valid inside the
242 // prelude.
243 if !self.is_directive_in_prelude(host_span) {
244 return Err(CompileError::new(
245 ErrorKind::InvalidLangItem {
246 reason: "`@lang(...)` is only valid in the prelude (under `prelude/`)"
247 .to_string(),
248 },
249 host_span,
250 ));
251 }
252 if d.args.len() != 1 {
253 return Err(CompileError::new(
254 ErrorKind::InvalidLangItem {
255 reason: format!(
256 "`@lang(...)` expects one string argument, got {}",
257 d.args.len()
258 ),
259 },
260 host_span,
261 ));
262 }
263 let lang_name = self.interner.resolve(&d.args[0]).to_string();
264 let lang_kind = match LangItemKind::from_str(&lang_name) {
265 Some(k) => k,
266 None => {
267 let known = gruel_builtins::all_lang_item_names().join(", ");
268 return Err(CompileError::new(
269 ErrorKind::InvalidLangItem {
270 reason: format!(
271 "unknown lang item `{}`; known: {}",
272 lang_name, known
273 ),
274 },
275 host_span,
276 ));
277 }
278 };
279 match (kind, lang_kind) {
280 (DeclKind::Interface, LangItemKind::Interface(item)) => {
281 let id = match self.interfaces.get(&name) {
282 Some(&id) => id,
283 None => continue,
284 };
285 pending.push(Pending {
286 kind: PendingKind::Interface(item, id),
287 site: host_span,
288 });
289 }
290 (DeclKind::Enum, LangItemKind::Enum(item)) => {
291 let id = match self.enums.get(&name) {
292 Some(&id) => id,
293 None => continue,
294 };
295 pending.push(Pending {
296 kind: PendingKind::Enum(item, id),
297 site: host_span,
298 });
299 }
300 (DeclKind::Fn, LangItemKind::Fn(item)) => {
301 // ADR-0082: bind the function's name `Spur` so
302 // `Sema::lang_items().vec_fn()` returns it. We
303 // bind unconditionally — `self.functions` isn't
304 // populated until `resolve_remaining_declarations`,
305 // which runs *after* `populate_lang_items`.
306 // Function-existence checks happen lazily at use
307 // sites (e.g. `populate_vec_instance`'s
308 // `self.functions.get(&vec_fn_sym)` lookup), by
309 // which time the map is populated.
310 pending.push(Pending {
311 kind: PendingKind::Fn(item, name),
312 site: host_span,
313 });
314 }
315 (DeclKind::Interface, LangItemKind::Enum(_))
316 | (DeclKind::Interface, LangItemKind::Fn(_)) => {
317 return Err(CompileError::new(
318 ErrorKind::InvalidLangItem {
319 reason: format!(
320 "lang item `{}` cannot appear on an interface declaration",
321 lang_name
322 ),
323 },
324 host_span,
325 ));
326 }
327 (DeclKind::Enum, LangItemKind::Interface(_))
328 | (DeclKind::Enum, LangItemKind::Fn(_)) => {
329 return Err(CompileError::new(
330 ErrorKind::InvalidLangItem {
331 reason: format!(
332 "lang item `{}` cannot appear on an enum declaration",
333 lang_name
334 ),
335 },
336 host_span,
337 ));
338 }
339 (DeclKind::Fn, LangItemKind::Interface(_))
340 | (DeclKind::Fn, LangItemKind::Enum(_)) => {
341 return Err(CompileError::new(
342 ErrorKind::InvalidLangItem {
343 reason: format!(
344 "lang item `{}` cannot appear on a function declaration",
345 lang_name
346 ),
347 },
348 host_span,
349 ));
350 }
351 }
352 }
353 }
354
355 for p in pending {
356 match p.kind {
357 PendingKind::Interface(item, id) => {
358 let slot: &mut Option<InterfaceId> = match item {
359 LangInterfaceItem::Drop => &mut self.lang_items.drop,
360 LangInterfaceItem::Clone => &mut self.lang_items.clone,
361 LangInterfaceItem::Handle => &mut self.lang_items.handle,
362 LangInterfaceItem::OpEq => &mut self.lang_items.op_eq,
363 LangInterfaceItem::OpCmp => &mut self.lang_items.op_cmp,
364 };
365 if let Some(_existing) = *slot {
366 return Err(CompileError::new(
367 ErrorKind::InvalidLangItem {
368 reason: format!("lang item `{}` is bound twice", item.name()),
369 },
370 p.site,
371 ));
372 }
373 *slot = Some(id);
374 }
375 PendingKind::Enum(item, id) => {
376 let slot: &mut Option<EnumId> = match item {
377 LangEnumItem::Ordering => &mut self.lang_items.ordering,
378 };
379 if let Some(_existing) = *slot {
380 return Err(CompileError::new(
381 ErrorKind::InvalidLangItem {
382 reason: format!("lang item `{}` is bound twice", item.name()),
383 },
384 p.site,
385 ));
386 }
387 *slot = Some(id);
388 }
389 PendingKind::Fn(item, sym) => {
390 let slot: &mut Option<Spur> = match item {
391 LangFnItem::Vec => &mut self.lang_items.vec_fn,
392 };
393 if let Some(_existing) = *slot {
394 return Err(CompileError::new(
395 ErrorKind::InvalidLangItem {
396 reason: format!("lang item `{}` is bound twice", item.name()),
397 },
398 p.site,
399 ));
400 }
401 *slot = Some(sym);
402 }
403 }
404 }
405 Ok(())
406 }
407
408 /// Return true iff the directive's source file is part of the
409 /// prelude (top-level `prelude/` directory). Recognizes:
410 ///
411 /// - `FileId::PRELUDE` itself (the prelude root).
412 /// - File IDs in the high reserved band (0xFFFF_F000 ..=
413 /// FileId::PRELUDE) that the prelude loader assigns to submodules
414 /// counting down from PRELUDE. Test fixtures inline submodules
415 /// without registering paths, but the file IDs they use sit in
416 /// this band.
417 /// - Any registered path that satisfies `is_prelude_path`.
418 fn is_directive_in_prelude(&self, span: Span) -> bool {
419 let id = span.file_id.index();
420 if id >= 0xFFFF_F000 {
421 return true;
422 }
423 match self.file_paths.get(&span.file_id) {
424 Some(path) => is_prelude_path(path),
425 // Without a registered path, default to denying — user
426 // files always have a path; absence implies a virtual or
427 // ungated source we don't trust to use `@lang(...)`.
428 None => false,
429 }
430 }
431}
432
433#[derive(Clone, Copy)]
434enum DeclKind {
435 Interface,
436 Enum,
437 Fn,
438}