code wiki / (root) / dom.nx

dom.nx source

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