code wiki / (root) / json.nx

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}