use crate::NovaStr;
pub(crate) fn gc_bytes(bytes: &[u8]) -> *mut NovaStr {
let (node, dst) = crate::alloc_str_object(bytes.len());
unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len()) };
node
}
pub(crate) unsafe fn as_bytes<'a>(b: *const NovaStr) -> &'a [u8] {
unsafe { std::slice::from_raw_parts((*b).ptr, (*b).len as usize) }
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_len(b: *const NovaStr) -> i64 {
unsafe { (*b).len as i64 }
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_from_string(s: *const NovaStr) -> *mut NovaStr {
gc_bytes(unsafe { as_bytes(s) })
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_is_utf8(b: *const NovaStr) -> i8 {
i8::from(std::str::from_utf8(unsafe { as_bytes(b) }).is_ok())
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_to_string_unchecked(b: *const NovaStr) -> *mut NovaStr {
gc_bytes(unsafe { as_bytes(b) })
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_at(b: *const NovaStr, i: i64) -> i64 {
let bytes = unsafe { as_bytes(b) };
let Ok(idx) = usize::try_from(i) else {
crate::task::abort_with("nova_rt_bytes_at: negative index");
};
match bytes.get(idx) {
Some(byte) => i64::from(*byte),
None => crate::task::abort_with("nova_rt_bytes_at: index out of range"),
}
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_slice(
b: *const NovaStr,
start: i64,
end: i64,
) -> *mut NovaStr {
let bytes = unsafe { as_bytes(b) };
let lo = start.max(0).min(bytes.len() as i64) as usize;
let hi = end.max(0).min(bytes.len() as i64) as usize;
if hi <= lo {
return gc_bytes(&[]);
}
gc_bytes(&bytes[lo..hi])
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_concat(
a: *const NovaStr,
b: *const NovaStr,
) -> *mut NovaStr {
let (a, b) = unsafe { (as_bytes(a), as_bytes(b)) };
let mut out = Vec::with_capacity(a.len() + b.len());
out.extend_from_slice(a);
out.extend_from_slice(b);
gc_bytes(&out)
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_to_ints(b: *const NovaStr) -> *mut u8 {
let bytes = unsafe { as_bytes(b) };
let n = bytes.len();
let block = crate::gc::alloc(8 + 8 * n, true);
let words = block as *mut i64;
unsafe {
*words = n as i64;
for (i, byte) in bytes.iter().enumerate() {
*words.add(1 + i) = i64::from(*byte);
}
}
block
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_from_ints(ints: *const u8) -> *mut NovaStr {
let words = ints as *const i64;
let n = unsafe { *words }.max(0) as usize;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let v = unsafe { *words.add(1 + i) };
let Ok(byte) = u8::try_from(v) else {
crate::task::abort_with("nova_rt_bytes_from_ints: element out of range 0..=255");
};
out.push(byte);
}
gc_bytes(&out)
}
#[no_mangle]
pub unsafe extern "C" fn nova_rt_bytes_eq(a: *const NovaStr, b: *const NovaStr) -> i8 {
i8::from(unsafe { as_bytes(a) == as_bytes(b) })
}
#[cfg(test)]
mod tests {
use super::*;
unsafe fn assert_one_leaf_bytes(b: *mut NovaStr, expected: &[u8]) {
let len = expected.len();
assert_eq!(
crate::gc::object_info(b as usize),
Some((16 + len.max(1), false)),
"len {len}: one leaf object of 16 + max(len, 1) bytes"
);
assert_eq!(
(*b).ptr as usize,
b as usize + 16,
"len {len}: bytes inline at offset 16"
);
assert_eq!((*b).len, len as u64, "len {len}");
assert_eq!(as_bytes(b), expected, "len {len}");
}
const BYTES_LENGTHS: [usize; 6] = [0, 2, 113, 2032, 2033, 2048];
fn payload_of(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
#[test]
fn a_bytes_value_is_one_leaf_object_with_its_bytes_inline() {
for len in BYTES_LENGTHS {
let payload = payload_of(len);
unsafe { assert_one_leaf_bytes(gc_bytes(&payload), &payload) };
}
}
#[test]
fn a_bytes_value_is_tracked_at_its_full_size() {
let b = gc_bytes(&[7u8; 32]);
assert_eq!(
crate::gc::object_info(b as usize),
Some((16 + 32, false)),
"a 32-byte payload is one object of 16 + 32 bytes"
);
}
#[test]
fn a_string_from_bytes_is_one_leaf_object_with_its_bytes_inline() {
for len in BYTES_LENGTHS {
let text: String = "abcdefghij".repeat(len / 10 + 1)[..len].to_string();
unsafe {
let s = nova_rt_bytes_to_string_unchecked(gc_bytes(text.as_bytes()));
assert_one_leaf_bytes(s, text.as_bytes());
}
}
}
#[test]
fn a_bytes_slice_of_a_combined_object_is_its_own_leaf_object() {
let payload = payload_of(113);
unsafe {
let b = gc_bytes(&payload);
let s = nova_rt_bytes_slice(b, 10, 50);
assert_ne!(s, b, "a slice is a fresh object, never a view");
assert_one_leaf_bytes(s, &payload[10..50]);
}
}
#[test]
fn is_utf8_distinguishes_valid_bytes_from_invalid() {
unsafe {
assert_eq!(nova_rt_bytes_is_utf8(gc_bytes(b"hi")), 1, "valid UTF-8");
assert_eq!(
nova_rt_bytes_is_utf8(gc_bytes(&[0xFF])),
0,
"0xFF is not a valid UTF-8 sequence on its own"
);
}
}
#[test]
fn bytes_len_reports_the_real_length_for_more_than_one_size() {
unsafe {
assert_eq!(nova_rt_bytes_len(gc_bytes(&[1, 2, 3])), 3);
assert_eq!(nova_rt_bytes_len(gc_bytes(&[7u8; 32])), 32);
}
}
#[test]
fn to_ints_writes_the_array_layout_codegen_expects() {
let b = gc_bytes(&[7, 8]);
let block = unsafe { nova_rt_bytes_to_ints(b) };
assert_eq!(
crate::gc::object_info(block as usize),
Some((8 + 8 * 2, true)),
"an array block is a length word plus one word per element, SCANNED"
);
let words = block as *mut i64;
unsafe {
assert_eq!(*words, 2);
assert_eq!(*words.add(1), 7);
assert_eq!(*words.add(2), 8);
}
}
#[test]
fn no_bytes_intrinsic_can_panic() {
let source = include_str!("bytes.rs");
let code = source.split("mod tests {").next().unwrap_or(source);
for needle in ["unwrap()", ".expect(", "panic!", "format!", "RefCell"] {
assert!(
!code.contains(needle),
"a std/bytes intrinsic must not panic: `{needle}` found in this \
file's production code, which is reachable from a generated poll \
boundary with no landing pad to unwind through"
);
}
}
}