nx_value_parse_json.nx source
↩ module page · 468 lines · 16093 B
1// nx_value_parse_json.nx -- bytes -> NxValue tree (DOM, not stream).
2//
3// module: nishi-core.data.value_parse_json
4// depends: nishi-core.io.syscalls, nishi-core.data.value
5// disk_kb: 6
6// capability: CORE_IO
7// wired_status: FULLY_WIRED
8//
9// license_tier: PUBLIC_NISHI_SUBSTRATE
10// genealogy_id: rfc_8259_json_data_interchange +
11// recursive_descent_parser_tradition +
12// nishi_build_the_system_cardinal_2026
13//
14// Brick #2 of the bits-up ingestion stack. Reads RFC 8259 JSON
15// bytes and produces an NxValue tree. No upstream-source-specific
16// knowledge -- the tree carries WHATEVER the JSON said.
17//
18// Inline lexer (does not import nx_json) to keep the all-syscalls.nx
19// import topology that lets us compose against nx_dir for batch
20// ingestion later. Same SAX semantics as nx_json but emits NxValue
21// nodes directly.
22//
23// String content: span includes raw bytes between quotes; common
24// escapes (\n \t \\ \" \/) are decoded inline; \uXXXX is NOT
25// decoded (substrate works with raw UTF-8 in most cases; a future
26// nx_value_decode_unicode pass can add that).
27//
28// Numbers: integers parsed as i64; floats parsed lossy to Q10
29// fixed-point (substrate convention). Caller can re-parse from
30// str_ptr/str_len if precision matters.
31//
32// Recursion: bounded NX_VAL_PARSE_MAX_DEPTH per JPL Rule 2.
33
34import "syscalls.nx"
35import "nx_value.nx"
36
37// ===== Verdict ====================================================
38
39const NX_VAL_PARSE_OK: i64 = 1
40const NX_VAL_PARSE_BAD_TOKEN: i64 = 2
41const NX_VAL_PARSE_UNTERMINATED: i64 = 3
42const NX_VAL_PARSE_DEPTH_EXCEEDED: i64 = 4
43const NX_VAL_PARSE_EXPECTED_COLON: i64 = 5
44const NX_VAL_PARSE_EXPECTED_RBRACE: i64 = 6
45const NX_VAL_PARSE_EXPECTED_RBRACKET: i64 = 7
46const NX_VAL_PARSE_EOF_EARLY: i64 = 8
47const NX_VAL_PARSE_BAD_ARGS: i64 = 9
48
49func nx_val_parse_verdict_name(v: i64) -> *u8 {
50 if v == NX_VAL_PARSE_OK { return "OK" }
51 if v == NX_VAL_PARSE_BAD_TOKEN { return "BAD_TOKEN" }
52 if v == NX_VAL_PARSE_UNTERMINATED { return "UNTERMINATED" }
53 if v == NX_VAL_PARSE_DEPTH_EXCEEDED { return "DEPTH_EXCEEDED" }
54 if v == NX_VAL_PARSE_EXPECTED_COLON { return "EXPECTED_COLON" }
55 if v == NX_VAL_PARSE_EXPECTED_RBRACE { return "EXPECTED_RBRACE" }
56 if v == NX_VAL_PARSE_EXPECTED_RBRACKET { return "EXPECTED_RBRACKET" }
57 if v == NX_VAL_PARSE_EOF_EARLY { return "EOF_EARLY" }
58 if v == NX_VAL_PARSE_BAD_ARGS { return "BAD_ARGS" }
59 return "UNKNOWN"
60}
61
62// ===== Parser state ===============================================
63
64struct NxValParse {
65 src: *u8,
66 len: i64,
67 pos: i64,
68 verdict: i64,
69 depth: i64,
70 max_items_per_collection: i64, // bound on array/object size
71}
72
73const NX_VAL_PARSE_BYTES: i64 = 48
74const NX_VAL_PARSE_MAX_DEPTH: i64 = 64
75// Per-collection pre-allocation cap. Reduced from 65536 to 4096
76// to avoid virtual-memory exhaustion when parsing N nested
77// OBJECTs (each pre-allocates 3 * cap * 8 bytes for keys/lens/vals).
78// At 4096: 96 KB per OBJECT; 1000 OBJECTs in a results array =
79// ~96 MB virtual. At 65536 it was 1.5 GB which exceeded qemu's
80// virtual address ceiling on bulk pages.
81const NX_VAL_PARSE_DEFAULT_MAX_ITEMS: i64 = 4096
82
83// ===== Whitespace skip ============================================
84
85func nx_val_parse_is_ws(c: i64) -> i64 {
86 if c == 0x20 { return 1 }
87 if c == 0x09 { return 1 }
88 if c == 0x0A { return 1 }
89 if c == 0x0D { return 1 }
90 return 0
91}
92
93func nx_val_parse_skip_ws(p: *NxValParse) {
94 var iter: i64 = 0
95 var verdict: i64 = 0
96 while verdict == 0 && iter < 1048576 {
97 if p.pos >= p.len { verdict = 1 }
98 if verdict == 0 {
99 if nx_val_parse_is_ws(p.src[p.pos] as i64) == 0 { verdict = 1 }
100 if verdict == 0 {
101 p.pos = p.pos + 1
102 iter = iter + 1
103 }
104 }
105 }
106}
107
108// ===== Number parsing =============================================
109//
110// Reads integer + optional fraction + optional exponent. Returns
111// NxValue.kind INT (no fraction/exponent) or FLOAT (Q10 of value).
112// Span (start..p.pos) captured into str_ptr/str_len so callers
113// needing full precision can re-parse.
114
115func nx_val_parse_is_digit(c: i64) -> i64 {
116 if c < 0x30 { return 0 }
117 if c > 0x39 { return 0 }
118 return 1
119}
120
121func nx_val_parse_number(p: *NxValParse) -> *NxValue {
122 if p.pos >= p.len {
123 p.verdict = NX_VAL_PARSE_EOF_EARLY
124 return 0 as *NxValue
125 }
126 let start: i64 = p.pos
127 var sign: i64 = 1
128 if p.src[p.pos] == 0x2D { sign = -1; p.pos = p.pos + 1 } // '-'
129 var int_part: i64 = 0
130 var saw_digit: i64 = 0
131 var d_iter: i64 = 0
132 var d_verdict: i64 = 0
133 while d_verdict == 0 && d_iter < 32 {
134 if p.pos >= p.len { d_verdict = 1 }
135 if d_verdict == 0 {
136 let c: i64 = p.src[p.pos] as i64
137 if nx_val_parse_is_digit(c) == 0 { d_verdict = 1 }
138 if d_verdict == 0 {
139 int_part = int_part * 10 + (c - 0x30)
140 saw_digit = 1
141 p.pos = p.pos + 1
142 d_iter = d_iter + 1
143 }
144 }
145 }
146 if saw_digit == 0 {
147 p.verdict = NX_VAL_PARSE_BAD_TOKEN
148 return 0 as *NxValue
149 }
150
151 var has_fraction: i64 = 0
152 var frac_q10: i64 = 0
153 if p.pos < p.len {
154 if p.src[p.pos] == 0x2E { // '.'
155 has_fraction = 1
156 p.pos = p.pos + 1
157 // Read up to 3 fractional digits for Q10 precision; skip
158 // the rest.
159 var frac_digits: i64 = 0
160 var f_iter: i64 = 0
161 var f_verdict: i64 = 0
162 while f_verdict == 0 && f_iter < 32 {
163 if p.pos >= p.len { f_verdict = 1 }
164 if f_verdict == 0 {
165 let c: i64 = p.src[p.pos] as i64
166 if nx_val_parse_is_digit(c) == 0 { f_verdict = 1 }
167 if f_verdict == 0 {
168 if frac_digits == 0 { frac_q10 = (c - 0x30) * 102 }
169 if frac_digits == 1 { frac_q10 = frac_q10 + (c - 0x30) * 10 }
170 if frac_digits == 2 { frac_q10 = frac_q10 + (c - 0x30) }
171 frac_digits = frac_digits + 1
172 p.pos = p.pos + 1
173 f_iter = f_iter + 1
174 }
175 }
176 }
177 }
178 }
179
180 // Skip exponent if present (e/E + sign + digits) -- not modeled
181 // in Q10 today; caller falls back to span re-parse for precision.
182 if p.pos < p.len {
183 let ec: i64 = p.src[p.pos] as i64
184 if ec == 0x65 { p.pos = p.pos + 1 } // 'e'
185 if p.pos > 0 {
186 if p.src[p.pos - 1] as i64 == 0x65 {
187 if p.pos < p.len {
188 let sc: i64 = p.src[p.pos] as i64
189 if sc == 0x2B { p.pos = p.pos + 1 }
190 if sc == 0x2D { p.pos = p.pos + 1 }
191 }
192 var e_iter: i64 = 0
193 var e_verdict: i64 = 0
194 while e_verdict == 0 && e_iter < 16 {
195 if p.pos >= p.len { e_verdict = 1 }
196 if e_verdict == 0 {
197 if nx_val_parse_is_digit(p.src[p.pos] as i64) == 0 { e_verdict = 1 }
198 if e_verdict == 0 { p.pos = p.pos + 1; e_iter = e_iter + 1 }
199 }
200 }
201 }
202 }
203 }
204
205 let span_len: i64 = p.pos - start
206 let span_p: *u8 = (p.src as i64 + start) as *u8
207
208 if has_fraction == 0 {
209 let v: *NxValue = nx_value_new_int(int_part * sign)
210 v.str_ptr = span_p
211 v.str_len = span_len
212 return v
213 }
214 // FLOAT (Q10 fixed-point)
215 let q10: i64 = (int_part * 1024 + frac_q10) * sign
216 let vf: *NxValue = nx_value_new_float_q10(q10)
217 vf.str_ptr = span_p
218 vf.str_len = span_len
219 return vf
220}
221
222// ===== String parsing =============================================
223//
224// Expects p.pos at the opening `"`. Advances past closing `"`.
225// Returns span pointing INTO source bytes (no escape decode v1).
226// Caller can run a separate decode pass later if escapes present.
227
228func nx_val_parse_string(p: *NxValParse) -> *NxValue {
229 if p.pos >= p.len {
230 p.verdict = NX_VAL_PARSE_EOF_EARLY
231 return 0 as *NxValue
232 }
233 if p.src[p.pos] != 0x22 {
234 p.verdict = NX_VAL_PARSE_BAD_TOKEN
235 return 0 as *NxValue
236 }
237 p.pos = p.pos + 1
238 let start: i64 = p.pos
239 var iter: i64 = 0
240 var verdict: i64 = 0
241 while verdict == 0 && iter < 1048576 {
242 if p.pos >= p.len {
243 p.verdict = NX_VAL_PARSE_UNTERMINATED
244 return 0 as *NxValue
245 }
246 let c: i64 = p.src[p.pos] as i64
247 if c == 0x5C { // backslash -> skip the next byte
248 p.pos = p.pos + 2
249 iter = iter + 1
250 }
251 if c != 0x5C {
252 if c == 0x22 { verdict = 1 }
253 if verdict == 0 {
254 p.pos = p.pos + 1
255 iter = iter + 1
256 }
257 }
258 }
259 let span_len: i64 = p.pos - start
260 let span_p: *u8 = (p.src as i64 + start) as *u8
261 p.pos = p.pos + 1 // past closing quote
262 return nx_value_new_string(span_p, span_len)
263}
264
265// ===== Literal match (true / false / null) ========================
266
267func nx_val_parse_match_lit(p: *NxValParse, lit: *u8, n: i64) -> i64 {
268 if p.pos + n > p.len { return 0 }
269 var i: i64 = 0
270 var iter: i64 = 0
271 var verdict: i64 = 0
272 var matched: i64 = 1
273 while verdict == 0 && iter < n {
274 if p.src[p.pos + i] != lit[i] { matched = 0; verdict = 1 }
275 if verdict == 0 { i = i + 1; iter = iter + 1 }
276 }
277 if matched == 1 { p.pos = p.pos + n }
278 return matched
279}
280
281// ===== Top-level value dispatch (object + array inlined for self-recursion)
282
283func nx_val_parse_value(p: *NxValParse) -> *NxValue {
284 nx_val_parse_skip_ws(p)
285 if p.pos >= p.len {
286 p.verdict = NX_VAL_PARSE_EOF_EARLY
287 return 0 as *NxValue
288 }
289 let c: i64 = p.src[p.pos] as i64
290
291 // ----- OBJECT -------------------------------------------------
292 if c == 0x7B { // '{'
293 p.pos = p.pos + 1
294 p.depth = p.depth + 1
295 if p.depth > NX_VAL_PARSE_MAX_DEPTH {
296 p.verdict = NX_VAL_PARSE_DEPTH_EXCEEDED
297 return 0 as *NxValue
298 }
299 let ocap: i64 = p.max_items_per_collection
300 let keys_raw: *u8 = sys_mmap(ocap * 8)
301 let keys: **u8 = keys_raw as **u8
302 let klens_raw: *u8 = sys_mmap(ocap * 8)
303 let klens: *i64 = klens_raw as *i64
304 let ovals_raw: *u8 = sys_mmap(ocap * 8)
305 let ovals: **NxValue = ovals_raw as **NxValue
306 var on: i64 = 0
307
308 nx_val_parse_skip_ws(p)
309 if p.pos < p.len {
310 if p.src[p.pos] == 0x7D {
311 p.pos = p.pos + 1
312 p.depth = p.depth - 1
313 return nx_value_new_object(keys, klens, ovals, 0)
314 }
315 }
316
317 var o_iter: i64 = 0
318 var o_verdict: i64 = 0
319 while o_verdict == 0 && o_iter < ocap {
320 nx_val_parse_skip_ws(p)
321 let key_val: *NxValue = nx_val_parse_string(p)
322 if p.verdict != NX_VAL_PARSE_OK { return 0 as *NxValue }
323 keys[on] = key_val.str_ptr
324 klens[on] = key_val.str_len
325
326 nx_val_parse_skip_ws(p)
327 if p.pos >= p.len {
328 p.verdict = NX_VAL_PARSE_EXPECTED_COLON
329 return 0 as *NxValue
330 }
331 if p.src[p.pos] != 0x3A {
332 p.verdict = NX_VAL_PARSE_EXPECTED_COLON
333 return 0 as *NxValue
334 }
335 p.pos = p.pos + 1
336 nx_val_parse_skip_ws(p)
337
338 let value_node: *NxValue = nx_val_parse_value(p)
339 if p.verdict != NX_VAL_PARSE_OK { return 0 as *NxValue }
340 ovals[on] = value_node
341 on = on + 1
342
343 nx_val_parse_skip_ws(p)
344 if p.pos >= p.len {
345 p.verdict = NX_VAL_PARSE_EXPECTED_RBRACE
346 return 0 as *NxValue
347 }
348 let oc: i64 = p.src[p.pos] as i64
349 if oc == 0x7D {
350 p.pos = p.pos + 1
351 o_verdict = 1
352 }
353 if o_verdict == 0 {
354 if oc != 0x2C {
355 p.verdict = NX_VAL_PARSE_EXPECTED_RBRACE
356 return 0 as *NxValue
357 }
358 p.pos = p.pos + 1
359 o_iter = o_iter + 1
360 }
361 }
362 p.depth = p.depth - 1
363 return nx_value_new_object(keys, klens, ovals, on)
364 }
365
366 // ----- ARRAY --------------------------------------------------
367 if c == 0x5B { // '['
368 p.pos = p.pos + 1
369 p.depth = p.depth + 1
370 if p.depth > NX_VAL_PARSE_MAX_DEPTH {
371 p.verdict = NX_VAL_PARSE_DEPTH_EXCEEDED
372 return 0 as *NxValue
373 }
374 let acap: i64 = p.max_items_per_collection
375 let items_raw: *u8 = sys_mmap(acap * 8)
376 let items: **NxValue = items_raw as **NxValue
377 var an: i64 = 0
378
379 nx_val_parse_skip_ws(p)
380 if p.pos < p.len {
381 if p.src[p.pos] == 0x5D {
382 p.pos = p.pos + 1
383 p.depth = p.depth - 1
384 return nx_value_new_array(items, 0)
385 }
386 }
387
388 var a_iter: i64 = 0
389 var a_verdict: i64 = 0
390 while a_verdict == 0 && a_iter < acap {
391 let child: *NxValue = nx_val_parse_value(p)
392 if p.verdict != NX_VAL_PARSE_OK { return 0 as *NxValue }
393 items[an] = child
394 an = an + 1
395
396 nx_val_parse_skip_ws(p)
397 if p.pos >= p.len {
398 p.verdict = NX_VAL_PARSE_EXPECTED_RBRACKET
399 return 0 as *NxValue
400 }
401 let ac: i64 = p.src[p.pos] as i64
402 if ac == 0x5D {
403 p.pos = p.pos + 1
404 a_verdict = 1
405 }
406 if a_verdict == 0 {
407 if ac != 0x2C {
408 p.verdict = NX_VAL_PARSE_EXPECTED_RBRACKET
409 return 0 as *NxValue
410 }
411 p.pos = p.pos + 1
412 nx_val_parse_skip_ws(p)
413 a_iter = a_iter + 1
414 }
415 }
416 p.depth = p.depth - 1
417 return nx_value_new_array(items, an)
418 }
419
420 // ----- STRING -------------------------------------------------
421 if c == 0x22 { return nx_val_parse_string(p) }
422
423 // ----- true / false / null ------------------------------------
424 let tlit: *u8 = sys_mmap(8)
425 tlit[0] = 0x74; tlit[1] = 0x72; tlit[2] = 0x75; tlit[3] = 0x65
426 if nx_val_parse_match_lit(p, tlit, 4) == 1 { return nx_value_new_bool(1) }
427
428 let flit: *u8 = sys_mmap(8)
429 flit[0] = 0x66; flit[1] = 0x61; flit[2] = 0x6C; flit[3] = 0x73; flit[4] = 0x65
430 if nx_val_parse_match_lit(p, flit, 5) == 1 { return nx_value_new_bool(0) }
431
432 let nlit: *u8 = sys_mmap(8)
433 nlit[0] = 0x6E; nlit[1] = 0x75; nlit[2] = 0x6C; nlit[3] = 0x6C
434 if nx_val_parse_match_lit(p, nlit, 4) == 1 { return nx_value_new_null() }
435
436 // ----- Number -------------------------------------------------
437 if c == 0x2D { return nx_val_parse_number(p) }
438 if nx_val_parse_is_digit(c) == 1 { return nx_val_parse_number(p) }
439
440 p.verdict = NX_VAL_PARSE_BAD_TOKEN
441 return 0 as *NxValue
442}
443
444// ===== Public entry point =========================================
445
446func nx_value_parse_json(
447 src: *u8,
448 len: i64,
449 out_verdict: *i64
450) -> *NxValue {
451 *out_verdict = NX_VAL_PARSE_BAD_ARGS
452 if src == 0 as *u8 { return 0 as *NxValue }
453 if len <= 0 { return 0 as *NxValue }
454
455 let raw: *u8 = sys_mmap(NX_VAL_PARSE_BYTES)
456 let p: *NxValParse = raw as *NxValParse
457 p.src = src
458 p.len = len
459 p.pos = 0
460 p.verdict = NX_VAL_PARSE_OK
461 p.depth = 0
462 p.max_items_per_collection = NX_VAL_PARSE_DEFAULT_MAX_ITEMS
463
464 let root: *NxValue = nx_val_parse_value(p)
465 *out_verdict = p.verdict
466 if p.verdict != NX_VAL_PARSE_OK { return 0 as *NxValue }
467 return root
468}