code wiki / (root) / nx_json_v1.nx

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}