code wiki / (root) / nx_mccabe_lib.nx

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}