code wiki / hub / nx_search_query_parser.nx

nx_search_query_parser.nx source

↩ module page · 542 lines · 22796 B

1// nx_search_query_parser.nx -- HUB primitive; HTTP query string -> NxSearchQuery. 2// 3// COMPOSES (per NISHI_SMALL_SHARP_COMPOSABLE_STANDARD §4.1 M7): 4// nx_search_inverted (nx_inv_is_token_char + nx_inv_hash_bytes_lower 5// token-character convention reuse) 6// nx_hygiene_prims (NxOptPtr; safe-arith; bounded-loop budget) 7// 8// COMPOSED BY: 9// wiki/nx_wiki_search_handler.nx (next commit; wiki wiring) 10// (future) obd-config-site search handler 11// (future) sprinkler-config-site search handler 12// any site that needs to parse search query strings 13// 14// Status: V1 SEED. 2026-05-27. SITE-AGNOSTIC HUB PRIMITIVE per 15// NISHI_SMALL_SHARP_COMPOSABLE_STANDARD §4.1 M3. 16// 17// WINNER-TIER: BASELINE-C provisional 18// INCUMBENTS: Elasticsearch's QueryParser (Lucene), Whoosh 19// (Python), Tantivy (Rust), Bleve (Go), Algolia's 20// query parser, Meilisearch's query parser 21// NUMBERS: V1 ships query parsing (URL-decode + tokenize + 22// field extraction); paired latency bench vs 23// Lucene QueryParser pending real query workload 24// GAP: Lucene QueryParser handles boolean operators 25// (AND/OR/NOT), phrase quotes, fuzzy ~, boost ^, 26// proximity ~N -- V1 ships JUST term-list parsing 27// (implicit AND); V2 adds the operators 28// PLAN: M-next: V2 boolean operators + phrase quotes; 29// paired latency bench vs Lucene on identical 30// workload 31// EXEMPTION REASON: n/a; provisional pending measurement 32// 33// V1 SCOPE per NISHI_SEARCH_CHARTER.md §6: 34// - URL-decode query string (% escapes + plus-is-space) 35// - Extract q= terms (implicit AND; V1) 36// - Extract scope= (ONSITE default / OFFSITE / BOTH; sealed) 37// - Extract n= max results 38// - Tokenize per nx_search_inverted conventions 39// (lowercase + nx_inv_is_token_char + length 2..64) 40// - Sealed NxSearchQuery emit (caller-allocated buffers) 41// 42// V2 SCOPE (TODO): 43// - Boolean operators: foo AND bar; foo OR bar; foo NOT bar 44// - Phrase quotes: "exact phrase" 45// - Field-scoped: title:foo, tags:bar 46// - Boost: term^2.5 47// - Fuzzy: term~2 48 49import "nx_syscalls.nx" 50import "nx_search_inverted.nx" 51import "nx_hygiene_prims.nx" 52 53// ===== Sealed verdict surface (codes 1500-1509) ================================================= 54const NX_SQP_OK: i64 = 0 55const NX_SQP_BAD_INPUT: i64 = 1500 56const NX_SQP_QUERY_TOO_LONG: i64 = 1501 57const NX_SQP_TOO_MANY_TERMS: i64 = 1502 58const NX_SQP_TERM_TOO_LONG: i64 = 1503 59const NX_SQP_TERMS_BUF_OVERFLOW: i64 = 1504 60const NX_SQP_BAD_URL_ESCAPE: i64 = 1505 61const NX_SQP_BAD_SCOPE: i64 = 1506 62const NX_SQP_LOOP_BUDGET: i64 = 1507 63const NX_SQP_NOT_IMPLEMENTED: i64 = 1508 64 65// ===== Sealed scope enum (per NISHI_SEARCH_CHARTER §9) ================================================= 66const NX_SEARCH_SCOPE_ONSITE: i64 = 0 // default; local docs only 67const NX_SEARCH_SCOPE_OFFSITE: i64 = 1 // crawled offsite content only (V3 ships engine) 68const NX_SEARCH_SCOPE_BOTH: i64 = 2 // federated; merged 69const NX_SEARCH_SCOPE_N: i64 = 3 70 71func nx_search_scope_is_valid(s: i64) -> i64 { 72 if s < 0 { return 0 } 73 if s >= NX_SEARCH_SCOPE_N { return 0 } 74 return 1 75} 76 77// ===== Named sizing constants (M7 no magic numbers) ================================================= 78const NX_SQP_MAX_QUERY_LEN: i64 = 2048 // total query-string bytes 79const NX_SQP_MAX_TERMS: i64 = 32 // hard cap per query (per NX_INV_MAX_POSTINGS_PER hint) 80const NX_SQP_MAX_TERM_LEN: i64 = 64 // per NX_INV_MAX_TOKEN_LEN 81const NX_SQP_MIN_TERM_LEN: i64 = 2 // per NX_INV_MIN_TOKEN_LEN 82const NX_SQP_TERMS_BUF_MIN_CAP: i64 = 1024 // 32 terms * 32 avg bytes 83const NX_SQP_DEFAULT_MAX_RESULTS: i64 = 20 84const NX_SQP_HARD_MAX_RESULTS: i64 = 1000 85const NX_SQP_LOOP_BUDGET: i64 = 100000 // per-parse iteration cap 86 87// ASCII byte literals (M7). 88const NX_SQP_ASCII_PLUS: i64 = 0x2B // '+' 89const NX_SQP_ASCII_PERCENT: i64 = 0x25 // '%' 90const NX_SQP_ASCII_AMP: i64 = 0x26 // '&' 91const NX_SQP_ASCII_EQ: i64 = 0x3D // '=' 92const NX_SQP_ASCII_SP: i64 = 0x20 // ' ' 93 94// Sealed param-name literals. 95const NX_SQP_PARAM_Q: *u8 = "q" as *u8 96const NX_SQP_PARAM_Q_N: i64 = 1 97const NX_SQP_PARAM_SCOPE: *u8 = "scope" as *u8 98const NX_SQP_PARAM_SCOPE_N: i64 = 5 99const NX_SQP_PARAM_N: *u8 = "n" as *u8 100const NX_SQP_PARAM_N_N: i64 = 1 101 102// Sealed scope-value literals. 103const NX_SQP_SCOPE_VAL_ONSITE: *u8 = "onsite" as *u8 104const NX_SQP_SCOPE_VAL_ONSITE_N: i64 = 6 105const NX_SQP_SCOPE_VAL_OFFSITE: *u8 = "offsite" as *u8 106const NX_SQP_SCOPE_VAL_OFFSITE_N: i64 = 7 107const NX_SQP_SCOPE_VAL_BOTH: *u8 = "both" as *u8 108const NX_SQP_SCOPE_VAL_BOTH_N: i64 = 4 109 110// ===== NxSearchQuery struct (caller-allocated per M4) ================================================= 111// 112// Terms are packed: per term [u8 len_byte][len bytes utf-8]. 113// Iterate via nx_search_query_term_at. 114 115struct NxSearchQuery { 116 terms_buf: *u8 // caller-allocated; cap NX_SQP_TERMS_BUF_MIN_CAP+ 117 terms_cap: i64 118 terms_used: i64 // bytes filled 119 terms_count: i64 // # of terms 120 scope: i64 // sealed enum value 121 max_results: i64 122 valid: i64 123 // V2: per-term kind (sealed enum SHOULD/MUST/MUST_NOT). Allocated 124 // by _init via sys_mmap (NX_SQP_MAX_TERMS bytes). 125 term_kinds: *u8 126} 127 128// Sealed enum for boolean operator kind per term (V2). 129const NX_SQP_TERM_KIND_SHOULD: i64 = 0 // bare term (V1 default; preserved) 130const NX_SQP_TERM_KIND_MUST: i64 = 1 // "+term" -- doc must contain 131const NX_SQP_TERM_KIND_MUST_NOT: i64 = 2 // "-term" -- doc must NOT contain 132const NX_SQP_TERM_KIND_N: i64 = 3 133 134// Sealed-init: stamps every field; refuses half-state. 135func nx_search_query_init(q: *NxSearchQuery, 136 terms_buf: *u8, terms_cap: i64) -> i64 { 137 if (q as i64) == 0 { return 0 - NX_SQP_BAD_INPUT } 138 if (terms_buf as i64) == 0 { return 0 - NX_SQP_BAD_INPUT } 139 if terms_cap < NX_SQP_TERMS_BUF_MIN_CAP { return 0 - NX_SQP_BAD_INPUT } 140 q.terms_buf = terms_buf 141 q.terms_cap = terms_cap 142 q.terms_used = 0 143 q.terms_count = 0 144 q.scope = NX_SEARCH_SCOPE_ONSITE // default per charter §5.1 M2 145 q.max_results = NX_SQP_DEFAULT_MAX_RESULTS 146 // V2: per-term kind array. 147 q.term_kinds = sys_mmap(NX_SQP_MAX_TERMS) 148 q.valid = 1 149 return NX_SQP_OK 150} 151 152// ===== URL-decode helpers (bounded; M3 + M5) ================================================= 153// 154// Decodes one byte; returns: 155// >= 0 -> the decoded byte value (0..255) via out-param; 156// returns bytes_consumed in source (1, 2, or 3) 157// < 0 -> verdict 158 159// Hex char to int 0..15; -1 if not hex. 160func nx_sqp_hex_val(c: i64) -> i64 { 161 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } } // '0'..'9' 162 if c >= 0x41 { if c <= 0x46 { return c - 0x37 } } // 'A'..'F' 163 if c >= 0x61 { if c <= 0x66 { return c - 0x57 } } // 'a'..'f' 164 return 0 - 1 165} 166 167// Read one source byte (URL-decoded). Returns bytes-consumed via 168// caller's idx; decoded byte via out-param. 169func nx_sqp_decode_one(src: *u8, src_n: i64, idx: i64, 170 out_byte: *i64, out_consumed: *i64) -> i64 { 171 if (out_byte as i64) == 0 { return 0 - NX_SQP_BAD_INPUT } 172 if (out_consumed as i64) == 0 { return 0 - NX_SQP_BAD_INPUT } 173 if idx < 0 { return 0 - NX_SQP_BAD_INPUT } 174 if idx >= src_n { return 0 - NX_SQP_BAD_INPUT } 175 176 let c: i64 = src[idx] as i64 177 if c == NX_SQP_ASCII_PLUS { 178 out_byte[0] = NX_SQP_ASCII_SP 179 out_consumed[0] = 1 180 return NX_SQP_OK 181 } 182 if c == NX_SQP_ASCII_PERCENT { 183 if idx + 2 >= src_n { return 0 - NX_SQP_BAD_URL_ESCAPE } 184 let h: i64 = nx_sqp_hex_val(src[idx + 1] as i64) 185 let l: i64 = nx_sqp_hex_val(src[idx + 2] as i64) 186 if h < 0 { return 0 - NX_SQP_BAD_URL_ESCAPE } 187 if l < 0 { return 0 - NX_SQP_BAD_URL_ESCAPE } 188 out_byte[0] = (h << 4) | l 189 out_consumed[0] = 3 190 return NX_SQP_OK 191 } 192 out_byte[0] = c 193 out_consumed[0] = 1 194 return NX_SQP_OK 195} 196 197// ===== Bytes-equal helper (case-sensitive; bounded) ================================================= 198 199func nx_sqp_bytes_eq(a: *u8, a_n: i64, b: *u8, b_n: i64) -> i64 { 200 if a_n != b_n { return 0 } 201 if (a as i64) == 0 { return 0 } 202 if (b as i64) == 0 { return 0 } 203 var i: i64 = 0 204 while i < a_n { 205 if i >= NX_SQP_MAX_QUERY_LEN { return 0 } 206 if a[i] != b[i] { return 0 } 207 i = i + 1 208 } 209 return 1 210} 211 212// ===== Term append (filters length + tokenizes + lowercases) ================================================= 213// 214// Appends one term to q.terms_buf. Filters: length in 215// [NX_SQP_MIN_TERM_LEN, NX_SQP_MAX_TERM_LEN]; only valid token chars 216// per nx_inv_is_token_char. Lowercases ASCII letters. 217 218func nx_search_query_append_term_kind(q: *NxSearchQuery, 219 src: *u8, src_n: i64, kind: i64) -> i64 { 220 if q.valid != 1 { return 0 - NX_SQP_BAD_INPUT } 221 if src_n < NX_SQP_MIN_TERM_LEN { return NX_SQP_OK } // silently drop too-short 222 if src_n > NX_SQP_MAX_TERM_LEN { return 0 - NX_SQP_TERM_TOO_LONG } 223 if q.terms_count >= NX_SQP_MAX_TERMS { return 0 - NX_SQP_TOO_MANY_TERMS } 224 if kind < 0 { return 0 - NX_SQP_BAD_INPUT } 225 if kind >= NX_SQP_TERM_KIND_N { return 0 - NX_SQP_BAD_INPUT } 226 // Need 1 byte for len prefix + src_n bytes for content. 227 if q.terms_used + 1 + src_n > q.terms_cap { 228 return 0 - NX_SQP_TERMS_BUF_OVERFLOW 229 } 230 // Stamp kind in parallel array. 231 q.term_kinds[q.terms_count] = (kind & 0xff) as u8 232 // Write length prefix. 233 q.terms_buf[q.terms_used] = (src_n & 0xff) as u8 234 var i: i64 = 0 235 while i < src_n { 236 if i >= NX_SQP_MAX_TERM_LEN { return 0 - NX_SQP_TERM_TOO_LONG } 237 var c: i64 = src[i] as i64 238 // Lowercase ASCII (per nx_search_inverted convention). 239 if c >= 0x41 { if c <= 0x5A { c = c + 32 } } 240 // Validate via nx_search_inverted's char predicate. 241 if nx_inv_is_token_char(c) != 1 { 242 // Skip invalid chars within a term; substitute nothing. 243 // For V1 we treat invalid mid-term as bad-input on the 244 // tokenizer side; here we've already broken on token 245 // boundaries so this branch shouldn't fire. Bail loud. 246 return 0 - NX_SQP_BAD_INPUT 247 } 248 q.terms_buf[q.terms_used + 1 + i] = (c & 0xff) as u8 249 i = i + 1 250 } 251 q.terms_used = q.terms_used + 1 + src_n 252 q.terms_count = q.terms_count + 1 253 return NX_SQP_OK 254} 255 256// V1 backward-compat: bare-term default kind is MUST (preserves V1 257// implicit-AND semantics; existing callers unchanged). 258func nx_search_query_append_term(q: *NxSearchQuery, 259 src: *u8, src_n: i64) -> i64 { 260 return nx_search_query_append_term_kind(q, src, src_n, NX_SQP_TERM_KIND_MUST) 261} 262 263// ===== Tokenize a URL-decoded query body ================================================= 264// 265// Walks the decoded q= value; emits terms on every whitespace / 266// non-token boundary. Bounded by NX_SQP_LOOP_BUDGET. 267 268func nx_search_query_tokenize_value(q: *NxSearchQuery, 269 value_buf: *u8, value_n: i64) -> i64 { 270 if q.valid != 1 { return 0 - NX_SQP_BAD_INPUT } 271 if value_n < 0 { return 0 - NX_SQP_BAD_INPUT } 272 if value_n > NX_SQP_MAX_QUERY_LEN { return 0 - NX_SQP_QUERY_TOO_LONG } 273 274 var i: i64 = 0 275 var iter: i64 = 0 276 while i < value_n { 277 if iter >= NX_SQP_LOOP_BUDGET { return 0 - NX_SQP_LOOP_BUDGET } 278 iter = iter + 1 279 // V2: detect boolean prefix on token start. 280 var pending_kind: i64 = NX_SQP_TERM_KIND_MUST // bare term default (V1 implicit AND preserved) 281 let c0: i64 = value_buf[i] as i64 282 if c0 == 0x2B { // '+' MUST prefix 283 if i + 1 < value_n { 284 if nx_inv_is_token_char(value_buf[i + 1] as i64) == 1 { 285 pending_kind = NX_SQP_TERM_KIND_MUST 286 i = i + 1 287 } 288 } 289 } 290 if c0 == 0x2D { // '-' MUST_NOT prefix 291 if i + 1 < value_n { 292 if nx_inv_is_token_char(value_buf[i + 1] as i64) == 1 { 293 pending_kind = NX_SQP_TERM_KIND_MUST_NOT 294 i = i + 1 295 } 296 } 297 } 298 // Skip non-token chars. 299 let c: i64 = value_buf[i] as i64 300 if nx_inv_is_token_char(c) != 1 { 301 i = i + 1 302 } 303 if nx_inv_is_token_char(c) == 1 { 304 // Find end of this token. 305 let start: i64 = i 306 var j: i64 = i 307 while j < value_n { 308 if iter >= NX_SQP_LOOP_BUDGET { return 0 - NX_SQP_LOOP_BUDGET } 309 iter = iter + 1 310 if nx_inv_is_token_char(value_buf[j] as i64) != 1 { j = value_n } 311 if j < value_n { 312 if nx_inv_is_token_char(value_buf[j] as i64) == 1 { j = j + 1 } 313 } 314 } 315 // Find the actual end (j may have been forced to value_n). 316 var actual_end: i64 = start 317 while actual_end < value_n { 318 if iter >= NX_SQP_LOOP_BUDGET { return 0 - NX_SQP_LOOP_BUDGET } 319 iter = iter + 1 320 if nx_inv_is_token_char(value_buf[actual_end] as i64) != 1 { actual_end = value_n + 1 } 321 if actual_end <= value_n { if actual_end < value_n { 322 if nx_inv_is_token_char(value_buf[actual_end] as i64) == 1 { actual_end = actual_end + 1 } 323 }} 324 } 325 // actual_end overshot by 1 due to sentinel; trim. 326 let real_end: i64 = if actual_end > value_n then value_n else actual_end - 1 327 let term_n: i64 = real_end - start 328 if term_n > 0 { 329 let term_ptr: *u8 = (value_buf as i64 + start) as *u8 330 let rc: i64 = nx_search_query_append_term_kind(q, term_ptr, term_n, pending_kind) 331 if rc != NX_SQP_OK { return rc } 332 } 333 i = real_end 334 if i <= start { i = start + 1 } // safety: forward progress 335 } 336 } 337 return NX_SQP_OK 338} 339 340// ===== Scope-value parser ================================================= 341 342func nx_search_query_parse_scope_value(q: *NxSearchQuery, 343 val: *u8, val_n: i64) -> i64 { 344 if q.valid != 1 { return 0 - NX_SQP_BAD_INPUT } 345 if nx_sqp_bytes_eq(val, val_n, NX_SQP_SCOPE_VAL_ONSITE, 346 NX_SQP_SCOPE_VAL_ONSITE_N) == 1 { 347 q.scope = NX_SEARCH_SCOPE_ONSITE 348 return NX_SQP_OK 349 } 350 if nx_sqp_bytes_eq(val, val_n, NX_SQP_SCOPE_VAL_OFFSITE, 351 NX_SQP_SCOPE_VAL_OFFSITE_N) == 1 { 352 q.scope = NX_SEARCH_SCOPE_OFFSITE 353 return NX_SQP_OK 354 } 355 if nx_sqp_bytes_eq(val, val_n, NX_SQP_SCOPE_VAL_BOTH, 356 NX_SQP_SCOPE_VAL_BOTH_N) == 1 { 357 q.scope = NX_SEARCH_SCOPE_BOTH 358 return NX_SQP_OK 359 } 360 return 0 - NX_SQP_BAD_SCOPE 361} 362 363// ===== n= (max_results) parser ================================================= 364 365func nx_search_query_parse_n_value(q: *NxSearchQuery, 366 val: *u8, val_n: i64) -> i64 { 367 if val_n < 1 { return 0 - NX_SQP_BAD_INPUT } 368 if val_n > 10 { return 0 - NX_SQP_BAD_INPUT } // i32 fits in 10 digits 369 var v: i64 = 0 370 var i: i64 = 0 371 while i < val_n { 372 let c: i64 = val[i] as i64 373 if c < 0x30 { return 0 - NX_SQP_BAD_INPUT } 374 if c > 0x39 { return 0 - NX_SQP_BAD_INPUT } 375 v = v * 10 + (c - 0x30) 376 if v > NX_SQP_HARD_MAX_RESULTS { v = NX_SQP_HARD_MAX_RESULTS } 377 i = i + 1 378 } 379 if v < 1 { v = NX_SQP_DEFAULT_MAX_RESULTS } 380 q.max_results = v 381 return NX_SQP_OK 382} 383 384// ===== Top-level: parse a full HTTP query string ================================================= 385// 386// Input: "q=hygiene+standard&scope=onsite&n=20" (no leading '?'). 387// Walks param-by-param; URL-decodes values; dispatches to per-param 388// handler. 389 390func nx_search_query_parse(q: *NxSearchQuery, 391 query_str: *u8, query_str_n: i64) -> i64 { 392 if q.valid != 1 { return 0 - NX_SQP_BAD_INPUT } 393 if query_str_n < 0 { return 0 - NX_SQP_BAD_INPUT } 394 if query_str_n > NX_SQP_MAX_QUERY_LEN { return 0 - NX_SQP_QUERY_TOO_LONG } 395 if query_str_n == 0 { return NX_SQP_OK } // empty query is valid (defaults) 396 397 // Working buffer for URL-decoded values. 398 let decoded: *u8 = sys_mmap(NX_SQP_MAX_QUERY_LEN) 399 400 var i: i64 = 0 401 var iter: i64 = 0 402 while i < query_str_n { 403 if iter >= NX_SQP_LOOP_BUDGET { return 0 - NX_SQP_LOOP_BUDGET } 404 iter = iter + 1 405 406 // Find name (up to '=' or '&'). 407 let name_start: i64 = i 408 var name_end: i64 = i 409 var found_eq: i64 = 0 410 while name_end < query_str_n { 411 if iter >= NX_SQP_LOOP_BUDGET { return 0 - NX_SQP_LOOP_BUDGET } 412 iter = iter + 1 413 let cc: i64 = query_str[name_end] as i64 414 if cc == NX_SQP_ASCII_EQ { found_eq = 1; name_end = query_str_n + 1 } 415 if cc == NX_SQP_ASCII_AMP { name_end = query_str_n + 1 } 416 if name_end <= query_str_n { name_end = name_end + 1 } 417 } 418 let real_name_end: i64 = if name_end > query_str_n then query_str_n else name_end - 1 419 let name_n: i64 = real_name_end - name_start 420 if name_n == 0 { return NX_SQP_OK } // trailing '&'; done 421 422 let name_ptr: *u8 = (query_str as i64 + name_start) as *u8 423 424 // If '=' found, value is from real_name_end+1 to next '&' or end. 425 var value_start: i64 = real_name_end 426 var value_end: i64 = real_name_end 427 if found_eq == 1 { 428 value_start = real_name_end + 1 429 value_end = value_start 430 while value_end < query_str_n { 431 if iter >= NX_SQP_LOOP_BUDGET { return 0 - NX_SQP_LOOP_BUDGET } 432 iter = iter + 1 433 let cc2: i64 = query_str[value_end] as i64 434 if cc2 == NX_SQP_ASCII_AMP { value_end = query_str_n + 1 } 435 if value_end <= query_str_n { value_end = value_end + 1 } 436 } 437 let real_value_end: i64 = if value_end > query_str_n then query_str_n else value_end - 1 438 value_end = real_value_end 439 } 440 441 // URL-decode the value into `decoded`. 442 var decoded_n: i64 = 0 443 if found_eq == 1 { 444 var k: i64 = value_start 445 let value_ptr: *u8 = (query_str as i64 + 0) as *u8 446 while k < value_end { 447 if iter >= NX_SQP_LOOP_BUDGET { return 0 - NX_SQP_LOOP_BUDGET } 448 iter = iter + 1 449 let byte_out: *i64 = (sys_mmap(8)) as *i64 450 let consumed_out: *i64 = (sys_mmap(8)) as *i64 451 byte_out[0] = 0 452 consumed_out[0] = 0 453 let drc: i64 = nx_sqp_decode_one(value_ptr, query_str_n, k, 454 byte_out, consumed_out) 455 if drc != NX_SQP_OK { return drc } 456 if decoded_n >= NX_SQP_MAX_QUERY_LEN { return 0 - NX_SQP_QUERY_TOO_LONG } 457 decoded[decoded_n] = (byte_out[0] & 0xff) as u8 458 decoded_n = decoded_n + 1 459 k = k + consumed_out[0] 460 } 461 } 462 463 // Dispatch by param name. 464 if nx_sqp_bytes_eq(name_ptr, name_n, NX_SQP_PARAM_Q, NX_SQP_PARAM_Q_N) == 1 { 465 let rc: i64 = nx_search_query_tokenize_value(q, decoded, decoded_n) 466 if rc != NX_SQP_OK { return rc } 467 } 468 if nx_sqp_bytes_eq(name_ptr, name_n, NX_SQP_PARAM_SCOPE, NX_SQP_PARAM_SCOPE_N) == 1 { 469 let rc: i64 = nx_search_query_parse_scope_value(q, decoded, decoded_n) 470 if rc != NX_SQP_OK { return rc } 471 } 472 if nx_sqp_bytes_eq(name_ptr, name_n, NX_SQP_PARAM_N, NX_SQP_PARAM_N_N) == 1 { 473 let rc: i64 = nx_search_query_parse_n_value(q, decoded, decoded_n) 474 if rc != NX_SQP_OK { return rc } 475 } 476 // Unknown params silently ignored (V1 forgiving; V2 may 477 // promote to BAD_INPUT once param vocabulary sealed). 478 479 // Advance past this param. 480 if found_eq == 1 { i = value_end } else { i = real_name_end } 481 // Skip the '&'. 482 if i < query_str_n { 483 if query_str[i] == (NX_SQP_ASCII_AMP as u8) { i = i + 1 } 484 } 485 } 486 return NX_SQP_OK 487} 488 489// ===== Reader API: iterate over parsed terms ================================================= 490 491// Returns term count (>=0) or 0 - verdict. 492func nx_search_query_count(q: *NxSearchQuery) -> i64 { 493 if q.valid != 1 { return 0 - NX_SQP_BAD_INPUT } 494 return q.terms_count 495} 496 497// Look up term at index i; fills out_ptr + out_len. 498func nx_search_query_term_at(q: *NxSearchQuery, idx: i64, 499 out_ptr: *i64, out_len: *i64) -> i64 { 500 if q.valid != 1 { return 0 - NX_SQP_BAD_INPUT } 501 if idx < 0 { return 0 - NX_SQP_BAD_INPUT } 502 if idx >= q.terms_count { return 0 - NX_SQP_BAD_INPUT } 503 if (out_ptr as i64) == 0 { return 0 - NX_SQP_BAD_INPUT } 504 if (out_len as i64) == 0 { return 0 - NX_SQP_BAD_INPUT } 505 506 // Walk terms_buf to find idx-th term. 507 var off: i64 = 0 508 var k: i64 = 0 509 while k < idx { 510 if k >= NX_SQP_MAX_TERMS { return 0 - NX_SQP_BAD_INPUT } 511 if off >= q.terms_used { return 0 - NX_SQP_BAD_INPUT } 512 let len: i64 = q.terms_buf[off] as i64 513 off = off + 1 + len 514 k = k + 1 515 } 516 if off >= q.terms_used { return 0 - NX_SQP_BAD_INPUT } 517 let len: i64 = q.terms_buf[off] as i64 518 out_ptr[0] = (q.terms_buf as i64) + off + 1 519 out_len[0] = len 520 return NX_SQP_OK 521} 522 523// ===== Scope + max-results accessors (M1 no NULL; M5 bounds) ================================================= 524 525func nx_search_query_scope(q: *NxSearchQuery) -> i64 { 526 if q.valid != 1 { return NX_SEARCH_SCOPE_ONSITE } 527 return q.scope 528} 529 530func nx_search_query_max_results(q: *NxSearchQuery) -> i64 { 531 if q.valid != 1 { return NX_SQP_DEFAULT_MAX_RESULTS } 532 return q.max_results 533} 534 535// V2: per-term kind accessor. Returns sealed enum value or 536// NX_SQP_TERM_KIND_SHOULD on out-of-range (defensive default). 537func nx_search_query_term_kind_at(q: *NxSearchQuery, idx: i64) -> i64 { 538 if q.valid != 1 { return NX_SQP_TERM_KIND_SHOULD } 539 if idx < 0 { return NX_SQP_TERM_KIND_SHOULD } 540 if idx >= q.terms_count { return NX_SQP_TERM_KIND_SHOULD } 541 return q.term_kinds[idx] as i64 542}