Skip to main content

gruel_intrinsics/
lib.rs

1//! Declarative registry of Gruel's `@intrinsic` set.
2//!
3//! This crate is the single source of truth for every intrinsic the compiler
4//! recognizes. Each intrinsic is described by an [`IntrinsicDef`] value; the
5//! full list lives in [`INTRINSICS`]. Compiler stages (RIR astgen, Sema,
6//! codegen) consult the registry instead of carrying their own name lists, and
7//! the website's intrinsic reference page is generated from the same data.
8//!
9//! Behavior (semantic analyzers, codegen arms) still lives in the consumer
10//! crates — the registry owns metadata and identity, not per-intrinsic logic.
11//! Stages dispatch on the stable [`IntrinsicId`] enum rather than matching
12//! strings.
13//!
14//! ## What earns a place in this registry
15//!
16//! ADR-0087 commits to the rule: **intrinsics carry compiler magic, not
17//! transport.** A row earns its place in `IntrinsicId` / [`INTRINSICS`] if it
18//! does at least one of:
19//!
20//! 1. **Codegen-emitted lowering** of a language feature (e.g. `@vec(...)`
21//!    constructs a `Vec(T)` aggregate; pointer ops emit a typed `load` /
22//!    `store`; `@spawn` synthesises a `@mark(c)` thunk per `(arg, ret, fn)`
23//!    triple).
24//! 2. **Compile-time type / kind dispatch** on heterogeneous arguments (e.g.
25//!    `@dbg(42, true, "hi")` routes per arg type; `@type_info(T)`).
26//! 3. **Compile-time evaluation** with no runtime presence (e.g. `@size_of`,
27//!    `@align_of`, `@target_arch`, `@compile_error`).
28//! 4. A row is **blocked on a missing language feature** that would otherwise
29//!    let it move to the prelude (ADR-0087 currently tracks four such rows —
30//!    `@panic` family, `@spawn` / `@thread_join`, `@cstr_to_vec`, `@dbg` —
31//!    each with a documented prerequisite).
32//!
33//! Rows that exist only because "there is a libc function we want to call"
34//! and have an expressible Gruel signature today do NOT belong here. ADR-0087
35//! migrated the original wave of those rows (`@read_line`, `@parse_*`,
36//! `@random_*`, `@utf8_validate`, `@bytes_eq`, `@alloc` / `@free` /
37//! `@realloc`) to prelude fns in `prelude/runtime_wrappers.gruel`; future
38//! additions of similar shape should land there first.
39//!
40//! See [ADR-0050](../../docs/designs/0050-intrinsics-crate.md) and
41//! [ADR-0087](../../docs/designs/0087-prelude-fns-for-libc-wrappers.md).
42
43use gruel_util::PreviewFeature;
44
45// ============================================================================
46// Enums
47// ============================================================================
48
49/// Stable identity for every intrinsic. Stages dispatch on this rather than
50/// comparing strings, so adding an intrinsic requires updating a closed match
51/// in each consumer — the compiler enforces coverage.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub enum IntrinsicId {
54    // ---- Debug / diagnostics ----
55    Dbg,
56    Panic,
57    Assert,
58    CompileError,
59
60    // ---- Casts ----
61    Cast,
62
63    // ---- I/O ----
64    // ADR-0087 Phase 3: `@read_line`, `@parse_*`, `@random_*` retired
65    // here — replaced by prelude fns `read_line()`, `parse_i32(s)`
66    // etc. in `prelude/runtime_wrappers.gruel`. The runtime helpers
67    // (`__gruel_parse_*`, `__gruel_random_*`) remain; the prelude fns
68    // wrap them.
69
70    // ---- Comptime / reflection ----
71    SizeOf,
72    AlignOf,
73    TypeName,
74    TypeInfo,
75    Ownership,
76    /// ADR-0084: comptime classification of `T` on the `Unsend < Send <
77    /// Sync` ladder.
78    ThreadSafety,
79    Implements,
80    Field,
81    Import,
82    EmbedFile,
83
84    // ---- Target platform ----
85    TargetArch,
86    TargetOs,
87
88    // ---- Pointer operations (require unchecked) ----
89    PtrRead,
90    PtrWrite,
91    PtrReadVolatile,
92    PtrWriteVolatile,
93    PtrOffset,
94    PtrToInt,
95    IntToPtr,
96    NullPtr,
97    IsNull,
98    PtrCopy,
99    Raw,
100    RawMut,
101
102    // ---- Syscall (requires unchecked) ----
103    Syscall,
104
105    // ---- For-loop iteration helpers ----
106    Range,
107
108    // ---- Slice operations (ADR-0064) ----
109    SliceLen,
110    SliceIsEmpty,
111    SliceIndexRead,
112    SliceIndexWrite,
113    SlicePtr,
114    SlicePtrMut,
115    PartsToSlice,
116    PartsToMutSlice,
117
118    // ---- Vec operations (ADR-0066) ----
119    // ADR-0082: most Vec method intrinsics retired in favour of
120    // dispatching through `prelude/vec.gruel`'s `pub fn Vec(...)`
121    // instantiation. The remaining variants here are the *user-facing*
122    // syntactic intrinsics (`@vec`, `@vec_repeat`, `@parts_to_vec`)
123    // — they bypass the prelude struct because they construct one.
124    VecLiteral,
125    VecRepeat,
126    PartsToVec,
127
128    // ---- ADR-0072 String / Vec(u8) bridge ----
129    // ADR-0087 Phase 3: `@utf8_validate` retired in favour of the
130    // `utf8_validate(s: Slice(u8)) -> bool` prelude fn. The runtime
131    // helper `__gruel_utf8_validate` remains. `@cstr_to_vec` stays
132    // (blocked on whole-aggregate `@uninit` per ADR-0087).
133    CStrToVec,
134
135    // ---- ADR-0082 memory intrinsics (require checked) ----
136    // ADR-0087 Phase 4: `@alloc` / `@realloc` / `@free` retired in
137    // favour of the prelude `mem_alloc` / `mem_realloc` / `mem_free`
138    // fns (named with the `mem_` prefix to avoid LLVM-symbol
139    // collisions with libc `free` / `realloc`).
140    /// `@ptr_cast(p) -> MutPtr(T)` / `Ptr(T)`: reinterpret the pointer as
141    /// the inferred target pointer type. Result type comes from HM
142    /// inference (let-annotation or call context).
143    PtrCast,
144    // ADR-0087 Phase 3: `@bytes_eq` retired in favour of the prelude
145    // `bytes_eq(a, b, n) -> bool` fn (wraps libc `memcmp`).
146
147    // ---- ADR-0079 Phase 2b: derive-construction primitives ----
148    /// `@uninit(T) -> Uninit(T)`: handle to T-sized storage. Drop is
149    /// suppressed on the slot until `@finalize` consumes it.
150    Uninit,
151    /// `@finalize(handle) -> T`: consume the handle and hand back a
152    /// real `T`. Sema verifies all fields written.
153    Finalize,
154    /// `@field_set(handle, name, value)`: write a field of an
155    /// in-progress `@uninit` / `@variant_uninit` handle. The
156    /// write-side counterpart to read-side `@field`.
157    FieldSet,
158    /// `@variant_uninit(Self, comptime tag) -> Uninit(Self)`:
159    /// variant-shaped counterpart to `@uninit`.
160    VariantUninit,
161    /// `@variant_field(self, comptime tag, name)`: read the named
162    /// field of variant `tag` from `self`. Symmetric counterpart to
163    /// `@variant_uninit + @field_set`.
164    VariantField,
165
166    // ---- ADR-0084 thread spawn ----
167    /// `@spawn(fn, arg) -> JoinHandle(R)` — spawn a worker thread.
168    Spawn,
169    /// `@thread_join(handle: MutPtr(u8)) -> R` — internal lowering
170    /// target for `JoinHandle::join`. Reads the runtime handle out
171    /// of the prelude struct's `handle` field, calls
172    /// `__gruel_thread_join`, copies the return value into a stack
173    /// slot, and returns it. Codegen consults the let-binding
174    /// context for the result type. Not user-callable.
175    ThreadJoin,
176
177    // ---- Preview / test infra ----
178    TestPreviewGate,
179}
180
181/// Whether an intrinsic takes an expression argument list (the common case)
182/// or a type argument (`@size_of(T)`, `@type_info(T)`, etc.).
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum IntrinsicKind {
185    /// Normal expression intrinsic: `@name(expr, ...)`.
186    Expr,
187    /// Type intrinsic: `@name(Type)` where the argument is a type expression.
188    Type,
189    /// Type-and-interface intrinsic: `@name(Type, Interface)` where the
190    /// first argument is a type expression and the second names an
191    /// interface (e.g. `@implements(T, Drop)`).
192    TypeInterface,
193}
194
195/// High-level grouping used when rendering the documentation reference page.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum Category {
198    Debug,
199    Cast,
200    Io,
201    Parse,
202    Random,
203    Comptime,
204    Platform,
205    Pointer,
206    Syscall,
207    Iteration,
208    Slice,
209    Vec,
210    Meta,
211}
212
213impl Category {
214    /// Human-readable heading for this category in generated docs.
215    pub fn heading(&self) -> &'static str {
216        match self {
217            Category::Debug => "Debug & Diagnostics",
218            Category::Cast => "Type Casts",
219            Category::Io => "I/O",
220            Category::Parse => "String Parsing",
221            Category::Random => "Random Numbers",
222            Category::Comptime => "Compile-time Reflection",
223            Category::Platform => "Target Platform",
224            Category::Pointer => "Raw Pointers",
225            Category::Syscall => "System Calls",
226            Category::Iteration => "Iteration",
227            Category::Slice => "Slices",
228            Category::Vec => "Vectors",
229            Category::Meta => "Preview / Meta",
230        }
231    }
232
233    /// Canonical render order for the documentation reference page. The
234    /// quick-reference table groups rows by category in this order, and the
235    /// per-category detail sections appear in the same order. Adding a new
236    /// `Category` variant requires extending this list — the test
237    /// `category_render_order_is_exhaustive` enforces that every variant
238    /// appears exactly once.
239    pub const RENDER_ORDER: &'static [Category] = &[
240        Category::Debug,
241        Category::Cast,
242        Category::Io,
243        Category::Parse,
244        Category::Random,
245        Category::Comptime,
246        Category::Platform,
247        Category::Iteration,
248        Category::Slice,
249        Category::Vec,
250        Category::Pointer,
251        Category::Syscall,
252        Category::Meta,
253    ];
254
255    /// Position in [`RENDER_ORDER`]. Used as the primary sort key for the
256    /// quick-reference table so categories don't interleave.
257    fn render_index(self) -> usize {
258        Self::RENDER_ORDER
259            .iter()
260            .position(|c| *c == self)
261            .expect("every Category variant must appear in RENDER_ORDER")
262    }
263}
264
265// ============================================================================
266// IntrinsicDef
267// ============================================================================
268
269/// Metadata for one intrinsic. Instances live as `const` entries in
270/// [`INTRINSICS`]; nothing in this type is runtime-mutable.
271#[derive(Debug, Clone, Copy)]
272pub struct IntrinsicDef {
273    /// Stable enum identity used for dispatch in consumer crates.
274    pub id: IntrinsicId,
275    /// Name as written in source (without the leading `@`).
276    pub name: &'static str,
277    /// Whether the sole argument is a type (`Type`) or a normal expression list (`Expr`).
278    pub kind: IntrinsicKind,
279    /// Category used for doc rendering and `by_category` lookups.
280    pub category: Category,
281    /// If `true`, calls must appear inside an `unchecked` block (enforced by sema).
282    pub requires_unchecked: bool,
283    /// Preview feature gate, if any. `None` means the intrinsic is stable.
284    pub preview: Option<PreviewFeature>,
285    /// Extern symbol in `gruel-runtime` that implements this intrinsic, if the
286    /// codegen path lowers to a runtime call. `None` if the codegen emits LLVM
287    /// IR directly (e.g. pointer ops) or is otherwise self-contained.
288    pub runtime_fn: Option<&'static str>,
289    /// Terse one-line description used in the quick-reference table.
290    pub summary: &'static str,
291    /// Longer markdown prose for the per-intrinsic detail section.
292    pub description: &'static str,
293    /// Sample code snippets rendered in the reference page.
294    pub examples: &'static [&'static str],
295}
296
297// ============================================================================
298// Registry
299// ============================================================================
300
301/// The canonical list of every intrinsic the compiler recognizes.
302///
303/// Adding an intrinsic: append a new [`IntrinsicDef`] here, extend
304/// [`IntrinsicId`] with a matching variant, and implement the per-intrinsic
305/// behavior arms in sema/codegen (the compiler's exhaustive matches will force
306/// you to).
307pub const INTRINSICS: &[IntrinsicDef] = &[
308    IntrinsicDef {
309        id: IntrinsicId::Dbg,
310        name: "dbg",
311        kind: IntrinsicKind::Expr,
312        category: Category::Debug,
313        requires_unchecked: false,
314        preview: None,
315        runtime_fn: None, // Lowers to multiple runtime calls depending on arg type.
316        summary: "Print values to stderr with a trailing newline.",
317        description: "`@dbg(v1, v2, ...)` prints each argument separated by spaces, then a newline. Accepts integers, bools, and `String` values.",
318        examples: &["@dbg(42, true, \"hello\")"],
319    },
320    IntrinsicDef {
321        id: IntrinsicId::Panic,
322        name: "panic",
323        kind: IntrinsicKind::Expr,
324        category: Category::Debug,
325        requires_unchecked: false,
326        preview: None,
327        runtime_fn: None,
328        summary: "Abort the program with an optional message.",
329        description: "`@panic()` or `@panic(\"message\")` terminates the program. Diverges (returns `Never`).",
330        examples: &["@panic(\"unreachable\")"],
331    },
332    IntrinsicDef {
333        id: IntrinsicId::Assert,
334        name: "assert",
335        kind: IntrinsicKind::Expr,
336        category: Category::Debug,
337        requires_unchecked: false,
338        preview: None,
339        runtime_fn: None,
340        summary: "Check a boolean condition; panic if false.",
341        description: "`@assert(cond)` panics with a diagnostic if `cond` is false. Elided in release builds (future work).",
342        examples: &["@assert(x > 0)"],
343    },
344    IntrinsicDef {
345        id: IntrinsicId::CompileError,
346        name: "compile_error",
347        kind: IntrinsicKind::Expr,
348        category: Category::Comptime,
349        requires_unchecked: false,
350        preview: None,
351        runtime_fn: None,
352        summary: "Emit a compile-time error.",
353        description: "`@compile_error(\"msg\")` aborts compilation with the given message. Useful for unreachable comptime branches.",
354        examples: &["@compile_error(\"unsupported target\")"],
355    },
356    IntrinsicDef {
357        id: IntrinsicId::Cast,
358        name: "cast",
359        kind: IntrinsicKind::Expr,
360        category: Category::Cast,
361        requires_unchecked: false,
362        preview: None,
363        runtime_fn: None,
364        summary: "Numeric type conversion.",
365        description: "`@cast(x)` converts between integer and/or float types. The target type is inferred from context.",
366        examples: &["let y: i64 = @cast(x);"],
367    },
368    // ADR-0087 Phase 3: read_line, parse_*, random_* moved to prelude
369    // fns in `prelude/runtime_wrappers.gruel`. The user-facing surface
370    // is now `read_line()`, `parse_i32(&s)`, `random_u32()` etc. (no
371    // `@` prefix). The runtime helpers (`__gruel_parse_*`,
372    // `__gruel_random_*`) remain; `__gruel_read_line` is deleted —
373    // the new `read_line` prelude body loops over libc `read`.
374    IntrinsicDef {
375        id: IntrinsicId::SizeOf,
376        name: "size_of",
377        kind: IntrinsicKind::Type,
378        category: Category::Comptime,
379        requires_unchecked: false,
380        preview: None,
381        runtime_fn: None,
382        summary: "Size of a type in bytes.",
383        description: "`@size_of(T)` returns `sizeof(T)` as `usize`, evaluated at compile time.",
384        examples: &["@size_of(i64)"],
385    },
386    IntrinsicDef {
387        id: IntrinsicId::AlignOf,
388        name: "align_of",
389        kind: IntrinsicKind::Type,
390        category: Category::Comptime,
391        requires_unchecked: false,
392        preview: None,
393        runtime_fn: None,
394        summary: "Alignment of a type in bytes.",
395        description: "`@align_of(T)` returns the required alignment of `T` as `usize`, evaluated at compile time.",
396        examples: &["@align_of(i64)"],
397    },
398    IntrinsicDef {
399        id: IntrinsicId::TypeName,
400        name: "type_name",
401        kind: IntrinsicKind::Type,
402        category: Category::Comptime,
403        requires_unchecked: false,
404        preview: None,
405        runtime_fn: None,
406        summary: "Name of a type as a comptime string.",
407        description: "`@type_name(T)` returns the canonical name of `T` as a comptime-known string.",
408        examples: &["@type_name(i64) // \"i64\""],
409    },
410    IntrinsicDef {
411        id: IntrinsicId::TypeInfo,
412        name: "type_info",
413        kind: IntrinsicKind::Type,
414        category: Category::Comptime,
415        requires_unchecked: false,
416        preview: None,
417        runtime_fn: None,
418        summary: "Reflective info about a type.",
419        description: "`@type_info(T)` returns a comptime struct describing `T` (kind, fields, variants, ...).",
420        examples: &["@type_info(MyStruct)"],
421    },
422    IntrinsicDef {
423        id: IntrinsicId::Ownership,
424        name: "ownership",
425        kind: IntrinsicKind::Type,
426        category: Category::Comptime,
427        requires_unchecked: false,
428        preview: None,
429        runtime_fn: None,
430        summary: "Ownership posture of a type (`Copy`, `Affine`, or `Linear`).",
431        description: "`@ownership(T)` returns a variant of the built-in `Ownership` enum classifying `T`'s ownership posture (see ADR-0008): `Copy` if values can be implicitly duplicated, `Linear` if values must be explicitly consumed, or `Affine` otherwise (move-once with implicit drop). Evaluated at compile time.",
432        examples: &[
433            "@ownership(i32) // Ownership::Copy",
434            "@ownership(String) // Ownership::Affine",
435            "match @ownership(T) { Ownership::Copy => ..., Ownership::Affine => ..., Ownership::Linear => ... }",
436        ],
437    },
438    IntrinsicDef {
439        id: IntrinsicId::ThreadSafety,
440        name: "thread_safety",
441        kind: IntrinsicKind::Type,
442        category: Category::Comptime,
443        requires_unchecked: false,
444        preview: None,
445        runtime_fn: None,
446        summary: "Thread-safety classification of a type (`Unsend`, `Send`, or `Sync`).",
447        description: "`@thread_safety(T)` returns a variant of the built-in `ThreadSafety` enum classifying `T` on the trichotomy `Unsend < Send < Sync` (see ADR-0084): `Unsend` if `T` cannot cross a thread boundary, `Send` if it can be moved between threads, `Sync` if it can be shared. Primitives are intrinsically `Sync`; raw pointers (`Ptr(T)` / `MutPtr(T)`) are intrinsically `Unsend`. Composite types take the structural minimum over their members, optionally overridden by `@mark(unsend)` / `@mark(checked_send)` / `@mark(checked_sync)`. Evaluated at compile time.",
448        examples: &[
449            "@thread_safety(i32) // ThreadSafety::Sync",
450            "@thread_safety(MutPtr(u8)) // ThreadSafety::Unsend",
451            "comptime if (@thread_safety(T) == ThreadSafety::Sync) { ... }",
452        ],
453    },
454    IntrinsicDef {
455        id: IntrinsicId::Implements,
456        name: "implements",
457        kind: IntrinsicKind::TypeInterface,
458        category: Category::Comptime,
459        requires_unchecked: false,
460        preview: None,
461        runtime_fn: None,
462        summary: "Whether a type structurally implements an interface.",
463        description: "`@implements(T, I)` returns `true` if type `T` satisfies every method requirement of interface `I` (see ADR-0056), and `false` otherwise. Built-in interfaces `Copy` and `Drop` use the language's ownership rules rather than user methods. The result is a `bool` evaluated at compile time, so `@implements(...)` can be used to gate `comptime if` branches and other comptime decisions.",
464        examples: &[
465            "@implements(i32, Copy) // true",
466            "@implements(String, Copy) // false",
467            "@implements(MyType, Drop)",
468        ],
469    },
470    IntrinsicDef {
471        id: IntrinsicId::Field,
472        name: "field",
473        kind: IntrinsicKind::Expr,
474        category: Category::Comptime,
475        requires_unchecked: false,
476        preview: None,
477        runtime_fn: None,
478        summary: "Access a field by comptime-known name.",
479        description: "`@field(value, \"name\")` reads the named field of `value`, with the name resolved at compile time.",
480        examples: &["@field(s, \"x\")"],
481    },
482    IntrinsicDef {
483        id: IntrinsicId::Import,
484        name: "import",
485        kind: IntrinsicKind::Expr,
486        category: Category::Comptime,
487        requires_unchecked: false,
488        preview: None,
489        runtime_fn: None,
490        summary: "Import another source file (placeholder).",
491        description: "`@import(\"path\")` — planned module-system hook; currently accepted by the compiler as a placeholder.",
492        examples: &["@import(\"utils.gruel\")"],
493    },
494    IntrinsicDef {
495        id: IntrinsicId::EmbedFile,
496        name: "embed_file",
497        kind: IntrinsicKind::Expr,
498        category: Category::Comptime,
499        requires_unchecked: false,
500        preview: None,
501        runtime_fn: None,
502        summary: "Embed a file's contents at compile time as `Slice(u8)`.",
503        description: "`@embed_file(\"path\")` reads the file at compile time and produces a `Slice(u8)` whose bytes are baked into the binary as a read-only global. The path is resolved relative to the source file containing the call.",
504        examples: &["let data: Slice(u8) = @embed_file(\"asset.bin\");"],
505    },
506    IntrinsicDef {
507        id: IntrinsicId::TargetArch,
508        name: "target_arch",
509        kind: IntrinsicKind::Expr,
510        category: Category::Platform,
511        requires_unchecked: false,
512        preview: None,
513        runtime_fn: None,
514        summary: "Compile target CPU architecture.",
515        description: "`@target_arch()` returns a variant of the built-in `Arch` enum.",
516        examples: &["if @target_arch() == Arch::Aarch64 { ... }"],
517    },
518    IntrinsicDef {
519        id: IntrinsicId::TargetOs,
520        name: "target_os",
521        kind: IntrinsicKind::Expr,
522        category: Category::Platform,
523        requires_unchecked: false,
524        preview: None,
525        runtime_fn: None,
526        summary: "Compile target operating system.",
527        description: "`@target_os()` returns a variant of the built-in `Os` enum.",
528        examples: &["if @target_os() == Os::Linux { ... }"],
529    },
530    // ADR-0063: pointer operations are no longer user-callable via the
531    // `@…` namespace — the surface form is `p.method(...)` /
532    // `Ptr(T)::name(...)`. The metadata entries remain so codegen and
533    // `lookup_by_id` can find each intrinsic by `IntrinsicId`. Sema's
534    // `analyze_intrinsic_impl` rejects the `@…` form for these
535    // intrinsics; the same `IntrinsicId` is reachable through the
536    // POINTER_METHODS registry.
537    IntrinsicDef {
538        id: IntrinsicId::PtrRead,
539        name: "ptr_read",
540        kind: IntrinsicKind::Expr,
541        category: Category::Pointer,
542        requires_unchecked: true,
543        preview: None,
544        runtime_fn: None,
545        summary: "Load a value through a raw pointer (internal).",
546        description: "Internal lowering target for `p.read()` (ADR-0063).",
547        examples: &[],
548    },
549    IntrinsicDef {
550        id: IntrinsicId::PtrWrite,
551        name: "ptr_write",
552        kind: IntrinsicKind::Expr,
553        category: Category::Pointer,
554        requires_unchecked: true,
555        preview: None,
556        runtime_fn: None,
557        summary: "Store a value through a raw mutable pointer (internal).",
558        description: "Internal lowering target for `p.write(v)` (ADR-0063).",
559        examples: &[],
560    },
561    IntrinsicDef {
562        id: IntrinsicId::PtrReadVolatile,
563        name: "ptr_read_volatile",
564        kind: IntrinsicKind::Expr,
565        category: Category::Pointer,
566        requires_unchecked: true,
567        preview: None,
568        runtime_fn: None,
569        summary: "Volatile load through a raw pointer (internal).",
570        description: "Internal lowering target for `p.read_volatile()`. Lowers to an LLVM `load volatile`, which the optimizer may not elide, duplicate, or reorder relative to other volatile accesses. Intended for memory-mapped I/O where every read has externally visible side effects.",
571        examples: &[],
572    },
573    IntrinsicDef {
574        id: IntrinsicId::PtrWriteVolatile,
575        name: "ptr_write_volatile",
576        kind: IntrinsicKind::Expr,
577        category: Category::Pointer,
578        requires_unchecked: true,
579        preview: None,
580        runtime_fn: None,
581        summary: "Volatile store through a raw mutable pointer (internal).",
582        description: "Internal lowering target for `p.write_volatile(v)`. Lowers to an LLVM `store volatile`, which the optimizer may not elide, duplicate, or reorder relative to other volatile accesses. Intended for memory-mapped I/O where every write has externally visible side effects.",
583        examples: &[],
584    },
585    IntrinsicDef {
586        id: IntrinsicId::PtrOffset,
587        name: "ptr_offset",
588        kind: IntrinsicKind::Expr,
589        category: Category::Pointer,
590        requires_unchecked: true,
591        preview: None,
592        runtime_fn: None,
593        summary: "Pointer arithmetic by element count (internal).",
594        description: "Internal lowering target for `p.offset(n)` (ADR-0063).",
595        examples: &[],
596    },
597    IntrinsicDef {
598        id: IntrinsicId::PtrToInt,
599        name: "ptr_to_int",
600        kind: IntrinsicKind::Expr,
601        category: Category::Pointer,
602        requires_unchecked: true,
603        preview: None,
604        runtime_fn: None,
605        summary: "Convert a pointer to its integer address (internal).",
606        description: "Internal lowering target for `p.to_int()` (ADR-0063).",
607        examples: &[],
608    },
609    IntrinsicDef {
610        id: IntrinsicId::IntToPtr,
611        name: "int_to_ptr",
612        kind: IntrinsicKind::Expr,
613        category: Category::Pointer,
614        requires_unchecked: true,
615        preview: None,
616        runtime_fn: None,
617        summary: "Construct a pointer from an integer address (internal).",
618        description: "Internal lowering target for `Ptr(T)::from_int(addr)` (ADR-0063).",
619        examples: &[],
620    },
621    IntrinsicDef {
622        id: IntrinsicId::NullPtr,
623        name: "null_ptr",
624        kind: IntrinsicKind::Expr,
625        category: Category::Pointer,
626        requires_unchecked: true,
627        preview: None,
628        runtime_fn: None,
629        summary: "A null pointer of the inferred type (internal).",
630        description: "Internal lowering target for `Ptr(T)::null()` (ADR-0063).",
631        examples: &[],
632    },
633    IntrinsicDef {
634        id: IntrinsicId::IsNull,
635        name: "is_null",
636        kind: IntrinsicKind::Expr,
637        category: Category::Pointer,
638        requires_unchecked: true,
639        preview: None,
640        runtime_fn: None,
641        summary: "Test whether a pointer is null (internal).",
642        description: "Internal lowering target for `p.is_null()` (ADR-0063).",
643        examples: &[],
644    },
645    IntrinsicDef {
646        id: IntrinsicId::PtrCopy,
647        name: "ptr_copy",
648        kind: IntrinsicKind::Expr,
649        category: Category::Pointer,
650        requires_unchecked: true,
651        preview: None,
652        runtime_fn: None,
653        summary: "Bulk copy between pointers (internal).",
654        description: "Internal lowering target for `dst.copy_from(src, n)` (ADR-0063).",
655        examples: &[],
656    },
657    IntrinsicDef {
658        id: IntrinsicId::Raw,
659        name: "raw",
660        kind: IntrinsicKind::Expr,
661        category: Category::Pointer,
662        requires_unchecked: true,
663        preview: None,
664        runtime_fn: None,
665        summary: "Take a const pointer to an lvalue (internal).",
666        description: "Internal lowering target for `Ptr(T)::from(&x)` (ADR-0063).",
667        examples: &[],
668    },
669    IntrinsicDef {
670        id: IntrinsicId::RawMut,
671        name: "raw_mut",
672        kind: IntrinsicKind::Expr,
673        category: Category::Pointer,
674        requires_unchecked: true,
675        preview: None,
676        runtime_fn: None,
677        summary: "Take a mutable pointer to an lvalue (internal).",
678        description: "Internal lowering target for `MutPtr(T)::from(&mut x)` (ADR-0063).",
679        examples: &[],
680    },
681    IntrinsicDef {
682        id: IntrinsicId::Syscall,
683        name: "syscall",
684        kind: IntrinsicKind::Expr,
685        category: Category::Syscall,
686        requires_unchecked: true,
687        preview: None,
688        runtime_fn: None,
689        summary: "Direct OS system call.",
690        description: "`@syscall(num, arg1, ...)` issues a raw syscall. Takes the syscall number plus up to 6 arguments; returns `i64`. Requires an `unchecked` block.",
691        examples: &["checked { let ret = @syscall(1, 1, buf, n); }"],
692    },
693    IntrinsicDef {
694        id: IntrinsicId::Range,
695        name: "range",
696        kind: IntrinsicKind::Expr,
697        category: Category::Iteration,
698        requires_unchecked: false,
699        preview: None,
700        runtime_fn: None,
701        summary: "Iterable range for `for`-loops.",
702        description: "`@range(end)`, `@range(start, end)`, or `@range(start, end, step)` produces an iterable over integers.",
703        examples: &["for i in @range(0, 10) { ... }"],
704    },
705    IntrinsicDef {
706        id: IntrinsicId::SliceLen,
707        name: "slice_len",
708        kind: IntrinsicKind::Expr,
709        category: Category::Slice,
710        requires_unchecked: false,
711        preview: None,
712        runtime_fn: None,
713        summary: "Length of a slice.",
714        description: "`@slice_len(s)` returns the number of elements in `s` (a `Slice(T)` or `MutSlice(T)`) as `usize`. Surface form: `s.len()`.",
715        examples: &[],
716    },
717    IntrinsicDef {
718        id: IntrinsicId::SliceIsEmpty,
719        name: "slice_is_empty",
720        kind: IntrinsicKind::Expr,
721        category: Category::Slice,
722        requires_unchecked: false,
723        preview: None,
724        runtime_fn: None,
725        summary: "Whether a slice has length zero.",
726        description: "`@slice_is_empty(s)` returns `s.len() == 0`. Surface form: `s.is_empty()`.",
727        examples: &[],
728    },
729    IntrinsicDef {
730        id: IntrinsicId::SliceIndexRead,
731        name: "slice_index_read",
732        kind: IntrinsicKind::Expr,
733        category: Category::Slice,
734        requires_unchecked: false,
735        preview: None,
736        runtime_fn: None,
737        summary: "Read an element from a slice with bounds checking.",
738        description: "`@slice_index_read(s, i)` returns `s[i]`. Bounds-checks at runtime; panics on out-of-range. Surface form: `s[i]`.",
739        examples: &[],
740    },
741    IntrinsicDef {
742        id: IntrinsicId::SlicePtr,
743        name: "slice_ptr",
744        kind: IntrinsicKind::Expr,
745        category: Category::Slice,
746        requires_unchecked: true,
747        preview: None,
748        runtime_fn: None,
749        summary: "Extract the data pointer from a slice.",
750        description: "`@slice_ptr(s)` returns a `Ptr(T)` to the slice's first element. Requires a `checked` block. Surface form: `s.ptr()`.",
751        examples: &[],
752    },
753    IntrinsicDef {
754        id: IntrinsicId::SlicePtrMut,
755        name: "slice_ptr_mut",
756        kind: IntrinsicKind::Expr,
757        category: Category::Slice,
758        requires_unchecked: true,
759        preview: None,
760        runtime_fn: None,
761        summary: "Extract the mutable data pointer from a mutable slice.",
762        description: "`@slice_ptr_mut(m)` returns a `MutPtr(T)` to a `MutSlice(T)`'s first element. Requires a `checked` block. Surface form: `m.ptr_mut()`.",
763        examples: &[],
764    },
765    IntrinsicDef {
766        id: IntrinsicId::PartsToSlice,
767        name: "parts_to_slice",
768        kind: IntrinsicKind::Expr,
769        category: Category::Slice,
770        requires_unchecked: true,
771        preview: None,
772        runtime_fn: None,
773        summary: "Build a slice from a raw pointer and a length.",
774        description: "`@parts_to_slice(p: Ptr(T), n: usize) -> Slice(T)` constructs a slice without checking that the underlying storage is valid. Requires a `checked` block.",
775        examples: &[],
776    },
777    IntrinsicDef {
778        id: IntrinsicId::PartsToMutSlice,
779        name: "parts_to_mut_slice",
780        kind: IntrinsicKind::Expr,
781        category: Category::Slice,
782        requires_unchecked: true,
783        preview: None,
784        runtime_fn: None,
785        summary: "Build a mutable slice from a raw mutable pointer and a length.",
786        description: "`@parts_to_mut_slice(p: MutPtr(T), n: usize) -> MutSlice(T)`. Requires a `checked` block.",
787        examples: &[],
788    },
789    IntrinsicDef {
790        id: IntrinsicId::SliceIndexWrite,
791        name: "slice_index_write",
792        kind: IntrinsicKind::Expr,
793        category: Category::Slice,
794        requires_unchecked: false,
795        preview: None,
796        runtime_fn: None,
797        summary: "Write an element to a mutable slice with bounds checking.",
798        description: "`@slice_index_write(m, i, v)` performs `m[i] = v`. Requires `MutSlice(T)`. Bounds-checks at runtime. Surface form: `m[i] = v`.",
799        examples: &[],
800    },
801    // ---- Vec construction intrinsics (ADR-0066 + ADR-0082) ----
802    // The per-method intrinsics (`vec_push`, `vec_pop`, `vec_clone`, …)
803    // retired with ADR-0082; their bodies live in `prelude/vec.gruel`.
804    // What remains is the syntactic sugar that *constructs* a Vec —
805    // `@vec(...)` literals and `@parts_to_vec` for FFI handoff.
806    IntrinsicDef {
807        id: IntrinsicId::VecLiteral,
808        name: "vec",
809        kind: IntrinsicKind::Expr,
810        category: Category::Vec,
811        requires_unchecked: false,
812        preview: None,
813        runtime_fn: None,
814        summary: "Construct a Vec from individual elements.",
815        description: "`@vec(a, b, c)` returns a `Vec(T)` of length 3 with the given elements. Mirrors Rust's `vec![…]`. Requires at least one argument; element types unify to a single `T`.",
816        examples: &["@vec(1, 2, 3)"],
817    },
818    IntrinsicDef {
819        id: IntrinsicId::VecRepeat,
820        name: "vec_repeat",
821        kind: IntrinsicKind::Expr,
822        category: Category::Vec,
823        requires_unchecked: false,
824        preview: None,
825        runtime_fn: None,
826        summary: "Construct a Vec with N copies of a value.",
827        description: "`@vec_repeat(v, n)` returns a `Vec(T)` of length `n` where every slot holds a clone of `v`. Requires `T: Clone`.",
828        examples: &["@vec_repeat(0, 100)"],
829    },
830    IntrinsicDef {
831        id: IntrinsicId::PartsToVec,
832        name: "parts_to_vec",
833        kind: IntrinsicKind::Expr,
834        category: Category::Vec,
835        requires_unchecked: true,
836        preview: None,
837        runtime_fn: None,
838        summary: "Build a Vec from raw parts.",
839        description: "`@parts_to_vec(p: MutPtr(T), len: usize, cap: usize) -> Vec(T)` takes ownership of `p`. Requires a `checked` block.",
840        examples: &[],
841    },
842    IntrinsicDef {
843        id: IntrinsicId::TestPreviewGate,
844        name: "test_preview_gate",
845        kind: IntrinsicKind::Expr,
846        category: Category::Meta,
847        requires_unchecked: false,
848        preview: Some(PreviewFeature::TestInfra),
849        runtime_fn: None,
850        summary: "Test hook for the preview-feature gate.",
851        description: "`@test_preview_gate()` exists solely to verify that the preview-feature gating mechanism works. Always gated behind `--preview test_infra`.",
852        examples: &[],
853    },
854    IntrinsicDef {
855        id: IntrinsicId::Spawn,
856        name: "spawn",
857        kind: IntrinsicKind::Expr,
858        category: Category::Comptime,
859        requires_unchecked: false,
860        preview: None,
861        runtime_fn: Some("__gruel_thread_spawn"),
862        summary: "Spawn a worker thread running `fn(arg) -> R`.",
863        description: "`@spawn(fn, arg) -> JoinHandle(R)` runs `fn` on a new thread with `arg` and yields a linear `JoinHandle(R)` consumed via `join(self) -> R`. The function and argument types are checked: arity must be one, the argument must be `≥ Send` and not Linear or a reference, and the return type must be `≥ Send`. Multi-argument workers wrap their inputs in a tuple/struct on the caller's side. ADR-0084.",
864        examples: &[
865            "let h = @spawn(worker, Job { id: 1 });",
866            "let report = h.join();",
867        ],
868    },
869    IntrinsicDef {
870        id: IntrinsicId::ThreadJoin,
871        name: "thread_join",
872        kind: IntrinsicKind::Expr,
873        category: Category::Comptime,
874        requires_unchecked: true,
875        preview: None,
876        runtime_fn: Some("__gruel_thread_join"),
877        summary: "Internal lowering target for JoinHandle::join (ADR-0084).",
878        description: "`@thread_join(h: MutPtr(u8)) -> R` is the codegen-level wrapper around `__gruel_thread_join`. Called only from the prelude `JoinHandle::join` body inside a `checked` block; user code reaches the runtime through the prelude method. Result type comes from the surrounding context.",
879        examples: &[],
880    },
881    IntrinsicDef {
882        id: IntrinsicId::Uninit,
883        name: "uninit",
884        kind: IntrinsicKind::Type,
885        category: Category::Comptime,
886        requires_unchecked: false,
887        preview: None,
888        runtime_fn: None,
889        summary: "Allocate a partially-initialized value of a given type (ADR-0079).",
890        description: "`@uninit(T) -> Uninit(T)` returns a handle to T-sized storage. The compiler does not run drop on the slot until `@finalize` consumes it. Sema tracks per-field initialization through CFG analysis; reads, returns, or escapes of the handle are blocked until every field has been written via `@field(handle, name) = expr`.",
891        examples: &[
892            "let mut h = @uninit(Point);",
893            "@field(h, \"x\") = 1;",
894            "@field(h, \"y\") = 2;",
895            "let p: Point = @finalize(h);",
896        ],
897    },
898    IntrinsicDef {
899        id: IntrinsicId::Finalize,
900        name: "finalize",
901        kind: IntrinsicKind::Expr,
902        category: Category::Comptime,
903        requires_unchecked: false,
904        preview: None,
905        runtime_fn: None,
906        summary: "Consume an `Uninit(T)` handle and return a real `T` (ADR-0079).",
907        description: "`@finalize(handle: Uninit(T)) -> T` takes an `Uninit(T)` handle whose every field has been written and produces a fully-initialized `T`. From this point on, the value drops normally. Compile error if any field is uninitialized along any path that reaches the call.",
908        examples: &["@finalize(h)"],
909    },
910    IntrinsicDef {
911        id: IntrinsicId::FieldSet,
912        name: "field_set",
913        kind: IntrinsicKind::Expr,
914        category: Category::Comptime,
915        requires_unchecked: false,
916        preview: None,
917        runtime_fn: None,
918        summary: "Write a field of an in-progress `@uninit`/`@variant_uninit` handle (ADR-0079).",
919        description: "`@field_set(handle, name, value)` writes the named field of an in-progress construction handle. Used inside derive bodies to populate the result one field at a time; sema records each write and `@finalize` verifies all fields are present.",
920        examples: &["@field_set(out, f.name, @field(self, f.name).clone())"],
921    },
922    IntrinsicDef {
923        id: IntrinsicId::VariantUninit,
924        name: "variant_uninit",
925        kind: IntrinsicKind::Expr,
926        category: Category::Comptime,
927        requires_unchecked: false,
928        preview: None,
929        runtime_fn: None,
930        summary: "Allocate an `Uninit(Self)` pre-tagged for a specific enum variant (ADR-0079).",
931        description: "`@variant_uninit(Self, comptime tag) -> Uninit(Self)` is the variant-shaped counterpart to `@uninit(T)`. Subsequent `@field(out, name) = expr` writes target the named variant's payload fields; `@finalize(out)` produces a `Self` of the correct variant. `tag` must be comptime-known.",
932        examples: &[
933            "let mut out = @variant_uninit(Self, v.tag);",
934            "@field(out, f.name) = ...;",
935            "let result: Self = @finalize(out);",
936        ],
937    },
938    IntrinsicDef {
939        id: IntrinsicId::VariantField,
940        name: "variant_field",
941        kind: IntrinsicKind::Expr,
942        category: Category::Comptime,
943        requires_unchecked: false,
944        preview: None,
945        runtime_fn: None,
946        summary: "Read a payload field of a known enum variant (ADR-0079).",
947        description: "`@variant_field(self, comptime tag, name)` reads the named payload field of variant `tag` from `self`. Only valid when `self`'s runtime variant is known to be `tag` — typically inside an arm dispatched by `comptime_unroll for v in @type_info(Self).variants`. `tag` and `name` are both comptime.",
948        examples: &["@variant_field(self, v.tag, f.name)"],
949    },
950    IntrinsicDef {
951        id: IntrinsicId::CStrToVec,
952        name: "cstr_to_vec",
953        kind: IntrinsicKind::Expr,
954        category: Category::Meta,
955        requires_unchecked: true,
956        // Implementation-detail intrinsic invoked by the prelude
957        // `String::from_c_str` body. The user-visible gate is on
958        // `String::from_c_str` itself (ADR-0072).
959        preview: None,
960        runtime_fn: Some("__gruel_cstr_to_vec"),
961        summary: "Copy a NUL-terminated C string into a fresh Vec(u8).",
962        description: "`@cstr_to_vec(p: Ptr(u8)) -> Vec(u8)` runs `strlen(p)`, allocates `cap >= len` bytes, and copies. Used by `String::from_c_str` (ADR-0072).",
963        examples: &["@cstr_to_vec(p)"],
964    },
965    // ADR-0087 Phase 3: `@utf8_validate` retired — replaced by the
966    // prelude `utf8_validate(s: Slice(u8)) -> bool` fn. Runtime helper
967    // `__gruel_utf8_validate` stays.
968    // ADR-0087 Phase 4: `@alloc` / `@realloc` / `@free` retired —
969    // replaced by the prelude `mem_alloc` / `mem_realloc` /
970    // `mem_free` fns. The runtime symbols (`__gruel_alloc` etc.)
971    // are also gone; the prelude wrappers call libc `malloc` /
972    // `realloc` / `free` directly via Phase 1's `link_extern("c")`
973    // block.
974    IntrinsicDef {
975        id: IntrinsicId::PtrCast,
976        name: "ptr_cast",
977        kind: IntrinsicKind::Expr,
978        category: Category::Pointer,
979        requires_unchecked: true,
980        preview: None,
981        runtime_fn: None,
982        summary: "Reinterpret a pointer as another pointer type (ADR-0082).",
983        description: "`@ptr_cast(p) -> MutPtr(T)` / `Ptr(T)` reinterprets the raw pointer `p` as a pointer of the target type, where the target is inferred from the binding context (HM inference, like `@cast`). Both source and target must be `MutPtr(_)` / `Ptr(_)`. The cast is a no-op at the LLVM level (pointers are opaque); only the Gruel-side type tracking changes. Requires a `checked` block.",
984        examples: &["let p: MutPtr(T) = checked { @ptr_cast(p_u8) };"],
985    },
986    // ADR-0087 Phase 3: `@bytes_eq` retired — replaced by the prelude
987    // `bytes_eq(a, b, n) -> bool` fn (wraps libc `memcmp`).
988];
989
990// ============================================================================
991// Pointer-method registry (ADR-0063)
992// ============================================================================
993
994/// Which builtin pointer constructor an entry is defined on.
995#[derive(Debug, Clone, Copy, PartialEq, Eq)]
996pub enum PointerKind {
997    /// Defined on `Ptr(T)`.
998    Ptr,
999    /// Defined on `MutPtr(T)`.
1000    MutPtr,
1001}
1002
1003/// Whether an entry is an instance method or an associated function.
1004#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1005pub enum PointerOpForm {
1006    /// Instance method called on a pointer value: `p.name(args)`.
1007    Method,
1008    /// Associated function called on the type: `Ptr(T)::name(args)`.
1009    AssocFn,
1010}
1011
1012/// One method or associated function on `Ptr(T)` / `MutPtr(T)` (ADR-0063).
1013///
1014/// Each entry is a pure metadata record describing the surface form. The
1015/// actual semantic / codegen behaviour is reused from the intrinsic
1016/// identified by [`PointerMethod::intrinsic`] — this registry exists only to
1017/// give sema the surface-to-intrinsic mapping. No new runtime functions.
1018///
1019/// `intrinsic_name` mirrors what the equivalent legacy `@…` form was called
1020/// (e.g. `"ptr_read"` for `IntrinsicId::PtrRead`). The codegen path
1021/// dispatches `AirInstData::Intrinsic` by name, so emitting the new surface
1022/// form lowers to the same string the old `@ptr_read` would have.
1023#[derive(Debug, Clone, Copy)]
1024pub struct PointerMethod {
1025    /// Constructor this method/fn is defined on.
1026    pub kind: PointerKind,
1027    /// Name as written by the user (after `.` for methods, after `::` for
1028    /// associated fns).
1029    pub name: &'static str,
1030    /// Method (`p.name(...)`) or associated fn (`Type(T)::name(...)`).
1031    pub form: PointerOpForm,
1032    /// Stable identity used by codegen / IR analyzers.
1033    pub intrinsic: IntrinsicId,
1034    /// Symbol the AIR `Intrinsic` instruction is tagged with.
1035    pub intrinsic_name: &'static str,
1036    /// ADR-0088: whether this op is unchecked. After Phase 3c the
1037    /// table reflects the body-side principle: only ops whose body
1038    /// dereferences a caller-supplied pointer or does
1039    /// provenance-sensitive arithmetic on one are unchecked
1040    /// (`read`, `read_volatile`, `write`, `write_volatile`, `offset`,
1041    /// `copy_from`). The opaque-token ops (`is_null`, `to_int`) and
1042    /// the constructors that don't themselves dereference (`from`,
1043    /// `null`, `from_int`) drop their gate — the deref-time gate at
1044    /// the eventual `read` / `write` call site handles the hazard.
1045    pub is_unchecked: bool,
1046}
1047
1048/// Closed registry of every pointer method / associated function (ADR-0063).
1049///
1050/// Sema's method-call path consults this when the receiver type is
1051/// `Ptr(_)` / `MutPtr(_)`; the path-call path consults it when the LHS
1052/// resolves to such a type.
1053pub const POINTER_METHODS: &[PointerMethod] = &[
1054    // ---- Methods on Ptr(T) ----
1055    PointerMethod {
1056        kind: PointerKind::Ptr,
1057        name: "read",
1058        form: PointerOpForm::Method,
1059        intrinsic: IntrinsicId::PtrRead,
1060        intrinsic_name: "ptr_read",
1061        is_unchecked: true,
1062    },
1063    PointerMethod {
1064        kind: PointerKind::Ptr,
1065        name: "read_volatile",
1066        form: PointerOpForm::Method,
1067        intrinsic: IntrinsicId::PtrReadVolatile,
1068        intrinsic_name: "ptr_read_volatile",
1069        is_unchecked: true,
1070    },
1071    PointerMethod {
1072        kind: PointerKind::Ptr,
1073        name: "offset",
1074        form: PointerOpForm::Method,
1075        intrinsic: IntrinsicId::PtrOffset,
1076        intrinsic_name: "ptr_offset",
1077        is_unchecked: true,
1078    },
1079    PointerMethod {
1080        kind: PointerKind::Ptr,
1081        name: "is_null",
1082        form: PointerOpForm::Method,
1083        intrinsic: IntrinsicId::IsNull,
1084        intrinsic_name: "is_null",
1085        is_unchecked: false,
1086    },
1087    PointerMethod {
1088        kind: PointerKind::Ptr,
1089        name: "to_int",
1090        form: PointerOpForm::Method,
1091        intrinsic: IntrinsicId::PtrToInt,
1092        intrinsic_name: "ptr_to_int",
1093        is_unchecked: false,
1094    },
1095    // ---- Associated fns on Ptr(T) ----
1096    PointerMethod {
1097        kind: PointerKind::Ptr,
1098        name: "from",
1099        form: PointerOpForm::AssocFn,
1100        intrinsic: IntrinsicId::Raw,
1101        intrinsic_name: "raw",
1102        is_unchecked: false,
1103    },
1104    PointerMethod {
1105        kind: PointerKind::Ptr,
1106        name: "null",
1107        form: PointerOpForm::AssocFn,
1108        intrinsic: IntrinsicId::NullPtr,
1109        intrinsic_name: "null_ptr",
1110        is_unchecked: false,
1111    },
1112    PointerMethod {
1113        kind: PointerKind::Ptr,
1114        name: "from_int",
1115        form: PointerOpForm::AssocFn,
1116        intrinsic: IntrinsicId::IntToPtr,
1117        intrinsic_name: "int_to_ptr",
1118        is_unchecked: false,
1119    },
1120    // ---- Methods on MutPtr(T) ----
1121    PointerMethod {
1122        kind: PointerKind::MutPtr,
1123        name: "read",
1124        form: PointerOpForm::Method,
1125        intrinsic: IntrinsicId::PtrRead,
1126        intrinsic_name: "ptr_read",
1127        is_unchecked: true,
1128    },
1129    PointerMethod {
1130        kind: PointerKind::MutPtr,
1131        name: "read_volatile",
1132        form: PointerOpForm::Method,
1133        intrinsic: IntrinsicId::PtrReadVolatile,
1134        intrinsic_name: "ptr_read_volatile",
1135        is_unchecked: true,
1136    },
1137    PointerMethod {
1138        kind: PointerKind::MutPtr,
1139        name: "write",
1140        form: PointerOpForm::Method,
1141        intrinsic: IntrinsicId::PtrWrite,
1142        intrinsic_name: "ptr_write",
1143        is_unchecked: true,
1144    },
1145    PointerMethod {
1146        kind: PointerKind::MutPtr,
1147        name: "write_volatile",
1148        form: PointerOpForm::Method,
1149        intrinsic: IntrinsicId::PtrWriteVolatile,
1150        intrinsic_name: "ptr_write_volatile",
1151        is_unchecked: true,
1152    },
1153    PointerMethod {
1154        kind: PointerKind::MutPtr,
1155        name: "offset",
1156        form: PointerOpForm::Method,
1157        intrinsic: IntrinsicId::PtrOffset,
1158        intrinsic_name: "ptr_offset",
1159        is_unchecked: true,
1160    },
1161    PointerMethod {
1162        kind: PointerKind::MutPtr,
1163        name: "is_null",
1164        form: PointerOpForm::Method,
1165        intrinsic: IntrinsicId::IsNull,
1166        intrinsic_name: "is_null",
1167        is_unchecked: false,
1168    },
1169    PointerMethod {
1170        kind: PointerKind::MutPtr,
1171        name: "to_int",
1172        form: PointerOpForm::Method,
1173        intrinsic: IntrinsicId::PtrToInt,
1174        intrinsic_name: "ptr_to_int",
1175        is_unchecked: false,
1176    },
1177    PointerMethod {
1178        kind: PointerKind::MutPtr,
1179        name: "copy_from",
1180        form: PointerOpForm::Method,
1181        intrinsic: IntrinsicId::PtrCopy,
1182        intrinsic_name: "ptr_copy",
1183        is_unchecked: true,
1184    },
1185    // ---- Associated fns on MutPtr(T) ----
1186    PointerMethod {
1187        kind: PointerKind::MutPtr,
1188        name: "from",
1189        form: PointerOpForm::AssocFn,
1190        intrinsic: IntrinsicId::RawMut,
1191        intrinsic_name: "raw_mut",
1192        is_unchecked: false,
1193    },
1194    PointerMethod {
1195        kind: PointerKind::MutPtr,
1196        name: "null",
1197        form: PointerOpForm::AssocFn,
1198        intrinsic: IntrinsicId::NullPtr,
1199        intrinsic_name: "null_ptr",
1200        is_unchecked: false,
1201    },
1202    PointerMethod {
1203        kind: PointerKind::MutPtr,
1204        name: "from_int",
1205        form: PointerOpForm::AssocFn,
1206        intrinsic: IntrinsicId::IntToPtr,
1207        intrinsic_name: "int_to_ptr",
1208        is_unchecked: false,
1209    },
1210];
1211
1212/// Look up a pointer method/assoc fn by `(kind, name, form)`.
1213pub fn lookup_pointer_method(
1214    kind: PointerKind,
1215    name: &str,
1216    form: PointerOpForm,
1217) -> Option<&'static PointerMethod> {
1218    POINTER_METHODS
1219        .iter()
1220        .find(|m| m.kind == kind && m.form == form && m.name == name)
1221}
1222
1223// ============================================================================
1224// Slice-method registry (ADR-0064)
1225// ============================================================================
1226
1227/// Which builtin slice constructor an entry is defined on.
1228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1229pub enum SliceKind {
1230    /// Defined on `Slice(T)`.
1231    Slice,
1232    /// Defined on `MutSlice(T)`.
1233    MutSlice,
1234}
1235
1236/// One method on `Slice(T)` / `MutSlice(T)` (ADR-0064).
1237///
1238/// Mirrors [`PointerMethod`]: each entry maps a surface name to an
1239/// [`IntrinsicId`] that owns the actual semantic / codegen behaviour.
1240#[derive(Debug, Clone, Copy)]
1241pub struct SliceMethod {
1242    /// Constructor this method is defined on.
1243    pub kind: SliceKind,
1244    /// Method name as written by the user (after `.`).
1245    pub name: &'static str,
1246    /// Stable identity used by codegen / IR analyzers.
1247    pub intrinsic: IntrinsicId,
1248    /// Symbol the AIR `Intrinsic` instruction is tagged with.
1249    pub intrinsic_name: &'static str,
1250    /// Whether the lowering requires a `checked` block.
1251    pub requires_checked: bool,
1252}
1253
1254/// Closed registry of every slice method (ADR-0064).
1255pub const SLICE_METHODS: &[SliceMethod] = &[
1256    // ---- Methods on Slice(T) ----
1257    SliceMethod {
1258        kind: SliceKind::Slice,
1259        name: "len",
1260        intrinsic: IntrinsicId::SliceLen,
1261        intrinsic_name: "slice_len",
1262        requires_checked: false,
1263    },
1264    SliceMethod {
1265        kind: SliceKind::Slice,
1266        name: "is_empty",
1267        intrinsic: IntrinsicId::SliceIsEmpty,
1268        intrinsic_name: "slice_is_empty",
1269        requires_checked: false,
1270    },
1271    SliceMethod {
1272        kind: SliceKind::Slice,
1273        name: "ptr",
1274        intrinsic: IntrinsicId::SlicePtr,
1275        intrinsic_name: "slice_ptr",
1276        requires_checked: true,
1277    },
1278    // ---- Methods on MutSlice(T) ----
1279    SliceMethod {
1280        kind: SliceKind::MutSlice,
1281        name: "len",
1282        intrinsic: IntrinsicId::SliceLen,
1283        intrinsic_name: "slice_len",
1284        requires_checked: false,
1285    },
1286    SliceMethod {
1287        kind: SliceKind::MutSlice,
1288        name: "is_empty",
1289        intrinsic: IntrinsicId::SliceIsEmpty,
1290        intrinsic_name: "slice_is_empty",
1291        requires_checked: false,
1292    },
1293    SliceMethod {
1294        kind: SliceKind::MutSlice,
1295        name: "ptr",
1296        intrinsic: IntrinsicId::SlicePtr,
1297        intrinsic_name: "slice_ptr",
1298        requires_checked: true,
1299    },
1300    SliceMethod {
1301        kind: SliceKind::MutSlice,
1302        name: "ptr_mut",
1303        intrinsic: IntrinsicId::SlicePtrMut,
1304        intrinsic_name: "slice_ptr_mut",
1305        requires_checked: true,
1306    },
1307];
1308
1309/// Look up a slice method by `(kind, name)`.
1310pub fn lookup_slice_method(kind: SliceKind, name: &str) -> Option<&'static SliceMethod> {
1311    SLICE_METHODS
1312        .iter()
1313        .find(|m| m.kind == kind && m.name == name)
1314}
1315
1316// ============================================================================
1317// Queries
1318// ============================================================================
1319
1320/// Look up an intrinsic by its source-level name (without the leading `@`).
1321pub fn lookup_by_name(name: &str) -> Option<&'static IntrinsicDef> {
1322    INTRINSICS.iter().find(|d| d.name == name)
1323}
1324
1325/// Look up an intrinsic by its stable [`IntrinsicId`].
1326pub fn lookup_by_id(id: IntrinsicId) -> &'static IntrinsicDef {
1327    INTRINSICS
1328        .iter()
1329        .find(|d| d.id == id)
1330        .expect("every IntrinsicId must have exactly one INTRINSICS entry (checked by tests)")
1331}
1332
1333/// Iterate over every registered intrinsic.
1334pub fn iter() -> impl Iterator<Item = &'static IntrinsicDef> {
1335    INTRINSICS.iter()
1336}
1337
1338/// Iterate over intrinsics in a single category.
1339pub fn by_category(cat: Category) -> impl Iterator<Item = &'static IntrinsicDef> {
1340    INTRINSICS.iter().filter(move |d| d.category == cat)
1341}
1342
1343/// Is this name a type intrinsic (takes a type arg, as `@size_of(T)` does)?
1344pub fn is_type_intrinsic(name: &str) -> bool {
1345    lookup_by_name(name).is_some_and(|d| d.kind == IntrinsicKind::Type)
1346}
1347
1348// ============================================================================
1349// Documentation export
1350// ============================================================================
1351
1352/// Render the full intrinsics reference page as markdown.
1353///
1354/// The output is a self-contained page with a quick-reference table,
1355/// followed by per-category detail sections listing each intrinsic's name,
1356/// summary, description, runtime binding (if any), preview gate (if any),
1357/// unchecked requirement (if any), and examples.
1358///
1359/// This function is the source of truth for the checked-in reference page;
1360/// `make check-intrinsic-docs` runs it and fails CI if the committed file
1361/// differs from the generated output.
1362pub fn render_reference_markdown() -> String {
1363    let mut out = String::new();
1364    out.push_str("<!-- AUTO-GENERATED by `cargo run -p gruel-intrinsics-docs`. Do not edit by hand; edit the IntrinsicDef entries in `crates/gruel-intrinsics/src/lib.rs` and regenerate. -->\n\n");
1365    out.push_str("# Intrinsics Reference\n\n");
1366    out.push_str("This page documents every `@intrinsic` the Gruel compiler recognizes. It is generated from the [`gruel-intrinsics`] registry (see [ADR-0050](../designs/0050-intrinsics-crate.md)); any changes must be made in Rust, not here.\n\n");
1367
1368    // ---- Quick reference table ----
1369    // Sort by the canonical category render order (stable on registration
1370    // order within a category) so rows for the same category cluster
1371    // together and the table reads like a table of contents for the detail
1372    // sections below.
1373    out.push_str("## Quick Reference\n\n");
1374    out.push_str("| Intrinsic | Kind | Category | Preview | Unchecked | Summary |\n");
1375    out.push_str("|---|---|---|---|---|---|\n");
1376    let mut ordered: Vec<&IntrinsicDef> = INTRINSICS.iter().collect();
1377    ordered.sort_by_key(|d| d.category.render_index());
1378    for d in ordered {
1379        let kind = match d.kind {
1380            IntrinsicKind::Expr => "expr",
1381            IntrinsicKind::Type => "type",
1382            IntrinsicKind::TypeInterface => "type+iface",
1383        };
1384        let preview = match d.preview {
1385            Some(f) => f.name(),
1386            None => "—",
1387        };
1388        let unchecked = if d.requires_unchecked { "yes" } else { "—" };
1389        out.push_str(&format!(
1390            "| `@{}` | {} | {} | {} | {} | {} |\n",
1391            d.name,
1392            kind,
1393            d.category.heading(),
1394            preview,
1395            unchecked,
1396            d.summary,
1397        ));
1398    }
1399    out.push('\n');
1400
1401    // ---- Per-category detail sections ----
1402    for &cat in Category::RENDER_ORDER {
1403        let mut entries = by_category(cat).peekable();
1404        if entries.peek().is_none() {
1405            continue;
1406        }
1407        out.push_str(&format!("## {}\n\n", cat.heading()));
1408        for d in entries {
1409            out.push_str(&format!("### `@{}`\n\n", d.name));
1410            out.push_str(&format!("{}\n\n", d.description));
1411            if let Some(rt) = d.runtime_fn {
1412                out.push_str(&format!("- **Runtime symbol:** `{rt}`\n"));
1413            }
1414            if let Some(feature) = d.preview {
1415                out.push_str(&format!(
1416                    "- **Preview gate:** `--preview {}` ({})\n",
1417                    feature.name(),
1418                    feature.adr()
1419                ));
1420            }
1421            if d.requires_unchecked {
1422                out.push_str("- **Requires:** `checked { ... }` block\n");
1423            }
1424            if !d.examples.is_empty() {
1425                out.push_str("\n**Examples:**\n\n");
1426                for ex in d.examples {
1427                    out.push_str("```gruel\n");
1428                    out.push_str(ex);
1429                    if !ex.ends_with('\n') {
1430                        out.push('\n');
1431                    }
1432                    out.push_str("```\n\n");
1433                }
1434            } else {
1435                out.push('\n');
1436            }
1437        }
1438    }
1439    out
1440}
1441
1442// ============================================================================
1443// Tests
1444// ============================================================================
1445
1446#[cfg(test)]
1447mod tests {
1448    use super::*;
1449    use rustc_hash::FxHashSet as HashSet;
1450
1451    #[test]
1452    fn no_duplicate_names() {
1453        let mut seen = HashSet::default();
1454        for d in INTRINSICS {
1455            assert!(seen.insert(d.name), "duplicate intrinsic name: {}", d.name);
1456        }
1457    }
1458
1459    #[test]
1460    fn no_duplicate_ids() {
1461        let mut seen = HashSet::default();
1462        for d in INTRINSICS {
1463            assert!(seen.insert(d.id), "duplicate IntrinsicId: {:?}", d.id);
1464        }
1465    }
1466
1467    #[test]
1468    fn every_id_variant_covered() {
1469        // Exhaustive match ensures adding a new IntrinsicId variant without an
1470        // INTRINSICS entry fails to compile.
1471        for d in INTRINSICS {
1472            match d.id {
1473                IntrinsicId::Dbg
1474                | IntrinsicId::Panic
1475                | IntrinsicId::Assert
1476                | IntrinsicId::CompileError
1477                | IntrinsicId::Cast
1478                | IntrinsicId::SizeOf
1479                | IntrinsicId::AlignOf
1480                | IntrinsicId::TypeName
1481                | IntrinsicId::TypeInfo
1482                | IntrinsicId::Ownership
1483                | IntrinsicId::ThreadSafety
1484                | IntrinsicId::Implements
1485                | IntrinsicId::Field
1486                | IntrinsicId::Import
1487                | IntrinsicId::EmbedFile
1488                | IntrinsicId::TargetArch
1489                | IntrinsicId::TargetOs
1490                | IntrinsicId::PtrRead
1491                | IntrinsicId::PtrWrite
1492                | IntrinsicId::PtrReadVolatile
1493                | IntrinsicId::PtrWriteVolatile
1494                | IntrinsicId::PtrOffset
1495                | IntrinsicId::PtrToInt
1496                | IntrinsicId::IntToPtr
1497                | IntrinsicId::NullPtr
1498                | IntrinsicId::IsNull
1499                | IntrinsicId::PtrCopy
1500                | IntrinsicId::Raw
1501                | IntrinsicId::RawMut
1502                | IntrinsicId::Syscall
1503                | IntrinsicId::Range
1504                | IntrinsicId::SliceLen
1505                | IntrinsicId::SliceIsEmpty
1506                | IntrinsicId::SliceIndexRead
1507                | IntrinsicId::SliceIndexWrite
1508                | IntrinsicId::SlicePtr
1509                | IntrinsicId::SlicePtrMut
1510                | IntrinsicId::PartsToSlice
1511                | IntrinsicId::PartsToMutSlice
1512                | IntrinsicId::VecLiteral
1513                | IntrinsicId::VecRepeat
1514                | IntrinsicId::PartsToVec
1515                | IntrinsicId::TestPreviewGate
1516                | IntrinsicId::Spawn
1517                | IntrinsicId::CStrToVec
1518                | IntrinsicId::Uninit
1519                | IntrinsicId::Finalize
1520                | IntrinsicId::FieldSet
1521                | IntrinsicId::VariantUninit
1522                | IntrinsicId::VariantField
1523                | IntrinsicId::PtrCast
1524                | IntrinsicId::ThreadJoin => {}
1525            }
1526        }
1527    }
1528
1529    #[test]
1530    fn lookup_by_name_roundtrip() {
1531        for d in INTRINSICS {
1532            let found = lookup_by_name(d.name).expect("name must resolve");
1533            assert_eq!(found.id, d.id);
1534        }
1535        assert!(lookup_by_name("definitely_not_an_intrinsic").is_none());
1536    }
1537
1538    #[test]
1539    fn lookup_by_id_roundtrip() {
1540        for d in INTRINSICS {
1541            assert_eq!(lookup_by_id(d.id).name, d.name);
1542        }
1543    }
1544
1545    #[test]
1546    fn type_intrinsics_match_legacy_list() {
1547        // The legacy TYPE_INTRINSICS constant in gruel-rir/astgen.rs lists:
1548        // size_of, align_of, type_name, type_info, ownership.
1549        // ADR-0079 Phase 2b adds @uninit(T) as a type intrinsic.
1550        // The registry must match exactly.
1551        let from_registry: HashSet<&'static str> = INTRINSICS
1552            .iter()
1553            .filter(|d| d.kind == IntrinsicKind::Type)
1554            .map(|d| d.name)
1555            .collect();
1556        let expected: HashSet<&'static str> = [
1557            "size_of",
1558            "align_of",
1559            "type_name",
1560            "type_info",
1561            "ownership",
1562            "thread_safety",
1563            "uninit",
1564        ]
1565        .into_iter()
1566        .collect();
1567        assert_eq!(from_registry, expected);
1568    }
1569
1570    #[test]
1571    fn unchecked_intrinsics_match_legacy_set() {
1572        // Intrinsics that current sema calls `require_checked_for_intrinsic` on.
1573        let from_registry: HashSet<&'static str> = INTRINSICS
1574            .iter()
1575            .filter(|d| d.requires_unchecked)
1576            .map(|d| d.name)
1577            .collect();
1578        let expected: HashSet<&'static str> = [
1579            "ptr_read",
1580            "ptr_write",
1581            "ptr_read_volatile",
1582            "ptr_write_volatile",
1583            "ptr_offset",
1584            "ptr_to_int",
1585            "int_to_ptr",
1586            "null_ptr",
1587            "is_null",
1588            "ptr_copy",
1589            "raw",
1590            "raw_mut",
1591            "syscall",
1592            "slice_ptr",
1593            "slice_ptr_mut",
1594            "parts_to_slice",
1595            "parts_to_mut_slice",
1596            "parts_to_vec",
1597            // ADR-0072
1598            "cstr_to_vec",
1599            // ADR-0082: memory intrinsics gated to checked blocks. The
1600            // alloc/realloc/free/bytes_eq names retired in ADR-0087
1601            // (migrated into prelude wrappers); only `ptr_cast` remains.
1602            "ptr_cast",
1603            // ADR-0084: prelude-internal join wrapper.
1604            "thread_join",
1605        ]
1606        .into_iter()
1607        .collect();
1608        assert_eq!(from_registry, expected);
1609    }
1610
1611    #[test]
1612    fn by_category_filters() {
1613        let ptrs: Vec<_> = by_category(Category::Pointer).collect();
1614        assert!(!ptrs.is_empty());
1615        assert!(ptrs.iter().all(|d| d.category == Category::Pointer));
1616    }
1617
1618    /// Every `Category` variant must appear exactly once in `RENDER_ORDER`,
1619    /// so the documentation renderer never silently drops a section. Adding
1620    /// a new variant without updating `RENDER_ORDER` makes this test fail.
1621    #[test]
1622    fn category_render_order_is_exhaustive() {
1623        // Enumerate every Category variant via an exhaustive match. The
1624        // compiler will force this to be updated when a new variant is
1625        // added; the assertion then forces RENDER_ORDER to be updated too.
1626        let all = [
1627            Category::Debug,
1628            Category::Cast,
1629            Category::Io,
1630            Category::Parse,
1631            Category::Random,
1632            Category::Comptime,
1633            Category::Platform,
1634            Category::Pointer,
1635            Category::Syscall,
1636            Category::Iteration,
1637            Category::Slice,
1638            Category::Vec,
1639            Category::Meta,
1640        ];
1641        // Exhaustiveness witness: any new variant added to `Category`
1642        // forces this match to be updated.
1643        for c in all {
1644            let _: &'static str = match c {
1645                Category::Debug => "Debug",
1646                Category::Cast => "Cast",
1647                Category::Io => "Io",
1648                Category::Parse => "Parse",
1649                Category::Random => "Random",
1650                Category::Comptime => "Comptime",
1651                Category::Platform => "Platform",
1652                Category::Pointer => "Pointer",
1653                Category::Syscall => "Syscall",
1654                Category::Iteration => "Iteration",
1655                Category::Slice => "Slice",
1656                Category::Vec => "Vec",
1657                Category::Meta => "Meta",
1658            };
1659            assert!(
1660                Category::RENDER_ORDER.contains(&c),
1661                "Category::{:?} is missing from Category::RENDER_ORDER \
1662                 — add it to keep the intrinsics reference page complete",
1663                c
1664            );
1665        }
1666        assert_eq!(
1667            Category::RENDER_ORDER.len(),
1668            all.len(),
1669            "Category::RENDER_ORDER has duplicates or is out of sync with Category"
1670        );
1671    }
1672
1673    #[test]
1674    fn is_type_intrinsic_basic() {
1675        assert!(is_type_intrinsic("size_of"));
1676        assert!(is_type_intrinsic("type_name"));
1677        assert!(!is_type_intrinsic("dbg"));
1678        assert!(!is_type_intrinsic("nonexistent"));
1679    }
1680
1681    #[test]
1682    fn all_names_are_valid_identifiers() {
1683        for d in INTRINSICS {
1684            assert!(!d.name.is_empty());
1685            assert!(
1686                d.name
1687                    .chars()
1688                    .all(|c| c.is_ascii_alphanumeric() || c == '_'),
1689                "intrinsic name {:?} has unexpected characters",
1690                d.name
1691            );
1692        }
1693    }
1694
1695    #[test]
1696    fn preview_gated_intrinsics_are_known_features() {
1697        // All preview gates reference the PreviewFeature enum, so the compiler
1698        // already enforces this at type-check time. This test just documents
1699        // that `test_preview_gate` currently carries a gate.
1700        let gated: Vec<_> = INTRINSICS.iter().filter(|d| d.preview.is_some()).collect();
1701        assert!(
1702            gated.iter().any(|d| d.name == "test_preview_gate"),
1703            "test_preview_gate must be preview-gated"
1704        );
1705    }
1706}