nova_mir/lib.rs
1//! Mid-level IR for the Nova compiler.
2//!
3//! MIR is a 3-address-style IR over a control-flow graph of basic blocks,
4//! produced from typed HIR (`nova-hir`) and consumed by the codegen
5//! backends. Lowering to MIR performs:
6//!
7//! - **Monomorphization**: generic functions are instantiated per unique
8//! concrete type-argument list, reachable from `main` (spec
9//! `14-CODEGEN.md` §6.3). MIR contains no generic types.
10//! - **Pattern-match compilation**: `match` lowers to a tag `Switch` over
11//! sum values (or value switches / equality chains for primitives).
12//! - **Short-circuit lowering**: `&&` / `||` become control flow.
13//!
14//! Sum values are boxed: `{ tag: i64, fields: 8 bytes each }`, allocated
15//! through the runtime. Strings and function values are opaque pointers.
16
17mod async_lower;
18mod lower;
19mod mono;
20
21pub use mono::lower_module;
22
23/// The async ABI this compiler generates against: the state object's slot
24/// layout, the future value's slot layout, and the poll statuses.
25///
26/// Re-exported from the crate root because `nova-runtime` declares the same
27/// layout independently — neither crate may depend on the other — and the pin
28/// that holds the two copies together has to live in a third crate that
29/// depends on both (`nova-codegen-cranelift`).
30pub use async_lower::{
31 FUTURE_SLOT_POLL, FUTURE_SLOT_STATE, POLL_PENDING, POLL_READY, STATE_MIN_SIZE,
32 STATE_SLOT_OUTPUT, STATE_SLOT_TAG, STATE_SLOT_TEMPS,
33};
34
35use nova_hir as hir;
36use nova_resolver::DefId;
37
38/// A machine-level value class for one MIR temporary.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum MirTy {
41 /// 64-bit integer (Int, Char).
42 I64,
43 /// 64-bit float.
44 F64,
45 /// Boolean (byte-sized).
46 I8,
47 /// Opaque pointer (String, Bytes, sum values, function values).
48 Ptr,
49 /// No runtime value (unit / diverging results).
50 Unit,
51}
52
53/// A virtual register within one function.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub struct Temp(pub u32);
56
57/// A basic block id within one function; `BlockId(0)` is the entry.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct BlockId(pub u32);
60
61/// A monomorphized module ready for codegen.
62#[derive(Debug, Default)]
63pub struct Module {
64 pub functions: Vec<Function>,
65 /// `extern` (FFI) symbols the program calls, to be declared as imports.
66 pub externs: Vec<ExternDecl>,
67}
68
69/// An imported external (C-ABI) function: its raw symbol and ABI classes.
70#[derive(Debug, Clone)]
71pub struct ExternDecl {
72 /// The raw C symbol to import and call (never mangled).
73 pub symbol: String,
74 pub params: Vec<MirTy>,
75 pub ret: MirTy,
76}
77
78/// A monomorphized function.
79#[derive(Debug)]
80pub struct Function {
81 /// Mangled, unique symbol name (e.g. `identity.7$i`); the entry point is
82 /// the sole exception, emitted under the bare symbol `main`.
83 pub name: String,
84 /// Number of real value parameters (the first `params` temps after the
85 /// optional leading environment pointer).
86 pub params: u32,
87 /// Whether the ABI has a leading environment pointer (closures and
88 /// bare-fn wrappers). When true, temp 0 is the env pointer, temps
89 /// `1..=params` are the real parameters, and `capture_count` captured
90 /// values are loaded from the environment at entry.
91 pub takes_env: bool,
92 pub capture_count: u32,
93 pub temps: Vec<MirTy>,
94 pub ret: MirTy,
95 /// Still awaiting the async state-machine transform: this function was
96 /// lowered from an `async fn` body, so `ret` is the class of the value the
97 /// BODY produces, not of the `Future<T>` its signature declares.
98 ///
99 /// **No function reaching a codegen backend has this set.**
100 /// `lower_module` runs `async_lower::transform` over the finished module,
101 /// which rewrites every such function into a poll function plus a wrapper
102 /// and clears the flag. A function that still carried it would be emitted
103 /// under its original symbol with the wrong return class — invisible
104 /// wherever the output type happens to share a register class with a
105 /// pointer, a verifier error where it does not.
106 pub is_async: bool,
107 pub blocks: Vec<Block>,
108}
109
110/// A basic block: straight-line statements plus one terminator.
111#[derive(Debug)]
112pub struct Block {
113 pub stmts: Vec<Stmt>,
114 pub term: Terminator,
115}
116
117/// Numeric class of a binary operation's operands.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum OperandClass {
120 Int,
121 Float,
122 Bool,
123}
124
125/// Defines `RtFunc`'s variants together with [`RtFunc::ALL`] from one
126/// variant list, so the two can never disagree.
127///
128/// Before this, each codegen backend hand-maintained its own copy of "every
129/// runtime function" (Cranelift's `ALL_RT`, LLVM's `DECLS`) with no
130/// compile-time tie back to this enum. Adding a variant here and forgetting
131/// to update those lists compiled clean, and for the LLVM backend silently
132/// emitted a call to an undeclared symbol. `ALL` closes that gap: it is
133/// generated from the exact identifier list that also declares the enum's
134/// variants, in the same macro expansion, so a variant cannot exist without
135/// also appearing in `ALL` — there is no second list to forget.
136///
137/// `symbol()` and `signature()` below stay ordinary hand-written exhaustive
138/// `match`es: the compiler already refuses to build if either omits a
139/// variant, so generating them buys nothing extra.
140macro_rules! rt_funcs {
141 ($($(#[$doc:meta])* $variant:ident),+ $(,)?) => {
142 /// Runtime support functions provided by `nova-runtime`.
143 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144 pub enum RtFunc {
145 $($(#[$doc])* $variant,)+
146 }
147
148 impl RtFunc {
149 /// Every variant, in the declaration order above — the sole
150 /// authoritative list of "all runtime functions". Codegen
151 /// backends iterate this instead of hand-maintaining their own
152 /// copy (see the `rt_funcs!` docs above).
153 pub const ALL: [RtFunc; rt_funcs!(@count $($variant)+)] = [
154 $(RtFunc::$variant),+
155 ];
156 }
157 };
158 (@count $($v:ident)+) => {
159 [$(rt_funcs!(@one $v)),+].len()
160 };
161 (@one $v:ident) => { () };
162}
163
164rt_funcs! {
165 /// `(str) -> unit`
166 Println,
167 /// `(str) -> unit`
168 Print,
169 /// `(str) -> unit` — write to stderr with no trailing newline.
170 EPrint,
171 /// `(str) -> unit` — write to stderr followed by a newline.
172 EPrintln,
173 /// `(str, str) -> str`
174 StrConcat,
175 /// `([str]) -> str` — concatenate every element of a string array; the
176 /// lowering of an interpolation of three or more parts.
177 StrConcatN,
178 /// `(i64) -> str`
179 IntToStr,
180 /// `(f64) -> str`
181 FloatToStr,
182 /// `(f64, i64) -> str` — a `Float` at a fixed number of decimal places.
183 FloatFixed,
184 /// `(str) -> f64` — decimal text parsed as a `Float`.
185 StrToFloat,
186 /// `(i8) -> str`
187 BoolToStr,
188 /// `(i64 codepoint) -> str`
189 CharToStr,
190 /// `(str, str) -> i8`
191 StrEq,
192 /// `(str, str) -> i64` — lexicographic compare: -1, 0, or 1.
193 StrCmp,
194 /// `(str) -> i64` — seeded, finalized hash of the bytes (may be negative).
195 /// Seeded FNV-1a then splitmix64's finalizer; the reasoning is at
196 /// `nova_rt_str_hash`.
197 StrHash,
198 /// `(str) -> i64` — count of Unicode scalar values.
199 StrLenChars,
200 /// `(str) -> ptr` — a Nova `[Char]`.
201 StrChars,
202 /// `(ptr to [Char]) -> str`
203 StrFromChars,
204 /// `(str, ptr to [String]) -> str` — the parts with the separator
205 /// between each pair.
206 StrJoin,
207 /// `(str, str) -> i64` — the character index of the first match, or
208 /// `-1`.
209 StrIndexOf,
210 /// `(str) -> str` — the string as a JSON string literal.
211 JsonQuote,
212 /// `(str) -> str` — full Unicode uppercase.
213 StrToUpper,
214 /// `(str) -> str` — full Unicode lowercase.
215 StrToLower,
216 /// `(size_bytes) -> ptr` — allocate a heap value (sum or record).
217 Alloc,
218 /// `(index, len) -> unit` — abort if `index` is out of `0..len`.
219 CheckBounds,
220 /// `(str) -> !` — abort with a message.
221 Panic,
222 /// `() -> i64`. See `Builtin::TestSelector`.
223 TestSelector,
224 /// `(ptr to { poll_code, state }) -> i64` — queue a task, return its id.
225 TaskSpawn,
226 /// `(ptr to { poll_code, state }) -> i64` — drive the executor until this
227 /// future completes; returns its output slot.
228 TaskBlockOn,
229 /// `(ptr to { poll_code, state }) -> i8` — whether the task named by that
230 /// future's state has completed.
231 TaskIsDone,
232 /// `(i64 task_id) -> i64` — a completed task's output, taken exactly once.
233 /// Declared for completeness of the executor's ABI; no MIR statement emits
234 /// it, because a task's output reaches Nova through the future's own state
235 /// object instead (`Builtin::TaskOutput`, whose class the runtime's `i64`
236 /// return cannot carry for a `Float`).
237 TaskTakeOutput,
238 /// `(ptr to { poll_code, state }) -> unit` — end the executor's claim on
239 /// the task named by that future's state, without handing its output
240 /// back. Idempotent.
241 TaskRelease,
242 /// `() -> ptr to { poll_code, state }` — a fresh future that reports
243 /// pending once and then completes. What `std/task`'s `yield_now` awaits.
244 TaskYieldFuture,
245 /// `(i64 nanos) -> ptr to { poll_code, state }` — a fresh future that
246 /// parks for `nanos` nanoseconds via the executor's park set, then
247 /// completes. What `std/time`'s `sleep` awaits.
248 TaskSleepFutureNanos,
249 /// `(ptr to { poll_code, state }) -> ptr to { poll_code, state }` — a
250 /// fresh future that parks via the executor's park set until the task
251 /// named by the given future's state completes, then completes itself.
252 /// What `std/task`'s `join` awaits.
253 TaskJoinFuture,
254 /// `(i64, ptr) -> ptr to { poll_code, state }` — a future that polls its
255 /// inner future under a deadline
256 TaskTimeoutFuture,
257 /// `(str) -> i64` — read the path as UTF-8. `0` on success, with the
258 /// contents waiting in `FsTakeString`; otherwise an `IoErrorKind` status
259 /// code (`crates/nova-runtime/src/fs.rs`).
260 FsReadToString,
261 /// `(str, str) -> i64` — write the second string to the path named by the
262 /// first, truncating. Status code, as `FsReadToString`.
263 FsWriteString,
264 /// `() -> str` — take the payload staged by a successful `FsReadToString`.
265 FsTakeString,
266 /// `() -> str` — take the message staged by a failed filesystem operation.
267 FsLastErrorMessage,
268 /// `() -> str` — the OS temporary-directory path (`std::env::temp_dir()`).
269 FsTempDir,
270 /// `(str) -> i8` — whether the path exists.
271 FsExists,
272 /// `(str) -> i64` — create the directory. Status code, as
273 /// `FsReadToString`.
274 FsCreateDir,
275 /// `(str) -> i64` — create the directory and any missing parents.
276 /// Status code, as `FsReadToString`.
277 FsCreateDirAll,
278 /// `(str) -> i64` — remove the file. Status code, as `FsReadToString`.
279 FsRemoveFile,
280 /// `(str) -> i64` — remove the directory and everything under it.
281 /// Status code, as `FsReadToString`.
282 FsRemoveDirAll,
283 /// `(str) -> i64` — list the directory's entry names, sorted. Status
284 /// code, as `FsReadToString`; on success the names are waiting in
285 /// `FsTakeStringArray`.
286 FsReadDir,
287 /// `() -> ptr` — take the `[String]` staged by a successful `FsReadDir`.
288 FsTakeStringArray,
289 /// `(str) -> i64` — what the path is: 0 = metadata unavailable (absent,
290 /// or unreadable), 1 file, 2 directory.
291 FsKind,
292 /// `(str) -> i64` — read the path's raw bytes. `0` on success, with the
293 /// contents waiting in `FsTakeBytes`; otherwise an `IoErrorKind` status
294 /// code, as `FsReadToString`. Unlike `FsReadToString` this cannot produce
295 /// `INVALID_DATA`: there is no encoding to violate.
296 FsRead,
297 /// `() -> bytes` — take the payload staged by a successful `FsRead`.
298 FsTakeBytes,
299 /// `(str, bytes) -> i64` — write the bytes to the path named by the
300 /// first argument, truncating. Status code, as `FsReadToString`.
301 FsWrite,
302 /// `(ptr, i8, i8, i8, i8, i8, i8) -> i64` — open the path through
303 /// `std::fs::OpenOptions`, forwarding the six flags (read, write, append,
304 /// truncate, create, create_new) one for one. `0` on success, with the
305 /// new fd waiting in `FsTakeBytes`, encoded as an 8-byte little-endian
306 /// value; otherwise an `IoErrorKind` status code, as `FsReadToString`.
307 FileOpen,
308 /// `(i64 fd) -> i64` — close `fd`, dropping the underlying file and
309 /// releasing its OS handle. Idempotent: closing an already-closed,
310 /// stale, or forged fd still reports success.
311 FileClose,
312 /// `(i64 fd, i64 max) -> i64` — read up to `max` bytes from `fd`. `0` on
313 /// success, with the bytes waiting in `FsTakeBytes`; otherwise an
314 /// `IoErrorKind` status code, as `FsReadToString` — including a closed,
315 /// stale, or forged fd. An empty payload means end of stream; a short
316 /// read does not.
317 FileRead,
318 /// `(i64 fd, bytes) -> i64` — write the bytes to `fd` with one
319 /// `Write::write` call, not a `write_all` loop. Status code, as
320 /// `FsReadToString` — including a closed, stale, or forged fd — with the
321 /// byte count waiting in `FsTakeBytes` on success, encoded as an 8-byte
322 /// little-endian count.
323 FileWrite,
324 /// `(i64 fd) -> i64` — flush `fd`. Status code, as `FsReadToString`,
325 /// including a closed, stale, or forged fd; no payload.
326 FileFlush,
327 /// `(i64) -> i64` — read up to `max` bytes from stdin. `0` on success,
328 /// with the bytes waiting in `FsTakeBytes`; otherwise an `IoErrorKind`
329 /// status code, as `FsReadToString`. An empty payload means end of
330 /// stream; a short read does not.
331 IoStdinRead,
332 /// `(bytes) -> i64` — write the bytes to stdout with one `Write::write`
333 /// call, not a `write_all` loop. Status code, as `FsReadToString`; on
334 /// success the number of bytes actually written is waiting in
335 /// `FsTakeBytes`, encoded as an 8-byte little-endian count.
336 IoStdoutWrite,
337 /// `(bytes) -> i64` — write the bytes to stderr. Mirrors `IoStdoutWrite`
338 /// against the other stream.
339 IoStderrWrite,
340 /// `() -> i64` — flush stdout. Status code, as `FsReadToString`; no
341 /// payload.
342 IoStdoutFlush,
343 /// `() -> i64` — flush stderr. Mirrors `IoStdoutFlush` against the other
344 /// stream.
345 IoStderrFlush,
346 /// `(ptr addr) -> ptr to { poll_code, state }` — a fresh future that
347 /// connects to `addr` ("host:port"), non-blockingly.
348 ///
349 /// **A future constructor, like `TaskSleepFutureNanos`/`TaskJoinFuture` above
350 /// — not a status word**, unlike every `FsWrite`/`File*`/`Io*` variant
351 /// above it. `.await`ing the returned future produces the `i64` status:
352 /// `0` on success, with the new fd waiting in `FsTakeBytes` as an 8-byte
353 /// little-endian payload, as `FileOpen`'s own fd; otherwise an
354 /// `IoErrorKind` status code, as `FsReadToString`.
355 NetConnect,
356 /// `(i64 fd) -> i64` — close `fd`, dropping the underlying handle (a
357 /// connection or a listener alike) and releasing its OS handle.
358 /// Idempotent, as `FileClose`.
359 ///
360 /// **Not a future constructor** — it returns its status directly, exactly
361 /// `FileClose`'s shape. `NetListen` and `NetLocalPort` below do the same.
362 /// A roster, not a closed world: this read "are the only other members of
363 /// this group that do", which is true today and is the shape that has
364 /// gone stale twice in this group already.
365 NetClose,
366 /// `(i64 fd, i64 max) -> ptr to { poll_code, state }` — a fresh future
367 /// that reads up to `max` bytes from `fd`, non-blockingly.
368 ///
369 /// A future constructor, as `NetConnect`'s doc comment explains —
370 /// `.await`ing it produces `0` on success, with the bytes waiting in
371 /// `FsTakeBytes`; otherwise an `IoErrorKind` status code, as
372 /// `FsReadToString` — including a closed, stale, or forged fd. An empty
373 /// payload means end of stream; a short read does not.
374 NetRead,
375 /// `(i64 fd, ptr bytes) -> ptr to { poll_code, state }` — a fresh future
376 /// that writes the bytes to `fd` with one `Write::write` attempt, not a
377 /// `write_all` loop, non-blockingly.
378 ///
379 /// A future constructor, as `NetConnect`'s doc comment explains —
380 /// `.await`ing it produces `0` on success, with the byte count actually
381 /// written waiting in `FsTakeBytes`, encoded as an 8-byte little-endian
382 /// count, as `FileWrite`; otherwise an `IoErrorKind` status code, as
383 /// `FsReadToString` — including a closed, stale, or forged fd.
384 NetWrite,
385 /// `(i64 fd, i64 max, i64 ms) -> ptr to { poll_code, state }` — a fresh
386 /// future that reads up to `max` bytes from `fd`, non-blockingly,
387 /// reporting `TIMED_OUT` if `ms` milliseconds pass with nothing to read
388 /// first.
389 ///
390 /// A future constructor, as `NetConnect`'s doc comment explains —
391 /// otherwise identical to `NetRead`, including EOF/short-read semantics
392 /// and where the bytes land on success.
393 NetReadTimeout,
394 /// `(ptr addr) -> i64` — bind and listen on `addr` ("host:port"),
395 /// registering a non-blocking listening socket.
396 ///
397 /// **Not a future constructor**, one of the three members of this group
398 /// that are not, with `NetClose` and `NetLocalPort`: the `bind` and
399 /// `listen` *syscalls* do not block,
400 /// so there is no suspension to model. `0` on success, with the new fd
401 /// waiting in `FsTakeBytes` as an 8-byte little-endian payload, as
402 /// `NetConnect`'s and `FileOpen`'s own; otherwise an `IoErrorKind` status
403 /// code, as `FsReadToString`. Closed with `NetClose`, which serves both
404 /// kinds of handle.
405 ///
406 /// A hostname argument still resolves through a blocking lookup, which no
407 /// future here parks on — see `nova_rt_net_listen`'s own doc comment in
408 /// `crates/nova-runtime/src/net.rs` for that caveat and for how its
409 /// resolution differs from `NetConnect`'s.
410 NetListen,
411 /// `(i64 fd) -> i64` — the port the listening socket behind `fd` is bound
412 /// to, which is how a caller learns the kernel's choice after a `NetListen`
413 /// on port 0.
414 ///
415 /// **Not a future constructor**, with `NetClose` and `NetListen` — a
416 /// roster rather than the ordinal this read before ("the third member of
417 /// this group that is not"), which ranks within a group that grows:
418 /// `local_addr` is a `getsockname` call over bookkeeping the kernel
419 /// already holds, so there is no suspension to model. Unlike `NetListen` it takes no address, so it
420 /// carries none of that variant's blocking-resolution caveat either.
421 ///
422 /// `0` on success, **with the port waiting in `FsTakeBytes`** as an 8-byte
423 /// little-endian payload rather than in the return value, which is the
424 /// status word itself; otherwise an `IoErrorKind` status code, as
425 /// `FsReadToString` — including an fd that names a connected stream rather
426 /// than a listener, which is a wrong-kind miss and not a port.
427 NetLocalPort,
428 /// `(i64 fd) -> ptr to { poll_code, state }` — a fresh future that waits
429 /// for the next incoming connection on the listening socket behind `fd`.
430 ///
431 /// A future constructor, as `NetConnect`'s doc comment explains — unlike
432 /// `NetClose` and `NetLocalPort`, whose syscalls cannot wait on a peer.
433 /// `.await`ing it produces `0` on success, with the accepted connection's
434 /// new fd waiting in `FsTakeBytes` as an 8-byte little-endian payload, as
435 /// `NetListen`'s and `NetConnect`'s own; otherwise an `IoErrorKind` status
436 /// code, as `FsReadToString` — including an fd that names a connected
437 /// stream rather than a listener, which is the same wrong-kind miss
438 /// `NetLocalPort` describes.
439 ///
440 /// The accepted fd is an ordinary stream fd, so `NetRead`, `NetWrite`,
441 /// `NetReadTimeout` and `NetClose` all act on it unchanged.
442 NetAccept,
443 /// `(bytes) -> i64` — the byte length. Not a character count: `Bytes` has
444 /// no encoding.
445 BytesLen,
446 /// `(str) -> bytes` — a `Bytes` holding `s`'s UTF-8 bytes.
447 BytesFromString,
448 /// `(bytes) -> i8` — whether the bytes are valid UTF-8.
449 BytesIsUtf8,
450 /// `(bytes) -> str` — the bytes as a `String`, without checking UTF-8
451 /// validity. The caller must have checked `BytesIsUtf8` first.
452 BytesToStringUnchecked,
453 /// `(bytes, i64) -> i64` — the byte at the given index, as a value in
454 /// `0..=255`. Aborts if the index is out of range.
455 BytesAt,
456 /// `(bytes, i64, i64) -> bytes` — the bytes in `start..end`, clamped to
457 /// the buffer and to `start <= end`.
458 BytesSlice,
459 /// `(bytes, bytes) -> bytes` — the first buffer's bytes followed by the
460 /// second's.
461 BytesConcat,
462 /// `(bytes) -> ptr` — a Nova `[Int]`, one element per byte.
463 BytesToInts,
464 /// `(ptr to [Int]) -> bytes` — a `Bytes` holding each array element as one
465 /// byte. Aborts if any element is outside `0..=255`.
466 BytesFromInts,
467 /// `(bytes, bytes) -> i8` — byte-for-byte equality.
468 BytesEq,
469 /// `() -> i64` — the per-process seed for `Int`, `Bool` and `Char` hashing,
470 /// drawn separately from the string hash's own seed.
471 IntHashSeed,
472 /// `(bytes) -> ptr` — a Nova `[Int]` offset table for an HTTP/1.1 request
473 /// head. Element 0 is a status; a non-zero status yields a length-1 array.
474 HttpParseRequest,
475 /// `(i64, ptr, ptr, ptr) -> i64` — `std/crypto`'s hash/HMAC intrinsic.
476 /// Status word, not the digest: `0` on success for the digest
477 /// operations, with the digest waiting in `FsTakeBytes`; `1` for a
478 /// failed tag check (an answer, not an error); no negative range at
479 /// all, since none of its operations can fail **for any input a Nova
480 /// program can construct** — a bound that is `ring`'s rather than this
481 /// tree's, stated in full on the `ERR_*` block in
482 /// `crates/nova-runtime/src/crypto.rs`.
483 CryptoHash,
484 /// `(i64) -> i64` — `std/crypto`'s random-bytes intrinsic. Status word,
485 /// not the bytes: `0` on success, with the bytes waiting in
486 /// `FsTakeBytes`; otherwise negative, naming one of `crypto.rs`'s
487 /// `ERR_*` kinds (`crates/nova-runtime/src/crypto.rs`).
488 CryptoRandomBytes,
489 /// `(i64, i64) -> i64` — `std/crypto`'s bounded random-integer intrinsic.
490 /// Status word, not the integer: `0` on success, with the value waiting
491 /// in `FsTakeBytes` as an 8-byte little-endian count; otherwise
492 /// negative, naming one of `crypto.rs`'s `ERR_*` kinds
493 /// (`crates/nova-runtime/src/crypto.rs`).
494 CryptoRandomInt,
495 /// `() -> i64` — nanoseconds since the runtime's process epoch.
496 TimeNowNanos,
497 /// `() -> i64` — nanoseconds since the **Unix** epoch. Distinct from
498 /// `TimeNowNanos`, which is process-relative.
499 TimeNowEpochNanos,
500 /// `() -> i64` — the logger's configured threshold, as `LogLevel::to_int`
501 /// numbers it.
502 LogConfigLevel,
503 /// `() -> i64` — `1` for stderr, `0` for stdout.
504 LogConfigToStderr,
505 /// `(i64 level, i64 to_stderr) -> unit` — install a logger configuration,
506 /// overwriting any previous one.
507 LogSetConfig,
508 /// `() -> i8` — whether a termination signal has been received; the first
509 /// call installs the handler (`crates/nova-runtime/src/signal.rs`).
510 ShutdownRequested,
511 /// `() -> i64` — the program's argument count
512 /// (`crates/nova-runtime/src/process.rs`).
513 ProcessArgCount,
514 /// `(i64 i) -> ptr` — argument `i` as a `String`, or the empty string.
515 ProcessArg,
516 /// `(i64 code) -> unit` — `std::process::exit`; never returns.
517 ProcessExit,
518}
519
520impl RtFunc {
521 /// Symbol name registered with the JIT / linker.
522 pub fn symbol(self) -> &'static str {
523 match self {
524 RtFunc::Println => "nova_rt_println",
525 RtFunc::Print => "nova_rt_print",
526 RtFunc::EPrint => "nova_rt_eprint",
527 RtFunc::EPrintln => "nova_rt_eprintln",
528 RtFunc::StrConcat => "nova_rt_str_concat",
529 RtFunc::StrConcatN => "nova_rt_str_concat_n",
530 RtFunc::IntToStr => "nova_rt_int_to_str",
531 RtFunc::FloatToStr => "nova_rt_float_to_str",
532 RtFunc::FloatFixed => "nova_rt_float_fixed",
533 RtFunc::StrToFloat => "nova_rt_str_to_float",
534 RtFunc::BoolToStr => "nova_rt_bool_to_str",
535 RtFunc::CharToStr => "nova_rt_char_to_str",
536 RtFunc::StrEq => "nova_rt_str_eq",
537 RtFunc::StrCmp => "nova_rt_str_cmp",
538 RtFunc::StrHash => "nova_rt_str_hash",
539 RtFunc::StrLenChars => "nova_rt_str_len_chars",
540 RtFunc::StrChars => "nova_rt_str_chars",
541 RtFunc::StrFromChars => "nova_rt_str_from_chars",
542 RtFunc::StrJoin => "nova_rt_str_join",
543 RtFunc::StrIndexOf => "nova_rt_str_index_of",
544 RtFunc::JsonQuote => "nova_rt_json_quote",
545 RtFunc::StrToUpper => "nova_rt_str_to_upper",
546 RtFunc::StrToLower => "nova_rt_str_to_lower",
547 RtFunc::Alloc => "nova_rt_alloc",
548 RtFunc::CheckBounds => "nova_rt_check_bounds",
549 RtFunc::Panic => "nova_rt_panic_str",
550 RtFunc::TestSelector => "nova_rt_test_selector",
551 RtFunc::TaskSpawn => "nova_rt_task_spawn",
552 RtFunc::TaskBlockOn => "nova_rt_task_block_on",
553 RtFunc::TaskIsDone => "nova_rt_task_is_done",
554 RtFunc::TaskTakeOutput => "nova_rt_task_take_output",
555 RtFunc::TaskRelease => "nova_rt_task_release",
556 RtFunc::TaskYieldFuture => "nova_rt_task_yield_future",
557 RtFunc::TaskSleepFutureNanos => "nova_rt_task_sleep_future_nanos",
558 RtFunc::TaskJoinFuture => "nova_rt_task_join_future",
559 RtFunc::TaskTimeoutFuture => "nova_rt_task_timeout_future",
560 RtFunc::FsReadToString => "nova_rt_fs_read_to_string",
561 RtFunc::FsWriteString => "nova_rt_fs_write_string",
562 RtFunc::FsTakeString => "nova_rt_fs_take_string",
563 RtFunc::FsLastErrorMessage => "nova_rt_fs_last_error_message",
564 RtFunc::FsTempDir => "nova_rt_fs_temp_dir",
565 RtFunc::FsExists => "nova_rt_fs_exists",
566 RtFunc::FsCreateDir => "nova_rt_fs_create_dir",
567 RtFunc::FsCreateDirAll => "nova_rt_fs_create_dir_all",
568 RtFunc::FsRemoveFile => "nova_rt_fs_remove_file",
569 RtFunc::FsRemoveDirAll => "nova_rt_fs_remove_dir_all",
570 RtFunc::FsReadDir => "nova_rt_fs_read_dir",
571 RtFunc::FsTakeStringArray => "nova_rt_fs_take_string_array",
572 RtFunc::FsKind => "nova_rt_fs_kind",
573 RtFunc::FsRead => "nova_rt_fs_read",
574 RtFunc::FsTakeBytes => "nova_rt_fs_take_bytes",
575 RtFunc::FsWrite => "nova_rt_fs_write",
576 RtFunc::FileOpen => "nova_rt_file_open",
577 RtFunc::FileClose => "nova_rt_file_close",
578 RtFunc::FileRead => "nova_rt_file_read",
579 RtFunc::FileWrite => "nova_rt_file_write",
580 RtFunc::FileFlush => "nova_rt_file_flush",
581 RtFunc::IoStdinRead => "nova_rt_io_stdin_read",
582 RtFunc::IoStdoutWrite => "nova_rt_io_stdout_write",
583 RtFunc::IoStderrWrite => "nova_rt_io_stderr_write",
584 RtFunc::IoStdoutFlush => "nova_rt_io_stdout_flush",
585 RtFunc::IoStderrFlush => "nova_rt_io_stderr_flush",
586 RtFunc::NetConnect => "nova_rt_net_connect_future",
587 RtFunc::NetClose => "nova_rt_net_close",
588 RtFunc::NetRead => "nova_rt_net_read_future",
589 RtFunc::NetWrite => "nova_rt_net_write_future",
590 RtFunc::NetReadTimeout => "nova_rt_net_read_timeout_future",
591 RtFunc::NetListen => "nova_rt_net_listen",
592 RtFunc::NetLocalPort => "nova_rt_net_local_port",
593 RtFunc::NetAccept => "nova_rt_net_accept_future",
594 RtFunc::BytesLen => "nova_rt_bytes_len",
595 RtFunc::BytesFromString => "nova_rt_bytes_from_string",
596 RtFunc::BytesIsUtf8 => "nova_rt_bytes_is_utf8",
597 RtFunc::BytesToStringUnchecked => "nova_rt_bytes_to_string_unchecked",
598 RtFunc::BytesAt => "nova_rt_bytes_at",
599 RtFunc::BytesSlice => "nova_rt_bytes_slice",
600 RtFunc::BytesConcat => "nova_rt_bytes_concat",
601 RtFunc::BytesToInts => "nova_rt_bytes_to_ints",
602 RtFunc::BytesFromInts => "nova_rt_bytes_from_ints",
603 RtFunc::BytesEq => "nova_rt_bytes_eq",
604 RtFunc::IntHashSeed => "nova_rt_int_hash_seed",
605 RtFunc::HttpParseRequest => "nova_rt_http_parse_request",
606 RtFunc::CryptoHash => "nova_rt_crypto_hash",
607 RtFunc::CryptoRandomBytes => "nova_rt_crypto_random_bytes",
608 RtFunc::CryptoRandomInt => "nova_rt_crypto_random_int",
609 RtFunc::TimeNowNanos => "nova_rt_time_now_nanos",
610 RtFunc::TimeNowEpochNanos => "nova_rt_time_now_epoch_nanos",
611 RtFunc::LogConfigLevel => "nova_rt_log_config_level",
612 RtFunc::LogConfigToStderr => "nova_rt_log_config_to_stderr",
613 RtFunc::LogSetConfig => "nova_rt_log_set_config",
614 RtFunc::ShutdownRequested => "nova_rt_shutdown_requested",
615 RtFunc::ProcessArgCount => "nova_rt_process_arg_count",
616 RtFunc::ProcessArg => "nova_rt_process_arg",
617 RtFunc::ProcessExit => "nova_rt_process_exit",
618 }
619 }
620
621 /// Parameter and return classes: `(params, ret)`.
622 pub fn signature(self) -> (Vec<MirTy>, MirTy) {
623 match self {
624 RtFunc::Println | RtFunc::Print | RtFunc::EPrint | RtFunc::EPrintln => {
625 (vec![MirTy::Ptr], MirTy::Unit)
626 }
627 RtFunc::StrConcat => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::Ptr),
628 RtFunc::StrConcatN => (vec![MirTy::Ptr], MirTy::Ptr),
629 RtFunc::IntToStr => (vec![MirTy::I64], MirTy::Ptr),
630 RtFunc::FloatToStr => (vec![MirTy::F64], MirTy::Ptr),
631 RtFunc::FloatFixed => (vec![MirTy::F64, MirTy::I64], MirTy::Ptr),
632 RtFunc::StrToFloat => (vec![MirTy::Ptr], MirTy::F64),
633 RtFunc::BoolToStr => (vec![MirTy::I8], MirTy::Ptr),
634 RtFunc::CharToStr => (vec![MirTy::I64], MirTy::Ptr),
635 RtFunc::StrEq => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::I8),
636 RtFunc::StrCmp => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::I64),
637 RtFunc::StrHash => (vec![MirTy::Ptr], MirTy::I64),
638 RtFunc::StrLenChars => (vec![MirTy::Ptr], MirTy::I64),
639 RtFunc::StrChars => (vec![MirTy::Ptr], MirTy::Ptr),
640 RtFunc::StrFromChars => (vec![MirTy::Ptr], MirTy::Ptr),
641 RtFunc::StrJoin => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::Ptr),
642 RtFunc::StrIndexOf => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::I64),
643 RtFunc::JsonQuote => (vec![MirTy::Ptr], MirTy::Ptr),
644 RtFunc::StrToUpper | RtFunc::StrToLower => (vec![MirTy::Ptr], MirTy::Ptr),
645 RtFunc::Alloc => (vec![MirTy::I64], MirTy::Ptr),
646 RtFunc::CheckBounds => (vec![MirTy::I64, MirTy::I64], MirTy::Unit),
647 RtFunc::Panic => (vec![MirTy::Ptr], MirTy::Unit),
648 RtFunc::TestSelector => (vec![], MirTy::I64),
649 RtFunc::TaskSpawn | RtFunc::TaskBlockOn => (vec![MirTy::Ptr], MirTy::I64),
650 RtFunc::TaskIsDone => (vec![MirTy::Ptr], MirTy::I8),
651 RtFunc::TaskTakeOutput => (vec![MirTy::I64], MirTy::I64),
652 RtFunc::TaskRelease => (vec![MirTy::Ptr], MirTy::Unit),
653 RtFunc::TaskYieldFuture => (vec![], MirTy::Ptr),
654 RtFunc::TaskSleepFutureNanos => (vec![MirTy::I64], MirTy::Ptr),
655 RtFunc::TaskJoinFuture => (vec![MirTy::Ptr], MirTy::Ptr),
656 RtFunc::TaskTimeoutFuture => (vec![MirTy::I64, MirTy::Ptr], MirTy::Ptr),
657 RtFunc::FsReadToString => (vec![MirTy::Ptr], MirTy::I64),
658 RtFunc::FsWriteString => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::I64),
659 RtFunc::FsTakeString
660 | RtFunc::FsLastErrorMessage
661 | RtFunc::FsTempDir
662 | RtFunc::FsTakeStringArray => (vec![], MirTy::Ptr),
663 RtFunc::FsExists => (vec![MirTy::Ptr], MirTy::I8),
664 RtFunc::FsCreateDir
665 | RtFunc::FsCreateDirAll
666 | RtFunc::FsRemoveFile
667 | RtFunc::FsRemoveDirAll
668 | RtFunc::FsReadDir
669 | RtFunc::FsKind
670 | RtFunc::FsRead => (vec![MirTy::Ptr], MirTy::I64),
671 RtFunc::FsTakeBytes => (vec![], MirTy::Ptr),
672 RtFunc::FsWrite => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::I64),
673 RtFunc::FileOpen => (
674 vec![
675 MirTy::Ptr,
676 MirTy::I8,
677 MirTy::I8,
678 MirTy::I8,
679 MirTy::I8,
680 MirTy::I8,
681 MirTy::I8,
682 ],
683 MirTy::I64,
684 ),
685 RtFunc::FileClose | RtFunc::FileFlush => (vec![MirTy::I64], MirTy::I64),
686 RtFunc::FileRead => (vec![MirTy::I64, MirTy::I64], MirTy::I64),
687 RtFunc::FileWrite => (vec![MirTy::I64, MirTy::Ptr], MirTy::I64),
688 RtFunc::IoStdinRead => (vec![MirTy::I64], MirTy::I64),
689 RtFunc::IoStdoutWrite | RtFunc::IoStderrWrite => (vec![MirTy::Ptr], MirTy::I64),
690 RtFunc::IoStdoutFlush | RtFunc::IoStderrFlush => (vec![], MirTy::I64),
691 // Most of this group return `MirTy::Ptr` (a future's `{ poll_code,
692 // state }` pointer), not `MirTy::I64` — the same shape
693 // `TaskSleepFutureNanos`/`TaskJoinFuture` use above, unlike every
694 // `FsWrite`/`File*`/`Io*` entry directly above this group.
695 // `NetClose`, `NetListen` and `NetLocalPort` are the three that
696 // return `MirTy::I64`, exactly `FileClose`'s shape. `NetListen` and
697 // `NetConnect` take the same single `Ptr` (an address string) and
698 // differ only in the return: `I64` for the status word, `Ptr` for
699 // the future.
700 //
701 // This table *declares* those classes and checks nothing itself.
702 // Its consumers are the two backends, which build every
703 // declaration and call from it, and LLVM's
704 // `every_rt_func_is_declared_with_its_real_signature` pins each
705 // entry against the IR that comes out. What nothing pins is a
706 // *lowering arm's choice of variant*: `lower.rs` naming
707 // `NetConnect` where it meant `NetListen` would emit a well-formed
708 // call with the wrong return class, caught by neither this table
709 // nor that test.
710 //
711 // `NetClose` and `NetLocalPort` sharpen that hazard, because they
712 // are *indistinguishable here* -- both `(vec![I64], I64)` -- so a
713 // lowering arm confusing the two would emit a call this table
714 // cannot fault and that IR test cannot fault either, and the only
715 // thing that separates them is which symbol `symbol()` names. The
716 // typeck-side `builtin_signatures_are_what_the_std_call_sites_use`
717 // is equally blind to the swap.
718 //
719 // Only a test that *runs* the compiled program catches it, and
720 // both directions now do. `NetClose` lowered to `NetLocalPort`
721 // would run `nova_rt_net_local_port` against a stream fd, miss the
722 // listener kind check, and make `TcpStream::close` report `Err`, so
723 // `tests/runtime/net_lifetime.stdout`'s first golden line
724 // (`close: ok`) and `net_roundtrip.stdout`'s last one both fail.
725 // `NetLocalPort` lowered to `NetClose` was unpinned until
726 // `tests/runtime/net_listener_accept.nova` -- the first and still
727 // only fixture that calls `local_port`. That direction was
728 // measured, not reasoned about: with `local_port`'s body pointed at
729 // `net_close`, that fixture's first golden line flips to
730 // `server: port in range false` (the close returns status `0`, so
731 // the `Ok` arm still runs and decodes the payload slot `bind`
732 // already emptied -- to something outside 1..=65535, which is what
733 // that flipped line measures) *and* the now-closed listener makes
734 // the next line `nova: panic: accept: socket is not open`, exit
735 // 127. So the swap fails both the golden and the `.success()`
736 // assertion, and
737 // fails them on the fixture's *first* line rather than hanging --
738 // which is the whole reason that line asserts the port's range
739 // instead of trusting the client's `connect` to notice.
740 //
741 // Anyone re-running that measurement must rebuild the compiler
742 // first: every `std/*/lib.nova` is `include_str!`'d into it, so a
743 // stale binary exercises the std baked into it rather than the
744 // mutation, and reports a **false pass**. The fixture's own
745 // header states the same hazard at length; it is repeated here
746 // because this is the other place the mutation is described, and
747 // a reader who arrives here first would otherwise not meet it.
748 //
749 // `NetAccept` shares that same single-`I64` parameter list and
750 // differs from the pair only in returning `Ptr`. An earlier
751 // version of this paragraph drew a conclusion from that which
752 // this block's own premise above forbids: it said confusing
753 // `NetAccept` with either of those two "changes the return class
754 // and this table faults it, exactly as it would fault
755 // `NetConnect` swapped for `NetListen`". That contradicted the
756 // premise -- this table declares and checks nothing -- and named
757 // the very same hypothetical swap the paragraph above calls
758 // caught by nothing. **The premise is the half that survives.**
759 //
760 // What a return-class change actually does, **read rather than
761 // run**, since no build here confirmed it: `Stmt::CallRuntime`
762 // carries no expected return type at all -- only `dst`, `func`
763 // and `args`. `lower_call` types `dst` from typeck's signature
764 // for the call expression, while the LLVM backend emits the call
765 // under this table's own `func.signature()` return. So a swapped
766 // variant makes those two disagree and surfaces as mismatched IR
767 // **in that backend alone**; Cranelift's `def_temp` would not see
768 // it, because `I64` and `Ptr` are one type there on a 64-bit
769 // target. A return-class change is therefore not caught *here*,
770 // and downstream it is caught on only one of two backends. The
771 // indistinguishable pair stays a pair, and nothing in this table
772 // is what separates any of them.
773 RtFunc::NetConnect => (vec![MirTy::Ptr], MirTy::Ptr),
774 RtFunc::NetClose => (vec![MirTy::I64], MirTy::I64),
775 RtFunc::NetRead => (vec![MirTy::I64, MirTy::I64], MirTy::Ptr),
776 RtFunc::NetWrite => (vec![MirTy::I64, MirTy::Ptr], MirTy::Ptr),
777 RtFunc::NetReadTimeout => (vec![MirTy::I64, MirTy::I64, MirTy::I64], MirTy::Ptr),
778 RtFunc::NetListen => (vec![MirTy::Ptr], MirTy::I64),
779 RtFunc::NetLocalPort => (vec![MirTy::I64], MirTy::I64),
780 RtFunc::NetAccept => (vec![MirTy::I64], MirTy::Ptr),
781 RtFunc::BytesLen => (vec![MirTy::Ptr], MirTy::I64),
782 RtFunc::BytesFromString => (vec![MirTy::Ptr], MirTy::Ptr),
783 RtFunc::BytesIsUtf8 => (vec![MirTy::Ptr], MirTy::I8),
784 RtFunc::BytesToStringUnchecked => (vec![MirTy::Ptr], MirTy::Ptr),
785 RtFunc::BytesAt => (vec![MirTy::Ptr, MirTy::I64], MirTy::I64),
786 RtFunc::BytesSlice => (vec![MirTy::Ptr, MirTy::I64, MirTy::I64], MirTy::Ptr),
787 RtFunc::BytesConcat => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::Ptr),
788 RtFunc::BytesToInts => (vec![MirTy::Ptr], MirTy::Ptr),
789 RtFunc::BytesFromInts => (vec![MirTy::Ptr], MirTy::Ptr),
790 RtFunc::BytesEq => (vec![MirTy::Ptr, MirTy::Ptr], MirTy::I8),
791 RtFunc::IntHashSeed => (vec![], MirTy::I64),
792 RtFunc::HttpParseRequest => (vec![MirTy::Ptr], MirTy::Ptr),
793 RtFunc::CryptoHash => (
794 vec![MirTy::I64, MirTy::Ptr, MirTy::Ptr, MirTy::Ptr],
795 MirTy::I64,
796 ),
797 RtFunc::CryptoRandomBytes => (vec![MirTy::I64], MirTy::I64),
798 RtFunc::CryptoRandomInt => (vec![MirTy::I64, MirTy::I64], MirTy::I64),
799 RtFunc::TimeNowNanos => (vec![], MirTy::I64),
800 RtFunc::TimeNowEpochNanos => (vec![], MirTy::I64),
801 RtFunc::LogConfigLevel => (vec![], MirTy::I64),
802 RtFunc::LogConfigToStderr => (vec![], MirTy::I64),
803 RtFunc::LogSetConfig => (vec![MirTy::I64, MirTy::I64], MirTy::Unit),
804 RtFunc::ShutdownRequested => (vec![], MirTy::I8),
805 RtFunc::ProcessArgCount => (vec![], MirTy::I64),
806 RtFunc::ProcessArg => (vec![MirTy::I64], MirTy::Ptr),
807 RtFunc::ProcessExit => (vec![MirTy::I64], MirTy::Unit),
808 }
809 }
810}
811
812#[cfg(test)]
813mod rt_func_tests {
814 use super::RtFunc;
815
816 /// `RtFunc::ALL` is generated by the `rt_funcs!` macro from the same
817 /// variant list that defines the enum, so it cannot omit a variant by
818 /// construction — but a copy-paste duplicate in that list is still
819 /// possible (e.g. writing `Panic` twice instead of adding a 13th
820 /// variant). Guard against that directly: every entry's symbol and
821 /// signature must be reachable and distinct.
822 #[test]
823 fn all_lists_every_variant_exactly_once() {
824 let symbols: Vec<&str> = RtFunc::ALL.iter().map(|f| f.symbol()).collect();
825 let mut sorted = symbols.clone();
826 sorted.sort_unstable();
827 sorted.dedup();
828 assert_eq!(
829 sorted.len(),
830 symbols.len(),
831 "RtFunc::ALL has a duplicate entry: {symbols:?}"
832 );
833 // Every variant's signature must resolve without panicking (the
834 // match in `signature()` is separately exhaustive, so this always
835 // holds — asserted here so a future refactor that breaks that
836 // guarantee fails a test, not just a build).
837 for f in RtFunc::ALL {
838 let _ = f.signature();
839 }
840 }
841}
842
843/// The largest element count `ArrayAlloc` may be handed.
844///
845/// Both backends compute the allocation size as `8*len + 8` in wrapping `i64`
846/// arithmetic, so any longer array would wrap the size — see `ArrayAlloc`. This
847/// is the exact largest `len` for which `8*len + 8` still fits in an `i64`.
848pub const MAX_ARRAY_LEN: i64 = (i64::MAX - 8) / 8;
849
850/// A single 3-address statement.
851#[derive(Debug)]
852pub enum Stmt {
853 ConstInt(Temp, i64),
854 ConstFloat(Temp, f64),
855 ConstBool(Temp, bool),
856 /// Interned string literal; codegen emits it as module data.
857 ConstStr(Temp, String),
858 ConstUnit(Temp),
859 Copy {
860 dst: Temp,
861 src: Temp,
862 },
863 Bin {
864 dst: Temp,
865 op: hir::BinOp,
866 class: OperandClass,
867 lhs: Temp,
868 rhs: Temp,
869 },
870 Neg {
871 dst: Temp,
872 class: OperandClass,
873 src: Temp,
874 },
875 /// Boolean not.
876 Not {
877 dst: Temp,
878 src: Temp,
879 },
880 /// Bitwise not (I64).
881 BitNot {
882 dst: Temp,
883 src: Temp,
884 },
885 /// Direct call to another Nova function by mangled name.
886 Call {
887 dst: Option<Temp>,
888 callee: String,
889 args: Vec<Temp>,
890 },
891 /// Indirect call through a function value (fat pointer). `callee` is a
892 /// `{ code_ptr, env_ptr }` block; codegen loads both and calls
893 /// `code_ptr(env_ptr, args...)`. `params` are the real parameter
894 /// classes (excluding the leading env).
895 CallIndirect {
896 dst: Option<Temp>,
897 callee: Temp,
898 params: Vec<MirTy>,
899 ret: MirTy,
900 args: Vec<Temp>,
901 },
902 /// A suspend point — `future.await` — before the async state-machine
903 /// transform has turned it into control flow.
904 ///
905 /// **No statement reaching a codegen backend is this one.** It is a marker
906 /// that `async_lower`'s block split consumes, replacing it with the
907 /// sequence that actually performs an await: poll `future` through its own
908 /// fat pointer, return `POLL_PENDING` if the answer is pending, and
909 /// otherwise copy the awaited value out of the inner state object's output
910 /// slot into `dst`'s slot. Every part of that sequence addresses the state
911 /// object of the function containing the await, which does not exist until
912 /// the transform builds it — so the suspend cannot be lowered into control
913 /// flow where it is parsed without inventing that layout twice. Both
914 /// backends reject this variant rather than emitting anything for it.
915 ///
916 /// `future` holds a `{ poll_code, state }` two-word future value, the same
917 /// fat-pointer shape `MakeClosure` builds. `dst` is `None` exactly when the
918 /// awaited output is unit, matching `Call`'s convention for a call that
919 /// produces no value.
920 Await {
921 dst: Option<Temp>,
922 future: Temp,
923 },
924 /// Call into the Nova runtime.
925 CallRuntime {
926 dst: Option<Temp>,
927 func: RtFunc,
928 args: Vec<Temp>,
929 },
930 /// Build a function value: a 2-word fat pointer `{ code_ptr, env_ptr }`.
931 /// `code` is the mangled callee whose address is `code_ptr`; `captures`
932 /// are stored into a freshly allocated environment (empty → null env).
933 MakeClosure {
934 dst: Temp,
935 code: String,
936 captures: Vec<(Temp, MirTy)>,
937 },
938 /// Allocate and initialize a sum value: `{ tag, fields... }`.
939 MakeSum {
940 dst: Temp,
941 tag: u32,
942 fields: Vec<(Temp, MirTy)>,
943 },
944 /// Load the tag of a sum value.
945 SumTag {
946 dst: Temp,
947 sum: Temp,
948 },
949 /// Load payload field `index` of a sum value.
950 SumField {
951 dst: Temp,
952 sum: Temp,
953 index: u32,
954 ty: MirTy,
955 },
956 /// Allocate and initialize a record value `{ fields... }` (no tag).
957 MakeRecord {
958 dst: Temp,
959 fields: Vec<(Temp, MirTy)>,
960 },
961 /// Load field `index` of a record value.
962 RecordField {
963 dst: Temp,
964 record: Temp,
965 index: u32,
966 ty: MirTy,
967 },
968 /// Store field `index` of a record. Mirrors `RecordField`'s `8 * index`
969 /// offset, so reads and writes stay in the same layout.
970 SetField {
971 record: Temp,
972 index: u32,
973 value: Temp,
974 ty: MirTy,
975 },
976 /// Allocate and initialize an array `{ len, elems... }`.
977 MakeArray {
978 dst: Temp,
979 elems: Vec<(Temp, MirTy)>,
980 },
981 /// Allocate an array of `len` elements: `8 + 8*len` zeroed bytes with `len`
982 /// stored at offset 0. Elements are left zeroed — the lowering fills them
983 /// with its own loop (see `lower_array_repeat`), so neither backend needs a
984 /// loop emitter.
985 ///
986 /// `len` must be in `0..=MAX_ARRAY_LEN`; the lowering guarantees that by
987 /// emitting guards that abort first. Both bounds are memory-safety
988 /// requirements, because both backends compute the size as `8*len + 8` with
989 /// *wrapping* arithmetic:
990 ///
991 /// - A small negative `len` is harmless on its own — `gc::alloc` clamps
992 /// every request with `size.max(8)`, so `8 + 8*(-1) = 0` still yields a
993 /// block the 8-byte length store fits in — but storing a negative length
994 /// would leave an array whose every access fails, and a large-magnitude
995 /// negative `len` overflows the `8 * len` multiplication into a wild size.
996 /// - A `len` above `MAX_ARRAY_LEN` wraps `8*len + 8` back to a *negative*
997 /// size (at `len = 2^60` it is exactly `i64::MIN + 8`), which the same
998 /// `size.max(8)` clamp turns into an 8-byte block. The huge length is then
999 /// stored into that block's header and the fill loop — which carries no
1000 /// bounds check by design — writes far past its end.
1001 ///
1002 /// Aborting up front reports the mistake where it was made instead.
1003 ArrayAlloc {
1004 dst: Temp,
1005 len: Temp,
1006 },
1007 /// Load the length (element count) of an array.
1008 ArrayLen {
1009 dst: Temp,
1010 arr: Temp,
1011 },
1012 /// Load `arr[index]` (dynamic index; caller has bounds-checked).
1013 ArrayGet {
1014 dst: Temp,
1015 arr: Temp,
1016 index: Temp,
1017 ty: MirTy,
1018 },
1019 /// Store `arr[index] = value` (dynamic index; caller has bounds-checked).
1020 ArraySet {
1021 arr: Temp,
1022 index: Temp,
1023 value: Temp,
1024 ty: MirTy,
1025 },
1026}
1027
1028/// Block terminators.
1029#[derive(Debug)]
1030pub enum Terminator {
1031 Goto(BlockId),
1032 Branch {
1033 cond: Temp,
1034 then_: BlockId,
1035 else_: BlockId,
1036 },
1037 /// Multi-way branch on an integer discriminant (sum tag or Int value).
1038 Switch {
1039 disc: Temp,
1040 arms: Vec<(i64, BlockId)>,
1041 default: BlockId,
1042 },
1043 Return(Option<Temp>),
1044 /// Unreachable (e.g. the default of an exhaustive match).
1045 Trap,
1046}
1047
1048/// Map a concrete HIR type to its machine class.
1049///
1050/// Must only be called on monomorphized types (no `Param`/`Var`).
1051pub fn mir_ty(ty: &hir::Ty) -> MirTy {
1052 match ty {
1053 hir::Ty::Int | hir::Ty::Char => MirTy::I64,
1054 hir::Ty::Float => MirTy::F64,
1055 hir::Ty::Bool => MirTy::I8,
1056 hir::Ty::String
1057 | hir::Ty::Bytes
1058 | hir::Ty::Fn { .. }
1059 | hir::Ty::Sum { .. }
1060 | hir::Ty::Record { .. }
1061 | hir::Ty::Array(_)
1062 | hir::Ty::Future(_) => MirTy::Ptr,
1063 hir::Ty::Unit | hir::Ty::Never => MirTy::Unit,
1064 // Post-typeck these should not occur; map defensively. `Assoc` is
1065 // normalized away by `mono` before lowering, and a projection that
1066 // survives that is reported as `E0079` rather than reaching here.
1067 //
1068 // This arm was *not* merely defensive before that landed: `Assoc`
1069 // reached it from ordinary source, and mapping to `MirTy::Unit` is
1070 // not the harmless default it looks like — **unit parameters are
1071 // dropped from the Cranelift signature**, so a caller passed two
1072 // arguments to a one-argument function, and a generic function
1073 // returning a projection returned wrong values with exit 0 and no
1074 // diagnostic. So reaching here is a compiler bug, but one that must
1075 // not panic in a library path.
1076 hir::Ty::Param(_) | hir::Ty::Var(_) | hir::Ty::Error | hir::Ty::Assoc { .. } => MirTy::Unit,
1077 }
1078}
1079
1080/// Mangle a function instance into a unique symbol name.
1081///
1082/// The owning `def_id` is folded in so that same-named items from different
1083/// modules (e.g. a private `helper` in each) get distinct symbols instead of
1084/// colliding into one at monomorphization; `type_args` further distinguish the
1085/// instances of a generic function. A non-generic function mangles to
1086/// `name.<def>` and a monomorphized instance to `name.<def>$<args>` (e.g.
1087/// `identity.7$i`).
1088///
1089/// The program entry point is the sole exception: `lower_module` names it with
1090/// the bare symbol `main`, which the codegen backends look up by name. It is
1091/// never passed here for a non-entry function of the same name, because every
1092/// other function carries its DefId suffix and so can never spell `main`.
1093pub fn mangle(def_id: DefId, name: &str, type_args: &[hir::Ty]) -> String {
1094 if type_args.is_empty() {
1095 return format!("{name}.{}", def_id.0);
1096 }
1097 let args: Vec<String> = type_args.iter().map(mangle_ty).collect();
1098 format!("{name}.{}${}", def_id.0, args.join("_"))
1099}
1100
1101fn mangle_ty(ty: &hir::Ty) -> String {
1102 match ty {
1103 hir::Ty::Int => "i".to_string(),
1104 hir::Ty::Float => "f".to_string(),
1105 hir::Ty::Bool => "b".to_string(),
1106 hir::Ty::Char => "c".to_string(),
1107 hir::Ty::String => "s".to_string(),
1108 // Distinct from `String`'s `"s"`: two types mangling to the same
1109 // string is the exact miscompile class `d49f896` shipped (see the
1110 // defensive `"X"` arm's comment below) -- two monomorphized instances
1111 // would collide on one symbol and both dispatch to the first one's
1112 // code.
1113 hir::Ty::Bytes => "y".to_string(),
1114 hir::Ty::Unit => "u".to_string(),
1115 hir::Ty::Never => "n".to_string(),
1116 hir::Ty::Fn { params, ret } => {
1117 let ps: Vec<String> = params.iter().map(mangle_ty).collect();
1118 format!("F{}R{}E", ps.join(""), mangle_ty(ret))
1119 }
1120 hir::Ty::Sum { def_id, args } => {
1121 if args.is_empty() {
1122 format!("S{}", def_id.0)
1123 } else {
1124 let a: Vec<String> = args.iter().map(mangle_ty).collect();
1125 format!("S{}L{}E", def_id.0, a.join(""))
1126 }
1127 }
1128 hir::Ty::Record { def_id, args } => {
1129 if args.is_empty() {
1130 format!("R{}", def_id.0)
1131 } else {
1132 let a: Vec<String> = args.iter().map(mangle_ty).collect();
1133 format!("R{}L{}E", def_id.0, a.join(""))
1134 }
1135 }
1136 hir::Ty::Array(elem) => format!("A{}E", mangle_ty(elem)),
1137 // `U` is not otherwise used as a leading letter here (nullary:
1138 // i/f/b/c/s/y/u/n; compound: F/S/R/A above), and the mangling must be
1139 // output-dependent like `Array`'s `A{elem}E` rather than a shared
1140 // placeholder — see `mangle_ty_distinguishes_futures_by_output_type`.
1141 hir::Ty::Future(out) => format!("U{}E", mangle_ty(out)),
1142 // Post-typeck these should not occur; map defensively, matching
1143 // `mir_ty`'s convention above (`Assoc` is normalized away by `mono`
1144 // before a type reaches mangling).
1145 //
1146 // **This shared `"X"` is a latent symbol collision, not a harmless
1147 // placeholder.** Four semantically distinct things map to it, and
1148 // this function's output is `mono`'s `done` dedup key — so two
1149 // genuinely different instantiations can produce the same symbol,
1150 // and the second is *skipped as already-emitted* while both dispatch
1151 // to the first's code. That is the same class as the head-only
1152 // mangling this project shipped as a miscompile in `d49f896`. It was
1153 // reproduced by mutation and is pinned; it is unreachable today only
1154 // because `mono` normalizes `type_args` before requesting a symbol.
1155 // The right fix is to make them *distinguishable* rather than to
1156 // panic — `mangle` runs before `E0011`, so aborting would turn a
1157 // diagnostic into a crash. Queued.
1158 hir::Ty::Param(_) | hir::Ty::Var(_) | hir::Ty::Error | hir::Ty::Assoc { .. } => {
1159 "X".to_string()
1160 }
1161 }
1162}