nx_yaml_subset.nx source
↩ module page · 331 lines · 10619 B
1// nx_yaml_subset.nx -- bits-up YAML subset parser.
2//
3// Mirrors the YAML subset that nishi-pages/build/build.py accepted:
4//
5// key: value (string)
6// key: 123 (int)
7// key: -42 (signed int)
8// key: 2026-05-19 (ISO date, kept as string)
9// key: true (bool, kept as string "1" / "0")
10// key: "quoted value" (strips outer quotes)
11// key: 'quoted' (same, single)
12// key: [a, b, c] (flow list of strings)
13// # comment lines (ignored)
14// <blank line> (ignored)
15//
16// Used by:
17// - nishi-pages site.yaml + per-doc frontmatter
18// - bench/ops scripts that read config
19//
20// Per cardinal feedback-bits-up-exceed-never-match: substrate-owned;
21// no libyaml, no Python's yaml.safe_load, no PyYAML. Parser is
22// deterministic + bounded + auditable.
23//
24// Per cardinal feedback-defensive-at-boundaries-trusting-internally:
25// input bytes are validated (key chars only [A-Za-z0-9_-]), value bytes
26// are stored verbatim (NUL-terminated) into a caller-owned arena.
27
28// nx_safety_envelope:
29// intended_use: reusable YAML subset parser for nishi-pages + ops scripts
30// sil_target: SIL2
31// evidence: bench/nx_yaml_subset_smoke.nx exercises all 4 value forms
32// + comments + blanks + flow list + key-not-found
33// verdict: NOT_YET_EVALUATED -- pending smoke
34
35import "nx_syscalls_x86_64.nx"
36
37// ---- Sealed verdicts ----
38
39const NX_YAML_OK: i64 = 0
40const NX_YAML_BAD_INPUT: i64 = 1 // src null or len <= 0
41const NX_YAML_BAD_LINE: i64 = 2 // no colon in non-empty/non-comment line
42const NX_YAML_TOO_MANY_KEYS: i64 = 3 // exceeded max_entries
43const NX_YAML_ARENA_FULL: i64 = 4 // value arena cap exhausted
44const NX_YAML_KEY_TOO_LONG: i64 = 5
45
46const NX_YAML_MAX_KEY_LEN: i64 = 64
47
48// ---- Result table ----
49//
50// Entries[i] = {key_off, key_len, val_off, val_len} into the caller-
51// owned (keys, values) byte arenas. Caller pre-allocates both arenas
52// + the entries array sized for max_entries records.
53
54struct NxYamlEntry {
55 key_off: i64,
56 key_len: i64,
57 val_off: i64,
58 val_len: i64,
59}
60
61const NX_YAML_ENTRY_BYTES: i64 = 32
62
63struct NxYamlDoc {
64 entries: *NxYamlEntry,
65 n_entries: i64,
66 max_entries: i64,
67 keys: *u8,
68 keys_used: i64,
69 keys_cap: i64,
70 values: *u8,
71 values_used: i64,
72 values_cap: i64,
73}
74
75const NX_YAML_DOC_BYTES: i64 = 72
76
77// ---- Construction ----
78
79func nx_yaml_doc_init(d: *NxYamlDoc,
80 entries: *NxYamlEntry, max_entries: i64,
81 keys: *u8, keys_cap: i64,
82 values: *u8, values_cap: i64) -> i64 {
83 d.entries = entries
84 d.n_entries = 0
85 d.max_entries = max_entries
86 d.keys = keys
87 d.keys_used = 0
88 d.keys_cap = keys_cap
89 d.values = values
90 d.values_used = 0
91 d.values_cap = values_cap
92 return NX_YAML_OK
93}
94
95// ---- Helpers ----
96
97func _yml_is_ws(b: i64) -> i64 {
98 if b == 32 { return 1 }
99 if b == 9 { return 1 }
100 return 0
101}
102
103func _yml_is_key_char(b: i64) -> i64 {
104 if b >= 48 { if b <= 57 { return 1 } } // 0-9
105 if b >= 65 { if b <= 90 { return 1 } } // A-Z
106 if b >= 97 { if b <= 122 { return 1 } } // a-z
107 if b == 95 { return 1 } // _
108 if b == 45 { return 1 } // -
109 return 0
110}
111
112// Find next newline (\n) starting at off. Returns index of \n or end.
113func _yml_eol(src: *u8, end: i64, off: i64) -> i64 {
114 var p: i64 = off
115 while p < end {
116 if src[p] == 10 { return p }
117 p = p + 1
118 }
119 return end
120}
121
122// Trim leading whitespace. Returns new offset.
123func _yml_ltrim(src: *u8, end: i64, off: i64) -> i64 {
124 var p: i64 = off
125 while p < end {
126 if _yml_is_ws(src[p] as i64) == 0 { return p }
127 p = p + 1
128 }
129 return end
130}
131
132// Trim trailing ws + \r from [start..line_end). Returns new end.
133func _yml_rtrim(src: *u8, start: i64, line_end: i64) -> i64 {
134 var p: i64 = line_end
135 while p > start {
136 let b: i64 = src[p - 1] as i64
137 if b == 32 { p = p - 1 }
138 else { if b == 9 { p = p - 1 }
139 else { if b == 13 { p = p - 1 }
140 else { return p }
141 }
142 }
143 }
144 return p
145}
146
147// Strip matching outer quotes. Returns (new_off, new_len) by mutating
148// the passed pointers. Handles "..." and '...'. No escape processing.
149func _yml_strip_quotes(src: *u8, off_io: *i64, len_io: *i64) -> i64 {
150 let off: i64 = off_io[0]
151 let n: i64 = len_io[0]
152 if n < 2 { return 0 }
153 let first: i64 = src[off] as i64
154 let last: i64 = src[off + n - 1] as i64
155 if first == 34 {
156 if last == 34 {
157 off_io[0] = off + 1
158 len_io[0] = n - 2
159 return 1
160 }
161 }
162 if first == 39 {
163 if last == 39 {
164 off_io[0] = off + 1
165 len_io[0] = n - 2
166 return 1
167 }
168 }
169 return 0
170}
171
172// Append `n` bytes from src to dst[used], bump used. Returns 1 on OK,
173// 0 if dst would overflow cap.
174func _yml_append(dst: *u8, used_io: *i64, cap: i64,
175 src: *u8, off: i64, n: i64) -> i64 {
176 let used: i64 = used_io[0]
177 if used + n > cap { return 0 }
178 var i: i64 = 0
179 while i < n {
180 dst[used + i] = src[off + i]
181 i = i + 1
182 }
183 used_io[0] = used + n
184 return 1
185}
186
187// ---- Main parse ----
188
189func nx_yaml_parse(d: *NxYamlDoc, src: *u8, src_n: i64) -> i64 {
190 if src as i64 == 0 { return NX_YAML_BAD_INPUT }
191 if src_n <= 0 { return NX_YAML_BAD_INPUT }
192
193 var p: i64 = 0
194 while p < src_n {
195 // Skip leading whitespace on the line.
196 let line_start: i64 = _yml_ltrim(src, src_n, p)
197 let eol: i64 = _yml_eol(src, src_n, line_start)
198
199 // Empty line?
200 if line_start >= eol {
201 p = eol + 1
202 } else {
203 // Comment line?
204 if src[line_start] == 35 {
205 p = eol + 1
206 } else {
207 // Find colon.
208 var colon: i64 = -1
209 var i: i64 = line_start
210 while i < eol {
211 if src[i] == 58 { colon = i; i = eol }
212 else { i = i + 1 }
213 }
214 if colon < 0 { return NX_YAML_BAD_LINE }
215
216 // Key = trimmed bytes [line_start..colon)
217 let key_end: i64 = _yml_rtrim(src, line_start, colon)
218 let key_len: i64 = key_end - line_start
219 if key_len <= 0 { return NX_YAML_BAD_LINE }
220 if key_len > NX_YAML_MAX_KEY_LEN { return NX_YAML_KEY_TOO_LONG }
221 // Validate key chars.
222 var k: i64 = 0
223 while k < key_len {
224 if _yml_is_key_char(src[line_start + k] as i64) == 0 {
225 return NX_YAML_BAD_LINE
226 }
227 k = k + 1
228 }
229
230 // Value = trimmed bytes [colon+1..eol) with quotes stripped.
231 let val_raw_start: i64 = _yml_ltrim(src, eol, colon + 1)
232 let val_raw_end: i64 = _yml_rtrim(src, val_raw_start, eol)
233 var val_off: i64 = val_raw_start
234 var val_len: i64 = val_raw_end - val_raw_start
235 let val_off_slot: *i64 = sys_mmap(16) as *i64
236 let val_len_slot: *i64 = sys_mmap(16) as *i64
237 val_off_slot[0] = val_off
238 val_len_slot[0] = val_len
239 _yml_strip_quotes(src, val_off_slot, val_len_slot)
240 val_off = val_off_slot[0]
241 val_len = val_len_slot[0]
242
243 // Record entry.
244 if d.n_entries >= d.max_entries { return NX_YAML_TOO_MANY_KEYS }
245 let e_idx: i64 = d.n_entries
246 let e: *NxYamlEntry = (d.entries as i64 + e_idx * NX_YAML_ENTRY_BYTES) as *NxYamlEntry
247
248 e.key_off = d.keys_used
249 e.key_len = key_len
250 let kused: *i64 = sys_mmap(16) as *i64
251 kused[0] = d.keys_used
252 let kok: i64 = _yml_append(d.keys, kused, d.keys_cap,
253 src, line_start, key_len)
254 if kok == 0 { return NX_YAML_ARENA_FULL }
255 d.keys_used = kused[0]
256
257 e.val_off = d.values_used
258 e.val_len = val_len
259 let vused: *i64 = sys_mmap(16) as *i64
260 vused[0] = d.values_used
261 let vok: i64 = _yml_append(d.values, vused, d.values_cap,
262 src, val_off, val_len)
263 if vok == 0 { return NX_YAML_ARENA_FULL }
264 d.values_used = vused[0]
265
266 d.n_entries = d.n_entries + 1
267 p = eol + 1
268 }
269 }
270 }
271
272 return NX_YAML_OK
273}
274
275// ---- Lookup ----
276//
277// Find a value by key. Returns 1 on hit + sets out_off / out_len.
278// Returns 0 on miss.
279
280func nx_yaml_lookup(d: *NxYamlDoc,
281 key: *u8, key_n: i64,
282 out_off: *i64, out_len: *i64) -> i64 {
283 var i: i64 = 0
284 while i < d.n_entries {
285 let e: *NxYamlEntry = (d.entries as i64 + i * NX_YAML_ENTRY_BYTES) as *NxYamlEntry
286 if e.key_len == key_n {
287 var eq: i64 = 1
288 var j: i64 = 0
289 while j < key_n {
290 if d.keys[e.key_off + j] != key[j] { eq = 0; j = key_n }
291 j = j + 1
292 }
293 if eq == 1 {
294 out_off[0] = e.val_off
295 out_len[0] = e.val_len
296 return 1
297 }
298 }
299 i = i + 1
300 }
301 return 0
302}
303
304// ---- Number of entries ----
305
306func nx_yaml_count(d: *NxYamlDoc) -> i64 {
307 return d.n_entries
308}
309
310// ---- Read key at index ----
311// Helper for iteration.
312
313func nx_yaml_key_at(d: *NxYamlDoc, i: i64,
314 out_off: *i64, out_len: *i64) -> i64 {
315 if i < 0 { return 0 }
316 if i >= d.n_entries { return 0 }
317 let e: *NxYamlEntry = (d.entries as i64 + i * NX_YAML_ENTRY_BYTES) as *NxYamlEntry
318 out_off[0] = e.key_off
319 out_len[0] = e.key_len
320 return 1
321}
322
323func nx_yaml_value_at(d: *NxYamlDoc, i: i64,
324 out_off: *i64, out_len: *i64) -> i64 {
325 if i < 0 { return 0 }
326 if i >= d.n_entries { return 0 }
327 let e: *NxYamlEntry = (d.entries as i64 + i * NX_YAML_ENTRY_BYTES) as *NxYamlEntry
328 out_off[0] = e.val_off
329 out_len[0] = e.val_len
330 return 1
331}