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}