use std::cell::Cell;
use std::fmt::Write as _;
use crate::GcRef;
#[cfg(test)]
use crate::descriptor::hash_value;
use crate::descriptor::{
BuiltinTypeId, DynamicHasher, FormatSink, FormatStyle, Tracer, TypeDescriptor,
};
const COUNT_IS_CACHED: bool = !cfg!(feature = "adr115-arm-a");
const NOT_COUNTED: u64 = u64::MAX;
#[repr(C)]
pub enum TextPayload {
Owned(OwnedText),
Slice(SourceSlice),
}
#[repr(C)]
pub struct OwnedText {
bytes: Box<str>,
char_count: Cell<u64>,
}
const _: () = {
assert!(std::mem::size_of::<TextPayload>() == 32);
assert!(std::mem::size_of::<OwnedText>() == 24);
assert!(std::mem::size_of::<SourceSlice>() == 24);
assert!(std::mem::align_of::<TextPayload>() == 8);
};
impl OwnedText {
#[must_use]
pub fn new(bytes: Box<str>) -> OwnedText {
OwnedText {
bytes,
char_count: Cell::new(NOT_COUNTED),
}
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.bytes
}
#[inline]
fn char_count(&self) -> u64 {
let cached = self.char_count.get();
if COUNT_IS_CACHED && cached != NOT_COUNTED {
return cached;
}
let counted = count_scalars(self.bytes.as_bytes());
if COUNT_IS_CACHED {
self.char_count.set(counted);
}
counted
}
#[inline]
fn is_one_byte_per_scalar(&self) -> bool {
self.char_count() == self.bytes.len() as u64
}
}
#[inline]
fn count_scalars(bytes: &[u8]) -> u64 {
if bytes.is_ascii() {
return bytes.len() as u64;
}
bytes.iter().filter(|&&b| !is_continuation(b)).count() as u64
}
#[inline]
const fn is_continuation(b: u8) -> bool {
(b as i8) < -0x40
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct SourceSlice {
owner: GcRef,
start: usize,
len: usize,
}
impl SourceSlice {
#[must_use]
pub unsafe fn new(owner: GcRef, start: usize, len: usize) -> Option<SourceSlice> {
let bytes = unsafe { text_bytes(owner.payload::<TextPayload>() as *const TextPayload) };
let end = start.checked_add(len)?;
if end > bytes.len() {
return None;
}
if !is_scalar_boundary(bytes, start) || !is_scalar_boundary(bytes, end) {
return None;
}
Some(SourceSlice { owner, start, len })
}
#[inline]
#[must_use]
pub fn owner(self) -> GcRef {
self.owner
}
}
#[inline]
fn is_scalar_boundary(bytes: &[u8], at: usize) -> bool {
match bytes.get(at) {
None => at == bytes.len(),
Some(&b) => !is_continuation(b),
}
}
impl TextPayload {
#[must_use]
pub fn owned(bytes: impl Into<Box<str>>) -> TextPayload {
TextPayload::Owned(OwnedText::new(bytes.into()))
}
pub fn is_owned(&self) -> bool {
matches!(self, Self::Owned(_))
}
}
pub unsafe fn text_bytes(payload: *const TextPayload) -> &'static [u8] {
let mut payload = payload;
let mut start = 0usize;
let mut len: Option<usize> = None;
loop {
match unsafe { &*payload } {
TextPayload::Owned(owned) => {
let bytes = owned.as_str().as_bytes();
return match len {
None => bytes,
Some(len) => &bytes[start..start + len],
};
}
TextPayload::Slice(slice) => {
start += slice.start;
if len.is_none() {
len = Some(slice.len);
}
payload = slice.owner.payload::<TextPayload>() as *const TextPayload;
}
}
}
}
#[must_use]
pub unsafe fn text_root(text: GcRef) -> (GcRef, usize) {
let mut root = text;
let mut base = 0usize;
loop {
match unsafe { &*(root.payload::<TextPayload>() as *const TextPayload) } {
TextPayload::Owned(_) => return (root, base),
TextPayload::Slice(slice) => {
base += slice.start;
root = slice.owner;
}
}
}
}
unsafe fn text_owner(payload: *const TextPayload) -> &'static OwnedText {
let mut payload = payload;
loop {
match unsafe { &*payload } {
TextPayload::Owned(owned) => return owned,
TextPayload::Slice(slice) => {
payload = slice.owner.payload::<TextPayload>() as *const TextPayload;
}
}
}
}
#[must_use]
pub unsafe fn text_char_count(payload: *const TextPayload) -> usize {
match unsafe { &*payload } {
TextPayload::Owned(owned) => owned.char_count() as usize,
TextPayload::Slice(_) => {
let bytes = unsafe { text_bytes(payload) };
if COUNT_IS_CACHED && unsafe { text_owner(payload) }.is_one_byte_per_scalar() {
bytes.len()
} else {
count_scalars(bytes) as usize
}
}
}
}
#[must_use]
pub unsafe fn text_ascii_bytes(payload: *const TextPayload) -> Option<&'static [u8]> {
if !COUNT_IS_CACHED {
return None;
}
if unsafe { text_owner(payload) }.is_one_byte_per_scalar() {
Some(unsafe { text_bytes(payload) })
} else {
None
}
}
pub unsafe fn text_str(payload: *const TextPayload) -> &'static str {
let bytes = unsafe { text_bytes(payload) };
std::str::from_utf8(bytes)
.expect("a Text payload is UTF-8 by construction; SourceSlice::new enforces it")
}
unsafe fn text_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
match unsafe { &*(payload as *const TextPayload) } {
TextPayload::Owned(_) => {}
TextPayload::Slice(slice) => tracer.trace(slice.owner),
}
}
unsafe fn text_drop(payload: *mut u8) {
unsafe { std::ptr::drop_in_place(payload as *mut TextPayload) };
}
unsafe fn text_format(payload: *const u8, out: &mut FormatSink<'_>) {
let s = unsafe { text_str(payload as *const TextPayload) };
let _ = match out.style() {
FormatStyle::Display => out.write_str(s),
FormatStyle::Debug => out.write_str(&praxis_syntax::literal::quote_text(s)),
};
}
unsafe fn text_equals(a: *const u8, b: *const u8) -> bool {
let a = unsafe { text_bytes(a as *const TextPayload) };
let b = unsafe { text_bytes(b as *const TextPayload) };
a == b
}
unsafe fn text_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
let bytes = unsafe { text_bytes(payload as *const TextPayload) };
hasher.write_bytes(bytes);
}
unsafe fn text_compare(a: *const u8, b: *const u8) -> std::cmp::Ordering {
let a = unsafe { text_bytes(a as *const TextPayload) };
let b = unsafe { text_bytes(b as *const TextPayload) };
a.cmp(b)
}
pub static TEXT: TypeDescriptor = TypeDescriptor::builtin::<TextPayload>(
BuiltinTypeId::Text,
"Text",
text_trace,
text_drop,
text_format,
Some(text_equals),
Some(text_hash),
Some(text_compare),
)
.with_owned_bytes(text_owned_bytes);
unsafe fn text_owned_bytes(payload: *const u8) -> usize {
match unsafe { &*(payload as *const TextPayload) } {
TextPayload::Owned(owned) => owned.as_str().len(),
TextPayload::Slice(_) => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ptr;
#[test]
fn text_renders_one_way_for_the_program_and_another_for_the_debugger() {
let render = |s: &str, style| {
let payload = TextPayload::owned(s);
let mut buf = String::new();
let mut sink = crate::FormatSink::styled(&mut buf, style);
unsafe { (TEXT.format)(ptr::addr_of!(payload) as *const u8, &mut sink) };
buf
};
use crate::FormatStyle::{Debug, Display};
assert_eq!(render("hello", Display), "hello");
assert_eq!(render("hello", Debug), "\"hello\"");
assert_eq!(render("", Display), "");
assert_eq!(render("", Debug), "\"\"");
assert_eq!(render("a\"b", Display), "a\"b");
assert_eq!(render("a\"b", Debug), r#""a\"b""#);
assert_eq!(render("a\nb", Debug), r#""a\nb""#);
}
#[test]
fn owned_text_descriptor_formats_and_compares() {
let a = TextPayload::owned("hello");
let b = TextPayload::owned("hello");
let c = TextPayload::owned("world");
let mut buf = String::new();
unsafe {
(TEXT.format)(
ptr::addr_of!(a) as *const u8,
&mut crate::FormatSink::display(&mut buf),
)
};
assert_eq!(buf, "hello");
assert!(unsafe {
(TEXT.equals.unwrap())(ptr::addr_of!(a) as *const u8, ptr::addr_of!(b) as *const u8)
});
assert!(!unsafe {
(TEXT.equals.unwrap())(ptr::addr_of!(a) as *const u8, ptr::addr_of!(c) as *const u8)
});
}
#[test]
fn text_compares_lexicographically_whatever_its_representation() {
let cmp = TEXT.compare.expect("Text is orderable");
let apple = TextPayload::owned("apple");
let banana = TextPayload::owned("banana");
let apple_again = TextPayload::owned("apple");
let at = |p: &TextPayload| ptr::addr_of!(*p) as *const u8;
assert_eq!(
unsafe { cmp(at(&apple), at(&banana)) },
std::cmp::Ordering::Less
);
assert_eq!(
unsafe { cmp(at(&banana), at(&apple)) },
std::cmp::Ordering::Greater
);
assert_eq!(
unsafe { cmp(at(&apple), at(&apple_again)) },
std::cmp::Ordering::Equal,
"two separately allocated `apple`s are one value"
);
let app = TextPayload::owned("app");
assert_eq!(
unsafe { cmp(at(&app), at(&apple)) },
std::cmp::Ordering::Less
);
let z = TextPayload::owned("z");
let e_acute = TextPayload::owned("é");
assert_eq!(
unsafe { cmp(at(&z), at(&e_acute)) },
std::cmp::Ordering::Less
);
}
#[test]
fn owned_text_hash_is_stable() {
let a = TextPayload::owned("hello");
let b = TextPayload::owned("hello");
let mut ha = crate::descriptor::StructHasher::new();
let mut hb = crate::descriptor::StructHasher::new();
unsafe {
(TEXT.hash.unwrap())(ptr::addr_of!(a) as *const u8, &mut ha);
(TEXT.hash.unwrap())(ptr::addr_of!(b) as *const u8, &mut hb);
}
assert_eq!(ha.finish(), hb.finish());
}
#[test]
fn owned_text_bytes_can_be_borrowed_as_a_manual_subslice() {
let owner = TextPayload::owned("hello, world");
let owner_ptr = ptr::addr_of!(owner);
let bytes = unsafe { text_bytes(owner_ptr) };
assert_eq!(&bytes[7..12], b"world");
let s = unsafe { text_str(owner_ptr) };
assert_eq!(s, "hello, world");
}
#[test]
fn source_slice_traces_its_owner_during_collection() {
let rt = crate::Runtime::new();
let owner = rt.alloc_text("hello");
let slice = unsafe { rt.alloc_text_slice(owner, 1, 3) }.expect("[1, 4) is in range");
let mut roots = crate::RootScope::new();
roots.root(slice);
rt.collect_with(&roots);
assert_eq!(
rt.heap().stats().live_count,
2,
"the rooted slice and its otherwise-unrooted owner must both survive"
);
assert_eq!(slice.as_text(), "ell");
}
#[test]
fn reading_a_deep_slice_chain_does_not_recurse() {
const DEPTH: usize = 4_000;
std::thread::Builder::new()
.stack_size(128 * 1024)
.spawn(|| {
let rt = crate::Runtime::new();
let mut text = rt.alloc_text("hello world");
let mut roots = crate::RootScope::new();
for _ in 0..DEPTH {
text = unsafe { rt.alloc_text_slice(text, 0, 11) }.expect("the whole owner");
roots.root(text);
}
assert_eq!(text.as_text(), "hello world");
let (root, base) = unsafe { text_root(text) };
assert_eq!(base, 0);
assert!(
unsafe { &*(root.payload::<TextPayload>() as *const TextPayload) }.is_owned(),
"text_root resolves to the owned text, not to another slice"
);
})
.expect("spawn")
.join()
.expect("a deep chain must be readable without recursing");
}
#[test]
fn an_out_of_range_or_non_boundary_slice_is_unconstructible() {
let rt = crate::Runtime::new();
let owner = rt.alloc_text("héllo");
let bytes = owner.as_text().len();
assert_eq!(bytes, 6);
unsafe {
assert!(
rt.alloc_text_slice(owner, 0, bytes).is_some(),
"the whole owner is a valid slice of itself"
);
assert!(
rt.alloc_text_slice(owner, bytes, 0).is_some(),
"an empty slice at the end is in range"
);
assert!(
rt.alloc_text_slice(owner, 0, bytes + 1).is_none(),
"a slice past the end is not a Text"
);
assert!(
rt.alloc_text_slice(owner, bytes + 1, 0).is_none(),
"a start past the end is not a Text"
);
assert!(
rt.alloc_text_slice(owner, 1, usize::MAX).is_none(),
"an overflowing length is not a Text"
);
assert!(
rt.alloc_text_slice(owner, 2, 1).is_none(),
"a start inside a multi-byte scalar is not a Text"
);
assert!(
rt.alloc_text_slice(owner, 1, 1).is_none(),
"an end inside a multi-byte scalar is not a Text"
);
let e = rt
.alloc_text_slice(owner, 1, 2)
.expect("[1, 3) is a scalar");
assert_eq!(e.as_text(), "é");
}
}
unsafe fn payload_of(r: GcRef) -> *const TextPayload {
r.payload::<TextPayload>() as *const TextPayload
}
#[cfg(not(feature = "adr115-arm-a"))]
#[test]
fn a_text_is_allocated_uncounted_and_counts_itself_once_when_asked() {
let rt = crate::Runtime::new();
let text = rt.alloc_text("hello");
let payload = unsafe { payload_of(text) };
let TextPayload::Owned(owned) = (unsafe { &*payload }) else {
panic!("a literal is owned")
};
assert_eq!(
owned.char_count.get(),
NOT_COUNTED,
"nothing has asked for the length yet"
);
assert_eq!(unsafe { text_char_count(payload) }, 5);
assert_eq!(
owned.char_count.get(),
5,
"the first ask is what pays for the scan"
);
owned.char_count.set(99);
assert_eq!(unsafe { text_char_count(payload) }, 99);
}
#[cfg(not(feature = "adr115-arm-a"))]
#[test]
fn the_count_equals_the_byte_length_exactly_when_every_scalar_is_one_byte() {
let rt = crate::Runtime::new();
for (src, chars, one_byte) in [
("", 0usize, true),
("hello", 5, true),
("héllo", 5, false),
("é", 1, false),
("aéb", 3, false),
("a\u{1F600}b", 3, false),
("\u{20AC}", 1, false),
("\u{0}\u{7f}", 2, true),
] {
let text = rt.alloc_text(src);
let payload = unsafe { payload_of(text) };
assert_eq!(unsafe { text_char_count(payload) }, chars, "{src:?}");
assert_eq!(chars == src.len(), one_byte, "{src:?}");
assert_eq!(
unsafe { text_ascii_bytes(payload) }.is_some(),
one_byte,
"{src:?} must {} take the byte-index path",
if one_byte { "" } else { "not" }
);
assert_eq!(chars, src.chars().count(), "{src:?}");
}
}
#[cfg(not(feature = "adr115-arm-a"))]
#[test]
fn a_slice_of_a_one_byte_owner_answers_its_length_from_its_byte_length() {
let rt = crate::Runtime::new();
let owner = rt.alloc_text("abcdefghij");
let slice = unsafe { rt.alloc_text_slice(owner, 3, 4) }.expect("[3, 7) is in range");
let (op, sp) = unsafe { (payload_of(owner), payload_of(slice)) };
assert_eq!(unsafe { text_char_count(sp) }, 4);
assert_eq!(unsafe { text_ascii_bytes(sp) }, Some(&b"defg"[..]));
let TextPayload::Owned(owned) = (unsafe { &*op }) else {
panic!("the owner is owned")
};
assert_eq!(owned.char_count.get(), 10);
}
#[test]
fn a_slice_of_a_multi_byte_owner_still_answers_in_scalars() {
let rt = crate::Runtime::new();
let owner = rt.alloc_text("héllo wörld");
assert_eq!(owner.as_text().len(), 13);
let with = unsafe { rt.alloc_text_slice(owner, 0, 3) }.expect("[0, 3) is 'hé'");
assert_eq!(with.as_text(), "hé");
assert_eq!(unsafe { text_char_count(payload_of(with)) }, 2);
assert!(unsafe { text_ascii_bytes(payload_of(with)) }.is_none());
let without = unsafe { rt.alloc_text_slice(owner, 3, 4) }.expect("[3, 7) is 'llo '");
assert_eq!(without.as_text(), "llo ");
assert_eq!(unsafe { text_char_count(payload_of(without)) }, 4);
assert!(unsafe { text_ascii_bytes(payload_of(without)) }.is_none());
let empty = unsafe { rt.alloc_text_slice(owner, 3, 0) }.expect("an empty view");
assert_eq!(unsafe { text_char_count(payload_of(empty)) }, 0);
}
#[test]
fn a_concatenation_counts_its_own_bytes_and_not_an_operands() {
let rt = crate::Runtime::new();
let left = rt.alloc_text("ab");
let right = rt.alloc_text("é");
unsafe {
assert_eq!(text_char_count(payload_of(left)), 2);
assert_eq!(text_char_count(payload_of(right)), 1);
}
let joined = rt.alloc_text(&format!("{}{}", left.as_text(), right.as_text()));
assert_eq!(unsafe { text_char_count(payload_of(joined)) }, 3);
assert!(
unsafe { text_ascii_bytes(payload_of(joined)) }.is_none(),
"an ASCII text joined to a multi-byte one is not byte-indexable"
);
}
#[test]
fn a_slice_reads_the_same_whether_its_owner_was_counted_before_or_after() {
let rt = crate::Runtime::new();
let owner = rt.alloc_text("wxyz");
let early = unsafe { rt.alloc_text_slice(owner, 1, 2) }.expect("[1, 3)");
assert_eq!(unsafe { text_char_count(payload_of(early)) }, 2);
let late = unsafe { rt.alloc_text_slice(owner, 1, 2) }.expect("[1, 3)");
assert_eq!(unsafe { text_char_count(payload_of(late)) }, 2);
assert_eq!(early.as_text(), late.as_text());
}
#[cfg(not(feature = "adr115-arm-a"))]
#[test]
fn counting_a_deep_slice_chain_does_not_recurse() {
const DEPTH: usize = 4_000;
std::thread::Builder::new()
.stack_size(128 * 1024)
.spawn(|| {
let rt = crate::Runtime::new();
let mut text = rt.alloc_text("hello world");
let mut roots = crate::RootScope::new();
for _ in 0..DEPTH {
text = unsafe { rt.alloc_text_slice(text, 0, 11) }.expect("the whole owner");
roots.root(text);
}
unsafe {
assert_eq!(text_char_count(payload_of(text)), 11);
assert_eq!(
text_ascii_bytes(payload_of(text)),
Some(&b"hello world"[..])
);
}
})
.expect("spawn")
.join()
.expect("a deep chain must be countable without recursing");
}
#[test]
fn taking_a_view_does_not_walk_the_owner() {
const OWNER_BYTES: usize = 256 * 1024;
const VIEWS: usize = 8_000;
let rt = crate::Runtime::new();
let owner = rt.alloc_text(&"x".repeat(OWNER_BYTES));
let mut roots = crate::RootScope::new();
roots.root(owner);
for i in 0..VIEWS {
let view = unsafe { rt.alloc_text_slice(owner, i, 4) }.expect("in range");
roots.root(view);
assert_eq!(view.as_text(), "xxxx");
}
}
#[test]
fn hash_value_helper_compiles() {
let mut h = crate::descriptor::StructHasher::new();
hash_value(&mut h, &"x");
}
}