json.nx source
↩ module page · 257 lines · 8516 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
26import "syscalls.nx"
27
28const JSON_END: i64 = 0
29const JSON_NULL: i64 = 1
30const JSON_BOOL: i64 = 2
31const JSON_INT: i64 = 3
32const JSON_STRING: i64 = 4
33const JSON_ARRAY_BEGIN: i64 = 5
34const JSON_ARRAY_END: i64 = 6
35const JSON_OBJECT_BEGIN: i64 = 7
36const JSON_OBJECT_END: i64 = 8
37const JSON_KEY: i64 = 9
38const JSON_ERROR: i64 = 10
39
40struct JsonParser {
41 src: *u8,
42 len: i64,
43 pos: i64,
44
45 // Last event's payload.
46 kind: i64, // JSON_* constant
47 int_val: i64, // for JSON_INT, JSON_BOOL (0/1)
48 str_start: i64, // byte offset of start of string/key
49 str_len: i64, // length in bytes (not including quotes)
50}
51
52// Construct a new parser over `src[0..len]`.
53func json_new(src: *u8, len: i64) -> *JsonParser {
54 let raw: *u8 = sys_mmap(64)
55 let p: *JsonParser = raw as *JsonParser
56 p.src = src
57 p.len = len
58 p.pos = 0
59 p.kind = JSON_END
60 return p
61}
62
63// Skip whitespace per RFC 8259 section 2.
64func json_skip_ws(p: *JsonParser) -> i64 {
65 var pos: i64 = p.pos
66 var ws: i64 = 1
67 while ws == 1 {
68 if pos >= p.len { ws = 0 }
69 if ws == 1 {
70 let c: i64 = p.src[pos]
71 if c == 0x20 { pos = pos + 1 } // space
72 else { if c == 0x09 { pos = pos + 1 } // tab
73 else { if c == 0x0A { pos = pos + 1 } // newline
74 else { if c == 0x0D { pos = pos + 1 } // CR
75 else { ws = 0 } } } }
76 }
77 }
78 p.pos = pos
79 return 0
80}
81
82// Peek current byte without advancing. Returns -1 on EOF.
83func json_peek(p: *JsonParser) -> i64 {
84 if p.pos >= p.len { return -1 }
85 return p.src[p.pos]
86}
87
88// Read a string starting at the opening quote. Sets str_start/str_len,
89// advances pos past the closing quote. Returns 0 on success or
90// JSON_ERROR if malformed. Handles simple escapes; \uXXXX deferred.
91func json_read_string(p: *JsonParser) -> i64 {
92 if p.src[p.pos] != 0x22 { return JSON_ERROR } // must start with '"'
93 p.pos = p.pos + 1
94 p.str_start = p.pos
95 var closed: i64 = 0
96 while closed == 0 {
97 if p.pos >= p.len { return JSON_ERROR }
98 let c: i64 = p.src[p.pos]
99 if c == 0x22 { // closing quote
100 p.str_len = p.pos - p.str_start
101 p.pos = p.pos + 1
102 closed = 1
103 }
104 if closed == 0 {
105 if c == 0x5C { // backslash
106 p.pos = p.pos + 2 // skip escape sequence
107 } else {
108 p.pos = p.pos + 1
109 }
110 }
111 }
112 return 0
113}
114
115// Read an integer. Supports leading minus. No fractional / exponent
116// parsing yet. Sets int_val, advances pos past last digit.
117func json_read_int(p: *JsonParser) -> i64 {
118 var v: i64 = 0
119 var neg: i64 = 0
120 if p.src[p.pos] == 0x2D { // '-'
121 neg = 1
122 p.pos = p.pos + 1
123 }
124 var go: i64 = 1 // FIX: leave pos AT the terminator (was jumping to len = died after 1 number)
125 while go == 1 {
126 if p.pos >= p.len { go = 0 }
127 if go == 1 {
128 let c: i64 = p.src[p.pos]
129 if c >= 0x30 {
130 if c <= 0x39 { v = v * 10 + (c - 0x30); p.pos = p.pos + 1 }
131 else { go = 0 }
132 } else { go = 0 }
133 }
134 }
135 if neg == 1 { v = 0 - v }
136 p.int_val = v
137 return 0
138}
139
140// skip a run of digits at pos
141func json_skip_digits(p: *JsonParser) -> i64 {
142 var go: i64 = 1
143 while go == 1 {
144 if p.pos >= p.len { go = 0 }
145 if go == 1 {
146 let c: i64 = p.src[p.pos]
147 if c >= 0x30 { if c <= 0x39 { p.pos = p.pos + 1 } else { go = 0 } } else { go = 0 }
148 }
149 }
150 return 0
151}
152
153// FIX (RFC 8259 numbers): skip a fractional ('.' digits) + exponent ([eE][+-]?digits) tail so
154// floats (e.g. cost_usd:0.001) advance the parser instead of halting at JSON_ERROR. int_val keeps
155// the integer part (JSON_INT semantics); the float just doesn't break the walk.
156func json_skip_frac(p: *JsonParser) -> i64 {
157 if p.pos < p.len { if p.src[p.pos] == 0x2E { p.pos = p.pos + 1; json_skip_digits(p) } }
158 if p.pos < p.len {
159 let e: i64 = p.src[p.pos]
160 var is_e: i64 = 0
161 if e == 0x65 { is_e = 1 }
162 if e == 0x45 { is_e = 1 }
163 if is_e == 1 {
164 p.pos = p.pos + 1
165 if p.pos < p.len { let s: i64 = p.src[p.pos]; if s == 0x2B { p.pos = p.pos + 1 } if s == 0x2D { p.pos = p.pos + 1 } }
166 json_skip_digits(p)
167 }
168 }
169 return 0
170}
171
172// Match a fixed keyword (null / true / false). Returns 1 on match
173// and advances pos; 0 otherwise.
174func json_match(p: *JsonParser, word: *u8, len: i64) -> i64 {
175 if p.pos + len > p.len { return 0 }
176 var i: i64 = 0
177 while i < len {
178 if p.src[p.pos + i] != word[i] { return 0 }
179 i = i + 1
180 }
181 p.pos = p.pos + len
182 return 1
183}
184
185// Emit the next event. Returns the event kind and writes any
186// payload fields on the parser.
187func json_next(p: *JsonParser) -> i64 {
188 json_skip_ws(p)
189 if p.pos >= p.len {
190 p.kind = JSON_END
191 return JSON_END
192 }
193 let c: i64 = p.src[p.pos]
194 // Structure tokens.
195 if c == 0x7B { p.pos = p.pos + 1; p.kind = JSON_OBJECT_BEGIN; return JSON_OBJECT_BEGIN }
196 if c == 0x7D { p.pos = p.pos + 1; p.kind = JSON_OBJECT_END; return JSON_OBJECT_END }
197 if c == 0x5B { p.pos = p.pos + 1; p.kind = JSON_ARRAY_BEGIN; return JSON_ARRAY_BEGIN }
198 if c == 0x5D { p.pos = p.pos + 1; p.kind = JSON_ARRAY_END; return JSON_ARRAY_END }
199 // Separators just skipped.
200 if c == 0x2C { p.pos = p.pos + 1; return json_next(p) } // ','
201 if c == 0x3A { p.pos = p.pos + 1; return json_next(p) } // ':'
202 // String.
203 if c == 0x22 {
204 let rc: i64 = json_read_string(p)
205 if rc != 0 { p.kind = JSON_ERROR; return JSON_ERROR }
206 // Distinguish key vs value by peeking next non-ws char: ':' =>
207 // this was a key. Otherwise a string value.
208 let save_pos: i64 = p.pos
209 json_skip_ws(p)
210 let nx: i64 = json_peek(p)
211 p.pos = save_pos
212 if nx == 0x3A {
213 p.kind = JSON_KEY
214 return JSON_KEY
215 }
216 p.kind = JSON_STRING
217 return JSON_STRING
218 }
219 // true / false / null.
220 if c == 0x74 {
221 if json_match(p, "true", 4) == 1 {
222 p.kind = JSON_BOOL
223 p.int_val = 1
224 return JSON_BOOL
225 }
226 }
227 if c == 0x66 {
228 if json_match(p, "false", 5) == 1 {
229 p.kind = JSON_BOOL
230 p.int_val = 0
231 return JSON_BOOL
232 }
233 }
234 if c == 0x6E {
235 if json_match(p, "null", 4) == 1 {
236 p.kind = JSON_NULL
237 return JSON_NULL
238 }
239 }
240 // Number (integer part kept; fractional/exponent skipped so floats don't halt the walk).
241 if c == 0x2D {
242 json_read_int(p)
243 json_skip_frac(p)
244 p.kind = JSON_INT
245 return JSON_INT
246 }
247 if c >= 0x30 {
248 if c <= 0x39 {
249 json_read_int(p)
250 json_skip_frac(p)
251 p.kind = JSON_INT
252 return JSON_INT
253 }
254 }
255 p.kind = JSON_ERROR
256 return JSON_ERROR
257}