gruel_air/sema/declarations.rs
1//! Declaration gathering for semantic analysis.
2//!
3//! This module handles the first phase of semantic analysis: gathering all
4//! type and function declarations from the RIR. This includes:
5//!
6//! - Registering struct and enum type names
7//! - Resolving struct field types
8//! - Collecting function signatures
9//! - Collecting method signatures from impl blocks
10//! - Validating @derive(Copy) and @handle structs
11
12use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
13
14use gruel_builtins::{
15 Abi, BUILTIN_MARKERS, ItemKinds, MarkerKind, Posture, ThreadSafety, get_builtin_marker,
16 is_reserved_type_constructor_name,
17};
18use gruel_rir::{InstData, InstRef, RirParamMode};
19use gruel_util::Span;
20use gruel_util::{CompileError, CompileResult, ErrorKind, ice};
21use lasso::Spur;
22
23use super::anon_interfaces::decode_receiver_mode;
24use super::{ConstInfo, FunctionInfo, InferenceContext, MethodInfo, Sema};
25use crate::inference::{FunctionSig, MethodSig};
26use crate::types::parse_type_call_syntax;
27use crate::types::{
28 EnumDef, EnumId, EnumVariantDef, InterfaceDef, InterfaceId, InterfaceMethodReq, ReceiverMode,
29 StructDef, StructField, StructId, Type,
30};
31
32/// Posture declared by the user on a type definition (ADR-0080 / ADR-0083).
33///
34/// `Unmarked` is the default — the user did not write a `copy`/`linear`
35/// keyword nor any `@mark(posture)` directive. Under uniform structural
36/// inference (ADR-0083), unmarked types pick up their final posture from
37/// their members; marked types must be consistent with the declaration.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39enum DeclaredPosture {
40 Copy,
41 Affine,
42 Linear,
43 Unmarked,
44}
45
46impl DeclaredPosture {
47 fn as_str(self) -> &'static str {
48 match self {
49 DeclaredPosture::Copy => "copy",
50 DeclaredPosture::Affine => "affine",
51 DeclaredPosture::Linear => "linear",
52 DeclaredPosture::Unmarked => "unmarked",
53 }
54 }
55}
56
57/// ADR-0083 / ADR-0084: outcome of processing the `@mark(...)` directive
58/// list on a type declaration. Posture markers (`copy`/`affine`/`linear`)
59/// and thread-safety markers (`unsend`/`checked_send`/`checked_sync`)
60/// occupy independent slots — a type may carry one of each.
61#[derive(Debug, Clone, Copy, Default)]
62pub(crate) struct MarkOutcome {
63 pub copy: bool,
64 pub affine: bool,
65 pub linear: bool,
66 /// ADR-0084: thread-safety override (`unsend`, `checked_send`,
67 /// `checked_sync`). At most one is permitted per directive list.
68 /// Behind the `thread_safety` preview gate.
69 pub thread_safety_override: Option<ThreadSafety>,
70 /// ADR-0085: C ABI / C layout marker (`@mark(c)`). On a struct,
71 /// switches layout to C rules and makes the type eligible to cross
72 /// the FFI boundary by value. Behind the `c_ffi` preview gate.
73 pub c_layout: bool,
74}
75
76/// Posture observed on a member's type (ADR-0080).
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78enum MemberPosture {
79 Copy,
80 Affine,
81 Linear,
82}
83
84impl MemberPosture {
85 fn as_str(self) -> &'static str {
86 match self {
87 MemberPosture::Copy => "Copy",
88 MemberPosture::Affine => "affine",
89 MemberPosture::Linear => "linear",
90 }
91 }
92}
93
94/// ADR-0080 / ADR-0083: compare a member's posture against the host's
95/// declared posture and produce a `PostureMismatch` error if the rule is
96/// violated.
97///
98/// Rules:
99///
100/// - `Copy` host: every member must be Copy.
101/// - `Affine` host (`@mark(affine)`): no member may be Linear (linear is
102/// contagious upward and cannot be hidden behind an affine declaration,
103/// ADR-0083).
104/// - `Linear` host: accepts anything.
105/// - `Unmarked`: no constraint here — under uniform inference the final
106/// posture is the inferred posture, never an error.
107fn check_posture_against_declared(
108 host_kind: &'static str,
109 host_name: &str,
110 declared: DeclaredPosture,
111 member_kind: &'static str,
112 member_name: &str,
113 member_type: &str,
114 member_posture: MemberPosture,
115 span: Span,
116) -> Option<CompileError> {
117 let violates = matches!(
118 (declared, member_posture),
119 (
120 DeclaredPosture::Copy,
121 MemberPosture::Affine | MemberPosture::Linear
122 ) | (DeclaredPosture::Affine, MemberPosture::Linear)
123 );
124 if !violates {
125 return None;
126 }
127 Some(CompileError::new(
128 ErrorKind::PostureMismatch(Box::new(gruel_util::PostureMismatchError {
129 host_kind,
130 host_name: host_name.to_string(),
131 declared_posture: declared.as_str(),
132 member_kind,
133 member_name: member_name.to_string(),
134 member_type: member_type.to_string(),
135 member_posture: member_posture.as_str(),
136 })),
137 span,
138 ))
139}
140
141/// Signature data for a `drop` method, bundled for inline-drop registration.
142struct DropSignature {
143 has_self: bool,
144 params_len: u32,
145 return_type: Spur,
146 body: InstRef,
147 span: Span,
148}
149
150/// Visibility / safety flags on a top-level function declaration.
151#[derive(Clone, Copy)]
152struct FnFlags {
153 is_pub: bool,
154 is_unchecked: bool,
155 /// Index into the extra array where this fn's directives start
156 /// (ADR-0085: used to detect `@mark(c)` / `@link_name(...)`).
157 directives_start: u32,
158 /// Number of directives.
159 directives_len: u32,
160}
161
162impl<'a> Sema<'a> {
163 /// Build an `InferenceContext` from the collected type information.
164 ///
165 /// This should be called after the collection phase and builds the
166 /// pre-computed maps needed for Hindley-Milner type inference.
167 /// Building this once and reusing for all function analyses avoids
168 /// the O(n²) cost of rebuilding these maps per function.
169 ///
170 /// # Performance
171 ///
172 /// This converts all function/method signatures to use `InferType`
173 /// (which handles arrays structurally rather than by ID). This conversion
174 /// is done once instead of per-function.
175 pub fn build_inference_context(&self) -> InferenceContext {
176 // Build function signatures with InferType for constraint generation
177 let func_sigs: HashMap<Spur, FunctionSig> = self
178 .functions
179 .iter()
180 .map(|(name, info)| {
181 (
182 *name,
183 FunctionSig {
184 param_types: self
185 .param_arena
186 .types(info.params)
187 .iter()
188 .map(|t| self.type_to_infer_type(*t))
189 .collect(),
190 return_type: self.type_to_infer_type(info.return_type),
191 is_generic: info.is_generic,
192 param_modes: self.param_arena.modes(info.params).to_vec(),
193 param_comptime: self.param_arena.comptime(info.params).to_vec(),
194 param_names: self.param_arena.names(info.params).to_vec(),
195 return_type_sym: info.return_type_sym,
196 },
197 )
198 })
199 .collect();
200
201 // Build struct types map (name -> Type::new_struct(id))
202 let struct_types: HashMap<Spur, Type> = self
203 .structs
204 .iter()
205 .map(|(name, id)| (*name, Type::new_struct(*id)))
206 .collect();
207
208 // Build enum types map (name -> Type::new_enum(id))
209 let enum_types: HashMap<Spur, Type> = self
210 .enums
211 .iter()
212 .map(|(name, id)| (*name, Type::new_enum(*id)))
213 .collect();
214
215 // Build method signatures with InferType for constraint generation.
216 // Method-level-generic methods (ADR-0055) are excluded: their stored
217 // `return_type` is a `COMPTIME_TYPE` placeholder, and their param
218 // types may reference unresolved method-level type params. The
219 // inference pass falls back to fresh type variables for calls to
220 // these methods, and the specialized bodies (synthesized by
221 // `specialize::create_specialized`) are analyzed separately with the
222 // concrete substitutions in place.
223 let method_sigs: HashMap<(StructId, Spur), MethodSig> = self
224 .methods
225 .iter()
226 .filter(|(_, info)| !info.is_generic)
227 .map(|((struct_id, method_name), info)| {
228 (
229 (*struct_id, *method_name),
230 MethodSig {
231 struct_type: info.struct_type,
232 has_self: info.has_self,
233 param_types: self
234 .param_arena
235 .types(info.params)
236 .iter()
237 .map(|t| self.type_to_infer_type(*t))
238 .collect(),
239 return_type: self.type_to_infer_type(info.return_type),
240 },
241 )
242 })
243 .collect();
244
245 // Build enum method signatures for constraint generation
246 let enum_method_sigs: HashMap<(EnumId, Spur), MethodSig> = self
247 .enum_methods
248 .iter()
249 .map(|((enum_id, method_name), info)| {
250 (
251 (*enum_id, *method_name),
252 MethodSig {
253 struct_type: info.struct_type,
254 has_self: info.has_self,
255 param_types: self
256 .param_arena
257 .types(info.params)
258 .iter()
259 .map(|t| self.type_to_infer_type(*t))
260 .collect(),
261 return_type: self.type_to_infer_type(info.return_type),
262 },
263 )
264 })
265 .collect();
266
267 InferenceContext {
268 func_sigs,
269 struct_types,
270 enum_types,
271 method_sigs,
272 enum_method_sigs,
273 }
274 }
275 // ADR-0080 retired `has_copy_directive`: `@derive(Copy)` no longer
276 // resolves to a known interface, so the directive falls through the
277 // existing "unknown interface" derive resolver path.
278
279 /// ADR-0075: walk every directive collected during parsing and reject any
280 /// whose name isn't in the recognized set (`@allow`, `@derive`). Retired
281 /// directives (`@handle`, `@copy`) get a targeted retirement note; other
282 /// unknowns get an edit-distance suggestion when there's a near-match.
283 pub(crate) fn validate_directives(&self) -> CompileResult<()> {
284 for (_, inst) in self.rir.iter() {
285 let (start, len, _is_fn_decl) = match &inst.data {
286 InstData::StructDecl {
287 directives_start,
288 directives_len,
289 ..
290 }
291 | InstData::Alloc {
292 directives_start,
293 directives_len,
294 ..
295 }
296 | InstData::ConstDecl {
297 directives_start,
298 directives_len,
299 ..
300 }
301 | InstData::AnonStructType {
302 directives_start,
303 directives_len,
304 ..
305 }
306 | InstData::AnonEnumType {
307 directives_start,
308 directives_len,
309 ..
310 } => (*directives_start, *directives_len, false),
311 InstData::FnDecl {
312 directives_start,
313 directives_len,
314 ..
315 } => (*directives_start, *directives_len, true),
316 _ => continue,
317 };
318 if len == 0 {
319 continue;
320 }
321 for directive in self.rir.get_directives(start, len) {
322 let name = self.interner.resolve(&directive.name).to_string();
323 if name == "allow"
324 || name == "derive"
325 || name == "lang"
326 || name == "mark"
327 || name == "link_name"
328 {
329 continue;
330 }
331 let note = directive_diagnosis_note(&name);
332 return Err(CompileError::new(
333 ErrorKind::UnknownDirective { name, note },
334 directive.span,
335 ));
336 }
337 }
338 Ok(())
339 }
340
341 /// Phase 1: Register all type names (enum and struct IDs).
342 ///
343 /// This creates name → ID mappings for all enums and structs in a single pass,
344 /// allowing types to reference each other in any order. Struct definitions are
345 /// created with placeholder empty fields that will be filled in during phase 2.
346 /// Validate and register `interface` declarations (ADR-0056).
347 ///
348 /// - Gates each declaration behind the `interfaces` preview feature.
349 /// - Rejects duplicate method names within a single interface.
350 /// - Rejects collisions between interface names and existing structs/enums
351 /// (and other interfaces).
352 /// - Resolves each method signature's parameter and return types and
353 /// stores an `InterfaceDef` on `Sema`.
354 ///
355 /// Conformance and dispatch wiring land in later phases.
356 pub(crate) fn validate_interface_decls(&mut self) -> CompileResult<()> {
357 // Two-pass walk: first collect raw decls (snapshotting RIR data so we
358 // can mutate Sema while resolving types), then register each one.
359 struct RawIface {
360 name: Spur,
361 is_pub: bool,
362 file_id: gruel_util::FileId,
363 decl_span: Span,
364 methods: Vec<RawIfaceMethod>,
365 }
366 struct RawIfaceMethod {
367 name: Spur,
368 params: Vec<(Spur, Spur)>, // (param_name, type_symbol)
369 return_type_sym: Spur,
370 receiver: ReceiverMode,
371 /// ADR-0088: whether this interface method signature is
372 /// `@mark(unchecked)`.
373 is_unchecked: bool,
374 span: Span,
375 }
376
377 let mut raw: Vec<RawIface> = Vec::new();
378 for (_, inst) in self.rir.iter() {
379 if let InstData::InterfaceDecl {
380 is_pub,
381 name,
382 methods_start,
383 methods_len,
384 directives_start,
385 directives_len,
386 } = &inst.data
387 {
388 // ADR-0088: `@mark(...)` is not legal on interface
389 // declaration heads. Interfaces don't carry ABI
390 // (`@mark(c)`), posture (`@mark(copy|affine|linear)`),
391 // thread-safety overrides, or per-fn `@mark(unchecked)`
392 // — those all attach to fn / struct / enum
393 // declarations. `@lang("…")` is the only directive
394 // currently legal on an interface head.
395 let directives = self.rir.get_directives(*directives_start, *directives_len);
396 for d in directives.iter() {
397 if self.interner.resolve(&d.name) != "mark" {
398 continue;
399 }
400 // Any `@mark(arg)` on an interface head is rejected
401 // — surface the first arg's name in the diagnostic.
402 if let Some(arg) = d.args.first() {
403 let arg_name = self.interner.resolve(arg);
404 return Err(CompileError::new(
405 ErrorKind::MarkerNotApplicable {
406 marker: arg_name.to_string(),
407 item_kind: "interface",
408 },
409 d.span,
410 ));
411 }
412 }
413 let method_refs = self.rir.get_inst_refs(*methods_start, *methods_len);
414 let mut seen: HashSet<Spur> = HashSet::default();
415 let mut methods = Vec::new();
416 for method_ref in method_refs {
417 let m = self.rir.get(method_ref);
418 if let InstData::InterfaceMethodSig {
419 name: method_name,
420 params_start,
421 params_len,
422 return_type,
423 receiver_mode,
424 is_unchecked,
425 directives_start,
426 directives_len,
427 } = &m.data
428 {
429 if !seen.insert(*method_name) {
430 let iface_name = self.interner.resolve(name).to_string();
431 let method_name_str = self.interner.resolve(method_name).to_string();
432 return Err(CompileError::new(
433 ErrorKind::DuplicateMethod {
434 type_name: format!("interface `{}`", iface_name),
435 method_name: method_name_str,
436 },
437 m.span,
438 ));
439 }
440
441 // ADR-0088: only `@mark(unchecked)` is legal on
442 // interface method signatures. Reject any other
443 // marker (e.g. `@mark(c)`, `@mark(copy)`).
444 let method_directives =
445 self.rir.get_directives(*directives_start, *directives_len);
446 for d in method_directives.iter() {
447 if self.interner.resolve(&d.name) != "mark" {
448 continue;
449 }
450 for arg in &d.args {
451 let arg_name = self.interner.resolve(arg);
452 if arg_name != "unchecked" {
453 return Err(CompileError::new(
454 ErrorKind::MarkerNotApplicable {
455 marker: arg_name.to_string(),
456 item_kind: "interface method",
457 },
458 d.span,
459 ));
460 }
461 }
462 }
463 let params = self
464 .rir
465 .get_params(*params_start, *params_len)
466 .into_iter()
467 .map(|p| (p.name, p.ty))
468 .collect();
469 let receiver = match *receiver_mode {
470 1 => ReceiverMode::MutRef,
471 2 => ReceiverMode::Ref,
472 _ => ReceiverMode::ByValue,
473 };
474 methods.push(RawIfaceMethod {
475 name: *method_name,
476 params,
477 return_type_sym: *return_type,
478 receiver,
479 is_unchecked: *is_unchecked,
480 span: m.span,
481 });
482 }
483 }
484
485 raw.push(RawIface {
486 name: *name,
487 is_pub: *is_pub,
488 file_id: inst.span.file_id,
489 decl_span: inst.span,
490 methods,
491 });
492 }
493 }
494
495 for r in raw {
496 // Reject collision with existing structs/enums/interfaces.
497 let name_str = self.interner.resolve(&r.name).to_string();
498 if self.structs.contains_key(&r.name)
499 || self.enums.contains_key(&r.name)
500 || self.interfaces.contains_key(&r.name)
501 {
502 return Err(CompileError::new(
503 ErrorKind::DuplicateTypeDefinition {
504 type_name: format!("interface `{}`", name_str),
505 },
506 r.decl_span,
507 ));
508 }
509
510 // Resolve each method's parameter and return slots, recognizing
511 // the symbol `Self` as `IfaceTy::SelfType` (ADR-0060). Receiver
512 // mode is `ByValue` until Phase 2 threads `inout`/`borrow self`
513 // from the parser.
514 let mut resolved_methods = Vec::with_capacity(r.methods.len());
515 for m in &r.methods {
516 let mut param_types = Vec::with_capacity(m.params.len());
517 for (_pname, ty_sym) in &m.params {
518 param_types.push(self.resolve_iface_ty(*ty_sym, m.span)?);
519 }
520 let return_type = self.resolve_iface_ty(m.return_type_sym, m.span)?;
521 resolved_methods.push(InterfaceMethodReq {
522 name: self.interner.resolve(&m.name).to_string(),
523 receiver: m.receiver,
524 param_types,
525 return_type,
526 is_unchecked: m.is_unchecked,
527 });
528 }
529
530 let id = InterfaceId(self.interface_defs.len() as u32);
531 self.interface_defs.push(InterfaceDef {
532 name: name_str,
533 methods: resolved_methods,
534 is_pub: r.is_pub,
535 file_id: r.file_id,
536 });
537 self.interfaces.insert(r.name, id);
538 }
539
540 Ok(())
541 }
542
543 pub(crate) fn register_type_names(&mut self) -> CompileResult<()> {
544 for (_, inst) in self.rir.iter() {
545 match &inst.data {
546 InstData::EnumDecl {
547 is_pub,
548 name,
549 variants_start,
550 variants_len,
551 directives_start,
552 directives_len,
553 ..
554 } => {
555 let enum_name = self.interner.resolve(name).to_string();
556
557 // ADR-0083 Phase 4: posture is declared exclusively via
558 // the `@mark(...)` directive — the keyword path retired.
559 // `@mark(affine)` is tracked separately on `MarkOutcome`
560 // because `Posture::Affine` is the default and the
561 // marker existing affects later inference suppression.
562 let mark_outcome = self.process_mark_directives(
563 *directives_start,
564 *directives_len,
565 "enum",
566 ItemKinds::ENUM,
567 inst.span,
568 )?;
569
570 if mark_outcome.copy && mark_outcome.linear {
571 return Err(CompileError::new(
572 ErrorKind::LinearStructCopy(enum_name.clone()),
573 inst.span,
574 ));
575 }
576 // ADR-0083: `@mark(affine)` cannot coexist with Copy or
577 // Linear declarations.
578 if mark_outcome.affine && (mark_outcome.copy || mark_outcome.linear) {
579 return Err(CompileError::new(
580 ErrorKind::LinearStructCopy(enum_name.clone()),
581 inst.span,
582 ));
583 }
584 let posture = if mark_outcome.copy {
585 Posture::Copy
586 } else if mark_outcome.linear {
587 Posture::Linear
588 } else {
589 Posture::Affine
590 };
591
592 // Check for collision with built-in type constructors
593 // (e.g. Ptr, MutPtr — see ADR-0061). ADR-0081 retired
594 // the BUILTIN_TYPES registry; collisions with prelude
595 // types are caught by the duplicate-type check below.
596 if is_reserved_type_constructor_name(&enum_name) {
597 return Err(CompileError::new(
598 ErrorKind::ReservedTypeName {
599 type_name: enum_name,
600 },
601 inst.span,
602 ));
603 }
604
605 // Check for duplicate type definitions (struct or enum with same name)
606 if self.enums.contains_key(name) || self.structs.contains_key(name) {
607 return Err(CompileError::new(
608 ErrorKind::DuplicateTypeDefinition {
609 type_name: enum_name,
610 },
611 inst.span,
612 ));
613 }
614
615 let raw_variants = self
616 .rir
617 .get_enum_variant_decls(*variants_start, *variants_len);
618
619 // Check for duplicate variant names
620 let mut seen_variants: HashSet<Spur> = HashSet::default();
621 for (variant_name, _, field_names) in &raw_variants {
622 if !seen_variants.insert(*variant_name) {
623 let variant_name_str = self.interner.resolve(variant_name).to_string();
624 return Err(CompileError::new(
625 ErrorKind::DuplicateVariant {
626 enum_name: enum_name.clone(),
627 variant_name: variant_name_str,
628 },
629 inst.span,
630 ));
631 }
632
633 if !field_names.is_empty() {
634 // Check for duplicate field names within the struct variant
635 let variant_str = self.interner.resolve(variant_name).to_string();
636 let mut seen_fields: HashSet<Spur> = HashSet::default();
637 for field_name in field_names {
638 if !seen_fields.insert(*field_name) {
639 let field_str = self.interner.resolve(field_name).to_string();
640 return Err(CompileError::new(
641 ErrorKind::DuplicateField {
642 struct_name: format!("{}::{}", enum_name, variant_str),
643 field_name: field_str,
644 },
645 inst.span,
646 ));
647 }
648 }
649 }
650 }
651
652 // Build EnumVariantDef list. Field types are stored as unit for now;
653 // full type resolution will be added in later phases when we
654 // lower them through the type checker.
655 let variants: Vec<EnumVariantDef> = raw_variants
656 .iter()
657 .map(|(vname, _fields, field_names)| EnumVariantDef {
658 name: self.interner.resolve(vname).to_string(),
659 fields: Vec::new(), // Field types resolved in later phases
660 field_names: field_names
661 .iter()
662 .map(|n| self.interner.resolve(n).to_string())
663 .collect(),
664 })
665 .collect();
666
667 let enum_def = EnumDef {
668 name: enum_name,
669 variants,
670 posture,
671 // ADR-0084: placeholder. The structural minimum
672 // and any `@mark(...)` override are folded in
673 // by `validate_consistency` once fields resolve.
674 // Defaulting to the user's explicit override (if
675 // any) lets `is_thread_safety_type` give a sane
676 // answer in the rare cases that consult the
677 // field before the inference pass runs.
678 thread_safety: mark_outcome
679 .thread_safety_override
680 .unwrap_or(ThreadSafety::Sync),
681 is_pub: *is_pub,
682 file_id: inst.span.file_id,
683 destructor: None,
684 // ADR-0086: `@mark(c)` flag flows from the
685 // directive walker via `mark_outcome.c_layout`.
686 is_c_layout: mark_outcome.c_layout,
687 };
688
689 // Register in type pool and get pool-based EnumId
690 let (enum_id, _) = self.type_pool.register_enum(*name, enum_def);
691
692 // Register in enum lookup with pool-based EnumId
693 self.enums.insert(*name, enum_id);
694
695 if mark_outcome.affine {
696 self.mark_affine_decls.insert(*name);
697 }
698 // ADR-0084: track thread-safety overrides so
699 // `validate_consistency` can apply them after
700 // computing the structural minimum.
701 if let Some(level) = mark_outcome.thread_safety_override {
702 self.mark_thread_safety_decls.insert(*name, level);
703 }
704 }
705 InstData::StructDecl {
706 directives_start,
707 directives_len,
708 is_pub,
709 name,
710 ..
711 } => {
712 let struct_name = self.interner.resolve(name).to_string();
713
714 // Check for collision with built-in type constructors
715 // (e.g. Ptr, MutPtr — see ADR-0061). ADR-0081 retired
716 // the BUILTIN_TYPES registry; collisions with prelude
717 // types are caught by the duplicate-type check below.
718 if is_reserved_type_constructor_name(&struct_name) {
719 return Err(CompileError::new(
720 ErrorKind::ReservedTypeName {
721 type_name: struct_name,
722 },
723 inst.span,
724 ));
725 }
726
727 // Check for duplicate type definitions (struct or enum with same name)
728 if self.structs.contains_key(name) || self.enums.contains_key(name) {
729 return Err(CompileError::new(
730 ErrorKind::DuplicateTypeDefinition {
731 type_name: struct_name,
732 },
733 inst.span,
734 ));
735 }
736
737 // ADR-0083 Phase 4: posture is declared exclusively via
738 // the `@mark(...)` directive — the keyword path retired.
739 let mark_outcome = self.process_mark_directives(
740 *directives_start,
741 *directives_len,
742 "struct",
743 ItemKinds::STRUCT,
744 inst.span,
745 )?;
746
747 // Linear types cannot be Copy.
748 if mark_outcome.linear && mark_outcome.copy {
749 return Err(CompileError::new(
750 ErrorKind::LinearStructCopy(struct_name.clone()),
751 inst.span,
752 ));
753 }
754 if mark_outcome.affine && (mark_outcome.copy || mark_outcome.linear) {
755 return Err(CompileError::new(
756 ErrorKind::LinearStructCopy(struct_name.clone()),
757 inst.span,
758 ));
759 }
760 let posture = if mark_outcome.copy {
761 Posture::Copy
762 } else if mark_outcome.linear {
763 Posture::Linear
764 } else {
765 Posture::Affine
766 };
767
768 // Create placeholder struct def (fields will be resolved in phase 2)
769 let struct_def = StructDef {
770 name: struct_name,
771 fields: Vec::new(), // Filled in during resolve_declarations
772 posture,
773 is_clone: false, // Filled in during resolve_declarations after fields known
774 // ADR-0084: see EnumDecl above for rationale.
775 thread_safety: mark_outcome
776 .thread_safety_override
777 .unwrap_or(ThreadSafety::Sync),
778 destructor: None, // Filled in during resolve_declarations
779 is_builtin: false, // User-defined struct
780 is_pub: *is_pub,
781 file_id: inst.span.file_id,
782 // ADR-0085: track whether `@mark(c)` was applied.
783 is_c_layout: mark_outcome.c_layout,
784 };
785
786 // Register in type pool and get pool-based StructId
787 let (struct_id, _) = self.type_pool.register_struct(*name, struct_def);
788
789 // Register in struct lookup with pool-based StructId
790 self.structs.insert(*name, struct_id);
791
792 if mark_outcome.affine {
793 self.mark_affine_decls.insert(*name);
794 }
795 // ADR-0084: track thread-safety overrides — see
796 // EnumDecl above.
797 if let Some(level) = mark_outcome.thread_safety_override {
798 self.mark_thread_safety_decls.insert(*name, level);
799 }
800 }
801 _ => {}
802 }
803 }
804 Ok(())
805 }
806
807 /// ADR-0083 / ADR-0084: apply `@mark(...)` directives recorded on
808 /// an anonymous struct expression to its already-created `StructDef`.
809 /// Mirrors what `validate_consistency` does for named declarations:
810 /// reads the marker outcome, then writes posture and thread-safety
811 /// fields. Called from the comptime evaluator and analysis paths
812 /// after `find_or_create_anon_struct` returns a fresh `StructId`.
813 pub(crate) fn apply_anon_struct_marks(
814 &mut self,
815 struct_id: StructId,
816 directives_start: u32,
817 directives_len: u32,
818 span: Span,
819 ) -> CompileResult<()> {
820 let outcome = self.process_mark_directives(
821 directives_start,
822 directives_len,
823 "struct",
824 ItemKinds::STRUCT,
825 span,
826 )?;
827 let mut def = self.type_pool.struct_def(struct_id);
828 // Posture: a marker overrides the structurally-inferred posture
829 // that `find_or_create_anon_struct` set.
830 if outcome.copy {
831 def.posture = Posture::Copy;
832 }
833 if outcome.linear {
834 def.posture = Posture::Linear;
835 }
836 if outcome.affine {
837 def.posture = Posture::Affine;
838 }
839 // Thread-safety: marker wins over the structural minimum the
840 // anon-struct constructor pre-computed.
841 if let Some(level) = outcome.thread_safety_override {
842 def.thread_safety = level;
843 }
844 self.type_pool.update_struct_def(struct_id, def);
845 Ok(())
846 }
847
848 /// Same as `apply_anon_struct_marks`, but for anonymous enums
849 /// (variant-payload structural fold + override).
850 pub(crate) fn apply_anon_enum_marks(
851 &mut self,
852 enum_id: EnumId,
853 directives_start: u32,
854 directives_len: u32,
855 span: Span,
856 ) -> CompileResult<()> {
857 let outcome = self.process_mark_directives(
858 directives_start,
859 directives_len,
860 "enum",
861 ItemKinds::ENUM,
862 span,
863 )?;
864 let mut def = self.type_pool.enum_def(enum_id);
865 if outcome.copy {
866 def.posture = Posture::Copy;
867 }
868 if outcome.linear {
869 def.posture = Posture::Linear;
870 }
871 if outcome.affine {
872 def.posture = Posture::Affine;
873 }
874 if let Some(level) = outcome.thread_safety_override {
875 def.thread_safety = level;
876 }
877 self.type_pool.update_enum_def(enum_id, def);
878 Ok(())
879 }
880
881 /// ADR-0083: process the `@mark(...)` directives on a type declaration.
882 ///
883 /// Walks the directive list and validates each marker against the
884 /// `BUILTIN_MARKERS` registry. Returns a flag-set describing which
885 /// posture markers were declared so the caller can fold them into the
886 /// type's `posture` field (and the `mark_affine_decls`
887 /// side set).
888 ///
889 /// Errors out on:
890 /// - Unknown markers (with edit-distance suggestions).
891 /// - Markers not applicable to the host item kind.
892 /// - Conflicting posture markers within the same directive list (e.g.
893 /// `@mark(copy)` + `@mark(linear)`).
894 /// - Conflicting thread-safety markers (e.g. `@mark(unsend)` +
895 /// `@mark(checked_send)`) — ADR-0084.
896 /// - Use of a thread-safety marker without `--preview thread_safety`.
897 pub(crate) fn process_mark_directives(
898 &self,
899 directives_start: u32,
900 directives_len: u32,
901 item_kind_name: &'static str,
902 item_kind: ItemKinds,
903 span: Span,
904 ) -> CompileResult<MarkOutcome> {
905 let mut outcome = MarkOutcome::default();
906 if directives_len == 0 {
907 return Ok(outcome);
908 }
909 let directives = self.rir.get_directives(directives_start, directives_len);
910 for directive in directives.iter() {
911 let dir_name = self.interner.resolve(&directive.name);
912 if dir_name != "mark" {
913 continue;
914 }
915 if directive.args.is_empty() {
916 return Err(CompileError::new(
917 ErrorKind::UnknownMarker {
918 name: String::new(),
919 note: Some(format!(
920 "`@mark(...)` requires at least one marker name; valid markers: {}",
921 BUILTIN_MARKERS
922 .iter()
923 .map(|m| m.name)
924 .collect::<Vec<_>>()
925 .join(", ")
926 )),
927 },
928 directive.span,
929 ));
930 }
931 for arg in &directive.args {
932 let marker_name = self.interner.resolve(arg).to_string();
933 let marker = match get_builtin_marker(&marker_name) {
934 Some(m) => m,
935 None => {
936 let note = marker_diagnosis_note(&marker_name);
937 return Err(CompileError::new(
938 ErrorKind::UnknownMarker {
939 name: marker_name,
940 note,
941 },
942 directive.span,
943 ));
944 }
945 };
946 let applicable = match item_kind {
947 k if k == ItemKinds::STRUCT => marker.applicable_to.includes_struct(),
948 k if k == ItemKinds::ENUM => marker.applicable_to.includes_enum(),
949 _ => true,
950 };
951 if !applicable {
952 return Err(CompileError::new(
953 ErrorKind::MarkerNotApplicable {
954 marker: marker.name.to_string(),
955 item_kind: item_kind_name,
956 },
957 directive.span,
958 ));
959 }
960 match marker.kind {
961 MarkerKind::Posture(Posture::Copy) => outcome.copy = true,
962 MarkerKind::Posture(Posture::Affine) => outcome.affine = true,
963 MarkerKind::Posture(Posture::Linear) => outcome.linear = true,
964 MarkerKind::ThreadSafety(level) => {
965 // ADR-0084: thread-safety markers stabilized
966 // — no preview gate after Phase 7.
967 if let Some(prev) = outcome.thread_safety_override
968 && prev != level
969 {
970 return Err(CompileError::new(
971 ErrorKind::ConflictingThreadSafetyMarkers {
972 type_name: item_kind_name.to_string(),
973 },
974 span,
975 ));
976 }
977 outcome.thread_safety_override = Some(level);
978 }
979 MarkerKind::Abi(Abi::C) => {
980 // ADR-0085 stabilised `@mark(c)` on fns / structs;
981 // ADR-0086 stabilised it on enums. The marker is
982 // already known to be applicable to the host item
983 // kind by this point.
984 outcome.c_layout = true;
985 }
986 MarkerKind::Unchecked => {
987 // ADR-0088: `@mark(unchecked)` only applies to
988 // fn-like declarations. The marker registry's
989 // `applicable_to` field already filters this out
990 // for struct/enum decls, but if we get here on a
991 // non-fn item we surface the not-applicable
992 // error rather than silently dropping it.
993 return Err(CompileError::new(
994 ErrorKind::MarkerNotApplicable {
995 marker: marker.name.to_string(),
996 item_kind: item_kind_name,
997 },
998 directive.span,
999 ));
1000 }
1001 }
1002 }
1003 }
1004 // Mutual exclusion across all `@mark` directives on this item.
1005 let posture_count = (outcome.copy as u8) + (outcome.affine as u8) + (outcome.linear as u8);
1006 if posture_count > 1 {
1007 // Reuse the existing `LinearStructCopy` diagnostic spelling for
1008 // Copy + Linear; for the rarer Copy + Affine / Affine + Linear
1009 // combinations we still want a clear error, so we fall back to
1010 // the same kind with a generic name placeholder.
1011 return Err(CompileError::new(
1012 ErrorKind::LinearStructCopy("<conflicting `@mark(...)` markers>".to_string()),
1013 span,
1014 ));
1015 }
1016 Ok(outcome)
1017 }
1018
1019 /// Phase 2: Resolve all declarations.
1020 ///
1021 /// Now that all type names are registered, this resolves:
1022 /// - Struct field types (must be done first for @derive(Copy) validation)
1023 /// - @derive(Copy) struct validation, destructors, functions, and methods
1024 ///
1025 /// # Array Type Registration
1026 ///
1027 /// Array types from explicit type annotations (struct fields, function parameters,
1028 /// return types, local variable annotations) are registered during this phase via
1029 /// `resolve_type()` calls. Array types from literals (inferred during HM inference)
1030 /// are created on-demand via the thread-safe `TypeInternPool` during function
1031 /// body analysis.
1032 pub(crate) fn resolve_declarations(&mut self) -> CompileResult<()> {
1033 // ADR-0075: surface unrecognized `@name` directives before any
1034 // downstream pass that would silently ignore them.
1035 self.validate_directives()?;
1036 // Derives (ADR-0058) are gated by the `comptime_derives` preview
1037 // feature. Reject any `derive` item before we touch the rest of
1038 // declaration gathering so users get a clear diagnostic.
1039 self.validate_derive_decls()?;
1040 // Interfaces (ADR-0056) must be registered before struct/enum field
1041 // and method-param resolution so that:
1042 // - using an interface name as a struct field type produces a
1043 // helpful diagnostic redirecting to the comptime path
1044 // - `comptime T: SomeInterface` bounds are recognized when
1045 // functions and methods are gathered
1046 // - `borrow t: SomeInterface` parameter types resolve correctly
1047 self.validate_interface_decls()?;
1048 // ADR-0079: bind `@lang("…")` directives in the prelude to
1049 // interface/enum IDs. Must run after interfaces are registered
1050 // and before any sema pass that consults `lang_items`. Enum
1051 // bindings only resolve once `register_type_names` has populated
1052 // `self.enums`, which already happened in Phase 1.
1053 self.populate_lang_items()?;
1054 self.resolve_struct_fields()?;
1055 self.resolve_enum_variant_fields()?;
1056 // ADR-0080 Phase 3: validate that each named declaration's
1057 // declared posture (`copy` / unmarked / `linear`) is consistent
1058 // with the postures of its fields/variants. Runs after field
1059 // resolution so member types are populated. Anonymous types are
1060 // checked at construction time (see `find_or_create_anon_struct`
1061 // / `find_or_create_anon_enum`).
1062 self.validate_consistency()?;
1063 // ADR-0058 sub-phase: resolve every `@derive(D)` directive on a
1064 // named struct or enum into a binding for the upcoming expansion
1065 // step. Runs after field-type resolution so the host type is
1066 // already fully known.
1067 self.resolve_derive_directives()?;
1068 // ADR-0058 sub-phase: splice every `(host_type, derive_id)`
1069 // binding's methods into the host's method list. Runs after the
1070 // host's fields are known so destructor / Copy validation
1071 // (`resolve_remaining_declarations`) sees the attached methods.
1072 self.expand_derives()?;
1073 self.resolve_remaining_declarations()?;
1074 Ok(())
1075 }
1076
1077 /// Phase 4 of ADR-0058: walk every recorded `DeriveBinding` and splice
1078 /// the named derive's methods into the host type's method list. The
1079 /// same routine is reused at the anonymous-host call site (see
1080 /// `splice_derive_methods_into_struct`).
1081 pub(crate) fn expand_derives(&mut self) -> CompileResult<()> {
1082 // Snapshot the bindings so we can iterate while mutating
1083 // `self.methods`. Bindings are restored after expansion so the
1084 // analysis loop can drive each spliced method's body analysis.
1085 let bindings = self.derive_bindings.clone();
1086 for b in &bindings {
1087 // Anonymous-host expansion uses a different call site; named
1088 // bindings always carry a `host_name` that resolves through
1089 // `self.structs` (or, in the future, `self.enums`).
1090 if b.host_is_enum {
1091 let enum_id = match self.enums.get(&b.host_name).copied() {
1092 Some(id) => id,
1093 None => continue,
1094 };
1095 self.splice_derive_methods_into_enum(b.derive_name, enum_id, b.directive_span)?;
1096 } else {
1097 let struct_id = match self.structs.get(&b.host_name).copied() {
1098 Some(id) => id,
1099 None => continue,
1100 };
1101 self.splice_derive_methods_into_struct(b.derive_name, struct_id, b.directive_span)?;
1102 }
1103 }
1104 // Replace the (now-consumed) bindings — the cloned working set is
1105 // already done. Phase 4 keeps the original list around for the
1106 // analysis loop to drive body analysis later.
1107 self.derive_bindings = bindings;
1108 Ok(())
1109 }
1110
1111 /// Anonymous-host call site for ADR-0058: walk an anonymous-struct
1112 /// expression's `@derive(...)` directives and splice each derive's
1113 /// methods into the freshly-built host. Should be called exactly once
1114 /// per new anonymous `StructId` (the structural-dedup path skips this).
1115 pub(crate) fn splice_anon_struct_derives(
1116 &mut self,
1117 host_id: StructId,
1118 directives_start: u32,
1119 directives_len: u32,
1120 ) -> CompileResult<()> {
1121 if directives_len == 0 {
1122 return Ok(());
1123 }
1124 let derive_dir_sym = self.interner.get_or_intern("derive");
1125 let directives = self.rir.get_directives(directives_start, directives_len);
1126 for d in directives {
1127 if d.name != derive_dir_sym {
1128 continue;
1129 }
1130 if d.args.len() != 1 {
1131 return Err(CompileError::new(
1132 ErrorKind::DeriveNotADerive {
1133 name: "<wrong arg count>".to_string(),
1134 found: format!("{} arguments", d.args.len()),
1135 },
1136 d.span,
1137 ));
1138 }
1139 let derive_name = d.args[0];
1140 // ADR-0059: `@derive(Copy)` on an anonymous host is no-op for
1141 // method splicing; the posture bookkeeping flows through the
1142 // existing copy-directive path.
1143 if self.is_compiler_derive(derive_name) {
1144 continue;
1145 }
1146 let name_str = self.interner.resolve(&derive_name).to_string();
1147 if !self.derives.contains_key(&derive_name) {
1148 let found = if self.structs.contains_key(&derive_name) {
1149 "struct"
1150 } else if self.enums.contains_key(&derive_name) {
1151 "enum"
1152 } else if self.interfaces.contains_key(&derive_name) {
1153 "interface"
1154 } else if self.functions.contains_key(&derive_name) {
1155 "function"
1156 } else {
1157 "unknown name"
1158 };
1159 return Err(CompileError::new(
1160 ErrorKind::DeriveNotADerive {
1161 name: name_str,
1162 found: found.to_string(),
1163 },
1164 d.span,
1165 ));
1166 }
1167 self.splice_derive_methods_into_struct(derive_name, host_id, d.span)?;
1168 }
1169 Ok(())
1170 }
1171
1172 /// Anonymous-enum mirror of `splice_anon_struct_derives`.
1173 pub(crate) fn splice_anon_enum_derives(
1174 &mut self,
1175 host_id: EnumId,
1176 directives_start: u32,
1177 directives_len: u32,
1178 ) -> CompileResult<()> {
1179 if directives_len == 0 {
1180 return Ok(());
1181 }
1182 let derive_dir_sym = self.interner.get_or_intern("derive");
1183 let directives = self.rir.get_directives(directives_start, directives_len);
1184 for d in directives {
1185 if d.name != derive_dir_sym {
1186 continue;
1187 }
1188 if d.args.len() != 1 {
1189 return Err(CompileError::new(
1190 ErrorKind::DeriveNotADerive {
1191 name: "<wrong arg count>".to_string(),
1192 found: format!("{} arguments", d.args.len()),
1193 },
1194 d.span,
1195 ));
1196 }
1197 let derive_name = d.args[0];
1198 // ADR-0059: `@derive(Copy)` short-circuits — no methods to splice.
1199 if self.is_compiler_derive(derive_name) {
1200 continue;
1201 }
1202 let name_str = self.interner.resolve(&derive_name).to_string();
1203 if !self.derives.contains_key(&derive_name) {
1204 let found = if self.structs.contains_key(&derive_name) {
1205 "struct"
1206 } else if self.enums.contains_key(&derive_name) {
1207 "enum"
1208 } else if self.interfaces.contains_key(&derive_name) {
1209 "interface"
1210 } else if self.functions.contains_key(&derive_name) {
1211 "function"
1212 } else {
1213 "unknown name"
1214 };
1215 return Err(CompileError::new(
1216 ErrorKind::DeriveNotADerive {
1217 name: name_str,
1218 found: found.to_string(),
1219 },
1220 d.span,
1221 ));
1222 }
1223 self.splice_derive_methods_into_enum(derive_name, host_id, d.span)?;
1224 }
1225 Ok(())
1226 }
1227
1228 /// Splice every method of `derive_name` into the struct identified by
1229 /// `host_id`. `Self` is bound to the host struct's `Type`.
1230 pub(crate) fn splice_derive_methods_into_struct(
1231 &mut self,
1232 derive_name: Spur,
1233 host_id: StructId,
1234 directive_span: Span,
1235 ) -> CompileResult<()> {
1236 // Snapshot the per-derive method list (we'll borrow `self` mutably
1237 // when calling `resolve_param_type` below).
1238 let methods: Vec<crate::sema::info::DeriveMethod> = match self.derives.get(&derive_name) {
1239 Some(info) => info.methods.clone(),
1240 None => return Ok(()),
1241 };
1242 let host_type = Type::new_struct(host_id);
1243
1244 // ADR-0079: linear types must not pick up a Clone impl. The
1245 // prelude `derive Clone` synthesizes a fresh `Self` field-by-
1246 // field, which would silently duplicate a linear value. Catch
1247 // this at splice time so the diagnostic points at the
1248 // `@derive(Clone)` directive rather than at the synthesized
1249 // body's first @field_set.
1250 let derive_iface = self.interfaces.get(&derive_name).copied();
1251 if derive_iface.is_some()
1252 && derive_iface == self.lang_items.clone()
1253 && self.type_pool.struct_def(host_id).posture == Posture::Linear
1254 {
1255 let host_str = self.type_pool.struct_def(host_id).name.clone();
1256 return Err(CompileError::new(
1257 ErrorKind::LinearStructClone(host_str),
1258 directive_span,
1259 ));
1260 }
1261
1262 // ADR-0076: bind `Self` to the host struct while resolving derived
1263 // method signatures.
1264 let saved_self = self.current_self.replace(host_type);
1265
1266 let host_file_id = self.type_pool.struct_def(host_id).file_id;
1267 for dm in methods {
1268 let m = self.rir.get(dm.method_ref);
1269 let InstData::FnDecl {
1270 name: method_name,
1271 is_pub: method_is_pub,
1272 is_unchecked,
1273 params_start,
1274 params_len,
1275 return_type,
1276 body,
1277 has_self,
1278 receiver_mode,
1279 ..
1280 } = m.data
1281 else {
1282 continue;
1283 };
1284 let receiver = decode_receiver_mode(receiver_mode);
1285 let key = (host_id, method_name);
1286 if self.methods.contains_key(&key) {
1287 let derive_str = self.interner.resolve(&derive_name).to_string();
1288 let method_str = self.interner.resolve(&method_name).to_string();
1289 let host_str = self.type_pool.struct_def(host_id).name.clone();
1290 let prior_span = self.methods.get(&key).map(|info| info.span);
1291 let mut err = CompileError::new(
1292 ErrorKind::DuplicateMethod {
1293 type_name: format!(
1294 "type `{}` (attached by `@derive({})`)",
1295 host_str, derive_str
1296 ),
1297 method_name: method_str.clone(),
1298 },
1299 directive_span,
1300 )
1301 .with_label(
1302 format!("`{}` declared inside `derive {}`", method_str, derive_str),
1303 m.span,
1304 );
1305 if let Some(s) = prior_span {
1306 err = err.with_label(format!("`{}` already attached here", method_str), s);
1307 }
1308 return Err(err);
1309 }
1310
1311 let params = self.rir.get_params(params_start, params_len);
1312 let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
1313 let param_comptime: Vec<bool> = params.iter().map(|p| p.is_comptime).collect();
1314 let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
1315 let mut param_modes: Vec<gruel_rir::RirParamMode> = Vec::with_capacity(params.len());
1316 for p in ¶ms {
1317 let (ty, mode) = self.resolve_param_type(p.ty, p.mode, m.span)?;
1318 param_types.push(ty);
1319 param_modes.push(mode);
1320 }
1321 let ret_type = self.resolve_type(return_type, m.span)?;
1322 let param_range =
1323 self.param_arena
1324 .alloc(param_names, param_types, param_modes, param_comptime);
1325
1326 // ADR-0073: derived methods are scoped to the host's module —
1327 // the host's file_id is the relevant target_file_id for any
1328 // visibility check at a call site.
1329 self.methods.insert(
1330 key,
1331 MethodInfo {
1332 struct_type: host_type,
1333 has_self,
1334 receiver,
1335 params: param_range,
1336 return_type: ret_type,
1337 body,
1338 span: m.span,
1339 is_unchecked,
1340 is_generic: false,
1341 return_type_sym: return_type,
1342 is_pub: method_is_pub,
1343 file_id: host_file_id,
1344 },
1345 );
1346 }
1347 self.current_self = saved_self;
1348 Ok(())
1349 }
1350
1351 /// Splice every method of `derive_name` into the enum identified by
1352 /// `host_id`. `Self` is bound to the host enum's `Type`. Mirrors the
1353 /// struct case; structs and enums share the splicing logic (only the
1354 /// destination map differs).
1355 pub(crate) fn splice_derive_methods_into_enum(
1356 &mut self,
1357 derive_name: Spur,
1358 host_id: EnumId,
1359 directive_span: Span,
1360 ) -> CompileResult<()> {
1361 let methods: Vec<crate::sema::info::DeriveMethod> = match self.derives.get(&derive_name) {
1362 Some(info) => info.methods.clone(),
1363 None => return Ok(()),
1364 };
1365 let host_type = Type::new_enum(host_id);
1366 // ADR-0076: bind `Self` to the host enum while resolving derived
1367 // method signatures.
1368 let saved_self = self.current_self.replace(host_type);
1369 let host_file_id = self.type_pool.enum_def(host_id).file_id;
1370
1371 for dm in methods {
1372 let m = self.rir.get(dm.method_ref);
1373 let InstData::FnDecl {
1374 name: method_name,
1375 is_pub: method_is_pub,
1376 is_unchecked,
1377 params_start,
1378 params_len,
1379 return_type,
1380 body,
1381 has_self,
1382 receiver_mode,
1383 ..
1384 } = m.data
1385 else {
1386 continue;
1387 };
1388 let receiver = decode_receiver_mode(receiver_mode);
1389 let key = (host_id, method_name);
1390 if self.enum_methods.contains_key(&key) {
1391 let derive_str = self.interner.resolve(&derive_name).to_string();
1392 let method_str = self.interner.resolve(&method_name).to_string();
1393 let prior_span = self.enum_methods.get(&key).map(|info| info.span);
1394 let mut err = CompileError::new(
1395 ErrorKind::DuplicateMethod {
1396 type_name: format!("enum (attached by `@derive({})`)", derive_str),
1397 method_name: method_str.clone(),
1398 },
1399 directive_span,
1400 )
1401 .with_label(
1402 format!("`{}` declared inside `derive {}`", method_str, derive_str),
1403 m.span,
1404 );
1405 if let Some(s) = prior_span {
1406 err = err.with_label(format!("`{}` already attached here", method_str), s);
1407 }
1408 return Err(err);
1409 }
1410
1411 let params = self.rir.get_params(params_start, params_len);
1412 let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
1413 let param_comptime: Vec<bool> = params.iter().map(|p| p.is_comptime).collect();
1414 let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
1415 let mut param_modes: Vec<gruel_rir::RirParamMode> = Vec::with_capacity(params.len());
1416 for p in ¶ms {
1417 let (ty, mode) = self.resolve_param_type(p.ty, p.mode, m.span)?;
1418 param_types.push(ty);
1419 param_modes.push(mode);
1420 }
1421 let ret_type = self.resolve_type(return_type, m.span)?;
1422 let param_range =
1423 self.param_arena
1424 .alloc(param_names, param_types, param_modes, param_comptime);
1425
1426 self.enum_methods.insert(
1427 key,
1428 MethodInfo {
1429 struct_type: host_type,
1430 has_self,
1431 receiver,
1432 params: param_range,
1433 return_type: ret_type,
1434 body,
1435 span: m.span,
1436 is_unchecked,
1437 is_generic: false,
1438 return_type_sym: return_type,
1439 is_pub: method_is_pub,
1440 file_id: host_file_id,
1441 },
1442 );
1443 }
1444 self.current_self = saved_self;
1445 Ok(())
1446 }
1447
1448 /// Walk every named struct/enum declaration; for each `@derive(D)`
1449 /// directive, resolve `D` against `Sema::derives` and record a
1450 /// `DeriveBinding` for Phase 4. Errors if `D` doesn't name a `derive`
1451 /// item.
1452 pub(crate) fn resolve_derive_directives(&mut self) -> CompileResult<()> {
1453 use super::DeriveBinding;
1454
1455 // Snapshot of struct/enum declarations carrying `@derive(...)`.
1456 struct RawAttach {
1457 host_name: Spur,
1458 host_is_enum: bool,
1459 host_span: Span,
1460 derive_names: Vec<(Spur, Span)>,
1461 }
1462 let derive_dir_sym = self.interner.get_or_intern("derive");
1463 let mut raw: Vec<RawAttach> = Vec::new();
1464
1465 for (_, inst) in self.rir.iter() {
1466 // ADR-0079 Phase 1 routed directives through both
1467 // `StructDecl` and `EnumDecl`; collect from both so
1468 // `@derive(...)` works on enums too.
1469 let (directives_start, directives_len, name, host_is_enum) = match &inst.data {
1470 InstData::StructDecl {
1471 directives_start,
1472 directives_len,
1473 name,
1474 ..
1475 } => (*directives_start, *directives_len, *name, false),
1476 InstData::EnumDecl {
1477 directives_start,
1478 directives_len,
1479 name,
1480 ..
1481 } => (*directives_start, *directives_len, *name, true),
1482 _ => continue,
1483 };
1484 if directives_len == 0 {
1485 continue;
1486 }
1487 let directives = self.rir.get_directives(directives_start, directives_len);
1488 let mut attached = Vec::new();
1489 for d in &directives {
1490 if d.name == derive_dir_sym {
1491 if d.args.len() != 1 {
1492 return Err(CompileError::new(
1493 ErrorKind::DeriveNotADerive {
1494 name: "<wrong arg count>".to_string(),
1495 found: format!("{} arguments", d.args.len()),
1496 },
1497 d.span,
1498 ));
1499 }
1500 attached.push((d.args[0], d.span));
1501 }
1502 }
1503 if !attached.is_empty() {
1504 raw.push(RawAttach {
1505 host_name: name,
1506 host_is_enum,
1507 host_span: inst.span,
1508 derive_names: attached,
1509 });
1510 }
1511 }
1512
1513 // Resolve each binding.
1514 for r in raw {
1515 for (derive_name, dir_span) in r.derive_names {
1516 // `@derive(Copy)` is compiler-recognized (ADR-0059): the
1517 // field-Copy validation already runs via the posture
1518 // bookkeeping, and there is no derive item to look up. Skip
1519 // the regular resolution path.
1520 if self.is_compiler_derive(derive_name) {
1521 continue;
1522 }
1523
1524 let name_str = self.interner.resolve(&derive_name).to_string();
1525
1526 // Resolution order: a `derive` item, else categorize what
1527 // we actually found for a clearer diagnostic.
1528 if !self.derives.contains_key(&derive_name) {
1529 let found = if self.structs.contains_key(&derive_name) {
1530 "struct"
1531 } else if self.enums.contains_key(&derive_name) {
1532 "enum"
1533 } else if self.interfaces.contains_key(&derive_name) {
1534 "interface"
1535 } else if self.functions.contains_key(&derive_name) {
1536 "function"
1537 } else {
1538 "unknown name"
1539 };
1540 return Err(CompileError::new(
1541 ErrorKind::DeriveNotADerive {
1542 name: name_str,
1543 found: found.to_string(),
1544 },
1545 dir_span,
1546 ));
1547 }
1548
1549 self.derive_bindings.push(DeriveBinding {
1550 host_name: r.host_name,
1551 host_is_enum: r.host_is_enum,
1552 derive_name,
1553 host_span: r.host_span,
1554 directive_span: dir_span,
1555 });
1556 }
1557 }
1558
1559 Ok(())
1560 }
1561
1562 /// ADR-0059 / ADR-0079: a name was previously recognized as a
1563 /// compiler-handled derive (`Copy` was the only such name).
1564 /// ADR-0080 retired `Copy` from the interface registry, so this
1565 /// hook always returns `false` and `@derive(...)` resolution
1566 /// flows entirely through the standard derive-resolution path —
1567 /// `@derive(Copy)` falls through the existing "unknown interface"
1568 /// diagnostic, exactly as the ADR specified.
1569 fn is_compiler_derive(&self, _name: Spur) -> bool {
1570 false
1571 }
1572
1573 /// ADR-0058 phases 1 + 2: gate `derive` items behind the
1574 /// `comptime_derives` preview feature, register each into
1575 /// `Sema::derives` for later expansion, and reject ill-formed bodies
1576 /// (duplicate names, name collisions with structs/enums/interfaces,
1577 /// duplicate methods within a single derive, and direct `self.field`
1578 /// projection — the host type's structure isn't known at
1579 /// derive-definition time).
1580 pub(crate) fn validate_derive_decls(&mut self) -> CompileResult<()> {
1581 use super::DeriveInfo;
1582 use crate::sema::info::DeriveMethod;
1583
1584 // Snapshot: copy out enough RIR data so we can mutate `self.derives`
1585 // and emit diagnostics without holding shared borrows on `self.rir`.
1586 struct RawDerive {
1587 name: Spur,
1588 decl_ref: gruel_rir::InstRef,
1589 span: Span,
1590 methods_start: u32,
1591 methods_len: u32,
1592 }
1593 let mut raw: Vec<RawDerive> = Vec::new();
1594 for (inst_ref, inst) in self.rir.iter() {
1595 if let InstData::DeriveDecl {
1596 name,
1597 methods_start,
1598 methods_len,
1599 } = &inst.data
1600 {
1601 raw.push(RawDerive {
1602 name: *name,
1603 decl_ref: inst_ref,
1604 span: inst.span,
1605 methods_start: *methods_start,
1606 methods_len: *methods_len,
1607 });
1608 }
1609 }
1610
1611 for d in raw {
1612 let name_str = self.interner.resolve(&d.name).to_string();
1613
1614 // ADR-0079: a derive may share its name with an interface so the
1615 // prelude can ship `derive Clone {...}` alongside
1616 // `interface Clone {...}` — the derive provides an implementation
1617 // for that interface. Other collisions (struct, enum, another
1618 // derive) remain rejected.
1619 if self.derives.contains_key(&d.name)
1620 || self.structs.contains_key(&d.name)
1621 || self.enums.contains_key(&d.name)
1622 {
1623 return Err(CompileError::new(
1624 ErrorKind::DuplicateTypeDefinition {
1625 type_name: format!("derive `{}`", name_str),
1626 },
1627 d.span,
1628 ));
1629 }
1630
1631 let method_refs = self.rir.get_inst_refs(d.methods_start, d.methods_len);
1632 let mut seen: HashSet<Spur> = HashSet::default();
1633 let mut methods: Vec<DeriveMethod> = Vec::with_capacity(method_refs.len());
1634 for method_ref in method_refs {
1635 let m = self.rir.get(method_ref);
1636 let InstData::FnDecl {
1637 name: method_name,
1638 has_self,
1639 ..
1640 } = &m.data
1641 else {
1642 // The grammar only admits method declarations inside a
1643 // derive body, but if RIR ever produces something else
1644 // we surface a clear diagnostic instead of panicking.
1645 return Err(CompileError::new(
1646 ErrorKind::InternalError(format!(
1647 "non-method instruction inside `derive {}` body",
1648 name_str
1649 )),
1650 m.span,
1651 ));
1652 };
1653
1654 if !seen.insert(*method_name) {
1655 return Err(CompileError::new(
1656 ErrorKind::DuplicateMethod {
1657 type_name: format!("derive `{}`", name_str),
1658 method_name: self.interner.resolve(method_name).to_string(),
1659 },
1660 m.span,
1661 ));
1662 }
1663
1664 self.reject_direct_self_projection(*method_name, &name_str, method_ref)?;
1665
1666 methods.push(DeriveMethod {
1667 name: *method_name,
1668 has_self: *has_self,
1669 method_ref,
1670 span: m.span,
1671 });
1672 }
1673
1674 self.derives.insert(
1675 d.name,
1676 DeriveInfo {
1677 name: d.name,
1678 decl_ref: d.decl_ref,
1679 span: d.span,
1680 methods,
1681 },
1682 );
1683 }
1684
1685 Ok(())
1686 }
1687
1688 /// Walk the RIR range `[body_start, decl)` for the method whose `FnDecl`
1689 /// is at `method_ref`, and reject any `FieldGet { base: VarRef "self" }`.
1690 /// The host's structure isn't known at derive-definition time, so direct
1691 /// projection (`self.x`) is illegal — users must go through
1692 /// `@field(self, "x")` (ADR-0058).
1693 fn reject_direct_self_projection(
1694 &self,
1695 method_name: Spur,
1696 derive_name: &str,
1697 method_ref: gruel_rir::InstRef,
1698 ) -> CompileResult<()> {
1699 let self_sym = match self.interner.get("self") {
1700 Some(s) => s,
1701 // No `self` symbol exists yet, so no method can reference it.
1702 None => return Ok(()),
1703 };
1704
1705 // Find this method's function span so we can iterate just its body.
1706 let span = match self
1707 .rir
1708 .function_spans()
1709 .iter()
1710 .find(|s| s.decl == method_ref)
1711 {
1712 Some(s) => s,
1713 None => return Ok(()),
1714 };
1715
1716 let view = self.rir.function_view(span);
1717 for (_, inst) in view.iter() {
1718 if let InstData::FieldGet { base, .. } = &inst.data {
1719 let base_inst = self.rir.get(*base);
1720 if let InstData::VarRef { name } = &base_inst.data
1721 && *name == self_sym
1722 {
1723 return Err(CompileError::new(
1724 ErrorKind::DeriveDirectFieldAccess {
1725 derive_name: derive_name.to_string(),
1726 method_name: self.interner.resolve(&method_name).to_string(),
1727 },
1728 inst.span,
1729 ));
1730 }
1731 }
1732 }
1733 Ok(())
1734 }
1735
1736 /// Resolve enum variant field types. Must run after all type names are registered
1737 /// so that field types can reference other enums/structs.
1738 pub(crate) fn resolve_enum_variant_fields(&mut self) -> CompileResult<()> {
1739 for (_, inst) in self.rir.iter() {
1740 if let InstData::EnumDecl {
1741 name,
1742 variants_start,
1743 variants_len,
1744 ..
1745 } = &inst.data
1746 {
1747 let enum_id = match self.enums.get(name) {
1748 Some(id) => *id,
1749 None => continue, // not registered (shouldn't happen)
1750 };
1751
1752 let raw_variants = self
1753 .rir
1754 .get_enum_variant_decls(*variants_start, *variants_len);
1755 let has_data = raw_variants.iter().any(|(_, fields, _)| !fields.is_empty());
1756 if !has_data {
1757 continue; // unit-only enum, no field types to resolve
1758 }
1759
1760 let mut resolved_variants = Vec::with_capacity(raw_variants.len());
1761 for (vname, field_type_spurs, field_name_spurs) in &raw_variants {
1762 let mut resolved_fields = Vec::with_capacity(field_type_spurs.len());
1763 for field_ty_spur in field_type_spurs {
1764 let field_ty = self.resolve_type(*field_ty_spur, inst.span)?;
1765 resolved_fields.push(field_ty);
1766 }
1767 let field_names: Vec<String> = field_name_spurs
1768 .iter()
1769 .map(|n| self.interner.resolve(n).to_string())
1770 .collect();
1771 resolved_variants.push(EnumVariantDef {
1772 name: self.interner.resolve(vname).to_string(),
1773 fields: resolved_fields,
1774 field_names,
1775 });
1776 }
1777
1778 let mut enum_def = self.type_pool.enum_def(enum_id);
1779 enum_def.variants = resolved_variants;
1780 self.type_pool.update_enum_def(enum_id, enum_def);
1781 }
1782 }
1783 Ok(())
1784 }
1785
1786 /// ADR-0080 Phase 3: posture-consistency walker.
1787 ///
1788 /// Runs after `resolve_struct_fields` and `resolve_enum_variant_fields`
1789 /// have populated every field/variant `Type`, but before the rest of
1790 /// `resolve_remaining_declarations`. For every named struct or enum
1791 /// declaration, it classifies each member's posture (Copy / Affine /
1792 /// Linear) and folds the result into the propagated posture, then
1793 /// compares against the declared posture from the keyword:
1794 ///
1795 /// | Declared | Rule |
1796 /// |-------------|----------------------------------------|
1797 /// | `copy` | every member must be Copy |
1798 /// | (unmarked) | no member may be Linear |
1799 /// | `linear` | (no constraint — linear holds anything)|
1800 ///
1801 /// On mismatch the error span is the host declaration's span and the
1802 /// message names the offending member's type and posture (richer
1803 /// per-field spans land when struct/enum field positions become
1804 /// addressable from `StructDef` / `EnumDef`).
1805 ///
1806 /// The walker is one function rather than a struct-validator + enum-
1807 /// validator pair because the only difference between the two cases is
1808 /// how members are enumerated; the posture-folding logic is identical.
1809 ///
1810 /// ADR-0084: in addition to computing posture, this pass also computes
1811 /// the structural minimum thread-safety classification over members
1812 /// and applies any `@mark(unsend)` / `@mark(checked_send)` /
1813 /// `@mark(checked_sync)` override recorded in
1814 /// `mark_thread_safety_decls`. The function was renamed from
1815 /// `validate_posture_consistency` when the thread-safety axis was
1816 /// added.
1817 pub(crate) fn validate_consistency(&mut self) -> CompileResult<()> {
1818 // ADR-0083 Phase 1: instead of just validating declared posture
1819 // against members, we run *uniform structural inference* on every
1820 // named struct/enum. The result reconciles with whatever the user
1821 // declared via the `copy`/`linear` keyword (ADR-0080) or
1822 // `@mark(...)` directive (ADR-0083):
1823 //
1824 // - declared Linear (or `@mark(linear)`) → final is Linear (no
1825 // member constraint).
1826 // - declared Copy (or `@mark(copy)`) → every member must be Copy;
1827 // final is Copy.
1828 // - declared Affine via `@mark(affine)` → no member may be
1829 // Linear; final is Affine (suppresses Copy inference).
1830 // - no declaration → final posture is the inferred posture
1831 // (NEW: previously named types were Affine by default; now
1832 // structs/enums of all-Copy members infer Copy).
1833 //
1834 // ADR-0080 carve-out: types with `fn __drop(self)` are never Copy
1835 // (Copy ⊥ Drop). We pre-scan RIR for both inline `fn __drop` methods
1836 // and top-level `drop fn TypeName(self)` declarations and downgrade
1837 // Copy → Affine when a destructor is present.
1838 //
1839 // Member-postures are classified via `classify_posture`, which
1840 // already handles arrays/tuples/Vec/Option/Result transparently.
1841 let types_with_drop = self.collect_types_with_drop();
1842 for (_, inst) in self.rir.iter() {
1843 match &inst.data {
1844 InstData::StructDecl {
1845 name,
1846 methods_start,
1847 methods_len,
1848 ..
1849 } => {
1850 let Some(&struct_id) = self.structs.get(name) else {
1851 continue;
1852 };
1853 let def = self.type_pool.struct_def(struct_id);
1854 let declared_affine = self.mark_affine_decls.contains(name);
1855 let declared = match def.posture {
1856 Posture::Linear => DeclaredPosture::Linear,
1857 Posture::Copy => DeclaredPosture::Copy,
1858 Posture::Affine if declared_affine => DeclaredPosture::Affine,
1859 Posture::Affine => DeclaredPosture::Unmarked,
1860 };
1861 let host_name = def.name.clone();
1862 let mut any_linear = false;
1863 let mut all_copy = true;
1864 for field in &def.fields {
1865 let posture = self.classify_posture(field.ty);
1866 match posture {
1867 MemberPosture::Linear => any_linear = true,
1868 MemberPosture::Affine => all_copy = false,
1869 MemberPosture::Copy => {}
1870 }
1871 if let Some(err) = check_posture_against_declared(
1872 "struct",
1873 host_name.as_str(),
1874 declared,
1875 "field",
1876 &field.name,
1877 self.format_type_name(field.ty).as_str(),
1878 posture,
1879 inst.span,
1880 ) {
1881 return Err(err);
1882 }
1883 }
1884 let has_drop = self.has_inline_drop_method(*methods_start, *methods_len)
1885 || types_with_drop.contains(name);
1886 let inferred = if any_linear {
1887 MemberPosture::Linear
1888 } else if all_copy && !has_drop {
1889 MemberPosture::Copy
1890 } else {
1891 MemberPosture::Affine
1892 };
1893 // ADR-0084: structural minimum over fields. Empty
1894 // composites fold to `Sync`, the identity element.
1895 let inferred_thread_safety = self
1896 .type_pool
1897 .struct_def(struct_id)
1898 .fields
1899 .iter()
1900 .map(|f| self.type_pool.is_thread_safety_type(f.ty))
1901 .min()
1902 .unwrap_or(ThreadSafety::Sync);
1903 let thread_safety_override = self.mark_thread_safety_decls.get(name).copied();
1904 self.apply_inferred_struct_posture(struct_id, declared, inferred);
1905 self.apply_thread_safety_struct(
1906 struct_id,
1907 inferred_thread_safety,
1908 thread_safety_override,
1909 );
1910 }
1911 InstData::EnumDecl {
1912 name,
1913 methods_start,
1914 methods_len,
1915 ..
1916 } => {
1917 let Some(&enum_id) = self.enums.get(name) else {
1918 continue;
1919 };
1920 let def = self.type_pool.enum_def(enum_id);
1921 let declared_affine = self.mark_affine_decls.contains(name);
1922 let declared = match def.posture {
1923 Posture::Linear => DeclaredPosture::Linear,
1924 Posture::Copy => DeclaredPosture::Copy,
1925 Posture::Affine if declared_affine => DeclaredPosture::Affine,
1926 Posture::Affine => DeclaredPosture::Unmarked,
1927 };
1928 let host_name = def.name.clone();
1929 let mut any_linear = false;
1930 let mut all_copy = true;
1931 for variant in &def.variants {
1932 for (i, field_ty) in variant.fields.iter().enumerate() {
1933 let member_name = if let Some(name) = variant.field_names.get(i) {
1934 format!("{}::{}.{}", host_name, variant.name, name)
1935 } else {
1936 format!("{}::{}.{}", host_name, variant.name, i)
1937 };
1938 let posture = self.classify_posture(*field_ty);
1939 match posture {
1940 MemberPosture::Linear => any_linear = true,
1941 MemberPosture::Affine => all_copy = false,
1942 MemberPosture::Copy => {}
1943 }
1944 if let Some(err) = check_posture_against_declared(
1945 "enum",
1946 host_name.as_str(),
1947 declared,
1948 "variant field",
1949 &member_name,
1950 self.format_type_name(*field_ty).as_str(),
1951 posture,
1952 inst.span,
1953 ) {
1954 return Err(err);
1955 }
1956 }
1957 }
1958 let has_drop = self.has_inline_drop_method(*methods_start, *methods_len)
1959 || types_with_drop.contains(name);
1960 let inferred = if any_linear {
1961 MemberPosture::Linear
1962 } else if all_copy && !has_drop {
1963 MemberPosture::Copy
1964 } else {
1965 MemberPosture::Affine
1966 };
1967 // ADR-0084: structural minimum across every variant
1968 // payload field. Mirrors the struct branch above.
1969 let inferred_thread_safety = self
1970 .type_pool
1971 .enum_def(enum_id)
1972 .variants
1973 .iter()
1974 .flat_map(|v| v.fields.iter().copied())
1975 .map(|f| self.type_pool.is_thread_safety_type(f))
1976 .min()
1977 .unwrap_or(ThreadSafety::Sync);
1978 let thread_safety_override = self.mark_thread_safety_decls.get(name).copied();
1979 self.apply_inferred_enum_posture(enum_id, declared, inferred);
1980 self.apply_thread_safety_enum(
1981 enum_id,
1982 inferred_thread_safety,
1983 thread_safety_override,
1984 );
1985 }
1986 _ => {}
1987 }
1988 }
1989 Ok(())
1990 }
1991
1992 /// ADR-0083 (revised): destructors are `fn __drop(self)` methods
1993 /// declared inside the struct body. The retired top-level
1994 /// `drop fn TypeName(self)` form had its own collection pass; with
1995 /// it gone, only `has_inline_drop_method` remains.
1996 fn collect_types_with_drop(&self) -> HashSet<Spur> {
1997 // No top-level drop fns; inline `fn __drop(self)` is detected via
1998 // `has_inline_drop_method` at the per-decl site.
1999 HashSet::default()
2000 }
2001
2002 /// ADR-0080 / ADR-0083: detect whether a struct/enum's inline method
2003 /// list contains a `fn __drop(self)` declaration.
2004 fn has_inline_drop_method(&self, methods_start: u32, methods_len: u32) -> bool {
2005 if methods_len == 0 {
2006 return false;
2007 }
2008 let drop_name = self.interner.get("__drop");
2009 let Some(drop_name) = drop_name else {
2010 return false;
2011 };
2012 let method_refs = self.rir.get_inst_refs(methods_start, methods_len);
2013 for method_ref in method_refs {
2014 let method_inst = self.rir.get(method_ref);
2015 if let InstData::FnDecl { name, has_self, .. } = &method_inst.data
2016 && *name == drop_name
2017 && *has_self
2018 {
2019 return true;
2020 }
2021 }
2022 false
2023 }
2024
2025 /// ADR-0083 Phase 1: write the final posture into the struct's
2026 /// `posture` field. If the user declared the posture (`copy struct`
2027 /// / `linear struct` / `@mark(...)`), the field is already set in
2028 /// `register_type_names`; we only need to fill it in for unmarked
2029 /// types from the inferred posture.
2030 fn apply_inferred_struct_posture(
2031 &mut self,
2032 struct_id: StructId,
2033 declared: DeclaredPosture,
2034 inferred: MemberPosture,
2035 ) {
2036 if declared != DeclaredPosture::Unmarked {
2037 return;
2038 }
2039 let mut def = self.type_pool.struct_def(struct_id);
2040 def.posture = match inferred {
2041 MemberPosture::Copy => Posture::Copy,
2042 MemberPosture::Affine => Posture::Affine,
2043 MemberPosture::Linear => Posture::Linear,
2044 };
2045 self.type_pool.update_struct_def(struct_id, def);
2046 }
2047
2048 fn apply_inferred_enum_posture(
2049 &mut self,
2050 enum_id: EnumId,
2051 declared: DeclaredPosture,
2052 inferred: MemberPosture,
2053 ) {
2054 if declared != DeclaredPosture::Unmarked {
2055 return;
2056 }
2057 let mut def = self.type_pool.enum_def(enum_id);
2058 def.posture = match inferred {
2059 MemberPosture::Copy => Posture::Copy,
2060 MemberPosture::Affine => Posture::Affine,
2061 MemberPosture::Linear => Posture::Linear,
2062 };
2063 self.type_pool.update_enum_def(enum_id, def);
2064 }
2065
2066 /// ADR-0084: write the final thread-safety classification on the
2067 /// struct's def. The user override (if any) wins, even when it
2068 /// contradicts the structurally-inferred value — that's the whole
2069 /// point of `@mark(checked_send)` / `@mark(checked_sync)`. The
2070 /// `unsend` marker is also accepted regardless of inference (always
2071 /// safe to restrict).
2072 fn apply_thread_safety_struct(
2073 &self,
2074 struct_id: StructId,
2075 inferred: ThreadSafety,
2076 override_: Option<ThreadSafety>,
2077 ) {
2078 let mut def = self.type_pool.struct_def(struct_id);
2079 def.thread_safety = override_.unwrap_or(inferred);
2080 self.type_pool.update_struct_def(struct_id, def);
2081 }
2082
2083 fn apply_thread_safety_enum(
2084 &self,
2085 enum_id: EnumId,
2086 inferred: ThreadSafety,
2087 override_: Option<ThreadSafety>,
2088 ) {
2089 let mut def = self.type_pool.enum_def(enum_id);
2090 def.thread_safety = override_.unwrap_or(inferred);
2091 self.type_pool.update_enum_def(enum_id, def);
2092 }
2093
2094 /// Classify a type's ownership posture (ADR-0080).
2095 fn classify_posture(&self, ty: crate::types::Type) -> MemberPosture {
2096 if self.is_type_linear(ty) {
2097 MemberPosture::Linear
2098 } else if self.is_type_copy(ty) {
2099 MemberPosture::Copy
2100 } else {
2101 MemberPosture::Affine
2102 }
2103 }
2104
2105 // ADR-0080 helper end.
2106
2107 /// Resolve struct field types. Must run before @derive(Copy) validation.
2108 pub(crate) fn resolve_struct_fields(&mut self) -> CompileResult<()> {
2109 for (_, inst) in self.rir.iter() {
2110 if let InstData::StructDecl {
2111 name,
2112 fields_start,
2113 fields_len,
2114 ..
2115 } = &inst.data
2116 {
2117 let name_str = self.interner.resolve(name).to_string();
2118 // Verify the struct exists in our lookup table
2119 if !self.structs.contains_key(name) {
2120 return Err(CompileError::new(
2121 ErrorKind::InternalError(
2122 ice!(
2123 "struct not found in struct map",
2124 phase: "sema/declarations",
2125 details: {
2126 "struct_name" => name_str.to_string()
2127 }
2128 )
2129 .to_string(),
2130 ),
2131 inst.span,
2132 ));
2133 }
2134
2135 // Get the struct ID from the lookup table
2136 let struct_id = *self.structs.get(name).ok_or_else(|| {
2137 CompileError::new(
2138 ErrorKind::InternalError(
2139 ice!(
2140 "struct not found in structs map",
2141 phase: "sema/declarations",
2142 details: {
2143 "struct_name" => name_str.to_string()
2144 }
2145 )
2146 .to_string(),
2147 ),
2148 inst.span,
2149 )
2150 })?;
2151
2152 let struct_name = name_str.clone();
2153 let fields = self
2154 .rir
2155 .get_field_decls_with_vis(*fields_start, *fields_len);
2156
2157 // Check for duplicate field names
2158 let mut seen_fields: HashSet<Spur> = HashSet::default();
2159 for (field_name, _, _) in &fields {
2160 if !seen_fields.insert(*field_name) {
2161 let field_name_str = self.interner.resolve(field_name).to_string();
2162 return Err(CompileError::new(
2163 ErrorKind::DuplicateField {
2164 struct_name,
2165 field_name: field_name_str,
2166 },
2167 inst.span,
2168 ));
2169 }
2170 }
2171
2172 // Resolve field types
2173 let mut resolved_fields = Vec::new();
2174 for (field_name, field_type, is_pub) in &fields {
2175 let field_ty = self.resolve_type(*field_type, inst.span)?;
2176 resolved_fields.push(StructField {
2177 name: self.interner.resolve(field_name).to_string(),
2178 ty: field_ty,
2179
2180 is_pub: *is_pub,
2181 });
2182 }
2183
2184 // Update the struct definition in the pool with resolved fields
2185 let mut struct_def = self.type_pool.struct_def(struct_id);
2186 // ADR-0085: validate `@mark(c)` struct fields recursively.
2187 // Any non-C field on a C-layout struct is rejected.
2188 if struct_def.is_c_layout {
2189 for f in &resolved_fields {
2190 self.validate_ffi_type(
2191 f.ty,
2192 &format!(
2193 "as the type of field `{}` on `@mark(c) struct {}`",
2194 f.name, struct_def.name
2195 ),
2196 inst.span,
2197 )?;
2198 }
2199 }
2200 struct_def.fields = resolved_fields;
2201 self.type_pool.update_struct_def(struct_id, struct_def);
2202 }
2203 }
2204 Ok(())
2205 }
2206
2207 /// Resolve @derive(Copy) validation, destructors, functions, and methods.
2208 pub(crate) fn resolve_remaining_declarations(&mut self) -> CompileResult<()> {
2209 // Collect all method InstRefs that should NOT be processed as
2210 // top-level functions:
2211 //
2212 // - Anonymous struct/enum methods are registered later during
2213 // comptime evaluation with proper Self resolution.
2214 // - `derive` body methods (ADR-0058) are spliced into a host type
2215 // at expansion time.
2216 // - Named struct/enum methods (including associated functions like
2217 // `fn new() -> Self`) are collected via `collect_struct_methods` /
2218 // `collect_enum_methods` with `current_self` set per ADR-0076.
2219 let mut anon_type_method_refs = rustc_hash::FxHashSet::default();
2220 for (_, inst) in self.rir.iter() {
2221 let (methods_start, methods_len) = match &inst.data {
2222 InstData::AnonStructType {
2223 methods_start,
2224 methods_len,
2225 ..
2226 } => (*methods_start, *methods_len),
2227 InstData::AnonEnumType {
2228 methods_start,
2229 methods_len,
2230 ..
2231 } => (*methods_start, *methods_len),
2232 InstData::DeriveDecl {
2233 methods_start,
2234 methods_len,
2235 ..
2236 } => (*methods_start, *methods_len),
2237 InstData::StructDecl {
2238 methods_start,
2239 methods_len,
2240 ..
2241 } => (*methods_start, *methods_len),
2242 InstData::EnumDecl {
2243 methods_start,
2244 methods_len,
2245 ..
2246 } => (*methods_start, *methods_len),
2247 _ => continue,
2248 };
2249 let method_refs = self.rir.get_inst_refs(methods_start, methods_len);
2250 for method_ref in method_refs {
2251 anon_type_method_refs.insert(method_ref);
2252 }
2253 }
2254
2255 // First pass: collect all declarations and validate @derive(Copy) structs
2256 for (inst_ref, inst) in self.rir.iter() {
2257 match &inst.data {
2258 InstData::StructDecl {
2259 directives_start,
2260 directives_len,
2261 name,
2262 methods_start,
2263 methods_len,
2264 ..
2265 } => {
2266 // ADR-0079: `validate_copy_struct` and
2267 // `validate_clone_struct` are both retired. The
2268 // prelude `derive Copy` / `derive Clone` bodies
2269 // express the field-Copy / field-Clone
2270 // invariants in Gruel via
2271 // `comptime_unroll for f in @type_info(Self).fields`
2272 // + `comptime if (!@implements(f.field_type, …))`
2273 // + `@compile_error`. Linearity is enforced by
2274 // the structural Copy/Clone conformance checks.
2275 let _ = (*directives_start, *directives_len);
2276 // Collect methods defined inline in the struct
2277 self.collect_struct_methods(*name, *methods_start, *methods_len, inst.span)?;
2278 }
2279
2280 InstData::EnumDecl {
2281 name,
2282 methods_start,
2283 methods_len,
2284 ..
2285 } => {
2286 // Collect methods defined inline in the enum (ADR-0053).
2287 self.collect_enum_methods(*name, *methods_start, *methods_len, inst.span)?;
2288 }
2289
2290 InstData::FnDecl {
2291 directives_start,
2292 directives_len,
2293 is_pub,
2294 is_unchecked,
2295 name,
2296 params_start,
2297 params_len,
2298 return_type,
2299 body,
2300 has_self,
2301 ..
2302 } => {
2303 // Skip methods (has_self = true) - these are handled elsewhere:
2304 // - Named struct methods are collected via ImplDecl
2305 if *has_self {
2306 continue;
2307 }
2308
2309 // Skip ALL methods from anonymous types (structs and enums)
2310 // These are registered during comptime evaluation with proper Self type context
2311 if anon_type_method_refs.contains(&inst_ref) {
2312 continue;
2313 }
2314 self.collect_function_signature(
2315 *name,
2316 (*params_start, *params_len),
2317 *return_type,
2318 *body,
2319 inst.span,
2320 FnFlags {
2321 is_pub: *is_pub,
2322 is_unchecked: *is_unchecked,
2323 directives_start: *directives_start,
2324 directives_len: *directives_len,
2325 },
2326 )?;
2327 }
2328
2329 InstData::ConstDecl {
2330 is_pub, name, init, ..
2331 } => {
2332 self.collect_const_declaration(*name, *is_pub, *init, inst.span)?;
2333 }
2334
2335 _ => {}
2336 }
2337 }
2338
2339 // ADR-0085: register every fn declared inside a `link_extern("…") { … }`
2340 // block. Gated behind the `c_ffi` preview feature.
2341 self.collect_extern_fn_signatures()?;
2342
2343 Ok(())
2344 }
2345
2346 /// ADR-0085: validate that a `Type` is allowed at the FFI boundary.
2347 ///
2348 /// Allowed: numeric primitives, `bool`, `()`, `Ptr(T)`, `MutPtr(T)`,
2349 /// and `@mark(c) struct T`. Enums are rejected entirely in v1 — see
2350 /// the deferred enum-FFI follow-up. `&T` / `&mut T`, slices, owned
2351 /// containers, and non-C-layout aggregates are rejected here.
2352 fn validate_ffi_type(&self, ty: Type, position: &str, span: Span) -> CompileResult<()> {
2353 // Scalar primitives and unit pass.
2354 if matches!(
2355 ty,
2356 Type::I8
2357 | Type::I16
2358 | Type::I32
2359 | Type::I64
2360 | Type::ISIZE
2361 | Type::U8
2362 | Type::U16
2363 | Type::U32
2364 | Type::U64
2365 | Type::USIZE
2366 | Type::F32
2367 | Type::F64
2368 | Type::BOOL
2369 | Type::UNIT
2370 // ADR-0086 C named arithmetic primitive types.
2371 | Type::C_SCHAR
2372 | Type::C_SHORT
2373 | Type::C_INT
2374 | Type::C_LONG
2375 | Type::C_LONGLONG
2376 | Type::C_UCHAR
2377 | Type::C_USHORT
2378 | Type::C_UINT
2379 | Type::C_ULONG
2380 | Type::C_ULONGLONG
2381 | Type::C_FLOAT
2382 | Type::C_DOUBLE
2383 ) {
2384 return Ok(());
2385 }
2386
2387 // Raw pointers (ADR-0061) cross the boundary as `const T*`/`T*`.
2388 // ADR-0086: `Ptr(c_void)` / `MutPtr(c_void)` are also permitted —
2389 // c_void is allowed inside pointer types but rejected as a value.
2390 if ty.is_ptr_const() || ty.is_ptr_mut() {
2391 return Ok(());
2392 }
2393
2394 // `@mark(c) struct T` is allowed by value.
2395 if let Some(struct_id) = ty.as_struct() {
2396 let def = self.type_pool.struct_def(struct_id);
2397 if def.is_c_layout {
2398 return Ok(());
2399 }
2400 return Err(CompileError::new(
2401 ErrorKind::CFfi(format!(
2402 "type `{}` cannot cross the FFI boundary {}: \
2403 non-`@mark(c)` struct (or container) types are not \
2404 C-ABI compatible",
2405 def.name, position
2406 )),
2407 span,
2408 ));
2409 }
2410
2411 // ADR-0086: `@mark(c) enum E` is allowed by value. Field-less
2412 // enums lower to a bare `c_int`; data-carrying enums use the
2413 // C tagged-union layout `{ tag: c_int; payload: union<…> }`.
2414 // Variant payload fields must themselves be C-FFI types.
2415 // Non-`@mark(c)` enums stay rejected.
2416 if let Some(enum_id) = ty.as_enum() {
2417 let def = self.type_pool.enum_def(enum_id);
2418 if def.is_c_layout {
2419 let enum_name = def.name.clone();
2420 let variants = def.variants.clone();
2421 // Recursively validate each variant payload field — it
2422 // must itself be an FFI-compatible type. Mirrors the
2423 // recursion the @mark(c) struct path uses.
2424 for variant in &variants {
2425 for &field_ty in &variant.fields {
2426 if let Err(mut e) = self.validate_ffi_type(field_ty, position, span) {
2427 // Wrap the inner diagnostic so the user
2428 // sees which enum variant payload field
2429 // is at fault.
2430 if let ErrorKind::CFfi(inner) = &e.kind {
2431 e.kind = ErrorKind::CFfi(format!(
2432 "variant `{}` of `@mark(c) enum {}` payload field has \
2433 a non-FFI type: {}",
2434 variant.name, enum_name, inner
2435 ));
2436 }
2437 return Err(e);
2438 }
2439 }
2440 }
2441 return Ok(());
2442 }
2443 return Err(CompileError::new(
2444 ErrorKind::CFfi(format!(
2445 "type `{}` cannot cross the FFI boundary {}: \
2446 non-`@mark(c)` enum types are not C-ABI compatible",
2447 def.name, position
2448 )),
2449 span,
2450 ));
2451 }
2452
2453 // Everything else is rejected.
2454 Err(CompileError::new(
2455 ErrorKind::CFfi(format!(
2456 "this type cannot cross the FFI boundary {}: \
2457 only numeric primitives, `bool`, `()`, `Ptr(T)`, `MutPtr(T)`, \
2458 and `@mark(c) struct` types are permitted",
2459 position
2460 )),
2461 span,
2462 ))
2463 }
2464
2465 /// ADR-0085: register every C extern fn declared in `link_extern` blocks.
2466 ///
2467 /// Each entry on `Rir::extern_fns()` becomes a `FunctionInfo` with
2468 /// `is_extern = true`, `is_c_abi = true`, and `link_library = Some(...)`.
2469 /// The body slot is filled with a sentinel `InstRef`; codegen sees the
2470 /// `is_extern` flag and emits a declaration only.
2471 fn collect_extern_fn_signatures(&mut self) -> CompileResult<()> {
2472 let extern_fns: Vec<_> = self.rir.extern_fns().to_vec();
2473 let empty_blocks: Vec<_> = self.rir.empty_link_extern_blocks().to_vec();
2474 if extern_fns.is_empty() && empty_blocks.is_empty() {
2475 return Ok(());
2476 }
2477 // ADR-0085: any `link_extern` block fires the c_ffi preview gate.
2478 // Pick the first block-span we see for the diagnostic anchor.
2479 // ADR-0085: validate library names — non-empty per spec 10.2:5.
2480 // (C FFI is stable as of ADR-0085 Phase 5; no preview gate.)
2481 for ext in &extern_fns {
2482 if self.interner.resolve(&ext.library).is_empty() {
2483 return Err(CompileError::new(
2484 ErrorKind::CFfi(
2485 "`link_extern(\"…\")` library name must not be empty".to_string(),
2486 ),
2487 ext.block_span,
2488 ));
2489 }
2490 }
2491 for (lib, _link_mode, block_span) in &empty_blocks {
2492 if self.interner.resolve(lib).is_empty() {
2493 return Err(CompileError::new(
2494 ErrorKind::CFfi(
2495 "`link_extern(\"…\")` library name must not be empty".to_string(),
2496 ),
2497 *block_span,
2498 ));
2499 }
2500 }
2501
2502 // ADR-0086: a given library cannot be declared with both
2503 // dynamic (`link_extern`) and static (`static_link_extern`)
2504 // linkage across the same compilation unit. Fire the
2505 // c_ffi_extras preview gate on any `static_link_extern` block
2506 // (the dynamic path is stable from ADR-0085).
2507 use gruel_rir::inst::RirLinkMode;
2508 use rustc_hash::FxHashMap;
2509 let mut linkage_by_lib: FxHashMap<Spur, (RirLinkMode, Span)> = FxHashMap::default();
2510 for ext in &extern_fns {
2511 let entry = linkage_by_lib
2512 .entry(ext.library)
2513 .or_insert((ext.link_mode, ext.block_span));
2514 if entry.0 != ext.link_mode {
2515 let lib_name = self.interner.resolve(&ext.library).to_string();
2516 return Err(CompileError::new(
2517 ErrorKind::CFfi(format!(
2518 "library `{}` is declared with both `link_extern` (dynamic) and \
2519 `static_link_extern` (static) linkage; a library must use a \
2520 single linkage mode across the compilation unit",
2521 lib_name
2522 )),
2523 ext.block_span,
2524 ));
2525 }
2526 }
2527 for (lib, link_mode, block_span) in &empty_blocks {
2528 let entry = linkage_by_lib
2529 .entry(*lib)
2530 .or_insert((*link_mode, *block_span));
2531 if entry.0 != *link_mode {
2532 let lib_name = self.interner.resolve(lib).to_string();
2533 return Err(CompileError::new(
2534 ErrorKind::CFfi(format!(
2535 "library `{}` is declared with both `link_extern` (dynamic) and \
2536 `static_link_extern` (static) linkage; a library must use a \
2537 single linkage mode across the compilation unit",
2538 lib_name
2539 )),
2540 *block_span,
2541 ));
2542 }
2543 }
2544
2545 for ext in &extern_fns {
2546 let params = self.rir.get_params(ext.params_start, ext.params_len);
2547 let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
2548 let mut param_types = Vec::with_capacity(params.len());
2549 let mut param_modes = Vec::with_capacity(params.len());
2550 let mut param_comptime = Vec::with_capacity(params.len());
2551 for p in ¶ms {
2552 if p.is_comptime {
2553 return Err(CompileError::new(
2554 ErrorKind::CFfi(
2555 "extern fns inside `link_extern` must not declare \
2556 comptime parameters"
2557 .to_string(),
2558 ),
2559 ext.span,
2560 ));
2561 }
2562 let (ty, mode) = self.resolve_param_type(p.ty, p.mode, ext.span)?;
2563 self.validate_ffi_type(ty, "as a parameter type", ext.span)?;
2564 param_types.push(ty);
2565 param_modes.push(mode);
2566 param_comptime.push(false);
2567 }
2568 let ret_type = self.resolve_type(ext.return_type, ext.span)?;
2569 self.validate_ffi_type(ret_type, "as a return type", ext.span)?;
2570 let params_range =
2571 self.param_arena
2572 .alloc(param_names, param_types, param_modes, param_comptime);
2573
2574 // ADR-0085: process directives on the extern fn. Today the only
2575 // supported one is `@link_name("…")`.
2576 // ADR-0088: extern fns also accept `@mark(unchecked)` and,
2577 // under the `unchecked_fn_extensions` preview gate, are
2578 // required to carry it. The gate fires below after the
2579 // directive scan completes.
2580 let directives = self
2581 .rir
2582 .get_directives(ext.directives_start, ext.directives_len);
2583 let mut link_name_override: Option<Spur> = None;
2584 let mut has_unchecked_mark = false;
2585 for d in directives.iter() {
2586 let dir_name = self.interner.resolve(&d.name).to_string();
2587 match dir_name.as_str() {
2588 "link_name" => {
2589 if d.args.len() != 1 {
2590 return Err(CompileError::new(
2591 ErrorKind::CFfi(
2592 "`@link_name(\"…\")` takes exactly one string argument"
2593 .to_string(),
2594 ),
2595 d.span,
2596 ));
2597 }
2598 link_name_override = Some(d.args[0]);
2599 }
2600 "mark" => {
2601 // ADR-0085: redundant `@mark(c)` inside a
2602 // `link_extern` block is permitted. ADR-0088:
2603 // `@mark(unchecked)` is the required form for
2604 // every extern fn (gated by preview). Other
2605 // marker names are rejected.
2606 for arg in &d.args {
2607 let name = self.interner.resolve(arg);
2608 if name == "c" {
2609 // accepted, no-op
2610 } else if name == "unchecked" {
2611 has_unchecked_mark = true;
2612 } else {
2613 return Err(CompileError::new(
2614 ErrorKind::CFfi(format!(
2615 "marker `{}` is not valid on extern fns inside `link_extern`",
2616 name
2617 )),
2618 d.span,
2619 ));
2620 }
2621 }
2622 }
2623 other => {
2624 return Err(CompileError::new(
2625 ErrorKind::CFfi(format!(
2626 "directive `@{}` is not valid on extern fns inside `link_extern`",
2627 other
2628 )),
2629 d.span,
2630 ));
2631 }
2632 }
2633 }
2634
2635 // ADR-0088: every extern fn must carry `@mark(unchecked)`.
2636 // FFI imports are unverified from the Gruel side by
2637 // construction; the marker makes the call-site discipline
2638 // visible — every caller must wrap the call in a
2639 // `checked { }` block.
2640 if !has_unchecked_mark {
2641 let fn_name = self.interner.resolve(&ext.name).to_string();
2642 let library = self.interner.resolve(&ext.library).to_string();
2643 return Err(CompileError::new(
2644 ErrorKind::ExternFnMissingUnchecked { fn_name, library },
2645 ext.span,
2646 ));
2647 }
2648
2649 if self.functions.contains_key(&ext.name) {
2650 let name_str = self.interner.resolve(&ext.name).to_string();
2651 return Err(CompileError::new(
2652 ErrorKind::DuplicateTypeDefinition {
2653 type_name: format!("function `{}`", name_str),
2654 },
2655 ext.span,
2656 ));
2657 }
2658
2659 self.functions.insert(
2660 ext.name,
2661 FunctionInfo {
2662 params: params_range,
2663 return_type: ret_type,
2664 return_type_sym: ext.return_type,
2665 body: InstRef::from_raw(u32::MAX),
2666 span: ext.span,
2667 is_generic: false,
2668 is_pub: true,
2669 // ADR-0088: every extern fn that carries
2670 // `@mark(unchecked)` is treated as unchecked at
2671 // call sites — every call must sit inside a
2672 // `checked { }` block.
2673 is_unchecked: has_unchecked_mark,
2674 file_id: ext.span.file_id,
2675 canonical_name: None,
2676 link_library: Some(ext.library),
2677 link_name: link_name_override,
2678 is_extern: true,
2679 is_c_abi: true,
2680 },
2681 );
2682 }
2683 Ok(())
2684 }
2685
2686 /// Collect a function signature for forward reference.
2687 fn collect_function_signature(
2688 &mut self,
2689 name: Spur,
2690 (params_start, params_len): (u32, u32),
2691 return_type_sym: Spur,
2692 body: InstRef,
2693 span: Span,
2694 flags: FnFlags,
2695 ) -> CompileResult<()> {
2696 let FnFlags {
2697 is_pub,
2698 is_unchecked,
2699 directives_start,
2700 directives_len,
2701 } = flags;
2702
2703 // ADR-0085: scan directives for `@mark(c)` (C ABI export) and
2704 // `@link_name("…")` (symbol override). Other markers and
2705 // directives are handled by their own passes; this is just the
2706 // C-FFI subset.
2707 //
2708 // ADR-0088: `@mark(unchecked)` on a fn / method is gated by the
2709 // `UncheckedFnExtensions` preview feature; the gate fires
2710 // centrally in `validate_directives`. The legacy `unchecked`
2711 // keyword path is unaffected.
2712 let mut is_c_abi = false;
2713 let mut link_name_override: Option<Spur> = None;
2714 let directives = self.rir.get_directives(directives_start, directives_len);
2715 for d in directives.iter() {
2716 let dir_name = self.interner.resolve(&d.name).to_string();
2717 match dir_name.as_str() {
2718 "mark" => {
2719 for arg in &d.args {
2720 let marker_name = self.interner.resolve(arg);
2721 if marker_name == "c" {
2722 is_c_abi = true;
2723 }
2724 }
2725 }
2726 "link_name" => {
2727 if d.args.len() != 1 {
2728 return Err(CompileError::new(
2729 ErrorKind::CFfi(
2730 "`@link_name(\"…\")` takes exactly one string argument".to_string(),
2731 ),
2732 d.span,
2733 ));
2734 }
2735 link_name_override = Some(d.args[0]);
2736 }
2737 _ => {}
2738 }
2739 }
2740
2741 // ADR-0085: `@link_name` is only valid on C-ABI fns (extern
2742 // declarations or `@mark(c)` exports).
2743 if link_name_override.is_some() && !is_c_abi {
2744 return Err(CompileError::new(
2745 ErrorKind::CFfi(
2746 "`@link_name(\"…\")` is only valid on `@mark(c)` functions \
2747 or extern fns inside `link_extern`"
2748 .to_string(),
2749 ),
2750 span,
2751 ));
2752 }
2753 let params = self.rir.get_params(params_start, params_len);
2754
2755 let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
2756
2757 // Check if this function has any comptime TYPE parameters (not value parameters).
2758 // A type parameter is a comptime param whose declared type is either:
2759 // - `type` (unbounded): `comptime T: type`
2760 // - a registered interface name (ADR-0056): `comptime T: SomeInterface`
2761 //
2762 // Both cases erase the parameter at codegen and substitute the
2763 // concrete type at specialization. Interface-bounded parameters
2764 // additionally trigger a conformance check at the call site.
2765 let type_sym = self.interner.get_or_intern("type");
2766 // Pre-compute per-param flags so the closure doesn't need to borrow
2767 // `self` (which conflicts with the resolve_type call below).
2768 let is_type_param: Vec<bool> = params
2769 .iter()
2770 .map(|p| p.is_comptime && (p.ty == type_sym || self.interfaces.contains_key(&p.ty)))
2771 .collect();
2772 // Any comptime parameter — type or value — makes the function generic.
2773 // Comptime value parameters (e.g. `comptime n: i32`) need per-call
2774 // specialization too: the body may contain a `comptime if n < 0 {…}`
2775 // guard that can only be evaluated once `n` is bound to a concrete
2776 // value at the call site.
2777 let is_generic = params.iter().any(|p| p.is_comptime);
2778
2779 // Collect type parameter names.
2780 let type_param_names: Vec<Spur> = params
2781 .iter()
2782 .zip(is_type_param.iter())
2783 .filter_map(|(p, &b)| if b { Some(p.name) } else { None })
2784 .collect();
2785
2786 // Record any interface bounds for use at specialization time.
2787 for p in params.iter() {
2788 if p.is_comptime
2789 && let Some(iid) = self.interfaces.get(&p.ty).copied()
2790 {
2791 self.comptime_interface_bounds.insert((name, p.name), iid);
2792 }
2793 }
2794
2795 // For generic functions, we defer type resolution of type parameters until specialization.
2796 // We use Type::COMPTIME_TYPE as a placeholder for comptime type parameters.
2797 // ADR-0076 Phase 2: also collect the (possibly normalized) param mode
2798 // returned by `resolve_param_type` so `Ref(I)` / `MutRef(I)` lower to
2799 // the same `(Interface, Borrow|Inout)` shape as legacy `borrow t: I`
2800 // / `inout t: I`.
2801 //
2802 // A parameter type "references" a type parameter when bare equality
2803 // misses but a substituted resolution succeeds — e.g. `MutRef(Vec(T))`
2804 // mentions `T` indirectly. Treat those exactly like the bare-name
2805 // case: resolve at specialization, placeholder here. Mirrors the
2806 // `references_method_type_param` helper used for inline methods.
2807 let references_type_param = |ty_sym: Spur, sema: &mut Self| -> bool {
2808 if type_param_names.contains(&ty_sym) {
2809 return true;
2810 }
2811 if type_param_names.is_empty() {
2812 return false;
2813 }
2814 let subst: rustc_hash::FxHashMap<Spur, Type> =
2815 type_param_names.iter().map(|&n| (n, Type::I32)).collect();
2816 let with_subst = sema.resolve_type_for_comptime_with_subst(ty_sym, &subst);
2817 let without_subst =
2818 sema.resolve_type_for_comptime_with_subst(ty_sym, &HashMap::default());
2819 with_subst.is_some() && without_subst.is_none()
2820 };
2821
2822 let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
2823 let mut param_modes: Vec<RirParamMode> = Vec::with_capacity(params.len());
2824 for (p, &is_tp) in params.iter().zip(is_type_param.iter()) {
2825 let (ty, mode) = if is_tp || references_type_param(p.ty, self) {
2826 // Comptime type parameter, or a parameter whose declared type
2827 // is (or transitively contains) a type parameter — e.g.
2828 // `x: T`, `v: MutRef(Vec(T))`. The concrete type is resolved
2829 // at specialization; mode is taken verbatim from RIR.
2830 //
2831 // ADR-0076 normalization for the deferred case: when the
2832 // declared type is `Ref(...)` / `MutRef(...)` wrapping a
2833 // type parameter, surface that ref-shape on the param mode.
2834 // `resolve_param_type` does this for the eager path (and
2835 // for interface inner types); here we do the same shape
2836 // detection on the source-level symbol so the implicit-
2837 // forwarding logic in `analyze_call` (which can only see
2838 // the deferred `COMPTIME_TYPE` placeholder, not the
2839 // original `Ref(...)` shape) can still recognize the
2840 // reference parameter and promote the arg mode.
2841 let normalized_mode = {
2842 let type_name = self.interner.resolve(&p.ty);
2843 if let Some((callee, args)) = parse_type_call_syntax(type_name)
2844 && args.len() == 1
2845 && (callee == "Ref" || callee == "MutRef")
2846 && matches!(p.mode, RirParamMode::Normal)
2847 {
2848 if callee == "MutRef" {
2849 RirParamMode::MutRef
2850 } else {
2851 RirParamMode::Ref
2852 }
2853 } else {
2854 p.mode
2855 }
2856 };
2857 (Type::COMPTIME_TYPE, normalized_mode)
2858 } else {
2859 self.resolve_param_type(p.ty, p.mode, span)?
2860 };
2861 param_types.push(ty);
2862 param_modes.push(mode);
2863 }
2864 let param_comptime: Vec<bool> = params.iter().map(|p| p.is_comptime).collect();
2865
2866 // Return type follows the same rule.
2867 let ret_type = if references_type_param(return_type_sym, self) {
2868 Type::COMPTIME_TYPE
2869 } else {
2870 self.resolve_type(return_type_sym, span)?
2871 };
2872
2873 // Allocate parameter data in the arena
2874 let params_range =
2875 self.param_arena
2876 .alloc(param_names, param_types, param_modes, param_comptime);
2877
2878 // ADR-0085: validate FFI types for `@mark(c)` exports — params
2879 // and return type must all be C-ABI compatible. Skipped for
2880 // regular Gruel fns.
2881 if is_c_abi {
2882 for ty in self.param_arena.types(params_range) {
2883 self.validate_ffi_type(*ty, "as a parameter type", span)?;
2884 }
2885 self.validate_ffi_type(ret_type, "as a return type", span)?;
2886 }
2887
2888 self.functions.insert(
2889 name,
2890 FunctionInfo {
2891 params: params_range,
2892 return_type: ret_type,
2893 return_type_sym,
2894 body,
2895 span,
2896 is_generic,
2897 is_pub,
2898 is_unchecked,
2899 file_id: span.file_id,
2900 canonical_name: None,
2901 link_library: None,
2902 link_name: link_name_override,
2903 is_extern: false,
2904 is_c_abi,
2905 },
2906 );
2907 Ok(())
2908 }
2909
2910 /// Collect methods defined inline in a struct.
2911 fn collect_struct_methods(
2912 &mut self,
2913 type_name: Spur,
2914 methods_start: u32,
2915 methods_len: u32,
2916 span: Span,
2917 ) -> CompileResult<()> {
2918 let struct_id = match self.structs.get(&type_name) {
2919 Some(id) => *id,
2920 None => {
2921 let type_name_str = self.interner.resolve(&type_name).to_string();
2922 return Err(CompileError::new(
2923 ErrorKind::UnknownType(type_name_str),
2924 span,
2925 ));
2926 }
2927 };
2928 let struct_type = Type::new_struct(struct_id);
2929 let drop_name_sym = self.interner.get_or_intern("__drop");
2930
2931 // ADR-0076: bind `Self` to the host struct while resolving method
2932 // signatures. Errors abort the whole analysis pass so a leak is
2933 // harmless; on the success path we restore the previous binding.
2934 let saved_self = self.current_self.replace(struct_type);
2935
2936 let methods = self.rir.get_inst_refs(methods_start, methods_len);
2937 for method_ref in methods {
2938 let method_inst = self.rir.get(method_ref);
2939 if let InstData::FnDecl {
2940 name: method_name,
2941 is_pub: method_is_pub,
2942 is_unchecked,
2943 params_start,
2944 params_len,
2945 return_type,
2946 body,
2947 has_self,
2948 receiver_mode,
2949 directives_start,
2950 directives_len,
2951 ..
2952 } = &method_inst.data
2953 {
2954 let receiver = decode_receiver_mode(*receiver_mode);
2955
2956 // ADR-0088: reject `@mark(unchecked) fn __drop`. Drop
2957 // glue runs implicitly at scope exit; there is no
2958 // caller `checked { }` to gate it. The preview gate
2959 // itself fires centrally in `validate_directives`.
2960 if *method_name == drop_name_sym
2961 && self.directives_have_mark_unchecked(*directives_start, *directives_len)
2962 {
2963 return Err(CompileError::new(
2964 ErrorKind::UncheckedDestructor,
2965 method_inst.span,
2966 ));
2967 }
2968
2969 // ADR-0053: a method named `__drop` is the struct's destructor,
2970 // not a regular method. Route it through the destructor slot.
2971 if *method_name == drop_name_sym {
2972 self.register_inline_struct_drop(
2973 type_name,
2974 struct_id,
2975 DropSignature {
2976 has_self: *has_self,
2977 params_len: *params_len,
2978 return_type: *return_type,
2979 body: *body,
2980 span: method_inst.span,
2981 },
2982 )?;
2983 continue;
2984 }
2985
2986 // Use StructId in key to support anonymous struct methods
2987 let key = (struct_id, *method_name);
2988 if self.methods.contains_key(&key) {
2989 let type_name_str = self.interner.resolve(&type_name).to_string();
2990 let method_name_str = self.interner.resolve(method_name).to_string();
2991 return Err(CompileError::new(
2992 ErrorKind::DuplicateMethod {
2993 type_name: type_name_str,
2994 method_name: method_name_str,
2995 },
2996 method_inst.span,
2997 ));
2998 }
2999
3000 let params = self.rir.get_params(*params_start, *params_len);
3001 let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
3002 let param_comptime: Vec<bool> = params.iter().map(|p| p.is_comptime).collect();
3003
3004 // Detect method-level comptime type parameters (e.g.,
3005 // `fn apply(self, comptime F: type, f: F) -> T`). When
3006 // present, the method is generic at the method level — its
3007 // param types that reference those names (`f: F`) and its
3008 // return type cannot be fully resolved until the method is
3009 // specialized at a call site. Mirrors the top-level
3010 // generic-function treatment above.
3011 //
3012 // ADR-0056: an interface-bounded comptime parameter
3013 // (`comptime T: SomeInterface`) is also a type parameter; we
3014 // record the bound for the conformance check at the call
3015 // site. The method-key is encoded as "StructName.method" to
3016 // share the bound side-table with top-level functions.
3017 let type_sym = self.interner.get_or_intern("type");
3018 let method_type_param_names: Vec<Spur> = params
3019 .iter()
3020 .filter(|p| {
3021 p.is_comptime && (p.ty == type_sym || self.interfaces.contains_key(&p.ty))
3022 })
3023 .map(|p| p.name)
3024 .collect();
3025 let is_method_generic = !method_type_param_names.is_empty();
3026
3027 // Record interface bounds for this method.
3028 if !method_type_param_names.is_empty() {
3029 let owner_str = format!(
3030 "{}.{}",
3031 self.interner.resolve(&type_name),
3032 self.interner.resolve(method_name)
3033 );
3034 let owner = self.interner.get_or_intern(&owner_str);
3035 for p in params.iter() {
3036 if p.is_comptime
3037 && let Some(iid) = self.interfaces.get(&p.ty).copied()
3038 {
3039 self.comptime_interface_bounds.insert((owner, p.name), iid);
3040 }
3041 }
3042 }
3043
3044 // Helper: does a type symbol mention any of our method-level
3045 // type param names? Covers bare `T`, compound `[T; N]`,
3046 // `ptr const T`, etc. — check via the comptime-substitution
3047 // resolver using a sentinel substitution.
3048 let references_method_type_param = |ty_sym: Spur, sema: &mut Self| -> bool {
3049 if method_type_param_names.contains(&ty_sym) {
3050 return true;
3051 }
3052 let subst: rustc_hash::FxHashMap<Spur, Type> = method_type_param_names
3053 .iter()
3054 .map(|&n| (n, Type::I32))
3055 .collect();
3056 let with_subst = sema.resolve_type_for_comptime_with_subst(ty_sym, &subst);
3057 let without_subst =
3058 sema.resolve_type_for_comptime_with_subst(ty_sym, &HashMap::default());
3059 with_subst.is_some() && without_subst.is_none()
3060 };
3061
3062 // ADR-0076 Phase 2: collect normalized modes alongside types
3063 // so `Ref(I)` / `MutRef(I)` lower like the legacy keyword form.
3064 let mut param_types: Vec<Type> = Vec::with_capacity(params.len());
3065 let mut param_modes: Vec<RirParamMode> = Vec::with_capacity(params.len());
3066 for p in ¶ms {
3067 let (ty, mode) = if (p.is_comptime && p.ty == type_sym)
3068 || references_method_type_param(p.ty, self)
3069 {
3070 (Type::COMPTIME_TYPE, p.mode)
3071 } else {
3072 self.resolve_param_type(p.ty, p.mode, method_inst.span)?
3073 };
3074 param_types.push(ty);
3075 param_modes.push(mode);
3076 }
3077 let ret_type = if references_method_type_param(*return_type, self) {
3078 Type::COMPTIME_TYPE
3079 } else {
3080 self.resolve_type(*return_type, method_inst.span)?
3081 };
3082
3083 // Allocate method parameters in the arena, preserving mode
3084 // and comptime flags so specialization can pick them up.
3085 let param_range =
3086 self.param_arena
3087 .alloc(param_names, param_types, param_modes, param_comptime);
3088
3089 self.methods.insert(
3090 key,
3091 MethodInfo {
3092 struct_type,
3093 has_self: *has_self,
3094 receiver,
3095 params: param_range,
3096 return_type: ret_type,
3097 body: *body,
3098 span: method_inst.span,
3099 is_unchecked: *is_unchecked,
3100 is_generic: is_method_generic,
3101 return_type_sym: *return_type,
3102 is_pub: *method_is_pub,
3103 file_id: method_inst.span.file_id,
3104 },
3105 );
3106 }
3107 }
3108 self.current_self = saved_self;
3109 Ok(())
3110 }
3111
3112 /// Register an inline `fn __drop(self)` as a struct's destructor.
3113 ///
3114 /// Validates the signature (must be exactly `fn __drop(self)`, no extra
3115 /// params, returns unit) and the type's copy/linear status (a destructor
3116 /// is illegal on `@derive(Copy)` and `linear` types per ADR-0053).
3117 fn register_inline_struct_drop(
3118 &mut self,
3119 type_name: Spur,
3120 struct_id: StructId,
3121 sig: DropSignature,
3122 ) -> CompileResult<()> {
3123 let DropSignature {
3124 has_self,
3125 params_len,
3126 return_type,
3127 body,
3128 span,
3129 } = sig;
3130 let type_name_str = self.interner.resolve(&type_name).to_string();
3131
3132 // Signature check: `fn __drop(self)`, no extra params, no non-unit return.
3133 if !has_self {
3134 return Err(CompileError::new(
3135 ErrorKind::InvalidInlineDrop {
3136 type_name: type_name_str.clone(),
3137 reason: "must take `self` — found an associated function".into(),
3138 },
3139 span,
3140 ));
3141 }
3142 if params_len > 0 {
3143 return Err(CompileError::new(
3144 ErrorKind::InvalidInlineDrop {
3145 type_name: type_name_str.clone(),
3146 reason: "must take only `self` — extra parameters are not allowed".into(),
3147 },
3148 span,
3149 ));
3150 }
3151 let ret_str = self.interner.resolve(&return_type);
3152 if ret_str != "()" {
3153 return Err(CompileError::new(
3154 ErrorKind::InvalidInlineDrop {
3155 type_name: type_name_str.clone(),
3156 reason: format!("must return unit — found return type `{}`", ret_str),
3157 },
3158 span,
3159 ));
3160 }
3161
3162 // Affine-only: `@derive(Copy)` structs cannot have destructors (double-free risk).
3163 // `linear` structs cannot either — they are never implicitly dropped, so
3164 // a destructor would be unreachable.
3165 let struct_def_snapshot = self.type_pool.struct_def(struct_id);
3166 match struct_def_snapshot.posture {
3167 Posture::Copy => {
3168 return Err(CompileError::new(
3169 ErrorKind::InvalidInlineDrop {
3170 type_name: type_name_str.clone(),
3171 reason:
3172 "`@derive(Copy)` types cannot declare `fn __drop` (would double-free on copy)"
3173 .into(),
3174 },
3175 span,
3176 ));
3177 }
3178 Posture::Linear => {
3179 return Err(CompileError::new(
3180 ErrorKind::InvalidInlineDrop {
3181 type_name: type_name_str.clone(),
3182 reason: "`linear` types cannot declare `fn __drop` (linear values are never implicitly dropped)".into(),
3183 },
3184 span,
3185 ));
3186 }
3187 Posture::Affine => {}
3188 }
3189
3190 // ADR-0085: `fn __drop` on `@mark(c)` structs is forbidden in
3191 // v1. The C side owns cross-boundary cleanup discipline; mixing
3192 // Gruel destructor semantics with raw extern calls is left to a
3193 // future ADR with defined cross-boundary semantics.
3194 if struct_def_snapshot.is_c_layout {
3195 return Err(CompileError::new(
3196 ErrorKind::CFfi(format!(
3197 "`@mark(c) struct {}` cannot declare `fn __drop`; manual cleanup is required \
3198 through raw pointer discipline",
3199 type_name_str
3200 )),
3201 span,
3202 ));
3203 }
3204
3205 // Only one destructor per type.
3206 let mut struct_def = struct_def_snapshot;
3207 if struct_def.destructor.is_some() {
3208 return Err(CompileError::new(
3209 ErrorKind::DuplicateDestructor {
3210 type_name: type_name_str.clone(),
3211 },
3212 span,
3213 ));
3214 }
3215 let destructor_name = format!("{}.__drop", type_name_str);
3216 struct_def.destructor = Some(destructor_name);
3217 self.type_pool.update_struct_def(struct_id, struct_def);
3218
3219 self.inline_struct_drops.insert(struct_id, (body, span));
3220 Ok(())
3221 }
3222
3223 /// Register an inline `fn __drop(self)` as an enum's destructor (ADR-0053 phase 3b).
3224 ///
3225 /// Mirrors `register_inline_struct_drop`. Validates the signature
3226 /// (must be `fn __drop(self)` returning unit, only one per type) and
3227 /// stores the destructor metadata in `EnumDef.destructor`.
3228 fn register_inline_enum_drop(
3229 &mut self,
3230 type_name: Spur,
3231 enum_id: EnumId,
3232 sig: DropSignature,
3233 ) -> CompileResult<()> {
3234 let DropSignature {
3235 has_self,
3236 params_len,
3237 return_type,
3238 body,
3239 span,
3240 } = sig;
3241 let type_name_str = self.interner.resolve(&type_name).to_string();
3242
3243 if !has_self {
3244 return Err(CompileError::new(
3245 ErrorKind::InvalidInlineDrop {
3246 type_name: type_name_str,
3247 reason: "must take `self` — found an associated function".into(),
3248 },
3249 span,
3250 ));
3251 }
3252 if params_len > 0 {
3253 return Err(CompileError::new(
3254 ErrorKind::InvalidInlineDrop {
3255 type_name: type_name_str,
3256 reason: "must take only `self` — extra parameters are not allowed".into(),
3257 },
3258 span,
3259 ));
3260 }
3261 let ret_str = self.interner.resolve(&return_type);
3262 if ret_str != "()" {
3263 return Err(CompileError::new(
3264 ErrorKind::InvalidInlineDrop {
3265 type_name: type_name_str,
3266 reason: format!("must return unit — found return type `{}`", ret_str),
3267 },
3268 span,
3269 ));
3270 }
3271
3272 let mut enum_def = self.type_pool.enum_def(enum_id);
3273 if enum_def.destructor.is_some() {
3274 return Err(CompileError::new(
3275 ErrorKind::DuplicateDestructor {
3276 type_name: type_name_str,
3277 },
3278 span,
3279 ));
3280 }
3281 let destructor_name = format!("{}.__drop", type_name_str);
3282 enum_def.destructor = Some(destructor_name);
3283 self.type_pool.update_enum_def(enum_id, enum_def);
3284
3285 self.inline_enum_drops.insert(enum_id, (body, span));
3286 Ok(())
3287 }
3288
3289 /// Collect methods defined inline in a named enum (ADR-0053).
3290 ///
3291 /// Mirrors `collect_struct_methods`: registers each method against the
3292 /// enum's `EnumId` in `self.enum_methods`, which the method-resolution
3293 /// machinery already consults for anonymous enums (ADR-0039).
3294 fn collect_enum_methods(
3295 &mut self,
3296 type_name: Spur,
3297 methods_start: u32,
3298 methods_len: u32,
3299 span: Span,
3300 ) -> CompileResult<()> {
3301 if methods_len == 0 {
3302 return Ok(());
3303 }
3304 let enum_id = match self.enums.get(&type_name) {
3305 Some(id) => *id,
3306 None => {
3307 let type_name_str = self.interner.resolve(&type_name).to_string();
3308 return Err(CompileError::new(
3309 ErrorKind::UnknownType(type_name_str),
3310 span,
3311 ));
3312 }
3313 };
3314 let enum_type = Type::new_enum(enum_id);
3315 let drop_name_sym = self.interner.get_or_intern("__drop");
3316
3317 // ADR-0076: bind `Self` to the host enum while resolving method
3318 // signatures.
3319 let saved_self = self.current_self.replace(enum_type);
3320
3321 let methods = self.rir.get_inst_refs(methods_start, methods_len);
3322 for method_ref in methods {
3323 let method_inst = self.rir.get(method_ref);
3324 if let InstData::FnDecl {
3325 name: method_name,
3326 is_pub: method_is_pub,
3327 is_unchecked,
3328 params_start,
3329 params_len,
3330 return_type,
3331 body,
3332 has_self,
3333 receiver_mode,
3334 directives_start,
3335 directives_len,
3336 ..
3337 } = &method_inst.data
3338 {
3339 let receiver = decode_receiver_mode(*receiver_mode);
3340
3341 // ADR-0088: reject `@mark(unchecked) fn __drop`.
3342 if *method_name == drop_name_sym
3343 && self.directives_have_mark_unchecked(*directives_start, *directives_len)
3344 {
3345 return Err(CompileError::new(
3346 ErrorKind::UncheckedDestructor,
3347 method_inst.span,
3348 ));
3349 }
3350
3351 // ADR-0053 phase 3b: a method named `__drop` is the enum's destructor,
3352 // not a regular method. Route it through the per-enum destructor slot.
3353 if *method_name == drop_name_sym {
3354 self.register_inline_enum_drop(
3355 type_name,
3356 enum_id,
3357 DropSignature {
3358 has_self: *has_self,
3359 params_len: *params_len,
3360 return_type: *return_type,
3361 body: *body,
3362 span: method_inst.span,
3363 },
3364 )?;
3365 continue;
3366 }
3367 let key = (enum_id, *method_name);
3368 if self.enum_methods.contains_key(&key) {
3369 let type_name_str = self.interner.resolve(&type_name).to_string();
3370 let method_name_str = self.interner.resolve(method_name).to_string();
3371 return Err(CompileError::new(
3372 ErrorKind::DuplicateMethod {
3373 type_name: type_name_str,
3374 method_name: method_name_str,
3375 },
3376 method_inst.span,
3377 ));
3378 }
3379
3380 let params = self.rir.get_params(*params_start, *params_len);
3381 let param_names: Vec<Spur> = params.iter().map(|p| p.name).collect();
3382 let param_types: Vec<Type> = params
3383 .iter()
3384 .map(|p| self.resolve_type(p.ty, method_inst.span))
3385 .collect::<CompileResult<Vec<_>>>()?;
3386 let ret_type = self.resolve_type(*return_type, method_inst.span)?;
3387
3388 let param_range = self
3389 .param_arena
3390 .alloc_method(param_names.into_iter(), param_types.into_iter());
3391
3392 self.enum_methods.insert(
3393 key,
3394 MethodInfo {
3395 struct_type: enum_type,
3396 has_self: *has_self,
3397 receiver,
3398 params: param_range,
3399 return_type: ret_type,
3400 body: *body,
3401 span: method_inst.span,
3402 is_unchecked: *is_unchecked,
3403 // Enum methods do not yet support method-level
3404 // comptime type params (ADR-0055 defers that path).
3405 is_generic: false,
3406 return_type_sym: *return_type,
3407 is_pub: *method_is_pub,
3408 file_id: method_inst.span.file_id,
3409 },
3410 );
3411 }
3412 }
3413 self.current_self = saved_self;
3414 Ok(())
3415 }
3416
3417 /// Collect a constant declaration.
3418 ///
3419 /// Constants are compile-time values. In the module system, they're primarily
3420 /// used for re-exports:
3421 /// ```gruel
3422 /// pub const strings = @import("utils/strings.gruel");
3423 /// ```
3424 ///
3425 /// When the initializer is an `@import(...)`, we evaluate it at compile time
3426 /// to resolve the module and register it in the module registry. This enables
3427 /// subsequent member access via `const_name.function()` syntax.
3428 fn collect_const_declaration(
3429 &mut self,
3430 name: Spur,
3431 is_pub: bool,
3432 init: InstRef,
3433 span: Span,
3434 ) -> CompileResult<()> {
3435 // ADR-0078: detect item-level re-exports first. `pub const X = mod.Y`
3436 // makes X an alias for `mod.Y`, registering it in the appropriate
3437 // name table (functions/structs/enums/interfaces) so call sites
3438 // can use X transparently. The fallback path below handles
3439 // `pub const X = @import("...")` (whole-module re-export) and
3440 // primitive constants.
3441 if self.try_collect_reexport(name, is_pub, init, span)? {
3442 return Ok(());
3443 }
3444
3445 let name_str = self.interner.resolve(&name).to_string();
3446
3447 // Check for duplicate constant names
3448 if self.constants.contains_key(&name) {
3449 return Err(CompileError::new(
3450 ErrorKind::DuplicateConstant {
3451 name: name_str,
3452 kind: "constant".to_string(),
3453 },
3454 span,
3455 ));
3456 }
3457
3458 // Check for collision with function names
3459 if self.functions.contains_key(&name) {
3460 return Err(CompileError::new(
3461 ErrorKind::DuplicateConstant {
3462 name: name_str.clone(),
3463 kind: "constant (conflicts with function)".to_string(),
3464 },
3465 span,
3466 ));
3467 }
3468
3469 // Evaluate the initializer at compile time to determine the constant type.
3470 let const_type = self.evaluate_const_init(init, span)?;
3471
3472 self.constants.insert(
3473 name,
3474 ConstInfo {
3475 is_pub,
3476 ty: const_type,
3477 init,
3478 span,
3479 },
3480 );
3481
3482 Ok(())
3483 }
3484
3485 /// ADR-0078: Try to handle an item-level re-export of the form
3486 /// `pub const X = some_module_const.Y`. Returns `Ok(true)` if the
3487 /// constant was recognized as a re-export and registered as an alias;
3488 /// `Ok(false)` if this isn't a re-export and the regular const path
3489 /// should run; `Err(...)` if it looks like a re-export but the item
3490 /// can't be resolved.
3491 fn try_collect_reexport(
3492 &mut self,
3493 name: Spur,
3494 is_pub: bool,
3495 init: InstRef,
3496 span: Span,
3497 ) -> CompileResult<bool> {
3498 let inst = self.rir.get(init);
3499 let InstData::FieldGet { base, field } = &inst.data else {
3500 return Ok(false);
3501 };
3502 let field = *field;
3503
3504 // The base must be a `VarRef` to a const that holds a module.
3505 let base_inst = self.rir.get(*base);
3506 let InstData::VarRef { name: base_name } = &base_inst.data else {
3507 return Ok(false);
3508 };
3509 let module_id = match self.constants.get(base_name).and_then(|c| c.ty.as_module()) {
3510 Some(id) => id,
3511 None => return Ok(false),
3512 };
3513 let module_file_path = self.module_registry.get_def(module_id).file_path.clone();
3514 let same_name = name == field;
3515
3516 // Look the field up across each item kind, restricted to items
3517 // declared in the imported module's file.
3518
3519 // Function.
3520 if let Some(fn_info) = self.functions.get(&field).copied() {
3521 let fn_path = self
3522 .get_file_path(fn_info.file_id)
3523 .map(|s| s.to_string())
3524 .unwrap_or_default();
3525 if fn_path == module_file_path {
3526 if !same_name {
3527 self.check_alias_collision(name, span)?;
3528 // Resolve to the canonical (non-aliased) name so chains
3529 // of re-exports collapse to the original function symbol.
3530 let canonical = fn_info.canonical_name.unwrap_or(field);
3531 let alias = FunctionInfo {
3532 is_pub,
3533 canonical_name: Some(canonical),
3534 ..fn_info
3535 };
3536 self.functions.insert(name, alias);
3537 }
3538 return Ok(true);
3539 }
3540 }
3541
3542 // Struct.
3543 if let Some(&struct_id) = self.structs.get(&field) {
3544 let s_path = self
3545 .get_file_path(self.type_pool.struct_def(struct_id).file_id)
3546 .map(|s| s.to_string())
3547 .unwrap_or_default();
3548 if s_path == module_file_path {
3549 if !same_name {
3550 self.check_alias_collision(name, span)?;
3551 self.structs.insert(name, struct_id);
3552 }
3553 return Ok(true);
3554 }
3555 }
3556
3557 // Enum.
3558 if let Some(&enum_id) = self.enums.get(&field) {
3559 let e_path = self
3560 .get_file_path(self.type_pool.enum_def(enum_id).file_id)
3561 .map(|s| s.to_string())
3562 .unwrap_or_default();
3563 if e_path == module_file_path {
3564 if !same_name {
3565 self.check_alias_collision(name, span)?;
3566 self.enums.insert(name, enum_id);
3567 }
3568 return Ok(true);
3569 }
3570 }
3571
3572 // Interface.
3573 if let Some(&iface_id) = self.interfaces.get(&field) {
3574 let iface_def = &self.interface_defs[iface_id.0 as usize];
3575 let i_path = self
3576 .get_file_path(iface_def.file_id)
3577 .map(|s| s.to_string())
3578 .unwrap_or_default();
3579 if i_path == module_file_path {
3580 if !same_name {
3581 self.check_alias_collision(name, span)?;
3582 self.interfaces.insert(name, iface_id);
3583 }
3584 return Ok(true);
3585 }
3586 }
3587
3588 // Field exists in the module's namespace but doesn't resolve to a
3589 // re-exportable item. Could be e.g. `mod.NotAnItem` — surface as a
3590 // standard "unknown module member" error.
3591 let module_def = self.module_registry.get_def(module_id);
3592 let module_name = module_def.import_path.clone();
3593 let field_str = self.interner.resolve(&field).to_string();
3594 Err(CompileError::new(
3595 ErrorKind::UnknownModuleMember {
3596 module_name,
3597 member_name: field_str,
3598 },
3599 span,
3600 ))
3601 }
3602
3603 /// Collision check for a re-export alias. The alias name must not
3604 /// shadow an existing function/struct/enum/interface/constant.
3605 fn check_alias_collision(&self, name: Spur, span: Span) -> CompileResult<()> {
3606 let name_str = self.interner.resolve(&name).to_string();
3607 if self.functions.contains_key(&name)
3608 || self.structs.contains_key(&name)
3609 || self.enums.contains_key(&name)
3610 || self.interfaces.contains_key(&name)
3611 || self.constants.contains_key(&name)
3612 {
3613 return Err(CompileError::new(
3614 ErrorKind::DuplicateConstant {
3615 name: name_str,
3616 kind: "re-export alias".to_string(),
3617 },
3618 span,
3619 ));
3620 }
3621 Ok(())
3622 }
3623
3624 /// Evaluate a constant initializer at compile time.
3625 ///
3626 /// Currently handles:
3627 /// - `@import("path")` - Returns Type::Module
3628 /// - Integer literals - Returns the integer type
3629 ///
3630 /// Future extensions (ADR-0025 comptime) will support:
3631 /// - Arithmetic on constants
3632 /// - comptime blocks
3633 /// - comptime function calls
3634 fn evaluate_const_init(&mut self, init: InstRef, span: Span) -> CompileResult<Type> {
3635 let init_inst = self.rir.get(init);
3636
3637 match &init_inst.data {
3638 // @import("path") evaluates to Type::Module at compile time
3639 InstData::Intrinsic {
3640 name,
3641 args_start,
3642 args_len,
3643 } => {
3644 if *name == self.known.import {
3645 // Validate exactly one argument
3646 if *args_len != 1 {
3647 return Err(CompileError::new(
3648 ErrorKind::IntrinsicWrongArgCount {
3649 name: "import".to_string(),
3650 expected: 1,
3651 found: *args_len as usize,
3652 },
3653 span,
3654 ));
3655 }
3656
3657 // Accept a string literal or a comptime_str expression.
3658 let arg_refs = self.rir.get_inst_refs(*args_start, *args_len);
3659 let import_path = self.resolve_import_path_arg(arg_refs[0])?;
3660
3661 // Resolve the import path to an absolute file path
3662 let resolved_path = self.resolve_import_path(&import_path, span)?;
3663
3664 // Register the module in the registry
3665 let (module_id, _is_new) = self
3666 .module_registry
3667 .get_or_create(import_path, resolved_path);
3668
3669 Ok(Type::new_module(module_id))
3670 } else {
3671 // For other intrinsics in const context, we don't support them yet
3672 let intrinsic_name = self.interner.resolve(name).to_string();
3673 Err(CompileError::new(
3674 ErrorKind::ConstExprNotSupported {
3675 expr_kind: format!("@{} intrinsic", intrinsic_name),
3676 },
3677 span,
3678 ))
3679 }
3680 }
3681
3682 // Integer literals evaluate to i32 (the default integer type)
3683 // Note: RIR doesn't distinguish between integer types at this level;
3684 // type inference happens later. For now, we treat all integer consts as i32.
3685 InstData::IntConst(_) => Ok(Type::I32),
3686
3687 // Boolean literals
3688 InstData::BoolConst(_) => Ok(Type::BOOL),
3689
3690 // Unit literal
3691 InstData::UnitConst => Ok(Type::UNIT),
3692
3693 // String literals
3694 InstData::StringConst(_) => {
3695 // String constants would need the String type
3696 // For now, we don't support them in const context
3697 Err(CompileError::new(
3698 ErrorKind::ConstExprNotSupported {
3699 expr_kind: "string literals".to_string(),
3700 },
3701 span,
3702 ))
3703 }
3704
3705 // Other expressions are not yet supported in const context
3706 _ => Err(CompileError::new(
3707 ErrorKind::ConstExprNotSupported {
3708 expr_kind: "this expression".to_string(),
3709 },
3710 span,
3711 )),
3712 }
3713 }
3714}
3715
3716/// Build the trailing note for an unknown-directive error (ADR-0075).
3717///
3718/// Retired directives get a targeted retirement message pointing at the
3719/// ADR that took them out of service. Other names get an edit-distance
3720/// hint when there's a near-match in the recognized set; otherwise no
3721/// note is attached.
3722fn directive_diagnosis_note(name: &str) -> Option<String> {
3723 match name {
3724 "handle" => {
3725 return Some(
3726 "the `@handle` directive was retired in ADR-0075; \
3727 conform to the `Handle` interface by defining \
3728 `fn handle(self: Ref(Self)) -> Self` directly"
3729 .to_string(),
3730 );
3731 }
3732 "copy" => {
3733 return Some(
3734 "the `@copy` directive was retired in ADR-0059; \
3735 use `@derive(Copy)` instead"
3736 .to_string(),
3737 );
3738 }
3739 _ => {}
3740 }
3741
3742 const RECOGNIZED: &[&str] = &["allow", "derive", "mark"];
3743 RECOGNIZED
3744 .iter()
3745 .map(|cand| (cand, levenshtein_distance(name, cand)))
3746 .filter(|(_, d)| *d <= 2)
3747 .min_by_key(|(_, d)| *d)
3748 .map(|(cand, _)| format!("did you mean `@{cand}`?"))
3749}
3750
3751/// ADR-0083: edit-distance suggestion for a typo'd marker name.
3752///
3753/// Returns a "did you mean `X`?" hint when there's a near-match in the
3754/// `BUILTIN_MARKERS` registry, otherwise emits a list of the recognized
3755/// markers so users see what's available.
3756fn marker_diagnosis_note(name: &str) -> Option<String> {
3757 let names: Vec<&'static str> = BUILTIN_MARKERS.iter().map(|m| m.name).collect();
3758 if let Some(suggestion) = names
3759 .iter()
3760 .map(|cand| (*cand, levenshtein_distance(name, cand)))
3761 .filter(|(_, d)| *d <= 2)
3762 .min_by_key(|(_, d)| *d)
3763 .map(|(cand, _)| cand)
3764 {
3765 return Some(format!("did you mean `{suggestion}`?"));
3766 }
3767 Some(format!("recognized markers: {}", names.join(", ")))
3768}
3769
3770/// Wagner-Fischer Levenshtein distance. Used by `directive_diagnosis_note`
3771/// to suggest near-matches for typo'd directive names. Both inputs are
3772/// short identifiers (~10 chars), so the O(n·m) cost is negligible and the
3773/// dependency-free implementation is preferable to pulling in `strsim`.
3774fn levenshtein_distance(a: &str, b: &str) -> usize {
3775 let a: Vec<char> = a.chars().collect();
3776 let b: Vec<char> = b.chars().collect();
3777 let (n, m) = (a.len(), b.len());
3778 if n == 0 {
3779 return m;
3780 }
3781 if m == 0 {
3782 return n;
3783 }
3784 let mut prev: Vec<usize> = (0..=m).collect();
3785 let mut curr: Vec<usize> = vec![0; m + 1];
3786 for i in 1..=n {
3787 curr[0] = i;
3788 for j in 1..=m {
3789 let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
3790 curr[j] = (curr[j - 1] + 1).min(prev[j] + 1).min(prev[j - 1] + cost);
3791 }
3792 std::mem::swap(&mut prev, &mut curr);
3793 }
3794 prev[m]
3795}
3796
3797#[cfg(test)]
3798mod directive_validation_tests {
3799 use super::{directive_diagnosis_note, levenshtein_distance};
3800
3801 #[test]
3802 fn levenshtein_basics() {
3803 assert_eq!(levenshtein_distance("kitten", "sitting"), 3);
3804 assert_eq!(levenshtein_distance("allow", "allwo"), 2);
3805 assert_eq!(levenshtein_distance("derive", "dervie"), 2);
3806 assert_eq!(levenshtein_distance("derive", "derive"), 0);
3807 assert_eq!(levenshtein_distance("", "abc"), 3);
3808 assert_eq!(levenshtein_distance("abc", ""), 3);
3809 }
3810
3811 #[test]
3812 fn note_for_typo_near_allow() {
3813 let note = directive_diagnosis_note("allwo").expect("near-match should suggest");
3814 assert!(note.contains("@allow"), "got: {note}");
3815 }
3816
3817 #[test]
3818 fn note_for_typo_near_derive() {
3819 let note = directive_diagnosis_note("dervie").expect("near-match should suggest");
3820 assert!(note.contains("@derive"), "got: {note}");
3821 }
3822
3823 #[test]
3824 fn note_for_far_typo_is_none() {
3825 assert!(directive_diagnosis_note("xyzzy").is_none());
3826 }
3827
3828 #[test]
3829 fn note_for_retired_handle_mentions_adr_0075() {
3830 let note = directive_diagnosis_note("handle").expect("retirement note");
3831 assert!(note.contains("ADR-0075"), "got: {note}");
3832 assert!(note.contains("Handle"), "got: {note}");
3833 }
3834
3835 #[test]
3836 fn note_for_retired_copy_mentions_adr_0059() {
3837 let note = directive_diagnosis_note("copy").expect("retirement note");
3838 assert!(note.contains("ADR-0059"), "got: {note}");
3839 assert!(note.contains("@derive(Copy)"), "got: {note}");
3840 }
3841}