nx_json_v1.nx source
↩ module page · 233 lines · 7313 B
1// json.nx -- pull-style JSON parser (Phase G12, RFC 8259).
2//
3// Research / reference:
4// RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format
5// Crockford 2002 — json.org, the original spec
6//
7// Design: pull-style / event-driven parser. The caller invokes
8// json_next(p) in a loop; each call emits one event:
9//
10// JSON_NULL / JSON_BOOL / JSON_INT / JSON_STRING
11// JSON_ARRAY_BEGIN / JSON_ARRAY_END
12// JSON_OBJECT_BEGIN / JSON_OBJECT_END
13// JSON_KEY (for object member names)
14// JSON_END (end of input)
15// JSON_ERROR (malformed input)
16//
17// Zero heap allocation per event. String values are returned as
18// (start, len) slices into the original input buffer; the caller
19// copies if they need persistence.
20//
21// Scope: RFC 8259 compliant for well-formed input. Skips whitespace
22// per spec, supports \" \\ \/ \b \f \n \r \t escape sequences in
23// strings. Number parsing handles integers; \uXXXX unicode escapes
24// and fractional numbers deferred to a follow-up.
25
26// nx_safety_envelope:
27// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
28// sil_target: SIL1
29// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
30// verdict: NOT_YET_EVALUATED
31
32import "nx_syscalls.nx"
33
34const JSON_END: i64 = 0
35const JSON_NULL: i64 = 1
36const JSON_BOOL: i64 = 2
37const JSON_INT: i64 = 3
38const JSON_STRING: i64 = 4
39const JSON_ARRAY_BEGIN: i64 = 5
40const JSON_ARRAY_END: i64 = 6
41const JSON_OBJECT_BEGIN: i64 = 7
42const JSON_OBJECT_END: i64 = 8
43const JSON_KEY: i64 = 9
44const JSON_ERROR: i64 = 10
45
46struct JsonParser {
47 src: *u8,
48 len: i64,
49 pos: i64,
50
51 // Last event's payload.
52 kind: i64, // JSON_* constant
53 int_val: i64, // for JSON_INT, JSON_BOOL (0/1)
54 str_start: i64, // byte offset of start of string/key
55 str_len: i64, // length in bytes (not including quotes)
56}
57
58// Construct a new parser over `src[0..len]`.
59func json_new(src: *u8, len: i64) -> *JsonParser {
60 let raw: *u8 = sys_mmap(64)
61 let p: *JsonParser = raw as *JsonParser
62 p.src = src
63 p.len = len
64 p.pos = 0
65 p.kind = JSON_END
66 return p
67}
68
69// Skip whitespace per RFC 8259 section 2.
70func json_skip_ws(p: *JsonParser) -> i64 {
71 var pos: i64 = p.pos
72 var ws: i64 = 1
73 while ws == 1 {
74 if pos >= p.len { ws = 0 }
75 if ws == 1 {
76 let c: i64 = p.src[pos]
77 if c == 0x20 { pos = pos + 1 } // space
78 else { if c == 0x09 { pos = pos + 1 } // tab
79 else { if c == 0x0A { pos = pos + 1 } // newline
80 else { if c == 0x0D { pos = pos + 1 } // CR
81 else { ws = 0 } } } }
82 }
83 }
84 p.pos = pos
85 return 0
86}
87
88// Peek current byte without advancing. Returns -1 on EOF.
89func json_peek(p: *JsonParser) -> i64 {
90 if p.pos >= p.len { return -1 }
91 return p.src[p.pos]
92}
93
94// Read a string starting at the opening quote. Sets str_start/str_len,
95// advances pos past the closing quote. Returns 0 on success or
96// JSON_ERROR if malformed. Handles simple escapes; \uXXXX deferred.
97func json_read_string(p: *JsonParser) -> i64 {
98 if p.src[p.pos] != 0x22 { return JSON_ERROR } // must start with '"'
99 p.pos = p.pos + 1
100 p.str_start = p.pos
101 var closed: i64 = 0
102 while closed == 0 {
103 if p.pos >= p.len { return JSON_ERROR }
104 let c: i64 = p.src[p.pos]
105 if c == 0x22 { // closing quote
106 p.str_len = p.pos - p.str_start
107 p.pos = p.pos + 1
108 closed = 1
109 }
110 if closed == 0 {
111 if c == 0x5C { // backslash
112 p.pos = p.pos + 2 // skip escape sequence
113 } else {
114 p.pos = p.pos + 1
115 }
116 }
117 }
118 return 0
119}
120
121// Read an integer. Supports leading minus. No fractional / exponent
122// parsing yet. Sets int_val, advances pos past last digit.
123func json_read_int(p: *JsonParser) -> i64 {
124 var v: i64 = 0
125 var neg: i64 = 0
126 if p.src[p.pos] == 0x2D { // '-'
127 neg = 1
128 p.pos = p.pos + 1
129 }
130 while p.pos < p.len {
131 let c: i64 = p.src[p.pos]
132 if c < 0x30 { p.pos = p.len + 1 } // hard break out of loop
133 if p.pos <= p.len {
134 if c >= 0x30 {
135 if c <= 0x39 {
136 v = v * 10 + (c - 0x30)
137 p.pos = p.pos + 1
138 } else {
139 p.pos = p.len + 1 // break
140 }
141 }
142 }
143 }
144 if p.pos > p.len { p.pos = p.len } // undo over-shoot
145 if neg == 1 { v = 0 - v }
146 p.int_val = v
147 return 0
148}
149
150// Match a fixed keyword (null / true / false). Returns 1 on match
151// and advances pos; 0 otherwise.
152func json_match(p: *JsonParser, word: *u8, len: i64) -> i64 {
153 if p.pos + len > p.len { return 0 }
154 var i: i64 = 0
155 while i < len {
156 if p.src[p.pos + i] != word[i] { return 0 }
157 i = i + 1
158 }
159 p.pos = p.pos + len
160 return 1
161}
162
163// Emit the next event. Returns the event kind and writes any
164// payload fields on the parser.
165func json_next(p: *JsonParser) -> i64 {
166 json_skip_ws(p)
167 if p.pos >= p.len {
168 p.kind = JSON_END
169 return JSON_END
170 }
171 let c: i64 = p.src[p.pos]
172 // Structure tokens.
173 if c == 0x7B { p.pos = p.pos + 1; p.kind = JSON_OBJECT_BEGIN; return JSON_OBJECT_BEGIN }
174 if c == 0x7D { p.pos = p.pos + 1; p.kind = JSON_OBJECT_END; return JSON_OBJECT_END }
175 if c == 0x5B { p.pos = p.pos + 1; p.kind = JSON_ARRAY_BEGIN; return JSON_ARRAY_BEGIN }
176 if c == 0x5D { p.pos = p.pos + 1; p.kind = JSON_ARRAY_END; return JSON_ARRAY_END }
177 // Separators just skipped.
178 if c == 0x2C { p.pos = p.pos + 1; return json_next(p) } // ','
179 if c == 0x3A { p.pos = p.pos + 1; return json_next(p) } // ':'
180 // String.
181 if c == 0x22 {
182 let rc: i64 = json_read_string(p)
183 if rc != 0 { p.kind = JSON_ERROR; return JSON_ERROR }
184 // Distinguish key vs value by peeking next non-ws char: ':' =>
185 // this was a key. Otherwise a string value.
186 let save_pos: i64 = p.pos
187 json_skip_ws(p)
188 let nx: i64 = json_peek(p)
189 p.pos = save_pos
190 if nx == 0x3A {
191 p.kind = JSON_KEY
192 return JSON_KEY
193 }
194 p.kind = JSON_STRING
195 return JSON_STRING
196 }
197 // true / false / null.
198 if c == 0x74 {
199 if json_match(p, "true", 4) == 1 {
200 p.kind = JSON_BOOL
201 p.int_val = 1
202 return JSON_BOOL
203 }
204 }
205 if c == 0x66 {
206 if json_match(p, "false", 5) == 1 {
207 p.kind = JSON_BOOL
208 p.int_val = 0
209 return JSON_BOOL
210 }
211 }
212 if c == 0x6E {
213 if json_match(p, "null", 4) == 1 {
214 p.kind = JSON_NULL
215 return JSON_NULL
216 }
217 }
218 // Number (integer for now).
219 if c == 0x2D {
220 json_read_int(p)
221 p.kind = JSON_INT
222 return JSON_INT
223 }
224 if c >= 0x30 {
225 if c <= 0x39 {
226 json_read_int(p)
227 p.kind = JSON_INT
228 return JSON_INT
229 }
230 }
231 p.kind = JSON_ERROR
232 return JSON_ERROR
233}