use std::sync::OnceLock;
pub mod bytes;
mod crypto;
mod file;
pub mod fs;
mod gc;
mod http;
pub mod io;
mod log;
mod net;
mod poll;
pub mod process;
pub mod signal;
pub mod task;
mod time;
#[repr(C)]
pub struct NovaStr {
pub len: u64,
pub ptr: *const u8,
}
pub(crate) const STR_HEADER: usize = std::mem::size_of::<NovaStr>();
pub(crate) fn alloc_str_object(len: usize) -> (*mut NovaStr, *mut u8) {
let size = STR_HEADER.saturating_add(len.max(1));
let base = gc::alloc(size, false);
let node = base as *mut NovaStr;
let bytes = unsafe { base.add(STR_HEADER) };
unsafe {
(*node).len = len as u64;
(*node).ptr = bytes;
}
(node, bytes)
}
pub(crate) fn gc_str(s: &str) -> *mut NovaStr {
unsafe { gc_str_filled(s.len(), |out| out.put(s.as_bytes())) }
}
pub(crate) unsafe fn gc_str_filled(len: usize, fill: impl FnOnce(&mut Fill<'_>)) -> *mut NovaStr {
let (node, bytes) = alloc_str_object(len);
let mut out = Fill {
out: unsafe { std::slice::from_raw_parts_mut(bytes, len) },
at: 0,
};
fill(&mut out);
debug_assert_eq!(
out.at, len,
"a GC string's writer filled a different length than it asked for"
);
node
}
pub(crate) struct Fill<'a> {
out: &'a mut [u8],
at: usize,
}
impl Fill<'_> {
pub(crate) fn put(&mut self, bytes: &[u8]) {
self.out[self.at..self.at + bytes.len()].copy_from_slice(bytes);
self.at += bytes.len();
}
}
#[cfg(test)]
pub(crate) fn gc_bytes_for_test(data: &[u8]) -> *mut NovaStr {
bytes::gc_bytes(data)
}
pub(crate) unsafe fn as_str<'a>(s: *const NovaStr) -> &'a str {
let s = &*s;
std::str::from_utf8_unchecked(std::slice::from_raw_parts(s.ptr, s.len as usize))
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_new(ptr: *const u8, len: u64) -> *mut NovaStr {
let node = gc::alloc(STR_HEADER, false) as *mut NovaStr;
(*node).len = len;
(*node).ptr = ptr;
node
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_println(s: *const NovaStr) {
println!("{}", as_str(s));
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_print(s: *const NovaStr) {
use std::io::Write;
let out = std::io::stdout();
let mut lock = out.lock();
let _ = lock.write_all(as_str(s).as_bytes());
let _ = lock.flush();
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_eprint(s: *const NovaStr) {
use std::io::Write;
let err = std::io::stderr();
let mut lock = err.lock();
let _ = lock.write_all(unsafe { as_str(s) }.as_bytes());
let _ = lock.flush();
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_eprintln(s: *const NovaStr) {
eprintln!("{}", unsafe { as_str(s) });
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_concat(a: *const NovaStr, b: *const NovaStr) -> *mut NovaStr {
let (a, b) = (as_str(a), as_str(b));
gc_str_filled(a.len() + b.len(), |out| {
out.put(a.as_bytes());
out.put(b.as_bytes());
})
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_concat_n(parts: *const u8) -> *mut NovaStr {
let words = parts as *const i64;
let n = (*words).max(0) as usize;
let part = |i: usize| as_str(*words.add(1 + i) as *const NovaStr);
let total = (0..n).map(|i| part(i).len()).sum();
gc_str_filled(total, |out| {
for i in 0..n {
out.put(part(i).as_bytes());
}
})
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_join(sep: *const NovaStr, parts: *const u8) -> *mut NovaStr {
let words = parts as *const i64;
let n = (*words).max(0) as usize;
let sep = as_str(sep);
let part = |i: usize| as_str(*words.add(1 + i) as *const NovaStr);
let total = sep.len() * n.saturating_sub(1) + (0..n).map(|i| part(i).len()).sum::<usize>();
gc_str_filled(total, |out| {
for i in 0..n {
if i > 0 {
out.put(sep.as_bytes());
}
out.put(part(i).as_bytes());
}
})
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_index_of(
haystack: *const NovaStr,
needle: *const NovaStr,
) -> i64 {
let h = as_str(haystack);
match h.find(as_str(needle)) {
Some(at) => h[..at].chars().count() as i64,
None => -1,
}
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_json_quote(s: *const NovaStr) -> *mut NovaStr {
const HEX: &[u8; 16] = b"0123456789abcdef";
let src = as_str(s).as_bytes();
let extra: usize = src
.iter()
.map(|&b| match b {
b'"' | b'\\' | b'\n' | b'\r' | b'\t' | 0x08 | 0x0c => 1,
0x00..=0x1f => 5,
_ => 0,
})
.sum();
gc_str_filled(src.len() + 2 + extra, |out| {
out.put(b"\"");
let mut start = 0;
for (i, &b) in src.iter().enumerate() {
let short: &[u8] = match b {
b'"' => b"\\\"",
b'\\' => b"\\\\",
b'\n' => b"\\n",
b'\r' => b"\\r",
b'\t' => b"\\t",
0x08 => b"\\b",
0x0c => b"\\f",
0x00..=0x1f => b"",
_ => continue,
};
out.put(&src[start..i]);
if short.is_empty() {
out.put(&[
b'\\',
b'u',
b'0',
b'0',
HEX[(b >> 4) as usize],
HEX[(b & 15) as usize],
]);
} else {
out.put(short);
}
start = i + 1;
}
out.put(&src[start..]);
out.put(b"\"");
})
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_eq(a: *const NovaStr, b: *const NovaStr) -> i8 {
(as_str(a) == as_str(b)) as i8
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_cmp(a: *const NovaStr, b: *const NovaStr) -> i64 {
match as_str(a).as_bytes().cmp(as_str(b).as_bytes()) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
}
}
fn str_hash_seed() -> u64 {
static SEED: OnceLock<u64> = OnceLock::new();
*SEED.get_or_init(|| {
use std::hash::{BuildHasher, Hasher};
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
h.write_u64(0);
h.finish()
})
}
fn int_hash_seed() -> u64 {
static SEED: OnceLock<u64> = OnceLock::new();
*SEED.get_or_init(|| {
use std::hash::{BuildHasher, Hasher};
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
h.write_u64(0);
h.finish()
})
}
#[no_mangle]
pub extern "C" fn nova_rt_int_hash_seed() -> i64 {
int_hash_seed() as i64
}
pub fn splitmix64_finalize(mut z: u64) -> u64 {
z ^= z >> 30;
z = z.wrapping_mul(0xbf58_476d_1ce4_e5b9);
z ^= z >> 27;
z = z.wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_hash(s: *const NovaStr) -> i64 {
let mut h: u64 = str_hash_seed();
for b in as_str(s).as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
splitmix64_finalize(h) as i64
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_len_chars(s: *const NovaStr) -> i64 {
as_str(s).chars().count() as i64
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_chars(s: *const NovaStr) -> *mut u8 {
let text = as_str(s);
let n = text.chars().count();
let block = gc::alloc(8 + 8 * n, true);
let words = block as *mut i64;
*words = n as i64;
for (i, c) in text.chars().enumerate() {
*words.add(1 + i) = c as i64;
}
block
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_from_chars(cs: *const u8) -> *mut NovaStr {
let words = cs as *const i64;
let n = (*words).max(0) as usize;
let mut out = String::new();
for i in 0..n {
let v = *words.add(1 + i);
out.push(char::from_u32(v as u32).unwrap_or(char::REPLACEMENT_CHARACTER));
}
gc_str(&out)
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_to_upper(s: *const NovaStr) -> *mut NovaStr {
gc_str(&as_str(s).to_uppercase())
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_to_lower(s: *const NovaStr) -> *mut NovaStr {
gc_str(&as_str(s).to_lowercase())
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_str_to_float(s: *const NovaStr) -> f64 {
as_str(s).parse::<f64>().unwrap_or(0.0)
}
#[no_mangle]
pub extern "C" fn nova_rt_int_to_str(v: i64) -> *mut NovaStr {
let mut text = [0u8; 20];
let mut at = text.len();
let mut rest = v.unsigned_abs();
loop {
at -= 1;
text[at] = b'0' + (rest % 10) as u8;
rest /= 10;
if rest == 0 {
break;
}
}
if v < 0 {
at -= 1;
text[at] = b'-';
}
let text = &text[at..];
unsafe { gc_str_filled(text.len(), |out| out.put(text)) }
}
#[no_mangle]
pub extern "C" fn nova_rt_float_to_str(v: f64) -> *mut NovaStr {
gc_str(&v.to_string())
}
#[no_mangle]
pub extern "C" fn nova_rt_float_fixed(v: f64, places: i64) -> *mut NovaStr {
let places = places.clamp(0, 17) as usize;
gc_str(&format!("{:.*}", places, v))
}
#[no_mangle]
pub extern "C" fn nova_rt_bool_to_str(v: i8) -> *mut NovaStr {
gc_str(if v != 0 { "true" } else { "false" })
}
#[no_mangle]
pub extern "C" fn nova_rt_char_to_str(v: i64) -> *mut NovaStr {
let c = char::from_u32(v as u32).unwrap_or(char::REPLACEMENT_CHARACTER);
gc_str(&c.to_string())
}
#[no_mangle]
pub extern "C" fn nova_rt_alloc(size: i64) -> *mut u8 {
gc::alloc(size.max(8) as usize, true)
}
#[no_mangle]
pub extern "C" fn nova_rt_check_bounds(index: i64, len: i64) {
if index < 0 || index >= len {
eprintln!("nova: panic: array index {index} out of bounds for length {len}");
std::process::abort();
}
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_panic_str(s: *const NovaStr) -> ! {
let msg = if s.is_null() { "" } else { as_str(s) };
eprintln!("nova: panic: {msg}");
std::process::abort();
}
#[no_mangle]
pub extern "C" fn nova_rt_test_selector() -> i64 {
std::env::var("NOVA_TEST_INDEX")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(-1)
}
pub fn symbols() -> Vec<(&'static str, *const u8)> {
vec![
("nova_rt_str_new", nova_rt_str_new as *const u8),
("nova_rt_println", nova_rt_println as *const u8),
("nova_rt_print", nova_rt_print as *const u8),
("nova_rt_eprint", nova_rt_eprint as *const u8),
("nova_rt_eprintln", nova_rt_eprintln as *const u8),
("nova_rt_str_concat", nova_rt_str_concat as *const u8),
("nova_rt_str_concat_n", nova_rt_str_concat_n as *const u8),
("nova_rt_str_join", nova_rt_str_join as *const u8),
("nova_rt_str_index_of", nova_rt_str_index_of as *const u8),
("nova_rt_json_quote", nova_rt_json_quote as *const u8),
("nova_rt_str_eq", nova_rt_str_eq as *const u8),
("nova_rt_str_cmp", nova_rt_str_cmp as *const u8),
("nova_rt_str_hash", nova_rt_str_hash as *const u8),
("nova_rt_str_len_chars", nova_rt_str_len_chars as *const u8),
("nova_rt_str_chars", nova_rt_str_chars as *const u8),
(
"nova_rt_str_from_chars",
nova_rt_str_from_chars as *const u8,
),
("nova_rt_str_to_upper", nova_rt_str_to_upper as *const u8),
("nova_rt_str_to_lower", nova_rt_str_to_lower as *const u8),
("nova_rt_int_to_str", nova_rt_int_to_str as *const u8),
("nova_rt_float_to_str", nova_rt_float_to_str as *const u8),
("nova_rt_float_fixed", nova_rt_float_fixed as *const u8),
("nova_rt_str_to_float", nova_rt_str_to_float as *const u8),
("nova_rt_bool_to_str", nova_rt_bool_to_str as *const u8),
("nova_rt_char_to_str", nova_rt_char_to_str as *const u8),
("nova_rt_alloc", nova_rt_alloc as *const u8),
("nova_rt_check_bounds", nova_rt_check_bounds as *const u8),
("nova_rt_panic_str", nova_rt_panic_str as *const u8),
("nova_rt_test_selector", nova_rt_test_selector as *const u8),
("nova_rt_task_spawn", task::nova_rt_task_spawn as *const u8),
(
"nova_rt_task_block_on",
task::nova_rt_task_block_on as *const u8,
),
(
"nova_rt_task_is_done",
task::nova_rt_task_is_done as *const u8,
),
(
"nova_rt_task_take_output",
task::nova_rt_task_take_output as *const u8,
),
(
"nova_rt_task_release",
task::nova_rt_task_release as *const u8,
),
(
"nova_rt_task_yield_future",
task::nova_rt_task_yield_future as *const u8,
),
(
"nova_rt_task_sleep_future_nanos",
task::nova_rt_task_sleep_future_nanos as *const u8,
),
(
"nova_rt_task_join_future",
task::nova_rt_task_join_future as *const u8,
),
(
"nova_rt_task_timeout_future",
task::nova_rt_task_timeout_future as *const u8,
),
(
"nova_rt_fs_read_to_string",
fs::nova_rt_fs_read_to_string as *const u8,
),
(
"nova_rt_fs_write_string",
fs::nova_rt_fs_write_string as *const u8,
),
(
"nova_rt_fs_take_string",
fs::nova_rt_fs_take_string as *const u8,
),
(
"nova_rt_fs_last_error_message",
fs::nova_rt_fs_last_error_message as *const u8,
),
("nova_rt_fs_temp_dir", fs::nova_rt_fs_temp_dir as *const u8),
("nova_rt_fs_exists", fs::nova_rt_fs_exists as *const u8),
(
"nova_rt_fs_create_dir",
fs::nova_rt_fs_create_dir as *const u8,
),
(
"nova_rt_fs_create_dir_all",
fs::nova_rt_fs_create_dir_all as *const u8,
),
(
"nova_rt_fs_remove_file",
fs::nova_rt_fs_remove_file as *const u8,
),
(
"nova_rt_fs_remove_dir_all",
fs::nova_rt_fs_remove_dir_all as *const u8,
),
("nova_rt_fs_read_dir", fs::nova_rt_fs_read_dir as *const u8),
(
"nova_rt_fs_take_string_array",
fs::nova_rt_fs_take_string_array as *const u8,
),
("nova_rt_fs_kind", fs::nova_rt_fs_kind as *const u8),
("nova_rt_fs_read", fs::nova_rt_fs_read as *const u8),
(
"nova_rt_fs_take_bytes",
fs::nova_rt_fs_take_bytes as *const u8,
),
("nova_rt_fs_write", fs::nova_rt_fs_write as *const u8),
(
"nova_rt_io_stdin_read",
io::nova_rt_io_stdin_read as *const u8,
),
(
"nova_rt_io_stdout_write",
io::nova_rt_io_stdout_write as *const u8,
),
(
"nova_rt_io_stderr_write",
io::nova_rt_io_stderr_write as *const u8,
),
(
"nova_rt_io_stdout_flush",
io::nova_rt_io_stdout_flush as *const u8,
),
(
"nova_rt_io_stderr_flush",
io::nova_rt_io_stderr_flush as *const u8,
),
("nova_rt_file_open", file::nova_rt_file_open as *const u8),
("nova_rt_file_close", file::nova_rt_file_close as *const u8),
("nova_rt_file_read", file::nova_rt_file_read as *const u8),
("nova_rt_file_write", file::nova_rt_file_write as *const u8),
("nova_rt_file_flush", file::nova_rt_file_flush as *const u8),
(
"nova_rt_net_connect_future",
net::nova_rt_net_connect_future as *const u8,
),
("nova_rt_net_close", net::nova_rt_net_close as *const u8),
(
"nova_rt_net_read_future",
net::nova_rt_net_read_future as *const u8,
),
(
"nova_rt_net_write_future",
net::nova_rt_net_write_future as *const u8,
),
(
"nova_rt_net_read_timeout_future",
net::nova_rt_net_read_timeout_future as *const u8,
),
("nova_rt_net_listen", net::nova_rt_net_listen as *const u8),
(
"nova_rt_net_local_port",
net::nova_rt_net_local_port as *const u8,
),
(
"nova_rt_net_accept_future",
net::nova_rt_net_accept_future as *const u8,
),
("nova_rt_bytes_len", bytes::nova_rt_bytes_len as *const u8),
(
"nova_rt_bytes_from_string",
bytes::nova_rt_bytes_from_string as *const u8,
),
(
"nova_rt_bytes_is_utf8",
bytes::nova_rt_bytes_is_utf8 as *const u8,
),
(
"nova_rt_bytes_to_string_unchecked",
bytes::nova_rt_bytes_to_string_unchecked as *const u8,
),
("nova_rt_bytes_at", bytes::nova_rt_bytes_at as *const u8),
(
"nova_rt_bytes_slice",
bytes::nova_rt_bytes_slice as *const u8,
),
(
"nova_rt_bytes_concat",
bytes::nova_rt_bytes_concat as *const u8,
),
(
"nova_rt_bytes_to_ints",
bytes::nova_rt_bytes_to_ints as *const u8,
),
(
"nova_rt_bytes_from_ints",
bytes::nova_rt_bytes_from_ints as *const u8,
),
("nova_rt_bytes_eq", bytes::nova_rt_bytes_eq as *const u8),
("nova_rt_int_hash_seed", nova_rt_int_hash_seed as *const u8),
(
"nova_rt_http_parse_request",
http::nova_rt_http_parse_request as *const u8,
),
(
"nova_rt_crypto_hash",
crypto::nova_rt_crypto_hash as *const u8,
),
(
"nova_rt_crypto_random_bytes",
crypto::nova_rt_crypto_random_bytes as *const u8,
),
(
"nova_rt_crypto_random_int",
crypto::nova_rt_crypto_random_int as *const u8,
),
(
"nova_rt_time_now_nanos",
time::nova_rt_time_now_nanos as *const u8,
),
(
"nova_rt_time_now_epoch_nanos",
time::nova_rt_time_now_epoch_nanos as *const u8,
),
(
"nova_rt_log_config_level",
log::nova_rt_log_config_level as *const u8,
),
(
"nova_rt_log_config_to_stderr",
log::nova_rt_log_config_to_stderr as *const u8,
),
(
"nova_rt_log_set_config",
log::nova_rt_log_set_config as *const u8,
),
(
"nova_rt_shutdown_requested",
signal::nova_rt_shutdown_requested as *const u8,
),
(
"nova_rt_process_arg_count",
process::nova_rt_process_arg_count as *const u8,
),
(
"nova_rt_process_arg",
process::nova_rt_process_arg as *const u8,
),
(
"nova_rt_process_exit",
process::nova_rt_process_exit as *const u8,
),
]
}
#[cfg(test)]
mod tests {
use super::*;
unsafe fn to_string(s: *mut NovaStr) -> String {
as_str(s).to_string()
}
unsafe fn make_str(s: &'static str) -> *mut NovaStr {
nova_rt_str_new(s.as_ptr(), s.len() as u64)
}
#[test]
fn int_to_str_formats() {
unsafe {
assert_eq!(to_string(nova_rt_int_to_str(6765)), "6765");
assert_eq!(to_string(nova_rt_int_to_str(-1)), "-1");
}
}
#[test]
fn int_to_str_matches_rust_at_every_digit_count_and_both_ends() {
let mut cases = vec![0, i64::MAX, i64::MIN, i64::MIN + 1];
let mut p: i64 = 1;
for _ in 0..19 {
cases.extend([p, p - 1, -p, 1 - p]);
p = p.saturating_mul(10);
}
for v in cases {
unsafe { assert_eq!(to_string(nova_rt_int_to_str(v)), v.to_string(), "{v}") };
}
}
#[test]
fn bool_to_str_formats() {
unsafe {
assert_eq!(to_string(nova_rt_bool_to_str(1)), "true");
assert_eq!(to_string(nova_rt_bool_to_str(0)), "false");
}
}
unsafe fn str_array(parts: &[&'static str]) -> *const u8 {
let ptrs: Vec<*mut NovaStr> = parts.iter().map(|p| make_str(p)).collect();
let block = gc::alloc(8 + 8 * ptrs.len(), true) as *mut i64;
*block = ptrs.len() as i64;
for (i, p) in ptrs.iter().enumerate() {
*block.add(1 + i) = *p as i64;
}
block as *const u8
}
const STR_LENGTHS: [usize; 6] = [0, 2, 113, 2032, 2033, 2048];
fn text_of(len: usize) -> &'static str {
let s: String = "abcdefghij".repeat(len / 10 + 1)[..len].to_string();
Box::leak(s.into_boxed_str())
}
unsafe fn assert_one_leaf_string(s: *mut NovaStr, expected: &str) {
let len = expected.len();
assert_eq!(
gc::object_info(s as usize),
Some((16 + len.max(1), false)),
"len {len}: one leaf object of 16 + max(len, 1) bytes"
);
assert_eq!(
(*s).ptr as usize,
s as usize + 16,
"len {len}: bytes inline at offset 16"
);
assert_eq!((*s).len, len as u64, "len {len}");
assert_eq!(as_str(s), expected, "len {len}");
}
#[test]
fn a_runtime_string_is_one_leaf_object_with_its_bytes_inline() {
for len in STR_LENGTHS {
let expected = text_of(len);
unsafe {
assert_one_leaf_string(gc_str(expected), expected);
let (a, b) = expected.split_at(len / 2);
assert_one_leaf_string(nova_rt_str_concat(make_str(a), make_str(b)), expected);
}
}
}
#[test]
fn a_literal_header_is_a_sixteen_byte_leaf() {
unsafe {
let s = make_str("literal");
assert_eq!(gc::object_info(s as usize), Some((16, false)));
assert_eq!(as_str(s), "literal");
}
}
#[test]
fn an_empty_strings_pointer_lies_inside_its_own_object() {
unsafe {
let s = gc_str("");
let base = s as usize;
let ptr = (*s).ptr as usize;
let size = gc::object_info(base).expect("a live object").0;
assert!(
base + 16 <= ptr && ptr < base + size,
"ptr {ptr:#x} must lie in [{:#x}, {:#x})",
base + 16,
base + size
);
}
}
#[test]
fn a_runtime_string_reached_only_through_its_bytes_survives_a_collection() {
for len in [2usize, 113, 2033] {
let expected = text_of(len);
unsafe {
let s = gc_str(expected);
let control = gc_str(expected);
assert!(gc::object_info(s as usize).is_some(), "len {len}");
assert!(gc::object_info(control as usize).is_some(), "len {len}");
for k in [0, len - 1] {
gc::sweep_with_roots_for_test(&[(*s).ptr as usize + k]);
assert_eq!(
gc::object_info(s as usize),
Some((16 + len, false)),
"len {len} k {k}: the whole object survives"
);
}
assert_eq!(
gc::object_info(control as usize),
None,
"len {len}: the unrooted control is swept"
);
assert_eq!(as_str(s), expected, "len {len}");
}
}
}
fn runtime_sources() -> Vec<(String, String)> {
fn walk(dir: &std::path::Path, out: &mut Vec<(String, String)>) {
let mut entries: Vec<_> = std::fs::read_dir(dir)
.expect("the crate's src directory is readable")
.map(|e| e.expect("a readable directory entry").path())
.collect();
entries.sort();
for path in entries {
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|x| x == "rs") {
let text = std::fs::read_to_string(&path).expect("a readable source file");
out.push((path.display().to_string(), text.replace("\r\n", "\n")));
}
}
}
let mut out = Vec::new();
walk(
&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
&mut out,
);
out
}
fn production_code(source: &str) -> String {
source
.lines()
.take_while(|l| l.trim() != "mod tests {")
.filter(|l| !l.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
fn ptr_assignments(code: &str) -> usize {
[".ptr =", ".ptr="]
.iter()
.map(|needle| {
code.match_indices(needle)
.filter(|(i, m)| !code[i + m.len()..].starts_with('='))
.count()
})
.sum()
}
#[test]
fn only_the_two_builders_write_a_novastr_ptr() {
let sources = runtime_sources();
assert!(
sources.len() >= 14,
"scanned only {} runtime source files",
sources.len()
);
let lib_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("lib.rs")
.display()
.to_string();
let lib = sources
.iter()
.find(|(p, _)| *p == lib_path)
.expect("lib.rs is among the scanned files");
for (path, source) in &sources {
let code = production_code(source);
let writes = ptr_assignments(&code);
if *path == lib_path {
assert_eq!(
writes, 2,
"{path}: exactly two `.ptr =` writes, one per builder"
);
} else {
assert_eq!(
writes, 0,
"{path}: writes a `NovaStr`'s `ptr` outside the two builders"
);
}
let literals = code.matches("NovaStr {").count()
- code.matches("struct NovaStr {").count()
- code.matches("*mut NovaStr {").count()
- code.matches("*const NovaStr {").count();
assert_eq!(
literals, 0,
"{path}: builds or implements `NovaStr` outside its definition"
);
let borrows = code
.lines()
.filter(|l| {
l.contains(".ptr")
&& ["&mut ", "addr_of_mut!", "addr_of!", "&raw "]
.iter()
.any(|b| l.contains(b))
})
.count();
assert_eq!(
borrows, 0,
"{path}: takes the address of a `NovaStr`'s `ptr`"
);
let calls = code.matches("nova_rt_str_new(").count();
let definitions = usize::from(*path == lib_path);
assert_eq!(
calls, definitions,
"{path}: calls `nova_rt_str_new` from production code"
);
}
let code = production_code(&lib.1);
for builder in ["fn alloc_str_object(", "fn nova_rt_str_new("] {
let start = code.find(builder).expect("the builder exists");
let end = start
+ code[start..]
.find("\n}\n")
.expect("the builder's closing brace");
assert_eq!(
ptr_assignments(&code[start..end]),
1,
"`{builder}` writes `ptr` exactly once"
);
}
}
#[test]
fn concat_n_joins_every_part_in_order() {
unsafe {
for parts in [
&[][..],
&["solo"][..],
&["a", "b", "c"][..],
&["", "x", ""][..],
&["日本", "語", "🦀", "-", "é"][..],
] {
let got = as_str(nova_rt_str_concat_n(str_array(parts))).to_string();
assert_eq!(got, parts.concat(), "parts {parts:?}");
}
}
}
#[test]
fn index_of_reports_the_first_match_as_a_character_index() {
fn reference(h: &str, n: &str) -> i64 {
let hc: Vec<char> = h.chars().collect();
let nc: Vec<char> = n.chars().collect();
if nc.len() > hc.len() {
return -1;
}
(0..=hc.len() - nc.len())
.find(|&i| hc[i..i + nc.len()] == nc[..])
.map_or(-1, |i| i as i64)
}
unsafe {
for (h, n) in [
("hello wörld, hello wörld", "wörld"),
("日本語のテキスト", "テキ"),
("aé🦀aé🦀", "🦀a"),
("axbab", "ab"),
("aaa", "aa"),
("abc", "abc"),
("ab", "abc"),
("application/json", "\r"),
("", "a"),
("", ""),
("é", ""),
("é", "e"),
] {
let got = nova_rt_str_index_of(make_str(h), make_str(n));
assert_eq!(got, reference(h, n), "haystack {h:?} needle {n:?}");
}
}
}
#[test]
fn json_quote_matches_the_character_level_escaper() {
fn reference(s: &str) -> String {
let mut out = String::from("\"");
for c in s.chars() {
let code = c as u32;
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
_ if code < 32 => match code {
10 => out.push_str("\\n"),
13 => out.push_str("\\r"),
9 => out.push_str("\\t"),
8 => out.push_str("\\b"),
12 => out.push_str("\\f"),
_ => out.push_str(&format!("\\u00{code:02x}")),
},
_ => out.push(c),
}
}
out.push('"');
out
}
let controls: &'static str = Box::leak(
(0u8..32)
.map(|b| b as char)
.collect::<String>()
.into_boxed_str(),
);
unsafe {
for s in [
"",
"User Number 7",
"a\"b\\c\nd",
controls,
"\u{7f}",
"é🦀日本",
"x\u{1f}y\u{0}z",
"\"\\\"",
"tab\there",
"é\"🦀\n",
"日本\u{1}語",
] {
let got = as_str(nova_rt_json_quote(make_str(s)));
assert_eq!(got, reference(s), "input {s:?}");
}
}
}
#[test]
fn join_puts_the_separator_between_every_pair_of_parts() {
unsafe {
for sep in ["", ",", "->", "—🦀"] {
for parts in [
&[][..],
&["solo"][..],
&["a", "b", "c"][..],
&["", "x", ""][..],
&["日本", "語", "🦀", "-", "é"][..],
] {
let got = as_str(nova_rt_str_join(make_str(sep), str_array(parts)));
assert_eq!(got, parts.join(sep), "sep {sep:?} parts {parts:?}");
}
}
}
}
#[test]
fn concat_keeps_both_sides_whole() {
unsafe {
for (a, b) in [
("", ""),
("", "x"),
("x", ""),
("foo", "bar"),
("日本", "🦀é"),
] {
let got = as_str(nova_rt_str_concat(make_str(a), make_str(b)));
assert_eq!(got, format!("{a}{b}"), "{a:?} + {b:?}");
}
}
}
#[test]
fn concat_and_eq_work() {
unsafe {
let a = nova_rt_str_new("foo".as_ptr(), 3);
let b = nova_rt_str_new("bar".as_ptr(), 3);
let ab = nova_rt_str_concat(a, b);
assert_eq!(to_string(ab), "foobar");
let c = nova_rt_str_new("foobar".as_ptr(), 6);
assert_eq!(nova_rt_str_eq(ab, c), 1);
assert_eq!(nova_rt_str_eq(a, b), 0);
}
}
#[test]
fn alloc_is_writable() {
let p = nova_rt_alloc(24);
assert!(!p.is_null());
unsafe {
*(p as *mut i64) = 42;
assert_eq!(*(p as *const i64), 42);
}
}
#[test]
fn str_cmp_orders_lexicographically() {
unsafe {
let a = make_str("abc");
let b = make_str("abd");
let c = make_str("abc");
assert_eq!(nova_rt_str_cmp(a, b), -1);
assert_eq!(nova_rt_str_cmp(b, a), 1);
assert_eq!(nova_rt_str_cmp(a, c), 0);
}
}
#[test]
fn str_cmp_prefix_is_less() {
unsafe {
let a = make_str("ab");
let b = make_str("abc");
assert_eq!(nova_rt_str_cmp(a, b), -1);
}
}
#[test]
fn str_hash_is_deterministic_and_distinguishes() {
unsafe {
let a = make_str("hello");
let b = make_str("hello");
let c = make_str("world");
assert_eq!(nova_rt_str_hash(a), nova_rt_str_hash(b));
assert_ne!(nova_rt_str_hash(a), nova_rt_str_hash(c));
}
}
#[test]
fn str_hash_handles_empty() {
unsafe {
let e = make_str("");
assert_eq!(nova_rt_str_hash(e), nova_rt_str_hash(make_str("")));
}
}
#[test]
fn int_hash_seed_is_stable_within_a_process() {
let first = nova_rt_int_hash_seed();
for _ in 0..64 {
assert_eq!(nova_rt_int_hash_seed(), first);
}
}
#[test]
fn int_and_str_hash_seeds_are_drawn_separately() {
assert_ne!(int_hash_seed(), str_hash_seed());
assert_ne!(nova_rt_int_hash_seed() as u64, str_hash_seed());
assert_eq!(nova_rt_int_hash_seed() as u64, int_hash_seed());
}
#[test]
fn str_len_chars_counts_scalars_not_bytes() {
unsafe {
assert_eq!(nova_rt_str_len_chars(make_str("café")), 4);
assert_eq!(nova_rt_str_len_chars(make_str("日本語")), 3);
assert_eq!(nova_rt_str_len_chars(make_str("")), 0);
assert_eq!(nova_rt_str_len_chars(make_str("🦀")), 1);
}
}
#[test]
fn str_chars_writes_the_array_layout_codegen_expects() {
unsafe {
let block = nova_rt_str_chars(make_str("a→🦀"));
let words = block as *const i64;
assert_eq!(*words, 3);
assert_eq!(*words.add(1), 'a' as i64);
assert_eq!(*words.add(2), '→' as i64);
assert_eq!(*words.add(3), '🦀' as i64);
assert_eq!(gc::object_info(block as usize), Some((32, true)));
let empty = nova_rt_str_chars(make_str(""));
assert_eq!(*(empty as *const i64), 0);
assert_eq!(gc::object_info(empty as usize), Some((8, true)));
}
}
#[test]
fn str_chars_sizes_and_fills_by_character_on_a_long_mixed_string() {
unsafe {
let text: &'static str = Box::leak("aé🦀".repeat(100).into_boxed_str());
assert_eq!(text.len(), 700);
let block = nova_rt_str_chars(make_str(text));
let words = block as *const i64;
assert_eq!(*words, 300);
for (i, c) in text.chars().enumerate() {
assert_eq!(*words.add(1 + i), c as i64, "element {i}");
}
assert_eq!(gc::object_info(block as usize), Some((8 + 8 * 300, true)));
}
}
#[test]
fn str_from_chars_round_trips_and_substitutes_invalid_scalars() {
unsafe {
for s in ["", "ascii", "café", "日本語", "🦀🇹🇭"] {
let back = nova_rt_str_from_chars(nova_rt_str_chars(make_str(s)));
assert_eq!(nova_rt_str_eq(back, make_str(s)), 1, "round-trip {s}");
}
let block = gc::alloc(16, true) as *mut i64;
*block = 1;
*block.add(1) = 0xD800;
let s = nova_rt_str_from_chars(block as *const u8);
assert_eq!(as_str(s), "\u{FFFD}");
}
}
#[test]
fn case_mapping_handles_the_non_one_to_one_cases() {
unsafe {
assert_eq!(as_str(nova_rt_str_to_upper(make_str("Straße"))), "STRASSE");
assert_eq!(as_str(nova_rt_str_to_lower(make_str("HÉLLO"))), "héllo");
assert_eq!(as_str(nova_rt_str_to_upper(make_str(""))), "");
}
}
#[test]
fn case_mapping_is_distinguishable_from_identity_and_from_each_other() {
unsafe {
let straße_upper = as_str(nova_rt_str_to_upper(make_str("Straße")));
assert_eq!(straße_upper, "STRASSE");
assert_ne!(straße_upper, "Straße"); assert_ne!(
straße_upper,
as_str(nova_rt_str_to_lower(make_str("Straße")))
);
let hello_lower = as_str(nova_rt_str_to_lower(make_str("HÉLLO")));
assert_eq!(hello_lower, "héllo");
assert_ne!(hello_lower, "HÉLLO"); assert_ne!(hello_lower, as_str(nova_rt_str_to_upper(make_str("HÉLLO"))));
assert_eq!(
as_str(nova_rt_str_to_upper(make_str("123 456!"))),
"123 456!"
);
assert_eq!(
as_str(nova_rt_str_to_lower(make_str("123 456!"))),
"123 456!"
);
}
}
#[test]
fn float_fixed_renders_the_requested_places() {
unsafe {
assert_eq!(to_string(nova_rt_float_fixed(100.0 / 3.0, 2)), "33.33");
assert_eq!(to_string(nova_rt_float_fixed(1.5, 3)), "1.500");
assert_eq!(to_string(nova_rt_float_fixed(33.333, 0)), "33");
}
}
#[test]
fn str_to_float_parses_every_shape_the_json_scanner_produces() {
unsafe {
assert_eq!(nova_rt_str_to_float(make_str("12")), 12.0);
assert_eq!(nova_rt_str_to_float(make_str("2.5")), 2.5);
assert_eq!(nova_rt_str_to_float(make_str("-3.5")), -3.5);
assert_eq!(nova_rt_str_to_float(make_str("1e3")), 1000.0);
assert_eq!(nova_rt_str_to_float(make_str("1e+3")), 1000.0);
assert_eq!(nova_rt_str_to_float(make_str("1.5E-2")), 0.015);
let neg_zero = nova_rt_str_to_float(make_str("-0"));
assert_eq!(neg_zero, 0.0);
assert!(neg_zero.is_sign_negative(), "-0 must keep its sign");
assert_eq!(nova_rt_str_to_float(make_str("nope")), 0.0);
assert_eq!(nova_rt_str_to_float(make_str("")), 0.0);
}
}
#[test]
fn negative_places_clamps_to_zero() {
unsafe {
assert_eq!(to_string(nova_rt_float_fixed(1.75, -1)), "2");
assert_eq!(to_string(nova_rt_float_fixed(1.75, -1000)), "2");
}
}
#[test]
fn places_above_seventeen_clamps() {
unsafe {
let at_limit = to_string(nova_rt_float_fixed(1.0 / 3.0, 17));
let beyond = to_string(nova_rt_float_fixed(1.0 / 3.0, 40));
assert_eq!(at_limit, beyond, "places above 17 must clamp to 17");
assert_eq!(at_limit.len(), 19, "0. plus 17 digits");
}
}
#[test]
fn non_finite_values_render_like_rust() {
unsafe {
assert_eq!(to_string(nova_rt_float_fixed(f64::NAN, 2)), "NaN");
assert_eq!(to_string(nova_rt_float_fixed(f64::INFINITY, 2)), "inf");
assert_eq!(to_string(nova_rt_float_fixed(f64::NEG_INFINITY, 2)), "-inf");
}
}
}