praxis_runtime/input.rs
1//! The process input buffer, read on **first use** (§7.1, §7.10).
2//!
3//! §7.10 is precise about when the read happens: "The first `read` lazily reads
4//! standard input once into an immutable GC-managed source buffer; later `read`
5//! expressions reuse it."
6//!
7//! This module is what expresses the laziness. The host installs a *reader* —
8//! it is not called — and [`praxis_get_input`](crate::abi::praxis_get_input),
9//! which is what a `read` lowers to first, calls it the one time. A program
10//! that never evaluates a `read` never touches the host's input at all, so a
11//! `read`-free program does not block against an open pipe (REP-51).
12//!
13//! **The reader is infallible by construction**, and deliberately so: what an
14//! unreadable stdin *means* is the host's question, not the runtime's. The CLI
15//! reports its own I/O failure the way it reports every other one. The runtime
16//! is left with bytes, and the only judgement it makes about them is §4.3's:
17//! text that is not UTF-8 is a fault. That judgement is made by
18//! [`praxis_get_input`](crate::abi::praxis_get_input) itself rather than by
19//! `praxis_alloc_text` (ADR-111): this is the one path in the runtime carrying
20//! bytes the compiler did not produce, so it is the one place the check
21//! belongs, and keeping it here is what leaves a `Text` *literal*'s allocation
22//! genuinely non-faulting. The reader's contract is bytes, infallibly.
23//!
24//! `praxis run` never reaches that fault, and it is worth knowing which caller
25//! can. `lazy_stdin::read` (`praxis-cli/src/run.rs`) goes through
26//! `std::io::read_to_string`, which refuses non-UTF-8 stdin and exits 2 before
27//! the runtime sees a byte. `InvalidText` is therefore reachable only from an
28//! embedder that installs an [`InputReader`] answering bytes of its own.
29//!
30//! The slot is thread-local because the runtime is single-threaded (§12.1) and
31//! because a `static mut` would be worse; there is one program per process, so
32//! there is one reader per process. A host that installs none — every JIT test,
33//! every embedder — costs nothing: `praxis_get_input` finds nothing to call and
34//! answers whatever `input_source` already holds.
35
36use std::cell::Cell;
37
38/// A host's process-input reader: the UTF-8 bytes of the input buffer.
39///
40/// Called at most once, from the first `read` a program evaluates. Its whole
41/// obligation is bytes — however many the host has, including none. What becomes
42/// of them, and why a zero-byte answer is still an input buffer, is stated once
43/// at [`praxis_get_input`](crate::abi::praxis_get_input) (ADR-087).
44///
45/// A plain `fn` and not a closure: it is stored across the ABI boundary and
46/// called from generated code's stack, so it carries no captured state and no
47/// lifetime. A host that needs state puts it in its own thread-local, which is
48/// what `praxis-cli` does.
49pub type InputReader = fn() -> Vec<u8>;
50
51thread_local! {
52 static READER: Cell<Option<InputReader>> = const { Cell::new(None) };
53}
54
55/// Install the process-input reader. The host calls this **instead of** reading
56/// its input up front; nothing here reads anything.
57pub fn install_input_reader(reader: InputReader) {
58 READER.with(|slot| slot.set(Some(reader)));
59}
60
61/// Forget any installed reader, so the next `read` finds the buffer the host
62/// installed directly rather than calling back.
63///
64/// The crash debugger's re-run path (§9.7) needs this: it re-installs the
65/// *same* input on each restart to keep re-runs identical, and a reader that
66/// fired again would read a stdin that is now at EOF.
67pub fn clear_input_reader() {
68 READER.with(|slot| slot.set(None));
69}
70
71/// Take the installed reader, leaving none. Taking rather than borrowing is
72/// what makes "once" structural: there is no second call to make.
73pub(crate) fn take_input_reader() -> Option<InputReader> {
74 READER.with(Cell::take)
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 fn nothing() -> Vec<u8> {
82 Vec::new()
83 }
84
85 /// The reader is taken, not borrowed, so a second `read` cannot re-read.
86 #[test]
87 fn a_reader_is_taken_once_and_then_gone() {
88 clear_input_reader();
89 assert!(take_input_reader().is_none());
90 install_input_reader(nothing);
91 assert!(take_input_reader().is_some());
92 assert!(take_input_reader().is_none());
93 }
94
95 /// Clearing an installed reader is what the debugger's re-run path does.
96 #[test]
97 fn clearing_disarms_an_installed_reader() {
98 install_input_reader(nothing);
99 clear_input_reader();
100 assert!(take_input_reader().is_none());
101 }
102}