1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! The interned ASCII `Char` range (§4.3, ADR-107).
//!
//! [`crate::small_int`]'s argument, applied to the second scalar that satisfies
//! it. §4.3's uniform object model is normative "even if later optimizations
//! intern small integers, use tagged pointers, or eliminate allocations through
//! escape analysis" — provided such an optimization "preserves reference and
//! aliasing semantics". A `Char` has none to preserve, for the same five reasons
//! an `Int` has none, each re-checked against `Char` rather than inherited:
//!
//! - **There is no identity operator.** `praxis_hir`'s `BinOp` is arithmetic,
//! the six comparisons and the two logical connectives; `UnaryOp` is
//! `Neg`/`Not`. There is no `is`, no `===`, no `ref_eq`.
//! - **`==` on `Char` compares payloads.** `compare_kind` answers
//! `CompareVia::Scalar(ScalarKind::Char)`, which lowers to `Inst::IntCmp` over
//! extracted code points. The structural fallback, `praxis_struct_eq`, has no
//! pointer fast path either — it checks descriptor identity and dispatches
//! `char_equals`.
//! - **Keyed collections are structural.** `Map`, `Set` and `Counter` are keyed
//! by [`DynamicKey`](crate::dynamic_key::DynamicKey), whose `eq` *does* open
//! with a pointer comparison — but that is a fast path **for** structural
//! equality, and `char_equals` is a reflexive `u32 ==`, so sharing an object
//! can only make it fire more often, never change the answer. (This is the leg
//! `Float` fails and why `Float` is not interned: `float_equals` is IEEE, so
//! NaN ≠ NaN.)
//! - **A `Char` payload is never written after allocation.** `Inst::StoreScalar`
//! has no builder site and the backend's arm for it is a documented no-op.
//! - **Nothing hashes an address into anything language-visible.** `impl Hash
//! for GcRef` exists and has no users.
//!
//! **Why the ceiling is 127 and not the BMP.** A `Char` block is
//! `{16 + 4 = 20, align 8}`, which rounds to the 24-byte rung of ADR-103's
//! ladder — the same rung an `Int` takes — so the table costs
//! [`SMALL_CHAR_COUNT`] × 24 = **3 KiB** of permanently resident arena. The BMP
//! is 63,488 scalar values, or ~1.45 MiB, against a change whose purpose is a
//! program's memory ceiling as much as its speed. And 3 KiB buys the whole
//! population: Praxis reads AoC-shaped input, every byte of which a UTF-8 `Text`
//! stores in one byte is exactly a code point ≤ 127, so a grid of `#`/`.`, a
//! line of digits and every letter of an English word are all inside the range.
//! A `Char` above it — `é`, a box-drawing glyph — is what the allocator is for.
//!
//! Widening past `0xD7FF` would also stop being one decision: the surrogate
//! range `0xD800..=0xDFFF` holds no scalar values, so a table over the BMP would
//! carry either a hole its index arithmetic had to know about or 2,048 immortals
//! whose payloads violate `Char`'s own invariant. `0..=127` needs no second rule
//! beside [`index_of`], and `every_interned_code_point_is_a_valid_unicode_scalar`
//! is what makes a future widening fail here rather than in the heap.
//!
//! **There is no `SMALL_CHAR_MIN`,** because 0 is the floor of the payload type
//! and a constant zero only invites a `code - MIN` that cannot be checked.
//!
//! There **is** a [`SMALL_CHAR_STRIDE`]: the character literal of ADR-141
//! lowers to `Inst::ConstGc { GcConst::Char }`, which indexes this table with a
//! compile-time byte offset exactly the way `GcConst::SmallInt` indexes
//! `crate::small_int`'s. The stride is declared here, beside the array it
//! measures, rather than written as an `8` in the backend, for [`index_of`]'s
//! reason: a table and the arithmetic that walks it are one statement or they
//! are two answers.
//!
//! It is not re-exported from the crate root, unlike `small_int`'s constants:
//! the backend reaches it as `praxis_runtime::small_char::SMALL_CHAR_STRIDE`,
//! which names the table it belongs to at the use site.
/// The highest code point the runtime interns: the last ASCII scalar.
///
/// See the module documentation for the range argument. The cost of raising it
/// is [`SMALL_CHAR_COUNT`] × 24 bytes of permanently resident arena (a
/// `GcHeader` is 16 bytes since ADR-109, a `CharPayload` is 4, and the 24-byte
/// rung of ADR-103's ladder is the smallest that holds them), so `0..=127` is
/// 3 KiB and the BMP would be ~1.45 MiB.
pub const SMALL_CHAR_MAX: u32 = 127;
/// How many `Char`s the table holds — the length of
/// [`Immortals::small_chars`](crate::Immortals) and the bound every index
/// derived from [`index_of`] respects.
pub const SMALL_CHAR_COUNT: usize = SMALL_CHAR_MAX as usize + 1;
/// The size of one table element, for the backend's element-offset arithmetic.
///
/// The Cranelift `Inst::ConstGc { GcConst::Char }` lowering indexes the table
/// with a compile-time constant byte offset, so it needs the stride — the same
/// two loads a small `Int` literal costs (ADR-100, ADR-141).
pub const SMALL_CHAR_STRIDE: usize = ;
/// `code`'s index in the interned table, or `None` if `code` is outside the
/// range and the caller must allocate.
///
/// A `const fn` and the *only* in-range test, for [`crate::small_int`]'s reason:
/// a caller that has the index has already proved the value was in range, so
/// "is it interned" and "which slot" cannot drift apart into two decisions.
///
/// The identity map is not an accident worth hiding behind arithmetic — the
/// table is built by `(0..=SMALL_CHAR_MAX)` in that order, and
/// `the_range_is_dense_and_ordered` pins the two together.
pub const