nx_gate_discover.nx source
↩ module page · 267 lines · 11314 B
1// nx_gate_discover.nx -- SOVEREIGN gate discovery (NishiLang, no .sh, no grep).
2//
3// The gate runner (nx_gate_runner.nx) hardcodes N=19 names[]/gates[] entries.
4// That list ROTS: a gate added under _hdl_build/ that nobody hand-enrolls is
5// silently never run. This organ is the DYNAMIC FEED for the runner: it scans
6// _hdl_build/ for *_test.nx gate files and returns the discovered count + the
7// list of absolute paths. The runner (or any consumer) iterates the result
8// instead of a frozen array -- self-REGISTRATION, not hand-maintenance.
9//
10// SINGLE RESPONSIBILITY: this organ DISCOVERS (enumerate + filter + path-join +
11// fail-loud-on-implausible-count). It does NOT run gates. A consumer feeds the
12// returned NxGateList to gr_gate(). The discovery is ADDITIVE: it does not edit
13// the runner; the runner opts in later.
14//
15// VERIFY-BEFORE-ENROLL (cardinal: a discovered gate must be a real, compilable
16// gate): the FAIL-LOUD floor (nx_gate_discover_floor_ok) refuses an implausibly
17// low count -- if the scan returns fewer gates than the known floor, the
18// directory was mis-pathed / the getdents call failed / the filesystem is not
19// where we think it is. A 0-or-tiny count is NEVER a healthy discovery; it is a
20// silent-skip waiting to happen, so we surface it loudly. The COMPILABILITY half
21// of verify-before-enroll is the gate-runner's gr_gate() the consumer calls on
22// each discovered path (nx_gate_runner.nx) -- this organ proves the candidate
23// SET is plausible; gr_gate proves each candidate COMPILES + PROVES.
24//
25// REUSES (concept-not-copy, DRY):
26// nx_dir.nx -- nx_dir_list (getdents enumeration into rows+arena),
27// nx_dir_name_ends_with (suffix filter), nx_dir_row_at,
28// NxDirRow/NxDirResult, NX_DIR_OK/NX_DIR_EMPTY/...
29// (transitively) -- syscalls.nx -> nx_syscalls.nx (sys_write/mmap/exit),
30// nx_fcntl.nx, nx_dirent.nx
31//
32// license_tier: ORIGINAL
33
34import "nx_dir.nx"
35
36// ===== Discovery verdicts ========================================
37
38const NX_GD_OK: i64 = 0 // discovered a plausible, non-empty gate set
39const NX_GD_EMPTY: i64 = 1 // directory readable but no *_test.nx gates
40const NX_GD_IMPLAUSIBLE: i64 = 2 // count below the fail-loud floor (mis-path / FS fault)
41const NX_GD_SCAN_FAILED: i64 = 3 // underlying nx_dir_list failed (open/getdents)
42const NX_GD_TRUNCATED: i64 = 4 // more gates than out_capacity (raise the cap)
43const NX_GD_BAD_ARGS: i64 = 5
44
45// The plausibility floor. The hand-enrolled runner already lists 16 distinct
46// _hdl_build/ gates (and the directory holds at least that many *_test.nx
47// files). A scan that finds fewer than this many gates is implausible: the
48// directory was not where we expected, or getdents short-read. Data-driven, one
49// named constant -- no magic number buried in a branch (cardinal #11).
50const NX_GD_PLAUSIBLE_FLOOR: i64 = 10
51
52// Sizing for the scan. _hdl_build/ holds well under this many entries; the
53// runner's universe is ~20 gates. Bounded, no unbounded growth.
54const NX_GD_MAX_GATES: i64 = 256
55const NX_GD_NAME_ARENA: i64 = 32768 // 256 names * ~128 bytes headroom
56const NX_GD_PATH_ARENA: i64 = 131072 // 256 abs-paths * ~512 bytes headroom
57
58// ===== NxGateList -- the discovered feed =========================
59//
60// paths[i] (0 <= i < count) is a NUL-terminated absolute path to a discovered
61// *_test.nx gate, ready to hand to gr_gate(). names[i] is the bare filename
62// (also NUL-terminated). Both point into arenas held by the list.
63
64struct NxGateList {
65 verdict: i64, // NX_GD_*
66 count: i64, // number of discovered gates
67 paths: *i64, // [count] of *u8 (absolute paths)
68 names: *i64, // [count] of *u8 (bare filenames)
69 name_arena: *u8, // backing store for filenames
70 path_arena: *u8, // backing store for absolute paths
71 path_used: i64,
72 dir_path: *u8, // the directory scanned (for forensics)
73}
74
75const NX_GATE_LIST_BYTES: i64 = 72
76
77// ===== small string helpers (local, no copy of foreign helpers) ==
78
79func gd_strlen(s: *u8) -> i64 {
80 var n: i64 = 0
81 while s[n] != 0 as u8 { n = n + 1 }
82 return n
83}
84
85// Append src[0..len) + a NUL into arena at off; return new off, or -1 on
86// overflow. Mirrors nx_dir_arena_copy_name's contract but for path-joins.
87func gd_arena_append(src: *u8, len: i64, arena: *u8, cap: i64, off: i64) -> i64 {
88 if off + len + 1 > cap { return 0 - 1 }
89 var i: i64 = 0
90 while i < len {
91 arena[off + i] = src[i]
92 i = i + 1
93 }
94 arena[off + len] = 0
95 return off + len + 1
96}
97
98// Byte-equality of two NUL-terminated strings (1 == equal).
99func gd_streq(a: *u8, b: *u8) -> i64 {
100 var i: i64 = 0
101 while a[i] != 0 as u8 {
102 if a[i] != b[i] { return 0 }
103 i = i + 1
104 }
105 if b[i] != 0 as u8 { return 0 }
106 return 1
107}
108
109// ===== suffix constant ===========================================
110// We discover NishiLang gate tests: files named "*_test.nx".
111
112func gd_suffix() -> *u8 { return "_test.nx" as *u8 }
113
114// ===== nx_gate_discover -- the discovery transaction =============
115//
116// Scans dir_path for *_test.nx, joining each onto dir_path to produce an
117// absolute path. Fills `out`. Returns out.verdict. include floor=1 enforces the
118// fail-loud plausibility floor; floor=0 reports EMPTY/OK without the floor (used
119// only by the negative-control branch of the test).
120func nx_gate_discover(dir_path: *u8, out: *NxGateList, enforce_floor: i64) -> i64 {
121 if out == 0 as *NxGateList { return NX_GD_BAD_ARGS }
122 out.verdict = NX_GD_BAD_ARGS
123 out.count = 0
124 out.path_used = 0
125 out.dir_path = dir_path
126 if dir_path == 0 as *u8 { return NX_GD_BAD_ARGS }
127
128 // arenas + index tables.
129 let rows: *NxDirRow = sys_mmap(NX_GD_MAX_GATES * NX_DIR_ROW_BYTES) as *NxDirRow
130 let dname_arena: *u8 = sys_mmap(NX_GD_NAME_ARENA)
131 let res: *NxDirResult = sys_mmap(NX_DIR_RESULT_BYTES) as *NxDirResult
132
133 let name_arena: *u8 = sys_mmap(NX_GD_NAME_ARENA)
134 let path_arena: *u8 = sys_mmap(NX_GD_PATH_ARENA)
135 let names: *i64 = sys_mmap(NX_GD_MAX_GATES * 8) as *i64
136 let paths: *i64 = sys_mmap(NX_GD_MAX_GATES * 8) as *i64
137 out.name_arena = name_arena
138 out.path_arena = path_arena
139 out.names = names
140 out.paths = paths
141
142 // 1. ENUMERATE (reuse nx_dir_list -- getdents under the hood).
143 let dv: i64 = nx_dir_list(dir_path, rows, NX_GD_MAX_GATES,
144 dname_arena, NX_GD_NAME_ARENA, 0, res)
145 if dv == NX_DIR_OPEN_FAILED { out.verdict = NX_GD_SCAN_FAILED; return NX_GD_SCAN_FAILED }
146 if dv == NX_DIR_GETDENTS_FAILED { out.verdict = NX_GD_SCAN_FAILED; return NX_GD_SCAN_FAILED }
147 if dv == NX_DIR_BAD_ARGS { out.verdict = NX_GD_SCAN_FAILED; return NX_GD_SCAN_FAILED }
148 // NX_DIR_OK / NX_DIR_EMPTY / NX_DIR_TRUNCATED all leave usable rows.
149
150 // 2. FILTER (*_test.nx) + 3. PATH-JOIN (dir_path + "/" + name).
151 let suffix: *u8 = gd_suffix()
152 let slen: i64 = gd_strlen(suffix)
153 let dlen: i64 = gd_strlen(dir_path)
154 var name_off: i64 = 0
155 var path_off: i64 = 0
156 var n_filled_total: i64 = res.n_filled
157 var count: i64 = 0
158 var truncated: i64 = 0
159
160 var i: i64 = 0
161 while i < n_filled_total {
162 let row: *NxDirRow = nx_dir_row_at(rows, i)
163 // regular file only (skip stray subdirs), ending in "_test.nx".
164 var keep: i64 = 0
165 if nx_dir_row_is_regular_file(row) == 1 { keep = 1 }
166 if keep == 1 {
167 if nx_dir_name_ends_with(row.name_ptr, row.name_len, suffix, slen) == 0 { keep = 0 }
168 }
169 if keep == 1 {
170 if count >= NX_GD_MAX_GATES {
171 truncated = 1
172 i = n_filled_total // stop scanning
173 }
174 }
175 if keep == 1 {
176 if truncated == 0 {
177 // copy filename
178 let nstart: i64 = name_off
179 let no2: i64 = gd_arena_append(row.name_ptr, row.name_len,
180 name_arena, NX_GD_NAME_ARENA, name_off)
181 if no2 < 0 { truncated = 1 }
182 if truncated == 0 {
183 name_off = no2
184 names[count] = (name_arena as i64 + nstart)
185 // build absolute path: dir_path + '/' + name
186 let pstart: i64 = path_off
187 var po: i64 = gd_arena_append(dir_path, dlen, path_arena, NX_GD_PATH_ARENA, path_off)
188 if po < 0 { truncated = 1 }
189 if truncated == 0 {
190 // overwrite the NUL just written with '/', then append name+NUL
191 po = po - 1
192 path_arena[po] = 0x2F // '/'
193 po = po + 1
194 let po2: i64 = gd_arena_append(row.name_ptr, row.name_len,
195 path_arena, NX_GD_PATH_ARENA, po)
196 if po2 < 0 { truncated = 1 }
197 if truncated == 0 {
198 path_off = po2
199 paths[count] = (path_arena as i64 + pstart)
200 count = count + 1
201 }
202 }
203 }
204 }
205 }
206 i = i + 1
207 }
208
209 out.count = count
210 out.path_used = path_off
211
212 if truncated == 1 { out.verdict = NX_GD_TRUNCATED; return NX_GD_TRUNCATED }
213 if count == 0 { out.verdict = NX_GD_EMPTY; return NX_GD_EMPTY }
214
215 // 4. FAIL-LOUD plausibility floor (verify-before-enroll: a healthy scan
216 // finds at least the known floor of gates; fewer = mis-path / FS fault).
217 if enforce_floor == 1 {
218 if count < NX_GD_PLAUSIBLE_FLOOR { out.verdict = NX_GD_IMPLAUSIBLE; return NX_GD_IMPLAUSIBLE }
219 }
220
221 out.verdict = NX_GD_OK
222 return NX_GD_OK
223}
224
225// ===== convenience: is a named gate in the discovered list? ======
226// Returns the index of the gate whose bare filename == want, else -1.
227func nx_gate_discover_find(out: *NxGateList, want: *u8) -> i64 {
228 if out == 0 as *NxGateList { return 0 - 1 }
229 var i: i64 = 0
230 while i < out.count {
231 let nm: *u8 = out.names[i] as *u8
232 if gd_streq(nm, want) == 1 { return i }
233 i = i + 1
234 }
235 return 0 - 1
236}
237
238// ===== convenience: print the discovered feed ====================
239func nx_gate_discover_print(out: *NxGateList) -> i64 {
240 let pre: *u8 = "gate-discover: count="
241 sys_write(1, pre, gd_strlen(pre))
242 // small decimal print
243 let n: i64 = out.count
244 if n == 0 {
245 sys_write(1, "0" as *u8, 1)
246 }
247 if n != 0 {
248 let d: *u8 = sys_mmap(24)
249 var m: i64 = n
250 var k: i64 = 0
251 while m > 0 { d[k] = (0x30 + (m % 10)) as u8; m = m / 10; k = k + 1 }
252 let o: *u8 = sys_mmap(24)
253 var j: i64 = 0
254 while j < k { o[j] = d[k - 1 - j]; j = j + 1 }
255 sys_write(1, o, k)
256 }
257 sys_write(1, "\n" as *u8, 1)
258 var i: i64 = 0
259 while i < out.count {
260 let nm: *u8 = out.names[i] as *u8
261 sys_write(1, " " as *u8, 2)
262 sys_write(1, nm, gd_strlen(nm))
263 sys_write(1, "\n" as *u8, 1)
264 i = i + 1
265 }
266 return 0
267}