code wiki / _hdl_build / nx_dupfunc.nx
nx_dupfunc.nx source
↩ module page · 373 lines · 16319 B
1// nx_dupfunc.nx -- WHICH FUNCTION BODIES ARE WRITTEN MORE THAN ONCE?
2//
3// ===== WHY THIS EXISTS ============================================
4//
5// The tree already has nx_dup_source_check, and it is a good tool, but it
6// answers a different question: it detects the SAME FILE existing in two
7// source trees. It is file-level and cross-tree by construction, so it
8// cannot see two files in the same directory that happen to contain the same
9// function written twice.
10//
11// That gap is not theoretical. Building nx_gatescan and nx_gatequality I
12// duplicated NINE functions between them -- the tree walk, file read, path
13// join, the file-type predicates, the attribution rule -- while holding two
14// tools designed to find exactly this class of problem. Nothing in the
15// ecosystem could have told me. This organ closes that.
16//
17// ===== THE DESIGN DECISION THAT MAKES IT WORK =====================
18//
19// ★NAMES ARE IGNORED. My nine duplicates were called gs_len and gq_len,
20// gs_walk and gq_walk -- identical bodies behind different prefixes, which is
21// what copying into a new file naturally produces. A detector keyed on names
22// would have found none of them. So a function is reduced to its BODY, and
23// the body alone decides identity.
24//
25// Normalisation before comparison, each for a reason:
26// - comments stripped : a copied function usually keeps the code and
27// rewrites the comment, or vice versa
28// - whitespace collapsed : re-indentation is not a difference
29// - the signature line dropped: that is where the name lives
30//
31// ===== RANKED BY COST, NOT BY COUNT ===============================
32//
33// Two copies of a 60-line walk are a different problem from two copies of a
34// one-line helper. Severity is therefore (copies - 1) x body size: what you
35// would actually save, and what is actually at risk of drifting apart.
36//
37// ⚠WHAT IT DOES NOT MEAN. Identical bodies are not automatically a defect.
38// This tree deliberately keeps independent re-derivations of a rule inside
39// gates, because a test that calls the code under test can only confirm
40// self-consistency -- and that decision is what caught a dead primitive
41// earlier. So this reports CANDIDATES, and gate-to-gate matches are counted
42// separately from product-code matches, because only the second kind is
43// unambiguously debt.
44//
45// nx_dupfunc <root-dir> [min-body-bytes] [topk]
46// default root runtime, min body 200 bytes, topk 25
47//
48// license_tier: ORIGINAL
49import "nx_syscalls.nx"
50import "nx_eco_graph.nx"
51import "nx_gatelib.nx"
52const DF_MAGIC_1469598103934665603: i64 = 1469598103934665603
53const DF_MAGIC_1099511628211: i64 = 1099511628211
54const DF_MAGIC_4096: i64 = 4096
55const DF_MAGIC_262144: i64 = 262144
56
57const DF_MAXNODE: i64 = 24000
58const DF_ARENA: i64 = 4194304
59const DF_HASH: i64 = 65536
60const DF_PATHARENA: i64 = 4194304
61const DF_MAXFUNCS: i64 = 64
62// Total functions tracked across the whole scan. DECLARED, and reported.
63const DF_MAXTOTAL: i64 = 120000
64
65func dw(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
66func dn(v: i64) -> i64 { let b: *u8 = sys_mmap(24); var m: i64 = v; if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } let t: *u8 = sys_mmap(24); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 } while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } var i: i64 = 0; while i < k { b[i] = t[k - 1 - i]; i = i + 1 } sys_write(1, b, k); return 0 }
67
68
69// FNV-1a over the normalised body. A 64-bit hash over bodies this size makes
70// an accidental collision vanishingly unlikely, and the report prints the
71// file names so a human confirms before acting -- the hash narrows the search,
72// it does not decide.
73func df_hash(buf: *u8, n: i64) -> i64 {
74 var h: i64 = DF_MAGIC_1469598103934665603
75 var i: i64 = 0
76 while i < n {
77 h = h ^ (buf[i] as i64)
78 h = h * DF_MAGIC_1099511628211
79 i = i + 1
80 }
81 return h
82}
83
84func main(argc: i64, argv: *i64) -> i64 {
85 var root: *u8 = "runtime" as *u8
86 var minbody: i64 = 200
87 var topk: i64 = 25
88 if argc >= 2 { root = argv[1] as *u8 }
89 if argc >= 3 {
90 let s: *u8 = argv[2] as *u8
91 var v: i64 = 0
92 var i: i64 = 0
93 while s[i] != (0 as u8) { let d: i64 = s[i] as i64; if d >= 48 { if d <= 57 { v = v * 10 + (d - 48) } } i = i + 1 }
94 minbody = v
95 }
96 if argc >= 4 {
97 let s2: *u8 = argv[3] as *u8
98 var v2: i64 = 0
99 var i2: i64 = 0
100 while s2[i2] != (0 as u8) { let d2: i64 = s2[i2] as i64; if d2 >= 48 { if d2 <= 57 { v2 = v2 * 10 + (d2 - 48) } } i2 = i2 + 1 }
101 if v2 > 0 { topk = v2 }
102 }
103
104 let g: *EcoGraph = eg_new(DF_MAXNODE, 8, DF_ARENA, DF_HASH)
105 let patharena: *u8 = sys_mmap(DF_PATHARENA)
106 let pathoff: *i64 = sys_mmap(DF_MAXNODE * 8) as *i64
107 let used: *i64 = sys_mmap(16)
108 let st: *i64 = sys_mmap(16)
109 used[0] = 0
110 st[0] = 0
111 var z: i64 = 0
112 while z < DF_MAXNODE { pathoff[z] = 0; z = z + 1 }
113 let path: *u8 = sys_mmap(DF_MAGIC_4096)
114 var rn: i64 = 0
115 while root[rn] != (0 as u8) { path[rn] = root[rn]; rn = rn + 1 }
116 gl_walk(g, path, rn, 0 as *u8, 0 as *i64, 0 as *i64, st, 0, patharena, pathoff, used, DF_PATHARENA)
117
118 dw("=== NISHI DUPFUNC -- function bodies written more than once ===\n" as *u8)
119 dw("root=" as *u8); dw(root)
120 dw(" files=" as *u8); dn(st[0])
121 dw(" min body bytes=" as *u8); dn(minbody)
122 dw(" caps: " as *u8); dn(DF_MAXFUNCS); dw("/organ " as *u8); dn(DF_MAXTOTAL)
123 dw(" total (DECLARED)\n" as *u8)
124
125 let hashes: *i64 = sys_mmap(DF_MAXTOTAL * 8) as *i64
126 let sizes: *i64 = sys_mmap(DF_MAXTOTAL * 8) as *i64
127 let owner: *i64 = sys_mmap(DF_MAXTOTAL * 8) as *i64
128 var nf: i64 = 0
129
130 let src: *u8 = sys_mmap(GL_FILECAP)
131 let strip: *u8 = sys_mmap(GL_FILECAP)
132 let norm: *u8 = sys_mmap(GL_FILECAP)
133 let names: *u8 = sys_mmap(DF_MAXFUNCS * 96)
134 let offs: *i64 = sys_mmap(DF_MAXFUNCS * 8) as *i64
135 let lens: *i64 = sys_mmap(DF_MAXFUNCS * 8) as *i64
136 let spans: *i64 = sys_mmap(DF_MAXFUNCS * 8) as *i64
137
138 var v: i64 = 0
139 while v < g.node_count {
140 if pathoff[v] > 0 {
141 if nf < DF_MAXTOTAL - DF_MAXFUNCS {
142 let fp: *u8 = ((patharena as i64) + pathoff[v] - 1) as *u8
143 let n: i64 = gl_read(fp, src, GL_FILECAP)
144 if n > 0 {
145 let sn: i64 = gl_strip_comments(src, n, strip)
146 let fc: i64 = gl_extract_funcs(strip, sn, names, offs, lens, spans, DF_MAXFUNCS)
147 var f: i64 = 0
148 while f < fc {
149 var e: i64 = sn
150 if f + 1 < fc { e = spans[f + 1] }
151 let bs: i64 = spans[f]
152 if e > bs {
153 // normalise: skip the signature up to '{', then
154 // collapse whitespace runs to one space
155 // ⚠NishiLang has no break, and the first cut of
156 // this loop escaped by assigning the loop variable
157 // (p = e) -- which DESTROYED the position it had
158 // just found, so every body came out empty and the
159 // whole scan reported 0 functions and verdict=GREEN
160 // over 17000 files. A tool that reports nothing is
161 // indistinguishable from a clean tree, which is the
162 // worst failure a detector has. LATCH the position
163 // in its own variable and let the loop run out.
164 var p: i64 = bs
165 var bodystart: i64 = 0 - 1
166 while p < e {
167 if bodystart < 0 { if strip[p] == (123 as u8) { bodystart = p + 1 } }
168 p = p + 1
169 }
170 if bodystart < 0 { bodystart = e }
171 var w: i64 = 0
172 var q: i64 = bodystart
173 var ws: i64 = 0
174 while q < e {
175 let c: i64 = strip[q] as i64
176 var isws: i64 = 0
177 if c == 32 { isws = 1 }
178 if c == 9 { isws = 1 }
179 if c == 10 { isws = 1 }
180 if c == 13 { isws = 1 }
181 if isws == 1 {
182 if ws == 0 { if w < GL_FILECAP { norm[w] = 32 as u8; w = w + 1 } ws = 1 }
183 } else {
184 if w < GL_FILECAP { norm[w] = strip[q]; w = w + 1 }
185 ws = 0
186 }
187 q = q + 1
188 }
189 if w >= minbody {
190 hashes[nf] = df_hash(norm, w)
191 sizes[nf] = w
192 owner[nf] = v
193 nf = nf + 1
194 }
195 }
196 f = f + 1
197 }
198 }
199 }
200 }
201 v = v + 1
202 }
203
204 dw("functions measured (body >= min): " as *u8); dn(nf); dw("\n" as *u8)
205
206 // Group by hash. O(n^2) would be 1e10 here, so sort indices by hash
207 // first with a simple insertion pass over a bucketed table instead:
208 // bucket by hash modulo a large prime, then compare within buckets.
209 let BK: i64 = DF_MAGIC_262144
210 let head: *i64 = sys_mmap(BK * 8) as *i64
211 let next: *i64 = sys_mmap(DF_MAXTOTAL * 8) as *i64
212 var b: i64 = 0
213 while b < BK { head[b] = 0 - 1; b = b + 1 }
214 var i2: i64 = 0
215 while i2 < nf {
216 var hh: i64 = hashes[i2]
217 if hh < 0 { hh = 0 - hh }
218 let slot: i64 = hh % BK
219 next[i2] = head[slot]
220 head[slot] = i2
221 i2 = i2 + 1
222 }
223
224 let bk_sev: *i64 = sys_mmap((topk + 2) * 8) as *i64
225 let bk_rep: *i64 = sys_mmap((topk + 2) * 8) as *i64
226 let bk_cnt: *i64 = sys_mmap((topk + 2) * 8) as *i64
227 let bk_sz: *i64 = sys_mmap((topk + 2) * 8) as *i64
228 var bn: i64 = 0
229 var groups: i64 = 0
230 var dupfuncs: i64 = 0
231 var wasted: i64 = 0
232 var g_gate: i64 = 0
233 var g_prod: i64 = 0
234 var g_mixed: i64 = 0
235 var c_prod: i64 = 0
236 var b_prod: i64 = 0
237
238 let done: *u8 = sys_mmap(DF_MAXTOTAL + 8)
239 var d0: i64 = 0
240 while d0 < nf { done[d0] = 0 as u8; d0 = d0 + 1 }
241
242 var i3: i64 = 0
243 while i3 < nf {
244 if done[i3] == (0 as u8) {
245 var hh2: i64 = hashes[i3]
246 if hh2 < 0 { hh2 = 0 - hh2 }
247 let slot2: i64 = hh2 % BK
248 var cnt: i64 = 0
249 var p2: i64 = head[slot2]
250 while p2 >= 0 {
251 if hashes[p2] == hashes[i3] {
252 if sizes[p2] == sizes[i3] {
253 done[p2] = 1 as u8
254 cnt = cnt + 1
255 }
256 }
257 p2 = next[p2]
258 }
259 if cnt > 1 {
260 // ★CLASSIFY THE GROUP BEFORE COUNTING IT. A group whose copies
261 // all live in gate files is usually a DELIBERATE independent
262 // re-derivation -- this tree keeps those on purpose, because a
263 // test that calls the code under test can only confirm
264 // self-consistency. A group entirely inside product code has
265 // no such defence and is unambiguous debt. Reporting one
266 // number for both makes the headline an upper bound that
267 // nobody can act on, which is what the first version did.
268 var gatecopies: i64 = 0
269 var p4: i64 = head[slot2]
270 while p4 >= 0 {
271 if hashes[p4] == hashes[i3] {
272 if sizes[p4] == sizes[i3] {
273 let nmc: *u8 = ((g.arena as i64) + g.node_off[owner[p4]]) as *u8
274 if gl_is_gate_name(nmc, gl_len(nmc)) == 1 { gatecopies = gatecopies + 1 }
275 }
276 }
277 p4 = next[p4]
278 }
279 var cls: i64 = 2
280 if gatecopies == cnt { cls = 0 }
281 if gatecopies == 0 { cls = 1 }
282 groups = groups + 1
283 if cls == 0 { g_gate = g_gate + 1 }
284 if cls == 1 { g_prod = g_prod + 1; c_prod = c_prod + (cnt - 1) }
285 if cls == 2 { g_mixed = g_mixed + 1 }
286 dupfuncs = dupfuncs + (cnt - 1)
287 let sev0: i64 = (cnt - 1) * sizes[i3]
288 wasted = wasted + sev0
289 // Only PRODUCT-ONLY groups compete for the board.
290 var sev: i64 = 0
291 if cls == 1 { sev = sev0 }
292 if cls == 1 { b_prod = b_prod + sev0 }
293 var start: i64 = 0 - 1
294 if sev > 0 {
295 if bn < topk {
296 start = bn
297 bk_sev[bn] = sev; bk_rep[bn] = i3; bk_cnt[bn] = cnt; bk_sz[bn] = sizes[i3]
298 bn = bn + 1
299 }
300 if bn >= topk {
301 if start < 0 {
302 if sev > bk_sev[topk - 1] {
303 start = topk - 1
304 bk_sev[start] = sev; bk_rep[start] = i3; bk_cnt[start] = cnt; bk_sz[start] = sizes[i3]
305 }
306 }
307 }
308 }
309 var q2: i64 = start
310 while q2 > 0 {
311 if bk_sev[q2 - 1] < bk_sev[q2] {
312 let t1: i64 = bk_sev[q2-1]; bk_sev[q2-1] = bk_sev[q2]; bk_sev[q2] = t1
313 let t2: i64 = bk_rep[q2-1]; bk_rep[q2-1] = bk_rep[q2]; bk_rep[q2] = t2
314 let t3: i64 = bk_cnt[q2-1]; bk_cnt[q2-1] = bk_cnt[q2]; bk_cnt[q2] = t3
315 let t4: i64 = bk_sz[q2-1]; bk_sz[q2-1] = bk_sz[q2]; bk_sz[q2] = t4
316 q2 = q2 - 1
317 } else { q2 = 0 }
318 }
319 }
320 }
321 i3 = i3 + 1
322 }
323
324 dw("duplicate GROUPS=" as *u8); dn(groups)
325 dw(" redundant copies=" as *u8); dn(dupfuncs)
326 dw(" bytes duplicated=" as *u8); dn(wasted)
327 dw("
328 split: gate-only=" as *u8); dn(g_gate)
329 dw(" (deliberate re-derivations, kept on purpose) mixed=" as *u8); dn(g_mixed)
330 dw("
331 ** PRODUCT-ONLY groups=" as *u8); dn(g_prod)
332 dw(" copies=" as *u8); dn(c_prod)
333 dw(" bytes=" as *u8); dn(b_prod)
334 dw(" <== the actionable debt **
335" as *u8)
336 dw("\n-- ranked by (copies-1) x body size; top " as *u8); dn(bn)
337 dw(" (bound DECLARED) --\n" as *u8)
338 var r: i64 = 0
339 while r < bn {
340 let rep: i64 = bk_rep[r]
341 dw(" cost=" as *u8); dn(bk_sev[r])
342 dw(" copies=" as *u8); dn(bk_cnt[r])
343 dw(" body=" as *u8); dn(bk_sz[r])
344 dw("B in: " as *u8)
345 // name every file holding this body
346 var hh3: i64 = hashes[rep]
347 if hh3 < 0 { hh3 = 0 - hh3 }
348 var p3: i64 = head[hh3 % BK]
349 var shown: i64 = 0
350 while p3 >= 0 {
351 if hashes[p3] == hashes[rep] {
352 if sizes[p3] == sizes[rep] {
353 if shown < 6 {
354 let nm: *u8 = ((g.arena as i64) + g.node_off[owner[p3]]) as *u8
355 dw(nm); dw(" " as *u8)
356 shown = shown + 1
357 }
358 }
359 }
360 p3 = next[p3]
361 }
362 dw("\n" as *u8)
363 r = r + 1
364 }
365 dw("\nNOTE: identical bodies are CANDIDATES, not automatically defects.\n" as *u8)
366 dw(" This tree deliberately keeps independent re-derivations inside\n" as *u8)
367 dw(" gates -- a test that calls the code under test can only confirm\n" as *u8)
368 dw(" self-consistency. Confirm by reading before removing anything.\n" as *u8)
369 if groups == 0 { dw("verdict=GREEN\n" as *u8); sys_exit(0); return 0 }
370 dw("verdict=CANDIDATES\n" as *u8)
371 sys_exit(0)
372 return 0
373}