Skip to main content

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}