nx_mccabe_lib.nx source
↩ module page · 356 lines · 17135 B
1// nx_mccabe_lib.nx -- CYCLOMATIC COMPLEXITY FOR NISHILANG. Closes seq251.
2//
3// WHY THIS EXISTS. nx_cwe_scan grades this estate against ISO/IEC 5055, and its own output has carried the
4// hole since the day it shipped: "iso5055_factors":{"covered_of_4":3,"gap":["Maintainability -- needs
5// cyclomatic/Halstead complexity, filed seq251"]}. The estate identified its own conformance gap, FILED it,
6// and then nobody built it. This is that build.
7//
8// THE MAPPING, STATED SO NOBODY HAS TO GUESS IT. McCabe 1976 defines v(G) = e - n + 2p over the control-flow
9// graph of a single procedure with one entry and one exit; for one connected procedure (p=1) that reduces to
10// DECISION POINTS + 1, which is the form computed here. This TRANSFERS CLEANLY -- it needs a procedure with
11// branching, not classes, not inheritance, not packages. It is not a re-target and not an analogue.
12// Decision points counted, and this list IS the mapping: if, while, for, and each short-circuit operator
13// (&& and ||), each of which adds an independent path. `else` is NOT counted -- it is the other side of a
14// predicate already counted. `else if` IS counted, via its own `if`.
15//
16// COMMENTS AND STRING LITERALS ARE SKIPPED, AND THAT IS THE WHOLE BALLGAME. Measured on this very corpus
17// before the organ existed: the first three `&&` occurrences found by a naive text search were all inside
18// COMMENTS, and one more was a lexer emitting the token as data. A SCANNER THAT DOES NOT SKIP COMMENTS
19// MEASURES THE DOCUMENTATION, NOT THE CODE -- the estate already paid for that law once (readOnly 15.5->32.5%).
20//
21// NO CAP. The body is read with sys_read_file, which sizes its buffer from the file itself and cannot
22// short-read, so there is no truncation to declare and no guessed ceiling to tune.
23//
24// NO THRESHOLD IS INVENTED HERE. The organ publishes the DISTRIBUTION and derives its own percentiles from
25// the CDF. The one bar it names -- 10 -- is CITED from McCabe 1976 and formalised in NIST SP 500-235, and is
26// reported as "above the cited limit", never as a pass or a fail. AN UNCALIBRATED CLASSIFIER MUST REPORT
27// NUMBERS, NEVER VERDICTS.
28// license_tier: ORIGINAL LIBRARY -- no main. nx_mccabe.nx and nx_mccabe_gate.nx both consume it, so the
29// census and its gate share ONE ruler by construction and cannot drift apart.
30import "nx_syscalls.nx"
31import "nx_headmark_lib.nx"
32
33const MC_DEF_DIR: *u8 = "buildroot/runtime\x00"
34const MC_LIMIT: i64 = 10 // CITED: McCabe 1976 and NIST SP 500-235. NOT chosen here.
35const MC_HIST_MAX: i64 = 512 // exact histogram buckets; anything above is counted in the overflow
36const MC_WORKCAP: i64 = 1048576
37const MC_WORKFLUSH: i64 = 1046000
38const MC_PATHCAP: i64 = 4096
39const MC_DIRBUF: i64 = 131072
40const MC_MAXDEPTH: i64 = 8
41const MC_NAMECAP: i64 = 128
42const MC_DT_DIR: i64 = 4
43const MC_PERMILLE: i64 = 1000
44
45// counters index map
46const MC_C_FUNCS: i64 = 0
47const MC_C_FILES: i64 = 1
48const MC_C_OVER: i64 = 2 // v(G) above MC_HIST_MAX (histogram overflow)
49const MC_C_MAX: i64 = 3
50const MC_C_ABOVE: i64 = 4 // v(G) > MC_LIMIT
51const MC_C_SUMVG: i64 = 5
52const MC_C_UNREAD: i64 = 6 // files that could not be read -- UNKNOWN, never counted as clean
53
54func mc_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
55func mc_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
56func mc_cat(o: *u8, at: i64, s: *u8) -> i64 { var i: i64=0; var a: i64=at; while s[i]!=(0 as u8){o[a]=s[i]; a=a+1; i=i+1} return a }
57func mc_catn(o: *u8, at: i64, v: i64) -> i64 {
58 var a: i64=at; var x: i64=v
59 if x<0 { o[a]=45 as u8; a=a+1; x=0-x }
60 let tm: *u8=sys_mmap(32); var k: i64=0
61 if x==0 { tm[0]=48 as u8; k=1 }
62 while x>0 { tm[k]=(48+x%10) as u8; x=x/10; k=k+1 }
63 var j: i64=0
64 while j<k { o[a]=tm[k-1-j]; a=a+1; j=j+1 }
65 return a
66}
67func mc_is_ident(c: i64) -> i64 {
68 if c>=97 { if c<=122 { return 1 } }
69 if c>=65 { if c<=90 { return 1 } }
70 if c>=48 { if c<=57 { return 1 } }
71 if c==95 { return 1 }
72 return 0
73}
74// keyword match at i with WORD BOUNDARIES on both sides
75func mc_kw(buf: *u8, i: i64, n: i64, kw: *u8) -> i64 {
76 let kl: i64=mc_slen(kw)
77 if i+kl>n { return 0 }
78 var k: i64=0
79 while k<kl { if buf[i+k]!=kw[k] { return 0 } k=k+1 }
80 if i>0 { if mc_is_ident(buf[i-1] as i64)==1 { return 0 } }
81 if i+kl<n { if mc_is_ident(buf[i+kl] as i64)==1 { return 0 } }
82 return 1
83}
84func mc_isdotdot(nm: *u8) -> i64 { if nm[0]==(46 as u8){ if nm[1]==(0 as u8){return 1} if nm[1]==(46 as u8){ if nm[2]==(0 as u8){return 1} } } return 0 }
85func mc_join(buf: *u8, base_n: i64, name: *u8) -> i64 { buf[base_n]=47 as u8; var o: i64=base_n+1; var i: i64=0; while name[i]!=(0 as u8){buf[o]=name[i];o=o+1;i=i+1} return o }
86func mc_ends_nx(p: *u8, n: i64) -> i64 {
87 if n<3 { return 0 }
88 if p[n-3]!=(46 as u8) { return 0 }
89 if p[n-2]!=(110 as u8) { return 0 }
90 if p[n-1]!=(120 as u8) { return 0 }
91 return 1
92}
93
94// ---- HEAD-MARKER CLASS JOIN (codeeffectiveness CE1, 2026-09-05) ---------------------------------------------
95// Every source is classified ONCE, from the buffer the scanner already holds, by the FIRST row of the class table
96// whose marker appears inside its head window (uw_headmark -- the estate's one head-marker rule, nx_headmark_lib).
97// Machine output signs itself (the builder, generators); an unmarked source is seat-authored. Per-class blocks
98// hold files, funcs, sum v(G) and above-the-cited-bar, so the census can say WHOSE code carries the complexity.
99// The table is DATA (rows `class|marker`, ORDER IS PRIORITY). A consumer that never calls mc_cls_load sees
100// c[MC_C_CLSN]==0 and the scanner classifies nothing -- the hand-computed teeth of nx_mccabe_gate are untouched.
101// A consumer that loads a table must size c[] at MC_C_SLOTS. Class 0 is UNMARKED: the partition's named remainder.
102const MC_CLS_MAX: i64 = 8 // table rows incl. row 0 (UNMARKED); conf rows land in 1..MC_CLS_MAX-1
103const MC_CLS_FIELDS: i64 = 4
104const MC_CF_FILES: i64 = 0
105const MC_CF_FUNCS: i64 = 1
106const MC_CF_SUMVG: i64 = 2
107const MC_CF_ABOVE: i64 = 3
108const MC_C_CLSN: i64 = 8 // conf rows loaded (0 = classification off)
109const MC_C_CLSTAB: i64 = 9 // the table, a *u8 carried as i64: MC_CLS_MAX rows of MC_CLS_ROW bytes
110const MC_C_CLSBASE: i64 = 16 // per-class blocks: c[MC_C_CLSBASE + cls*MC_CLS_FIELDS + field]
111const MC_C_SLOTS: i64 = 64
112const MC_CLS_ROW: i64 = 160 // one row: name NUL marker NUL
113const MC_CLS_NAMECAP: i64 = 31
114const MC_CLS_NULS: i64 = 2 // the two terminators a row carries
115const MC_CLS_CONF: *u8 = "knowledge/headmark_classes.conf\x00"
116const MC_CH_NL: i64 = 10
117const MC_CH_CR: i64 = 13
118const MC_CH_HASH: i64 = 35
119const MC_CH_BAR: i64 = 124
120
121func mc_cls_name(c: *i64, k: i64) -> *u8 { return (c[MC_C_CLSTAB] + k*MC_CLS_ROW) as *u8 }
122func mc_cls_marker(c: *i64, k: i64) -> *u8 { let nm: *u8 = mc_cls_name(c, k); return ((nm as i64) + mc_slen(nm) + 1) as *u8 }
123// Load the class table from PATH (rows `name|marker`; '#' lines and blanks skipped; a trailing CR dropped so a conf
124// edited on any host reads the same). Returns rows loaded; 0 for an absent or empty conf -- the CALLER announces it.
125func mc_cls_load_from(c: *i64, path: *u8) -> i64 {
126 let tab: *u8 = sys_mmap(MC_CLS_MAX*MC_CLS_ROW + 16)
127 c[MC_C_CLSTAB] = tab as i64
128 c[MC_C_CLSN] = 0
129 let r0: i64 = mc_cat(tab, 0, "UNMARKED\x00" as *u8)
130 tab[r0] = 0 as u8
131 tab[r0+1] = 0 as u8
132 let lp: *i64 = sys_mmap(16) as *i64
133 lp[0] = 0
134 let b: *u8 = sys_read_file(path, lp)
135 let n: i64 = lp[0]
136 if n <= 0 { return 0 }
137 var rows: i64 = 0
138 var i: i64 = 0
139 while i < n {
140 var e: i64 = i
141 var eol: i64 = 0
142 while eol == 0 { if e >= n { eol = 1 } else { if b[e] == (MC_CH_NL as u8) { eol = 1 } else { e = e + 1 } } }
143 var ee: i64 = e
144 if ee > i { if b[ee-1] == (MC_CH_CR as u8) { ee = ee - 1 } }
145 if ee > i { if b[i] != (MC_CH_HASH as u8) { if rows + 1 < MC_CLS_MAX {
146 var bar: i64 = 0 - 1
147 var p: i64 = i
148 while p < ee { if bar < 0 { if b[p] == (MC_CH_BAR as u8) { bar = p } } p = p + 1 }
149 if bar > i {
150 let row: *u8 = ((tab as i64) + (rows+1)*MC_CLS_ROW) as *u8
151 var nl: i64 = bar - i
152 if nl > MC_CLS_NAMECAP { nl = MC_CLS_NAMECAP }
153 var k: i64 = 0
154 while k < nl { row[k] = b[i+k]; k = k + 1 }
155 row[nl] = 0 as u8
156 var ml: i64 = ee - bar - 1
157 let mcap: i64 = MC_CLS_ROW - nl - MC_CLS_NULS
158 if ml > mcap { ml = mcap }
159 var q: i64 = 0
160 while q < ml { row[nl+1+q] = b[bar+1+q]; q = q + 1 }
161 row[nl+1+ml] = 0 as u8
162 rows = rows + 1
163 }
164 } } }
165 i = e + 1
166 }
167 c[MC_C_CLSN] = rows
168 return rows
169}
170func mc_cls_load(c: *i64) -> i64 { return mc_cls_load_from(c, MC_CLS_CONF as *u8) }
171// Classify ONE buffer's head: the first table row whose marker sits inside the head window wins; 0 = UNMARKED.
172func mc_headclass(buf: *u8, n: i64, c: *i64) -> i64 {
173 let rows: i64 = c[MC_C_CLSN]
174 var k: i64 = 1
175 while k <= rows {
176 let m: *u8 = mc_cls_marker(c, k)
177 let ml: i64 = mc_slen(m)
178 if ml > 0 { if uw_headmark(buf, n, m, ml) == 1 { return k } }
179 k = k + 1
180 }
181 return 0
182}
183
184// Scan ONE source. Walks the byte stream ONCE, skipping // comments and "string" literals, tracking brace
185// depth, and attributing every decision point to the function that encloses it.
186// hist[0..MC_HIST_MAX-1], c[] counters, work/wo worklist.
187func mc_scan_file(path: *u8, hist: *i64, c: *i64, work: *u8, wo: *i64) -> i64 {
188 let lp: *i64=sys_mmap(16) as *i64
189 lp[0]=0
190 let buf: *u8=sys_read_file(path, lp)
191 let n: i64=lp[0]
192 if n<=0 { c[MC_C_UNREAD]=c[MC_C_UNREAD]+1; return 0 }
193 c[MC_C_FILES]=c[MC_C_FILES]+1
194 // CE1: classify ONCE from the buffer in hand; this file's figures fold into its class block at the end
195 var fcls: i64=0
196 if c[MC_C_CLSN]>0 { fcls=mc_headclass(buf, n, c) }
197 let cf0: i64=c[MC_C_FUNCS]
198 let cs0: i64=c[MC_C_SUMVG]
199 let ca0: i64=c[MC_C_ABOVE]
200
201 let nm: *u8=sys_mmap(MC_NAMECAP+8)
202 var nml: i64=0
203 var have: i64=0 // inside a function body?
204 var depth: i64=0
205 var dec: i64=0 // decision points in the current function
206 var line: i64=1
207 var fline: i64=1
208 var i: i64=0
209 while i<n {
210 let ch: i64 = buf[i] as i64
211 if ch==10 { line=line+1; i=i+1 }
212 else {
213 // ---- comment: // to end of line
214 var handled: i64=0
215 // A LOOP THAT BREAKS BY CLOBBERING ITS OWN CURSOR CANNOT REPORT WHERE IT STOPPED: use a flag and
216 // leave i ON the newline so the outer loop still counts the line.
217 if ch==47 { if i+1<n { if buf[i+1]==(47 as u8) {
218 var cdone: i64=0
219 while cdone==0 {
220 if i>=n { cdone=1 } else { if buf[i]==(10 as u8) { cdone=1 } else { i=i+1 } }
221 }
222 handled=1
223 } } }
224 if handled==0 {
225 // ---- string literal
226 if ch==34 {
227 i=i+1
228 var closed: i64=0
229 while closed==0 {
230 if i>=n { closed=1 } else {
231 let sc: i64 = buf[i] as i64
232 if sc==92 { i=i+2 } else { if sc==34 { i=i+1; closed=1 } else { if sc==10 { line=line+1; i=i+1 } else { i=i+1 } } }
233 }
234 }
235 handled=1
236 }
237 }
238 if handled==0 {
239 if ch==123 { // {
240 if have==1 { depth=depth+1 }
241 i=i+1
242 } else { if ch==125 { // }
243 if have==1 {
244 depth=depth-1
245 if depth==0 {
246 let vg: i64=dec+1
247 c[MC_C_FUNCS]=c[MC_C_FUNCS]+1
248 c[MC_C_SUMVG]=c[MC_C_SUMVG]+vg
249 if vg>c[MC_C_MAX] { c[MC_C_MAX]=vg }
250 if vg<MC_HIST_MAX { hist[vg]=hist[vg]+1 } else { c[MC_C_OVER]=c[MC_C_OVER]+1 }
251 if vg>MC_LIMIT {
252 c[MC_C_ABOVE]=c[MC_C_ABOVE]+1
253 if wo[0]<MC_WORKFLUSH {
254 var a: i64=wo[0]
255 a=mc_cat(work, a, " vG=" as *u8); a=mc_catn(work, a, vg)
256 a=mc_cat(work, a, " " as *u8)
257 nm[nml]=0 as u8
258 a=mc_cat(work, a, nm)
259 a=mc_cat(work, a, " " as *u8); a=mc_cat(work, a, path)
260 a=mc_cat(work, a, ":" as *u8); a=mc_catn(work, a, fline)
261 a=mc_cat(work, a, "\n" as *u8)
262 wo[0]=a
263 }
264 }
265 have=0; dec=0; nml=0
266 }
267 }
268 i=i+1
269 } else {
270 // ---- func header at top level
271 if have==0 { if mc_kw(buf, i, n, "func\x00" as *u8)==1 {
272 var q: i64=i+4
273 var stop: i64=0
274 while stop==0 { if q<n { if buf[q]==(32 as u8) { q=q+1 } else { stop=1 } } else { stop=1 } }
275 nml=0
276 var stop2: i64=0
277 while stop2==0 {
278 if q<n { if mc_is_ident(buf[q] as i64)==1 { if nml<MC_NAMECAP { nm[nml]=buf[q]; nml=nml+1 } q=q+1 } else { stop2=1 } } else { stop2=1 }
279 }
280 have=1; depth=0; dec=0; fline=line
281 i=q
282 } else { i=i+1 } }
283 else {
284 // ---- decision points, only inside a function body
285 var d: i64=0
286 if mc_kw(buf, i, n, "if\x00" as *u8)==1 { d=1; i=i+2 }
287 else { if mc_kw(buf, i, n, "while\x00" as *u8)==1 { d=1; i=i+5 }
288 else { if mc_kw(buf, i, n, "for\x00" as *u8)==1 { d=1; i=i+3 }
289 else {
290 if ch==38 { if i+1<n { if buf[i+1]==(38 as u8) { d=1; i=i+2 } else { i=i+1 } } else { i=i+1 } }
291 else { if ch==124 { if i+1<n { if buf[i+1]==(124 as u8) { d=1; i=i+2 } else { i=i+1 } } else { i=i+1 } }
292 else { i=i+1 } }
293 } } }
294 if d==1 { dec=dec+1 }
295 }
296 }
297 }
298 }
299 }
300 }
301 // CE1: fold this file's figures into its class block (only when a table is loaded -- see MC_C_CLSN)
302 if c[MC_C_CLSN]>0 {
303 let cb: i64=MC_C_CLSBASE+fcls*MC_CLS_FIELDS
304 c[cb+MC_CF_FILES]=c[cb+MC_CF_FILES]+1
305 c[cb+MC_CF_FUNCS]=c[cb+MC_CF_FUNCS]+(c[MC_C_FUNCS]-cf0)
306 c[cb+MC_CF_SUMVG]=c[cb+MC_CF_SUMVG]+(c[MC_C_SUMVG]-cs0)
307 c[cb+MC_CF_ABOVE]=c[cb+MC_CF_ABOVE]+(c[MC_C_ABOVE]-ca0)
308 }
309 return 0
310}
311
312func mc_walk(p: *u8, pn: i64, depth: i64, hist: *i64, c: *i64, work: *u8, wo: *i64) -> i64 {
313 if depth>MC_MAXDEPTH { return 0 }
314 if pn>MC_PATHCAP-256 { return 0 }
315 p[pn]=0 as u8
316 let fd: i64=sys_openat_rd(p)
317 if fd<0 { return 0 }
318 let dbuf: *u8=sys_mmap(MC_DIRBUF)
319 var go: i64=1
320 while go==1 {
321 let nr: i64=sys_getdents64(fd, dbuf, MC_DIRBUF)
322 if nr<=0 { go=0 } else {
323 var off: i64=0
324 while off<nr {
325 let rec: *u8=(dbuf as i64+off) as *u8
326 let ty: i64=dirent_type(rec)
327 let nmm: *u8=dirent_name(rec)
328 if mc_isdotdot(nmm)==0 {
329 let cs: i64=mc_join(p, pn, nmm)
330 p[cs]=0 as u8
331 if ty==MC_DT_DIR { mc_walk(p, cs, depth+1, hist, c, work, wo) }
332 else { if mc_ends_nx(p, cs)==1 { mc_scan_file(p, hist, c, work, wo) } }
333 }
334 off=off+dirent_reclen(rec)
335 }
336 }
337 }
338 sys_close(fd)
339 return 0
340}
341
342// percentile from the histogram CDF: smallest v with cumulative >= target permille of total
343func mc_pct(hist: *i64, total: i64, over: i64, maxv: i64, permille: i64) -> i64 {
344 if total<=0 { return 0 }
345 var target: i64 = (total*permille)/MC_PERMILLE
346 if target<1 { target=1 } // a 1-function corpus must still yield a percentile, not fall through
347 var cum: i64=0
348 var v: i64=0
349 while v<MC_HIST_MAX {
350 cum=cum+hist[v]
351 if cum>=target { return v }
352 v=v+1
353 }
354 if over>0 { return maxv }
355 return MC_HIST_MAX
356}