praxis_runtime/text.rs
1//! The `Text` scalar descriptor (§4.3, ADR-013).
2//!
3//! `Text` is an immutable UTF-8 payload referenced through a `GcRef` (§4.3).
4//! §4.3 allows two representations:
5//!
6//! - An **owned** UTF-8 payload (`Box<str>`).
7//! - A **source slice** carrying `(owner: GcRef, start, length)` (§7.10) — a
8//! zero-copy view into another `Text` (typically the process-input buffer).
9//!
10//! Both are produced: the input parser allocates source slices pointing into
11//! the immutable stdin buffer, and string literals are owned. The descriptor
12//! callbacks handle both variants through [`text_bytes`], which follows slice
13//! owners — sound because the collector is non-moving (ADR-011) and owners are
14//! kept alive by GC reachability. Every walk of an owner chain in this module
15//! is iterative, because nothing bounds its depth
16//! (`reading_a_deep_slice_chain_does_not_recurse`).
17//!
18//! Owned payloads own a Rust allocation (`Box<str>`), so [`TEXT`]'s `drop_value`
19//! releases it on sweep (§12.5). Slice payloads carry only a `GcRef` (traced, not
20//! owned) plus offsets — no Rust resources to drop.
21//!
22//! An owned payload additionally carries a **lazily computed scalar count**
23//! (ADR-115), which is what makes `t.len()` and `t[i]` O(1) on the texts a
24//! program actually indexes. The count is the whole mechanism: `count ==
25//! bytes.len()` is exactly "every scalar in this text is one byte", so it is
26//! both the length answer and the byte-indexing licence, and a slice inherits
27//! the licence from its owner because a view of one-byte scalars is one-byte
28//! scalars. See [`text_char_count`] and [`text_ascii_bytes`].
29
30use std::cell::Cell;
31use std::fmt::Write as _;
32
33use crate::GcRef;
34#[cfg(test)]
35use crate::descriptor::hash_value;
36use crate::descriptor::{
37 BuiltinTypeId, DynamicHasher, FormatSink, FormatStyle, Tracer, TypeDescriptor,
38};
39
40/// **The ADR-115 measurement toggle**: the only difference between the A/B arms
41/// the caching decision is measured with.
42///
43/// `false` — enabled by the `adr115-arm-a` feature — keeps the representation
44/// byte-for-byte identical and makes the cache never answer: every count is
45/// recomputed from the bytes and [`text_ascii_bytes`] refuses, so `t.len()`
46/// walks the text and `t[i]` decodes to the index. It exists so that arm A
47/// differs from arm B in this mechanism and in nothing else.
48const COUNT_IS_CACHED: bool = !cfg!(feature = "adr115-arm-a");
49
50/// The value [`OwnedText::char_count`] holds until someone asks.
51///
52/// A real count can never collide with it: a text with `u64::MAX` scalars would
53/// need `u64::MAX` bytes, and the `Box<str>` holding them cannot be allocated
54/// on a 64-bit host. So this is a sentinel in the strong sense — the state
55/// "counted, and the count happens to be `NOT_COUNTED`" is unreachable rather
56/// than merely unlikely.
57const NOT_COUNTED: u64 = u64::MAX;
58
59/// The `Text` payload: either an owned UTF-8 string or a zero-copy slice into
60/// another `Text` (the input buffer) (§4.3, §7.10, ADR-013).
61#[repr(C)]
62pub enum TextPayload {
63 /// An owned, heap-allocated UTF-8 string (string literals, runtime-built
64 /// text) together with its lazily computed scalar count (ADR-115).
65 Owned(OwnedText),
66 /// A zero-copy view into another `Text`'s bytes (§7.10). `owner` is traced
67 /// by the descriptor so the slice keeps its backing alive.
68 Slice(SourceSlice),
69}
70
71/// An owned UTF-8 payload and the number of Unicode scalars in it, computed on
72/// first demand (ADR-115).
73///
74/// The fields are private and [`OwnedText::new`] is the only constructor,
75/// because a count that does not describe these bytes is not a slow `Text`, it
76/// is a wrong one: `t.len()` would answer someone else's length and `t[i]`
77/// would index bytes in a text that has multi-byte scalars. The only writer is
78/// [`OwnedText::char_count`], which writes what it just counted from
79/// `self.bytes`, and `Text` is immutable (ADR-085 allocates a fresh payload for
80/// `+`), so there is no path that could invalidate one.
81///
82/// **This costs zero bytes.** `Box<str>` is 16 and [`SourceSlice`] is 24, so
83/// the `#[repr(C)]` enum's union already reserved 8 bytes the owned variant
84/// never used. The `const _` below is what holds that claim true.
85#[repr(C)]
86pub struct OwnedText {
87 bytes: Box<str>,
88 /// [`NOT_COUNTED`] until the first [`char_count`](Self::char_count).
89 ///
90 /// `Cell` because counting happens behind the `&TextPayload` every
91 /// descriptor callback and every accessor already has — the runtime is
92 /// single-threaded (`RuntimeContext` is not `Sync`) and this is the same
93 /// interior mutability `GcHeader` uses for its own sweep-time writes.
94 char_count: Cell<u64>,
95}
96
97/// `size_of::<TextPayload>()` is **32**, so a `Text` block is 48 and its size
98/// class does not move (ADR-109's ladder is 16, 24, 32, …, 128 with a 16-byte
99/// header). This is the claim ADR-115 rests on, asserted here so that a future
100/// field cannot silently move a `Text` to the 56-byte class and charge the
101/// pacer 16.7% more per text object.
102const _: () = {
103 assert!(std::mem::size_of::<TextPayload>() == 32);
104 assert!(std::mem::size_of::<OwnedText>() == 24);
105 assert!(std::mem::size_of::<SourceSlice>() == 24);
106 assert!(std::mem::align_of::<TextPayload>() == 8);
107};
108
109impl OwnedText {
110 /// An owned payload over `bytes`, not yet counted.
111 #[must_use]
112 pub fn new(bytes: Box<str>) -> OwnedText {
113 OwnedText {
114 bytes,
115 char_count: Cell::new(NOT_COUNTED),
116 }
117 }
118
119 /// The payload's bytes as a `&str`. Owned text is a `Box<str>`, so this
120 /// needs no validation and no decoding.
121 #[inline]
122 #[must_use]
123 pub fn as_str(&self) -> &str {
124 &self.bytes
125 }
126
127 /// The number of Unicode scalars in these bytes, counting them the first
128 /// time and remembering the answer.
129 ///
130 /// **Lazy, not computed at construction**: `praxis_get_input`'s buffer is
131 /// one owned `Text` that can be tens of megabytes, and a program that reads
132 /// its input and never indexes any text would pay a full scan of it for
133 /// nothing. Every caller of this is already asking a question whose honest
134 /// answer is a scan.
135 #[inline]
136 fn char_count(&self) -> u64 {
137 let cached = self.char_count.get();
138 if COUNT_IS_CACHED && cached != NOT_COUNTED {
139 return cached;
140 }
141 let counted = count_scalars(self.bytes.as_bytes());
142 if COUNT_IS_CACHED {
143 self.char_count.set(counted);
144 }
145 counted
146 }
147
148 /// True iff every scalar in these bytes is one byte wide — that is, iff a
149 /// byte index into them is a character index.
150 ///
151 /// **This is the count, not a second field.** A UTF-8 text has one byte per
152 /// scalar exactly when its scalar count equals its byte length, so the
153 /// cached count already answers it. A separate `is_ascii` flag would be a
154 /// second thing to keep true about the same bytes, and the pair
155 /// `(count, flag)` has states — a text that claims 3 scalars in 5 bytes and
156 /// claims to be ASCII — that this cannot express.
157 ///
158 /// The equivalence needs the bytes to be **valid** UTF-8, and that is the
159 /// payload's invariant (`text_str`, ADR-111): a leading byte at or above
160 /// `0x80` is followed by at least one continuation byte, so "no
161 /// continuation bytes" and "every byte below `0x80`" are the same statement
162 /// about a valid encoding and different statements about arbitrary bytes.
163 #[inline]
164 fn is_one_byte_per_scalar(&self) -> bool {
165 self.char_count() == self.bytes.len() as u64
166 }
167}
168
169/// The number of Unicode scalars in `bytes`, which must be valid UTF-8.
170///
171/// Counting the bytes that are *not* continuation bytes is the same answer as
172/// `str::chars().count()` and needs neither a `&str` nor a decode: in UTF-8
173/// every scalar contributes exactly one leading byte. The `is_ascii` short
174/// circuit is not an optimization of the answer but of the loop — it is the
175/// case that holds for essentially all puzzle input, and the standard library's
176/// `is_ascii` is word-at-a-time where a filtered count is byte-at-a-time.
177#[inline]
178fn count_scalars(bytes: &[u8]) -> u64 {
179 if bytes.is_ascii() {
180 return bytes.len() as u64;
181 }
182 bytes.iter().filter(|&&b| !is_continuation(b)).count() as u64
183}
184
185/// True iff `b` is a UTF-8 continuation byte — `0b10xx_xxxx`.
186#[inline]
187const fn is_continuation(b: u8) -> bool {
188 (b as i8) < -0x40
189}
190
191/// A validated zero-copy view of `owner`'s bytes over `[start, start + len)`.
192///
193/// The fields are private and [`SourceSlice::new`] is the only constructor,
194/// because the range is not a hint: a view whose end runs past the owner, or
195/// whose ends fall inside a multi-byte scalar, is not a `Text`. Neither is
196/// constructible, so bad ranges cannot surface far from their cause as an
197/// out-of-range slice or as a `Text` that reads empty.
198///
199/// `#[repr(C)]` fixes the field order, so the payload layout is stable.
200#[repr(C)]
201#[derive(Clone, Copy, Debug)]
202pub struct SourceSlice {
203 owner: GcRef,
204 start: usize,
205 len: usize,
206}
207
208impl SourceSlice {
209 /// A view of `owner`'s bytes over `[start, start + len)`, or `None` if that
210 /// is not a `Text`: a range past the end, a length that overflows, or ends
211 /// that are not UTF-8 scalar boundaries.
212 ///
213 /// # Safety
214 /// `owner` must be a live `Text` `GcRef` — its payload is read to validate
215 /// the range.
216 #[must_use]
217 pub unsafe fn new(owner: GcRef, start: usize, len: usize) -> Option<SourceSlice> {
218 // SAFETY: caller guarantees `owner` is a live Text.
219 let bytes = unsafe { text_bytes(owner.payload::<TextPayload>() as *const TextPayload) };
220 let end = start.checked_add(len)?;
221 if end > bytes.len() {
222 return None;
223 }
224 // Both ends must begin a scalar. A view that splits one is not UTF-8,
225 // and reading it as a `&str` would fail.
226 //
227 // **Two byte tests, not a validation of the whole owner** (ADR-115).
228 // Re-validating the owner's UTF-8 here would make parsing an n-byte
229 // input into k captures O(n·k), and it would answer a question that is
230 // already settled: since ADR-111 the one door raw host bytes enter
231 // through (`praxis_get_input`) validates them, `praxis_alloc_text`'s
232 // callers owe UTF-8 as a precondition, and `text_str` states the
233 // invariant by `expect`ing it. `is_scalar_boundary` is
234 // `str::is_char_boundary`'s test spelled on bytes.
235 if !is_scalar_boundary(bytes, start) || !is_scalar_boundary(bytes, end) {
236 return None;
237 }
238 Some(SourceSlice { owner, start, len })
239 }
240
241 /// The `Text` this view borrows from. Traced, so the backing stays alive.
242 #[inline]
243 #[must_use]
244 pub fn owner(self) -> GcRef {
245 self.owner
246 }
247}
248
249/// True iff `at` begins a UTF-8 scalar in `bytes`, or is the end of them.
250///
251/// `str::is_char_boundary` without the `&str`: a byte begins a scalar iff it is
252/// not a continuation byte, and the end position always does.
253#[inline]
254fn is_scalar_boundary(bytes: &[u8], at: usize) -> bool {
255 match bytes.get(at) {
256 None => at == bytes.len(),
257 Some(&b) => !is_continuation(b),
258 }
259}
260
261impl TextPayload {
262 /// An owned payload over `bytes`, not yet counted.
263 ///
264 /// The variant's field is an [`OwnedText`] whose own field is private, so
265 /// this is the only way to build one and there is no way to build one whose
266 /// count does not describe its bytes.
267 #[must_use]
268 pub fn owned(bytes: impl Into<Box<str>>) -> TextPayload {
269 TextPayload::Owned(OwnedText::new(bytes.into()))
270 }
271
272 /// True iff this is the [`Owned`](Self::Owned) variant.
273 pub fn is_owned(&self) -> bool {
274 matches!(self, Self::Owned(_))
275 }
276}
277
278/// Read the UTF-8 bytes of a `TextPayload`, following slice owners.
279///
280/// For a [`TextPayload::Slice`], this reads through the `owner` reference. This
281/// is sound because the GC is non-moving (ADR-011): the owner stays at its
282/// address as long as it is reachable, and the slice's `trace` keeps it reachable.
283///
284/// # Safety
285/// `payload` must point at a fully initialized, validly-linked `TextPayload` —
286/// every `owner` along the chain must point at a live `Text` object.
287pub unsafe fn text_bytes(payload: *const TextPayload) -> &'static [u8] {
288 // **Iterative, not recursive.** Recursing through `owner` would cost a
289 // frame per link, and a long enough chain would overflow the stack and
290 // abort the process — inside `extern "C"`, where an abort is the one
291 // outcome §10.4 rules out. Nothing about a chain is illegal, so the depth
292 // cannot be bounded by validation; the read simply must not recurse. (The
293 // parser also refuses to *build* chains: `Input::new` collapses to the root
294 // owner, see `text_root`.)
295 let mut payload = payload;
296 let mut start = 0usize;
297 // The window is the OUTERMOST slice's length: each step inward widens the
298 // owner, so only the first `len` describes the text being read.
299 let mut len: Option<usize> = None;
300 loop {
301 // SAFETY: caller guarantees `payload` points at a valid TextPayload and
302 // that every `owner` along the chain is a live Text.
303 match unsafe { &*payload } {
304 TextPayload::Owned(owned) => {
305 let bytes = owned.as_str().as_bytes();
306 // In range by construction: `SourceSlice::new` is the only
307 // constructor and it rejects anything else. Nothing is clamped
308 // here — a clamp would turn a bad range into a *different,
309 // plausible* Text.
310 return match len {
311 None => bytes,
312 Some(len) => &bytes[start..start + len],
313 };
314 }
315 TextPayload::Slice(slice) => {
316 start += slice.start;
317 if len.is_none() {
318 len = Some(slice.len);
319 }
320 // SAFETY: the owner is a live Text; the GC is non-moving
321 // (ADR-011) and the slice's `trace` keeps the owner reachable.
322 payload = slice.owner.payload::<TextPayload>() as *const TextPayload;
323 }
324 }
325 }
326}
327
328/// The root **owned** `Text` behind `text`, and the absolute offset at which
329/// `text`'s own bytes begin inside it.
330///
331/// A `SourceSlice` may name another `SourceSlice`, so "the owner" is in general
332/// a chain. The parser refuses to extend one: `parse(t, P)` over a `t` that is
333/// itself a slice would otherwise allocate slices of a slice, and every `Text`
334/// that parse produced would pay the chain's depth on every read. Resolving
335/// once, when the [`Input`](crate::parser::cursor::Input) is built, keeps every
336/// slice the interpreter allocates exactly one level deep.
337///
338/// # Safety
339/// `text` must be a live `Text` `GcRef`, and every `owner` along its chain must
340/// point at a live `Text`.
341#[must_use]
342pub unsafe fn text_root(text: GcRef) -> (GcRef, usize) {
343 let mut root = text;
344 let mut base = 0usize;
345 loop {
346 // SAFETY: caller guarantees the chain is live.
347 match unsafe { &*(root.payload::<TextPayload>() as *const TextPayload) } {
348 TextPayload::Owned(_) => return (root, base),
349 TextPayload::Slice(slice) => {
350 base += slice.start;
351 root = slice.owner;
352 }
353 }
354 }
355}
356
357/// The root **owned** payload behind `payload`, following slice owners.
358///
359/// This is [`text_root`] over a raw payload pointer rather than a `GcRef`, and
360/// it exists for the same reason [`text_bytes`] is iterative: the depth of an
361/// owner chain is not bounded by anything (`reading_a_deep_slice_chain_does_not_recurse`),
362/// so a read must not recurse through it. The parser separately refuses to
363/// build chains at all — `Input::new` collapses to the root owner — so in
364/// practice this is one step.
365///
366/// # Safety
367/// See [`text_bytes`]: every `owner` along the chain must point at a live
368/// `Text`.
369unsafe fn text_owner(payload: *const TextPayload) -> &'static OwnedText {
370 let mut payload = payload;
371 loop {
372 // SAFETY: caller guarantees `payload` points at a valid TextPayload and
373 // that every `owner` along the chain is a live Text.
374 match unsafe { &*payload } {
375 TextPayload::Owned(owned) => return owned,
376 TextPayload::Slice(slice) => {
377 payload = slice.owner.payload::<TextPayload>() as *const TextPayload;
378 }
379 }
380 }
381}
382
383/// The number of Unicode scalars in `payload` — `t.len()`'s answer (§4.3,
384/// ADR-086) — in O(1) once the text or its owner has been counted once.
385///
386/// **Where the count lives is the decision** (ADR-115). An owned text caches
387/// its own; a slice has no room for one, and takes the answer from its owner
388/// instead: a view of a text whose scalars are all one byte has one scalar per
389/// byte, so its length *is* its byte length. When the owner has a multi-byte
390/// scalar anywhere the slice has to count its own bytes, which is O(its own
391/// length) — the same cost `t[i]` pays on that text either way, so no loop that
392/// was quadratic becomes linear by caching it and no loop that is linear
393/// becomes quadratic by not.
394///
395/// # Safety
396/// See [`text_bytes`].
397#[must_use]
398pub unsafe fn text_char_count(payload: *const TextPayload) -> usize {
399 // SAFETY: caller guarantees the chain is live.
400 match unsafe { &*payload } {
401 TextPayload::Owned(owned) => owned.char_count() as usize,
402 TextPayload::Slice(_) => {
403 // SAFETY: same guarantee.
404 let bytes = unsafe { text_bytes(payload) };
405 // SAFETY: same guarantee.
406 if COUNT_IS_CACHED && unsafe { text_owner(payload) }.is_one_byte_per_scalar() {
407 bytes.len()
408 } else {
409 count_scalars(bytes) as usize
410 }
411 }
412 }
413}
414
415/// The bytes of `payload` when a byte index into them is a character index —
416/// that is, when every scalar in the text is one byte wide — and `None`
417/// otherwise.
418///
419/// `t[i]` is defined on characters (§4.3, ADR-086); indexing bytes is an
420/// optimization that is only valid here. The property is decided by the **root
421/// owner's** count rather than by this text's own bytes, and that is what makes
422/// it O(1) for a slice: scanning a slice to find out whether it is ASCII costs
423/// exactly what decoding it to the index costs, so it would buy nothing,
424/// whereas the owner's count is computed once and answers for every view of it
425/// forever. `SourceSlice::new` refuses ends that split a scalar, so a slice of
426/// a one-byte-per-scalar owner is itself one-byte-per-scalar with no further
427/// check.
428///
429/// # Safety
430/// See [`text_bytes`].
431#[must_use]
432pub unsafe fn text_ascii_bytes(payload: *const TextPayload) -> Option<&'static [u8]> {
433 if !COUNT_IS_CACHED {
434 return None;
435 }
436 // SAFETY: caller guarantees the chain is live.
437 if unsafe { text_owner(payload) }.is_one_byte_per_scalar() {
438 // SAFETY: same guarantee.
439 Some(unsafe { text_bytes(payload) })
440 } else {
441 None
442 }
443}
444
445/// Read a `TextPayload` as a `&str`, following slice owners.
446///
447/// # Safety
448/// See [`text_bytes`]; additionally the bytes must be valid UTF-8 (always true
449/// for Text by construction — the parser only splits on UTF-8 boundaries).
450pub unsafe fn text_str(payload: *const TextPayload) -> &'static str {
451 // SAFETY: Text payloads are always valid UTF-8 by construction. An owned
452 // payload is a `Box<str>`; a slice comes from `SourceSlice::new`, which
453 // rejects ends that are not scalar boundaries. The error case panics with a
454 // diagnosis rather than reading as `""` or going through
455 // `from_utf8_unchecked`, either of which would hide a mis-sliced `Text`.
456 let bytes = unsafe { text_bytes(payload) };
457 std::str::from_utf8(bytes)
458 .expect("a Text payload is UTF-8 by construction; SourceSlice::new enforces it")
459}
460
461// ---- descriptor callbacks -------------------------------------------------
462
463unsafe fn text_trace(payload: *mut u8, tracer: &mut dyn Tracer) {
464 // SAFETY: caller guarantees `payload` points at an initialized TextPayload.
465 match unsafe { &*(payload as *const TextPayload) } {
466 // Owned text has no nested GcRef; trace is a no-op.
467 TextPayload::Owned(_) => {}
468 // A slice must keep its owner alive (ADR-013).
469 TextPayload::Slice(slice) => tracer.trace(slice.owner),
470 }
471}
472
473unsafe fn text_drop(payload: *mut u8) {
474 // SAFETY: caller guarantees `payload` points at an initialized TextPayload.
475 // `drop_in_place` frees the owned Box<str> (Owned variant); for Slice it
476 // drops the GcRef (a no-op pointer copy) without touching the owner object.
477 unsafe { std::ptr::drop_in_place(payload as *mut TextPayload) };
478}
479
480/// **The one descriptor callback that reads its sink's style**, and the reason
481/// the style exists (§11.4, [`FormatStyle`]).
482///
483/// `Display` writes the characters: that is what `out(s)` means, what `"{s}"`
484/// splices, and what `praxis run` prints for a program whose answer is a string.
485///
486/// `Debug` writes a quoted literal, because the debugger's displays give a value
487/// one line and no other context, and a bare `Text` is ambiguous there in three
488/// ways at once. An empty one writes nothing, which the renderer can only report
489/// as `<unreadable>`, since "the descriptor wrote no bytes" and "the read
490/// failed" are the same observation. One containing a `"` could not be told from
491/// two values, and one containing a newline would take a row that belongs to the
492/// local underneath it.
493unsafe fn text_format(payload: *const u8, out: &mut FormatSink<'_>) {
494 // SAFETY: caller guarantees `payload` points at a TextPayload.
495 let s = unsafe { text_str(payload as *const TextPayload) };
496 let _ = match out.style() {
497 FormatStyle::Display => out.write_str(s),
498 // Through `praxis-syntax`, which owns the escape table this inverts: a
499 // second copy of the rule here would be free to disagree with
500 // `decode_escape` about what `\t` is.
501 FormatStyle::Debug => out.write_str(&praxis_syntax::literal::quote_text(s)),
502 };
503}
504
505unsafe fn text_equals(a: *const u8, b: *const u8) -> bool {
506 // SAFETY: caller guarantees both pointers point at TextPayloads.
507 let a = unsafe { text_bytes(a as *const TextPayload) };
508 let b = unsafe { text_bytes(b as *const TextPayload) };
509 a == b
510}
511
512unsafe fn text_hash(payload: *const u8, hasher: &mut dyn DynamicHasher) {
513 // SAFETY: caller guarantees `payload` points at a TextPayload.
514 let bytes = unsafe { text_bytes(payload as *const TextPayload) };
515 hasher.write_bytes(bytes);
516}
517
518/// Lexicographic order over the text's bytes (ADR-045). UTF-8 byte order *is*
519/// code-point order, so this needs no decoding. Comparing the payload itself
520/// would order texts by address: its first eight bytes are a `Box<str>` pointer
521/// for an owned text and a `GcRef` for a slice.
522///
523/// # Safety
524/// Both pointers must point at `TextPayload`s.
525unsafe fn text_compare(a: *const u8, b: *const u8) -> std::cmp::Ordering {
526 // SAFETY: caller guarantees both pointers point at TextPayloads.
527 let a = unsafe { text_bytes(a as *const TextPayload) };
528 let b = unsafe { text_bytes(b as *const TextPayload) };
529 a.cmp(b)
530}
531
532/// Descriptor for the `Text` scalar (§4.3). Handles both owned and source-slice
533/// payloads (ADR-013). A single descriptor serves all `Text` values.
534pub static TEXT: TypeDescriptor = TypeDescriptor::builtin::<TextPayload>(
535 BuiltinTypeId::Text,
536 "Text",
537 text_trace,
538 text_drop,
539 text_format,
540 Some(text_equals),
541 Some(text_hash),
542 // Lexicographic by UTF-8 bytes (ADR-045).
543 Some(text_compare),
544)
545.with_owned_bytes(text_owned_bytes);
546
547/// The heap bytes a `Text` owns beyond its payload.
548///
549/// An `Owned` text is a `Box<str>` whose length is the whole point: charging
550/// pacing only the payload's own bytes for a megabyte of input would make a
551/// text-heavy program invisible to the collector. A `Slice` owns nothing — it
552/// borrows its owner's buffer, and charging its length would count the same
553/// bytes once per slice.
554///
555/// # Safety
556/// `payload` must point at an initialized `TextPayload`.
557unsafe fn text_owned_bytes(payload: *const u8) -> usize {
558 // SAFETY: caller guarantees `payload` points at an initialized TextPayload.
559 match unsafe { &*(payload as *const TextPayload) } {
560 TextPayload::Owned(owned) => owned.as_str().len(),
561 TextPayload::Slice(_) => 0,
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use std::ptr;
569
570 /// `Text` is the one type whose two renderings differ, and this is the pair
571 /// (§11.4, [`FormatStyle`]).
572 ///
573 /// `Display` writes the characters unquoted: it is `out(s)`, `"{s}"` and
574 /// `praxis run`'s result line, and a quote appearing in any of those is a
575 /// change to what programs print. `Debug` is the debugger's, and the empty
576 /// string is the case that motivates it — zero bytes out is a value the
577 /// renderer could only report as unreadable.
578 #[test]
579 fn text_renders_one_way_for_the_program_and_another_for_the_debugger() {
580 let render = |s: &str, style| {
581 let payload = TextPayload::owned(s);
582 let mut buf = String::new();
583 let mut sink = crate::FormatSink::styled(&mut buf, style);
584 // SAFETY: `payload` is an initialized `TextPayload`.
585 unsafe { (TEXT.format)(ptr::addr_of!(payload) as *const u8, &mut sink) };
586 buf
587 };
588 use crate::FormatStyle::{Debug, Display};
589
590 assert_eq!(render("hello", Display), "hello");
591 assert_eq!(render("hello", Debug), "\"hello\"");
592
593 // The empty string: nothing at all, versus something.
594 assert_eq!(render("", Display), "");
595 assert_eq!(render("", Debug), "\"\"");
596
597 // And the escaping, so a value cannot end its own quoting or take a
598 // second row of a display that allots it one.
599 assert_eq!(render("a\"b", Display), "a\"b");
600 assert_eq!(render("a\"b", Debug), r#""a\"b""#);
601 assert_eq!(render("a\nb", Debug), r#""a\nb""#);
602 }
603
604 #[test]
605 fn owned_text_descriptor_formats_and_compares() {
606 let a = TextPayload::owned("hello");
607 let b = TextPayload::owned("hello");
608 let c = TextPayload::owned("world");
609
610 let mut buf = String::new();
611 unsafe {
612 (TEXT.format)(
613 ptr::addr_of!(a) as *const u8,
614 &mut crate::FormatSink::display(&mut buf),
615 )
616 };
617 assert_eq!(buf, "hello");
618
619 assert!(unsafe {
620 (TEXT.equals.unwrap())(ptr::addr_of!(a) as *const u8, ptr::addr_of!(b) as *const u8)
621 });
622 assert!(!unsafe {
623 (TEXT.equals.unwrap())(ptr::addr_of!(a) as *const u8, ptr::addr_of!(c) as *const u8)
624 });
625 }
626
627 /// ADR-045: `Text` orders by its bytes, and the ordering is the same
628 /// whether the text is owned or a zero-copy slice of another — never by the
629 /// payload's first eight bytes, which are a `Box<str>` pointer here and a
630 /// `GcRef` there, i.e. an address.
631 #[test]
632 fn text_compares_lexicographically_whatever_its_representation() {
633 let cmp = TEXT.compare.expect("Text is orderable");
634 let apple = TextPayload::owned("apple");
635 let banana = TextPayload::owned("banana");
636 let apple_again = TextPayload::owned("apple");
637 let at = |p: &TextPayload| ptr::addr_of!(*p) as *const u8;
638
639 assert_eq!(
640 unsafe { cmp(at(&apple), at(&banana)) },
641 std::cmp::Ordering::Less
642 );
643 assert_eq!(
644 unsafe { cmp(at(&banana), at(&apple)) },
645 std::cmp::Ordering::Greater
646 );
647 assert_eq!(
648 unsafe { cmp(at(&apple), at(&apple_again)) },
649 std::cmp::Ordering::Equal,
650 "two separately allocated `apple`s are one value"
651 );
652
653 // A prefix precedes what extends it.
654 let app = TextPayload::owned("app");
655 assert_eq!(
656 unsafe { cmp(at(&app), at(&apple)) },
657 std::cmp::Ordering::Less
658 );
659
660 // UTF-8 byte order is code-point order: "é" (U+00E9) follows "z".
661 let z = TextPayload::owned("z");
662 let e_acute = TextPayload::owned("é");
663 assert_eq!(
664 unsafe { cmp(at(&z), at(&e_acute)) },
665 std::cmp::Ordering::Less
666 );
667 }
668
669 #[test]
670 fn owned_text_hash_is_stable() {
671 let a = TextPayload::owned("hello");
672 let b = TextPayload::owned("hello");
673 let mut ha = crate::descriptor::StructHasher::new();
674 let mut hb = crate::descriptor::StructHasher::new();
675 unsafe {
676 (TEXT.hash.unwrap())(ptr::addr_of!(a) as *const u8, &mut ha);
677 (TEXT.hash.unwrap())(ptr::addr_of!(b) as *const u8, &mut hb);
678 }
679 assert_eq!(ha.finish(), hb.finish());
680 }
681
682 #[test]
683 fn owned_text_bytes_can_be_borrowed_as_a_manual_subslice() {
684 let owner = TextPayload::owned("hello, world");
685 let owner_ptr = ptr::addr_of!(owner);
686 let bytes = unsafe { text_bytes(owner_ptr) };
687 assert_eq!(&bytes[7..12], b"world");
688 let s = unsafe { text_str(owner_ptr) };
689 assert_eq!(s, "hello, world");
690 }
691
692 #[test]
693 fn source_slice_traces_its_owner_during_collection() {
694 let rt = crate::Runtime::new();
695 let owner = rt.alloc_text("hello");
696 // SAFETY: `owner` is the live Text allocated above.
697 let slice = unsafe { rt.alloc_text_slice(owner, 1, 3) }.expect("[1, 4) is in range");
698 let mut roots = crate::RootScope::new();
699 roots.root(slice);
700
701 rt.collect_with(&roots);
702
703 assert_eq!(
704 rt.heap().stats().live_count,
705 2,
706 "the rooted slice and its otherwise-unrooted owner must both survive"
707 );
708 assert_eq!(slice.as_text(), "ell");
709 }
710
711 /// **Reading a `Text` costs no stack, however deep its owner chain is.**
712 ///
713 /// A recursive `text_bytes` would cost a frame per link, and a long enough
714 /// chain would overflow the stack and abort the process — inside
715 /// `extern "C"`, which is the one outcome §10.4 rules out. A chain is not
716 /// illegal, so the depth cannot be bounded by validation; the read simply
717 /// must not be recursive.
718 ///
719 /// The thread's stack is deliberately small: a recursive read at this depth
720 /// overflows it, and no assertion can catch that, so the test's passing
721 /// *is* the assertion. The parser separately refuses to build chains at all
722 /// (`Input::new` resolves to the root owner), which is why this has to be
723 /// built by hand to be tested.
724 #[test]
725 fn reading_a_deep_slice_chain_does_not_recurse() {
726 const DEPTH: usize = 4_000;
727 std::thread::Builder::new()
728 .stack_size(128 * 1024)
729 .spawn(|| {
730 let rt = crate::Runtime::new();
731 let mut text = rt.alloc_text("hello world");
732 let mut roots = crate::RootScope::new();
733 for _ in 0..DEPTH {
734 // Each link is the whole of its owner, so the answer never
735 // changes and only the depth grows.
736 // SAFETY: `text` is the live Text from the previous step.
737 text = unsafe { rt.alloc_text_slice(text, 0, 11) }.expect("the whole owner");
738 roots.root(text);
739 }
740 assert_eq!(text.as_text(), "hello world");
741
742 // And the root resolution the parser relies on is iterative for
743 // the same reason, and lands on the owned text.
744 // SAFETY: `text` is live and its chain is live (all rooted).
745 let (root, base) = unsafe { text_root(text) };
746 assert_eq!(base, 0);
747 // SAFETY: `root` is a live Text.
748 assert!(
749 unsafe { &*(root.payload::<TextPayload>() as *const TextPayload) }.is_owned(),
750 "text_root resolves to the owned text, not to another slice"
751 );
752 })
753 .expect("spawn")
754 .join()
755 .expect("a deep chain must be readable without recursing");
756 }
757
758 /// The range is not a hint. A view past the owner's end, one whose length
759 /// overflows, or one whose ends split a multi-byte scalar is not a `Text`,
760 /// and must be unconstructible rather than clamped or merely
761 /// `debug_assert`'d — a clamped range yields a `Text` that reads as `""` or
762 /// slices out of range in a release build.
763 #[test]
764 fn an_out_of_range_or_non_boundary_slice_is_unconstructible() {
765 let rt = crate::Runtime::new();
766 // "héllo" — 'é' is two bytes, so byte 1 starts it and byte 2 splits it.
767 let owner = rt.alloc_text("héllo");
768 let bytes = owner.as_text().len();
769 assert_eq!(bytes, 6);
770
771 // SAFETY: `owner` is a live Text for every call below.
772 unsafe {
773 assert!(
774 rt.alloc_text_slice(owner, 0, bytes).is_some(),
775 "the whole owner is a valid slice of itself"
776 );
777 assert!(
778 rt.alloc_text_slice(owner, bytes, 0).is_some(),
779 "an empty slice at the end is in range"
780 );
781 assert!(
782 rt.alloc_text_slice(owner, 0, bytes + 1).is_none(),
783 "a slice past the end is not a Text"
784 );
785 assert!(
786 rt.alloc_text_slice(owner, bytes + 1, 0).is_none(),
787 "a start past the end is not a Text"
788 );
789 assert!(
790 rt.alloc_text_slice(owner, 1, usize::MAX).is_none(),
791 "an overflowing length is not a Text"
792 );
793 assert!(
794 rt.alloc_text_slice(owner, 2, 1).is_none(),
795 "a start inside a multi-byte scalar is not a Text"
796 );
797 assert!(
798 rt.alloc_text_slice(owner, 1, 1).is_none(),
799 "an end inside a multi-byte scalar is not a Text"
800 );
801 // The boundaries either side of 'é' are fine.
802 let e = rt
803 .alloc_text_slice(owner, 1, 2)
804 .expect("[1, 3) is a scalar");
805 assert_eq!(e.as_text(), "é");
806 }
807 }
808
809 // ---- ADR-115: the scalar count ---------------------------------------
810
811 /// The payload of a live `Text`, for the count tests below.
812 ///
813 /// # Safety
814 /// `r` must be a live `Text`.
815 unsafe fn payload_of(r: GcRef) -> *const TextPayload {
816 r.payload::<TextPayload>() as *const TextPayload
817 }
818
819 /// **The count is lazy, and that is the decision** (ADR-115). An owned
820 /// `Text` is allocated uncounted; `praxis_get_input`'s buffer is one such
821 /// payload and can be tens of megabytes, so counting at construction would
822 /// charge a full scan to every program that reads its input and never
823 /// indexes a text.
824 ///
825 /// This and the three tests below observe the cache itself, so they
826 /// describe arm B rather than the language. Under the `adr115-arm-a`
827 /// measurement feature there is no cache to observe by construction; the
828 /// tests that state what a `Text` *answers* are not gated, and they are the
829 /// ones that must hold in both arms.
830 #[cfg(not(feature = "adr115-arm-a"))]
831 #[test]
832 fn a_text_is_allocated_uncounted_and_counts_itself_once_when_asked() {
833 let rt = crate::Runtime::new();
834 let text = rt.alloc_text("hello");
835 // SAFETY: `text` is the live Text allocated above.
836 let payload = unsafe { payload_of(text) };
837 // SAFETY: same.
838 let TextPayload::Owned(owned) = (unsafe { &*payload }) else {
839 panic!("a literal is owned")
840 };
841 assert_eq!(
842 owned.char_count.get(),
843 NOT_COUNTED,
844 "nothing has asked for the length yet"
845 );
846
847 // SAFETY: same.
848 assert_eq!(unsafe { text_char_count(payload) }, 5);
849 assert_eq!(
850 owned.char_count.get(),
851 5,
852 "the first ask is what pays for the scan"
853 );
854
855 // And the second ask reads the cell rather than the bytes. Poisoning
856 // the cell is how the read is observed: a recount would answer 5.
857 owned.char_count.set(99);
858 // SAFETY: same.
859 assert_eq!(unsafe { text_char_count(payload) }, 99);
860 }
861
862 /// The count is the whole ASCII test. `char_count == bytes.len()` iff every
863 /// scalar is one byte, so there is no second flag to keep true — and the
864 /// byte-indexing licence follows from the length answer rather than sitting
865 /// beside it.
866 #[cfg(not(feature = "adr115-arm-a"))]
867 #[test]
868 fn the_count_equals_the_byte_length_exactly_when_every_scalar_is_one_byte() {
869 let rt = crate::Runtime::new();
870 for (src, chars, one_byte) in [
871 ("", 0usize, true),
872 ("hello", 5, true),
873 ("héllo", 5, false),
874 ("é", 1, false),
875 ("aéb", 3, false),
876 // A four-byte scalar, and a three-byte one.
877 ("a\u{1F600}b", 3, false),
878 ("\u{20AC}", 1, false),
879 // Every ASCII byte, including the ones a naive `is_ascii` on
880 // `char` boundaries would still accept.
881 ("\u{0}\u{7f}", 2, true),
882 ] {
883 let text = rt.alloc_text(src);
884 // SAFETY: `text` is live.
885 let payload = unsafe { payload_of(text) };
886 // SAFETY: same.
887 assert_eq!(unsafe { text_char_count(payload) }, chars, "{src:?}");
888 assert_eq!(chars == src.len(), one_byte, "{src:?}");
889 // SAFETY: same.
890 assert_eq!(
891 unsafe { text_ascii_bytes(payload) }.is_some(),
892 one_byte,
893 "{src:?} must {} take the byte-index path",
894 if one_byte { "" } else { "not" }
895 );
896 assert_eq!(chars, src.chars().count(), "{src:?}");
897 }
898 }
899
900 /// **A slice takes the licence from its owner, and that is why the
901 /// mechanism works at all** (ADR-115). The `Text`s a program indexes are
902 /// mostly the parser's captures, which are `Slice`s of the input buffer, so
903 /// a count that lived only in the `Owned` variant's spare bytes would do
904 /// nothing for the case the decision exists for.
905 #[cfg(not(feature = "adr115-arm-a"))]
906 #[test]
907 fn a_slice_of_a_one_byte_owner_answers_its_length_from_its_byte_length() {
908 let rt = crate::Runtime::new();
909 let owner = rt.alloc_text("abcdefghij");
910 // SAFETY: `owner` is live.
911 let slice = unsafe { rt.alloc_text_slice(owner, 3, 4) }.expect("[3, 7) is in range");
912 // SAFETY: both are live.
913 let (op, sp) = unsafe { (payload_of(owner), payload_of(slice)) };
914
915 // SAFETY: live.
916 assert_eq!(unsafe { text_char_count(sp) }, 4);
917 // SAFETY: live.
918 assert_eq!(unsafe { text_ascii_bytes(sp) }, Some(&b"defg"[..]));
919
920 // The scan that answered it landed on the **owner**, so every other
921 // view of the same buffer is now free.
922 // SAFETY: live.
923 let TextPayload::Owned(owned) = (unsafe { &*op }) else {
924 panic!("the owner is owned")
925 };
926 assert_eq!(owned.char_count.get(), 10);
927 }
928
929 /// A slice of a multi-byte owner has no cached count of its own — there are
930 /// no bytes left in the payload to put one in — so it counts its own bytes.
931 /// The answer must still be scalars, and the byte-index path must stay
932 /// shut even when the slice's *own* bytes happen to be one-byte, because
933 /// deciding that per slice costs exactly what decoding it costs.
934 #[test]
935 fn a_slice_of_a_multi_byte_owner_still_answers_in_scalars() {
936 let rt = crate::Runtime::new();
937 // "héllo wörld" — 'é' at bytes 1..3 and 'ö' at bytes 8..10.
938 let owner = rt.alloc_text("héllo wörld");
939 assert_eq!(owner.as_text().len(), 13);
940
941 // A view that contains the multi-byte scalar.
942 // SAFETY: `owner` is live.
943 let with = unsafe { rt.alloc_text_slice(owner, 0, 3) }.expect("[0, 3) is 'hé'");
944 assert_eq!(with.as_text(), "hé");
945 // SAFETY: live.
946 assert_eq!(unsafe { text_char_count(payload_of(with)) }, 2);
947 // SAFETY: live.
948 assert!(unsafe { text_ascii_bytes(payload_of(with)) }.is_none());
949
950 // A view that does not — the answer is the same either way, and the
951 // fast path is still refused because the owner is what carries the
952 // licence.
953 // SAFETY: live.
954 let without = unsafe { rt.alloc_text_slice(owner, 3, 4) }.expect("[3, 7) is 'llo '");
955 assert_eq!(without.as_text(), "llo ");
956 // SAFETY: live.
957 assert_eq!(unsafe { text_char_count(payload_of(without)) }, 4);
958 // SAFETY: live.
959 assert!(unsafe { text_ascii_bytes(payload_of(without)) }.is_none());
960
961 // An empty view at a scalar boundary.
962 // SAFETY: live.
963 let empty = unsafe { rt.alloc_text_slice(owner, 3, 0) }.expect("an empty view");
964 // SAFETY: live.
965 assert_eq!(unsafe { text_char_count(payload_of(empty)) }, 0);
966 }
967
968 /// `Text + Text` allocates a fresh owned payload (ADR-085), so the
969 /// concatenation is uncounted and counts *its own* bytes. A count inherited
970 /// from either operand would be a wrong program rather than a slow one.
971 #[test]
972 fn a_concatenation_counts_its_own_bytes_and_not_an_operands() {
973 let rt = crate::Runtime::new();
974 let left = rt.alloc_text("ab");
975 let right = rt.alloc_text("é");
976 // SAFETY: both live.
977 unsafe {
978 assert_eq!(text_char_count(payload_of(left)), 2);
979 assert_eq!(text_char_count(payload_of(right)), 1);
980 }
981
982 let joined = rt.alloc_text(&format!("{}{}", left.as_text(), right.as_text()));
983 // SAFETY: live.
984 assert_eq!(unsafe { text_char_count(payload_of(joined)) }, 3);
985 // SAFETY: live.
986 assert!(
987 unsafe { text_ascii_bytes(payload_of(joined)) }.is_none(),
988 "an ASCII text joined to a multi-byte one is not byte-indexable"
989 );
990 }
991
992 /// A slice taken from an owner that has **already** been counted reads the
993 /// same answer as one taken before. There is nothing to invalidate — `Text`
994 /// is immutable and a view cannot change what it views — but the pair is
995 /// what makes that a tested property rather than an argued one.
996 #[test]
997 fn a_slice_reads_the_same_whether_its_owner_was_counted_before_or_after() {
998 let rt = crate::Runtime::new();
999 let owner = rt.alloc_text("wxyz");
1000 // SAFETY: live.
1001 let early = unsafe { rt.alloc_text_slice(owner, 1, 2) }.expect("[1, 3)");
1002 // SAFETY: live.
1003 assert_eq!(unsafe { text_char_count(payload_of(early)) }, 2);
1004 // SAFETY: live.
1005 let late = unsafe { rt.alloc_text_slice(owner, 1, 2) }.expect("[1, 3)");
1006 // SAFETY: live.
1007 assert_eq!(unsafe { text_char_count(payload_of(late)) }, 2);
1008 assert_eq!(early.as_text(), late.as_text());
1009 }
1010
1011 /// The count and the byte view follow an owner chain **iteratively**, for
1012 /// the reason `reading_a_deep_slice_chain_does_not_recurse` gives: the
1013 /// depth is not bounded by anything, and this thread's stack is small
1014 /// enough that a recursive walk would overflow it. The test passing is the
1015 /// assertion.
1016 #[cfg(not(feature = "adr115-arm-a"))]
1017 #[test]
1018 fn counting_a_deep_slice_chain_does_not_recurse() {
1019 const DEPTH: usize = 4_000;
1020 std::thread::Builder::new()
1021 .stack_size(128 * 1024)
1022 .spawn(|| {
1023 let rt = crate::Runtime::new();
1024 let mut text = rt.alloc_text("hello world");
1025 let mut roots = crate::RootScope::new();
1026 for _ in 0..DEPTH {
1027 // SAFETY: `text` is the live Text from the previous step.
1028 text = unsafe { rt.alloc_text_slice(text, 0, 11) }.expect("the whole owner");
1029 roots.root(text);
1030 }
1031 // SAFETY: `text` and its whole chain are live and rooted.
1032 unsafe {
1033 assert_eq!(text_char_count(payload_of(text)), 11);
1034 assert_eq!(
1035 text_ascii_bytes(payload_of(text)),
1036 Some(&b"hello world"[..])
1037 );
1038 }
1039 })
1040 .expect("spawn")
1041 .join()
1042 .expect("a deep chain must be countable without recursing");
1043 }
1044
1045 /// **Allocating a view is O(1), not O(the owner).** Validating the owner's
1046 /// whole byte range per view would make parsing an n-byte input into k
1047 /// captures O(n·k); since ADR-111 the owner's bytes are UTF-8 by a
1048 /// precondition checked at the one door raw bytes enter, so the boundary
1049 /// test is two byte comparisons.
1050 ///
1051 /// The sizes are chosen so a per-view validation cannot finish this test:
1052 /// 8000 views of a 256 KiB owner is two billion byte validations. There is
1053 /// no assertion to make about that beyond the test returning.
1054 #[test]
1055 fn taking_a_view_does_not_walk_the_owner() {
1056 const OWNER_BYTES: usize = 256 * 1024;
1057 const VIEWS: usize = 8_000;
1058 let rt = crate::Runtime::new();
1059 let owner = rt.alloc_text(&"x".repeat(OWNER_BYTES));
1060 let mut roots = crate::RootScope::new();
1061 roots.root(owner);
1062 for i in 0..VIEWS {
1063 // SAFETY: `owner` is live and rooted for the whole loop.
1064 let view = unsafe { rt.alloc_text_slice(owner, i, 4) }.expect("in range");
1065 roots.root(view);
1066 assert_eq!(view.as_text(), "xxxx");
1067 }
1068 }
1069
1070 #[test]
1071 fn hash_value_helper_compiles() {
1072 let mut h = crate::descriptor::StructHasher::new();
1073 hash_value(&mut h, &"x");
1074 }
1075}