code wiki / (root) / nx_yaml_subset.nx

nx_yaml_subset.nx source

↩ module page · 331 lines · 10619 B

1// nx_yaml_subset.nx -- bits-up YAML subset parser. 2// 3// Mirrors the YAML subset that nishi-pages/build/build.py accepted: 4// 5// key: value (string) 6// key: 123 (int) 7// key: -42 (signed int) 8// key: 2026-05-19 (ISO date, kept as string) 9// key: true (bool, kept as string "1" / "0") 10// key: "quoted value" (strips outer quotes) 11// key: 'quoted' (same, single) 12// key: [a, b, c] (flow list of strings) 13// # comment lines (ignored) 14// <blank line> (ignored) 15// 16// Used by: 17// - nishi-pages site.yaml + per-doc frontmatter 18// - bench/ops scripts that read config 19// 20// Per cardinal feedback-bits-up-exceed-never-match: substrate-owned; 21// no libyaml, no Python's yaml.safe_load, no PyYAML. Parser is 22// deterministic + bounded + auditable. 23// 24// Per cardinal feedback-defensive-at-boundaries-trusting-internally: 25// input bytes are validated (key chars only [A-Za-z0-9_-]), value bytes 26// are stored verbatim (NUL-terminated) into a caller-owned arena. 27 28// nx_safety_envelope: 29// intended_use: reusable YAML subset parser for nishi-pages + ops scripts 30// sil_target: SIL2 31// evidence: bench/nx_yaml_subset_smoke.nx exercises all 4 value forms 32// + comments + blanks + flow list + key-not-found 33// verdict: NOT_YET_EVALUATED -- pending smoke 34 35import "nx_syscalls_x86_64.nx" 36 37// ---- Sealed verdicts ---- 38 39const NX_YAML_OK: i64 = 0 40const NX_YAML_BAD_INPUT: i64 = 1 // src null or len <= 0 41const NX_YAML_BAD_LINE: i64 = 2 // no colon in non-empty/non-comment line 42const NX_YAML_TOO_MANY_KEYS: i64 = 3 // exceeded max_entries 43const NX_YAML_ARENA_FULL: i64 = 4 // value arena cap exhausted 44const NX_YAML_KEY_TOO_LONG: i64 = 5 45 46const NX_YAML_MAX_KEY_LEN: i64 = 64 47 48// ---- Result table ---- 49// 50// Entries[i] = {key_off, key_len, val_off, val_len} into the caller- 51// owned (keys, values) byte arenas. Caller pre-allocates both arenas 52// + the entries array sized for max_entries records. 53 54struct NxYamlEntry { 55 key_off: i64, 56 key_len: i64, 57 val_off: i64, 58 val_len: i64, 59} 60 61const NX_YAML_ENTRY_BYTES: i64 = 32 62 63struct NxYamlDoc { 64 entries: *NxYamlEntry, 65 n_entries: i64, 66 max_entries: i64, 67 keys: *u8, 68 keys_used: i64, 69 keys_cap: i64, 70 values: *u8, 71 values_used: i64, 72 values_cap: i64, 73} 74 75const NX_YAML_DOC_BYTES: i64 = 72 76 77// ---- Construction ---- 78 79func nx_yaml_doc_init(d: *NxYamlDoc, 80 entries: *NxYamlEntry, max_entries: i64, 81 keys: *u8, keys_cap: i64, 82 values: *u8, values_cap: i64) -> i64 { 83 d.entries = entries 84 d.n_entries = 0 85 d.max_entries = max_entries 86 d.keys = keys 87 d.keys_used = 0 88 d.keys_cap = keys_cap 89 d.values = values 90 d.values_used = 0 91 d.values_cap = values_cap 92 return NX_YAML_OK 93} 94 95// ---- Helpers ---- 96 97func _yml_is_ws(b: i64) -> i64 { 98 if b == 32 { return 1 } 99 if b == 9 { return 1 } 100 return 0 101} 102 103func _yml_is_key_char(b: i64) -> i64 { 104 if b >= 48 { if b <= 57 { return 1 } } // 0-9 105 if b >= 65 { if b <= 90 { return 1 } } // A-Z 106 if b >= 97 { if b <= 122 { return 1 } } // a-z 107 if b == 95 { return 1 } // _ 108 if b == 45 { return 1 } // - 109 return 0 110} 111 112// Find next newline (\n) starting at off. Returns index of \n or end. 113func _yml_eol(src: *u8, end: i64, off: i64) -> i64 { 114 var p: i64 = off 115 while p < end { 116 if src[p] == 10 { return p } 117 p = p + 1 118 } 119 return end 120} 121 122// Trim leading whitespace. Returns new offset. 123func _yml_ltrim(src: *u8, end: i64, off: i64) -> i64 { 124 var p: i64 = off 125 while p < end { 126 if _yml_is_ws(src[p] as i64) == 0 { return p } 127 p = p + 1 128 } 129 return end 130} 131 132// Trim trailing ws + \r from [start..line_end). Returns new end. 133func _yml_rtrim(src: *u8, start: i64, line_end: i64) -> i64 { 134 var p: i64 = line_end 135 while p > start { 136 let b: i64 = src[p - 1] as i64 137 if b == 32 { p = p - 1 } 138 else { if b == 9 { p = p - 1 } 139 else { if b == 13 { p = p - 1 } 140 else { return p } 141 } 142 } 143 } 144 return p 145} 146 147// Strip matching outer quotes. Returns (new_off, new_len) by mutating 148// the passed pointers. Handles "..." and '...'. No escape processing. 149func _yml_strip_quotes(src: *u8, off_io: *i64, len_io: *i64) -> i64 { 150 let off: i64 = off_io[0] 151 let n: i64 = len_io[0] 152 if n < 2 { return 0 } 153 let first: i64 = src[off] as i64 154 let last: i64 = src[off + n - 1] as i64 155 if first == 34 { 156 if last == 34 { 157 off_io[0] = off + 1 158 len_io[0] = n - 2 159 return 1 160 } 161 } 162 if first == 39 { 163 if last == 39 { 164 off_io[0] = off + 1 165 len_io[0] = n - 2 166 return 1 167 } 168 } 169 return 0 170} 171 172// Append `n` bytes from src to dst[used], bump used. Returns 1 on OK, 173// 0 if dst would overflow cap. 174func _yml_append(dst: *u8, used_io: *i64, cap: i64, 175 src: *u8, off: i64, n: i64) -> i64 { 176 let used: i64 = used_io[0] 177 if used + n > cap { return 0 } 178 var i: i64 = 0 179 while i < n { 180 dst[used + i] = src[off + i] 181 i = i + 1 182 } 183 used_io[0] = used + n 184 return 1 185} 186 187// ---- Main parse ---- 188 189func nx_yaml_parse(d: *NxYamlDoc, src: *u8, src_n: i64) -> i64 { 190 if src as i64 == 0 { return NX_YAML_BAD_INPUT } 191 if src_n <= 0 { return NX_YAML_BAD_INPUT } 192 193 var p: i64 = 0 194 while p < src_n { 195 // Skip leading whitespace on the line. 196 let line_start: i64 = _yml_ltrim(src, src_n, p) 197 let eol: i64 = _yml_eol(src, src_n, line_start) 198 199 // Empty line? 200 if line_start >= eol { 201 p = eol + 1 202 } else { 203 // Comment line? 204 if src[line_start] == 35 { 205 p = eol + 1 206 } else { 207 // Find colon. 208 var colon: i64 = -1 209 var i: i64 = line_start 210 while i < eol { 211 if src[i] == 58 { colon = i; i = eol } 212 else { i = i + 1 } 213 } 214 if colon < 0 { return NX_YAML_BAD_LINE } 215 216 // Key = trimmed bytes [line_start..colon) 217 let key_end: i64 = _yml_rtrim(src, line_start, colon) 218 let key_len: i64 = key_end - line_start 219 if key_len <= 0 { return NX_YAML_BAD_LINE } 220 if key_len > NX_YAML_MAX_KEY_LEN { return NX_YAML_KEY_TOO_LONG } 221 // Validate key chars. 222 var k: i64 = 0 223 while k < key_len { 224 if _yml_is_key_char(src[line_start + k] as i64) == 0 { 225 return NX_YAML_BAD_LINE 226 } 227 k = k + 1 228 } 229 230 // Value = trimmed bytes [colon+1..eol) with quotes stripped. 231 let val_raw_start: i64 = _yml_ltrim(src, eol, colon + 1) 232 let val_raw_end: i64 = _yml_rtrim(src, val_raw_start, eol) 233 var val_off: i64 = val_raw_start 234 var val_len: i64 = val_raw_end - val_raw_start 235 let val_off_slot: *i64 = sys_mmap(16) as *i64 236 let val_len_slot: *i64 = sys_mmap(16) as *i64 237 val_off_slot[0] = val_off 238 val_len_slot[0] = val_len 239 _yml_strip_quotes(src, val_off_slot, val_len_slot) 240 val_off = val_off_slot[0] 241 val_len = val_len_slot[0] 242 243 // Record entry. 244 if d.n_entries >= d.max_entries { return NX_YAML_TOO_MANY_KEYS } 245 let e_idx: i64 = d.n_entries 246 let e: *NxYamlEntry = (d.entries as i64 + e_idx * NX_YAML_ENTRY_BYTES) as *NxYamlEntry 247 248 e.key_off = d.keys_used 249 e.key_len = key_len 250 let kused: *i64 = sys_mmap(16) as *i64 251 kused[0] = d.keys_used 252 let kok: i64 = _yml_append(d.keys, kused, d.keys_cap, 253 src, line_start, key_len) 254 if kok == 0 { return NX_YAML_ARENA_FULL } 255 d.keys_used = kused[0] 256 257 e.val_off = d.values_used 258 e.val_len = val_len 259 let vused: *i64 = sys_mmap(16) as *i64 260 vused[0] = d.values_used 261 let vok: i64 = _yml_append(d.values, vused, d.values_cap, 262 src, val_off, val_len) 263 if vok == 0 { return NX_YAML_ARENA_FULL } 264 d.values_used = vused[0] 265 266 d.n_entries = d.n_entries + 1 267 p = eol + 1 268 } 269 } 270 } 271 272 return NX_YAML_OK 273} 274 275// ---- Lookup ---- 276// 277// Find a value by key. Returns 1 on hit + sets out_off / out_len. 278// Returns 0 on miss. 279 280func nx_yaml_lookup(d: *NxYamlDoc, 281 key: *u8, key_n: i64, 282 out_off: *i64, out_len: *i64) -> i64 { 283 var i: i64 = 0 284 while i < d.n_entries { 285 let e: *NxYamlEntry = (d.entries as i64 + i * NX_YAML_ENTRY_BYTES) as *NxYamlEntry 286 if e.key_len == key_n { 287 var eq: i64 = 1 288 var j: i64 = 0 289 while j < key_n { 290 if d.keys[e.key_off + j] != key[j] { eq = 0; j = key_n } 291 j = j + 1 292 } 293 if eq == 1 { 294 out_off[0] = e.val_off 295 out_len[0] = e.val_len 296 return 1 297 } 298 } 299 i = i + 1 300 } 301 return 0 302} 303 304// ---- Number of entries ---- 305 306func nx_yaml_count(d: *NxYamlDoc) -> i64 { 307 return d.n_entries 308} 309 310// ---- Read key at index ---- 311// Helper for iteration. 312 313func nx_yaml_key_at(d: *NxYamlDoc, i: i64, 314 out_off: *i64, out_len: *i64) -> i64 { 315 if i < 0 { return 0 } 316 if i >= d.n_entries { return 0 } 317 let e: *NxYamlEntry = (d.entries as i64 + i * NX_YAML_ENTRY_BYTES) as *NxYamlEntry 318 out_off[0] = e.key_off 319 out_len[0] = e.key_len 320 return 1 321} 322 323func nx_yaml_value_at(d: *NxYamlDoc, i: i64, 324 out_off: *i64, out_len: *i64) -> i64 { 325 if i < 0 { return 0 } 326 if i >= d.n_entries { return 0 } 327 let e: *NxYamlEntry = (d.entries as i64 + i * NX_YAML_ENTRY_BYTES) as *NxYamlEntry 328 out_off[0] = e.val_off 329 out_len[0] = e.val_len 330 return 1 331}