code wiki / (root) / nx_phrase_match.nx

nx_phrase_match.nx source

↩ module page · 154 lines · 5264 B

1// nx_phrase_match.nx -- case-insensitive substring matcher. 2// 3// Tiny utility used by the Tier-2 element extractors 4// (nx_outfit_extract / nx_location_extract / nx_domain_classify) 5// when scanning beat text for known phrases. 6// 7// Why dedicated and not nx_str_find: the canonical nx_string_ops 8// search is case-sensitive (matches C strstr semantics) and pulls 9// in a longer import chain. This file is a small, self-contained 10// case-INSENSITIVE substring detector that handles ASCII upper / 11// lower equivalence inline. 12// 13// Limitations (deliberate): 14// - ASCII only. Multi-byte UTF-8 case folding is NOT done; CJK 15// phrases are matched byte-exact (they have no case anyway). 16// - Word-boundary aware via optional require_boundary flag: if 17// true, match only when neither end is adjacent to an ASCII 18// letter / digit (prevents "apron" matching inside "apronk"). 19// 20// Per the bounded-loop cardinal: outer + inner loops both ride 21// explicit JPL Rule 2 BUDGETS. 22// 23// nx_safety_envelope: 24// intended_use: "Case-insensitive substring search inside a 25// byte range, used by element extractors." 26// sil_target: SIL2 27// asil_target: QM 28// dal_target: DAL C 29// iec_62304_class: NONE 30// evidence: [no_floating_point, 31// bounded_outer_loop_jpl_rule_2, 32// bounded_inner_loop_jpl_rule_2, 33// ascii_only_case_folding_documented] 34// hazard_register: [bug-tape-utf8-case-fold-not-supported, 35// bug-tape-boundary-check-permissive-on-punct] 36// residual_risk: "ASCII-only fold; CJK phrases match 37// byte-exact. Boundary check treats non- 38// alphanumeric (incl. punctuation) as a 39// boundary -- intended." 40// verdict: NOT_YET_EVALUATED 41 42import "nx_syscalls.nx" 43 44// ===== byte helpers =============================================== 45 46func nx_pm_load_u8(p: *u8, i: i64) -> i64 { 47 let q: *u8 = ((p as i64) + i) as *u8 48 return *q 49} 50 51// Lowercase an ASCII byte; non-ASCII / non-letter returned unchanged. 52func nx_pm_to_lower(c: i64) -> i64 { 53 if c >= 0x41 { 54 if c <= 0x5A { return c | 0x20 } 55 } 56 return c 57} 58 59// 1 if c is ASCII letter or digit (boundary classifier). 60func nx_pm_is_alnum(c: i64) -> i64 { 61 if c >= 0x30 { 62 if c <= 0x39 { return 1 } 63 } 64 if c >= 0x41 { 65 if c <= 0x5A { return 1 } 66 } 67 if c >= 0x61 { 68 if c <= 0x7A { return 1 } 69 } 70 return 0 71} 72 73// ===== core matcher =============================================== 74// 75// Search `hay[0..nh)` for case-insensitive occurrence of 76// `needle[0..nn)`. Returns offset of first match, or -1 if none. 77// 78// require_boundary: when nonzero, only match when neither the 79// preceding nor following byte is alphanumeric (prevents inside- 80// word matches). 81// 82// Empty needle returns 0 (vacuously matches at start). 83 84func nx_phrase_find_ci(hay: *u8, nh: i64, 85 needle: *u8, nn: i64, 86 require_boundary: i64) -> i64 { 87 if nn <= 0 { return 0 } 88 if nh < nn { return -1 } 89 let last_start: i64 = nh - nn 90 let OUTER_BUDGET: i64 = nh + 1 91 var outer: i64 = 0 92 var i: i64 = 0 93 while i <= last_start { 94 if outer >= OUTER_BUDGET { return -1 } 95 96 // Optional left-boundary check. 97 var ok_left: i64 = 1 98 if require_boundary != 0 { 99 if i > 0 { 100 let left: i64 = nx_pm_load_u8(hay, i - 1) 101 if nx_pm_is_alnum(left) == 1 { ok_left = 0 } 102 } 103 } 104 105 if ok_left == 1 { 106 // Inner compare. 107 var matched: i64 = 1 108 let INNER_BUDGET: i64 = nn + 1 109 var inner: i64 = 0 110 var j: i64 = 0 111 while j < nn { 112 if inner >= INNER_BUDGET { matched = 0; j = nn } 113 if matched == 1 { 114 let h: i64 = nx_pm_to_lower(nx_pm_load_u8(hay, i + j)) 115 let p: i64 = nx_pm_to_lower(nx_pm_load_u8(needle, j)) 116 if h != p { matched = 0; j = nn } 117 } 118 j = j + 1 119 inner = inner + 1 120 } 121 if matched == 1 { 122 // Optional right-boundary check. 123 var ok_right: i64 = 1 124 if require_boundary != 0 { 125 if i + nn < nh { 126 let right: i64 = nx_pm_load_u8(hay, i + nn) 127 if nx_pm_is_alnum(right) == 1 { ok_right = 0 } 128 } 129 } 130 if ok_right == 1 { return i } 131 } 132 } 133 i = i + 1 134 outer = outer + 1 135 } 136 return -1 137} 138 139// Convenience: case-insensitive contains. Returns 1 / 0. 140func nx_phrase_contains_ci(hay: *u8, nh: i64, 141 needle: *u8, nn: i64) -> i64 { 142 if nx_phrase_find_ci(hay, nh, needle, nn, 1) >= 0 { return 1 } 143 return 0 144} 145 146// Compute C string length (NUL-terminated, bounded by max). 147func nx_phrase_strlen(s: *u8, max: i64) -> i64 { 148 var i: i64 = 0 149 while i < max { 150 if nx_pm_load_u8(s, i) == 0 { return i } 151 i = i + 1 152 } 153 return max 154}