code wiki / (root) / nx_dom.nx

nx_dom.nx source

↩ module page · 432 lines · 16511 B

1// dom.nx -- Cooper-Harvey-Kennedy dominance, in NishiLang. 2// 3// Ports the algorithm in dom.c to NishiLang. This is the first 4// module of the self-hosted compiler: pure graph operations on a 5// CFG, no dependency on ir.c's types. Future integration wraps this 6// so the rest of the compiler can call it. 7// 8// Algorithm summary: 9// 1. Reverse-postorder DFS from entry -> rpo[], rpo_num[]. 10// 2. Iterative dominators until fixed point (CHK 2001). 11// 3. Dominance frontier via Cytron's closed-form walk. 12// 4. Dominator-tree children table for pre-order walks. 13 14// ---- runtime dependencies (inline copies for now) ---- 15 16// ---- CFG representation ---- 17// 18// A miniature CFG for this module. Real integration maps each nxc2 19// BasicBlock to one of these via id. Each Block holds succ / pred 20// index lists as offsets into a side array. 21 22// nx_safety_envelope: 23// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 24// sil_target: SIL1 25// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 26// verdict: NOT_YET_EVALUATED 27 28import "nx_syscalls.nx" 29struct Block { 30 id: i64, 31 n_succs: i64, 32 succ0: i64, // first two succs inline; wider CFGs would use a heap list 33 succ1: i64, 34 n_preds: i64, 35 pred0: i64, 36 pred1: i64, 37 pred2: i64, 38 rpo_num: i64, 39 idom: i64, // index of immediate dominator block, or -1 for entry 40} 41 42// ---- DFS for RPO ---- 43// 44// Visit `b` and recursively visit every successor reachable from it, 45// assigning post-order indices into `post_order` via `*post_idx`. 46 47func rpo_dfs(blocks: *Block, b: i64, 48 visited: *u8, post_order: *i64, post_idx: *i64) -> i64 { 49 if visited[b] != 0 { return 0 } 50 visited[b] = 1 51 let base: i64 = blocks as i64 52 let bb: *Block = (base + b * 80) as *Block 53 let n: i64 = bb.n_succs 54 if n > 0 { rpo_dfs(blocks, bb.succ0, visited, post_order, post_idx) } 55 if n > 1 { rpo_dfs(blocks, bb.succ1, visited, post_order, post_idx) } 56 post_order[*post_idx] = b 57 *post_idx = *post_idx + 1 58 return 0 59} 60 61// ---- intersect two dom chains ---- 62// 63// Walk `b1` and `b2` up their idom links until they meet. Both 64// walks terminate because each step increases the meeting block's 65// rpo_num upper bound. 66 67func dom_intersect(blocks: *Block, b1: i64, b2: i64) -> i64 { 68 let base: i64 = blocks as i64 69 var f1: i64 = b1 70 var f2: i64 = b2 71 while f1 != f2 { 72 while f1 > f2 { 73 let b: *Block = (base + f1 * 80) as *Block 74 f1 = b.idom 75 if f1 < 0 { return -1 } 76 } 77 while f2 > f1 { 78 let b: *Block = (base + f2 * 80) as *Block 79 f2 = b.idom 80 if f2 < 0 { return -1 } 81 } 82 } 83 return f1 84} 85 86// ---- main dominator computation ---- 87// 88// Mutates each Block's idom field to the index of its immediate 89// dominator. `entry` = index of the entry block (usually 0). 90// Assumes all blocks are reachable from entry. 91 92func dom_compute(blocks: *Block, n_blocks: i64, entry: i64) -> i64 { 93 // --- DFS to build post-order then RPO numbering --- 94 let visited: *u8 = sys_mmap(n_blocks + 8) 95 let post_raw: *u8 = sys_mmap(n_blocks * 8 + 16) 96 let post: *i64 = post_raw as *i64 97 let idx_raw: *u8 = sys_mmap(8) 98 let idx: *i64 = idx_raw as *i64 99 *idx = 0 100 rpo_dfs(blocks, entry, visited, post, idx) 101 let n_reached: i64 = *idx 102 103 let base: i64 = blocks as i64 104 105 // Assign rpo_num; entry gets 0, next block 1, ... 106 var i: i64 = 0 107 while i < n_reached { 108 let rpo_i: i64 = n_reached - 1 - i 109 let b: *Block = (base + post[i] * 80) as *Block 110 b.rpo_num = rpo_i 111 i = i + 1 112 } 113 114 // Initialize idom: entry dominates itself; everyone else -1. 115 var j: i64 = 0 116 while j < n_blocks { 117 let b: *Block = (base + j * 80) as *Block 118 b.idom = -1 119 j = j + 1 120 } 121 let eb: *Block = (base + entry * 80) as *Block 122 eb.idom = entry 123 124 // --- CHK iterative fixed point --- 125 var changed: i64 = 1 126 while changed { 127 changed = 0 128 var ri: i64 = 1 129 while ri < n_reached { 130 // Which block has rpo_num == ri? Simplest: linear scan. 131 var bi: i64 = 0 132 var found: i64 = -1 133 while bi < n_blocks { 134 let b: *Block = (base + bi * 80) as *Block 135 if b.rpo_num == ri { found = bi } 136 bi = bi + 1 137 } 138 if found < 0 { ri = ri + 1; continue } 139 let cur: *Block = (base + found * 80) as *Block 140 // Iterate preds; compute intersection. 141 var new_idom: i64 = -1 142 var k: i64 = 0 143 let np: i64 = cur.n_preds 144 while k < np { 145 var pid: i64 = -1 146 if k == 0 { pid = cur.pred0 } 147 if k == 1 { pid = cur.pred1 } 148 if k == 2 { pid = cur.pred2 } 149 if pid >= 0 { 150 let p: *Block = (base + pid * 80) as *Block 151 if p.idom >= 0 { 152 if new_idom < 0 { 153 new_idom = pid 154 } 155 if new_idom >= 0 { 156 if new_idom != pid { 157 new_idom = dom_intersect(blocks, pid, new_idom) 158 } 159 } 160 } 161 } 162 k = k + 1 163 } 164 if new_idom >= 0 { 165 if cur.idom != new_idom { 166 cur.idom = new_idom 167 changed = 1 168 } 169 } 170 ri = ri + 1 171 } 172 } 173 return n_reached 174} 175 176// ---- dominance frontier (Cytron 1991 closed-form) ---- 177// 178// For every join block b (|preds(b)| >= 2), walk each pred's dom 179// chain up until it reaches idom(b), adding b to each walked block's 180// DF. The algorithm is linear in the size of the dominator tree 181// times the number of edges, and writes directly into a flat 182// (df_starts, df_flat) layout so callers can iterate without 183// allocation. 184// 185// Outputs: 186// df_counts[b] = number of blocks for which b is in the frontier 187// df_starts[b] = start index into df_flat where b's DF is listed 188// df_flat[k] = block id in b's frontier 189// df_flat_used = total entries written 190// 191// Caller sizes df_flat >= worst-case sum of (2 * |preds|) across 192// blocks; MAX_BLOCKS * MAX_BLOCKS is an easy safe upper bound. 193 194func df_compute(blocks: *Block, n_blocks: i64, 195 df_counts: *i64, df_starts: *i64, 196 df_flat: *i64, df_flat_cap: i64) -> i64 { 197 let base: i64 = blocks as i64 198 // Pass 1: count DF sizes per block. 199 var b: i64 = 0 200 while b < n_blocks { 201 df_counts[b] = 0 202 b = b + 1 203 } 204 var bi: i64 = 0 205 while bi < n_blocks { 206 let bb: *Block = (base + bi * 80) as *Block 207 let np: i64 = bb.n_preds 208 if np >= 2 { 209 let idom_b: i64 = bb.idom 210 var k: i64 = 0 211 while k < np { 212 var pid: i64 = -1 213 if k == 0 { pid = bb.pred0 } 214 if k == 1 { pid = bb.pred1 } 215 if k == 2 { pid = bb.pred2 } 216 if pid >= 0 { 217 var runner: i64 = pid 218 while runner != idom_b { 219 df_counts[runner] = df_counts[runner] + 1 220 let rb: *Block = (base + runner * 80) as *Block 221 runner = rb.idom 222 if runner < 0 { runner = idom_b } 223 } 224 } 225 k = k + 1 226 } 227 } 228 bi = bi + 1 229 } 230 // Pass 2: prefix sum -> df_starts, clear df_counts for re-fill. 231 var acc: i64 = 0 232 var i: i64 = 0 233 while i < n_blocks { 234 df_starts[i] = acc 235 acc = acc + df_counts[i] 236 df_counts[i] = 0 237 i = i + 1 238 } 239 if acc > df_flat_cap { return -1 } 240 // Pass 3: fill df_flat. 241 var bi2: i64 = 0 242 while bi2 < n_blocks { 243 let bb: *Block = (base + bi2 * 80) as *Block 244 let np: i64 = bb.n_preds 245 if np >= 2 { 246 let idom_b: i64 = bb.idom 247 var k: i64 = 0 248 while k < np { 249 var pid: i64 = -1 250 if k == 0 { pid = bb.pred0 } 251 if k == 1 { pid = bb.pred1 } 252 if k == 2 { pid = bb.pred2 } 253 if pid >= 0 { 254 var runner: i64 = pid 255 while runner != idom_b { 256 let slot: i64 = df_starts[runner] + df_counts[runner] 257 df_flat[slot] = bi2 258 df_counts[runner] = df_counts[runner] + 1 259 let rb: *Block = (base + runner * 80) as *Block 260 runner = rb.idom 261 if runner < 0 { runner = idom_b } 262 } 263 } 264 k = k + 1 265 } 266 } 267 bi2 = bi2 + 1 268 } 269 return acc 270} 271 272// ---- natural-loop detection ---- 273// 274// A back edge is (n -> h) where h dominates n. The natural loop 275// of the back edge is {h} union all blocks from which n is reachable 276// without going through h (Allen-Cocke 1970). 277// 278// Output layout mirrors df_*: loop_counts / loop_starts / loop_flat, 279// indexed by header block b. Headers with no back-edges get count=0. 280 281func loop_detect(blocks: *Block, n_blocks: i64, 282 loop_counts: *i64, loop_starts: *i64, 283 loop_flat: *i64, loop_flat_cap: i64) -> i64 { 284 let base: i64 = blocks as i64 285 // First identify headers and back-edges by scanning succs. 286 // For each edge n -> h, check whether h dominates n via the 287 // idom chain from n. 288 var h: i64 = 0 289 while h < n_blocks { 290 loop_counts[h] = 0 291 h = h + 1 292 } 293 // Per-header worklist: simple re-discovery on each call is fine 294 // for small CFGs. For each (n, h) back-edge, mark h + every 295 // predecessor chain that reaches n without passing through h. 296 // 297 // We first count, then fill. 298 var pass: i64 = 0 299 while pass < 2 { 300 var n_idx: i64 = 0 301 while n_idx < n_blocks { 302 let nb: *Block = (base + n_idx * 80) as *Block 303 let ns: i64 = nb.n_succs 304 var si: i64 = 0 305 while si < ns { 306 var tgt: i64 = -1 307 if si == 0 { tgt = nb.succ0 } 308 if si == 1 { tgt = nb.succ1 } 309 if tgt >= 0 { 310 // Does tgt dominate n_idx? 311 var walker: i64 = n_idx 312 var is_back: i64 = 0 313 while walker >= 0 { 314 if walker == tgt { is_back = 1; walker = -1 } else { 315 let wb: *Block = (base + walker * 80) as *Block 316 if wb.idom == walker { walker = -1 } else { 317 walker = wb.idom 318 } 319 } 320 } 321 if is_back == 1 { 322 // Found back-edge n_idx -> tgt. Enumerate 323 // loop body: BFS backwards from n_idx, 324 // stopping at tgt. We record each member 325 // exactly once by checking loop_flat in the 326 // current header's slice. 327 if pass == 0 { 328 // Counting pass: conservative upper 329 // bound = every block except tgt's 330 // non-members -- but we need exactness 331 // for the flat layout. Use visited 332 // bitmap. 333 let vis: *u8 = sys_mmap(n_blocks + 8) 334 vis[tgt] = 1 335 // count header itself 336 loop_counts[tgt] = loop_counts[tgt] + 1 337 // reverse-walk queue implemented on stack 338 let q_raw: *u8 = sys_mmap(n_blocks * 8 + 16) 339 let q: *i64 = q_raw as *i64 340 q[0] = n_idx 341 var qh: i64 = 0 342 var qt: i64 = 1 343 while qh < qt { 344 let cur: i64 = q[qh] 345 qh = qh + 1 346 if vis[cur] == 0 { 347 vis[cur] = 1 348 loop_counts[tgt] = loop_counts[tgt] + 1 349 // enqueue preds 350 let cb: *Block = (base + cur * 80) as *Block 351 let npp: i64 = cb.n_preds 352 var pk: i64 = 0 353 while pk < npp { 354 var pid: i64 = -1 355 if pk == 0 { pid = cb.pred0 } 356 if pk == 1 { pid = cb.pred1 } 357 if pk == 2 { pid = cb.pred2 } 358 if pid >= 0 { 359 if vis[pid] == 0 { 360 q[qt] = pid 361 qt = qt + 1 362 } 363 } 364 pk = pk + 1 365 } 366 } 367 } 368 } else { 369 // Filling pass -- write into loop_flat. 370 let start: i64 = loop_starts[tgt] 371 var fill: i64 = 0 372 let vis2: *u8 = sys_mmap(n_blocks + 8) 373 vis2[tgt] = 1 374 loop_flat[start + fill] = tgt 375 fill = fill + 1 376 let q2_raw: *u8 = sys_mmap(n_blocks * 8 + 16) 377 let q2: *i64 = q2_raw as *i64 378 q2[0] = n_idx 379 var qh2: i64 = 0 380 var qt2: i64 = 1 381 while qh2 < qt2 { 382 let cur: i64 = q2[qh2] 383 qh2 = qh2 + 1 384 if vis2[cur] == 0 { 385 vis2[cur] = 1 386 loop_flat[start + fill] = cur 387 fill = fill + 1 388 let cb: *Block = (base + cur * 80) as *Block 389 let npp: i64 = cb.n_preds 390 var pk: i64 = 0 391 while pk < npp { 392 var pid: i64 = -1 393 if pk == 0 { pid = cb.pred0 } 394 if pk == 1 { pid = cb.pred1 } 395 if pk == 2 { pid = cb.pred2 } 396 if pid >= 0 { 397 if vis2[pid] == 0 { 398 q2[qt2] = pid 399 qt2 = qt2 + 1 400 } 401 } 402 pk = pk + 1 403 } 404 } 405 } 406 } 407 } 408 } 409 si = si + 1 410 } 411 n_idx = n_idx + 1 412 } 413 if pass == 0 { 414 // After counting, prefix-sum loop_starts and clear counts 415 // for a re-count while filling (counts now double as 416 // fill-offset by construction because we write exactly 417 // loop_counts[tgt] entries per header). 418 var acc: i64 = 0 419 var hh: i64 = 0 420 while hh < n_blocks { 421 loop_starts[hh] = acc 422 acc = acc + loop_counts[hh] 423 hh = hh + 1 424 } 425 if acc > loop_flat_cap { return -1 } 426 } 427 pass = pass + 1 428 } 429 return 0 430} 431 432// Library only; self-test lives in dom_test.nx.