nova_runtime/lib.rs
1//! The Nova runtime: support functions linked into every Nova program.
2//!
3//! Phase 1 scope (spec `13-RUNTIME.md`, "minimal: panic, allocator, basic
4//! types"): string values, console output, and sum-type allocation. All
5//! functions use the C ABI so compiled Nova code (Cranelift/LLVM) can call
6//! them directly; for `nova run` they are registered as JIT symbols via
7//! [`symbols`].
8//!
9//! **Memory:** heap values (records, sums, arrays, closures, strings, and
10//! `Bytes`) are managed by a conservative mark-and-sweep garbage
11//! collector — see [`gc`] and `docs/adr/0002-phase1-leaking-allocator.md`.
12//! All heap allocation routes through [`gc::alloc`], which reclaims
13//! unreachable objects.
14
15use std::sync::OnceLock;
16
17/// Byte-buffer intrinsics for `std/bytes`. `pub`, not private, for the same
18/// reason as [`fs`]: its own doc comment explains the boundary it implements
19/// (one representation, shared with `NovaStr`, distinguished only by
20/// `hir::Ty`). Branch `byte-type`'s own Task 3
21/// (`docs/superpowers/plans/2026-08-12-byte-type.md`) added the rest of the
22/// byte surface beside this module's first four intrinsics, the same way
23/// this crate's other modules build on `task`.
24pub mod bytes;
25/// SHA-256, SHA-512, HMAC-SHA-256 and its constant-time check, plus random
26/// bytes and a bounded random integer, for `std/crypto`. Private, like
27/// [`file`], [`http`], [`net`], [`poll`] and [`log`]: nothing outside this
28/// crate names `crypto::` directly -- the three `nova_rt_crypto_*` symbols
29/// reach the JIT and linked binaries through their `#[no_mangle]` C names and
30/// through [`symbols`], which needs only this module's *items* public, not
31/// the module path itself.
32mod crypto;
33/// The open-file table and its five intrinsics for `std/fs`'s `File`.
34/// Private, not `pub` like [`fs`]/[`bytes`]/[`io`]: nothing outside this
35/// crate names `file::` directly — the five `nova_rt_file_*` symbols reach
36/// the JIT and linked binaries through their `#[no_mangle]` C names and
37/// through [`symbols`], both of which need only this module's *items*
38/// public, not the module path itself. Branch `file-open-openoptions`'s
39/// Task 1 added this module beside `fs`, `io` and `bytes`, reusing `fs`'s
40/// per-task slot table rather than owning a second one.
41mod file;
42/// Filesystem intrinsics for `std/fs`. `pub`, not private: its own doc
43/// comment explains the boundary it implements; nothing about that requires
44/// hiding the module itself. Branch `std-fs-strings`'s Task 3 and Task 4
45/// (`docs/superpowers/plans/2026-08-11-std-fs-strings.md`) built most of this
46/// module, and branch `byte-type`'s own Task 4 later added `read`/`write`
47/// beside them, the same way this crate's other modules build on `task`.
48pub mod fs;
49mod gc;
50/// The request-head parser for `std/http`. Private, like [`file`], [`net`],
51/// [`poll`] and [`time`]: nothing outside this crate names `http::` directly
52/// -- the `nova_rt_http_parse_request` symbol reaches the JIT and linked
53/// binaries through its `#[no_mangle]` C name and through [`symbols`], which
54/// needs only this module's *items* public, not the module path itself.
55mod http;
56/// The three standard streams' intrinsics for `std/io`. `pub`, not private,
57/// for the same reason as [`fs`] and [`bytes`]: its own doc comment explains
58/// the boundary it implements. Branch `read-write-stdio`'s Task 1 added this
59/// module beside `fs` and `bytes`, reusing `fs`'s per-task slot table rather
60/// than owning a second one.
61pub mod io;
62/// The logger's configuration cell for `std/log`. Private, like [`file`],
63/// [`net`], [`poll`] and [`time`]: nothing outside this crate names `log::`
64/// directly -- the three `nova_rt_log_*` symbols reach the JIT and linked
65/// binaries through their `#[no_mangle]` C names and through [`symbols`].
66mod log;
67/// The open-socket table and the two-phase, non-blocking `connect` for
68/// `std/net`'s `TcpStream`. Private, like [`file`]: nothing outside this
69/// crate names `net::` directly -- the `nova_rt_net_*` symbols reach the JIT
70/// and linked binaries through their `#[no_mangle]` C names and through
71/// [`symbols`], both of which need only this module's *items* public, not
72/// the module path itself. Branch `io-poller-std-net`'s Task 3 added this
73/// module beside `file`, reusing `file`'s handle-table model rather than
74/// inventing a second one, and `poll`'s `RawSocket`/`Interest`/
75/// `set_nonblocking` rather than a second socket representation.
76mod net;
77/// The executor's third wake source (socket readiness), and the two types
78/// (`RawSocket`, `Interest`) that let `task.rs` name a socket wait without
79/// depending on this module's platform types. Private, like [`gc`] and
80/// `file`: nothing outside this crate needs `poll::` directly -- its two
81/// callers are both in this crate, `task.rs` (for `wait`) and `net.rs` (for
82/// `set_nonblocking` and `wait`).
83mod poll;
84/// The program's arguments and `exit`, behind `std/process` (see its module
85/// doc and `docs/adr/0023-program-arguments.md`). `pub` so the driver's JIT
86/// path can set the list, and so `tests/process_args_*.rs` can drive it.
87pub mod process;
88/// The process-wide shutdown flag `std/http`'s `Server::listen` polls (see
89/// its module doc and `docs/adr/0022-process-shutdown-signals.md`). `pub`,
90/// unlike [`time`], so `tests/signal_*.rs` can drive it.
91pub mod signal;
92/// `pub`, not private like [`gc`]: `task`'s ABI constants (`PollFn`,
93/// `POLL_READY`, `STATE_SLOT_TAG`, `STATE_SLOT_TEMPS`) are not all read by
94/// this crate's own runtime logic -- some exist purely as the documented
95/// contract codegen (Task 5's `async_lower.rs`) must reproduce. A private
96/// module would make those `pub` items unreachable from outside the crate,
97/// which both defeats their purpose and makes rustc's `dead_code` lint treat
98/// them as genuinely dead, since nothing inside this crate alone uses them.
99pub mod task;
100/// The monotonic clock behind `std/time`.
101mod time;
102
103/// A Nova string value: immutable UTF-8, `{ len, ptr }`.
104///
105/// A runtime-made `String` or `Bytes` value is **one leaf GC object**: this
106/// header at offset 0, and its bytes inline at offset 16, where `ptr` points
107/// ([`alloc_str_object`]). A string literal's header from
108/// [`nova_rt_str_new`] is a 16-byte leaf whose `ptr` targets static data.
109///
110/// **Invariant: `ptr` never points into another GC object.** Every header is
111/// a leaf, so the collector never traces `ptr`. A header that pointed into a
112/// different GC object would not keep it alive. A future zero-copy slice or
113/// view must not reuse this type with a leaf header. Only
114/// [`alloc_str_object`] and [`nova_rt_str_new`] write a `ptr`, and nothing
115/// in this crate's production code calls `nova_rt_str_new`. The test
116/// `only_the_two_builders_write_a_novastr_ptr` checks both by scanning the
117/// source text; its doc names some of what such a scan cannot see. What the
118/// code generators pass to `nova_rt_str_new` rests on its `# Safety`
119/// contract.
120///
121/// **A convention, not an invariant: [`gc_str_filled`]'s `fill` does not
122/// allocate GC memory.** Memory safety does not depend on it, since the new
123/// object stays reachable from `gc_str_filled`'s own frame across `fill`.
124/// It keeps `fill` cheap, and its liveness argument to the one allocation
125/// before it. Every current writer only calls [`Fill::put`].
126#[repr(C)]
127pub struct NovaStr {
128 pub len: u64,
129 pub ptr: *const u8,
130}
131
132/// The size of a [`NovaStr`] header, and the offset of a runtime-made
133/// string's bytes inside its one object.
134pub(crate) const STR_HEADER: usize = std::mem::size_of::<NovaStr>();
135
136/// Allocate one leaf GC object for a `String` or `Bytes` value of `len` bytes:
137/// the [`NovaStr`] header at offset 0, written here, then room for the bytes
138/// at offset [`STR_HEADER`], which the caller fills. Returns the header and
139/// the address of the bytes.
140///
141/// `max(len, 1)` keeps `ptr` inside the object even when `len` is 0; without
142/// it an empty value's `ptr` would sit one past a 16-byte slot, in the next
143/// object. The size saturates, so an overflowing `len` reaches `gc::alloc`'s
144/// size-limit abort instead of wrapping to a too-small object.
145///
146/// One allocation where there were two: in
147/// `examples/05-json-api/BENCHMARK.md`'s "(alloc-mix)", `String` and `Bytes`
148/// buffers were 67.0 of 300.9 allocations per ten-user request, each with a
149/// separate header.
150pub(crate) fn alloc_str_object(len: usize) -> (*mut NovaStr, *mut u8) {
151 let size = STR_HEADER.saturating_add(len.max(1));
152 let base = gc::alloc(size, false);
153 let node = base as *mut NovaStr;
154 // SAFETY: `base` has `size` writable bytes: the header and at least one
155 // byte after it.
156 let bytes = unsafe { base.add(STR_HEADER) };
157 // SAFETY: as above; nothing else refers to this object yet.
158 unsafe {
159 (*node).len = len as u64;
160 (*node).ptr = bytes;
161 }
162 (node, bytes)
163}
164
165/// Store a Rust string as a GC-managed `NovaStr` value: one leaf object
166/// holding the header and, inline after it, a copy of the bytes.
167///
168/// `pub(crate)`, not private: `fs`'s `gc_message` reuses this rather than
169/// reproducing `NovaStr { len, ptr }`'s layout a second time, which is
170/// precisely the drift class this shared helper exists to avoid.
171pub(crate) fn gc_str(s: &str) -> *mut NovaStr {
172 // SAFETY: the writer puts exactly `s.len()` bytes of valid UTF-8.
173 unsafe { gc_str_filled(s.len(), |out| out.put(s.as_bytes())) }
174}
175
176/// Allocate a GC string of exactly `len` bytes as one leaf object -- the
177/// header, then the bytes inline ([`alloc_str_object`]) -- and let `fill`
178/// write the bytes straight into it.
179///
180/// The builtins that build a result from their arguments use this rather
181/// than collecting into a Rust `String` and calling [`gc_str`]. That buffer
182/// cost a system-heap allocation, a free and a second copy per call: in
183/// `examples/05-json-api/BENCHMARK.md`'s "(reprofile-3)", the three largest
184/// such builtins spent 3.5–3.6% of the server thread in the system heap.
185///
186/// GC safety: there is one allocation, and `fill` runs after it. That
187/// allocation can run a collection, so whatever `fill` reads from the GC heap
188/// must still be reachable then. A caller's arguments are: a pointer held in
189/// the caller's frame or in a callee-saved register is a root, and marking is
190/// range-based, so a pointer into a string's bytes keeps that string's whole
191/// object alive (`gc.rs`'s module doc comment). [`nova_rt_str_chars`] relies
192/// on the same. By convention `fill` does not allocate GC memory (see
193/// [`NovaStr`]); every writer only calls [`Fill::put`].
194///
195/// # Safety
196/// `fill` must write exactly `len` bytes of valid UTF-8 through
197/// [`Fill::put`]. Writing more panics. Writing fewer leaves zero bytes in the
198/// string, which a debug build catches.
199pub(crate) unsafe fn gc_str_filled(len: usize, fill: impl FnOnce(&mut Fill<'_>)) -> *mut NovaStr {
200 let (node, bytes) = alloc_str_object(len);
201 // SAFETY: `alloc_str_object` reserved `len` writable bytes at `bytes`
202 // that nothing else refers to yet.
203 let mut out = Fill {
204 out: unsafe { std::slice::from_raw_parts_mut(bytes, len) },
205 at: 0,
206 };
207 fill(&mut out);
208 debug_assert_eq!(
209 out.at, len,
210 "a GC string's writer filled a different length than it asked for"
211 );
212 node
213}
214
215/// The write position in a byte buffer [`gc_str_filled`] sized exactly.
216pub(crate) struct Fill<'a> {
217 out: &'a mut [u8],
218 at: usize,
219}
220
221impl Fill<'_> {
222 /// Append `bytes`. Past the buffer's end this panics rather than write
223 /// out of bounds, and from an `extern "C"` builtin that panic aborts the
224 /// process.
225 pub(crate) fn put(&mut self, bytes: &[u8]) {
226 self.out[self.at..self.at + bytes.len()].copy_from_slice(bytes);
227 self.at += bytes.len();
228 }
229}
230
231/// Test-only: allocate a `Bytes` payload, for a test that hands a runtime
232/// intrinsic a `Bytes` argument directly rather than going through `std/
233/// bytes`'s own Nova-level constructors.
234///
235/// Delegates to [`bytes::gc_bytes`], which already does exactly this and is
236/// `pub(crate)` -- named at the crate root only so `io`'s own tests can reach
237/// it as `crate::gc_bytes_for_test` without spelling out the `bytes` module
238/// path at each call site.
239///
240/// `#[cfg(test)]` only, no platform gate: every caller is an ordinary,
241/// cross-platform unit test.
242#[cfg(test)]
243pub(crate) fn gc_bytes_for_test(data: &[u8]) -> *mut NovaStr {
244 bytes::gc_bytes(data)
245}
246
247/// Read a `NovaStr` back as a `&str`.
248///
249/// `pub(crate)`, not private: `fs`'s intrinsics read their `NovaStr`
250/// arguments through this rather than a second copy of the same unsafe cast.
251///
252/// # Safety
253/// `s` must point to a valid `NovaStr` whose `ptr`/`len` reference valid
254/// UTF-8 (guaranteed for strings produced by the compiler and runtime).
255pub(crate) unsafe fn as_str<'a>(s: *const NovaStr) -> &'a str {
256 let s = &*s;
257 std::str::from_utf8_unchecked(std::slice::from_raw_parts(s.ptr, s.len as usize))
258}
259
260/// Create a string value from raw bytes (used for string literals).
261///
262/// The header is a 16-byte **leaf**: its `ptr` targets static literal data,
263/// which the collector never frees, so tracing it could never reach a GC
264/// object. That depends on this function's safety contract.
265///
266/// # Safety
267/// `ptr` must point to `len` bytes of valid UTF-8 that outlive the program
268/// (string literal data emitted by codegen), never into the GC heap.
269#[no_mangle]
270pub unsafe extern "C" fn nova_rt_str_new(ptr: *const u8, len: u64) -> *mut NovaStr {
271 let node = gc::alloc(STR_HEADER, false) as *mut NovaStr;
272 (*node).len = len;
273 (*node).ptr = ptr;
274 node
275}
276
277/// Print a string followed by a newline to stdout.
278///
279/// # Safety
280/// `s` must be a valid `NovaStr` pointer.
281#[no_mangle]
282pub unsafe extern "C" fn nova_rt_println(s: *const NovaStr) {
283 println!("{}", as_str(s));
284}
285
286/// Print a string to stdout without a trailing newline.
287///
288/// # Safety
289/// `s` must be a valid `NovaStr` pointer.
290#[no_mangle]
291pub unsafe extern "C" fn nova_rt_print(s: *const NovaStr) {
292 use std::io::Write;
293 let out = std::io::stdout();
294 let mut lock = out.lock();
295 let _ = lock.write_all(as_str(s).as_bytes());
296 let _ = lock.flush();
297}
298
299/// Write `s` to stderr with no trailing newline.
300///
301/// Mirrors [`nova_rt_print`]'s explicit lock-and-flush shape rather than
302/// [`nova_rt_println`]'s `println!`, but not its reason. Rust 1.95's
303/// `Stdout` is a `LineWriter` unconditionally, so a partial line with no
304/// trailing newline can sit in that buffer until something flushes it --
305/// there, the explicit flush is load-bearing. `Stderr` carries no such
306/// buffer at all (Rust's own source marks its raw handle "not buffered"),
307/// so a write reaches the OS on contact regardless of a trailing newline;
308/// the flush call below is a harmless no-op kept for symmetry with
309/// [`nova_rt_print`], not a mechanism this stream needs.
310///
311/// # Safety
312/// `s` must point to a live `NovaStr`.
313#[no_mangle]
314pub unsafe extern "C" fn nova_rt_eprint(s: *const NovaStr) {
315 use std::io::Write;
316 let err = std::io::stderr();
317 let mut lock = err.lock();
318 // SAFETY: forwarding this function's own contract.
319 let _ = lock.write_all(unsafe { as_str(s) }.as_bytes());
320 let _ = lock.flush();
321}
322
323/// Write `s` to stderr followed by a newline.
324///
325/// # Safety
326/// `s` must point to a live `NovaStr`.
327#[no_mangle]
328pub unsafe extern "C" fn nova_rt_eprintln(s: *const NovaStr) {
329 // SAFETY: forwarding this function's own contract.
330 eprintln!("{}", unsafe { as_str(s) });
331}
332
333/// Concatenate two strings into a new string value.
334///
335/// GC safety: `a`'s and `b`'s bytes are read after the result's buffer is
336/// allocated, kept alive across it as [`gc_str_filled`] describes.
337///
338/// # Safety
339/// `a` and `b` must be valid `NovaStr` pointers.
340#[no_mangle]
341pub unsafe extern "C" fn nova_rt_str_concat(a: *const NovaStr, b: *const NovaStr) -> *mut NovaStr {
342 let (a, b) = (as_str(a), as_str(b));
343 gc_str_filled(a.len() + b.len(), |out| {
344 out.put(a.as_bytes());
345 out.put(b.as_bytes());
346 })
347}
348
349/// Concatenate every string in `parts` into one new string: the n-ary form
350/// string interpolation of three or more parts lowers to.
351///
352/// One copy of each part into one result, where pairwise concatenation made
353/// a new string per part, each copying everything built so far. Each part is
354/// copied straight into the result's bytes.
355///
356/// GC safety: the parts are read after the result's buffer is allocated.
357/// `parts`, held in this frame, keeps the array and so every part alive
358/// across that allocation, as [`gc_str_filled`] describes.
359///
360/// # Safety
361/// `parts` must point to a Nova array `{ len: i64, elems… }` whose element
362/// `i`, at byte offset `8 + 8*i`, is a valid `NovaStr` pointer. A negative
363/// length is treated as zero, as `nova_rt_str_from_chars` does.
364#[no_mangle]
365pub unsafe extern "C" fn nova_rt_str_concat_n(parts: *const u8) -> *mut NovaStr {
366 let words = parts as *const i64;
367 let n = (*words).max(0) as usize;
368 let part = |i: usize| as_str(*words.add(1 + i) as *const NovaStr);
369 let total = (0..n).map(|i| part(i).len()).sum();
370 gc_str_filled(total, |out| {
371 for i in 0..n {
372 out.put(part(i).as_bytes());
373 }
374 })
375}
376
377/// Join every string in `parts` with `sep` between each pair: the body of
378/// `std/strings`' `String.join`.
379///
380/// One copy of each part and separator, straight into the result's buffer,
381/// sized up front. The Nova-level `join` it replaces walked every part
382/// character by character into a character array.
383///
384/// GC safety: the parts and `sep` are read after the result's buffer is
385/// allocated, kept alive across it as in [`nova_rt_str_concat_n`].
386///
387/// # Safety
388/// `sep` must be a valid `NovaStr` pointer. `parts` must point to a Nova
389/// array laid out as [`nova_rt_str_concat_n`] requires. A negative length is
390/// treated as zero, as there.
391#[no_mangle]
392pub unsafe extern "C" fn nova_rt_str_join(sep: *const NovaStr, parts: *const u8) -> *mut NovaStr {
393 let words = parts as *const i64;
394 let n = (*words).max(0) as usize;
395 let sep = as_str(sep);
396 let part = |i: usize| as_str(*words.add(1 + i) as *const NovaStr);
397 let total = sep.len() * n.saturating_sub(1) + (0..n).map(|i| part(i).len()).sum::<usize>();
398 gc_str_filled(total, |out| {
399 for i in 0..n {
400 if i > 0 {
401 out.put(sep.as_bytes());
402 }
403 out.put(part(i).as_bytes());
404 }
405 })
406}
407
408/// The character index of the first occurrence of `needle` in `haystack`,
409/// or `-1`: the body of `std/strings`' `String.index_of` and
410/// `String.contains`. An empty needle occurs at `0`.
411///
412/// A byte search finds the same first match a character-by-character one
413/// would. Both strings are valid UTF-8, and no character starts with a
414/// continuation byte, so a byte match can only begin on a character
415/// boundary. The characters before the match are then counted to turn its
416/// byte offset into a character index. Nothing here allocates, and a miss
417/// counts nothing. The Nova-level search this replaces built a `[Char]` for
418/// each string first.
419///
420/// # Safety
421/// `haystack` and `needle` must be valid `NovaStr` pointers.
422#[no_mangle]
423pub unsafe extern "C" fn nova_rt_str_index_of(
424 haystack: *const NovaStr,
425 needle: *const NovaStr,
426) -> i64 {
427 let h = as_str(haystack);
428 match h.find(as_str(needle)) {
429 Some(at) => h[..at].chars().count() as i64,
430 None => -1,
431 }
432}
433
434/// `s` as a JSON string literal, quotation marks included: the body of
435/// `std/json`'s `quote`.
436///
437/// `"` and `\` take a backslash. The five control characters with a short
438/// escape become `\n`, `\r`, `\t`, `\b` and `\f`, and every other byte below
439/// `0x20` becomes `\u00XX` in lowercase hex (RFC 8259). Everything else is
440/// copied as it is. Working on bytes is exact: every byte of a multi-byte
441/// UTF-8 character is `0x80` or above, so none can be mistaken for one of
442/// these, and every slice copied ends next to an ASCII byte, on a character
443/// boundary. The Nova-level `quote` this replaces built a `[Char]` and
444/// scanned it character by character.
445///
446/// The escaped length is counted first, so the result is written once,
447/// straight into its GC buffer.
448///
449/// GC safety: `s`'s bytes are read after the result's buffer is allocated,
450/// kept alive across it as [`gc_str_filled`] describes.
451///
452/// # Safety
453/// `s` must be a valid `NovaStr` pointer.
454#[no_mangle]
455pub unsafe extern "C" fn nova_rt_json_quote(s: *const NovaStr) -> *mut NovaStr {
456 const HEX: &[u8; 16] = b"0123456789abcdef";
457 let src = as_str(s).as_bytes();
458 // What each escape adds to its one source byte: one more byte for a
459 // two-byte escape, five more for `\u00XX`.
460 let extra: usize = src
461 .iter()
462 .map(|&b| match b {
463 b'"' | b'\\' | b'\n' | b'\r' | b'\t' | 0x08 | 0x0c => 1,
464 0x00..=0x1f => 5,
465 _ => 0,
466 })
467 .sum();
468 gc_str_filled(src.len() + 2 + extra, |out| {
469 out.put(b"\"");
470 let mut start = 0;
471 for (i, &b) in src.iter().enumerate() {
472 let short: &[u8] = match b {
473 b'"' => b"\\\"",
474 b'\\' => b"\\\\",
475 b'\n' => b"\\n",
476 b'\r' => b"\\r",
477 b'\t' => b"\\t",
478 0x08 => b"\\b",
479 0x0c => b"\\f",
480 0x00..=0x1f => b"",
481 _ => continue,
482 };
483 out.put(&src[start..i]);
484 if short.is_empty() {
485 out.put(&[
486 b'\\',
487 b'u',
488 b'0',
489 b'0',
490 HEX[(b >> 4) as usize],
491 HEX[(b & 15) as usize],
492 ]);
493 } else {
494 out.put(short);
495 }
496 start = i + 1;
497 }
498 out.put(&src[start..]);
499 out.put(b"\"");
500 })
501}
502
503/// Compare two strings for byte equality.
504///
505/// # Safety
506/// `a` and `b` must be valid `NovaStr` pointers.
507#[no_mangle]
508pub unsafe extern "C" fn nova_rt_str_eq(a: *const NovaStr, b: *const NovaStr) -> i8 {
509 (as_str(a) == as_str(b)) as i8
510}
511
512/// Byte-lexicographic comparison of two strings: `-1`, `0`, or `1`.
513///
514/// # Safety
515/// `a` and `b` must be valid `NovaStr` pointers.
516#[no_mangle]
517pub unsafe extern "C" fn nova_rt_str_cmp(a: *const NovaStr, b: *const NovaStr) -> i64 {
518 match as_str(a).as_bytes().cmp(as_str(b).as_bytes()) {
519 std::cmp::Ordering::Less => -1,
520 std::cmp::Ordering::Equal => 0,
521 std::cmp::Ordering::Greater => 1,
522 }
523}
524
525/// The per-process seed for [`nova_rt_str_hash`].
526///
527/// ONE value for the whole process. Not per thread and not per call: a `Map`
528/// built under one seed and probed under another finds nothing, so a
529/// thread-local would corrupt a map shared across threads and reseeding per
530/// call would corrupt every map. `OnceLock` gives exactly that, and `gc.rs`
531/// already uses the same pattern.
532///
533/// `RandomState` is `std`'s own `HashMap` seed source, so this draws OS
534/// entropy with no new dependency and nothing above MSRV 1.78.
535///
536/// **Nova code CAN recover this value, exactly, in one call.** An earlier
537/// version of this comment said it could not, on the grounds that no builtin
538/// exposes it and that `collect_externs` refuses every `nova_`-prefixed
539/// `extern` name. The `collect_externs` half is true; the closed world over
540/// routes is not. `str_hash` is the builtin that exposes it: `("").hash()`
541/// runs FNV's loop body zero times, so it returns [`splitmix64_finalize`]
542/// applied to this raw seed, and that finalizer is a bijection with a
543/// published inverse. Measured on this tree -- `("").hash()` from an ordinary
544/// Nova program gave `-4152075758151904758`, inverting the finalizer gave the
545/// seed `0xa3881b6692503399`, and that seed predicted `("abc").hash()` and
546/// `("hello world").hash()` from the same process exactly; four processes gave
547/// four different values, so the seed is live. What that costs and what it
548/// does not is at [`nova_rt_str_hash`].
549fn str_hash_seed() -> u64 {
550 static SEED: OnceLock<u64> = OnceLock::new();
551 *SEED.get_or_init(|| {
552 use std::hash::{BuildHasher, Hasher};
553 let mut h = std::collections::hash_map::RandomState::new().build_hasher();
554 h.write_u64(0);
555 h.finish()
556 })
557}
558
559/// The per-process seed for `Int`, `Bool` and `Char` hashing.
560///
561/// A SEPARATE call from [`str_hash_seed`], deliberately. Were the two one
562/// value, `(0).hash()` would equal `("").hash()` exactly, since both reduce to
563/// [`splitmix64_finalize`] applied to the seed.
564///
565/// What the separate call buys is that the two seeds DIFFER, effectively
566/// certainly. It does not buy independence, and an earlier version of this
567/// comment claimed "two independent keys", which is false.
568/// `RandomState::new()` caches one 128-bit OS draw per THREAD and hands out
569/// `(k0, k1)` from it, storing `(k0 + 1, k1)` on each call — so two calls on a
570/// thread share `k1` exactly and differ in `k0` by the number of calls between
571/// them. `std`'s own comment says that increment exists to vary `HashMap`
572/// iteration order, not to make keys independent. Each seed here is then the
573/// same keyed hash over the same one-word message under those related keys, and
574/// the two differ because those outputs do. The variant is deliberately not
575/// named: `std` documents `DefaultHasher`'s algorithm as unspecified and free
576/// to change, so naming it would go stale on a `std` upgrade, and the argument
577/// holds for any keyed hash. (Whichever thread first reaches each `OnceLock`
578/// supplies its draw, and neither initialiser pins a thread, so the two can
579/// also come from two separate OS draws.)
580///
581/// Whether recovering one seed yields the other therefore rests on that keyed
582/// hash's behaviour under a RELATED KEY, not on independence. This increment
583/// does not verify that, and `int_and_str_hash_seeds_are_drawn_separately` does
584/// not test it: that test pins only that the two differ.
585///
586/// One value for the whole process, for the same reason `str_hash_seed` gives:
587/// a `Map` built under one seed and probed under another finds nothing.
588fn int_hash_seed() -> u64 {
589 static SEED: OnceLock<u64> = OnceLock::new();
590 *SEED.get_or_init(|| {
591 use std::hash::{BuildHasher, Hasher};
592 let mut h = std::collections::hash_map::RandomState::new().build_hasher();
593 h.write_u64(0);
594 h.finish()
595 })
596}
597
598/// The per-process seed for `std/core`'s `Int`, `Bool` and `Char` hashing,
599/// XORed into the input before `mix64` runs.
600///
601/// **Nova code can recover this value in one call**, since those `Hash` impls
602/// XOR it in: `(0).hash()` is `mix64(0 ^ seed)`, which is `mix64(seed)`, and
603/// `mix64` is an invertible bijection. That is the same shape `("").hash()`
604/// already has for the string seed, it follows from ADR 0005's one-shot `Hash`
605/// returning a plain `Int`, and the gate claim already declines the adaptive
606/// attacker for whom it matters. What the seed buys is precomputation
607/// resistance. `tests/runtime/hash.nova` performs that recovery, to cancel the
608/// seed when it asserts `mix64`'s canonical vectors.
609///
610/// Nullary and takes no pointer, so it needs no `unsafe`. Plain `"C"`, not
611/// `"C-unwind"`: every caller is a compiled Nova frame — `std/core`'s three
612/// `Hash` impls, reached from user code — and [`crate::task::PollFn`]'s doc
613/// closes its constraint block by saying the permission `"C-unwind"` grants
614/// exists for that module's Rust-side entry points, not for compiled Nova
615/// frames. `task`'s `abort_with` states the general form in its own doc: a
616/// generated frame carries no landing pads, no drop glue and no unwind table,
617/// so an unwind that passed through one would have to be resolved against a
618/// frame the unwinder has no description of.
619///
620/// The panic that would do it is real rather than hypothetical. `get_or_init`
621/// runs `RandomState::new`, which draws OS entropy on the first call, and `std`
622/// panics when that draw fails — unconditionally on Linux and on the
623/// `getrandom`/`getentropy` platforms, and under a `debug_assert` on current
624/// Windows. `(0).hash()` inside an `async fn` body puts that first call inside
625/// a generated poll function. Plain `"C"` escalates such an unwind to `panic in
626/// a function that cannot unwind` and aborts, making the prohibited case
627/// structurally impossible instead of resting on the panic not happening.
628///
629/// [`nova_rt_str_hash`] is the precedent this follows: it makes the same
630/// `RandomState::new` draw through the same `OnceLock` and is plain `"C"` too.
631/// The nullary readers in `log` and `time` use `"C-unwind"` at this same
632/// signature; that divergence is pre-existing, is not resolved here, and their
633/// panic profiles differ from this one's. Earlier versions of this comment
634/// specified `"C-unwind"` by reasoning from those readers' shape, which is the
635/// inference [`nova_rt_float_fixed`]'s doc warns against — a neighbouring
636/// family's ABI is not an argument — and one of them went further and asserted
637/// that `task`'s rule was narrower than the module states. It is not.
638#[no_mangle]
639pub extern "C" fn nova_rt_int_hash_seed() -> i64 {
640 int_hash_seed() as i64
641}
642
643/// splitmix64's finalizer — the same function `std/core`'s `mix64` computes.
644///
645/// `std/core` writes it in signed Nova with masked shifts, because Nova's `>>`
646/// on a negative `Int` sign-extends; on `u64` here the plain form is the same
647/// function. Its constants correspond: Nova's `-4658895280553007687` is
648/// `0xbf58476d1ce4e5b9` and `-7723592293110705685` is `0x94d049bb133111eb`.
649///
650/// This is LOAD-BEARING, not decoration. `Map` selects buckets with
651/// `hash & (cap - 1)` -- the low bits -- and FNV-1a's low bits barely depend on
652/// its starting value: bit 0 of an FNV-1a result is bit 0 of the basis XOR the
653/// parity of the input bytes' low bits, so changing the basis flips bit 0
654/// identically for both keys of a colliding pair and leaves the collision
655/// standing. Measured, seeding alone left 30 of a constructed 48-key attack in
656/// one bucket and moved only 66.7% of keys at capacity 8; with this finalizer
657/// the same attack falls to 3 and 87.3% of keys move, against an ideal of
658/// 87.5%. The spec's section 2 carries the method and the full figures.
659///
660/// `pub` for one reason: `nova-driver`'s `async_end_to_end` tests tie this
661/// function to `std/core`'s `mix64` by running Nova's `.hash()` through the
662/// real pipeline and comparing against this one, over a seed both sides read
663/// from the same process-global `OnceLock`. The records have long said the two
664/// compute the same function and nothing asserted it.
665///
666/// **This is Rust-internal visibility and creates no Nova surface**, which is
667/// what distinguishes it from ADR 0005's prohibition on publishing `mix64`.
668/// That prohibition is about Nova's namespace: publishing `mix64` would make
669/// its exact definition a compatibility surface and forecloses the freedom ADR
670/// 0005 reserves to replace its constants or rounds. Nothing here reaches a
671/// Nova program, and `mix64` stays module-private.
672pub fn splitmix64_finalize(mut z: u64) -> u64 {
673 z ^= z >> 30;
674 z = z.wrapping_mul(0xbf58_476d_1ce4_e5b9);
675 z ^= z >> 27;
676 z = z.wrapping_mul(0x94d0_49bb_1331_11eb);
677 z ^ (z >> 31)
678}
679
680/// Seeded, finalized hash of a Nova string's bytes, as an `i64`.
681///
682/// `std/core`'s `impl Hash for String` reaches this through the `str_hash`
683/// builtin. An earlier version of this comment justified that by saying Nova
684/// cannot walk a string's bytes itself, on the grounds that `String` has no
685/// length, indexing or iteration and is not FFI-safe. The FFI-safety half is
686/// true. The byte-walking half was true when written and is now **stale**,
687/// by two separate routes rather than the one an earlier draft of this
688/// paragraph named. The byte and char BUILTINS -- `str_chars`,
689/// `bytes_from_string_intrinsic`, `bytes_at` -- are `Builtin::STD_ONLY`
690/// members, and the resolver seeds every member of that array into every std
691/// module's scope, which is how `std/core` comes to walk a string at its own
692/// `impl Debug for String`. Separately, `std/bytes` and `std/strings` are
693/// `STD_MODULES` entries whose public items reach other modules through
694/// `import_std_module`. `STD_ONLY` holds builtins, not modules; conflating
695/// the two is what that earlier draft did.
696/// The reason that survives does not rest on impossibility: the hash lives in
697/// one place, and reimplementing FNV-1a in Nova would pin a second copy of it
698/// free to diverge from this one. ADR 0005's 2026-08-27 amendment records the
699/// same correction against the same sentence.
700///
701/// Seeded FNV-1a over the bytes, then splitmix64's finalizer. The seed is
702/// per-process (see `str_hash_seed`) and the finalizer is what makes the seed
703/// reach the low bits `Map` selects buckets from; the reasoning and the
704/// measurements are at `splitmix64_finalize`. This used to be unseeded FNV-1a
705/// alone, described here as "adequate for a hash map's bucket selection" --
706/// true of an honest workload and false of an adversarial one, which is why it
707/// changed.
708///
709/// STILL NOT COLLISION-RESISTANT, and still not for anything
710/// security-sensitive. What the seed and finalizer buy is resistance to a
711/// PRECOMPUTED collision set: the seed is drawn per process, so a colliding key
712/// set cannot be built offline against a process that has not started yet, and
713/// recovering the seed means first obtaining a hash from the running process.
714/// This paragraph used to close "`mix64` -- the `Int`, `Bool` and `Char` impls
715/// -- is not seeded at all." That is no longer true: those impls XOR
716/// [`nova_rt_int_hash_seed`]'s separate per-process draw into `mix64`'s input,
717/// so they carry the same precomputation resistance and the same recoverable
718/// seed as this one does. `mix64` itself is still unseeded, which is a
719/// different statement -- the seeding is in the impls that call it.
720///
721/// An adversary who can observe the process and adapt is OUT OF SCOPE, and the
722/// route is concrete rather than a category: one string hash read out of this
723/// process recovers the seed, because `("").hash()` is the finalizer applied to
724/// the seed alone (see `str_hash_seed`), so an attacker who can obtain any
725/// `String` hash from a running process can construct collisions for it. The
726/// channel is ADR 0005's one-shot `fn hash(self) -> Int` returning a whole
727/// 64-bit result in one call, which predates the seeding: seeding did not open
728/// the channel, it made the value behind it worth recovering. Precomputation
729/// resistance is the half that survives.
730///
731/// The `u64 -> i64` reinterpretation at the end means the result may be
732/// negative, so a caller selecting buckets must mask (`hash & (cap - 1)`,
733/// which is non-negative for any `i64`) rather than take a remainder.
734///
735/// # Safety
736/// `s` must point to a valid `NovaStr`.
737#[no_mangle]
738pub unsafe extern "C" fn nova_rt_str_hash(s: *const NovaStr) -> i64 {
739 let mut h: u64 = str_hash_seed();
740 for b in as_str(s).as_bytes() {
741 h ^= *b as u64;
742 h = h.wrapping_mul(0x0000_0100_0000_01b3);
743 }
744 splitmix64_finalize(h) as i64
745}
746
747/// Number of Unicode scalar values in `s`.
748///
749/// Separate from [`nova_rt_str_chars`] so that asking a string's length does
750/// not allocate an array of its characters.
751///
752/// # Safety
753/// `s` must point to a valid `NovaStr`.
754#[no_mangle]
755pub unsafe extern "C" fn nova_rt_str_len_chars(s: *const NovaStr) -> i64 {
756 as_str(s).chars().count() as i64
757}
758
759/// Decompose `s` into a Nova `[Char]`.
760///
761/// The result must match **exactly** what codegen emits for an array: one
762/// block holding `{ len: i64, elem0, elem1, … }`, element `i` at byte offset
763/// `8 + 8*i`, allocated *scanned* the way [`nova_rt_alloc`] allocates (it
764/// takes no scan parameter and always scans). A `Char` element is its `i64`
765/// Unicode scalar value, because `Ty::Char` and `Ty::Int` are both
766/// `MirTy::I64`.
767///
768/// Scanning an array of scalars can retain garbage that happens to look like
769/// a pointer. That is the conservative collector's existing behaviour for any
770/// `[Int]`, not something new here.
771///
772/// # Safety
773/// `s` must point to a valid `NovaStr`.
774#[no_mangle]
775pub unsafe extern "C" fn nova_rt_str_chars(s: *const NovaStr) -> *mut u8 {
776 // Counted first and written straight into the GC array, rather than
777 // collected into a `Vec<char>` and copied. The intermediate vector was a
778 // system-heap allocation per call, regrown whenever the string outgrew
779 // its first guess, and a free: 6.1–6.8% of the server thread in
780 // `examples/05-json-api/BENCHMARK.md`'s "(reprofile)".
781 let text = as_str(s);
782 let n = text.chars().count();
783 // `8` for the length header plus `8` per element — the same size
784 // arithmetic `nova_rt_alloc` is asked for when codegen builds an array.
785 // A char count cannot overflow this on a 64-bit target, and `gc::alloc`
786 // rejects an undescribable size regardless.
787 //
788 // `text` is read again after this allocation, which can run a
789 // collection. Its bytes stay alive because `text` itself, a pointer to
790 // them held in this frame, is a root: the collector scans every frame on
791 // the stack and the callee-saved registers (`gc.rs`'s module doc
792 // comment). The `NovaStr` header is not read again.
793 let block = gc::alloc(8 + 8 * n, true);
794 let words = block as *mut i64;
795 *words = n as i64;
796 for (i, c) in text.chars().enumerate() {
797 *words.add(1 + i) = c as i64;
798 }
799 block
800}
801
802/// Encode a Nova `[Char]` back into a string.
803///
804/// A word that is not a valid Unicode scalar value becomes
805/// [`char::REPLACEMENT_CHARACTER`] rather than aborting, matching what
806/// [`nova_rt_char_to_str`] already does. Nova source cannot produce one —
807/// there is no `Int` → `Char` conversion in the language (`let c: Char = 65`
808/// is `E0010`, `'a' + 1` is `E0010`, and no such builtin exists) — so this is
809/// defensive only.
810///
811/// # Safety
812/// `cs` must point to a Nova array of `Char`: `{ len: i64, elems… }` with
813/// element `i` at byte offset `8 + 8*i`.
814#[no_mangle]
815pub unsafe extern "C" fn nova_rt_str_from_chars(cs: *const u8) -> *mut NovaStr {
816 let words = cs as *const i64;
817 let n = (*words).max(0) as usize;
818 let mut out = String::new();
819 for i in 0..n {
820 let v = *words.add(1 + i);
821 out.push(char::from_u32(v as u32).unwrap_or(char::REPLACEMENT_CHARACTER));
822 }
823 gc_str(&out)
824}
825
826/// Uppercase `s` with full Unicode case mapping.
827///
828/// Whole-string rather than `Char` → `Char` because the mapping is not 1:1 —
829/// `ß` uppercases to `SS` — so a per-character signature could not express it
830/// and would silently corrupt such input.
831///
832/// # Safety
833/// `s` must point to a valid `NovaStr`.
834#[no_mangle]
835pub unsafe extern "C" fn nova_rt_str_to_upper(s: *const NovaStr) -> *mut NovaStr {
836 gc_str(&as_str(s).to_uppercase())
837}
838
839/// Lowercase `s` with full Unicode case mapping. Whole-string for the same
840/// reason as [`nova_rt_str_to_upper`].
841///
842/// # Safety
843/// `s` must point to a valid `NovaStr`.
844#[no_mangle]
845pub unsafe extern "C" fn nova_rt_str_to_lower(s: *const NovaStr) -> *mut NovaStr {
846 gc_str(&as_str(s).to_lowercase())
847}
848
849/// Parse decimal text as a `Float`.
850///
851/// Returns `0.0` for anything Rust's parser rejects. That fallback is
852/// unreachable by construction: `std/json`'s `scan_number` validates the
853/// number's lexical shape against JSON's grammar — `-?(0|[1-9][0-9]*)`
854/// `(\.[0-9]+)?([eE][+-]?[0-9]+)?` — and only then slices the matched span and
855/// calls this, so every input is a text Rust's `f64` parser also accepts (its
856/// accepted grammar is a strict superset of JSON's).
857///
858/// The fallback exists because **this function must not panic.** It is
859/// `extern "C"` with no `-unwind`, so a panic cannot unwind out of it: it
860/// escalates to `panic in a function that cannot unwind` and aborts the
861/// process. See [`nova_rt_float_fixed`]'s doc comment for the measured
862/// behaviour of that escalation. Hence `unwrap_or`, never `unwrap`.
863///
864/// # Safety
865/// `s` must point to a valid `NovaStr`.
866#[no_mangle]
867pub unsafe extern "C" fn nova_rt_str_to_float(s: *const NovaStr) -> f64 {
868 as_str(s).parse::<f64>().unwrap_or(0.0)
869}
870
871/// Format an `Int` as a string.
872///
873/// The digits are written on the stack and copied once into GC memory,
874/// rather than through `to_string`'s heap `String`.
875#[no_mangle]
876pub extern "C" fn nova_rt_int_to_str(v: i64) -> *mut NovaStr {
877 // Twenty bytes hold `i64::MIN`: a sign and nineteen digits.
878 let mut text = [0u8; 20];
879 let mut at = text.len();
880 let mut rest = v.unsigned_abs();
881 loop {
882 at -= 1;
883 text[at] = b'0' + (rest % 10) as u8;
884 rest /= 10;
885 if rest == 0 {
886 break;
887 }
888 }
889 if v < 0 {
890 at -= 1;
891 text[at] = b'-';
892 }
893 let text = &text[at..];
894 // SAFETY: the writer puts exactly `text.len()` ASCII bytes.
895 unsafe { gc_str_filled(text.len(), |out| out.put(text)) }
896}
897
898/// Format a `Float` as a string.
899#[no_mangle]
900pub extern "C" fn nova_rt_float_to_str(v: f64) -> *mut NovaStr {
901 gc_str(&v.to_string())
902}
903
904/// Format a `Float` to a fixed number of decimal places.
905///
906/// `extern "C"` with no `unsafe` and no `-unwind`, matching
907/// [`nova_rt_float_to_str`] directly above: this takes no pointer, and
908/// `format!` on an `f64` cannot panic once `places` is clamped. Do not reach
909/// for `extern "C-unwind"` here because the async intrinsics use it -- the two
910/// families differ, and an earlier draft of this increment's spec got it wrong
911/// by pattern-matching the wrong neighbour.
912///
913/// `places` is clamped to `0..=17`. The ceiling is where an `f64` stops
914/// carrying information. **The floor is load-bearing in a stronger way than
915/// "it would panic":** a negative precision reaches `format!` and panics
916/// `Formatting argument out of range`, and because this function is
917/// `extern "C"` with no `-unwind` that panic cannot unwind — it escalates to
918/// `panic in a function that cannot unwind` and **aborts the process**
919/// (measured: `STATUS_STACK_BUFFER_OVERRUN`, on both the direct Rust path and
920/// the JIT-compiled path, where the backtrace truncates at this frame because
921/// generated code carries no unwind tables).
922///
923/// That escalation is the argument *for* `extern "C"` here rather than against
924/// it. [`crate::task`]'s module docs already set the rule this follows:
925/// anything reachable from a generated call site must abort rather than
926/// attempt to unwind. The clamp is what keeps this function inside that rule.
927#[no_mangle]
928pub extern "C" fn nova_rt_float_fixed(v: f64, places: i64) -> *mut NovaStr {
929 let places = places.clamp(0, 17) as usize;
930 gc_str(&format!("{:.*}", places, v))
931}
932
933/// Format a `Bool` as `true` / `false`.
934#[no_mangle]
935pub extern "C" fn nova_rt_bool_to_str(v: i8) -> *mut NovaStr {
936 gc_str(if v != 0 { "true" } else { "false" })
937}
938
939/// Format a `Char` (Unicode scalar value) as a string.
940#[no_mangle]
941pub extern "C" fn nova_rt_char_to_str(v: i64) -> *mut NovaStr {
942 let c = char::from_u32(v as u32).unwrap_or(char::REPLACEMENT_CHARACTER);
943 gc_str(&c.to_string())
944}
945
946/// Allocate `size` zeroed bytes for a heap value — a sum `{ tag, fields... }`,
947/// record `{ fields... }`, array `{ len, elems... }`, or closure environment.
948/// The result is GC-managed and its slots are traced for further pointers.
949///
950/// Never returns null: a size too large to describe as a heap layout, and one
951/// the system allocator cannot satisfy, both abort (see [`gc::alloc`]).
952#[no_mangle]
953pub extern "C" fn nova_rt_alloc(size: i64) -> *mut u8 {
954 gc::alloc(size.max(8) as usize, true)
955}
956
957/// Abort if `index` is outside `0..len` (an array bounds violation).
958#[no_mangle]
959pub extern "C" fn nova_rt_check_bounds(index: i64, len: i64) {
960 if index < 0 || index >= len {
961 eprintln!("nova: panic: array index {index} out of bounds for length {len}");
962 std::process::abort();
963 }
964}
965
966/// Abort the program with a panic message given as a Nova string.
967///
968/// # Safety
969/// `s` must point to a valid `NovaStr`.
970#[no_mangle]
971pub unsafe extern "C" fn nova_rt_panic_str(s: *const NovaStr) -> ! {
972 let msg = if s.is_null() { "" } else { as_str(s) };
973 eprintln!("nova: panic: {msg}");
974 std::process::abort();
975}
976
977/// `test_selector() -> i64`. See `Builtin::TestSelector`.
978#[no_mangle]
979pub extern "C" fn nova_rt_test_selector() -> i64 {
980 std::env::var("NOVA_TEST_INDEX")
981 .ok()
982 .and_then(|s| s.parse::<i64>().ok())
983 .unwrap_or(-1)
984}
985
986/// All runtime symbols, for registration with the JIT (or a linker map).
987///
988/// **Every `nova_mir::RtFunc` must appear here.** A `RtFunc` variant is
989/// declared to both codegen backends by the `rt_funcs!` macro, which guarantees
990/// the two backends cannot disagree about the enum — but it says nothing about
991/// this table, which is hand-maintained. A variant missing from here compiles
992/// clean and only fails when the JIT tries to resolve the symbol, inside
993/// `cranelift-jit`'s `finalize_definitions`, which `panic!`s rather than
994/// returning an error. `nova-codegen-cranelift`'s
995/// `every_rt_func_symbol_is_registered_with_the_jit` pins the containment,
996/// since that crate is the one that depends on both this one and `nova-mir`.
997pub fn symbols() -> Vec<(&'static str, *const u8)> {
998 vec![
999 ("nova_rt_str_new", nova_rt_str_new as *const u8),
1000 ("nova_rt_println", nova_rt_println as *const u8),
1001 ("nova_rt_print", nova_rt_print as *const u8),
1002 ("nova_rt_eprint", nova_rt_eprint as *const u8),
1003 ("nova_rt_eprintln", nova_rt_eprintln as *const u8),
1004 ("nova_rt_str_concat", nova_rt_str_concat as *const u8),
1005 ("nova_rt_str_concat_n", nova_rt_str_concat_n as *const u8),
1006 ("nova_rt_str_join", nova_rt_str_join as *const u8),
1007 ("nova_rt_str_index_of", nova_rt_str_index_of as *const u8),
1008 ("nova_rt_json_quote", nova_rt_json_quote as *const u8),
1009 ("nova_rt_str_eq", nova_rt_str_eq as *const u8),
1010 ("nova_rt_str_cmp", nova_rt_str_cmp as *const u8),
1011 ("nova_rt_str_hash", nova_rt_str_hash as *const u8),
1012 ("nova_rt_str_len_chars", nova_rt_str_len_chars as *const u8),
1013 ("nova_rt_str_chars", nova_rt_str_chars as *const u8),
1014 (
1015 "nova_rt_str_from_chars",
1016 nova_rt_str_from_chars as *const u8,
1017 ),
1018 ("nova_rt_str_to_upper", nova_rt_str_to_upper as *const u8),
1019 ("nova_rt_str_to_lower", nova_rt_str_to_lower as *const u8),
1020 ("nova_rt_int_to_str", nova_rt_int_to_str as *const u8),
1021 ("nova_rt_float_to_str", nova_rt_float_to_str as *const u8),
1022 ("nova_rt_float_fixed", nova_rt_float_fixed as *const u8),
1023 ("nova_rt_str_to_float", nova_rt_str_to_float as *const u8),
1024 ("nova_rt_bool_to_str", nova_rt_bool_to_str as *const u8),
1025 ("nova_rt_char_to_str", nova_rt_char_to_str as *const u8),
1026 ("nova_rt_alloc", nova_rt_alloc as *const u8),
1027 ("nova_rt_check_bounds", nova_rt_check_bounds as *const u8),
1028 ("nova_rt_panic_str", nova_rt_panic_str as *const u8),
1029 ("nova_rt_test_selector", nova_rt_test_selector as *const u8),
1030 ("nova_rt_task_spawn", task::nova_rt_task_spawn as *const u8),
1031 (
1032 "nova_rt_task_block_on",
1033 task::nova_rt_task_block_on as *const u8,
1034 ),
1035 (
1036 "nova_rt_task_is_done",
1037 task::nova_rt_task_is_done as *const u8,
1038 ),
1039 (
1040 "nova_rt_task_take_output",
1041 task::nova_rt_task_take_output as *const u8,
1042 ),
1043 (
1044 "nova_rt_task_release",
1045 task::nova_rt_task_release as *const u8,
1046 ),
1047 (
1048 "nova_rt_task_yield_future",
1049 task::nova_rt_task_yield_future as *const u8,
1050 ),
1051 (
1052 "nova_rt_task_sleep_future_nanos",
1053 task::nova_rt_task_sleep_future_nanos as *const u8,
1054 ),
1055 (
1056 "nova_rt_task_join_future",
1057 task::nova_rt_task_join_future as *const u8,
1058 ),
1059 (
1060 "nova_rt_task_timeout_future",
1061 task::nova_rt_task_timeout_future as *const u8,
1062 ),
1063 (
1064 "nova_rt_fs_read_to_string",
1065 fs::nova_rt_fs_read_to_string as *const u8,
1066 ),
1067 (
1068 "nova_rt_fs_write_string",
1069 fs::nova_rt_fs_write_string as *const u8,
1070 ),
1071 (
1072 "nova_rt_fs_take_string",
1073 fs::nova_rt_fs_take_string as *const u8,
1074 ),
1075 (
1076 "nova_rt_fs_last_error_message",
1077 fs::nova_rt_fs_last_error_message as *const u8,
1078 ),
1079 ("nova_rt_fs_temp_dir", fs::nova_rt_fs_temp_dir as *const u8),
1080 ("nova_rt_fs_exists", fs::nova_rt_fs_exists as *const u8),
1081 (
1082 "nova_rt_fs_create_dir",
1083 fs::nova_rt_fs_create_dir as *const u8,
1084 ),
1085 (
1086 "nova_rt_fs_create_dir_all",
1087 fs::nova_rt_fs_create_dir_all as *const u8,
1088 ),
1089 (
1090 "nova_rt_fs_remove_file",
1091 fs::nova_rt_fs_remove_file as *const u8,
1092 ),
1093 (
1094 "nova_rt_fs_remove_dir_all",
1095 fs::nova_rt_fs_remove_dir_all as *const u8,
1096 ),
1097 ("nova_rt_fs_read_dir", fs::nova_rt_fs_read_dir as *const u8),
1098 (
1099 "nova_rt_fs_take_string_array",
1100 fs::nova_rt_fs_take_string_array as *const u8,
1101 ),
1102 ("nova_rt_fs_kind", fs::nova_rt_fs_kind as *const u8),
1103 ("nova_rt_fs_read", fs::nova_rt_fs_read as *const u8),
1104 (
1105 "nova_rt_fs_take_bytes",
1106 fs::nova_rt_fs_take_bytes as *const u8,
1107 ),
1108 ("nova_rt_fs_write", fs::nova_rt_fs_write as *const u8),
1109 (
1110 "nova_rt_io_stdin_read",
1111 io::nova_rt_io_stdin_read as *const u8,
1112 ),
1113 (
1114 "nova_rt_io_stdout_write",
1115 io::nova_rt_io_stdout_write as *const u8,
1116 ),
1117 (
1118 "nova_rt_io_stderr_write",
1119 io::nova_rt_io_stderr_write as *const u8,
1120 ),
1121 (
1122 "nova_rt_io_stdout_flush",
1123 io::nova_rt_io_stdout_flush as *const u8,
1124 ),
1125 (
1126 "nova_rt_io_stderr_flush",
1127 io::nova_rt_io_stderr_flush as *const u8,
1128 ),
1129 ("nova_rt_file_open", file::nova_rt_file_open as *const u8),
1130 ("nova_rt_file_close", file::nova_rt_file_close as *const u8),
1131 ("nova_rt_file_read", file::nova_rt_file_read as *const u8),
1132 ("nova_rt_file_write", file::nova_rt_file_write as *const u8),
1133 ("nova_rt_file_flush", file::nova_rt_file_flush as *const u8),
1134 (
1135 "nova_rt_net_connect_future",
1136 net::nova_rt_net_connect_future as *const u8,
1137 ),
1138 ("nova_rt_net_close", net::nova_rt_net_close as *const u8),
1139 (
1140 "nova_rt_net_read_future",
1141 net::nova_rt_net_read_future as *const u8,
1142 ),
1143 (
1144 "nova_rt_net_write_future",
1145 net::nova_rt_net_write_future as *const u8,
1146 ),
1147 (
1148 "nova_rt_net_read_timeout_future",
1149 net::nova_rt_net_read_timeout_future as *const u8,
1150 ),
1151 ("nova_rt_net_listen", net::nova_rt_net_listen as *const u8),
1152 (
1153 "nova_rt_net_local_port",
1154 net::nova_rt_net_local_port as *const u8,
1155 ),
1156 (
1157 "nova_rt_net_accept_future",
1158 net::nova_rt_net_accept_future as *const u8,
1159 ),
1160 ("nova_rt_bytes_len", bytes::nova_rt_bytes_len as *const u8),
1161 (
1162 "nova_rt_bytes_from_string",
1163 bytes::nova_rt_bytes_from_string as *const u8,
1164 ),
1165 (
1166 "nova_rt_bytes_is_utf8",
1167 bytes::nova_rt_bytes_is_utf8 as *const u8,
1168 ),
1169 (
1170 "nova_rt_bytes_to_string_unchecked",
1171 bytes::nova_rt_bytes_to_string_unchecked as *const u8,
1172 ),
1173 ("nova_rt_bytes_at", bytes::nova_rt_bytes_at as *const u8),
1174 (
1175 "nova_rt_bytes_slice",
1176 bytes::nova_rt_bytes_slice as *const u8,
1177 ),
1178 (
1179 "nova_rt_bytes_concat",
1180 bytes::nova_rt_bytes_concat as *const u8,
1181 ),
1182 (
1183 "nova_rt_bytes_to_ints",
1184 bytes::nova_rt_bytes_to_ints as *const u8,
1185 ),
1186 (
1187 "nova_rt_bytes_from_ints",
1188 bytes::nova_rt_bytes_from_ints as *const u8,
1189 ),
1190 ("nova_rt_bytes_eq", bytes::nova_rt_bytes_eq as *const u8),
1191 ("nova_rt_int_hash_seed", nova_rt_int_hash_seed as *const u8),
1192 (
1193 "nova_rt_http_parse_request",
1194 http::nova_rt_http_parse_request as *const u8,
1195 ),
1196 (
1197 "nova_rt_crypto_hash",
1198 crypto::nova_rt_crypto_hash as *const u8,
1199 ),
1200 (
1201 "nova_rt_crypto_random_bytes",
1202 crypto::nova_rt_crypto_random_bytes as *const u8,
1203 ),
1204 (
1205 "nova_rt_crypto_random_int",
1206 crypto::nova_rt_crypto_random_int as *const u8,
1207 ),
1208 (
1209 "nova_rt_time_now_nanos",
1210 time::nova_rt_time_now_nanos as *const u8,
1211 ),
1212 (
1213 "nova_rt_time_now_epoch_nanos",
1214 time::nova_rt_time_now_epoch_nanos as *const u8,
1215 ),
1216 (
1217 "nova_rt_log_config_level",
1218 log::nova_rt_log_config_level as *const u8,
1219 ),
1220 (
1221 "nova_rt_log_config_to_stderr",
1222 log::nova_rt_log_config_to_stderr as *const u8,
1223 ),
1224 (
1225 "nova_rt_log_set_config",
1226 log::nova_rt_log_set_config as *const u8,
1227 ),
1228 (
1229 "nova_rt_shutdown_requested",
1230 signal::nova_rt_shutdown_requested as *const u8,
1231 ),
1232 (
1233 "nova_rt_process_arg_count",
1234 process::nova_rt_process_arg_count as *const u8,
1235 ),
1236 (
1237 "nova_rt_process_arg",
1238 process::nova_rt_process_arg as *const u8,
1239 ),
1240 (
1241 "nova_rt_process_exit",
1242 process::nova_rt_process_exit as *const u8,
1243 ),
1244 ]
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249 use super::*;
1250
1251 unsafe fn to_string(s: *mut NovaStr) -> String {
1252 as_str(s).to_string()
1253 }
1254
1255 unsafe fn make_str(s: &'static str) -> *mut NovaStr {
1256 nova_rt_str_new(s.as_ptr(), s.len() as u64)
1257 }
1258
1259 #[test]
1260 fn int_to_str_formats() {
1261 unsafe {
1262 assert_eq!(to_string(nova_rt_int_to_str(6765)), "6765");
1263 assert_eq!(to_string(nova_rt_int_to_str(-1)), "-1");
1264 }
1265 }
1266
1267 /// Every digit count from one to nineteen, both signs, and both ends of
1268 /// `i64`, against Rust's own formatting. `i64::MIN` has no positive
1269 /// counterpart, so a sign handled by negating the value overflows there.
1270 #[test]
1271 fn int_to_str_matches_rust_at_every_digit_count_and_both_ends() {
1272 let mut cases = vec![0, i64::MAX, i64::MIN, i64::MIN + 1];
1273 let mut p: i64 = 1;
1274 for _ in 0..19 {
1275 cases.extend([p, p - 1, -p, 1 - p]);
1276 p = p.saturating_mul(10);
1277 }
1278 for v in cases {
1279 unsafe { assert_eq!(to_string(nova_rt_int_to_str(v)), v.to_string(), "{v}") };
1280 }
1281 }
1282
1283 #[test]
1284 fn bool_to_str_formats() {
1285 unsafe {
1286 assert_eq!(to_string(nova_rt_bool_to_str(1)), "true");
1287 assert_eq!(to_string(nova_rt_bool_to_str(0)), "false");
1288 }
1289 }
1290
1291 /// Build a Nova `[String]` array `{ len, elems… }` from `parts`, for
1292 /// `nova_rt_str_concat_n`. The parts are allocated first so the array is
1293 /// the last allocation before the call.
1294 unsafe fn str_array(parts: &[&'static str]) -> *const u8 {
1295 let ptrs: Vec<*mut NovaStr> = parts.iter().map(|p| make_str(p)).collect();
1296 let block = gc::alloc(8 + 8 * ptrs.len(), true) as *mut i64;
1297 *block = ptrs.len() as i64;
1298 for (i, p) in ptrs.iter().enumerate() {
1299 *block.add(1 + i) = *p as i64;
1300 }
1301 block as *const u8
1302 }
1303
1304 /// The lengths every layout test covers: empty, short, one whose
1305 /// `16 + len` crosses a class boundary, the largest that stays a small
1306 /// object (`16 + 2032 = 2048`), the first that goes to the large path, and
1307 /// a large one at a class size.
1308 const STR_LENGTHS: [usize; 6] = [0, 2, 113, 2032, 2033, 2048];
1309
1310 /// `len` ASCII bytes with a static lifetime, so `make_str` can take them.
1311 fn text_of(len: usize) -> &'static str {
1312 let s: String = "abcdefghij".repeat(len / 10 + 1)[..len].to_string();
1313 Box::leak(s.into_boxed_str())
1314 }
1315
1316 /// One leaf object per runtime string: the header at its start, the bytes
1317 /// inline at offset 16
1318 /// (`docs/superpowers/specs/2026-10-04-one-allocation-strings-design.md`).
1319 unsafe fn assert_one_leaf_string(s: *mut NovaStr, expected: &str) {
1320 let len = expected.len();
1321 assert_eq!(
1322 gc::object_info(s as usize),
1323 Some((16 + len.max(1), false)),
1324 "len {len}: one leaf object of 16 + max(len, 1) bytes"
1325 );
1326 assert_eq!(
1327 (*s).ptr as usize,
1328 s as usize + 16,
1329 "len {len}: bytes inline at offset 16"
1330 );
1331 assert_eq!((*s).len, len as u64, "len {len}");
1332 assert_eq!(as_str(s), expected, "len {len}");
1333 }
1334
1335 #[test]
1336 fn a_runtime_string_is_one_leaf_object_with_its_bytes_inline() {
1337 for len in STR_LENGTHS {
1338 let expected = text_of(len);
1339 unsafe {
1340 assert_one_leaf_string(gc_str(expected), expected);
1341 // `gc_str_filled` through a builtin, the text split across two
1342 // literals.
1343 let (a, b) = expected.split_at(len / 2);
1344 assert_one_leaf_string(nova_rt_str_concat(make_str(a), make_str(b)), expected);
1345 }
1346 }
1347 }
1348
1349 #[test]
1350 fn a_literal_header_is_a_sixteen_byte_leaf() {
1351 unsafe {
1352 let s = make_str("literal");
1353 assert_eq!(gc::object_info(s as usize), Some((16, false)));
1354 assert_eq!(as_str(s), "literal");
1355 }
1356 }
1357
1358 #[test]
1359 fn an_empty_strings_pointer_lies_inside_its_own_object() {
1360 unsafe {
1361 let s = gc_str("");
1362 let base = s as usize;
1363 let ptr = (*s).ptr as usize;
1364 let size = gc::object_info(base).expect("a live object").0;
1365 assert!(
1366 base + 16 <= ptr && ptr < base + size,
1367 "ptr {ptr:#x} must lie in [{:#x}, {:#x})",
1368 base + 16,
1369 base + size
1370 );
1371 }
1372 }
1373
1374 /// A string reached only through a pointer into its bytes -- at offset 16
1375 /// and at its last byte -- survives a collection, with an unrooted control
1376 /// swept. Explicit roots, so it is deterministic on every platform.
1377 #[test]
1378 fn a_runtime_string_reached_only_through_its_bytes_survives_a_collection() {
1379 for len in [2usize, 113, 2033] {
1380 let expected = text_of(len);
1381 unsafe {
1382 let s = gc_str(expected);
1383 let control = gc_str(expected);
1384 assert!(gc::object_info(s as usize).is_some(), "len {len}");
1385 assert!(gc::object_info(control as usize).is_some(), "len {len}");
1386 for k in [0, len - 1] {
1387 gc::sweep_with_roots_for_test(&[(*s).ptr as usize + k]);
1388 assert_eq!(
1389 gc::object_info(s as usize),
1390 Some((16 + len, false)),
1391 "len {len} k {k}: the whole object survives"
1392 );
1393 }
1394 assert_eq!(
1395 gc::object_info(control as usize),
1396 None,
1397 "len {len}: the unrooted control is swept"
1398 );
1399 assert_eq!(as_str(s), expected, "len {len}");
1400 }
1401 }
1402 }
1403
1404 /// Every `.rs` file under this crate's `src`, found by walking the
1405 /// directory at test time, so a module added later is scanned without
1406 /// anyone listing it.
1407 fn runtime_sources() -> Vec<(String, String)> {
1408 fn walk(dir: &std::path::Path, out: &mut Vec<(String, String)>) {
1409 let mut entries: Vec<_> = std::fs::read_dir(dir)
1410 .expect("the crate's src directory is readable")
1411 .map(|e| e.expect("a readable directory entry").path())
1412 .collect();
1413 entries.sort();
1414 for path in entries {
1415 if path.is_dir() {
1416 walk(&path, out);
1417 } else if path.extension().is_some_and(|x| x == "rs") {
1418 let text = std::fs::read_to_string(&path).expect("a readable source file");
1419 out.push((path.display().to_string(), text.replace("\r\n", "\n")));
1420 }
1421 }
1422 }
1423 let mut out = Vec::new();
1424 walk(
1425 &std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
1426 &mut out,
1427 );
1428 out
1429 }
1430
1431 /// A file's production code: the lines before the first line that is
1432 /// exactly `mod tests {`, with comment lines dropped. Matching the whole
1433 /// line, not a substring, keeps a mention of `mod tests {` in a comment
1434 /// from cutting the scan short.
1435 fn production_code(source: &str) -> String {
1436 source
1437 .lines()
1438 .take_while(|l| l.trim() != "mod tests {")
1439 .filter(|l| !l.trim_start().starts_with("//"))
1440 .collect::<Vec<_>>()
1441 .join("\n")
1442 }
1443
1444 /// `.ptr =` and `.ptr=` assignments in `code`, not counting a `==`
1445 /// comparison that starts the same way.
1446 fn ptr_assignments(code: &str) -> usize {
1447 [".ptr =", ".ptr="]
1448 .iter()
1449 .map(|needle| {
1450 code.match_indices(needle)
1451 .filter(|(i, m)| !code[i + m.len()..].starts_with('='))
1452 .count()
1453 })
1454 .sum()
1455 }
1456
1457 /// **Where a `NovaStr`'s `ptr` may be written, pinned.** Every header is a
1458 /// leaf, so the collector never traces a `ptr`: a header pointing into
1459 /// another GC object would not keep it alive. Only [`alloc_str_object`]
1460 /// (`ptr` = its own bytes) and [`nova_rt_str_new`] (`ptr` = its caller's
1461 /// pointer, static literal data by its `# Safety` contract) write one.
1462 ///
1463 /// This scans the production code of every `.rs` file under the crate's
1464 /// `src` and fails on any of:
1465 /// - a `.ptr =` assignment outside those two builders;
1466 /// - a `.ptr` on a line that also takes an address (`&mut `,
1467 /// `addr_of_mut!`, `addr_of!`, `&raw `), the start of a write through
1468 /// `ptr::write`, `mem::replace`, `mem::swap` or a `cast_mut`;
1469 /// - a `NovaStr` struct literal, or an `impl` block on it;
1470 /// - a call to `nova_rt_str_new` from this crate's production code, so a
1471 /// zero-copy slice cannot hand it a pointer into a GC string. Its only
1472 /// callers are the code generators, for string literals.
1473 ///
1474 /// It is a text scan, not a proof, and what it cannot see is open-ended.
1475 /// Among it: a raw write at an offset (`*(p as *mut *const u8).add(1) =
1476 /// ...`), a header copied whole or built from raw words (`ptr::read`,
1477 /// `copy_nonoverlapping`, `transmute`), a call to `nova_rt_str_new`
1478 /// through a function pointer or alias, anything reached through a
1479 /// renamed path (`use ... as`, a type alias), a write split across lines,
1480 /// and code outside this crate.
1481 #[test]
1482 fn only_the_two_builders_write_a_novastr_ptr() {
1483 let sources = runtime_sources();
1484 // Coverage floor: a scan that found nothing would pass vacuously.
1485 assert!(
1486 sources.len() >= 14,
1487 "scanned only {} runtime source files",
1488 sources.len()
1489 );
1490 let lib_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1491 .join("src")
1492 .join("lib.rs")
1493 .display()
1494 .to_string();
1495 let lib = sources
1496 .iter()
1497 .find(|(p, _)| *p == lib_path)
1498 .expect("lib.rs is among the scanned files");
1499
1500 for (path, source) in &sources {
1501 let code = production_code(source);
1502 let writes = ptr_assignments(&code);
1503 if *path == lib_path {
1504 assert_eq!(
1505 writes, 2,
1506 "{path}: exactly two `.ptr =` writes, one per builder"
1507 );
1508 } else {
1509 assert_eq!(
1510 writes, 0,
1511 "{path}: writes a `NovaStr`'s `ptr` outside the two builders"
1512 );
1513 }
1514 let literals = code.matches("NovaStr {").count()
1515 - code.matches("struct NovaStr {").count()
1516 - code.matches("*mut NovaStr {").count()
1517 - code.matches("*const NovaStr {").count();
1518 assert_eq!(
1519 literals, 0,
1520 "{path}: builds or implements `NovaStr` outside its definition"
1521 );
1522 let borrows = code
1523 .lines()
1524 .filter(|l| {
1525 l.contains(".ptr")
1526 && ["&mut ", "addr_of_mut!", "addr_of!", "&raw "]
1527 .iter()
1528 .any(|b| l.contains(b))
1529 })
1530 .count();
1531 assert_eq!(
1532 borrows, 0,
1533 "{path}: takes the address of a `NovaStr`'s `ptr`"
1534 );
1535 // lib.rs holds the definition; nothing in production code calls it.
1536 let calls = code.matches("nova_rt_str_new(").count();
1537 let definitions = usize::from(*path == lib_path);
1538 assert_eq!(
1539 calls, definitions,
1540 "{path}: calls `nova_rt_str_new` from production code"
1541 );
1542 }
1543
1544 let code = production_code(&lib.1);
1545 for builder in ["fn alloc_str_object(", "fn nova_rt_str_new("] {
1546 let start = code.find(builder).expect("the builder exists");
1547 let end = start
1548 + code[start..]
1549 .find("\n}\n")
1550 .expect("the builder's closing brace");
1551 assert_eq!(
1552 ptr_assignments(&code[start..end]),
1553 1,
1554 "`{builder}` writes `ptr` exactly once"
1555 );
1556 }
1557 }
1558
1559 #[test]
1560 fn concat_n_joins_every_part_in_order() {
1561 unsafe {
1562 for parts in [
1563 &[][..],
1564 &["solo"][..],
1565 &["a", "b", "c"][..],
1566 &["", "x", ""][..],
1567 &["日本", "語", "🦀", "-", "é"][..],
1568 ] {
1569 let got = as_str(nova_rt_str_concat_n(str_array(parts))).to_string();
1570 assert_eq!(got, parts.concat(), "parts {parts:?}");
1571 }
1572 }
1573 }
1574
1575 /// Against a character-level reference: the first `i` at which the
1576 /// needle's characters match the haystack's, or `-1`. Multi-byte text
1577 /// before a match is what tells a character index from a byte index.
1578 #[test]
1579 fn index_of_reports_the_first_match_as_a_character_index() {
1580 fn reference(h: &str, n: &str) -> i64 {
1581 let hc: Vec<char> = h.chars().collect();
1582 let nc: Vec<char> = n.chars().collect();
1583 if nc.len() > hc.len() {
1584 return -1;
1585 }
1586 (0..=hc.len() - nc.len())
1587 .find(|&i| hc[i..i + nc.len()] == nc[..])
1588 .map_or(-1, |i| i as i64)
1589 }
1590 unsafe {
1591 for (h, n) in [
1592 ("hello wörld, hello wörld", "wörld"),
1593 ("日本語のテキスト", "テキ"),
1594 ("aé🦀aé🦀", "🦀a"),
1595 ("axbab", "ab"),
1596 ("aaa", "aa"),
1597 ("abc", "abc"),
1598 ("ab", "abc"),
1599 ("application/json", "\r"),
1600 ("", "a"),
1601 ("", ""),
1602 ("é", ""),
1603 ("é", "e"),
1604 ] {
1605 let got = nova_rt_str_index_of(make_str(h), make_str(n));
1606 assert_eq!(got, reference(h, n), "haystack {h:?} needle {n:?}");
1607 }
1608 }
1609 }
1610
1611 /// Against a character-level port of the Nova `quote` this replaced in
1612 /// `std/json`: every control character, both characters that need a
1613 /// backslash, the boundary at `0x7F`, and multi-byte text.
1614 #[test]
1615 fn json_quote_matches_the_character_level_escaper() {
1616 fn reference(s: &str) -> String {
1617 let mut out = String::from("\"");
1618 for c in s.chars() {
1619 let code = c as u32;
1620 match c {
1621 '"' => out.push_str("\\\""),
1622 '\\' => out.push_str("\\\\"),
1623 _ if code < 32 => match code {
1624 10 => out.push_str("\\n"),
1625 13 => out.push_str("\\r"),
1626 9 => out.push_str("\\t"),
1627 8 => out.push_str("\\b"),
1628 12 => out.push_str("\\f"),
1629 _ => out.push_str(&format!("\\u00{code:02x}")),
1630 },
1631 _ => out.push(c),
1632 }
1633 }
1634 out.push('"');
1635 out
1636 }
1637 let controls: &'static str = Box::leak(
1638 (0u8..32)
1639 .map(|b| b as char)
1640 .collect::<String>()
1641 .into_boxed_str(),
1642 );
1643 unsafe {
1644 for s in [
1645 "",
1646 "User Number 7",
1647 "a\"b\\c\nd",
1648 controls,
1649 "\u{7f}",
1650 "é🦀日本",
1651 "x\u{1f}y\u{0}z",
1652 "\"\\\"",
1653 "tab\there",
1654 // Multi-byte text right before an escape: a copy measured in
1655 // characters instead of bytes would take the wrong run here.
1656 "é\"🦀\n",
1657 "日本\u{1}語",
1658 ] {
1659 let got = as_str(nova_rt_json_quote(make_str(s)));
1660 assert_eq!(got, reference(s), "input {s:?}");
1661 }
1662 }
1663 }
1664
1665 /// Against Rust's own `join`, over `concat_n`'s shapes and separators
1666 /// from empty to multi-byte: a dropped, doubled or trailing separator,
1667 /// or a skipped part, changes the result for at least one pair.
1668 #[test]
1669 fn join_puts_the_separator_between_every_pair_of_parts() {
1670 unsafe {
1671 for sep in ["", ",", "->", "—🦀"] {
1672 for parts in [
1673 &[][..],
1674 &["solo"][..],
1675 &["a", "b", "c"][..],
1676 &["", "x", ""][..],
1677 &["日本", "語", "🦀", "-", "é"][..],
1678 ] {
1679 let got = as_str(nova_rt_str_join(make_str(sep), str_array(parts)));
1680 assert_eq!(got, parts.join(sep), "sep {sep:?} parts {parts:?}");
1681 }
1682 }
1683 }
1684 }
1685
1686 /// Against Rust's own concatenation, with either side empty and with
1687 /// multi-byte text on both.
1688 #[test]
1689 fn concat_keeps_both_sides_whole() {
1690 unsafe {
1691 for (a, b) in [
1692 ("", ""),
1693 ("", "x"),
1694 ("x", ""),
1695 ("foo", "bar"),
1696 ("日本", "🦀é"),
1697 ] {
1698 let got = as_str(nova_rt_str_concat(make_str(a), make_str(b)));
1699 assert_eq!(got, format!("{a}{b}"), "{a:?} + {b:?}");
1700 }
1701 }
1702 }
1703
1704 #[test]
1705 fn concat_and_eq_work() {
1706 unsafe {
1707 let a = nova_rt_str_new("foo".as_ptr(), 3);
1708 let b = nova_rt_str_new("bar".as_ptr(), 3);
1709 let ab = nova_rt_str_concat(a, b);
1710 assert_eq!(to_string(ab), "foobar");
1711 let c = nova_rt_str_new("foobar".as_ptr(), 6);
1712 assert_eq!(nova_rt_str_eq(ab, c), 1);
1713 assert_eq!(nova_rt_str_eq(a, b), 0);
1714 }
1715 }
1716
1717 #[test]
1718 fn alloc_is_writable() {
1719 let p = nova_rt_alloc(24);
1720 assert!(!p.is_null());
1721 unsafe {
1722 *(p as *mut i64) = 42;
1723 assert_eq!(*(p as *const i64), 42);
1724 }
1725 }
1726
1727 #[test]
1728 fn str_cmp_orders_lexicographically() {
1729 unsafe {
1730 let a = make_str("abc");
1731 let b = make_str("abd");
1732 let c = make_str("abc");
1733 assert_eq!(nova_rt_str_cmp(a, b), -1);
1734 assert_eq!(nova_rt_str_cmp(b, a), 1);
1735 assert_eq!(nova_rt_str_cmp(a, c), 0);
1736 }
1737 }
1738
1739 #[test]
1740 fn str_cmp_prefix_is_less() {
1741 unsafe {
1742 let a = make_str("ab");
1743 let b = make_str("abc");
1744 assert_eq!(nova_rt_str_cmp(a, b), -1);
1745 }
1746 }
1747
1748 #[test]
1749 fn str_hash_is_deterministic_and_distinguishes() {
1750 unsafe {
1751 let a = make_str("hello");
1752 let b = make_str("hello");
1753 let c = make_str("world");
1754 assert_eq!(nova_rt_str_hash(a), nova_rt_str_hash(b));
1755 assert_ne!(nova_rt_str_hash(a), nova_rt_str_hash(c));
1756 }
1757 }
1758
1759 #[test]
1760 fn str_hash_handles_empty() {
1761 unsafe {
1762 let e = make_str("");
1763 // Must not panic and must be stable.
1764 assert_eq!(nova_rt_str_hash(e), nova_rt_str_hash(make_str("")));
1765 }
1766 }
1767
1768 #[test]
1769 fn int_hash_seed_is_stable_within_a_process() {
1770 // Whatever the value is, every call in one process must agree: a `Map`
1771 // built under one seed and probed under another finds nothing.
1772 let first = nova_rt_int_hash_seed();
1773 for _ in 0..64 {
1774 assert_eq!(nova_rt_int_hash_seed(), first);
1775 }
1776 }
1777
1778 #[test]
1779 fn int_and_str_hash_seeds_are_drawn_separately() {
1780 // Were these one value, `(0).hash()` would equal `("").hash()` exactly
1781 // in Nova. This pins that they differ, which is what the separate
1782 // `RandomState::new()` call buys. It does NOT pin that recovering one
1783 // fails to yield the other: two calls on one thread share `k1` and differ
1784 // in `k0` by the calls between them, and these two may instead come from
1785 // separate draws, so that question is about a keyed hash
1786 // under a related key and is untested here. The variant is deliberately
1787 // not named, for the reason `int_hash_seed`'s doc comment gives: `std`
1788 // documents `DefaultHasher`'s algorithm as unspecified, and the
1789 // related-key argument holds for any keyed hash. See that comment.
1790 // Equality is possible by chance at about 2^-64, which is the same
1791 // order this project already accepts for the cross-process string test.
1792 assert_ne!(int_hash_seed(), str_hash_seed());
1793 // The line above compares the two PRIVATE helpers, so on its own it
1794 // would still pass if the export returned the string seed -- the other
1795 // test in this pair calls the export but compares it only against
1796 // itself. These two pin the export: the first rules out its returning
1797 // the string seed, and the second pins it to the int seed, which is
1798 // the wiring this intrinsic exists to get right. The second is the
1799 // stronger claim and implies the first given the assert above; both
1800 // are here because the `!=` names the specific confusion worth
1801 // excluding if the `==` is ever weakened.
1802 assert_ne!(nova_rt_int_hash_seed() as u64, str_hash_seed());
1803 assert_eq!(nova_rt_int_hash_seed() as u64, int_hash_seed());
1804 }
1805
1806 #[test]
1807 fn str_len_chars_counts_scalars_not_bytes() {
1808 unsafe {
1809 assert_eq!(nova_rt_str_len_chars(make_str("café")), 4);
1810 assert_eq!(nova_rt_str_len_chars(make_str("日本語")), 3);
1811 assert_eq!(nova_rt_str_len_chars(make_str("")), 0);
1812 // A 4-byte scalar outside the BMP is still one character.
1813 assert_eq!(nova_rt_str_len_chars(make_str("🦀")), 1);
1814 }
1815 }
1816
1817 #[test]
1818 fn str_chars_writes_the_array_layout_codegen_expects() {
1819 unsafe {
1820 let block = nova_rt_str_chars(make_str("a→🦀"));
1821 let words = block as *const i64;
1822 // Length header first, then one i64 scalar per element.
1823 assert_eq!(*words, 3);
1824 assert_eq!(*words.add(1), 'a' as i64);
1825 assert_eq!(*words.add(2), '→' as i64);
1826 assert_eq!(*words.add(3), '🦀' as i64);
1827 // Reading back the written words alone cannot tell a correctly
1828 // sized 32-byte block (8 header + 8*3 elements) from one that
1829 // merely has enough allocator slop past a *wrong* declared size
1830 // for these four words to still land in live memory, and cannot
1831 // observe the `scan` flag at all — it only affects GC behaviour,
1832 // not what's readable. Assert what `alloc` actually recorded.
1833 assert_eq!(gc::object_info(block as usize), Some((32, true)));
1834 // An empty string still yields a well-formed zero-length array.
1835 let empty = nova_rt_str_chars(make_str(""));
1836 assert_eq!(*(empty as *const i64), 0);
1837 assert_eq!(gc::object_info(empty as usize), Some((8, true)));
1838 }
1839 }
1840
1841 /// A long string whose byte length and character count differ, so an
1842 /// array sized or filled by bytes instead of characters cannot pass: 100
1843 /// repetitions of `aé🦀` are 300 characters in 700 bytes.
1844 #[test]
1845 fn str_chars_sizes_and_fills_by_character_on_a_long_mixed_string() {
1846 unsafe {
1847 let text: &'static str = Box::leak("aé🦀".repeat(100).into_boxed_str());
1848 assert_eq!(text.len(), 700);
1849 let block = nova_rt_str_chars(make_str(text));
1850 let words = block as *const i64;
1851 assert_eq!(*words, 300);
1852 for (i, c) in text.chars().enumerate() {
1853 assert_eq!(*words.add(1 + i), c as i64, "element {i}");
1854 }
1855 assert_eq!(gc::object_info(block as usize), Some((8 + 8 * 300, true)));
1856 }
1857 }
1858
1859 #[test]
1860 fn str_from_chars_round_trips_and_substitutes_invalid_scalars() {
1861 unsafe {
1862 for s in ["", "ascii", "café", "日本語", "🦀🇹🇭"] {
1863 let back = nova_rt_str_from_chars(nova_rt_str_chars(make_str(s)));
1864 assert_eq!(nova_rt_str_eq(back, make_str(s)), 1, "round-trip {s}");
1865 }
1866 // A surrogate is not a scalar value; substitute, do not abort.
1867 let block = gc::alloc(16, true) as *mut i64;
1868 *block = 1;
1869 *block.add(1) = 0xD800;
1870 let s = nova_rt_str_from_chars(block as *const u8);
1871 assert_eq!(as_str(s), "\u{FFFD}");
1872 }
1873 }
1874
1875 #[test]
1876 fn case_mapping_handles_the_non_one_to_one_cases() {
1877 unsafe {
1878 assert_eq!(as_str(nova_rt_str_to_upper(make_str("Straße"))), "STRASSE");
1879 assert_eq!(as_str(nova_rt_str_to_lower(make_str("HÉLLO"))), "héllo");
1880 assert_eq!(as_str(nova_rt_str_to_upper(make_str(""))), "");
1881 }
1882 }
1883
1884 /// `case_mapping_handles_the_non_one_to_one_cases` only proves the two
1885 /// directions are each individually *correct*; it does not by itself
1886 /// prove that `nova_rt_str_to_upper` and `nova_rt_str_to_lower` are
1887 /// distinguishable from EACH OTHER or from an identity function that
1888 /// just returns its input. Both are real mutations a one-character typo
1889 /// could introduce (swapping which `.to_*case()` call a body makes, or
1890 /// dropping the case-mapping call entirely), and neither would be caught
1891 /// by a test that only checks the "forward" direction of each function in
1892 /// isolation if a caller isn't careful to pick inputs that actually
1893 /// change under the wrong operation too. Every input below is chosen so
1894 /// upper/lower/identity all disagree, so any of the three mutations
1895 /// produces a visibly wrong string here.
1896 #[test]
1897 fn case_mapping_is_distinguishable_from_identity_and_from_each_other() {
1898 unsafe {
1899 let straße_upper = as_str(nova_rt_str_to_upper(make_str("Straße")));
1900 assert_eq!(straße_upper, "STRASSE");
1901 assert_ne!(straße_upper, "Straße"); // rules out identity
1902 assert_ne!(
1903 straße_upper,
1904 as_str(nova_rt_str_to_lower(make_str("Straße")))
1905 ); // rules out the swap
1906
1907 let hello_lower = as_str(nova_rt_str_to_lower(make_str("HÉLLO")));
1908 assert_eq!(hello_lower, "héllo");
1909 assert_ne!(hello_lower, "HÉLLO"); // rules out identity
1910 assert_ne!(hello_lower, as_str(nova_rt_str_to_upper(make_str("HÉLLO")))); // rules out the swap
1911
1912 // A string with no cased characters at all (distinct from a
1913 // mixed string like "abc123", which still has letters to
1914 // transform) is unchanged by either direction — the identity
1915 // outcome is *correct* here, so this specifically exercises that
1916 // neither wrapper corrupts input it has nothing to do to.
1917 assert_eq!(
1918 as_str(nova_rt_str_to_upper(make_str("123 456!"))),
1919 "123 456!"
1920 );
1921 assert_eq!(
1922 as_str(nova_rt_str_to_lower(make_str("123 456!"))),
1923 "123 456!"
1924 );
1925 }
1926 }
1927
1928 /// The requested number of places, including a trailing zero the value does
1929 /// not itself carry.
1930 #[test]
1931 fn float_fixed_renders_the_requested_places() {
1932 unsafe {
1933 assert_eq!(to_string(nova_rt_float_fixed(100.0 / 3.0, 2)), "33.33");
1934 assert_eq!(to_string(nova_rt_float_fixed(1.5, 3)), "1.500");
1935 assert_eq!(to_string(nova_rt_float_fixed(33.333, 0)), "33");
1936 }
1937 }
1938
1939 /// Every lexical shape `std/json`'s `scan_number` can hand this — an
1940 /// integer, a decimal, a negative, both exponent letters, and all three
1941 /// exponent signs — plus the rejection path.
1942 ///
1943 /// "Both exponent spellings" is what this used to say, and it covered only
1944 /// `e` versus `E`, leaving out the `[+-]?` the grammar quoted on
1945 /// [`nova_rt_str_to_float`] admits beside them. `scan_number` accepts an
1946 /// explicit `+` there, so `1e+3` is a shape it really can hand this, and
1947 /// before that case was added no explicit `+` exponent existed anywhere in
1948 /// the repository — grepped, not assumed.
1949 ///
1950 /// The rejection cases are the load-bearing ones. `unwrap_or(0.0)` rather
1951 /// than `unwrap` is not a style choice: `unwrap` here would abort the whole
1952 /// process rather than fail an assertion, because this function cannot
1953 /// unwind (see its doc comment). These two cases are what proves the
1954 /// fallback returns instead.
1955 #[test]
1956 fn str_to_float_parses_every_shape_the_json_scanner_produces() {
1957 unsafe {
1958 assert_eq!(nova_rt_str_to_float(make_str("12")), 12.0);
1959 assert_eq!(nova_rt_str_to_float(make_str("2.5")), 2.5);
1960 assert_eq!(nova_rt_str_to_float(make_str("-3.5")), -3.5);
1961 assert_eq!(nova_rt_str_to_float(make_str("1e3")), 1000.0);
1962 assert_eq!(nova_rt_str_to_float(make_str("1e+3")), 1000.0);
1963 assert_eq!(nova_rt_str_to_float(make_str("1.5E-2")), 0.015);
1964
1965 // `-0` must not collapse to `0`: JSON writes both, they are
1966 // distinct `f64` bit patterns, and Display shows the sign. Compared
1967 // via the sign bit because `-0.0 == 0.0` is true, so an `assert_eq!`
1968 // against `-0.0` would pass even if the sign were lost.
1969 let neg_zero = nova_rt_str_to_float(make_str("-0"));
1970 assert_eq!(neg_zero, 0.0);
1971 assert!(neg_zero.is_sign_negative(), "-0 must keep its sign");
1972
1973 // Rejected input: text that is not a number, and the empty string.
1974 assert_eq!(nova_rt_str_to_float(make_str("nope")), 0.0);
1975 assert_eq!(nova_rt_str_to_float(make_str("")), 0.0);
1976 }
1977 }
1978
1979 /// A negative `places` has no meaning and must never reach `format!`'s
1980 /// precision argument, which would panic.
1981 #[test]
1982 fn negative_places_clamps_to_zero() {
1983 unsafe {
1984 assert_eq!(to_string(nova_rt_float_fixed(1.75, -1)), "2");
1985 assert_eq!(to_string(nova_rt_float_fixed(1.75, -1000)), "2");
1986 }
1987 }
1988
1989 /// Beyond 17 digits an `f64` carries no further information.
1990 #[test]
1991 fn places_above_seventeen_clamps() {
1992 unsafe {
1993 let at_limit = to_string(nova_rt_float_fixed(1.0 / 3.0, 17));
1994 let beyond = to_string(nova_rt_float_fixed(1.0 / 3.0, 40));
1995 assert_eq!(at_limit, beyond, "places above 17 must clamp to 17");
1996 assert_eq!(at_limit.len(), 19, "0. plus 17 digits");
1997 }
1998 }
1999
2000 /// A formatter that fails on a value its type permits is worse than one that
2001 /// shows it, so the non-finite values render as Rust renders them.
2002 #[test]
2003 fn non_finite_values_render_like_rust() {
2004 unsafe {
2005 assert_eq!(to_string(nova_rt_float_fixed(f64::NAN, 2)), "NaN");
2006 assert_eq!(to_string(nova_rt_float_fixed(f64::INFINITY, 2)), "inf");
2007 assert_eq!(to_string(nova_rt_float_fixed(f64::NEG_INFINITY, 2)), "-inf");
2008 }
2009 }
2010}