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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
/*
* Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* domain.h -- Symbol-resolution domains.
*
* A domain is the dictionary a RAY_SYM vector's indices resolve over.
* Two kinds:
*
* RUNTIME -- an immortal singleton wrapping the global intern table
* (g_sym, src/table/sym.c). Every runtime-created SYM vec
* points at it. retain/release skip it entirely (no atomic
* churn on the vec free hot path).
*
* FILE -- an mmapped symfile base (the STRL format) plus a growable
* in-memory tail of appended symbols, opened through a
* process-wide resolved-path-keyed refcounted cache:
* opening the same resolved path twice yields the SAME
* object — pointer equality is domain identity, always.
* The reverse index (find) is built lazily on first use;
* per-position string atoms are materialized EAGERLY at
* open / append and owned by the domain (borrowed by
* callers, exactly like ray_sym_str), making
* ray_sym_domain_str a lock-free array read.
* ray_sym_domain_new creates a private in-memory instance of
* this dictionary, with no backing path or cache entry.
*
* Position-0 reservation: position 0 of every non-empty FILE domain is
* the canonical empty/null symbol "" (mirrors global id 0). Group kernels
* retain id 0 as a groupable null key. ray_sym_domain_open
* VALIDATES this and refuses (NULL) files that violate it; intern on an
* empty domain seeds "" at position 0 before the first real symbol, so
* newly created symfiles always carry it. Empty vocabularies are fine.
*
* Growth (the flip, Task 7b): ray_sym_domain_intern find-or-appends into
* the shared object — append-only, positions are permanent. Appends
* extend the published atom array by REPLACING it with a grown copy
* (atomic publish; replaced arrays are retired, not freed, so lock-free
* readers holding the old pointer stay valid for the domain's lifetime).
* ray_sym_domain_flush persists base+tail via tmp + atomic rename under
* the symfile's `.lk` flock and verifies the on-disk prefix still
* matches what this object knows (anything else is a loud
* concurrent-writer RAY_ERR_CORRUPT — never a silent remap).
* Note: the spec's hold-the-`.lk`-for-the-writer's-lifetime contract is
* deferred with multi-process writer stress (out of scope this phase);
* the flock is taken per flush.
*
* Lifecycle: FILE domains are refcounted. Every attached SYM column
* holds a ref (taken on attach and in ray_retain_owned_refs, dropped on
* vec free next to the str_pool/index owned-ref handling). The cache
* entry itself is weak: the last release unlinks the entry, munmaps the
* base mapping and frees the lazy structures — a subsequent open builds
* a fresh mapping.
*/
typedef struct ray_sym_domain_s ray_sym_domain_t;
/* Immortal singleton delegating to the global intern table.
* (Also declared in rayforce.h for the inline ray_sym_vec_domain.) */
ray_sym_domain_t* ;
/* Open (or re-use from the cache) the FILE domain for `path` — a symfile
* in the STRL format. Returns NULL on I/O or format errors. The
* returned object carries one reference per open; release with
* ray_sym_domain_release. A cache hit revalidates against the file:
* external append-only growth EXTENDS the shared object in place;
* any other divergence (shrunk / rewritten file, or growth while this
* process holds unflushed appends) returns NULL — loud, never a silent
* remap. */
ray_sym_domain_t* ;
/* Like ray_sym_domain_open, but a missing file yields a fresh EMPTY
* domain (vocabulary written on first flush). The save path's
* open-or-create entry point. */
ray_sym_domain_t* ;
/* A fresh symbol file at `path` that one converter builds alone, the
* strings appended straight into the mapped file (see table/symimp.h):
* intern, intern_batch, count, flush / flush_append (append: header and
* writeback; durable: cut and sync) and release only. Not cached by path.
* NULL where unsupported or on failure; the caller then uses
* ray_sym_domain_open_or_create. */
ray_sym_domain_t* ;
/* Such a domain's interning counts (RAY_CSV_TRACE), as ray_symimp_stats
* gives them (table/symimp.h). False for any other domain. */
;
bool ;
/* Such a domain's import dictionary (for the grouped import,
* table/symgrp.h); NULL for any other domain. */
struct ray_symimp_s* ;
/* New private, refcounted in-memory dictionary. Supports the same concurrent
* append and resolution operations as FILE domains, without a cache entry or
* backing file. Attached vectors keep it alive. flush returns RAY_ERR_DOMAIN. */
ray_sym_domain_t* ;
/* No-ops on the runtime singleton. */
void ;
void ;
/* Borrowed string atom for position `pos` (NULL if out of range).
* RUNTIME: delegates to ray_sym_str. FILE: lazily materialized atom
* owned by the domain (arena-backed, RAY_ATTR_ARENA) — valid for the
* domain's lifetime, do not release. */
ray_t* ;
/* Raw vocabulary access for a FILE domain: the bytes of the entries the
* file holds, read straight from the mapping — no atom, no lock. Pin
* takes an atomically published snapshot of (map, block table, count); an
* entry is found from the checkpoint of its block of RAY_SYM_INDEX_STRIDE
* entries.
*
* LIFETIME: a snapshot stays readable across in-process appends
* (ray_sym_domain_intern, _batch, flush) for as long as its holder keeps
* the domain alive. It does NOT survive a reopen of the same symfile
* after another process grew it (ray_sym_domain_open on a cached path:
* the extend frees the mapping and block table it replaces). Nothing
* counts pins, so this is a rule for callers: never hold a snapshot across
* a call that may reopen (.db.parted.get, splayed reads,
* ray_sym_domain_open), and never reopen from a thread while another runs
* a kernel over the domain. Today every reopen runs on the calling thread
* between kernels.
*
* Returns false for the runtime domain or an empty file; the caller then
* resolves through ray_sym_domain_str as before. Only positions below
* `count` (the file prefix) are readable this way; runtime-appended
* positions keep going through ray_sym_domain_str. ray_sym_domain_raw_str
* returns NULL (and *len 0) for a position out of range. Unpin is a no-op
* kept for symmetry.
*
* A compressed symfile (each record FSST-coded, see domain.c) has `z` set:
* its strings are decoded into a small ring of buffers of the calling
* thread, so a pointer ray_sym_domain_raw_str returns for it stays valid
* only until that thread's RAY_SYM_RAW_RING-th next read of a compressed
* entry. Enough for a comparison of two entries; a caller that keeps
* pointers longer copies the bytes (or reads their lengths with
* ray_sym_domain_raw_len, which decodes nothing). */
typedef struct ray_sym_ixblk_t;
;
typedef struct ray_sym_domain_raw_t;
bool ;
/* Reads the file of a FILE domain whole into the page cache, in parallel,
* when the memory left after it (the RAM limit less the committed heap and
* the files read so far) is at least `headroom`; else leaves it lazy. Once
* per domain. RAY_SYM_PRELOAD=0 never reads, =1 always; RAY_SYM_TRACE
* prints the decision. */
void ;
void ;
const char* ;
/* Entry `pos` (below r->count) of a compressed file decoded into `out`, which
* holds its length (ray_sym_domain_raw_len) and 8 bytes more: a caller that
* keeps the bytes places them itself. */
void ;
static inline const char*
/* The length of entry `pos`'s string (-1 where ray_sym_domain_raw_str would
* give NULL): from its length (2 bytes in the index), without reading the
* record, unless the string is 65535 bytes or longer (the escape) — then
* its record header (4 bytes). For a function of a string's length per
* symbol (strlen per code), so that a vocabulary's lengths are read and
* not its strings. */
static inline int64_t
/* The same for any domain and position (the appended ones from their atom). */
int64_t ;
/* What the record index did for a FILE domain, for tests: the open took
* its blocks from the index file (covering `covered` entries), and whether
* the lengths are read from the index file. */
typedef struct ray_sym_domain_ix_info_t;
bool ;
/* What the record index guarantees, and where it stops (domain.c):
* - An index is taken only for the very file it was written for: the
* inode, the modification and status-change times and the size it
* records match the file mapped. The status-change time cannot be set
* back by a writer, so an in-place rewrite, even with its modification
* time restored, is not taken for the same file. Any other index —
* another file of the name, the file grown or rewritten by a writer
* that left the index as it was, an earlier format — is not used: the
* open walks every record once and writes the index anew. Where the
* platform has no such identity (Windows, WASM) every open walks.
* - The symfile and its index are ours: an index taken is read as
* written, its blocks and the records they name are not checked. */
/* The symfile's record index, `<symfile>.ix` (format in domain.c): written
* by the writers of a symfile so that opening it reads neither the records
* nor their lengths. Save: ckpt[k] is the byte offset of the record of
* entry k * RAY_SYM_INDEX_STRIDE, n = ceil(count / RAY_SYM_INDEX_STRIDE),
* lens[i] the length of entry i's string (65535: 65535 or longer), the first
* `count` records ending at byte `end`; best effort, and a no-op for a small
* file. Remove: before a new file is made under the name. */
void ;
void ;
/* Position of `str` in the domain, or -1 if absent.
* FILE: builds the reverse index on first call (O(|vocabulary|)). */
int64_t ;
/* Position → runtime-intern-id translation table.
*
* RUNTIME: NULL — ids ARE runtime ids, callers use them as-is.
* FILE: `count` entries, lut[pos] = ray_sym_intern(vocab[pos]);
* built ONCE on first request (double-checked under the
* domain lock, atomically published), then lock-free reads.
* Invalidated by growth: an append retires it (the pointer
* stays valid memory), a reopen after external growth frees
* it — same rule as a raw snapshot, see ray_sym_domain_raw_pin.
*
* Building the LUT INTERNS the whole vocabulary into the global table —
* the sanctioned, SEQUENTIAL one-time cost of crossing a FILE domain
* into runtime-id space (O(|vocabulary|), never O(rows)). Contract:
* any path that translates ids inside ray_pool_dispatch workers must
* obtain the LUT during sequential setup (join/window setup do) —
* interning inside a worker violates sym.c's frozen-table rule.
* Returns NULL on OOM (FILE domains with a non-empty vocabulary). */
const int64_t* ;
/* Find-or-append. RUNTIME: delegates to ray_sym_intern. FILE:
* append-only in-memory tail over the mmapped base; an empty domain is
* seeded with "" at position 0 before the first real symbol. Returns
* the position, or -1 on OOM. Appends are visible to every holder of
* the shared object immediately; ray_sym_domain_flush persists them. */
int64_t ;
/* Batch find-or-append of n strings (hashes[i] = (uint32_t)ray_hash_bytes
* of strs[i]); out_pos[i] receives the position. Repeats inside the
* batch resolve to one position. FILE: the lookup of the existing
* vocabulary runs in parallel on the pool; new strings are appended in
* batch order (a string's first occurrence), whatever the worker count. RUNTIME: one ray_sym_intern per entry.
* Returns false on OOM (out_pos then holds -1 for the entries not
* resolved). */
bool ;
;
void ;
/* The same with hashes[i] = ray_hash_bytes of strs[i], all 64 bits: the
* import dictionary keys on them (the 32-bit entry point hashes its strings
* again for it); the other domains take their low 32 bits. */
bool ;
/* Number of entries in the domain. */
int64_t ;
/* Resolved (realpath) symfile path; NULL for the runtime domain. */
const char* ;
/* Write base + dirty tail to the symfile (tmp + atomic rename under the
* `.lk` flock). Verifies the on-disk file still matches this object's
* persisted prefix first — divergence is a hard RAY_ERR_CORRUPT
* ("concurrent writer"), never a silent remap. No-op (RAY_OK) when
* nothing new was interned. RUNTIME: RAY_OK (the global table owns its
* own persistence). */
ray_err_t ;
/* Append the entries interned since the last flush to the symfile in place
* (header count rewritten after the records), instead of rewriting the
* whole file through a tmp + rename. For a symfile one writer builds
* alone, such as a converter's staging domain, flushed as it grows so the
* last flush writes only the tail. Same `.lk` lock as
* ray_sym_domain_flush; the file must still have the persisted size.
* `durable` also syncs the file and its directory, even when nothing new
* was interned. */
ray_err_t ;
/* Rewrite the plain symfile at `path` compressed: each string coded with an
* FSST symbol table trained on a sample of them (domain.c, "STRZ"), the
* positions, lengths and index kept. For a file written whole and read
* after, such as one column's vocabulary: reads of an entry then decode it
* (ray_sym_domain_raw_str), and the file and the memory it takes when read
* shrink. No domain may have the file open (RAY_ERR_DOMAIN). A file with
* no entries, one already compressed or one no table helps stays as it is
* (RAY_OK). */
ray_err_t ;
/* RAY_SYM_AUDIT=1 support (cached at ray_sym_init): when set,
* ray_sym_vec_cell cross-checks every resolution and aborts with full
* context on an invariant violation (unresolvable atom / position out
* of domain range). OFF by default — a single predictable branch. */
extern uint8_t ray_g_sym_audit;
void ;
/* RAY_DOMAIN_H */