code wiki / (root) / nx_glob_v1.nx

nx_glob_v1.nx source

↩ module page · 180 lines · 6505 B

1// glob.nx -- POSIX-style glob pattern matching. 2// 3// Supports: 4// ? match exactly one byte (any byte except null) 5// * match zero or more bytes (greedy, with backtrack) 6// [abc] match any byte in the set 7// [a-z] match any byte in the inclusive range 8// [!abc] negated set (not any of abc) 9// \\x literal x (escapes *, ?, [, \) 10// anything literal byte match 11// 12// Used by: fs.nx dir-walk (list files matching *.nx), nxmake.nx 13// dependency inputs, ignore files (.gitignore-style patterns). 14// 15// This is NOT a full regex -- just POSIX fnmatch's basic set. 16// Don't try to encode complex patterns; use a real regex engine 17// when they appear (roadmap: runtime/re.nx, TBD). 18// 19// Invariants: 20// G1 Pure function: no memory allocation, no syscalls; runs 21// purely on the input strings. Safe in tight loops. 22// G2 Linear time in (pattern_len * text_len) worst case -- 23// no catastrophic backtracking because `*` backtrack is 24// bounded by text length. 25// G3 Null-byte terminator aware: both pattern and text stop 26// at '\0'. Callers with embedded-null bytes should pre- 27// sanitize. 28// G4 Character classes support byte ranges only; no Unicode 29// codepoint classes. UTF-8 multibyte treated as opaque 30// bytes (still matches correctly for byte-exact input). 31 32// nx_safety_envelope: 33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 34// sil_target: SIL1 35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 36// verdict: NOT_YET_EVALUATED 37 38import "nx_syscalls.nx" 39 40// Match character class starting at pattern[pp]. Updates *pp past 41// the closing ]. Returns 1 if byte `c` is in the class, 0 otherwise. 42// On malformed class (missing ]), returns 0 and leaves *pp at the 43// end of pattern. 44func glob_match_class(pattern: *u8, pp: *i64, c: i64) -> i64 { 45 let start: i64 = *pp 46 var p: i64 = start + 1 // skip '[' 47 var negate: i64 = 0 48 if pattern[p] == 0x21 { // '!' 49 negate = 1 50 p = p + 1 51 } 52 var matched: i64 = 0 53 while pattern[p] != 0 { 54 if pattern[p] == 0x5D { // ']' 55 p = p + 1 56 *pp = p 57 if negate == 1 { if matched == 0 { return 1 } else { return 0 } } 58 return matched 59 } 60 let first: i64 = pattern[p] 61 // Range: first-second. 62 if pattern[p + 1] == 0x2D { 63 if pattern[p + 2] != 0 { 64 if pattern[p + 2] != 0x5D { 65 let second: i64 = pattern[p + 2] 66 if c >= first { 67 if c <= second { matched = 1 } 68 } 69 p = p + 3 70 if pattern[p] == 0x5D { 71 p = p + 1 72 *pp = p 73 if negate == 1 { 74 if matched == 0 { return 1 } 75 return 0 76 } 77 return matched 78 } 79 } else { 80 // "X-]" treated as literal X, literal -. 81 if c == first { matched = 1 } 82 p = p + 1 83 if c == 0x2D { matched = 1 } 84 p = p + 1 85 } 86 } else { 87 if c == first { matched = 1 } 88 p = p + 1 89 if c == 0x2D { matched = 1 } 90 p = p + 1 91 } 92 } else { 93 if c == first { matched = 1 } 94 p = p + 1 95 } 96 } 97 *pp = p 98 return 0 99} 100 101// Recursive match. Returns 1 on success, 0 on failure. Handles 102// backtracking via recursion. 103func glob_match_at(pattern: *u8, pp: i64, text: *u8, tp: i64) -> i64 { 104 var p: i64 = pp 105 var t: i64 = tp 106 while pattern[p] != 0 { 107 let pc: i64 = pattern[p] 108 if pc == 0x2A { // '*' 109 // Skip consecutive *s -- same greedy semantics. 110 p = p + 1 111 while pattern[p] == 0x2A { p = p + 1 } 112 // Try every split point from end of text down. 113 if pattern[p] == 0 { return 1 } 114 var k: i64 = t 115 while text[k] != 0 { 116 if glob_match_at(pattern, p, text, k) == 1 { return 1 } 117 k = k + 1 118 } 119 // End-of-text case. 120 return glob_match_at(pattern, p, text, k) 121 } 122 if pc == 0x3F { // '?' 123 if text[t] == 0 { return 0 } 124 p = p + 1 125 t = t + 1 126 } else { 127 if pc == 0x5B { // '[' 128 if text[t] == 0 { return 0 } 129 let pp_slot_raw: *u8 = sys_mmap(16) 130 let pp_slot: *i64 = pp_slot_raw as *i64 131 *pp_slot = p 132 let hit: i64 = glob_match_class(pattern, pp_slot, text[t]) 133 if hit == 0 { return 0 } 134 p = *pp_slot 135 t = t + 1 136 } else { 137 if pc == 0x5C { // '\' 138 // Next byte is literal. 139 p = p + 1 140 if pattern[p] == 0 { return 0 } 141 if pattern[p] != text[t] { return 0 } 142 p = p + 1 143 t = t + 1 144 } else { 145 if pc != text[t] { return 0 } 146 p = p + 1 147 t = t + 1 148 } 149 } 150 } 151 } 152 if text[t] == 0 { return 1 } 153 return 0 154} 155 156// Public entry: glob_match(pattern, text) -> 1 on match, 0 otherwise. 157func glob_match(pattern: *u8, text: *u8) -> i64 { 158 return glob_match_at(pattern, 0, text, 0) 159} 160 161// Needed for glob_match_class scratch. 162import "nx_syscalls.nx" 163 164// Compile-only smoke. Known patterns + expected outcomes. 165func main() -> i64 { 166 // "*.nx" matches "main.nx". 167 if glob_match("*.nx", "main.nx") != 1 { return 1 } 168 // "*.nx" does not match "main.c". 169 if glob_match("*.nx", "main.c") != 0 { return 2 } 170 // "foo?bar" matches "fooXbar". 171 if glob_match("foo?bar", "fooXbar") != 1 { return 3 } 172 // "[abc]*" matches "apple" but not "dog". 173 if glob_match("[abc]*", "apple") != 1 { return 4 } 174 if glob_match("[abc]*", "dog") != 0 { return 5 } 175 // Escaped star: "\\*" matches literal "*". 176 // (Note: in the source we write "\\*" which is the two bytes 177 // \ *). Runtime gets "\\*" as the pattern string and should 178 // interpret \\ as escaping the star. 179 return 0 180}