Skip to main content

nova_resolver/
lib.rs

1//! Name resolution for the Nova compiler.
2//!
3//! Phase 1 scope: a single-file module. The resolver walks the top-level
4//! items of a parsed [`nova_ast::File`] and produces a [`Definitions`] table
5//! mapping every top-level name (functions, sum types, sum-type variants,
6//! constants) to a stable [`DefId`]. Duplicate definitions are reported as
7//! `E0002` diagnostics.
8//!
9//! Local (block-scoped) variable resolution happens during type checking,
10//! where scoping interacts with inference; this crate owns the *item-level*
11//! namespace and the shared [`DefId`] / [`Res`] vocabulary used by the rest
12//! of the pipeline.
13
14use indexmap::IndexMap;
15use nova_ast::item::{Attribute, ExternItem, Import, ImportKind, TypeDef};
16use nova_ast::{File, Function, Item, Type};
17use nova_diagnostics::{suggest, Diagnostic, Edit, FileId, Fix, Span};
18use rustc_hash::FxHashMap;
19
20pub mod index;
21pub use index::{Index, LocalFlags, Occurrence, Role, Target};
22
23/// A stable identifier for a top-level definition within a module.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct DefId(pub u32);
26
27/// Defines `Builtin`'s variants together with [`Builtin::ALL`] from one
28/// variant list, so the two can never disagree — the exact pattern
29/// `rt_funcs!` (`nova-mir`) uses for `RtFunc`/`RtFunc::ALL`. Before this,
30/// `ALL` was a hand-maintained `[Builtin; N]` literal: it caught a variant
31/// added and never mentioned anywhere (the array's length wouldn't match),
32/// but not a copy-paste duplicate that swapped in an existing variant for a
33/// *different* one while leaving the length (and therefore the build)
34/// unchanged — e.g. `Print` listed twice with `Panic` quietly dropped. That
35/// is structurally impossible here: `ALL`'s elements come from the exact
36/// same identifier list that declares the enum, so duplicating one would
37/// duplicate an enum variant name, which does not compile.
38macro_rules! builtins {
39    ($($(#[$doc:meta])* $variant:ident),+ $(,)?) => {
40        /// A compiler-provided builtin function.
41        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42        pub enum Builtin {
43            $($(#[$doc])* $variant,)+
44        }
45
46        impl Builtin {
47            /// Every `Builtin` variant, in the declaration order above — the
48            /// sole authoritative list of "every builtin", generated by the
49            /// `builtins!` macro so a variant cannot exist without appearing
50            /// here (see the macro's own doc comment for why a duplicate
51            /// entry can't silently drop a different variant either).
52            /// `builtin_signatures_are_what_the_std_call_sites_use` iterates
53            /// this to check every variant's signature.
54            pub const ALL: [Builtin; builtins!(@count $($variant)+)] = [
55                $(Builtin::$variant),+
56            ];
57        }
58    };
59    (@count $($v:ident)+) => {
60        [$(builtins!(@one $v)),+].len()
61    };
62    (@one $v:ident) => { () };
63}
64
65builtins! {
66    /// `println(s: String)` — write a line to stdout.
67    Println,
68    /// `print(s: String)` — write to stdout without a trailing newline.
69    Print,
70    /// `eprint(s: String)` — write to stderr without a trailing newline.
71    EPrint,
72    /// `eprintln(s: String)` — write a line to stderr.
73    EPrintln,
74    /// `panic(msg: String)` — abort the program with a message.
75    Panic,
76    /// `str_cmp(a: String, b: String) -> Int` — byte-lexicographic compare,
77    /// returning `-1`, `0`, or `1`. Backs `std/core`'s `impl Ord for String`:
78    /// `<` is not defined for `String` (E0013) and `String` is not FFI-safe
79    /// (`require_ffi_safe` in `nova-typeck` only accepts scalars), so a
80    /// user-visible `extern` is not an option. Reached the same way
81    /// `println`/`print`/`panic` are — a `Res::Builtin` checked specially by
82    /// `nova-typeck` and lowered straight to a `CallRuntime` in `nova-mir` —
83    /// *not* the way `==` on `String` reaches `nova_rt_str_eq`: that is a
84    /// pure operator lowering (`binary_result_ty` permits `Eq`/`Ne` for
85    /// `Ty::String`, then MIR's `lower_binary` intercepts it) that introduces
86    /// no name in any namespace, whereas `str_cmp` is a named function.
87    /// Unlike `println`/`print`/`panic`, `str_cmp` is *not* part of the
88    /// user-visible language surface: it is seeded only into std modules' own
89    /// scopes (see [`Builtin::STD_ONLY`]), so it never becomes a
90    /// reserved word in user code.
91    StrCmp,
92    /// `str_hash(s: String) -> Int` — seeded FNV-1a over the string's bytes,
93    /// then splitmix64's finalizer. The seed is per-process, so the same
94    /// string hashes differently in two runs; why the finalizer is needed and
95    /// what the pair does and does not buy are documented at
96    /// `nova_rt_str_hash` and `splitmix64_finalize` in `nova-runtime` rather
97    /// than restated here. Backs
98    /// `std/core`'s `impl Hash for String`, and exists for the same reason
99    /// [`Builtin::StrCmp`] does: no primitive walks a `String`, so a NAMED
100    /// operation over its bytes needs a builtin like this one, and an
101    /// `extern` cannot stand in because `String` is not FFI-safe. Named is
102    /// the operative word -- `==` reaches `nova_rt_str_eq` as a pure operator
103    /// lowering introducing no name at all, as [`Builtin::StrCmp`]'s own doc
104    /// comment sets out just above. That is a statement
105    /// about the primitive layer, not about what Nova code can reach --
106    /// `std/bytes` and `std/strings` build byte and char access on builtins
107    /// of this kind, and ADR 0005's 2026-08-27 amendment records where an
108    /// earlier wording read as the wider claim. Std-only, so `str_hash` is
109    /// not a reserved word in user code.
110    StrHash,
111    /// `char_to_int(c: Char) -> Int` — a `Char`'s Unicode scalar value. The
112    /// only way any std module can compare or classify a `Char` numerically:
113    /// Nova has no other `Char` → `Int` conversion (`as` casts are
114    /// unsupported, `E0900`; `Char` has no methods beyond its trait impls;
115    /// no arithmetic operator accepts a `Char`). Backs `std/core`'s
116    /// `impl Hash for Char`, and `std/strings`' non-ASCII whitespace check
117    /// (`char_is_whitespace`), and any future std module with the same need.
118    /// Unlike every other builtin this is *not* a runtime call — `Char` and
119    /// `Int` are both `MirTy::I64`, so `nova-mir` lowers it to a register
120    /// move (see `lower_call`) rather than adding a runtime ABI symbol whose
121    /// body would be the identity function. Std-only, so `char_to_int` is
122    /// not a reserved word in user code.
123    CharToInt,
124    /// `str_len_chars(s: String) -> Int` — the number of Unicode scalar
125    /// values in `s`. Backs `std/strings`' `String::len`. No primitive walks
126    /// a `String`, so a length query has to be a builtin, and an `extern`
127    /// cannot stand in because `String` is not FFI-safe. Note this builtin is
128    /// part of what supplies the capability it names as absent, so read the
129    /// absence as one about primitives and not about `String` as user code
130    /// sees it. Separate from `str_chars` so a length query allocates
131    /// nothing. Std-only, so it is not a reserved word in user code.
132    StrLenChars,
133    /// `str_chars(s: String) -> [Char]` — the string's Unicode scalar values
134    /// in order. Backs `std/strings`' `String::chars`, and every operation in
135    /// that module that inspects a string. Also backs `std/core`'s
136    /// `impl Debug for String`, which needs to escape the contents. Std-only.
137    StrChars,
138    /// `str_from_chars(cs: [Char]) -> String` — encode codepoints as UTF-8.
139    /// The inverse of [`Builtin::StrChars`], and how every `std/strings`
140    /// operation that produces a string produces it: Nova has no `+` for
141    /// `String` (`E0013`), so building a result any other way would mean
142    /// quadratic interpolation. Std-only.
143    StrFromChars,
144    /// `str_join(sep: String, parts: [String]) -> String` — `parts` with
145    /// `sep` between each pair, copied byte for byte into one result. Backs
146    /// `std/strings`' `String.join`, which otherwise has to walk every part
147    /// character by character through [`Builtin::StrChars`] and
148    /// [`Builtin::StrFromChars`]. Std-only.
149    StrJoin,
150    /// `str_index_of(haystack: String, needle: String) -> Int` — the
151    /// character index of the first occurrence of `needle`, or `-1`, found by
152    /// a byte search. Backs `std/strings`' `String.index_of` and
153    /// `String.contains`, which used to build a [`Builtin::StrChars`] array
154    /// for each string first. Std-only.
155    StrIndexOf,
156    /// `json_quote(s: String) -> String` — `s` as a JSON string literal,
157    /// quotation marks included: the escaped length counted, then the result
158    /// written once. Backs `std/json`'s `quote`, which used to scan a
159    /// [`Builtin::StrChars`] array and rebuild any string that needed escaping
160    /// character by character. Std-only.
161    JsonQuote,
162    /// `str_to_upper(s: String) -> String` — full Unicode uppercase. Backs
163    /// `std/strings`' `String::to_upper`. Whole-string rather than
164    /// `Char` → `Char` because `ß` → `SS` is not 1:1. Std-only.
165    StrToUpper,
166    /// `str_to_lower(s: String) -> String` — full Unicode lowercase, for the
167    /// same reason as [`Builtin::StrToUpper`]. Std-only.
168    StrToLower,
169    /// `float_fixed(v: Float, places: Int) -> String` — render `v` to a fixed
170    /// number of decimal places. **Two callers, not one.** It was introduced
171    /// for `std/fmt`'s `Float::fixed` (`std/fmt/lib.nova`), and `std/json`'s
172    /// `impl FromJson for Int` (`std/json/lib.nova`) is now the second: it
173    /// takes an integral `Float`'s digits from `float_fixed(n, 0)` because
174    /// `Float`'s `Display` is a shortest-round-trip rendering rather than an
175    /// exact one. So a change to its rounding or its clamping reaches the JSON
176    /// decode path as well as `std/fmt`. Nova cannot implement this itself:
177    /// decimal rendering of a binary float is a solved problem with sharp
178    /// edges, and Nova has no `Float` to `Int` conversion at all to begin one
179    /// with. Std-only.
180    FloatFixed,
181    /// `str_to_float(s: String) -> Float` — parse decimal text as a `Float`.
182    /// **Two callers, not one.** It was introduced for `std/json`'s number
183    /// scanner, and `impl ToJson for Int` is now the second, which does not go
184    /// through the scanner at all. The exact mirror of [`Builtin::FloatFixed`],
185    /// and a builtin for the same reason: correctly rounded decimal-to-binary
186    /// conversion is a solved problem with sharp edges, and the obvious
187    /// `digits × 10^exp` accumulation double-rounds — once into the
188    /// significand, once into the scaling — so a Nova reimplementation would
189    /// not round-trip its own output. That is the whole of the refusal. The
190    /// language also has no `Int` → `Float` conversion (no such builtin, and
191    /// `as` casts are unsupported, `E0900`), but that is a cost rather than a
192    /// wall: an if/else chain from `Int` to a `Float` literal spans any fixed
193    /// digit range, the same construction `std/json`'s `hex_digit` used over
194    /// sixteen arms to reach `String` literals, until [`Builtin::JsonQuote`]
195    /// replaced it on 2026-10-03. Std-only, so
196    /// `str_to_float` is not a reserved word in user code.
197    StrToFloat,
198    /// `test_selector() -> Int` — which `@test` to run in this process, or a
199    /// negative value meaning "print the inventory instead".
200    ///
201    /// Reads `NOVA_TEST_INDEX`. A negative value is the sentinel rather than
202    /// "index >= count", because producing a negative needs no knowledge of the
203    /// count — and "greater than or equal" would require the runner to already
204    /// know the number it is asking for. An absent or unparseable variable
205    /// reads as negative, so running the test binary by hand with no variable
206    /// set prints its inventory rather than silently doing nothing.
207    ///
208    /// `STD_ONLY`, like `str_cmp`: seeded into std modules' scopes only, so it
209    /// never becomes a name a user program could call or collide with.
210    TestSelector,
211    /// `task_spawn(fut: Future<T>) -> Int` — register a future with this
212    /// thread's executor and return its task id. Backs `std/task`'s `spawn`.
213    ///
214    /// A builtin because its parameter is a `Future<T>`, which no `extern` can
215    /// mention (`Future` is not FFI-safe, `require_ffi_safe` in `nova-typeck`)
216    /// and no Nova expression can construct. Like every `Future`-taking builtin
217    /// below it is typed with `Ty::Param(0)`, so it unifies only against a
218    /// *generic parameter of index zero* — which is why every `std/task`
219    /// function that calls one declares exactly one type parameter, and why a
220    /// second one declared before it is a compile error at the call site
221    /// (`E0010`) rather than anything quieter. Std-only, so `task_spawn` is not
222    /// a reserved word in user code.
223    TaskSpawn,
224    /// `task_is_done(fut: Future<T>) -> Bool` — whether the task named by
225    /// `fut`'s state has completed.
226    ///
227    /// Backed `JoinHandle::join`'s spin loop before Task 3 replaced it with a
228    /// real park on completion ([`Builtin::TaskJoinFuture`]); no call site in
229    /// `std/task` remains, but the query is still part of the executor's
230    /// Nova-facing surface. Std-only.
231    TaskIsDone,
232    /// `task_release(fut: Future<T>)` — end the executor's claim on the task
233    /// named by `fut`'s state, so a joined task's state is not rooted for the
234    /// rest of the process. Idempotent, which is what lets `join` be called
235    /// twice on one handle. Std-only.
236    TaskRelease,
237    /// `task_drive(fut: Future<T>)` — run this thread's executor until every
238    /// queued task has finished. Backs `std/task`'s `block_on`, paired with
239    /// [`Builtin::TaskOutput`] to read the value: the runtime returns the
240    /// output as an `i64`, which is not the machine class of a `Float`.
241    /// Std-only.
242    TaskDrive,
243    /// `task_output(fut: Future<T>) -> T` — the value a finished future wrote
244    /// to its state object's output slot.
245    ///
246    /// Unlike every other builtin here this is *not* a runtime call: it is two
247    /// typed field loads (`nova-mir`'s `lower_call`), which is the only way the
248    /// value can arrive in `T`'s own machine class — a `Float` output crosses
249    /// register banks, so routing it through the executor's `i64` return would
250    /// reinterpret its bits. Safe to read at any point after the future has
251    /// completed, and any number of times, because the value stays in the state
252    /// object and the future keeps that object alive. Std-only.
253    TaskOutput,
254    /// `task_yield_future() -> Future<unit>` — a fresh future that reports
255    /// pending once and then completes.
256    ///
257    /// The whole of what `std/task`'s `yield_now` awaits, and it has to be a
258    /// builtin because a Nova `async fn` body suspends *only* at an `.await`:
259    /// there is nothing in the language to await that is not already ready.
260    /// Its future is built by `nova_rt_task_yield_future`, the one poll
261    /// function `nova-mir`'s async transform did not generate. Std-only.
262    TaskYieldFuture,
263    /// `task_sleep_future_nanos(nanos: Int) -> Future<unit>` — a fresh future
264    /// that parks for `nanos` nanoseconds, then completes.
265    ///
266    /// The first builtin that actually parks: unlike
267    /// [`Builtin::TaskYieldFuture`], which always resumes on the next turn,
268    /// this future registers a deadline with the executor's park set (Task
269    /// 1's `Wait::Deadline`) and is not polled again until that deadline
270    /// passes. Backs `std/time`'s `sleep`. A builtin for the same reason
271    /// `task_yield_future` is: an `async fn` body suspends only at an
272    /// `.await`, and nothing expressible in Nova produces a future that
273    /// starts out pending. Std-only.
274    TaskSleepFutureNanos,
275    /// `task_join_future(fut: Future<T>) -> Future<unit>` — a fresh future
276    /// that parks until the task named by `fut`'s state completes, then
277    /// completes itself.
278    ///
279    /// Resolves `fut` to a task id up front, through the same state-address
280    /// lookup [`Builtin::TaskIsDone`]/[`Builtin::TaskRelease`] use, so a
281    /// future no task was spawned for aborts immediately rather than
282    /// becoming a park nothing can wake. Backs `std/task`'s `join`, which
283    /// used to spin on [`Builtin::TaskIsDone`] instead: joining a task that
284    /// is parked with nothing left in the program that could ever wake it —
285    /// for example, two tasks joining each other in a cycle — now reports a
286    /// deadlock rather than costing one poll per turn forever. Joining a
287    /// task parked on a deadline (one that is sleeping) is not, by itself,
288    /// this. Joining a task that never finishes because it keeps
289    /// re-queueing itself (a `yield_now` loop) is a livelock, not a
290    /// deadlock, and is not reported: nothing there is parked, so it still
291    /// costs one poll per turn forever, undiagnosed
292    /// (`docs/adr/0009-async-execution-model.md` §1's 2026-08-10 amendment).
293    /// A builtin for the same reason `task_sleep_future_nanos`
294    /// is: an `async fn` body suspends only at an `.await`, and nothing
295    /// expressible in Nova produces a future that starts out pending.
296    /// Std-only.
297    TaskJoinFuture,
298    /// `task_timeout_future(nanos: Int, fut: Future<T>) -> Future<Int>` — a
299    /// fresh future that polls `fut` until it completes or `nanos` nanoseconds
300    /// pass, producing `0` if the inner future completed and `1` if the
301    /// deadline elapsed.
302    ///
303    /// Carries a status rather than the value, which is what keeps it
304    /// expressible: a builtin's signature is a fixed list of types, and the
305    /// value is read separately with [`Builtin::TaskOutput`] on the inner
306    /// future, from the slot the inner future itself wrote. Backs `std/time`'s
307    /// `timeout`. Std-only.
308    TaskTimeoutFuture,
309    /// `fs_read_to_string(path: String) -> Int` — read `path` as UTF-8.
310    ///
311    /// Returns a status code (`0` on success, an `IoErrorKind` code otherwise
312    /// — see `crates/nova-runtime/src/fs.rs`), not the string itself: an
313    /// intrinsic returns exactly one word, and this call must convey both a
314    /// `String` and enough information to build an `IoError`. On success the
315    /// contents are waiting in [`Builtin::FsTakeString`]. Backs `std/fs`'s
316    /// `read_to_string`. Std-only.
317    FsReadToString,
318    /// `fs_write_string(path: String, content: String) -> Int` — write
319    /// `content` to `path`, truncating any existing contents. A status code,
320    /// for the same reason as [`Builtin::FsReadToString`]. Backs `std/fs`'s
321    /// `write_string`. Std-only.
322    FsWriteString,
323    /// `fs_take_string() -> String` — take the payload staged by a successful
324    /// [`Builtin::FsReadToString`]. Backs `std/fs`'s `read_to_string`.
325    /// Std-only.
326    FsTakeString,
327    /// `fs_last_error_message() -> String` — take the OS-provided message
328    /// staged by a failed filesystem operation. Backs `std/fs`'s
329    /// `read_to_string` and `write_string`. Std-only.
330    FsLastErrorMessage,
331    /// `fs_temp_dir() -> String` — the OS temporary-directory path
332    /// (`std::env::temp_dir()`, lossily reencoded from UTF-16 on Windows —
333    /// see `crates/nova-runtime/src/fs.rs`). Backs `std/fs`'s `temp_dir`.
334    /// Not a runtime-call-only concern: this is what lets a fixture build a
335    /// writable path without hardcoding one. Std-only.
336    FsTempDir,
337    /// `fs_exists(path: String) -> Bool` — whether `path` exists on disk.
338    /// Not a status code like its siblings: `nova-spec/20-STDLIB.md` §5
339    /// gives `exists` a plain `Bool`, so a path that exists but cannot be
340    /// examined is indistinguishable from one that is absent. Backs
341    /// `std/fs`'s `exists`. Std-only.
342    FsExists,
343    /// `fs_create_dir(path: String) -> Int` — create the directory `path`.
344    /// Its parent must exist; an existing `path` is `AlreadyExists`. A
345    /// status code, for the same reason as [`Builtin::FsReadToString`].
346    /// Backs `std/fs`'s `create_dir`. Std-only.
347    FsCreateDir,
348    /// `fs_create_dir_all(path: String) -> Int` — create the directory
349    /// `path`, and any missing parent directories along the way. Unlike
350    /// [`Builtin::FsCreateDir`], an already-existing directory at `path` is
351    /// success, not `AlreadyExists`. Backs `std/fs`'s `create_dir_all`.
352    /// Std-only.
353    FsCreateDirAll,
354    /// `fs_remove_file(path: String) -> Int` — remove the file `path`. A
355    /// status code, for the same reason as [`Builtin::FsReadToString`].
356    /// Backs `std/fs`'s `remove_file`. Std-only.
357    FsRemoveFile,
358    /// `fs_remove_dir_all(path: String) -> Int` — remove the directory
359    /// `path` and everything beneath it. A status code, for the same reason
360    /// as [`Builtin::FsReadToString`]. Backs `std/fs`'s `remove_dir_all`.
361    /// Std-only.
362    FsRemoveDirAll,
363    /// `fs_read_dir(path: String) -> Int` — list `path`'s entry names,
364    /// sorted. A status code, for the same reason as
365    /// [`Builtin::FsReadToString`]; on success the names are waiting in
366    /// [`Builtin::FsTakeStringArray`]. Sorted in the runtime because
367    /// directory order is unspecified by every OS. Backs `std/fs`'s
368    /// `read_dir`. Std-only.
369    FsReadDir,
370    /// `fs_take_string_array() -> [String]` — take the array staged by a
371    /// successful [`Builtin::FsReadDir`]. Backs `std/fs`'s `read_dir`.
372    /// Std-only.
373    FsTakeStringArray,
374    /// `fs_kind(path: String) -> Int` — what `path` is: `0` metadata
375    /// unavailable (absent, or unreadable), `1` file, `2` directory. One call
376    /// rather than separate `is_file`/`is_dir` intrinsics, so a `DirEntry`
377    /// costs one syscall instead of two and the two answers cannot disagree.
378    /// Backs `std/fs`'s `read_dir`. Std-only.
379    FsKind,
380    /// `fs_read(path: String) -> Int` — read the path's raw bytes. `0` on
381    /// success, with the contents waiting in [`Builtin::FsTakeBytes`];
382    /// otherwise an `IoErrorKind` status code (`crates/nova-runtime/src/fs.rs`).
383    /// Unlike [`Builtin::FsReadToString`] this cannot report `InvalidData`:
384    /// there is no encoding to violate. Backs `std/fs`'s `read`. Std-only.
385    FsRead,
386    /// `fs_take_bytes() -> Bytes` — take the payload staged by a successful
387    /// [`Builtin::FsRead`]. Backs `std/fs`'s `read`. Std-only.
388    FsTakeBytes,
389    /// `fs_write(path: String, content: Bytes) -> Int` — write the bytes to
390    /// the path, truncating an existing file. Status code, as
391    /// [`Builtin::FsReadToString`]. Backs `std/fs`'s `write`. Std-only.
392    FsWrite,
393    /// `file_open(path: String, read: Bool, write: Bool, append: Bool,
394    /// truncate: Bool, create: Bool, create_new: Bool) -> Int` — open `path`
395    /// through `std::fs::OpenOptions`, forwarding all six flags one for one.
396    /// `0` on success, with the new fd waiting in [`Builtin::FsTakeBytes`] as
397    /// an 8-byte little-endian payload — the fd cannot travel in the status
398    /// word because the status word already carries the `IoErrorKind`, the
399    /// same reason [`Builtin::IoStdoutWrite`]'s byte count does not either;
400    /// otherwise an `IoErrorKind` status code, as [`Builtin::FsReadToString`].
401    /// The widest builtin in this table, one parameter per `OpenOptions`
402    /// method rather than a packed bitmask, so the Nova surface matches
403    /// `std::fs::OpenOptions` field for field. Runtime symbol
404    /// `nova_rt_file_open` (`crates/nova-runtime/src/file.rs`). Std-only.
405    FileOpen,
406    /// `file_close(fd: Int) -> Int` — close `fd`, dropping the underlying
407    /// file and releasing its OS handle. Idempotent: closing an
408    /// already-closed, stale, or forged fd finds nothing in the runtime's
409    /// handle table and still reports success, so a caller may close the
410    /// same value more than once. No payload, mirroring
411    /// [`Builtin::IoStdoutFlush`]. Runtime symbol `nova_rt_file_close`
412    /// (`crates/nova-runtime/src/file.rs`). Std-only.
413    FileClose,
414    /// `file_read(fd: Int, max: Int) -> Int` — read up to `max` bytes from
415    /// `fd`, the same status/payload shape as [`Builtin::IoStdinRead`]. `0`
416    /// on success, with the bytes read waiting in [`Builtin::FsTakeBytes`];
417    /// otherwise an `IoErrorKind` status code, as [`Builtin::FsReadToString`]
418    /// — including a closed, stale, or forged fd, which the runtime's handle
419    /// table reports as an ordinary `IoError` rather than a panic. An
420    /// **empty** payload means end of stream; a short read does not. Runtime
421    /// symbol `nova_rt_file_read` (`crates/nova-runtime/src/file.rs`).
422    /// Std-only.
423    FileRead,
424    /// `file_write(fd: Int, buf: Bytes) -> Int` — write `buf` to `fd` with
425    /// one `Write::write` call, not a `write_all` loop, the same shape as
426    /// [`Builtin::IoStdoutWrite`]. `0` on success, with the number of bytes
427    /// actually written waiting in [`Builtin::FsTakeBytes`], encoded as an
428    /// 8-byte little-endian count; otherwise an `IoErrorKind` status code, as
429    /// [`Builtin::FsReadToString`] — including a closed, stale, or forged
430    /// fd, as [`Builtin::FileRead`]. Runtime symbol `nova_rt_file_write`
431    /// (`crates/nova-runtime/src/file.rs`). Std-only.
432    FileWrite,
433    /// `file_flush(fd: Int) -> Int` — flush `fd`. A status code, as
434    /// [`Builtin::FsReadToString`], including a closed, stale, or forged fd,
435    /// as [`Builtin::FileRead`]; unlike [`Builtin::FileWrite`], a successful
436    /// flush stashes no payload of its own for [`Builtin::FsTakeBytes`] to
437    /// collect, mirroring [`Builtin::IoStdoutFlush`]. Runtime symbol
438    /// `nova_rt_file_flush` (`crates/nova-runtime/src/file.rs`). Std-only.
439    FileFlush,
440    /// `io_stdin_read(max: Int) -> Int` — read up to `max` bytes from stdin,
441    /// the same status/payload shape as [`Builtin::FsRead`]. `0` on success,
442    /// with the bytes read waiting in [`Builtin::FsTakeBytes`]; otherwise an
443    /// `IoErrorKind` status code, as [`Builtin::FsReadToString`]. An
444    /// **empty** payload means end of stream; a short read does not. Runtime
445    /// symbol `nova_rt_io_stdin_read` (`crates/nova-runtime/src/io.rs`).
446    /// Std-only.
447    IoStdinRead,
448    /// `io_stdout_write(buf: Bytes) -> Int` — write `buf` to stdout with one
449    /// `Write::write` call, not a `write_all` loop, so a short write is
450    /// reported rather than retried underneath the caller. `0` on success,
451    /// with the number of bytes actually written waiting in
452    /// [`Builtin::FsTakeBytes`], encoded as an 8-byte little-endian count;
453    /// otherwise an `IoErrorKind` status code, as [`Builtin::FsReadToString`].
454    /// Runtime symbol `nova_rt_io_stdout_write`
455    /// (`crates/nova-runtime/src/io.rs`). Std-only.
456    IoStdoutWrite,
457    /// `io_stderr_write(buf: Bytes) -> Int` — write `buf` to stderr. Mirrors
458    /// [`Builtin::IoStdoutWrite`] exactly, against the other stream.
459    /// Std-only.
460    IoStderrWrite,
461    /// `io_stdout_flush() -> Int` — flush stdout. A status code, as
462    /// [`Builtin::FsReadToString`]; unlike [`Builtin::IoStdoutWrite`], a
463    /// successful flush stashes no payload of its own for
464    /// [`Builtin::FsTakeBytes`] to collect -- it does not clear whatever a
465    /// prior operation left there either. Std-only.
466    IoStdoutFlush,
467    /// `io_stderr_flush() -> Int` — flush stderr. Mirrors
468    /// [`Builtin::IoStdoutFlush`] exactly, against the other stream.
469    /// Std-only.
470    IoStderrFlush,
471    /// `net_connect(addr: String) -> Future<Int>` — connect to `addr`
472    /// ("host:port"), non-blockingly.
473    ///
474    /// **A future constructor, not a status word — the one thing that makes
475    /// this builtin and the other future constructors in this group
476    /// ([`Builtin::NetRead`], [`Builtin::NetWrite`],
477    /// [`Builtin::NetReadTimeout`], [`Builtin::NetAccept`]) unlike every
478    /// `fs_*`/`file_*`/`io_*` intrinsic above.** Named rather than counted
479    /// positionally, and deliberately with no count at all: the variants below
480    /// this one are a mix of future constructors and status words in no
481    /// particular order, so "the three below it" would read as "the next
482    /// three" and be wrong that way, and any number written here goes stale
483    /// the next time `std/net` grows — **this sentence has already been
484    /// rewritten twice for exactly that reason**, once when
485    /// [`Builtin::NetLocalPort`] landed and again when
486    /// [`Builtin::NetAccept`] did.
487    ///
488    /// The archaeology that used to close this paragraph is retracted rather
489    /// than recounted. It said: "Two rewrites over three staleness events:
490    /// `main`'s 'the three below it' first went wrong when
491    /// [`Builtin::NetListen`] arrived and was left standing for an
492    /// increment." The count and the ordinal are both wrong, under either
493    /// reading of the wording they describe. Read as its own words invite --
494    /// the next three variants -- "the three below it" was **already** wrong
495    /// on `main`, because [`Builtin::NetClose`] is the variant directly below
496    /// and is not a future constructor at all. Read as the three *other*
497    /// future constructors, it stayed true through both
498    /// [`Builtin::NetListen`] and [`Builtin::NetLocalPort`], since both are
499    /// status words, and only [`Builtin::NetAccept`] falsified it. So
500    /// [`Builtin::NetListen`] falsified no wording here at all. The sentence
501    /// it did falsify was [`Builtin::NetClose`]'s own "the one member of this
502    /// group of five", and that was corrected in the very commit that added
503    /// [`Builtin::NetListen`] rather than left standing.
504    ///
505    /// Those status words return an `i64` directly and synchronously; this
506    /// one returns a `*mut u8` future state object
507    /// (surfaced to Nova as `Future<Int>`) that must itself be `.await`ed to
508    /// produce the `Int` status — `0` on success, with the new fd waiting in
509    /// [`Builtin::FsTakeBytes`] as an 8-byte little-endian payload, the same
510    /// reason [`Builtin::FileOpen`]'s new fd travels that way; otherwise an
511    /// `IoErrorKind` status code, as [`Builtin::FsReadToString`], including
512    /// `CONNECTION_REFUSED`'s first producer. A reader arriving here from
513    /// `file.rs` should not expect the old shape. Runtime symbol
514    /// `nova_rt_net_connect_future` (`crates/nova-runtime/src/net.rs`).
515    /// Std-only.
516    NetConnect,
517    /// `net_close(fd: Int) -> Int` — close `fd`, dropping the underlying
518    /// handle and releasing its OS handle.
519    ///
520    /// **Not a future constructor**, unlike every future constructor
521    /// [`Builtin::NetConnect`]'s own doc comment rosters — an ordinary status
522    /// word, exactly [`Builtin::FileClose`]'s shape, for the identical reason:
523    /// idempotent,
524    /// since `std/net`'s `close` cannot consume its receiver (Nova has no
525    /// move checking), so a caller can always reach a second call, and
526    /// closing an already-closed, stale, or forged fd finds nothing in the
527    /// runtime's handle table and still reports success. No payload. Runtime
528    /// symbol `nova_rt_net_close` (`crates/nova-runtime/src/net.rs`).
529    ///
530    /// That contrast read "unlike [`Builtin::NetConnect`] and the read/write
531    /// group below it", phrased that way in the commit that added
532    /// [`Builtin::NetListen`] so it would survive the group growing. It did
533    /// not survive [`Builtin::NetAccept`], which is a future constructor
534    /// below this one and is neither a read nor a write, so the one variant
535    /// added after the rewrite is exactly the one the growth-proof phrasing
536    /// excluded. Deferring to [`Builtin::NetConnect`]'s roster leaves one
537    /// list to keep current instead of two that can disagree.
538    ///
539    /// **Serves a listener as well as a connected stream**, which is why
540    /// [`Builtin::NetListen`] adds no close of its own: the runtime keeps both
541    /// kinds in one handle table and removes an entry by key regardless of
542    /// which kind it holds. `TcpListener::close` in `std/net` calls this same
543    /// builtin. Std-only.
544    NetClose,
545    /// `net_read(fd: Int, max: Int) -> Future<Int>` — read up to `max` bytes
546    /// from `fd`, non-blockingly.
547    ///
548    /// A future constructor, as [`Builtin::NetConnect`]'s doc comment
549    /// explains — `.await`ing it produces `0` on success, with the bytes
550    /// read waiting in [`Builtin::FsTakeBytes`]; otherwise an `IoErrorKind`
551    /// status code, as [`Builtin::FsReadToString`] — including a closed,
552    /// stale, or forged fd. An **empty** payload means end of stream; a
553    /// short read does not, the same contract [`Builtin::FileRead`] and
554    /// [`Builtin::IoStdinRead`] already carry. Runtime symbol
555    /// `nova_rt_net_read_future` (`crates/nova-runtime/src/net.rs`).
556    /// Std-only.
557    NetRead,
558    /// `net_write(fd: Int, buf: Bytes) -> Future<Int>` — write `buf` to `fd`
559    /// with one non-blocking `Write::write` attempt, not a `write_all` loop.
560    ///
561    /// A future constructor, as [`Builtin::NetConnect`]'s doc comment
562    /// explains — `.await`ing it produces `0` on success, with the number of
563    /// bytes actually written waiting in [`Builtin::FsTakeBytes`], encoded
564    /// as an 8-byte little-endian count, the same shape
565    /// [`Builtin::FileWrite`] uses; otherwise an `IoErrorKind` status code,
566    /// as [`Builtin::FsReadToString`] — including a closed, stale, or forged
567    /// fd. Runtime symbol `nova_rt_net_write_future`
568    /// (`crates/nova-runtime/src/net.rs`). Std-only.
569    NetWrite,
570    /// `net_read_timeout(fd: Int, max: Int, ms: Int) -> Future<Int>` — read
571    /// up to `max` bytes from `fd`, non-blockingly, reporting `TIMED_OUT` if
572    /// `ms` milliseconds pass with nothing to read first.
573    ///
574    /// A future constructor, as [`Builtin::NetConnect`]'s doc comment
575    /// explains — otherwise identical to [`Builtin::NetRead`], including
576    /// EOF/short-read semantics and where the bytes land on success. Runtime
577    /// symbol `nova_rt_net_read_timeout_future`
578    /// (`crates/nova-runtime/src/net.rs`). Std-only.
579    NetReadTimeout,
580    /// `net_listen(addr: String) -> Int` — bind and listen on `addr`
581    /// ("host:port"), registering a non-blocking listening socket.
582    ///
583    /// **Not a future constructor**, one of the three builtins in this group
584    /// that are not, with [`Builtin::NetClose`] and
585    /// [`Builtin::NetLocalPort`]: the `bind` and `listen` *syscalls*
586    /// are immediate kernel bookkeeping and do not block, so there is no
587    /// suspension to model and `std/net`'s `bind` is a plain `fn` rather than
588    /// an `async fn`. The waiting a server does happens in
589    /// [`Builtin::NetAccept`], which is a separate builtin and *is* a future
590    /// constructor.
591    ///
592    /// **That is a claim about the syscalls, not about every argument.** A
593    /// numeric address never blocks, but this builtin's parameter is a
594    /// `String` a user program chooses, and a hostname resolves through
595    /// `std`'s `ToSocketAddrs` — a blocking lookup with no future to park on.
596    /// `resolve_addr`'s own doc comment in `crates/nova-runtime/src/net.rs`
597    /// records that caveat for `connect`, and it applies here unchanged:
598    /// out of scope rather than handled. `nova_rt_net_listen`'s doc comment
599    /// also records where its resolution *differs* from `connect`'s, since it
600    /// does not go through `resolve_addr` at all.
601    ///
602    /// Returns a status code as [`Builtin::FsReadToString`] does — `0` on
603    /// success, with the new fd waiting in [`Builtin::FsTakeBytes`] as an
604    /// 8-byte little-endian payload, the same channel [`Builtin::NetConnect`]
605    /// and [`Builtin::FileOpen`] use for their own fds. The fd is closed with
606    /// [`Builtin::NetClose`], which serves both kinds of handle.
607    ///
608    /// **An address already in use arrives as `Other`, not a distinct kind.**
609    /// `fs.rs`'s `IoErrorKind` table has no `AddrInUse` variant, so the most
610    /// likely failure of this builtin is discriminable only by the message
611    /// text the operating system supplied. Adding a variant is a wire-contract
612    /// change across the runtime's `fail` and `std/io`'s `io_error_kind_of`
613    /// together, and is deliberately not done. Runtime symbol
614    /// `nova_rt_net_listen` (`crates/nova-runtime/src/net.rs`). Std-only.
615    NetListen,
616    /// `net_local_port(fd: Int) -> Int` — the port the listening socket behind
617    /// `fd` is bound to, which is how a caller learns the kernel's choice after
618    /// [`Builtin::NetListen`] on port 0.
619    ///
620    /// **Not a future constructor**, joining [`Builtin::NetClose`] and
621    /// [`Builtin::NetListen`] rather than the future constructors
622    /// [`Builtin::NetConnect`]'s own doc comment rosters. This read "the third
623    /// builtin in this group that is not", contrasted "rather than the
624    /// read/write group": the ordinal ranks within a group that grows, and
625    /// "the read/write group" stopped naming the future constructors the
626    /// moment [`Builtin::NetAccept`] joined them, which is the same pair of
627    /// habits corrected at [`Builtin::NetConnect`] and [`Builtin::NetClose`]
628    /// above. `local_addr` is a `getsockname` call over
629    /// bookkeeping the kernel already holds, so there is no suspension to model
630    /// and `std/net`'s `local_port` is a plain `fn` rather than an `async fn`.
631    ///
632    /// **Unlike [`Builtin::NetListen`], that claim needs no caveat about its
633    /// argument.** This builtin takes an `Int` fd, not a `String`, so there is
634    /// no address for `std`'s `ToSocketAddrs` to resolve and no blocking lookup
635    /// to reach. It really cannot block, on any argument.
636    ///
637    /// Returns a status code as [`Builtin::FsReadToString`] does — `0` on
638    /// success, **with the port waiting in [`Builtin::FsTakeBytes`]** as an
639    /// 8-byte little-endian payload, the same channel [`Builtin::NetListen`]'s
640    /// and [`Builtin::NetConnect`]'s fds ride. The port cannot ride the return
641    /// value instead: that word *is* the `IoErrorKind` status, so a port there
642    /// would make `0` ambiguous and every non-zero port indistinguishable from
643    /// a failure.
644    ///
645    /// **An `fd` naming a connected stream rather than a listener is an error
646    /// here, and an `Other` one.** The runtime keeps both kinds of handle in
647    /// one table and its kind-checked lookup collapses wrong-kind into the same
648    /// miss an absent, closed, stale or forged fd gives — so a stream's own
649    /// local port is unreachable through this builtin, and no distinguishable
650    /// wrong-kind status exists to rely on. Runtime symbol
651    /// `nova_rt_net_local_port` (`crates/nova-runtime/src/net.rs`). Std-only.
652    NetLocalPort,
653    /// `net_accept(fd: Int) -> Future<Int>` — wait for the next incoming
654    /// connection on the listening socket behind `fd`.
655    ///
656    /// A future constructor, as [`Builtin::NetConnect`]'s doc comment
657    /// explains — unlike [`Builtin::NetClose`] and [`Builtin::NetLocalPort`],
658    /// which are status words because their syscalls cannot wait on a peer.
659    /// Waiting on a peer is the whole of what this one does, which is why
660    /// `std/net`'s `accept` is an `async fn` while `bind` and
661    /// `TcpListener::local_port` beside it are plain `fn`s. `.await`ing it
662    /// produces `0` on success, with the accepted
663    /// connection's new fd waiting in [`Builtin::FsTakeBytes`] as an 8-byte
664    /// little-endian payload, the same channel [`Builtin::NetListen`]'s and
665    /// [`Builtin::NetConnect`]'s own fds ride; otherwise an `IoErrorKind`
666    /// status code, as [`Builtin::FsReadToString`] — including a closed,
667    /// stale, or forged fd, and an fd naming a connected stream rather than a
668    /// listener, which is the same wrong-kind miss
669    /// [`Builtin::NetLocalPort`] describes.
670    ///
671    /// **The accepted fd is an ordinary `TcpStream` fd**, indistinguishable
672    /// from one [`Builtin::NetConnect`] produced, so
673    /// [`Builtin::NetRead`]/[`Builtin::NetWrite`]/[`Builtin::NetReadTimeout`]
674    /// and [`Builtin::NetClose`] all work on it unchanged.
675    ///
676    /// **A task parked here cannot also be waiting on a connection.** Staging
677    /// two I/O waits in one poll ends the process (`stage_park` in
678    /// `crates/nova-runtime/src/task.rs`), so a server that accepts while
679    /// serving needs at least two tasks, and this language has no
680    /// `select`/`race` to avoid that. Runtime symbol
681    /// `nova_rt_net_accept_future` (`crates/nova-runtime/src/net.rs`).
682    /// Std-only.
683    NetAccept,
684    /// `bytes_len(b: Bytes) -> Int` — the byte length. Not a character
685    /// count: `Bytes` has no encoding. Backs `std/bytes`'s `Bytes::len`.
686    /// No primitive reads a `Bytes` value's own header, exactly as none walks
687    /// a `String`, so this reaches the runtime the same way `str_len_chars`
688    /// does -- and as there, the absence is one about primitives rather than
689    /// about what `std/bytes` then offers on top of them. Std-only.
690    BytesLen,
691    /// `bytes_from_string_intrinsic(s: String) -> Bytes` — a `Bytes` holding
692    /// `s`'s UTF-8 bytes. Backs the free function `bytes_from_string` in
693    /// `std/bytes/lib.nova`.
694    ///
695    /// **Named `..._intrinsic`, not `bytes_from_string`, deliberately.**
696    /// `std/bytes` is itself an entry of [`STD_MODULES`], so `Builtin::STD_ONLY`
697    /// is seeded into its *own* scope before that module's items are
698    /// collected (see `resolve_program`'s pass order). A `pub fn
699    /// bytes_from_string` in that same file would then hit `insert_value`'s
700    /// `scope.values.contains_key` check against this builtin's own
701    /// pre-seeded entry and fail name resolution with `E0002 duplicate
702    /// definition` — **not** silently compile into a recursive call, and not
703    /// silently shadow the builtin either; `insert_value` has no soft/shadow
704    /// path for a module's own item colliding with something already in its
705    /// own scope, unlike the separate, later, `entry`-style glob import
706    /// `import_std_module` uses to let one *std module's* export lose to
707    /// another module's own definition. Verified empirically (see the Task 2
708    /// report): naming this builtin `bytes_from_string` reproduces the
709    /// `E0002` against `std/bytes/lib.nova` itself, on every compile. Std-only.
710    BytesFromString,
711    /// `bytes_is_utf8(b: Bytes) -> Bool` — whether `b`'s bytes are valid
712    /// UTF-8. Backs `std/bytes`'s `Bytes::to_string`, paired with
713    /// [`Builtin::BytesToStringUnchecked`] so that method can be a Nova-level
714    /// `if` rather than needing a status-code protocol for one boolean.
715    /// Std-only.
716    BytesIsUtf8,
717    /// `bytes_to_string_unchecked(b: Bytes) -> String` — `b`'s bytes as a
718    /// `String`, without checking UTF-8 validity. Backs `std/bytes`'s
719    /// `Bytes::to_string`, whose only call site checks
720    /// [`Builtin::BytesIsUtf8`] first — an invalid `String` would be unsound,
721    /// which is why that check is not optional at the call site even though
722    /// this builtin cannot enforce it itself. Std-only.
723    BytesToStringUnchecked,
724    /// `bytes_at(b: Bytes, i: Int) -> Int` — the byte at `i`, in `0..=255`.
725    /// Backs `std/bytes`'s `Bytes::byte_at`, which bounds-checks `i` first and
726    /// returns `Option`; this intrinsic aborts on an out-of-range index rather
727    /// than returning a sentinel, since that path guards a `std/bytes` bug
728    /// rather than a user mistake. Nova cannot index a `Bytes` value's own
729    /// buffer (no indexing operator reaches the runtime), so this reaches the
730    /// runtime the same way `bytes_len` does. Std-only.
731    BytesAt,
732    /// `bytes_slice(b: Bytes, start: Int, end: Int) -> Bytes` — the bytes in
733    /// `start..end`, clamped to the buffer and to `start <= end`. Backs
734    /// `std/bytes`'s `Bytes::slice`. Clamps rather than aborting — a
735    /// deliberate divergence from `std/strings`'s `String::slice`, which
736    /// panics on an out-of-range bound; a `Bytes` length often comes from
737    /// disk rather than from a caller who already knows it, so clamping is
738    /// the better failure mode here. Recorded, accepted divergence — see the
739    /// design spec's dated note — not agreement with `String::slice`.
740    /// Std-only.
741    BytesSlice,
742    /// `bytes_concat(a: Bytes, b: Bytes) -> Bytes` — `a`'s bytes followed by
743    /// `b`'s. Backs `std/bytes`'s `Bytes::concat`. Std-only.
744    BytesConcat,
745    /// `bytes_to_ints(b: Bytes) -> [Int]` — each byte as an `Int` in
746    /// `0..=255`, in order. Backs `std/bytes`'s `Bytes::to_ints`, and in turn
747    /// `index_of`/`contains`, which search over the resulting array in Nova
748    /// rather than through a per-search intrinsic. Std-only.
749    BytesToInts,
750    /// `bytes_from_ints_intrinsic(ints: [Int]) -> Bytes` — a `Bytes` holding
751    /// each element of `ints` as one byte. Aborts if any element is outside
752    /// `0..=255`. Backs the free function `bytes_from_ints` in
753    /// `std/bytes/lib.nova`.
754    ///
755    /// **Named `..._intrinsic`, not `bytes_from_ints`, for the same reason as
756    /// [`Builtin::BytesFromString`]** — see that variant's doc comment.
757    /// `std/bytes` is itself an entry of [`STD_MODULES`], so a `pub fn
758    /// bytes_from_ints` there sharing this builtin's name would collide with
759    /// its own pre-seeded builtin entry (`insert_value`'s `E0002`) rather than
760    /// calling it. Std-only.
761    BytesFromInts,
762    /// `bytes_eq(a: Bytes, b: Bytes) -> Bool` — byte-for-byte equality. Backs
763    /// `std/bytes`'s `impl Eq for Bytes`. A dedicated intrinsic rather than an
764    /// operator lowering: unlike `String`'s `==` (a pure operator lowering
765    /// straight to `nova_rt_str_eq`, see [`Builtin::StrCmp`]'s doc comment),
766    /// `Bytes` equality is reached only through the `Eq` trait's `eq` method,
767    /// so `binary_result_ty` needs no `Ty::Bytes` arm for `==`/`!=`. Std-only.
768    BytesEq,
769    /// `int_hash_seed() -> Int` — the per-process seed for `std/core`'s `Int`,
770    /// `Bool` and `Char` hashing, XORed into the input before `mix64` runs, as
771    /// `nova_rt_int_hash_seed` draws it. What it buys is precomputation
772    /// resistance: a colliding key set cannot be built offline against a
773    /// process that has not started yet.
774    ///
775    /// A SEPARATE call from the one behind [`Builtin::StrHash`]'s seed. Were
776    /// the two one value, `(0).hash()` would equal `("").hash()` exactly, since
777    /// both reduce to the same finalizer applied to the seed alone. That the
778    /// two differ is what the separate call buys; whether recovering one yields
779    /// the other is a question about the draw's internals, answered at
780    /// `int_hash_seed` in `nova-runtime` and not settled there either.
781    ///
782    /// **Nova code can recover this value in one call**, since those `Hash`
783    /// impls XOR it in: `(0).hash()` is `mix64(0 ^ seed)`, which is
784    /// `mix64(seed)`, and `mix64` is an invertible bijection. That is the same
785    /// shape `("").hash()` already has for the string seed, it follows from ADR
786    /// 0005's one-shot `Hash` returning a plain `Int`, and the gate claim
787    /// already declines the adaptive attacker for whom it matters.
788    /// `tests/runtime/hash.nova` performs that recovery, to cancel the seed
789    /// when it asserts `mix64`'s canonical vectors. Std-only.
790    IntHashSeed,
791    /// `http_parse_request(buf: Bytes) -> [Int]` — an HTTP/1.1 request head
792    /// parsed into a flat table of byte offsets into `buf` itself. Copies
793    /// nothing, allocates nothing on the Rust heap, and holds no state between
794    /// calls, so `std/http` has nothing to release and nothing to leak per
795    /// request.
796    ///
797    /// Element 0 is a status: `0` complete, `1` partial (read more and call
798    /// again), negative for an error. **On any non-zero status the array has
799    /// length 1**, so a caller who skips the status check indexes out of
800    /// bounds rather than reading a plausible-looking offset. The full
801    /// encoding lives on `nova_rt_http_parse_request` in
802    /// `crates/nova-runtime/src/http.rs`, which is the half that writes it.
803    /// Std-only.
804    HttpParseRequest,
805    /// `crypto_hash(op: Int, key: Bytes, data: Bytes, tag: Bytes) -> Int` —
806    /// `std/crypto`'s hash/HMAC intrinsic: SHA-256, SHA-512, HMAC-SHA-256, or
807    /// HMAC-SHA-256's constant-time tag check, selected by `op` (the `OP_*`
808    /// constants in `crates/nova-runtime/src/crypto.rs`). Nova has no way to
809    /// reach a vetted hash implementation from source, so every one of these
810    /// operations crosses into Rust, which calls into `ring`.
811    ///
812    /// **The return is a status word, not the digest.** For the digest
813    /// operations, `0` means success, with the digest waiting in
814    /// [`Builtin::FsTakeBytes`]. For the tag check, `0` means the tag
815    /// matched and `1` means it did not — an answer, not an error. **No
816    /// negative range at all**: none of `crypto_hash`'s operations can fail
817    /// for any input a Nova program can construct. That scope is deliberate
818    /// rather than hedging — the digest, HMAC-key and HMAC-sign paths each
819    /// end in an `.unwrap()` inside `ring`, on an input-length error needing
820    /// roughly 2^61 bytes, so the bound is `ring`'s and not this tree's; the
821    /// tag check's `ring::hmac::verify` returns a `Result` and unwraps
822    /// nothing. `crates/nova-runtime/src/crypto.rs`'s `ERR_*` block states
823    /// this at length.
824    /// Runtime symbol `nova_rt_crypto_hash`
825    /// (`crates/nova-runtime/src/crypto.rs`). Std-only.
826    CryptoHash,
827    /// `crypto_random_bytes(n: Int) -> Int` — `std/crypto`'s random-bytes
828    /// intrinsic: `n` bytes drawn from the OS entropy source via `ring`.
829    /// Nova has no way to reach an OS entropy source from source, so this
830    /// crosses into Rust.
831    ///
832    /// **The return is a status word, not the bytes.** `0` on success, with
833    /// the bytes waiting in [`Builtin::FsTakeBytes`]; otherwise
834    /// **negative**, naming one of `crypto.rs`'s `ERR_*` kinds — an `n` that
835    /// does not fit a `usize`, a request over the size cap, or the entropy
836    /// source itself failing. Runtime symbol `nova_rt_crypto_random_bytes`
837    /// (`crates/nova-runtime/src/crypto.rs`). Std-only.
838    CryptoRandomBytes,
839    /// `crypto_random_int(min: Int, max: Int) -> Int` — `std/crypto`'s
840    /// bounded random-integer intrinsic: a uniform draw from `min..=max` by
841    /// rejection sampling against the OS entropy source. Nova has no way to
842    /// reach an OS entropy source from source, so this crosses into Rust.
843    ///
844    /// **The return is a status word, not the integer.** `0` on success,
845    /// with the value waiting in the same [`Builtin::FsTakeBytes`] slot that
846    /// [`Builtin::CryptoHash`] and [`Builtin::CryptoRandomBytes`] use,
847    /// decoded on the Nova side as `decode_count(fs_take_bytes())`;
848    /// otherwise **negative**, naming one of `crypto.rs`'s `ERR_*` kinds.
849    /// Three conditions reach that range, not two: an inverted range
850    /// (`ERR_INVALID_RANGE`), the entropy source itself failing, and the
851    /// bounded rejection loop running out of attempts without landing above
852    /// the biased tail — the last two share `ERR_ENTROPY_UNAVAILABLE`, so a
853    /// caller cannot tell them apart. Runtime symbol
854    /// `nova_rt_crypto_random_int`
855    /// (`crates/nova-runtime/src/crypto.rs`). Std-only.
856    CryptoRandomInt,
857    /// `time_now_nanos() -> Int` — nanoseconds since the runtime's single
858    /// process epoch, monotonic and never negative.
859    ///
860    /// The only part of `std/time` that cannot be Nova code: `Instant` and
861    /// `Duration` arithmetic is plain multiplication, division and
862    /// subtraction over a private `Int`, so nothing else in §9 needs to cross
863    /// into Rust. Saturates at `i64::MAX` rather than wrapping; see
864    /// `nova_rt_time_now_nanos`. Std-only.
865    TimeNowNanos,
866    /// `time_now_epoch_nanos() -> Int` — nanoseconds since the **Unix**
867    /// epoch, as read by `nova_rt_time_now_epoch_nanos`. Distinct from
868    /// `TimeNowNanos`, which is relative to this process's own start, not
869    /// to 1970. Std-only.
870    TimeNowEpochNanos,
871    /// `log_config_level() -> Int` — the logger's configured threshold, as
872    /// `nova_rt_log_config_level` reads it. Std-only.
873    LogConfigLevel,
874    /// `log_config_to_stderr() -> Int` — `1` for stderr, `0` for stdout, as
875    /// `nova_rt_log_config_to_stderr` reads it. Std-only.
876    LogConfigToStderr,
877    /// `log_set_config(level: Int, to_stderr: Int) -> unit` — install a
878    /// logger configuration, overwriting any previous one, via
879    /// `nova_rt_log_set_config`. Std-only.
880    LogSetConfig,
881    /// `shutdown_requested() -> Bool` — whether the process has been asked to
882    /// stop: the first SIGTERM or SIGINT on Unix, CTRL_BREAK or CTRL_C on
883    /// Windows. The first call installs the handler that sets it, so a
884    /// program that never calls this keeps every signal's default action.
885    /// Read by `std/http`'s `Server::listen` and its connections, once per
886    /// tick. Runtime symbol `nova_rt_shutdown_requested`
887    /// (`crates/nova-runtime/src/signal.rs`, docs/adr/0022). Std-only.
888    ShutdownRequested,
889    /// `process_arg_count() -> Int` — how many arguments the program has,
890    /// counting the program itself at index 0. Runtime symbol
891    /// `nova_rt_process_arg_count` (`crates/nova-runtime/src/process.rs`,
892    /// docs/adr/0023). Std-only.
893    ProcessArgCount,
894    /// `process_arg(i: Int) -> String` — argument `i`, or the empty string
895    /// for an index out of range. Runtime symbol `nova_rt_process_arg`.
896    /// Std-only.
897    ProcessArg,
898    /// `process_exit(code: Int) -> unit` — ends the process with `code`
899    /// after flushing stdout, and never returns. Runtime symbol
900    /// `nova_rt_process_exit`. Std-only.
901    ProcessExit,
902}
903
904impl Builtin {
905    /// The source-level name of the builtin.
906    pub fn name(self) -> &'static str {
907        match self {
908            Builtin::Println => "println",
909            Builtin::Print => "print",
910            Builtin::EPrint => "eprint",
911            Builtin::EPrintln => "eprintln",
912            Builtin::Panic => "panic",
913            Builtin::StrCmp => "str_cmp",
914            Builtin::StrHash => "str_hash",
915            Builtin::CharToInt => "char_to_int",
916            Builtin::StrLenChars => "str_len_chars",
917            Builtin::StrChars => "str_chars",
918            Builtin::StrFromChars => "str_from_chars",
919            Builtin::StrJoin => "str_join",
920            Builtin::StrIndexOf => "str_index_of",
921            Builtin::JsonQuote => "json_quote",
922            Builtin::StrToUpper => "str_to_upper",
923            Builtin::StrToLower => "str_to_lower",
924            Builtin::FloatFixed => "float_fixed",
925            Builtin::StrToFloat => "str_to_float",
926            Builtin::TestSelector => "test_selector",
927            Builtin::TaskSpawn => "task_spawn",
928            Builtin::TaskIsDone => "task_is_done",
929            Builtin::TaskRelease => "task_release",
930            Builtin::TaskDrive => "task_drive",
931            Builtin::TaskOutput => "task_output",
932            Builtin::TaskYieldFuture => "task_yield_future",
933            Builtin::TaskSleepFutureNanos => "task_sleep_future_nanos",
934            Builtin::TaskJoinFuture => "task_join_future",
935            Builtin::TaskTimeoutFuture => "task_timeout_future",
936            Builtin::FsReadToString => "fs_read_to_string",
937            Builtin::FsWriteString => "fs_write_string",
938            Builtin::FsTakeString => "fs_take_string",
939            Builtin::FsLastErrorMessage => "fs_last_error_message",
940            Builtin::FsTempDir => "fs_temp_dir",
941            Builtin::FsExists => "fs_exists",
942            Builtin::FsCreateDir => "fs_create_dir",
943            Builtin::FsCreateDirAll => "fs_create_dir_all",
944            Builtin::FsRemoveFile => "fs_remove_file",
945            Builtin::FsRemoveDirAll => "fs_remove_dir_all",
946            Builtin::FsReadDir => "fs_read_dir",
947            Builtin::FsTakeStringArray => "fs_take_string_array",
948            Builtin::FsKind => "fs_kind",
949            Builtin::FsRead => "fs_read",
950            Builtin::FsTakeBytes => "fs_take_bytes",
951            Builtin::FsWrite => "fs_write",
952            Builtin::FileOpen => "file_open",
953            Builtin::FileClose => "file_close",
954            Builtin::FileRead => "file_read",
955            Builtin::FileWrite => "file_write",
956            Builtin::FileFlush => "file_flush",
957            Builtin::IoStdinRead => "io_stdin_read",
958            Builtin::IoStdoutWrite => "io_stdout_write",
959            Builtin::IoStderrWrite => "io_stderr_write",
960            Builtin::IoStdoutFlush => "io_stdout_flush",
961            Builtin::IoStderrFlush => "io_stderr_flush",
962            Builtin::NetConnect => "net_connect",
963            Builtin::NetClose => "net_close",
964            Builtin::NetRead => "net_read",
965            Builtin::NetWrite => "net_write",
966            Builtin::NetReadTimeout => "net_read_timeout",
967            Builtin::NetListen => "net_listen",
968            Builtin::NetLocalPort => "net_local_port",
969            Builtin::NetAccept => "net_accept",
970            Builtin::BytesLen => "bytes_len",
971            // Deliberately not "bytes_from_string" — see this variant's doc
972            // comment for why that would collide with `std/bytes`'s own
973            // `pub fn bytes_from_string` wrapper.
974            Builtin::BytesFromString => "bytes_from_string_intrinsic",
975            Builtin::BytesIsUtf8 => "bytes_is_utf8",
976            Builtin::BytesToStringUnchecked => "bytes_to_string_unchecked",
977            Builtin::BytesAt => "bytes_at",
978            Builtin::BytesSlice => "bytes_slice",
979            Builtin::BytesConcat => "bytes_concat",
980            Builtin::BytesToInts => "bytes_to_ints",
981            // Deliberately not "bytes_from_ints" — see this variant's doc
982            // comment for why that would collide with `std/bytes`'s own
983            // `pub fn bytes_from_ints` wrapper.
984            Builtin::BytesFromInts => "bytes_from_ints_intrinsic",
985            Builtin::BytesEq => "bytes_eq",
986            Builtin::IntHashSeed => "int_hash_seed",
987            Builtin::HttpParseRequest => "http_parse_request",
988            Builtin::CryptoHash => "crypto_hash",
989            Builtin::CryptoRandomBytes => "crypto_random_bytes",
990            Builtin::CryptoRandomInt => "crypto_random_int",
991            Builtin::TimeNowNanos => "time_now_nanos",
992            Builtin::TimeNowEpochNanos => "time_now_epoch_nanos",
993            Builtin::LogConfigLevel => "log_config_level",
994            Builtin::LogConfigToStderr => "log_config_to_stderr",
995            Builtin::LogSetConfig => "log_set_config",
996            Builtin::ShutdownRequested => "shutdown_requested",
997            Builtin::ProcessArgCount => "process_arg_count",
998            Builtin::ProcessArg => "process_arg",
999            Builtin::ProcessExit => "process_exit",
1000        }
1001    }
1002
1003    /// Builtins injected into *every* module's scope — part of the
1004    /// user-visible language surface, and reserved words everywhere.
1005    pub const GLOBAL: [Builtin; 5] = [
1006        Builtin::Println,
1007        Builtin::Print,
1008        Builtin::EPrint,
1009        Builtin::EPrintln,
1010        Builtin::Panic,
1011    ];
1012
1013    /// Builtins injected only into an std module's own scope (any module in
1014    /// [`STD_MODULES`], not only `std/core`). These are implementation
1015    /// details of the standard library, not user-visible language surface:
1016    /// reserving one of these names in *every* module (as [`Builtin::GLOBAL`]
1017    /// does) would permanently take it away from user code to serve the
1018    /// standard library's own internals. Each member exists because Nova
1019    /// source cannot express what it does, and each is documented on its own
1020    /// variant above, next to the capability it provides and the reason the
1021    /// language cannot reach it. This list is membership only; call sites
1022    /// are a `grep` away and are deliberately not enumerated here: doing so
1023    /// produced stale or wrong counts in three places across three
1024    /// consecutive review rounds (see the Phase 2.2b whole-branch review),
1025    /// because the roster is duplicated information that only this array
1026    /// needs to stay exact.
1027    pub const STD_ONLY: [Builtin; 81] = [
1028        Builtin::StrCmp,
1029        Builtin::StrHash,
1030        Builtin::CharToInt,
1031        Builtin::StrLenChars,
1032        Builtin::StrChars,
1033        Builtin::StrFromChars,
1034        Builtin::StrJoin,
1035        Builtin::StrIndexOf,
1036        Builtin::JsonQuote,
1037        Builtin::StrToUpper,
1038        Builtin::StrToLower,
1039        Builtin::FloatFixed,
1040        Builtin::StrToFloat,
1041        Builtin::TestSelector,
1042        Builtin::TaskSpawn,
1043        Builtin::TaskIsDone,
1044        Builtin::TaskRelease,
1045        Builtin::TaskDrive,
1046        Builtin::TaskOutput,
1047        Builtin::TaskYieldFuture,
1048        Builtin::TaskSleepFutureNanos,
1049        Builtin::TaskJoinFuture,
1050        Builtin::TaskTimeoutFuture,
1051        Builtin::FsReadToString,
1052        Builtin::FsWriteString,
1053        Builtin::FsTakeString,
1054        Builtin::FsLastErrorMessage,
1055        Builtin::FsTempDir,
1056        Builtin::FsExists,
1057        Builtin::FsCreateDir,
1058        Builtin::FsCreateDirAll,
1059        Builtin::FsRemoveFile,
1060        Builtin::FsRemoveDirAll,
1061        Builtin::FsReadDir,
1062        Builtin::FsTakeStringArray,
1063        Builtin::FsKind,
1064        Builtin::FsRead,
1065        Builtin::FsTakeBytes,
1066        Builtin::FsWrite,
1067        Builtin::FileOpen,
1068        Builtin::FileClose,
1069        Builtin::FileRead,
1070        Builtin::FileWrite,
1071        Builtin::FileFlush,
1072        Builtin::IoStdinRead,
1073        Builtin::IoStdoutWrite,
1074        Builtin::IoStderrWrite,
1075        Builtin::IoStdoutFlush,
1076        Builtin::IoStderrFlush,
1077        Builtin::NetConnect,
1078        Builtin::NetClose,
1079        Builtin::NetRead,
1080        Builtin::NetWrite,
1081        Builtin::NetReadTimeout,
1082        Builtin::NetListen,
1083        Builtin::NetLocalPort,
1084        Builtin::NetAccept,
1085        Builtin::BytesLen,
1086        Builtin::BytesFromString,
1087        Builtin::BytesIsUtf8,
1088        Builtin::BytesToStringUnchecked,
1089        Builtin::BytesAt,
1090        Builtin::BytesSlice,
1091        Builtin::BytesConcat,
1092        Builtin::BytesToInts,
1093        Builtin::BytesFromInts,
1094        Builtin::BytesEq,
1095        Builtin::IntHashSeed,
1096        Builtin::HttpParseRequest,
1097        Builtin::CryptoHash,
1098        Builtin::CryptoRandomBytes,
1099        Builtin::CryptoRandomInt,
1100        Builtin::TimeNowNanos,
1101        Builtin::TimeNowEpochNanos,
1102        Builtin::LogConfigLevel,
1103        Builtin::LogConfigToStderr,
1104        Builtin::LogSetConfig,
1105        Builtin::ShutdownRequested,
1106        Builtin::ProcessArgCount,
1107        Builtin::ProcessArg,
1108        Builtin::ProcessExit,
1109    ];
1110}
1111
1112/// Type names the compiler owns, which a user type declaration may not take.
1113///
1114/// `nova-typeck`'s `convert_ty` resolves each of these to its built-in type,
1115/// or to a shadowing generic parameter, wherever it runs — never to a
1116/// declaration under this name, which is what `resolve_type` would have to
1117/// find instead. So a type declared under one of these names could never be
1118/// referred to in a type annotation.
1119///
1120/// That is narrower than "every use". A record literal resolves its head
1121/// through `resolve_type` directly, never through `convert_ty`
1122/// (`check_record_literal`), and a sum type's variants live in the *value*
1123/// namespace, independent of the type name — so construction and pattern
1124/// matching for a type declared under one of these names worked before this
1125/// list was enforced. An `impl` header's self type is a `convert_ty` site
1126/// too, so an `impl` written for one of these names attaches to the
1127/// built-in, never to the declaration. So what worked before this list was
1128/// enforced was construction, pattern matching, and inferred local
1129/// bindings; a signature or an `impl` did not. See `CHANGELOG.md` for
1130/// exactly what this change breaks.
1131///
1132/// `Future` is here and is not a primitive: it is the one built-in type name
1133/// taking a type argument, handled separately from the nullary table.
1134/// `Unit` is deliberately absent — it is not a nameable type name; unit is
1135/// spelled `()`.
1136///
1137/// `Bytes` joined later, for the same reason as the five original
1138/// primitives: an ordinary nullary entry in `convert_ty`'s table, with no
1139/// carve-out of its own like `Future`'s or `Unit`'s above.
1140///
1141/// This is the list, and both `convert_ty`'s and `qualifier_self_ty`'s
1142/// built-in-name tables (`crates/nova-typeck/src/check.rs`) are expected to
1143/// agree with it.
1144pub const RESERVED_TYPE_NAMES: [&str; 7] =
1145    ["Int", "Float", "Bool", "Char", "String", "Future", "Bytes"];
1146
1147/// What kind of definition a [`DefId`] refers to.
1148#[derive(Debug, Clone)]
1149pub enum DefKind {
1150    /// A function; the payload is the index of the item in `File::items`.
1151    Fn { item_index: usize },
1152    /// A sum type declaration with its variants.
1153    Sum {
1154        item_index: usize,
1155        variants: Vec<VariantDef>,
1156    },
1157    /// A record (struct) declaration.
1158    Record { item_index: usize },
1159    /// A constant; payload is the item index.
1160    Const { item_index: usize },
1161    /// A trait declaration.
1162    Trait { item_index: usize },
1163    /// A method: an `ast::Function` living inside an impl block, or a
1164    /// trait's default-method body. `method_index` selects it within the
1165    /// owner (impl `functions`, or the trait's provided items).
1166    Method {
1167        item_index: usize,
1168        method_index: usize,
1169        owner: MethodOwner,
1170    },
1171    /// An `extern` function declaration (a C-ABI import, no Nova body).
1172    /// `fn_index` selects the declaration within the extern block's `items`.
1173    ExternFn { item_index: usize, fn_index: usize },
1174    /// An associated type declared in a trait (`type Item`). Carries its
1175    /// owning trait so a projection can be checked against the right trait
1176    /// without a separate lookup table.
1177    AssocType { trait_def: DefId },
1178}
1179
1180/// Where a [`DefKind::Method`] lives.
1181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1182pub enum MethodOwner {
1183    /// A method defined in an `impl` block (`item_index` → the impl item).
1184    Impl,
1185    /// A default-method body in a `trait` declaration (`item_index` → the
1186    /// trait item, `method_index` → its `functions`-style provided list).
1187    TraitDefault,
1188}
1189
1190/// One variant of a sum type.
1191#[derive(Debug, Clone)]
1192pub struct VariantDef {
1193    pub name: String,
1194    pub span: Span,
1195    /// Number of payload fields (types live in the AST, indexed by the parent).
1196    pub arity: usize,
1197}
1198
1199/// A single top-level definition.
1200#[derive(Debug, Clone)]
1201pub struct Def {
1202    pub name: String,
1203    pub span: Span,
1204    pub kind: DefKind,
1205}
1206
1207/// What a name in expression position resolves to.
1208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1209pub enum Res {
1210    /// A top-level definition.
1211    Def(DefId),
1212    /// Variant `variant_index` of the sum type `DefId`.
1213    Variant(DefId, usize),
1214    /// A compiler builtin.
1215    Builtin(Builtin),
1216}
1217
1218/// Index of a module within a compiled program.
1219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
1220pub struct ModuleId(pub u32);
1221
1222/// One module's visible namespaces: its own items (public and private), the
1223/// implicit std modules (see [`STD_MODULES`]), and names it imports.
1224/// Resolution is performed relative to a module.
1225#[derive(Debug, Default)]
1226struct ModuleScope {
1227    name: String,
1228    values: FxHashMap<String, Res>,
1229    types: FxHashMap<String, DefId>,
1230    traits: FxHashMap<String, DefId>,
1231}
1232
1233/// The item-level namespaces of a whole program (one or more modules).
1234///
1235/// `defs` and `DefId`s are global; name lookups are **module-relative** — a
1236/// name is resolved in the scope of the module that owns the item currently
1237/// being checked (see [`Definitions::module_of`]).
1238#[derive(Debug, Default)]
1239pub struct Definitions {
1240    defs: Vec<Def>,
1241    modules: Vec<ModuleScope>,
1242    /// Merged-item-index → owning module.
1243    item_module: Vec<u32>,
1244    /// Each module's exports, std's included (spec 3.4b §4.6).
1245    exports: Vec<Exports>,
1246    /// What each module could import, by the name an import writes.
1247    importable: Vec<Vec<(String, ModuleId)>>,
1248    /// Each module's package key ([`ModuleSource::package`]).
1249    packages: Vec<Option<u32>>,
1250    /// The first std module's index: the user modules come before it.
1251    std_start: usize,
1252}
1253
1254impl Definitions {
1255    /// All collected definitions, indexable by `DefId.0`.
1256    pub fn defs(&self) -> &[Def] {
1257        &self.defs
1258    }
1259
1260    /// Look up a definition by id.
1261    pub fn def(&self, id: DefId) -> &Def {
1262        &self.defs[id.0 as usize]
1263    }
1264
1265    /// The module that owns the item at `item_index` in the merged file.
1266    pub fn module_of(&self, item_index: usize) -> ModuleId {
1267        ModuleId(self.item_module.get(item_index).copied().unwrap_or(0))
1268    }
1269
1270    /// Number of modules in the program.
1271    pub fn module_count(&self) -> usize {
1272        self.modules.len()
1273    }
1274
1275    /// The name of a module.
1276    pub fn module_name(&self, module: ModuleId) -> &str {
1277        self.modules
1278            .get(module.0 as usize)
1279            .map(|m| m.name.as_str())
1280            .unwrap_or("")
1281    }
1282
1283    /// Resolve a name in *value* position, relative to `module`.
1284    pub fn resolve_value(&self, module: ModuleId, name: &str) -> Option<Res> {
1285        self.modules
1286            .get(module.0 as usize)
1287            .and_then(|m| m.values.get(name).copied())
1288    }
1289
1290    /// Resolve a name in *type* position, relative to `module`.
1291    pub fn resolve_type(&self, module: ModuleId, name: &str) -> Option<DefId> {
1292        self.modules
1293            .get(module.0 as usize)
1294            .and_then(|m| m.types.get(name).copied())
1295    }
1296
1297    /// Resolve a name in *trait* position, relative to `module`.
1298    pub fn resolve_trait(&self, module: ModuleId, name: &str) -> Option<DefId> {
1299        self.modules
1300            .get(module.0 as usize)
1301            .and_then(|m| m.traits.get(name).copied())
1302    }
1303
1304    /// Iterate all method definitions as `(DefId, item_index, method_index,
1305    /// owner)`.
1306    pub fn methods(&self) -> impl Iterator<Item = (DefId, usize, usize, MethodOwner)> + '_ {
1307        self.defs
1308            .iter()
1309            .enumerate()
1310            .filter_map(|(i, d)| match d.kind {
1311                DefKind::Method {
1312                    item_index,
1313                    method_index,
1314                    owner,
1315                } => Some((DefId(i as u32), item_index, method_index, owner)),
1316                _ => None,
1317            })
1318    }
1319
1320    /// Iterate all function definitions as `(DefId, item_index)`.
1321    pub fn functions(&self) -> impl Iterator<Item = (DefId, usize)> + '_ {
1322        self.defs
1323            .iter()
1324            .enumerate()
1325            .filter_map(|(i, d)| match d.kind {
1326                DefKind::Fn { item_index } => Some((DefId(i as u32), item_index)),
1327                _ => None,
1328            })
1329    }
1330
1331    /// Iterate all `extern` function declarations as
1332    /// `(DefId, item_index, fn_index)` (fn_index into the extern block's items).
1333    pub fn extern_functions(&self) -> impl Iterator<Item = (DefId, usize, usize)> + '_ {
1334        self.defs
1335            .iter()
1336            .enumerate()
1337            .filter_map(|(i, d)| match d.kind {
1338                DefKind::ExternFn {
1339                    item_index,
1340                    fn_index,
1341                } => Some((DefId(i as u32), item_index, fn_index)),
1342                _ => None,
1343            })
1344    }
1345}
1346
1347/// What a name in a module's scope is bound to: what completion offers
1348/// (spec `docs/superpowers/specs/2026-10-08-phase-3-2-lsp-core-design.md`
1349/// §4.3).
1350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1351pub enum ScopeEntry {
1352    /// A value: a definition, a variant or a builtin.
1353    Value(Res),
1354    /// A record or a sum type.
1355    Type(DefId),
1356    /// A trait.
1357    Trait(DefId),
1358}
1359
1360impl Definitions {
1361    /// Every name `module` can see, in all three namespaces:
1362    /// - its own items;
1363    /// - its imports;
1364    /// - the builtins;
1365    /// - std's glob-imported names.
1366    ///
1367    /// A module binds each name once per namespace, so a std name the
1368    /// module shadows appears once, as the module's own. The result is
1369    /// sorted by name, then values, types and traits, for a stable order.
1370    pub fn names_in_scope(&self, module: ModuleId) -> Vec<(String, ScopeEntry)> {
1371        let Some(scope) = self.modules.get(module.0 as usize) else {
1372            return Vec::new();
1373        };
1374        let mut out: Vec<(String, ScopeEntry)> = Vec::new();
1375        out.extend(
1376            scope
1377                .values
1378                .iter()
1379                .map(|(n, r)| (n.clone(), ScopeEntry::Value(*r))),
1380        );
1381        out.extend(
1382            scope
1383                .types
1384                .iter()
1385                .map(|(n, d)| (n.clone(), ScopeEntry::Type(*d))),
1386        );
1387        out.extend(
1388            scope
1389                .traits
1390                .iter()
1391                .map(|(n, d)| (n.clone(), ScopeEntry::Trait(*d))),
1392        );
1393        let rank = |e: &ScopeEntry| match e {
1394            ScopeEntry::Value(_) => 0,
1395            ScopeEntry::Type(_) => 1,
1396            ScopeEntry::Trait(_) => 2,
1397        };
1398        out.sort_by(|a, b| a.0.cmp(&b.0).then(rank(&a.1).cmp(&rank(&b.1))));
1399        out
1400    }
1401}
1402
1403/// What a module exports under one name, in each namespace (spec 3.4b
1404/// §4.2).
1405#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1406pub struct Exported {
1407    pub value: Option<Res>,
1408    pub ty: Option<DefId>,
1409    pub trait_def: Option<DefId>,
1410}
1411
1412impl Definitions {
1413    /// The modules `module` could import, by the name an import writes
1414    /// (spec 3.4b §4.6).
1415    pub fn importable(&self, module: ModuleId) -> &[(String, ModuleId)] {
1416        self.importable
1417            .get(module.0 as usize)
1418            .map_or(&[], Vec::as_slice)
1419    }
1420
1421    /// What `module` exports under `name`.
1422    pub fn exported(&self, module: ModuleId, name: &str) -> Exported {
1423        let Some(e) = self.exports.get(module.0 as usize) else {
1424            return Exported::default();
1425        };
1426        Exported {
1427            value: e.values.get(name).copied(),
1428            ty: e.types.get(name).copied(),
1429            trait_def: e.traits.get(name).copied(),
1430        }
1431    }
1432
1433    /// Whether `a` and `b` are modules of one package. A loose program's
1434    /// modules are.
1435    pub fn same_package(&self, a: ModuleId, b: ModuleId) -> bool {
1436        self.packages.get(a.0 as usize) == self.packages.get(b.0 as usize)
1437    }
1438
1439    /// What `module` binds `name` to, in each namespace, other than through
1440    /// std's glob (plan decision 8): a binding equal to some std module's
1441    /// export of that name came through it. Builtins are bound outside std.
1442    pub fn bound_outside_std(&self, module: ModuleId, name: &str) -> Exported {
1443        let Some(scope) = self.modules.get(module.0 as usize) else {
1444            return Exported::default();
1445        };
1446        let std = &self.exports[self.std_start.min(self.exports.len())..];
1447        Exported {
1448            value: scope
1449                .values
1450                .get(name)
1451                .copied()
1452                .filter(|r| !std.iter().any(|e| e.values.get(name) == Some(r))),
1453            ty: scope
1454                .types
1455                .get(name)
1456                .copied()
1457                .filter(|d| !std.iter().any(|e| e.types.get(name) == Some(d))),
1458            trait_def: scope
1459                .traits
1460                .get(name)
1461                .copied()
1462                .filter(|d| !std.iter().any(|e| e.traits.get(name) == Some(d))),
1463        }
1464    }
1465
1466    /// Whether `module` is one of the implicit std modules.
1467    pub fn is_std_module(&self, module: ModuleId) -> bool {
1468        let m = module.0 as usize;
1469        m >= self.std_start && m < self.modules.len()
1470    }
1471
1472    /// Whether a fix may edit `module` (spec 3.4b §3.1; plan decision 19):
1473    /// it is the root package's, whose key is 0, or a loose program's, and
1474    /// not std's.
1475    pub fn owned(&self, module: ModuleId) -> bool {
1476        !self.is_std_module(module)
1477            && matches!(self.packages.get(module.0 as usize), Some(None | Some(0)))
1478    }
1479}
1480
1481/// What an `import` names, by its first segment (spec 3.3a §4.5).
1482#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1483pub enum ImportTarget {
1484    /// The module at this index of [`resolve_program`]'s `modules`.
1485    Module(usize),
1486    /// The driver has already reported this import (E0004, or a
1487    /// dev-dependency imported from `src/`): bind nothing, and say nothing
1488    /// more.
1489    Reported,
1490}
1491
1492/// A source module: a parsed file, its label, and what its imports name.
1493pub struct ModuleSource<'a> {
1494    /// A label for the module. Messages print an import as written, so the
1495    /// label is never a lookup key.
1496    pub name: String,
1497    pub file: &'a File,
1498    /// What each import names, by its first segment. An import missing here
1499    /// is "cannot find module".
1500    pub imports: std::collections::HashMap<String, ImportTarget>,
1501    /// The module's source text, for fixes placed by lines (spec
1502    /// `docs/superpowers/specs/2026-10-10-phase-3-4b-fixes-and-colour-design.md`
1503    /// §4.7). `None` in the resolver's own tests.
1504    pub text: Option<&'a str>,
1505    /// A key equal for two modules of one package; `None` for a loose
1506    /// program, whose files count as one package (spec 3.4b §4.6).
1507    pub package: Option<u32>,
1508    /// Every loaded module this one could import: the name an import of it
1509    /// writes, and its index in `modules` (spec 3.4b §4.6).
1510    pub importable: Vec<(String, usize)>,
1511}
1512
1513impl<'a> ModuleSource<'a> {
1514    /// A module with an empty import table; [`name_imports`] fills it by
1515    /// name.
1516    pub fn new(name: impl Into<String>, file: &'a File) -> Self {
1517        ModuleSource {
1518            name: name.into(),
1519            file,
1520            imports: std::collections::HashMap::new(),
1521            text: None,
1522            package: None,
1523            importable: Vec::new(),
1524        }
1525    }
1526}
1527
1528/// Fill each module's import table by module name, as a program without
1529/// packages has always resolved (ADR 0003): `import m` names the first
1530/// module called `m`. Every other module becomes importable by its name
1531/// (spec 3.4b §4.6).
1532pub fn name_imports(modules: &mut [ModuleSource]) {
1533    let names: Vec<String> = modules.iter().map(|m| m.name.clone()).collect();
1534    for (i, module) in modules.iter_mut().enumerate() {
1535        for item in &module.file.items {
1536            let Item::Import(import) = &item.value else {
1537                continue;
1538            };
1539            let Some(first) = import.path.value.segments.first() else {
1540                continue;
1541            };
1542            if let Some(index) = names.iter().position(|n| *n == first.value) {
1543                module
1544                    .imports
1545                    .insert(first.value.clone(), ImportTarget::Module(index));
1546            }
1547        }
1548        module.importable = names
1549            .iter()
1550            .enumerate()
1551            .filter(|(j, _)| *j != i)
1552            .map(|(j, n)| (n.clone(), j))
1553            .collect();
1554    }
1555}
1556
1557/// The public exports of one module (its `pub` items), used to resolve imports.
1558#[derive(Debug, Default)]
1559struct Exports {
1560    values: FxHashMap<String, Res>,
1561    types: FxHashMap<String, DefId>,
1562    traits: FxHashMap<String, DefId>,
1563}
1564
1565fn push_def(defs: &mut Vec<Def>, def: Def) -> DefId {
1566    let id = DefId(defs.len() as u32);
1567    defs.push(def);
1568    id
1569}
1570
1571/// A well-formed `@test` function, collected by [`collect_item`] in the same
1572/// order [`resolve_program`]'s item walk visits it — source order (modules in
1573/// registration order, items within a module in file order; see
1574/// `collected_tests_are_in_source_order_with_should_panic_attached` in
1575/// `nova-typeck` for why that order must be exact and stable). Task 3 reads
1576/// this to build a dispatching `main` that runs one test per process,
1577/// selected by index; Task 5 reads test names back out of the compiled
1578/// binary, not from this struct directly — so an index and a name can never
1579/// silently disagree between the two only if this order never does either.
1580#[derive(Debug, Clone)]
1581pub struct TestFn {
1582    pub def_id: DefId,
1583    pub name: String,
1584    pub should_panic: bool,
1585}
1586
1587/// Output of [`resolve`]: the namespace table, the merged file whose
1588/// `item_index`es the definitions refer to (the input plus the implicit std
1589/// modules, see [`STD_MODULES`]), and any diagnostics. Downstream stages must
1590/// type-check against this `file`, not the original input, since
1591/// `item_index`es index into it.
1592#[derive(Debug)]
1593pub struct ResolveResult {
1594    pub definitions: Definitions,
1595    pub file: File,
1596    pub diagnostics: Vec<Diagnostic>,
1597    pub tests: Vec<TestFn>,
1598}
1599
1600/// Output of [`resolve_program`]: the merged file, its namespaces, any
1601/// diagnostics, and the `@test` functions collected along the way. The merged
1602/// file's `item_index`es are what `Definitions` refers to, so downstream
1603/// stages consume both together.
1604pub struct ProgramResolution {
1605    pub file: File,
1606    pub definitions: Definitions,
1607    pub diagnostics: Vec<Diagnostic>,
1608    pub tests: Vec<TestFn>,
1609    /// The occurrences of import items, for the language server's index
1610    /// (spec 3.4a §3.3). Always filled; the driver merges them only when
1611    /// asked.
1612    pub imports: Vec<Occurrence>,
1613}
1614
1615/// Collect the item-level namespace of a single file.
1616///
1617/// Reports `E0002` for duplicate definitions in the same namespace. Imports
1618/// and `module` declarations are accepted but ignored in Phase 1 (single-file
1619/// compilation); traits/impls/records are collected in later Phase 1 steps.
1620pub fn resolve(file: &File) -> ResolveResult {
1621    let mut sources = [ModuleSource::new("main", file)];
1622    name_imports(&mut sources);
1623    // No `FileDb` to register the embedded std modules into here, so every
1624    // one of them gets the same `FileId::DUMMY` sentinel used throughout the
1625    // test suite — one per `STD_MODULES` entry, in order.
1626    let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
1627    let prog = resolve_program(&sources, &std_files, None);
1628    ResolveResult {
1629        definitions: prog.definitions,
1630        file: prog.file,
1631        diagnostics: prog.diagnostics,
1632        tests: prog.tests,
1633    }
1634}
1635
1636/// Resolve a multi-module program: collect each module's item namespace,
1637/// enforce `pub` visibility across modules, wire up `import`s, and merge all
1638/// items into one file for whole-program compilation downstream.
1639///
1640/// `std_files` are the [`FileId`]s the caller registered each of
1641/// [`STD_MODULES`]'s sources under, one per entry, in the same order. Only the
1642/// driver owns a `FileDb`, so the ids cannot be invented here; threading them
1643/// through means a syntax error inside an embedded std module is reported
1644/// against a real file instead of [`FileId::DUMMY`]. The single-module
1645/// [`resolve`] wrapper passes `FileId::DUMMY` for each, since it has no
1646/// `FileDb` to register into. Entries beyond `std_files`'s length are simply
1647/// not compiled in (the driver always supplies one id per `STD_MODULES`
1648/// entry, so this is not expected to happen).
1649///
1650/// `extra_std` is `Some((`[`STD_TEST_MODULE`]`, file_id))` under `nova test`
1651/// and `None` otherwise (the driver decides). When present it is treated as
1652/// one more std module — glob-imported into every other module exactly like
1653/// each [`STD_MODULES`] entry, and on the receiving end of their glob-imports too
1654/// (so `assert_eq`'s bounds can name `Eq`/`Debug` with no `import`) — but it
1655/// is threaded through this one parameter rather than folded into
1656/// `STD_MODULES` itself, so that array's length stays fixed regardless of
1657/// compilation mode; every other consumer of `STD_MODULES` (and the driver's
1658/// one-`FileId`-per-entry allocation) is therefore unaffected by its
1659/// existence.
1660///
1661/// `item_index`es in the returned [`ProgramResolution::file`] are global
1662/// (module items concatenated in `modules` order, plus the implicit std
1663/// modules last, in [`STD_MODULES`] order, plus `extra_std` if present).
1664pub fn resolve_program(
1665    modules: &[ModuleSource],
1666    std_files: &[FileId],
1667    extra_std: Option<((&str, &str), FileId)>,
1668) -> ProgramResolution {
1669    let mut definitions = Definitions::default();
1670    let mut diagnostics = Vec::new();
1671    let mut merged = Vec::new();
1672    let mut tests = Vec::new();
1673    let mut imports: Vec<Occurrence> = Vec::new();
1674
1675    // Every `STD_MODULES` entry plus, only when the caller passed one, the
1676    // extra std module — combined here so the rest of this function has one
1677    // uniform list to iterate instead of a special case for the extra entry.
1678    let std_entries: Vec<(&str, &str)> = STD_MODULES
1679        .iter()
1680        .copied()
1681        .chain(extra_std.map(|(entry, _)| entry))
1682        .collect();
1683    let std_file_ids: Vec<FileId> = std_files
1684        .iter()
1685        .copied()
1686        .chain(extra_std.map(|(_, file_id)| file_id))
1687        .collect();
1688
1689    // Compile each implicit std module (in `std_entries` order) so their
1690    // public types/traits/values get real DefIds and layouts (and are then
1691    // glob-imported into every other module below). Each ships with the
1692    // compiler, so a parse failure here is a compiler bug — but it is
1693    // reported against a real file so it is debuggable, and a module that
1694    // fails to parse outright is skipped entirely (`None`) rather than
1695    // silently merged in as empty.
1696    let std_modules: Vec<Option<File>> = std_entries
1697        .iter()
1698        .zip(std_file_ids.iter())
1699        .map(|(&(_, src), &file_id)| {
1700            let (ast, diags) = std_module(src, file_id);
1701            diagnostics.extend(diags);
1702            ast
1703        })
1704        .collect();
1705
1706    // They go *after* all user modules, so user module indices are
1707    // unaffected — module 0 stays the first user module.
1708    let user_count = modules.len();
1709    let all: Vec<ModuleSource> = modules
1710        .iter()
1711        .map(|m| ModuleSource {
1712            name: m.name.clone(),
1713            file: m.file,
1714            imports: m.imports.clone(),
1715            text: m.text,
1716            package: m.package,
1717            importable: m.importable.clone(),
1718        })
1719        .chain(
1720            std_entries
1721                .iter()
1722                .zip(std_modules.iter())
1723                .filter_map(|(&(name, _), file)| {
1724                    file.as_ref()
1725                        .map(|file| ModuleSource::new(name.to_string(), file))
1726                }),
1727        )
1728        .collect();
1729
1730    // The std modules that parsed successfully are exactly the contiguous
1731    // range after the user modules — nothing else is appended after them —
1732    // so "is `mid` *a* std module" (rather than checking against one stored
1733    // "the std module" index) is this one range check.
1734    let std_start = user_count;
1735    let is_std_module = |mid: usize| mid >= std_start;
1736
1737    let mut exports: Vec<Exports> = Vec::new();
1738
1739    // A scope per module, seeded with the compiler builtins. `Builtin::GLOBAL`
1740    // goes into every module; `Builtin::STD_ONLY` goes only into an std
1741    // module's own scope, so none of them ever becomes a reserved word in
1742    // user code (see the `Builtin` doc comments).
1743    for (mid, m) in all.iter().enumerate() {
1744        let mut scope = ModuleScope {
1745            name: m.name.clone(),
1746            ..Default::default()
1747        };
1748        for b in Builtin::GLOBAL {
1749            scope.values.insert(b.name().to_string(), Res::Builtin(b));
1750        }
1751        if is_std_module(mid) {
1752            for b in Builtin::STD_ONLY {
1753                scope.values.insert(b.name().to_string(), Res::Builtin(b));
1754            }
1755        }
1756        definitions.modules.push(scope);
1757        exports.push(Exports::default());
1758    }
1759
1760    // Spec 3.4b §3.1 (plan decision 19): a fix may edit only the root
1761    // package's modules or a loose program's.
1762    let owned = |mid: usize| mid < std_start && matches!(all[mid].package, None | Some(0));
1763
1764    // Pass 1: collect each module's own definitions into its scope + exports.
1765    for (mid, m) in all.iter().enumerate() {
1766        let mut first_value: IndexMap<String, Span> = IndexMap::new();
1767        let mut first_type: IndexMap<String, Span> = IndexMap::new();
1768        let before = diagnostics.len();
1769        for item in &m.file.items {
1770            let item_index = merged.len();
1771            definitions.item_module.push(mid as u32);
1772            collect_item(
1773                &mut definitions.defs,
1774                &mut definitions.modules[mid],
1775                &mut exports[mid],
1776                &mut first_value,
1777                &mut first_type,
1778                &mut diagnostics,
1779                &mut tests,
1780                item_index,
1781                &item.value,
1782            );
1783            merged.push(item.clone());
1784        }
1785        if !owned(mid) {
1786            diagnostics[before..]
1787                .iter_mut()
1788                .for_each(|d| d.fixes.clear());
1789        }
1790    }
1791
1792    // Pass 2: resolve `import`s, binding other modules' public names. What
1793    // each import names is its module's table (spec 3.3a §4.5).
1794    let mut public_fixes: Vec<(usize, usize, String)> = Vec::new();
1795    for (mid, m) in all.iter().enumerate() {
1796        let before = diagnostics.len();
1797        for item in &m.file.items {
1798            if let Item::Import(imp) = &item.value {
1799                resolve_import(
1800                    &mut definitions,
1801                    &exports,
1802                    &all,
1803                    mid,
1804                    imp,
1805                    &mut diagnostics,
1806                    &mut imports,
1807                    &mut public_fixes,
1808                );
1809            }
1810        }
1811        if !owned(mid) {
1812            diagnostics[before..]
1813                .iter_mut()
1814                .for_each(|d| d.fixes.clear());
1815        }
1816    }
1817
1818    // Spec 3.4b §3.2: making an item public must not bind its name into a
1819    // module that already has it, which would be E0002 there. Every module's
1820    // own items and imports are bound by now, and std's glob, which never
1821    // conflicts, has not run.
1822    for (at, target, name) in public_fixes {
1823        let clash = all.iter().enumerate().any(|(g, m)| {
1824            g != target && would_receive(m, target, &name) && binds(&definitions.modules[g], &name)
1825        });
1826        if clash {
1827            diagnostics[at].fixes.clear();
1828        }
1829    }
1830
1831    // Glob each std module's public names into every *other* module — LAST,
1832    // so it is the lowest-priority binding: a name the module already defines
1833    // or imports wins (no conflict), and only otherwise-unbound names fall
1834    // through to a std module. A program may thus define or import its own
1835    // `Option`/`Result`/`Vec`. This also runs std-into-std — std/core's
1836    // exports glob into std/collections's scope exactly as they would into a
1837    // user module's, which is what lets std/collections use
1838    // `Option`/`Some`/`None` with no `import` of its own.
1839    for (mid, std_exports) in exports.iter().enumerate().skip(std_start) {
1840        import_std_module(&mut definitions, std_exports, mid);
1841    }
1842
1843    // Spec 3.4b §4.6: what a fix needs to know.
1844    definitions.importable = all
1845        .iter()
1846        .map(|m| {
1847            m.importable
1848                .iter()
1849                .map(|(n, i)| (n.clone(), ModuleId(*i as u32)))
1850                .collect()
1851        })
1852        .collect();
1853    definitions.packages = all.iter().map(|m| m.package).collect();
1854    definitions.std_start = std_start;
1855    definitions.exports = exports;
1856
1857    ProgramResolution {
1858        file: File { items: merged },
1859        definitions,
1860        diagnostics,
1861        tests,
1862        imports,
1863    }
1864}
1865
1866/// The embedded standard-library modules, compiled as implicit modules and
1867/// glob-imported into every user module. Order is significant only in that it
1868/// fixes module indices; user modules always come first, then these in the
1869/// order listed here — `std/core` stays first so its module index is
1870/// unchanged from when it was the only embedded module. The sources come
1871/// from the `nova-std` crate, which embeds `std/` at build time through
1872/// paths inside its own package, so the compiler stays a single
1873/// self-contained executable and `cargo package` keeps the files. Each name
1874/// is `$std.*`, not a valid identifier, so it can never collide with a user
1875/// module name or be named in an `import`.
1876pub const STD_MODULES: [(&str, &str); 16] = [
1877    ("$std.core", nova_std::CORE),
1878    ("$std.bytes", nova_std::BYTES),
1879    ("$std.io", nova_std::IO),
1880    ("$std.fs", nova_std::FS),
1881    ("$std.collections", nova_std::COLLECTIONS),
1882    ("$std.strings", nova_std::STRINGS),
1883    ("$std.fmt", nova_std::FMT),
1884    ("$std.task", nova_std::TASK),
1885    ("$std.sync", nova_std::SYNC),
1886    ("$std.net", nova_std::NET),
1887    ("$std.time", nova_std::TIME),
1888    ("$std.json", nova_std::JSON),
1889    ("$std.log", nova_std::LOG),
1890    ("$std.http", nova_std::HTTP),
1891    ("$std.crypto", nova_std::CRYPTO),
1892    ("$std.process", nova_std::PROCESS),
1893];
1894
1895/// `std/test`, seeded only under `nova test`. Kept out of [`STD_MODULES`] so
1896/// that array's length does not depend on compilation mode: every consumer
1897/// iterates it and the driver allocates one `FileId` per entry, so a
1898/// conditional member would make `FileId` allocation depend on which
1899/// subcommand is running.
1900pub const STD_TEST_MODULE: (&str, &str) = ("$std.test", nova_std::TEST);
1901
1902/// Lex and parse one embedded std module. Its source ships with the compiler,
1903/// so any failure is a compiler bug — but it is reported against `file_id` so
1904/// it is debuggable rather than silently dropped. Returns `None` when parsing
1905/// fails outright (nothing to merge in as a module); the caller still gets
1906/// the diagnostics either way.
1907fn std_module(src: &str, file_id: FileId) -> (Option<File>, Vec<Diagnostic>) {
1908    let (tokens, lex_errors) = nova_lexer::lex(src, file_id);
1909    let mut diags: Vec<Diagnostic> = lex_errors
1910        .iter()
1911        .map(|e| Diagnostic::error("L0001", e.to_string()).with_primary_label(e.span(), "here"))
1912        .collect();
1913    let (ast, parse_errors) = nova_parser::parse(&tokens, file_id);
1914    diags.extend(
1915        parse_errors.iter().map(|e| {
1916            Diagnostic::error("P0001", e.to_string()).with_primary_label(e.span(), "here")
1917        }),
1918    );
1919    (ast, diags)
1920}
1921
1922/// Bind one std module's public names into every *other* module's scope,
1923/// leaving any name a module already defines or imports untouched (a user —
1924/// or another std module's own — item shadows a std one). Called once per
1925/// entry in [`STD_MODULES`], so this also runs std-into-std (e.g. std/core's
1926/// `Option` into std/collections's scope), which is what lets std/collections
1927/// use `Option`/`Some`/`None` with no `import` of its own.
1928fn import_std_module(definitions: &mut Definitions, std_exports: &Exports, std_mid: usize) {
1929    let values: Vec<(String, Res)> = std_exports
1930        .values
1931        .iter()
1932        .map(|(k, v)| (k.clone(), *v))
1933        .collect();
1934    let types: Vec<(String, DefId)> = std_exports
1935        .types
1936        .iter()
1937        .map(|(k, v)| (k.clone(), *v))
1938        .collect();
1939    let traits: Vec<(String, DefId)> = std_exports
1940        .traits
1941        .iter()
1942        .map(|(k, v)| (k.clone(), *v))
1943        .collect();
1944    for mid in 0..definitions.modules.len() {
1945        if mid == std_mid {
1946            continue;
1947        }
1948        let scope = &mut definitions.modules[mid];
1949        for (n, r) in &values {
1950            scope.values.entry(n.clone()).or_insert(*r);
1951        }
1952        for (n, id) in &types {
1953            scope.types.entry(n.clone()).or_insert(*id);
1954        }
1955        for (n, id) in &traits {
1956            scope.traits.entry(n.clone()).or_insert(*id);
1957        }
1958    }
1959}
1960
1961/// Collect one item's definitions into `scope` (and `exp` if `pub`), pushing
1962/// global `Def`s into `defs`.
1963///
1964/// Also validates the item's `attrs` (Task 2): an unknown attribute name is
1965/// always `E0082`, `@test` on anything other than a function is `E0083`, a
1966/// `@test` function that is `async` or has parameters, generics, or a
1967/// non-`Unit` return is `E0084`, and an unknown `@test(...)` argument is
1968/// `E0085` — never silently ignored, so a mistyped `@tset` cannot compile into
1969/// a function that looks like a test and never runs as one. A well-formed
1970/// `@test` function is pushed onto `tests`.
1971///
1972/// Every `Item` variant carries `attrs` and is validated here, `import`,
1973/// `module` and `extern` included (fix round 1: `@tset` before an `extern`
1974/// block used to compile with zero diagnostics, since neither the AST nor
1975/// the parser gave those three item kinds anywhere to put a parsed
1976/// attribute — see `nova-ast`'s `Import`/`Module`/`ExternBlock` and
1977/// `nova-parser`'s `try_parse_item`). The rule is now uniform: every item may
1978/// carry attributes, and validation — not the AST shape — decides what is
1979/// legal, so there is no item kind left where a silent drop is possible.
1980#[allow(clippy::too_many_arguments)]
1981fn collect_item(
1982    defs: &mut Vec<Def>,
1983    scope: &mut ModuleScope,
1984    exp: &mut Exports,
1985    first_value: &mut IndexMap<String, Span>,
1986    first_type: &mut IndexMap<String, Span>,
1987    diagnostics: &mut Vec<Diagnostic>,
1988    tests: &mut Vec<TestFn>,
1989    item_index: usize,
1990    item: &Item,
1991) {
1992    match item {
1993        Item::Function(f) => {
1994            let name = f.name.value.clone();
1995            let span = f.name.span;
1996            let id = push_def(
1997                defs,
1998                Def {
1999                    name: name.clone(),
2000                    span,
2001                    kind: DefKind::Fn { item_index },
2002                },
2003            );
2004            insert_value(
2005                scope,
2006                exp,
2007                first_value,
2008                diagnostics,
2009                name,
2010                span,
2011                Res::Def(id),
2012                is_pub(f.vis),
2013            );
2014            validate_test_function(f, id, diagnostics, tests);
2015        }
2016        Item::Type(t) => {
2017            validate_attrs_reject_test(&t.attrs, "a type declaration", diagnostics);
2018            match &t.def {
2019                TypeDef::Sum(variants) => {
2020                    let name = t.name.value.clone();
2021                    let span = t.name.span;
2022                    if reject_reserved_type_name(diagnostics, &name, span) {
2023                        return;
2024                    }
2025                    let pubv = is_pub(t.vis);
2026                    let variant_defs: Vec<VariantDef> = variants
2027                        .iter()
2028                        .map(|v| VariantDef {
2029                            name: v.name.value.clone(),
2030                            span: v.name.span,
2031                            arity: v.fields.len(),
2032                        })
2033                        .collect();
2034                    let id = push_def(
2035                        defs,
2036                        Def {
2037                            name: name.clone(),
2038                            span,
2039                            kind: DefKind::Sum {
2040                                item_index,
2041                                variants: variant_defs,
2042                            },
2043                        },
2044                    );
2045                    insert_type(scope, exp, first_type, diagnostics, name, span, id, pubv);
2046                    // Variants live in the value namespace and inherit the type's
2047                    // visibility, so `Some(x)` / `Circle(1.0)` resolve unprefixed.
2048                    for (vi, v) in variants.iter().enumerate() {
2049                        insert_value(
2050                            scope,
2051                            exp,
2052                            first_value,
2053                            diagnostics,
2054                            v.name.value.clone(),
2055                            v.name.span,
2056                            Res::Variant(id, vi),
2057                            pubv,
2058                        );
2059                    }
2060                }
2061                TypeDef::Alias(_) => {
2062                    diagnostics.push(unsupported(
2063                        t.name.span,
2064                        "type aliases are not supported yet in the Phase 1 compiler",
2065                    ));
2066                }
2067            }
2068        }
2069        Item::Const(c) => {
2070            validate_attrs_reject_test(&c.attrs, "a const", diagnostics);
2071            let name = c.name.value.clone();
2072            let span = c.name.span;
2073            let id = push_def(
2074                defs,
2075                Def {
2076                    name: name.clone(),
2077                    span,
2078                    kind: DefKind::Const { item_index },
2079                },
2080            );
2081            insert_value(
2082                scope,
2083                exp,
2084                first_value,
2085                diagnostics,
2086                name,
2087                span,
2088                Res::Def(id),
2089                is_pub(c.vis),
2090            );
2091        }
2092        Item::Record(r) => {
2093            validate_attrs_reject_test(&r.attrs, "a record", diagnostics);
2094            let name = r.name.value.clone();
2095            let span = r.name.span;
2096            if reject_reserved_type_name(diagnostics, &name, span) {
2097                return;
2098            }
2099            let id = push_def(
2100                defs,
2101                Def {
2102                    name: name.clone(),
2103                    span,
2104                    kind: DefKind::Record { item_index },
2105                },
2106            );
2107            insert_type(
2108                scope,
2109                exp,
2110                first_type,
2111                diagnostics,
2112                name,
2113                span,
2114                id,
2115                is_pub(r.vis),
2116            );
2117        }
2118        Item::Trait(t) => {
2119            validate_attrs_reject_test(&t.attrs, "a trait", diagnostics);
2120            let name = t.name.value.clone();
2121            let span = t.name.span;
2122            let pubv = is_pub(t.vis);
2123            let id = push_def(
2124                defs,
2125                Def {
2126                    name: name.clone(),
2127                    span,
2128                    kind: DefKind::Trait { item_index },
2129                },
2130            );
2131            if scope.traits.contains_key(&name) {
2132                diagnostics.push(
2133                    Diagnostic::error("E0002", format!("duplicate trait `{name}`"))
2134                        .with_primary_label(span, "redefined here"),
2135                );
2136            } else {
2137                scope.traits.insert(name.clone(), id);
2138                if pubv {
2139                    exp.traits.insert(name, id);
2140                }
2141            }
2142            // Default-method bodies become their own method defs (global);
2143            // each associated type gets its own `DefId` too, named after
2144            // itself. A `match` (not `if let`) on purpose: unlike a
2145            // non-exhaustive `if let`, this is not something `cargo check`
2146            // can force open when a new `TraitItem` variant is added, so it
2147            // must already cover every arm.
2148            for (method_index, ti) in t.items.iter().enumerate() {
2149                match ti {
2150                    nova_ast::item::TraitItem::Provided(f) => {
2151                        push_def(
2152                            defs,
2153                            Def {
2154                                name: format!("{}::{}$default", t.name.value, f.name.value),
2155                                span: f.name.span,
2156                                kind: DefKind::Method {
2157                                    item_index,
2158                                    method_index,
2159                                    owner: MethodOwner::TraitDefault,
2160                                },
2161                            },
2162                        );
2163                    }
2164                    nova_ast::item::TraitItem::AssocType { name: at_name, .. } => {
2165                        // Not inserted into `scope.types`: an associated type
2166                        // is not a type name you can write bare, only
2167                        // reachable through a projection (`Self::Item`).
2168                        push_def(
2169                            defs,
2170                            Def {
2171                                name: at_name.value.clone(),
2172                                span: at_name.span,
2173                                kind: DefKind::AssocType { trait_def: id },
2174                            },
2175                        );
2176                    }
2177                    nova_ast::item::TraitItem::Required(_) => {}
2178                }
2179            }
2180        }
2181        Item::Impl(i) => {
2182            validate_attrs_reject_test(&i.attrs, "an impl block", diagnostics);
2183            // Impls are globally coherent — collected program-wide, resolved by
2184            // type rather than by name, so they are not bound into any scope.
2185            let self_name = type_full_name(&i.ty.value);
2186            for (method_index, f) in i.functions.iter().enumerate() {
2187                let mangled = match &i.trait_ {
2188                    Some(tr) => format!("{}.{}.{}", self_name, path_tail(&tr.value), f.name.value),
2189                    None => format!("{}.{}", self_name, f.name.value),
2190                };
2191                push_def(
2192                    defs,
2193                    Def {
2194                        name: mangled,
2195                        span: f.name.span,
2196                        kind: DefKind::Method {
2197                            item_index,
2198                            method_index,
2199                            owner: MethodOwner::Impl,
2200                        },
2201                    },
2202                );
2203            }
2204        }
2205        Item::Extern(block) => {
2206            validate_attrs_reject_test(&block.attrs, "an extern block", diagnostics);
2207            // Each declaration binds as a callable value under its (unmangled)
2208            // C symbol name. ABI / signature validity is checked in typeck.
2209            // Extern fns are module-private (the grammar has no `pub` on them).
2210            for (fn_index, ext_item) in block.items.iter().enumerate() {
2211                let ExternItem::Fn(sig) = ext_item;
2212                let name = sig.name.value.clone();
2213                let span = sig.name.span;
2214                let id = push_def(
2215                    defs,
2216                    Def {
2217                        name: name.clone(),
2218                        span,
2219                        kind: DefKind::ExternFn {
2220                            item_index,
2221                            fn_index,
2222                        },
2223                    },
2224                );
2225                insert_value(
2226                    scope,
2227                    exp,
2228                    first_value,
2229                    diagnostics,
2230                    name,
2231                    span,
2232                    Res::Def(id),
2233                    false,
2234                );
2235            }
2236        }
2237        Item::Import(imp) => {
2238            validate_attrs_reject_test(&imp.attrs, "an import", diagnostics);
2239            // Otherwise handled in pass 2.
2240        }
2241        Item::Module(m) => {
2242            validate_attrs_reject_test(&m.attrs, "a module declaration", diagnostics);
2243            // Carries no items of its own to collect.
2244        }
2245    }
2246}
2247
2248/// `E0089`: `name` is one of [`RESERVED_TYPE_NAMES`]. Called from the sum-type
2249/// and record arms of [`collect_item`] — the two forms that introduce a type
2250/// name — before either pushes a `Def` or inserts into the type namespace, so
2251/// a rejected declaration registers nothing: it does not occupy a `DefId`, is
2252/// not collected by any later pass, and cannot collide with a genuine
2253/// duplicate of the same name. Two declarations under the same reserved name
2254/// each raise their own `E0089` rather than the second colliding as `E0002`
2255/// — for the same reason: neither one is ever registered, so there is
2256/// nothing on record for a second occurrence to collide with.
2257///
2258/// "Not collected by any later pass" holds only because every such pass in
2259/// `nova-typeck` walks `Definitions::defs()` rather than the AST directly, so
2260/// skipping `push_def` here is sufficient — today. That is a cross-crate
2261/// invariant this function's ordering relies on but cannot enforce; nothing
2262/// stops a future pass from walking items directly instead. The same drift
2263/// class this module already flags for `convert_ty`'s and `qualifier_self_ty`'s
2264/// tables (see [`RESERVED_TYPE_NAMES`]'s own doc): a comment, not a compiler
2265/// backstop.
2266fn reject_reserved_type_name(diagnostics: &mut Vec<Diagnostic>, name: &str, span: Span) -> bool {
2267    if !RESERVED_TYPE_NAMES.contains(&name) {
2268        return false;
2269    }
2270    diagnostics.push(
2271        Diagnostic::error(
2272            "E0089",
2273            format!(
2274                "`{name}` is already a built-in type; a type declared under \
2275                 this name could never be referred to in a type annotation, \
2276                 since `{name}` in a type position never resolves to a \
2277                 declaration under this name"
2278            ),
2279        )
2280        .with_primary_label(span, "redeclares a built-in type name"),
2281    );
2282    true
2283}
2284
2285/// Attribute names the compiler recognizes. An attribute whose name is not in
2286/// this list is always `E0082` — parsing an attribute and then enforcing
2287/// nothing about its name is exactly the failure mode `@test` must not be
2288/// allowed: a misspelled `@tset` that compiled would define a function that
2289/// looks like a test and never runs as one.
2290const KNOWN_ATTRIBUTES: &[&str] = &["test"];
2291
2292/// Arguments `@test(...)` accepts. Anything else is `E0085`.
2293const KNOWN_TEST_ARGS: &[&str] = &["should_panic"];
2294
2295/// The `E0084` note attached when a `@test` function is `async`
2296/// (see [`test_shape_violations`]).
2297///
2298/// A note rather than a longer message because it has to be *actionable on its
2299/// own*: the whole point of rejecting the shape is that the user's next edit
2300/// should be the working one, so the replacement pattern is spelled out as
2301/// source rather than described, and `block_on`'s role in it is named.
2302const TEST_ASYNC_NOTE: &str = "an `async fn` returns a future, and a `@test` function's result is \
2303     discarded rather than awaited, so an `async` test's body would never run; \
2304     write the test as a synchronous one that drives the future itself, e.g. \
2305     `@test fn t() { assert_eq(block_on(f()), 42) }` — `block_on` runs `f()`'s \
2306     future to completion and produces its value";
2307
2308/// `E0082`: `attr`'s name is not in [`KNOWN_ATTRIBUTES`]. Shared by every
2309/// item kind that can carry attributes, so a typo is caught the same way no
2310/// matter what it is attached to.
2311///
2312/// Labels `attr.name.span`, never `attr.span`: `attr.span` (see
2313/// `nova-parser`'s `parse_attributes`) is set to the span of the leading `@`
2314/// token alone — one byte wide, never covering the name that follows it — so
2315/// underlining it would point at the `@` and leave the actual misspelled
2316/// word unmarked. `attr.name` is a separate `Spanned<String>` built by
2317/// `parse_ident`, with its own accurate span over just the identifier text,
2318/// and is what every diagnostic in this file uses instead. `attr.span` is
2319/// read nowhere in this module.
2320fn unknown_attribute(attr: &Attribute) -> Diagnostic {
2321    Diagnostic::error(
2322        "E0082",
2323        format!(
2324            "unknown attribute `@{}`; known attributes are: {}",
2325            attr.name.value,
2326            KNOWN_ATTRIBUTES.join(", "),
2327        ),
2328    )
2329    .with_primary_label(attr.name.span, "unknown attribute")
2330}
2331
2332/// Validate the attributes of an item that can never itself be a test: every
2333/// item kind except a function (record, type declaration, trait, impl block,
2334/// const, import, module, extern block — see [`validate_test_function`] for
2335/// the function case). An unknown name is still `E0082`; `@test` on one of
2336/// these is `E0083`, since only a function can ever run as a test. `kind`
2337/// names the item for the message and already carries its article (e.g.
2338/// `"a record"`, `"an impl block"`).
2339fn validate_attrs_reject_test(attrs: &[Attribute], kind: &str, diagnostics: &mut Vec<Diagnostic>) {
2340    for attr in attrs {
2341        if attr.name.value == "test" {
2342            diagnostics.push(
2343                Diagnostic::error(
2344                    "E0083",
2345                    format!("`@test` can only be applied to a function, not {kind}"),
2346                )
2347                .with_primary_label(attr.name.span, "not a function"),
2348            );
2349        } else if !KNOWN_ATTRIBUTES.contains(&attr.name.value.as_str()) {
2350            diagnostics.push(unknown_attribute(attr));
2351        }
2352    }
2353}
2354
2355/// Validate a function's attributes and, if it carries a well-formed `@test`,
2356/// push a [`TestFn`] onto `tests`. `def_id` is the caller's already-collected
2357/// `DefId` for `f` (from its own `push_def`), so a rejected `@test` still
2358/// leaves the function itself resolved and callable like any other —
2359/// rejection refuses only its *test-ness*, never its existence in the
2360/// namespace.
2361///
2362/// Pushes onto `tests` in the order [`resolve_program`]'s item walk visits
2363/// this function, which is source order — see [`TestFn`]'s doc comment for
2364/// why that must hold exactly and
2365/// `collected_tests_are_in_source_order_with_should_panic_attached` in
2366/// `nova-typeck` for the test that pins it.
2367///
2368/// Pushes **at most one** `TestFn`, even if `f` carries more than one
2369/// well-formed `@test` (syntactically legal — `f.attrs` is a plain `Vec`
2370/// with no uniqueness constraint on `name`, the same reason `@derive(...)`
2371/// and `@test` can coexist on one function). Without the `collected` guard
2372/// below, `@test\n@test\nfn t() {}` would push the same `def_id`/name onto
2373/// `tests` twice, and `nova test` would list and run one function as if it
2374/// were two different tests. The first well-formed `@test`, in attribute
2375/// order, is the one whose `should_panic` wins; later ones are still
2376/// validated (an invalid argument or bad shape on a second `@test` still
2377/// reports its own diagnostic) but never collected again.
2378fn validate_test_function(
2379    f: &Function,
2380    def_id: DefId,
2381    diagnostics: &mut Vec<Diagnostic>,
2382    tests: &mut Vec<TestFn>,
2383) {
2384    let mut collected = false;
2385    for attr in &f.attrs {
2386        if attr.name.value != "test" {
2387            // Spec 3.4b §4.3: on a function only, since `@test` on anything
2388            // else is E0083 (decision 38).
2389            let mut d = unknown_attribute(attr);
2390            if let Some(fix) = suggest::change_to(&attr.name, KNOWN_ATTRIBUTES.iter().copied()) {
2391                d = d.with_fix(fix);
2392            }
2393            diagnostics.push(d);
2394            continue;
2395        }
2396        let mut well_formed = true;
2397        let mut should_panic = false;
2398        for arg in &attr.args {
2399            if arg.value == "should_panic" {
2400                should_panic = true;
2401            } else {
2402                let mut d = Diagnostic::error(
2403                    "E0085",
2404                    format!(
2405                        "unknown `@test` argument `{}`; the accepted arguments are: {}",
2406                        arg.value,
2407                        KNOWN_TEST_ARGS.join(", "),
2408                    ),
2409                )
2410                .with_primary_label(arg.span, "unknown argument");
2411                if let Some(fix) = suggest::change_to(arg, KNOWN_TEST_ARGS.iter().copied()) {
2412                    d = d.with_fix(fix);
2413                }
2414                diagnostics.push(d);
2415                well_formed = false;
2416            }
2417        }
2418        let violations = test_shape_violations(f);
2419        if !violations.is_empty() {
2420            let names: Vec<&str> = violations.iter().map(|(name, _)| *name).collect();
2421            let mut diag = Diagnostic::error(
2422                "E0084",
2423                format!("a `@test` function may not have {}", names.join(", ")),
2424            )
2425            .with_primary_label(f.name.span, "invalid test signature");
2426            for (name, span) in &violations {
2427                if let Some(span) = span {
2428                    diag = diag.with_secondary_label(*span, format!("{name} here"));
2429                }
2430            }
2431            if f.is_async {
2432                diag = diag.with_note(TEST_ASYNC_NOTE);
2433            }
2434            diagnostics.push(diag);
2435            well_formed = false;
2436        }
2437        if well_formed && !collected {
2438            tests.push(TestFn {
2439                def_id,
2440                name: f.name.value.clone(),
2441                should_panic,
2442            });
2443            collected = true;
2444        }
2445    }
2446}
2447
2448/// Ways a function's signature disqualifies it from `@test` (`E0084`): an
2449/// `async` body, parameters, generics, or an explicit non-`Unit` return type. A
2450/// test is called with no arguments and its result is discarded, so there is no
2451/// caller that could ever supply the one or consume the other. A missing
2452/// return type and an explicit `-> ()` are both `Unit` and neither is a
2453/// violation — only an explicit *non*-`Unit` return type is.
2454///
2455/// `async` is rejected for the same reason and one more: an `async fn` returns
2456/// a future, and a discarded future is never awaited, so the body of an
2457/// `async` `@test` could not run at all. Rejecting it is what the `nova test`
2458/// design specified from the start (`@test fn t() { block_on(f()) }` is the
2459/// supported way to test async code); accepting it silently is what
2460/// `TEST_ASYNC_NOTE` exists to steer a user out of.
2461///
2462/// The `async` violation carries no span because the AST records `is_async` as
2463/// a bare `bool` with no span of its own, and the `async` keyword precedes the
2464/// name, so `f.name.span` would point a label at the wrong token. `None` is
2465/// therefore "no better span than the primary label's", not "no location".
2466fn test_shape_violations(f: &Function) -> Vec<(&'static str, Option<Span>)> {
2467    let mut violations = Vec::new();
2468    if f.is_async {
2469        violations.push(("an `async` body", None));
2470    }
2471    if let (Some(first), Some(last)) = (f.params.first(), f.params.last()) {
2472        violations.push(("parameters", Some(first.name.span.merge(last.ty.span))));
2473    }
2474    if let (Some(first), Some(last)) = (f.generics.first(), f.generics.last()) {
2475        violations.push((
2476            "generic parameters",
2477            Some(first.name.span.merge(last.name.span)),
2478        ));
2479    }
2480    if let Some(rt) = &f.return_ty {
2481        if !matches!(&rt.value, Type::Tuple(items) if items.is_empty()) {
2482            violations.push(("a non-Unit return type", Some(rt.span)));
2483        }
2484    }
2485    violations
2486}
2487
2488/// Bind another module's public names into the importing module's scope.
2489/// Each "make public" fix it offers is recorded in `public_fixes`: the
2490/// diagnostic's index, the target module and the name (spec 3.4b §4.4).
2491#[allow(clippy::too_many_arguments)]
2492fn resolve_import(
2493    definitions: &mut Definitions,
2494    exports: &[Exports],
2495    modules: &[ModuleSource],
2496    mid: usize,
2497    imp: &Import,
2498    diagnostics: &mut Vec<Diagnostic>,
2499    occurrences: &mut Vec<Occurrence>,
2500    public_fixes: &mut Vec<(usize, usize, String)>,
2501) {
2502    let span = imp.path.span;
2503    // Only single-segment module imports are supported. A qualified or nested
2504    // path (`a::b`) would otherwise silently drop its leading segments and bind
2505    // an unrelated module; reject it explicitly like `import ... as`.
2506    if imp.path.value.segments.len() > 1 {
2507        diagnostics.push(unsupported(
2508            span,
2509            "qualified or nested import paths (`a::b`) are not supported yet; \
2510             import a top-level module by its name",
2511        ));
2512        return;
2513    }
2514    let target_name = imp
2515        .path
2516        .value
2517        .segments
2518        .last()
2519        .map(|s| s.value.as_str())
2520        .unwrap_or("");
2521    let target = match modules[mid].imports.get(target_name) {
2522        Some(ImportTarget::Module(target)) => *target,
2523        Some(ImportTarget::Reported) => return,
2524        None => {
2525            diagnostics.push(
2526                Diagnostic::error("E0001", format!("cannot find module `{target_name}`"))
2527                    .with_primary_label(span, "no such module"),
2528            );
2529            return;
2530        }
2531    };
2532    if target == mid {
2533        diagnostics.push(
2534            Diagnostic::error("E0001", format!("module `{target_name}` imports itself"))
2535                .with_primary_label(span, "self-import"),
2536        );
2537        return;
2538    }
2539    // Spec 3.4a §3.3: the module, at its own segment. Only one-segment
2540    // paths reach here, and `path.span` would also cover a list's `::`.
2541    if let Some(first) = imp.path.value.segments.first() {
2542        occurrences.push(Occurrence {
2543            span: first.span,
2544            role: Role::Use,
2545            target: Target::Module(ModuleId(target as u32)),
2546            shorthand: false,
2547        });
2548    }
2549    match &imp.kind {
2550        ImportKind::Simple => {
2551            // Glob: bring all of the target module's public names into scope.
2552            let names: Vec<(String, Res)> = exports[target]
2553                .values
2554                .iter()
2555                .map(|(k, v)| (k.clone(), *v))
2556                .collect();
2557            for (n, r) in names {
2558                bind_value(&mut definitions.modules[mid], diagnostics, n, span, r);
2559            }
2560            let tys: Vec<(String, DefId)> = exports[target]
2561                .types
2562                .iter()
2563                .map(|(k, v)| (k.clone(), *v))
2564                .collect();
2565            for (n, id) in tys {
2566                bind_type(&mut definitions.modules[mid], diagnostics, n, span, id);
2567            }
2568            let trs: Vec<(String, DefId)> = exports[target]
2569                .traits
2570                .iter()
2571                .map(|(k, v)| (k.clone(), *v))
2572                .collect();
2573            for (n, id) in trs {
2574                bind_trait(&mut definitions.modules[mid], diagnostics, n, span, id);
2575            }
2576        }
2577        ImportKind::List(names) => {
2578            for n in names {
2579                let name = &n.value;
2580                let mut found = false;
2581                if let Some(r) = exports[target].values.get(name).copied() {
2582                    bind_value(
2583                        &mut definitions.modules[mid],
2584                        diagnostics,
2585                        name.clone(),
2586                        n.span,
2587                        r,
2588                    );
2589                    let named = match r {
2590                        Res::Def(id) => Target::Def(id),
2591                        Res::Variant(sum, i) => Target::Variant(sum, i as u32),
2592                        Res::Builtin(b) => Target::Builtin(b),
2593                    };
2594                    occurrences.push(Occurrence {
2595                        span: n.span,
2596                        role: Role::Use,
2597                        target: named,
2598                        shorthand: false,
2599                    });
2600                    found = true;
2601                }
2602                if let Some(id) = exports[target].types.get(name).copied() {
2603                    bind_type(
2604                        &mut definitions.modules[mid],
2605                        diagnostics,
2606                        name.clone(),
2607                        n.span,
2608                        id,
2609                    );
2610                    occurrences.push(Occurrence {
2611                        span: n.span,
2612                        role: Role::Use,
2613                        target: Target::Def(id),
2614                        shorthand: false,
2615                    });
2616                    found = true;
2617                }
2618                if let Some(id) = exports[target].traits.get(name).copied() {
2619                    bind_trait(
2620                        &mut definitions.modules[mid],
2621                        diagnostics,
2622                        name.clone(),
2623                        n.span,
2624                        id,
2625                    );
2626                    occurrences.push(Occurrence {
2627                        span: n.span,
2628                        role: Role::Use,
2629                        target: Target::Def(id),
2630                        shorthand: false,
2631                    });
2632                    found = true;
2633                }
2634                if !found {
2635                    let mut diag = Diagnostic::error(
2636                        "E0001",
2637                        format!("`{name}` is not a public item of module `{target_name}`"),
2638                    )
2639                    .with_primary_label(n.span, "not found or not `pub`");
2640                    let same_package = modules[mid].package == modules[target].package;
2641                    if let Some(fix) =
2642                        make_public(&modules[target], name, target_name, same_package)
2643                    {
2644                        diag = diag.with_fix(fix);
2645                        public_fixes.push((diagnostics.len(), target, name.clone()));
2646                    }
2647                    diagnostics.push(diag);
2648                }
2649            }
2650        }
2651        ImportKind::Alias(_) => {
2652            diagnostics.push(unsupported(
2653                span,
2654                "`import ... as` aliases are not supported yet",
2655            ));
2656        }
2657    }
2658}
2659
2660fn is_pub(vis: nova_ast::item::Visibility) -> bool {
2661    matches!(vis, nova_ast::item::Visibility::Pub)
2662}
2663
2664/// "make `x` public in `m`" (spec 3.4b §4.4): `pub ` before the keyword of
2665/// each private item `module` declares named `name`, when `module` is the
2666/// importer's own package's. The keyword is found in the module's text.
2667fn make_public(module: &ModuleSource, name: &str, import: &str, same_package: bool) -> Option<Fix> {
2668    if !same_package {
2669        return None;
2670    }
2671    let text = module.text?;
2672    let mut edits = Vec::new();
2673    for item in &module.file.items {
2674        let (vis, item_name) = match &item.value {
2675            Item::Function(f) => (f.vis, &f.name),
2676            Item::Record(r) => (r.vis, &r.name),
2677            Item::Type(t) => (t.vis, &t.name),
2678            Item::Trait(t) => (t.vis, &t.name),
2679            Item::Const(c) => (c.vis, &c.name),
2680            _ => continue,
2681        };
2682        if item_name.value != name || is_pub(vis) {
2683            continue;
2684        }
2685        let at = nova_diagnostics::lines::keyword_start(text, item_name.span.start as usize)?;
2686        edits.push(Edit::insert(at as u32, item_name.span.file, "pub "));
2687    }
2688    (!edits.is_empty()).then(|| Fix::new(format!("make `{name}` public in `{import}`"), edits))
2689}
2690
2691/// Whether `module` would receive `name` once module `target` exports it:
2692/// it imports `target` whole, or by a list naming `name`.
2693fn would_receive(module: &ModuleSource, target: usize, name: &str) -> bool {
2694    module.file.items.iter().any(|item| {
2695        let Item::Import(imp) = &item.value else {
2696            return false;
2697        };
2698        let [first] = imp.path.value.segments.as_slice() else {
2699            return false;
2700        };
2701        if module.imports.get(&first.value) != Some(&ImportTarget::Module(target)) {
2702            return false;
2703        }
2704        match &imp.kind {
2705            ImportKind::Simple => true,
2706            ImportKind::List(names) => names.iter().any(|n| n.value == name),
2707            _ => false,
2708        }
2709    })
2710}
2711
2712/// Whether `scope` binds `name` in any namespace.
2713fn binds(scope: &ModuleScope, name: &str) -> bool {
2714    scope.values.contains_key(name)
2715        || scope.types.contains_key(name)
2716        || scope.traits.contains_key(name)
2717}
2718
2719#[allow(clippy::too_many_arguments)]
2720fn insert_value(
2721    scope: &mut ModuleScope,
2722    exp: &mut Exports,
2723    first: &mut IndexMap<String, Span>,
2724    diagnostics: &mut Vec<Diagnostic>,
2725    name: String,
2726    span: Span,
2727    res: Res,
2728    is_pub: bool,
2729) {
2730    if scope.values.contains_key(&name) {
2731        let mut diag = Diagnostic::error("E0002", format!("duplicate definition of `{name}`"))
2732            .with_primary_label(span, "redefined here");
2733        if let Some(prev) = first.get(&name) {
2734            diag = diag.with_secondary_label(*prev, "first defined here");
2735        } else {
2736            diag = diag.with_note(format!("`{name}` is a compiler builtin"));
2737        }
2738        diagnostics.push(diag);
2739        return;
2740    }
2741    first.insert(name.clone(), span);
2742    if is_pub {
2743        exp.values.insert(name.clone(), res);
2744    }
2745    scope.values.insert(name, res);
2746}
2747
2748#[allow(clippy::too_many_arguments)]
2749fn insert_type(
2750    scope: &mut ModuleScope,
2751    exp: &mut Exports,
2752    first: &mut IndexMap<String, Span>,
2753    diagnostics: &mut Vec<Diagnostic>,
2754    name: String,
2755    span: Span,
2756    id: DefId,
2757    is_pub: bool,
2758) {
2759    if scope.types.contains_key(&name) {
2760        let mut diag = Diagnostic::error("E0002", format!("duplicate definition of type `{name}`"))
2761            .with_primary_label(span, "redefined here");
2762        if let Some(prev) = first.get(&name) {
2763            diag = diag.with_secondary_label(*prev, "first defined here");
2764        }
2765        diagnostics.push(diag);
2766        return;
2767    }
2768    first.insert(name.clone(), span);
2769    if is_pub {
2770        exp.types.insert(name.clone(), id);
2771    }
2772    scope.types.insert(name, id);
2773}
2774
2775/// Bind an imported name into a module's value namespace, or report a conflict.
2776fn bind_value(
2777    scope: &mut ModuleScope,
2778    diagnostics: &mut Vec<Diagnostic>,
2779    name: String,
2780    span: Span,
2781    res: Res,
2782) {
2783    if scope.values.contains_key(&name) {
2784        diagnostics.push(
2785            Diagnostic::error(
2786                "E0002",
2787                format!("`{name}` is already defined or imported in this module"),
2788            )
2789            .with_primary_label(span, "conflicting import"),
2790        );
2791        return;
2792    }
2793    scope.values.insert(name, res);
2794}
2795
2796fn bind_type(
2797    scope: &mut ModuleScope,
2798    diagnostics: &mut Vec<Diagnostic>,
2799    name: String,
2800    span: Span,
2801    id: DefId,
2802) {
2803    if scope.types.contains_key(&name) {
2804        diagnostics.push(
2805            Diagnostic::error(
2806                "E0002",
2807                format!("type `{name}` is already defined or imported in this module"),
2808            )
2809            .with_primary_label(span, "conflicting import"),
2810        );
2811        return;
2812    }
2813    scope.types.insert(name, id);
2814}
2815
2816fn bind_trait(
2817    scope: &mut ModuleScope,
2818    diagnostics: &mut Vec<Diagnostic>,
2819    name: String,
2820    span: Span,
2821    id: DefId,
2822) {
2823    if scope.traits.contains_key(&name) {
2824        diagnostics.push(
2825            Diagnostic::error(
2826                "E0002",
2827                format!("trait `{name}` is already defined or imported in this module"),
2828            )
2829            .with_primary_label(span, "conflicting import"),
2830        );
2831        return;
2832    }
2833    scope.traits.insert(name, id);
2834}
2835
2836fn unsupported(span: Span, msg: &str) -> Diagnostic {
2837    Diagnostic::error("E0900", msg).with_primary_label(span, "not supported yet")
2838}
2839
2840/// A short textual head for a type, used only to build unique method
2841/// symbol names (semantic resolution happens later in the type checker).
2842/// A textual name for an impl self type that includes its type arguments, so
2843/// that two impls sharing a head but differing in arguments (`Pair<Int, Bool>`
2844/// vs `Pair<Int, Int>`) yield distinct method names and never collide when
2845/// monomorphized. Uses only `_`/`.`-style separators to stay linker-safe.
2846/// A generic parameter contributes its source name (`Box<T>` → `Box_T`); a
2847/// concrete instantiation still gets a distinct mangled instance via type
2848/// arguments at monomorphization.
2849fn type_full_name(ty: &nova_ast::Type) -> String {
2850    match ty {
2851        nova_ast::Type::Path { path, args } => {
2852            let head = path_tail(path);
2853            if args.is_empty() {
2854                head
2855            } else {
2856                let parts: Vec<String> = args.iter().map(|a| type_full_name(&a.value)).collect();
2857                format!("{head}_{}", parts.join("_"))
2858            }
2859        }
2860        nova_ast::Type::Array(elem) => format!("Arr_{}", type_full_name(&elem.value)),
2861        _ => "anon".to_string(),
2862    }
2863}
2864
2865/// The last segment of a path (`a::b::c` → `c`).
2866fn path_tail(path: &nova_ast::Path) -> String {
2867    path.segments
2868        .last()
2869        .map(|s| s.value.clone())
2870        .unwrap_or_default()
2871}
2872
2873#[cfg(test)]
2874mod tests {
2875    use super::*;
2876    use nova_diagnostics::FileId;
2877    use nova_lexer::lex;
2878    use nova_parser::parse;
2879
2880    fn resolve_src(src: &str) -> ResolveResult {
2881        let file_id = FileId::DUMMY;
2882        let (tokens, lex_errors) = lex(src, file_id);
2883        assert!(lex_errors.is_empty(), "lex errors: {lex_errors:?}");
2884        let (ast, parse_errors) = parse(&tokens, file_id);
2885        assert!(parse_errors.is_empty(), "parse errors: {parse_errors:?}");
2886        resolve(&ast.expect("no AST produced"))
2887    }
2888
2889    fn parse_file(src: &str) -> nova_ast::File {
2890        let (tokens, _) = lex(src, FileId::DUMMY);
2891        let (ast, errs) = parse(&tokens, FileId::DUMMY);
2892        assert!(errs.is_empty(), "parse errors: {errs:?}");
2893        ast.expect("no AST")
2894    }
2895
2896    /// Resolve a two-module program: `main` plus `lib`.
2897    fn resolve_two(main_src: &str, lib_src: &str) -> ProgramResolution {
2898        let main = parse_file(main_src);
2899        let lib = parse_file(lib_src);
2900        let mut sources = [
2901            ModuleSource::new("main", &main),
2902            ModuleSource::new("lib", &lib),
2903        ];
2904        name_imports(&mut sources);
2905        let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
2906        resolve_program(&sources, &std_files, None)
2907    }
2908
2909    fn error_codes(diags: &[Diagnostic]) -> Vec<&str> {
2910        diags.iter().map(|d| d.code.as_str()).collect()
2911    }
2912
2913    // === Phase 3.3a: import tables (spec
2914    // docs/superpowers/specs/2026-10-08-phase-3-3a-local-packages-design.md §4.5) ===
2915
2916    #[test]
2917    fn an_import_table_can_name_a_module_by_any_label() {
2918        let main = parse_file("import geom::{area}\nfn main() { let a = area() }\n");
2919        let lib = parse_file("pub fn area() -> Int { 1 }\n");
2920        let mut sources = [
2921            ModuleSource::new("main", &main),
2922            ModuleSource::new("geom", &lib),
2923        ];
2924        sources[1].name = "a label no import names".to_string();
2925        sources[0]
2926            .imports
2927            .insert("geom".to_string(), ImportTarget::Module(1));
2928        let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
2929        let p = resolve_program(&sources, &std_files, None);
2930        assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
2931        assert!(matches!(
2932            p.definitions.resolve_value(ModuleId(0), "area"),
2933            Some(Res::Def(_))
2934        ));
2935    }
2936
2937    #[test]
2938    fn a_reported_import_binds_nothing_and_says_nothing() {
2939        let main = parse_file("import geom\nfn main() {}\n");
2940        let mut sources = [ModuleSource::new("main", &main)];
2941        sources[0]
2942            .imports
2943            .insert("geom".to_string(), ImportTarget::Reported);
2944        let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
2945        let p = resolve_program(&sources, &std_files, None);
2946        assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
2947    }
2948
2949    #[test]
2950    fn an_import_missing_from_the_table_cannot_find_its_module() {
2951        let main = parse_file("import geom\nfn main() {}\n");
2952        let lib = parse_file("pub fn area() -> Int { 1 }\n");
2953        // No `name_imports`: the table is empty, whatever the modules' names.
2954        let sources = [
2955            ModuleSource::new("main", &main),
2956            ModuleSource::new("geom", &lib),
2957        ];
2958        let std_files: Vec<FileId> = STD_MODULES.iter().map(|_| FileId::DUMMY).collect();
2959        let p = resolve_program(&sources, &std_files, None);
2960        assert_eq!(error_codes(&p.diagnostics), ["E0001"]);
2961        assert!(p.diagnostics[0]
2962            .message
2963            .contains("cannot find module `geom`"));
2964    }
2965
2966    #[test]
2967    fn import_binds_public_names_across_modules() {
2968        let p = resolve_two(
2969            "import lib::{add}\nfn main() { let x = add(1, 2) }\n",
2970            "pub fn add(a: Int, b: Int) -> Int { a + b }\nfn secret() -> Int { 0 }\n",
2971        );
2972        assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
2973        // `add` is visible in module 0 (main) after the import.
2974        assert!(matches!(
2975            p.definitions.resolve_value(ModuleId(0), "add"),
2976            Some(Res::Def(_))
2977        ));
2978    }
2979
2980    #[test]
2981    fn importing_a_private_item_is_rejected() {
2982        let p = resolve_two(
2983            "import lib::{secret}\nfn main() { }\n",
2984            "pub fn add() -> Int { 0 }\nfn secret() -> Int { 0 }\n",
2985        );
2986        assert!(
2987            error_codes(&p.diagnostics).contains(&"E0001"),
2988            "{:?}",
2989            p.diagnostics
2990        );
2991    }
2992
2993    #[test]
2994    fn dangling_import_reports_missing_module() {
2995        let p = resolve_two(
2996            "import nope::{x}\nfn main() { }\n",
2997            "pub fn add() -> Int { 0 }\n",
2998        );
2999        assert!(
3000            error_codes(&p.diagnostics).contains(&"E0001"),
3001            "{:?}",
3002            p.diagnostics
3003        );
3004    }
3005
3006    #[test]
3007    fn private_item_is_not_visible_to_other_modules() {
3008        let p = resolve_two(
3009            "import lib\nfn main() { }\n",
3010            "pub fn add() -> Int { 0 }\nfn secret() -> Int { 0 }\n",
3011        );
3012        assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
3013        // Glob import brings `add` but not `secret`.
3014        assert!(p.definitions.resolve_value(ModuleId(0), "add").is_some());
3015        assert!(p.definitions.resolve_value(ModuleId(0), "secret").is_none());
3016    }
3017
3018    #[test]
3019    fn same_name_in_two_modules_does_not_collide() {
3020        // Each module has its own `helper`; no duplicate-definition error.
3021        // (Codegen keeps them distinct via DefId-mangled symbols; see the
3022        // `modules_same_name_functions_dispatch_correctly` CLI test.)
3023        let p = resolve_two(
3024            "fn helper() -> Int { 1 }\nfn main() { }\n",
3025            "pub fn helper() -> Int { 2 }\n",
3026        );
3027        assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
3028    }
3029
3030    #[test]
3031    fn qualified_import_path_is_rejected() {
3032        // `import lib::extra` is a qualified/nested path; it must be reported,
3033        // not silently resolved to the last segment's module.
3034        let p = resolve_two(
3035            "import lib::extra\nfn main() { }\n",
3036            "pub fn add() -> Int { 0 }\n",
3037        );
3038        assert!(
3039            error_codes(&p.diagnostics).contains(&"E0900"),
3040            "{:?}",
3041            p.diagnostics
3042        );
3043    }
3044
3045    #[test]
3046    fn std_core_option_result_are_in_scope() {
3047        // Option/Result and their variants resolve with no import or definition.
3048        let r = resolve_src("fn main() { }\n");
3049        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3050        assert!(r.definitions.resolve_type(ModuleId(0), "Option").is_some());
3051        assert!(r.definitions.resolve_type(ModuleId(0), "Result").is_some());
3052        assert!(matches!(
3053            r.definitions.resolve_value(ModuleId(0), "Some"),
3054            Some(Res::Variant(_, 0))
3055        ));
3056        assert!(matches!(
3057            r.definitions.resolve_value(ModuleId(0), "None"),
3058            Some(Res::Variant(_, 1))
3059        ));
3060        assert!(matches!(
3061            r.definitions.resolve_value(ModuleId(0), "Ok"),
3062            Some(Res::Variant(_, 0))
3063        ));
3064        assert!(matches!(
3065            r.definitions.resolve_value(ModuleId(0), "Err"),
3066            Some(Res::Variant(_, 1))
3067        ));
3068    }
3069
3070    #[test]
3071    fn extern_fn_resolves_as_callable_extern_def() {
3072        let r = resolve_src("extern \"C\" { fn sqrt(x: Float) -> Float }\nfn main() { }\n");
3073        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3074        let Some(Res::Def(id)) = r.definitions.resolve_value(ModuleId(0), "sqrt") else {
3075            panic!("sqrt should resolve to a Def");
3076        };
3077        assert!(matches!(
3078            r.definitions.def(id).kind,
3079            DefKind::ExternFn { .. }
3080        ));
3081    }
3082
3083    #[test]
3084    fn user_type_shadows_std_core() {
3085        // A user-defined `Option` shadows std/core's without an E0002 clash:
3086        // the module's own definition wins over the soft std/core import.
3087        let r = resolve_src("type Option<T> = | Present(T) | Absent\nfn main() { }\n");
3088        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3089        let opt = r
3090            .definitions
3091            .resolve_type(ModuleId(0), "Option")
3092            .expect("Option resolves");
3093        // It is the user's two-variant `Option`, not std/core's.
3094        assert!(matches!(
3095            r.definitions.def(opt).kind,
3096            DefKind::Sum { ref variants, .. } if variants.len() == 2 && variants[0].name == "Present"
3097        ));
3098    }
3099
3100    #[test]
3101    fn user_trait_shadows_std_core() {
3102        // The trait-namespace analogue of `user_type_shadows_std_core`: a
3103        // user-defined `Display` shadows std/core's without an E0002 clash,
3104        // the module's own trait winning over the soft std/core import. Pins
3105        // the `or_insert`-vs-`insert` precedence `import_std_module`'s `traits`
3106        // loop relies on (mirrored from the pre-existing `values`/`types`
3107        // loops) — the easiest part of that fix to accidentally break later,
3108        // since nothing else in this file failed the way it would if this
3109        // regressed (std/core's `Display` would silently win instead, and
3110        // every method call would still type-check, just against the wrong
3111        // trait).
3112        let r = resolve_src("trait Display { fn only_here(self) -> Int }\nfn main() { }\n");
3113        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3114        let tr = r
3115            .definitions
3116            .resolve_trait(ModuleId(0), "Display")
3117            .expect("Display resolves");
3118        // It is module 0's own trait (declaring `only_here`), not std/core's
3119        // `Display` (declaring `fmt`), which lives in a different module.
3120        assert!(matches!(
3121            r.definitions.def(tr).kind,
3122            DefKind::Trait { item_index } if r.definitions.module_of(item_index) == ModuleId(0)
3123        ));
3124    }
3125
3126    #[test]
3127    fn associated_type_is_not_in_the_type_namespace() {
3128        // `Item` is not a type name you can write bare — it is reachable only
3129        // through a projection (`Self::Item`, resolved in a later task). If a
3130        // future change ever seeded it into `scope.types` too, this would
3131        // start returning `Some`, silently making it a global type name.
3132        let r = resolve_src("trait It { type Item }\nfn main() { }\n");
3133        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3134        assert_eq!(r.definitions.resolve_type(ModuleId(0), "Item"), None);
3135    }
3136
3137    #[test]
3138    fn associated_type_is_not_in_the_value_namespace() {
3139        // Mirrors the test above for the other namespace: an associated
3140        // type's `Def` is created by `push_def` alone and never registered in
3141        // `scope.values` either, so it must not resolve as an expression name
3142        // (there is no value `Item` could denote). Untested until now, not
3143        // known broken.
3144        let r = resolve_src("trait It { type Item }\nfn main() { }\n");
3145        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3146        assert_eq!(r.definitions.resolve_value(ModuleId(0), "Item"), None);
3147    }
3148
3149    #[test]
3150    fn user_fn_named_str_cmp_is_not_a_reserved_word() {
3151        // Regression for the `str_cmp` builtin becoming an accidental
3152        // reserved word in every module: `Builtin::STD_ONLY` (unlike
3153        // `Builtin::GLOBAL`) is seeded only into an std module's own scope,
3154        // so a user module defining its own `str_cmp` must compile cleanly —
3155        // no E0002 "duplicate definition" / "compiler builtin" diagnostic.
3156        let r = resolve_src("fn str_cmp(a: Int, b: Int) -> Int { a + b }\nfn main() { }\n");
3157        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3158        assert!(matches!(
3159            r.definitions.resolve_value(ModuleId(0), "str_cmp"),
3160            Some(Res::Def(_))
3161        ));
3162    }
3163
3164    /// Same property as `user_fn_named_str_cmp_is_not_a_reserved_word`, for
3165    /// every other member of `Builtin::STD_ONLY`. Written as a loop over
3166    /// the constant so that adding a builtin to it without deciding this
3167    /// question cannot happen: a new std-only builtin is covered here the
3168    /// moment it joins the list.
3169    #[test]
3170    fn no_std_only_builtin_is_a_reserved_word() {
3171        for b in Builtin::STD_ONLY {
3172            let name = b.name();
3173            let src = format!("fn {name}(a: Int, b: Int) -> Int {{ a + b }}\nfn main() {{ }}\n");
3174            let r = resolve_src(&src);
3175            assert!(r.diagnostics.is_empty(), "`{name}`: {:?}", r.diagnostics);
3176            assert!(
3177                matches!(
3178                    r.definitions.resolve_value(ModuleId(0), name),
3179                    Some(Res::Def(_))
3180                ),
3181                "`{name}` should resolve to the user's own definition"
3182            );
3183        }
3184    }
3185
3186    /// The other half of the std-only contract: each of these names *is*
3187    /// visible inside an std module, so `std/core` can call it. Without this,
3188    /// dropping a builtin from `STD_ONLY` altogether would still leave
3189    /// `no_std_only_builtin_is_a_reserved_word` passing.
3190    #[test]
3191    fn std_only_builtins_are_visible_inside_std_modules() {
3192        let r = resolve_src("fn main() { }\n");
3193        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3194        // Module 0 is the single user module; the std modules follow it, one
3195        // per `STD_MODULES` entry, in order (ADR 0004). *Every* one of them
3196        // gets these builtins, not only `$std.core`.
3197        for mid in 1..=STD_MODULES.len() {
3198            for b in Builtin::STD_ONLY {
3199                assert!(
3200                    matches!(
3201                        r.definitions.resolve_value(ModuleId(mid as u32), b.name()),
3202                        Some(Res::Builtin(seen)) if seen == b
3203                    ),
3204                    "`{}` must resolve to its builtin inside std module {mid}",
3205                    b.name()
3206                );
3207            }
3208        }
3209    }
3210
3211    /// Every `std/net` builtin is `STD_ONLY`, per the brief this task
3212    /// was written against. `STD_ONLY.contains` rather than the loop
3213    /// `no_std_only_builtin_is_a_reserved_word`/
3214    /// `std_only_builtins_are_visible_inside_std_modules` already run over
3215    /// the whole array: this test pins each one *by name*, so deleting one
3216    /// from `STD_ONLY` (rather than merely narrowing its behaviour) fails
3217    /// here specifically instead of only shrinking a loop bound elsewhere.
3218    ///
3219    /// The list grows with the module, and has grown three times while
3220    /// `std/net` gained its listener: `NetListen` joined the original five,
3221    /// then `NetLocalPort`, then `NetAccept`. This test is the reason a new net
3222    /// builtin cannot be
3223    /// added to the enum and left out of `STD_ONLY` without a named failure --
3224    /// provided whoever adds it also adds it *here*, which no compiler
3225    /// enforces. That is the one gap: `STD_ONLY`'s own declared length is a
3226    /// build error when it drifts, but this list is a hand-maintained roster,
3227    /// so an omission here is silent rather than caught.
3228    ///
3229    /// `STD_ONLY` membership is the whole of what this asserts. It was
3230    /// originally named `..._and_build_futures`, which promised a second
3231    /// property its body never checked -- that coverage genuinely exists, in
3232    /// `nova-typeck`'s `builtin_signatures_are_what_the_std_call_sites_use`
3233    /// (`crates/nova-typeck/src/check.rs`), which is where each builtin's
3234    /// `Future<_>` return type is actually pinned against its `std/net` call
3235    /// site.
3236    #[test]
3237    fn the_net_builtins_are_std_only() {
3238        for b in [
3239            Builtin::NetConnect,
3240            Builtin::NetClose,
3241            Builtin::NetRead,
3242            Builtin::NetWrite,
3243            Builtin::NetReadTimeout,
3244            Builtin::NetListen,
3245            Builtin::NetLocalPort,
3246            Builtin::NetAccept,
3247        ] {
3248            assert!(
3249                Builtin::STD_ONLY.contains(&b),
3250                "{b:?} must be invisible to user code"
3251            );
3252        }
3253    }
3254
3255    #[test]
3256    fn user_fn_named_panic_is_a_reserved_word() {
3257        // The deliberate counterpart of
3258        // `user_fn_named_str_cmp_is_not_a_reserved_word`, and the reason that
3259        // test needs a sibling: `panic` is in `Builtin::GLOBAL`, so it *is*
3260        // seeded into every module's scope and a user `fn panic` is an E0002
3261        // clash against a builtin. That is intended — `panic` is user-visible
3262        // language surface like `println`/`print` — but it permanently takes
3263        // the name away from user code, so which side of the
3264        // `GLOBAL`/`STD_ONLY` split each builtin sits on has to be pinned
3265        // in both directions. Moving `Panic` to `STD_ONLY` would silently
3266        // make `panic(...)` unresolvable in user programs while every existing
3267        // test still passed.
3268        let r = resolve_src("fn panic(msg: String) { }\nfn main() { }\n");
3269        let codes: Vec<&str> = r.diagnostics.iter().map(|d| d.code.as_str()).collect();
3270        assert!(codes.contains(&"E0002"), "{:?}", r.diagnostics);
3271        // Specifically the builtin-clash form of E0002 (no earlier user
3272        // definition to point at), not a duplicate-user-definition one.
3273        assert!(
3274            r.diagnostics
3275                .iter()
3276                .any(|d| d.notes.iter().any(|n| n.contains("compiler builtin"))),
3277            "expected the `panic` is a compiler builtin note: {:?}",
3278            r.diagnostics
3279        );
3280    }
3281
3282    #[test]
3283    fn std_core_traits_are_visible_from_a_user_module() {
3284        // Regression test for a gap `import_std_module` had: it merged `values`
3285        // and `types` from std/core's exports into every other module's scope,
3286        // but not `traits`, so a trait declared in std/core (e.g. `Display`,
3287        // `Eq`, `Ord`) could never be named from user code even though its
3288        // impls live right there in std/core alongside it. A user module that
3289        // declares no trait of its own must still resolve std/core's by name.
3290        let r = resolve_src("fn main() { }\n");
3291        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3292        for name in ["Display", "Debug", "Eq", "Ord", "Clone", "Default"] {
3293            assert!(
3294                r.definitions.resolve_trait(ModuleId(0), name).is_some(),
3295                "expected std/core's `{name}` trait to resolve from module 0"
3296            );
3297        }
3298    }
3299
3300    #[test]
3301    fn std_collections_module_is_compiled_and_visible() {
3302        // A name defined in std/collections must resolve from a user module,
3303        // exactly as std/core's names do.
3304        let r = resolve_src("fn main() { }");
3305        assert!(
3306            r.definitions.resolve_type(ModuleId(0), "Vec").is_some(),
3307            "Vec should be visible from a user module"
3308        );
3309    }
3310
3311    #[test]
3312    fn importing_a_std_core_name_from_a_module_is_allowed() {
3313        // A user module may export a name coinciding with a std/core name; a
3314        // glob import of it binds (shadowing the soft std/core import), not a
3315        // spurious E0002.
3316        let p = resolve_two(
3317            "import lib\nfn main() { }\n",
3318            "pub type Status = | Ok | Fail\n",
3319        );
3320        assert!(p.diagnostics.is_empty(), "{:?}", p.diagnostics);
3321        // `Ok` resolves (to the imported Status variant), not rejected.
3322        assert!(p.definitions.resolve_value(ModuleId(0), "Ok").is_some());
3323    }
3324
3325    #[test]
3326    fn collects_functions_and_sum_types() {
3327        let r = resolve_src(
3328            "type Shape = | Circle(Int) | Empty\n\
3329             fn area(s: Shape) -> Int { 0 }\n\
3330             fn main() { }\n",
3331        );
3332        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3333        assert!(matches!(
3334            r.definitions.resolve_value(ModuleId(0), "area"),
3335            Some(Res::Def(_))
3336        ));
3337        assert!(matches!(
3338            r.definitions.resolve_value(ModuleId(0), "Circle"),
3339            Some(Res::Variant(_, 0))
3340        ));
3341        assert!(matches!(
3342            r.definitions.resolve_value(ModuleId(0), "Empty"),
3343            Some(Res::Variant(_, 1))
3344        ));
3345        assert!(r.definitions.resolve_type(ModuleId(0), "Shape").is_some());
3346        assert!(matches!(
3347            r.definitions.resolve_value(ModuleId(0), "println"),
3348            Some(Res::Builtin(Builtin::Println))
3349        ));
3350    }
3351
3352    #[test]
3353    fn collects_records() {
3354        let r = resolve_src("record Point { x: Float, y: Float }\nfn main() { }\n");
3355        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3356        let id = r
3357            .definitions
3358            .resolve_type(ModuleId(0), "Point")
3359            .expect("Point resolves");
3360        assert!(matches!(r.definitions.def(id).kind, DefKind::Record { .. }));
3361    }
3362
3363    #[test]
3364    fn collects_traits_and_impl_methods() {
3365        let r = resolve_src(
3366            "record P { v: Int }\n\
3367             trait Show { fn name(self) -> String\n fn shout(self) -> String { self.name() } }\n\
3368             impl Show for P { fn name(self) -> String { \"p\" } }\n\
3369             impl P { fn get(self) -> Int { self.v } }\n\
3370             fn main() { }\n",
3371        );
3372        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3373        assert!(r.definitions.resolve_trait(ModuleId(0), "Show").is_some());
3374        // `methods()` is whole-program — it also sees every method std/core
3375        // defines on Option/Result — so scope the count to this source's own
3376        // module (module 0) rather than asserting a total that shifts every
3377        // time std/core grows. This source defines exactly 3: the trait's
3378        // default `shout`, the trait-impl `name`, and the inherent `get`.
3379        let methods = r
3380            .definitions
3381            .methods()
3382            .filter(|&(_, item_index, _, _)| r.definitions.module_of(item_index) == ModuleId(0))
3383            .count();
3384        assert_eq!(methods, 3, "expected this module's own 3 method defs");
3385    }
3386
3387    #[test]
3388    fn duplicate_function_reports_e0002() {
3389        let r = resolve_src("fn f() { }\nfn f() { }\n");
3390        assert_eq!(r.diagnostics.len(), 1);
3391        assert_eq!(r.diagnostics[0].code, "E0002");
3392    }
3393
3394    #[test]
3395    fn shadowing_builtin_reports_e0002() {
3396        let r = resolve_src("fn println() { }\n");
3397        assert_eq!(r.diagnostics.len(), 1);
3398        assert_eq!(r.diagnostics[0].code, "E0002");
3399    }
3400
3401    // === Phase 3.2: names in scope (spec
3402    // docs/superpowers/specs/2026-10-08-phase-3-2-lsp-core-design.md §4.3) ===
3403
3404    #[test]
3405    fn names_in_scope_lists_items_imports_builtins_variants_and_std() {
3406        let r = resolve_two(
3407            "import lib::{area}\nrecord Point { x: Int }\nfn main() {}\n",
3408            "pub fn area() -> Int { 1 }\n",
3409        );
3410        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3411        let names = r.definitions.names_in_scope(ModuleId(0));
3412        let has = |name: &str, kind: fn(&ScopeEntry) -> bool| {
3413            names.iter().any(|(n, e)| n == name && kind(e))
3414        };
3415        assert!(has("main", |e| matches!(e, ScopeEntry::Value(Res::Def(_)))));
3416        assert!(has("Point", |e| matches!(e, ScopeEntry::Type(_))));
3417        assert!(has("area", |e| matches!(e, ScopeEntry::Value(Res::Def(_)))));
3418        assert!(has("println", |e| matches!(
3419            e,
3420            ScopeEntry::Value(Res::Builtin(_))
3421        )));
3422        assert!(has("Some", |e| matches!(
3423            e,
3424            ScopeEntry::Value(Res::Variant(_, _))
3425        )));
3426        assert!(has("Vec", |e| matches!(e, ScopeEntry::Type(_))));
3427        assert!(has("Display", |e| matches!(e, ScopeEntry::Trait(_))));
3428        let mut sorted = names.clone();
3429        sorted.sort_by(|a, b| a.0.cmp(&b.0));
3430        assert_eq!(
3431            names.iter().map(|n| &n.0).collect::<Vec<_>>(),
3432            sorted.iter().map(|n| &n.0).collect::<Vec<_>>(),
3433            "sorted by name"
3434        );
3435    }
3436
3437    #[test]
3438    fn a_std_name_the_module_shadows_is_listed_once_as_its_own() {
3439        let r = resolve_two("record Vec { n: Int }\nfn main() {}\n", "pub fn f() {}\n");
3440        assert!(r.diagnostics.is_empty(), "{:?}", r.diagnostics);
3441        let vecs: Vec<DefId> = r
3442            .definitions
3443            .names_in_scope(ModuleId(0))
3444            .into_iter()
3445            .filter_map(|(n, e)| match e {
3446                ScopeEntry::Type(id) if n == "Vec" => Some(id),
3447                _ => None,
3448            })
3449            .collect();
3450        assert_eq!(vecs.len(), 1, "{vecs:?}");
3451        let DefKind::Record { item_index } = r.definitions.def(vecs[0]).kind else {
3452            panic!("not a record");
3453        };
3454        assert_eq!(r.definitions.module_of(item_index), ModuleId(0));
3455    }
3456
3457    // === Phase 3.4a: import occurrences (spec
3458    // docs/superpowers/specs/2026-10-10-phase-3-4a-navigation-design.md §3.3) ===
3459
3460    fn occurrence_at(prog: &ProgramResolution, src: &str, needle: &str) -> Vec<(Role, Target)> {
3461        let start = src.find(needle).unwrap() as u32;
3462        prog.imports
3463            .iter()
3464            .filter(|o| o.span.start == start && o.span.end == start + needle.len() as u32)
3465            .map(|o| (o.role, o.target))
3466            .collect()
3467    }
3468
3469    #[test]
3470    fn an_imports_module_is_a_use_of_it() {
3471        let main = "import lib\nfn main() { let a = area() }\n";
3472        let prog = resolve_two(main, "pub fn area() -> Int { 1 }\n");
3473        assert_eq!(
3474            occurrence_at(&prog, main, "lib"),
3475            vec![(Role::Use, Target::Module(ModuleId(1)))]
3476        );
3477    }
3478
3479    #[test]
3480    fn a_list_imports_module_is_its_first_segment() {
3481        let main = "import lib::{area}\nfn main() { let a = area() }\n";
3482        let prog = resolve_two(main, "pub fn area() -> Int { 1 }\n");
3483        // `lib` alone, not `lib::`.
3484        assert_eq!(
3485            occurrence_at(&prog, main, "lib"),
3486            vec![(Role::Use, Target::Module(ModuleId(1)))]
3487        );
3488    }
3489
3490    #[test]
3491    fn a_listed_name_is_a_use_of_each_namespace_it_binds() {
3492        // User modules' defs come before std's, so `position` finds these.
3493        let main = "import lib::{area, Figure, Paint, Round}\nfn main() {}\n";
3494        let lib = "pub fn area() -> Int { 1 }\n\
3495                   pub type Figure = | Round(Int) | Square(Int)\n\
3496                   pub trait Paint { fn paint(self) -> String }\n";
3497        let prog = resolve_two(main, lib);
3498        let id = |name: &str| {
3499            DefId(
3500                prog.definitions
3501                    .defs()
3502                    .iter()
3503                    .position(|d| d.name == name)
3504                    .unwrap() as u32,
3505            )
3506        };
3507        assert_eq!(
3508            occurrence_at(&prog, main, "area"),
3509            vec![(Role::Use, Target::Def(id("area")))]
3510        );
3511        assert_eq!(
3512            occurrence_at(&prog, main, "Figure"),
3513            vec![(Role::Use, Target::Def(id("Figure")))]
3514        );
3515        assert_eq!(
3516            occurrence_at(&prog, main, "Paint"),
3517            vec![(Role::Use, Target::Def(id("Paint")))]
3518        );
3519        assert_eq!(
3520            occurrence_at(&prog, main, "Round"),
3521            vec![(Role::Use, Target::Variant(id("Figure"), 0))]
3522        );
3523    }
3524
3525    #[test]
3526    fn an_unresolved_import_records_nothing() {
3527        let main = "import nowhere\nimport lib::{missing}\nfn main() {}\n";
3528        let prog = resolve_two(main, "pub fn area() -> Int { 1 }\n");
3529        assert!(occurrence_at(&prog, main, "nowhere").is_empty());
3530        assert!(occurrence_at(&prog, main, "missing").is_empty());
3531        // The module of a list import still resolves.
3532        assert_eq!(occurrence_at(&prog, main, "lib").len(), 1);
3533    }
3534
3535    // === Phase 3.4b: what a fix needs to know (spec
3536    // docs/superpowers/specs/2026-10-10-phase-3-4b-fixes-and-colour-design.md §4.6) ===
3537
3538    #[test]
3539    fn name_imports_makes_every_other_module_importable() {
3540        let p = resolve_two("fn main() {}\n", "pub fn area() -> Int { 1 }\n");
3541        let d = &p.definitions;
3542        assert_eq!(
3543            d.importable(ModuleId(0)),
3544            [("lib".to_string(), ModuleId(1))]
3545        );
3546        assert_eq!(
3547            d.importable(ModuleId(1)),
3548            [("main".to_string(), ModuleId(0))]
3549        );
3550        assert!(d.same_package(ModuleId(0), ModuleId(1)));
3551        // A loose program's modules are owned; std's are not.
3552        assert!(d.owned(ModuleId(0)) && d.owned(ModuleId(1)) && !d.owned(ModuleId(2)));
3553    }
3554
3555    #[test]
3556    fn exported_names_each_namespace_a_public_name_has() {
3557        let p = resolve_two(
3558            "fn main() {}\n",
3559            "pub record Area { x: Int }\npub fn area() -> Int { 1 }\nfn hidden() -> Int { 0 }\n",
3560        );
3561        let d = &p.definitions;
3562        let area = d.exported(ModuleId(1), "area");
3563        assert!(matches!(area.value, Some(Res::Def(_))), "{area:?}");
3564        assert!(area.ty.is_none() && area.trait_def.is_none(), "{area:?}");
3565        assert!(d.exported(ModuleId(1), "Area").ty.is_some());
3566        assert_eq!(d.exported(ModuleId(1), "hidden"), Exported::default());
3567    }
3568
3569    #[test]
3570    fn a_name_bound_through_std_is_not_bound_outside_it() {
3571        let p = resolve_two(
3572            "record Option { x: Int }\nfn main() {}\n",
3573            "pub fn area() -> Int { 1 }\n",
3574        );
3575        let d = &p.definitions;
3576        assert_eq!(
3577            d.bound_outside_std(ModuleId(0), "Some"),
3578            Exported::default()
3579        );
3580        assert!(d.bound_outside_std(ModuleId(0), "Option").ty.is_some());
3581        assert!(matches!(
3582            d.bound_outside_std(ModuleId(0), "println").value,
3583            Some(Res::Builtin(_))
3584        ));
3585        assert!(d.is_std_module(ModuleId(2)) && !d.is_std_module(ModuleId(1)));
3586    }
3587}