code wiki / (root) / json_emit.nx

json_emit.nx source

↩ module page · 273 lines · 9701 B

1// json_emit.nx -- RFC 8259 JSON writer (complement to json.nx parser). 2// 3// Used by: TLS session tickets, API responses, config serialisation, 4// structured logs, Nishi Pages content negotiation (Accept: json). 5// 6// Shape: 7// - Caller allocates an output buffer; we append into it. 8// - A JsonWriter struct tracks position + depth + "needs comma" 9// state so the caller doesn't manage separators manually. 10// - Begin/end array/object and emit_* primitives for scalars. 11// - String escaping per RFC 8259 §7 (\\, \", \/, \b, \f, \n, 12// \r, \t, \uXXXX for < 0x20 + > 0x7E). Handles UTF-8 13// passthrough for BMP + astral. 14// 15// Invariants: 16// JE1 Output is valid JSON for any sequence of begin/end + 17// emit calls that matches the grammar. Callers can 18// misuse (nest mismatched) and get garbage, but we never 19// emit syntax errors in-pattern. 20// JE2 String emission escapes ALL required characters per 21// RFC 8259 §7; no silent acceptance of control bytes 22// that would produce ambiguous output. 23// JE3 Buffer overruns return a negative error; no silent 24// truncation (malformed JSON tail is worse than a 25// visible error). 26// JE4 Emit is single-pass, no back-patching; total size can 27// be computed up-front from the structure if needed. 28 29import "syscalls.nx" 30 31const JE_ERR_OVERFLOW: i64 = -1 32const JE_MAX_DEPTH: i64 = 64 33 34// Writer state -- caller allocates + initialises via json_writer_init. 35struct JsonWriter { 36 buf: *u8, 37 pos: i64, 38 cap: i64, 39 depth: i64, 40 // Per-depth: "have we emitted an item at this level yet?" 41 // 0 = first item (no leading comma), 1 = subsequent (emit comma). 42 // Indexed [0..depth]. depth 0 is the top-level value. 43 prior: *u8, 44} 45 46func json_writer_init(w: *JsonWriter, buf: *u8, cap: i64) -> i64 { 47 let prior_raw: *u8 = sys_mmap(JE_MAX_DEPTH + 16) 48 var i: i64 = 0 49 while i < JE_MAX_DEPTH { prior_raw[i] = 0; i = i + 1 } 50 w.buf = buf 51 w.pos = 0 52 w.cap = cap 53 w.depth = 0 54 w.prior = prior_raw 55 return 0 56} 57 58// Raw byte append (bounds-checked). 59func je_put(w: *JsonWriter, b: i64) -> i64 { 60 if w.pos >= w.cap { return JE_ERR_OVERFLOW } 61 w.buf[w.pos] = b 62 w.pos = w.pos + 1 63 return 0 64} 65 66// Emit separator before a new item at the current depth. If 67// prior[depth] == 1, prepend a comma. Always sets prior[depth] = 1 68// on return. 69func je_sep(w: *JsonWriter) -> i64 { 70 let d: i64 = w.depth 71 if w.prior[d] == 1 { 72 let rc: i64 = je_put(w, 0x2C) // ',' 73 if rc < 0 { return rc } 74 } 75 w.prior[d] = 1 76 return 0 77} 78 79// Begin an object. Emits `{` at the current value position. 80func json_begin_object(w: *JsonWriter) -> i64 { 81 let rc0: i64 = je_sep(w) 82 if rc0 < 0 { return rc0 } 83 let rc1: i64 = je_put(w, 0x7B) // '{' 84 if rc1 < 0 { return rc1 } 85 if w.depth + 1 >= JE_MAX_DEPTH { return JE_ERR_OVERFLOW } 86 w.depth = w.depth + 1 87 w.prior[w.depth] = 0 88 return 0 89} 90 91// End an object. `}` + depth--. 92func json_end_object(w: *JsonWriter) -> i64 { 93 let rc: i64 = je_put(w, 0x7D) // '}' 94 if rc < 0 { return rc } 95 w.depth = w.depth - 1 96 return 0 97} 98 99func json_begin_array(w: *JsonWriter) -> i64 { 100 let rc0: i64 = je_sep(w) 101 if rc0 < 0 { return rc0 } 102 let rc1: i64 = je_put(w, 0x5B) // '[' 103 if rc1 < 0 { return rc1 } 104 if w.depth + 1 >= JE_MAX_DEPTH { return JE_ERR_OVERFLOW } 105 w.depth = w.depth + 1 106 w.prior[w.depth] = 0 107 return 0 108} 109 110func json_end_array(w: *JsonWriter) -> i64 { 111 let rc: i64 = je_put(w, 0x5D) // ']' 112 if rc < 0 { return rc } 113 w.depth = w.depth - 1 114 return 0 115} 116 117// String emission with RFC 8259 §7 escaping. 118func je_emit_string_raw(w: *JsonWriter, s: *u8, n: i64) -> i64 { 119 let rc0: i64 = je_put(w, 0x22) // '"' 120 if rc0 < 0 { return rc0 } 121 var i: i64 = 0 122 while i < n { 123 let b: i64 = s[i] 124 if b == 0x22 { 125 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 } 126 let r2: i64 = je_put(w, 0x22); if r2 < 0 { return r2 } 127 } else { 128 if b == 0x5C { 129 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 } 130 let r2: i64 = je_put(w, 0x5C); if r2 < 0 { return r2 } 131 } else { 132 if b == 0x0A { 133 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 } 134 let r2: i64 = je_put(w, 0x6E); if r2 < 0 { return r2 } 135 } else { 136 if b == 0x0D { 137 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 } 138 let r2: i64 = je_put(w, 0x72); if r2 < 0 { return r2 } 139 } else { 140 if b == 0x09 { 141 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 } 142 let r2: i64 = je_put(w, 0x74); if r2 < 0 { return r2 } 143 } else { 144 if b < 0x20 { 145 // \uXXXX for control chars we don't 146 // have a short escape for. 147 let r1: i64 = je_put(w, 0x5C); if r1 < 0 { return r1 } 148 let r2: i64 = je_put(w, 0x75); if r2 < 0 { return r2 } 149 let r3: i64 = je_put(w, 0x30); if r3 < 0 { return r3 } 150 let r4: i64 = je_put(w, 0x30); if r4 < 0 { return r4 } 151 let hi: i64 = (b >> 4) & 0xF 152 let lo: i64 = b & 0xF 153 var hc: i64 = 0x30 + hi 154 if hi > 9 { hc = 0x61 + hi - 10 } 155 var lc: i64 = 0x30 + lo 156 if lo > 9 { lc = 0x61 + lo - 10 } 157 let r5: i64 = je_put(w, hc); if r5 < 0 { return r5 } 158 let r6: i64 = je_put(w, lc); if r6 < 0 { return r6 } 159 } else { 160 // Passthrough for printable ASCII 161 // and UTF-8 multibyte (>= 0x80). 162 let r: i64 = je_put(w, b); if r < 0 { return r } 163 } 164 } 165 } 166 } 167 } 168 } 169 i = i + 1 170 } 171 return je_put(w, 0x22) // closing '"' 172} 173 174// Emit a string as the next value (with separator). 175func json_emit_string(w: *JsonWriter, s: *u8, n: i64) -> i64 { 176 let rc: i64 = je_sep(w) 177 if rc < 0 { return rc } 178 return je_emit_string_raw(w, s, n) 179} 180 181// Emit an object key. Contract: must be followed by a value emit 182// before any further key or end_object. We don't set prior[depth] 183// because the key-value pair emits the separator via the value's 184// own je_sep -- this key call emits the key, ':', and leaves 185// prior unchanged (the VALUE sets prior). 186func json_emit_key(w: *JsonWriter, name: *u8, n: i64) -> i64 { 187 let rc0: i64 = je_sep(w) 188 if rc0 < 0 { return rc0 } 189 let rc1: i64 = je_emit_string_raw(w, name, n) 190 if rc1 < 0 { return rc1 } 191 let rc2: i64 = je_put(w, 0x3A) // ':' 192 if rc2 < 0 { return rc2 } 193 // The next value emitted should NOT prepend a comma; the key 194 // has already set prior[depth] = 1 via its own sep call. But 195 // we want the value itself to NOT emit a leading comma. We 196 // cheat: set prior[depth] back to 0 so the VALUE's je_sep 197 // doesn't add another comma. 198 w.prior[w.depth] = 0 199 return 0 200} 201 202// Emit integer as decimal. 203func json_emit_int(w: *JsonWriter, n: i64) -> i64 { 204 let rc: i64 = je_sep(w) 205 if rc < 0 { return rc } 206 if n == 0 { return je_put(w, 0x30) } 207 var v: i64 = n 208 if v < 0 { 209 let r: i64 = je_put(w, 0x2D) // '-' 210 if r < 0 { return r } 211 v = 0 - v 212 } 213 let scratch: *u8 = sys_mmap(32) 214 var k: i64 = 0 215 while v > 0 { 216 scratch[k] = 0x30 + (v % 10) 217 v = v / 10 218 k = k + 1 219 } 220 while k > 0 { 221 k = k - 1 222 let r: i64 = je_put(w, scratch[k]) 223 if r < 0 { return r } 224 } 225 return 0 226} 227 228// Emit a null-terminated literal; caller guarantees bounds. 229func je_emit_literal(w: *JsonWriter, lit: *u8, n: i64) -> i64 { 230 var i: i64 = 0 231 while i < n { 232 let r: i64 = je_put(w, lit[i]) 233 if r < 0 { return r } 234 i = i + 1 235 } 236 return 0 237} 238 239func json_emit_bool(w: *JsonWriter, b: i64) -> i64 { 240 let rc0: i64 = je_sep(w) 241 if rc0 < 0 { return rc0 } 242 if b != 0 { return je_emit_literal(w, "true", 4) } 243 return je_emit_literal(w, "false", 5) 244} 245 246func json_emit_null(w: *JsonWriter) -> i64 { 247 let rc: i64 = je_sep(w) 248 if rc < 0 { return rc } 249 return je_emit_literal(w, "null", 4) 250} 251 252// Compile-only smoke. Emit: {"name":"Nishi","year":2026,"active":true}. 253func main() -> i64 { 254 let buf: *u8 = sys_mmap(256) 255 let w_raw: *u8 = sys_mmap(128) 256 let w: *JsonWriter = w_raw as *JsonWriter 257 json_writer_init(w, buf, 256) 258 259 json_begin_object(w) 260 json_emit_key(w, "name", 4) 261 json_emit_string(w, "Nishi", 5) 262 json_emit_key(w, "year", 4) 263 json_emit_int(w, 2026) 264 json_emit_key(w, "active", 6) 265 json_emit_bool(w, 1) 266 json_end_object(w) 267 268 // First byte should be '{'. 269 if buf[0] != 0x7B { return 1 } 270 // w.pos should be > 0. 271 if w.pos <= 0 { return 2 } 272 return 0 273}