code wiki / (root) / nx_bck_elide.nx

nx_bck_elide.nx source

↩ module page · 994 lines · 47244 B

1// nx_bck_elide.nx -- LN7 SOUND BOUNDS-CHECK ELISION (bck_elide_dominated). 2// 3// WHAT THIS IS. The consumer nx_opt.nx's own VRA header has NAMED and never built since 4// v0.0.1: "opt_bounds_check_elim: drop array bounds checks when index is provably within 5// range". This is that pass, and it is deliberately NOT the range-based version that comment 6// imagines -- it is the DOMINANCE version, which needs no range lattice and whose soundness 7// argument is short enough to be checked by eye. 8// 9// THE THEOREM. emit_bounds_check_v (nx_parse.nx) emits, for one access: 10// 11// b: c1 = idx >=s 0 ; br_cond c1 -> lo, fail 12// lo: c2 = idx <s len ; br_cond c2 -> ok, fail 13// fail: write(2,msg) ; exit_group(71) ; br ok 14// ok: <the access> 15// 16// If a site S1 and a site S2 carry the SAME (idx value id, len constant) and S1's `ok` block 17// DOMINATES S2's entry block, then S2's comparisons can only ever answer true, so making S2's 18// two branches unconditional changes no observable behaviour. Why the facts still hold at S2: 19// values here are SSA, so an id names one immutable quantity for the whole function; dominance 20// says EVERY path from entry to S2 runs through S1's ok; and S1's ok is only entered with the 21// facts established (see the four structural preconditions below). Nothing about the distance 22// between the two sites, or what sits between them, can weaken that -- there is no store that 23// can retarget an SSA id. 24// 25// WRONG IN THE DIRECTION OF KEEPING THE CHECK, BY CONSTRUCTION. Every predicate below is a 26// REASON TO ELIDE; the default is to keep. An unrecognised shape, an unreadable value, a 27// truncated CFG, a length that is not a constant -- all fall through to "keep the check". An 28// unsound elision is a silent memory-safety hole: it deletes the check that would have caught a 29// real overrun, and nothing anywhere reports it. So this file has no "probably" cases. 30// 31// THE FOUR STRUCTURAL PRECONDITIONS a site must meet before it may JUSTIFY an elision -- each 32// one closes a way for `ok` to be entered WITHOUT the facts: 33// 34// (1) `lo` has exactly ONE predecessor, and it is `b`. Otherwise control could enter lo from 35// elsewhere and take the true edge to ok having proven `idx < len` but never `idx >= 0`. 36// (2) every predecessor of `ok` is `lo` or `fail`. Any third edge into ok reaches it with no 37// facts at all. A pred count above the pred slots the struct actually has means the edge 38// list is TRUNCATED and cannot be enumerated -- that is a refusal, not a pass. 39// (3) `fail` provably does not return: it must contain a syscall whose number operand is the 40// exit_group constant. This is the precondition it would be easiest to assume instead of 41// check. nx_parse.nx gives fail a `br ok` terminator purely so every block ends in a 42// terminator, and its own comment calls that edge unreachable BECAUSE exit_group never 43// returns. So `ok` genuinely has two predecessors and "control reached ok" does not 44// SYNTACTICALLY imply the comparisons held -- it implies it only via the exit. This 45// function proves that structurally rather than trusting the comment. 46// (4) both branches must share ONE fail block, and both comparisons must name the SAME idx 47// value id -- otherwise the pair is not one check. 48// 49// DECLARED IMPRECISION (each sound in the do-nothing direction, and each stated so the next 50// reader does not mistake a floor for a value): 51// * THE LENGTH MUST BE A COMPILE-TIME CONSTANT. That covers fixed arrays [N]T and the LN3 52// provenance extents. A SLICE's length is LOADED from its header, so two sites agree only 53// if some earlier pass unified those loads -- and taking that on trust would make this 54// pass's memory safety depend on CSE's alias reasoning. A performance pass may inherit 55// another pass's imprecision; a SAFETY pass may not inherit another pass's correctness. 56// Slices therefore keep every check, and that is a deliberate cost, not an oversight. 57// * The index must be the SAME SSA value id. Two separate loads of the same `var` are two 58// ids and are not matched, even though they usually hold the same number. Proving that 59// needs alias analysis this pass does not have. 60// * Only re-checks are removed. The FIRST check of any (idx, len) is always kept, so no 61// access is ever left unchecked on its first reach. 62// 63// DECLARED MODE, OFF BY DEFAULT (--bckelide), the LN3/LN1 contract exactly: with the flag off 64// nothing below emits or rewrites, so every default build is byte-identical BY CONSTRUCTION and 65// the corpus keeps building unchanged. On the most dangerous rung on the board that is the 66// only responsible default; the per-class ratchet flips it after a clean corpus census. 67// 68// RESOURCE ENVELOPE. Zero allocation for any function with fewer than two check sites (the 69// overwhelming majority, and the site count is taken by a scan that allocates nothing). Above 70// that: six i64 arrays sized to the SITE count -- not the block count -- plus one 64-byte 71// scratch and the 128-byte dominator handle, all from the compiler's own short-lived process. 72// Nothing is retained across calls except the counters, which are scalars. 73// 74// license_tier: ORIGINAL No hw writes (Rule 26). 75import "nx_types.nx" 76import "nx_ir.nx" 77import "nx_dom_fn.nx" 78 79// DERIVED, NOT CHOSEN: struct BasicBlock (nx_types.nx) carries pred0, pred1, pred2 and nothing 80// else, so a block reporting more predecessors than this has edges the CFG never recorded. 81// opt_licm declines such functions for the same reason and cites the same measurement 82// (T#ir-pred-list-truncation): dominance over a graph that is missing edges is a true statement 83// about a DIFFERENT graph. For LICM that costs a missed hoist; here it would delete a check. 84const BCK_PRED_SLOTS: i64 = 3 85 86// Scratch slot layout for bck_match_site's out parameter. Named so the call sites read as 87// fields rather than as numbers. 88const BCK_O_LO: i64 = 0 89const BCK_O_OK: i64 = 1 90const BCK_O_IDX: i64 = 2 91const BCK_O_LEN: i64 = 3 92const BCK_SCRATCH_BYTES: i64 = 64 93 94// ---- WHY A SITE WAS DECLINED -------------------------------------------------------------- 95// A REFUSING RUN AND A RUN THAT NEVER HAPPENED ARE INDISTINGUISHABLE FROM OUTSIDE, and a pass 96// that declines everything silently reports the same zero as a pass that was never reached. 97// Every rejection below is therefore attributed to a NAMED reason and tallied, and the tally is 98// printed (stderr only, so not one byte of emitted assembly moves) whenever the declared mode 99// is live and the function held at least one candidate. This is what turned a silent zero into 100// a diagnosis the first time this pass failed to fire. 101const BCK_R_NOT_BRCOND: i64 = 0 102const BCK_R_GE_NOT_FOUND: i64 = 1 103const BCK_R_NOT_GE: i64 = 2 104const BCK_R_NOT_ZERO: i64 = 3 105const BCK_R_NO_LO: i64 = 4 106const BCK_R_LO_PREDS: i64 = 5 107const BCK_R_LO_NOT_BRCOND: i64 = 6 108const BCK_R_FAIL_MISMATCH: i64 = 7 109const BCK_R_LT_NOT_FOUND: i64 = 8 110const BCK_R_NOT_LT: i64 = 9 111const BCK_R_IDX_MISMATCH: i64 = 10 112const BCK_R_NO_FAIL: i64 = 11 113const BCK_R_FAIL_NOT_TERM: i64 = 12 114const BCK_R_NO_OK: i64 = 13 115const BCK_R_OK_PREDS: i64 = 14 116const BCK_R_MATCHED: i64 = 15 117// 16..25 are the LN7b induction-guarded reasons, declared with that pass at the tail of this file. 118const BCK_R_N: i64 = 26 119 120static g_bckelide_live: i64 121// The exit_group syscall number is PASSED IN, never copied here. It belongs to the trap 122// emitter (nx_parse.nx NX_RV64_SYS_EXIT_GROUP); a second spelling of it in this file would be a 123// constant that can drift out of agreement with the very instruction sequence it recognises, 124// and the failure mode of that drift is precondition (3) silently never matching -- which reads 125// as "the pass found nothing to do" rather than as a defect. 126static g_bck_exitgrp: i64 127 128// Counters. Deliberately NOT printed from the compile path: this pass must be observable 129// without changing a single byte of what the compiler emits, and the gate's oracle is the 130// emitted assembly itself. A pass that narrates is a pass whose narration can disagree with 131// what it did. 132static g_bck_sites: i64 133static g_bck_cand: i64 134static g_bck_elided: i64 135static g_bck_ref_nolen: i64 136static g_bck_ref_nodom: i64 137static g_bck_decl_preds: i64 138 139func nx_bckelide_set(v: i64, exit_group_nr: i64) -> i64 { 140 g_bckelide_live = v 141 g_bck_exitgrp = exit_group_nr 142 return 0 143} 144func bck_elide_live() -> i64 { return g_bckelide_live } 145func bck_exitgrp() -> i64 { return g_bck_exitgrp } 146func bck_stat_sites() -> i64 { return g_bck_sites } 147func bck_stat_elided() -> i64 { return g_bck_elided } 148func bck_stat_ref_nolen() -> i64 { return g_bck_ref_nolen } 149func bck_stat_ref_nodom() -> i64 { return g_bck_ref_nodom } 150func bck_stat_decl_preds() -> i64 { return g_bck_decl_preds } 151 152// Lazily allocated reason tally -- a static holds the pointer, so a compiler run that never 153// enables the mode allocates nothing at all. 154static g_bck_reasons: i64 155func bck_reasons() -> *i64 { 156 if g_bck_reasons == 0 { 157 let r: *i64 = sys_mmap(BCK_R_N * 8 + 64) as *i64 158 var i: i64 = 0 159 while i < BCK_R_N { r[i] = 0; i = i + 1 } 160 g_bck_reasons = r as i64 161 } 162 return g_bck_reasons as *i64 163} 164func bck_reason_bump(code: i64) -> i64 { 165 let r: *i64 = bck_reasons() 166 r[code] = r[code] + 1 167 return 0 168} 169 170// Decimal to STDERR. A local printer rather than an import: nx_opt.nx -- this file's only 171// consumer -- does not import the parser's diagnostic layer, and pulling one in for a status 172// line would couple the optimizer to it. stderr only, so nothing here can move a byte of the 173// emitted assembly, which is what the gate actually measures. 174func bck_eputs(s: *u8) -> i64 { 175 var n: i64 = 0 176 while s[n] != (0 as u8) { n = n + 1 } 177 sys_write(2, s, n) 178 return 0 179} 180func bck_eputn(v: i64) -> i64 { 181 let t: *u8 = sys_mmap(BCK_SCRATCH_BYTES) 182 let o: *u8 = sys_mmap(BCK_SCRATCH_BYTES) 183 var m: i64 = v 184 if m < 0 { sys_write(2, "-" as *u8, 1); m = 0 - m } 185 var k: i64 = 0 186 if m == 0 { t[0] = 48 as u8; k = 1 } 187 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 188 var i: i64 = 0 189 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 190 sys_write(2, o, k) 191 sys_munmap(t, BCK_SCRATCH_BYTES) 192 sys_munmap(o, BCK_SCRATCH_BYTES) 193 return 0 194} 195func bck_ekv(label: *u8, v: i64) -> i64 { bck_eputs(label); bck_eputn(v); return 0 } 196 197// Print one reason only when it actually happened -- a report padded with zeroes buries the one 198// number that matters. 199func bck_ereason(label: *u8, code: i64) -> i64 { 200 let r: *i64 = bck_reasons() 201 if r[code] == 0 { return 0 } 202 bck_ekv(label, r[code]) 203 return 0 204} 205 206// Resolve a terminator's block-ID operand to an index in f.blocks. -1 when no such block 207// exists, which every caller treats as "do not touch this site". 208func bck_block_index_by_id(f: *Function, id: i64) -> i64 { 209 var i: i64 = 0 210 while i < f.n_blocks { 211 let b: *BasicBlock = block_at(f, i) 212 if b.id == id { return i } 213 i = i + 1 214 } 215 return 0 - 1 216} 217 218// The instruction in `b` defining value id `vid`, or null. Kept local to the block on purpose: 219// a definition found in some OTHER block would not tell us the comparison is part of THIS site. 220func bck_def_in_block(b: *BasicBlock, vid: i64) -> *Instr { 221 var inst: *Instr = b.head 222 while inst != (0 as *Instr) { 223 if inst.result == vid { return inst } 224 inst = inst.next 225 } 226 return 0 as *Instr 227} 228 229// The instruction defining `vid`: looked for in `b` FIRST (the freshly emitted shape), then 230// anywhere in the function. The function-wide fallback is what lets this pass survive the 231// optimizer rounds that run before it -- CSE and GVN can unify or hoist a comparison so its 232// definition no longer sits in the block that branches on it, and the block-local-only lookup 233// this replaced then reported "no site here" for a site that was plainly there. 234// Sound: values are SSA, so an id names one immutable quantity wherever it is defined, and a 235// use that did not dominate its definition would be malformed IR the backend could not lower. 236func bck_def_of(f: *Function, b: *BasicBlock, vid: i64) -> *Instr { 237 let local: *Instr = bck_def_in_block(b, vid) 238 if local != (0 as *Instr) { return local } 239 var i: i64 = 0 240 while i < f.n_blocks { 241 let ob: *BasicBlock = block_at(f, i) 242 let d: *Instr = bck_def_in_block(ob, vid) 243 if d != (0 as *Instr) { return d } 244 i = i + 1 245 } 246 return 0 as *Instr 247} 248 249// Is value `v` the integer constant `want`? Out-of-range ids answer no rather than reading 250// past the value pool. 251func bck_val_is_const(f: *Function, v: i64, want: i64) -> i64 { 252 if v < 0 { return 0 } 253 if v >= f.n_values { return 0 } 254 let vv: *Value = val_at(f, v) 255 if vv == (0 as *Value) { return 0 } 256 if vv.kind != VK_CONST_INT { return 0 } 257 if vv.const_int != want { return 0 } 258 return 1 259} 260 261// The constant value of `v`; flag[0] is set to 1 only when v really is a constant. The flag is 262// the whole point -- a bare 0 return would be indistinguishable from the constant 0. 263func bck_const_of(f: *Function, v: i64, flag: *i64) -> i64 { 264 flag[0] = 0 265 if v < 0 { return 0 } 266 if v >= f.n_values { return 0 } 267 let vv: *Value = val_at(f, v) 268 if vv == (0 as *Value) { return 0 } 269 if vv.kind != VK_CONST_INT { return 0 } 270 flag[0] = 1 271 return vv.const_int 272} 273 274// PRECONDITION (3). Proven, not assumed -- see the header. 275func bck_fail_is_terminal(f: *Function, fb: *BasicBlock, exit_group_nr: i64) -> i64 { 276 if exit_group_nr <= 0 { return 0 } 277 var inst: *Instr = fb.head 278 while inst != (0 as *Instr) { 279 if inst.op == OP_SYSCALL { 280 if bck_val_is_const(f, inst.op0, exit_group_nr) == 1 { return 1 } 281 } 282 inst = inst.next 283 } 284 return 0 285} 286 287// PRECONDITION (2). A pred count above the slots the struct HAS means the list is truncated, 288// so the honest answer is "cannot enumerate", which is a refusal. 289func bck_ok_preds_clean(ok: *BasicBlock, lo: *BasicBlock, fl: *BasicBlock) -> i64 { 290 if ok.n_preds > BCK_PRED_SLOTS { return 0 } 291 if ok.n_preds < 1 { return 0 } 292 var i: i64 = 0 293 while i < ok.n_preds { 294 var p: *BasicBlock = ok.pred0 295 if i == 1 { p = ok.pred1 } 296 if i == 2 { p = ok.pred2 } 297 if p != lo { if p != fl { return 0 } } 298 i = i + 1 299 } 300 return 1 301} 302 303// PRECONDITION (1). 304func bck_lo_preds_clean(lo: *BasicBlock, b: *BasicBlock) -> i64 { 305 if lo.n_preds != 1 { return 0 } 306 if lo.pred0 != b { return 0 } 307 return 1 308} 309 310// Recognise ONE complete check site rooted at block index bi, writing its parts into out[]. 311// Returns 1 only when all four structural preconditions hold; 0 otherwise, and 0 always means 312// "leave this alone". 313// Decline with a NAMED reason. `tally` is 1 only on the counting scan, so the two passes over 314// the blocks cannot double-count a rejection. 315func bck_decline(code: i64, tally: i64) -> i64 { 316 if tally == 1 { bck_reason_bump(code) } 317 return 0 318} 319 320func bck_match_site(f: *Function, bi: i64, exit_group_nr: i64, out: *i64, tally: i64) -> i64 { 321 let b: *BasicBlock = block_at(f, bi) 322 let t1: *Instr = b.tail 323 if t1 == (0 as *Instr) { return 0 } 324 // Not tallied: most blocks end in a return or a plain branch, so counting them as 325 // "rejections" would bury the reasons that actually distinguish one shape from another. 326 if t1.op != OP_BR_COND { return 0 } 327 if tally == 1 { g_bck_cand = g_bck_cand + 1 } 328 let d1: *Instr = bck_def_of(f, b, t1.op0) 329 if d1 == (0 as *Instr) { return bck_decline(BCK_R_GE_NOT_FOUND, tally) } 330 if d1.op != OP_GE_S { return bck_decline(BCK_R_NOT_GE, tally) } 331 if bck_val_is_const(f, d1.op1, 0) != 1 { return bck_decline(BCK_R_NOT_ZERO, tally) } 332 let idx: i64 = d1.op0 333 let fail_id: i64 = t1.op2 334 335 let lo_i: i64 = bck_block_index_by_id(f, t1.op1) 336 if lo_i < 0 { return bck_decline(BCK_R_NO_LO, tally) } 337 let lo: *BasicBlock = block_at(f, lo_i) 338 if bck_lo_preds_clean(lo, b) != 1 { return bck_decline(BCK_R_LO_PREDS, tally) } 339 let t2: *Instr = lo.tail 340 if t2 == (0 as *Instr) { return bck_decline(BCK_R_LO_NOT_BRCOND, tally) } 341 if t2.op != OP_BR_COND { return bck_decline(BCK_R_LO_NOT_BRCOND, tally) } 342 if t2.op2 != fail_id { return bck_decline(BCK_R_FAIL_MISMATCH, tally) } 343 let d2: *Instr = bck_def_of(f, lo, t2.op0) 344 if d2 == (0 as *Instr) { return bck_decline(BCK_R_LT_NOT_FOUND, tally) } 345 if d2.op != OP_LT_S { return bck_decline(BCK_R_NOT_LT, tally) } 346 if d2.op0 != idx { return bck_decline(BCK_R_IDX_MISMATCH, tally) } 347 348 let fail_i: i64 = bck_block_index_by_id(f, fail_id) 349 if fail_i < 0 { return bck_decline(BCK_R_NO_FAIL, tally) } 350 let fl: *BasicBlock = block_at(f, fail_i) 351 if bck_fail_is_terminal(f, fl, exit_group_nr) != 1 { return bck_decline(BCK_R_FAIL_NOT_TERM, tally) } 352 353 let ok_i: i64 = bck_block_index_by_id(f, t2.op1) 354 if ok_i < 0 { return bck_decline(BCK_R_NO_OK, tally) } 355 let ok: *BasicBlock = block_at(f, ok_i) 356 if bck_ok_preds_clean(ok, lo, fl) != 1 { return bck_decline(BCK_R_OK_PREDS, tally) } 357 358 if tally == 1 { bck_reason_bump(BCK_R_MATCHED) } 359 out[BCK_O_LO] = lo_i 360 out[BCK_O_OK] = ok_i 361 out[BCK_O_IDX] = idx 362 // d2.op1, NOT t2.op1. t2 is the BRANCH, whose op1 is the true-target BLOCK ID (used just 363 // above to find `ok`); the length is the second operand of the COMPARISON. Reading the 364 // branch operand here keyed every site on a block id reinterpreted as a value id, so the 365 // key was a different arbitrary value at each site -- which is why the pass matched sites 366 // correctly and then elided nothing at all. It failed in the safe direction only by luck: 367 // had two of those garbage keys ever collided, this would have elided a check that was 368 // never proven redundant. Found by the r_nonconst_len tally in one run. 369 out[BCK_O_LEN] = d2.op1 370 return 1 371} 372 373// THE REWRITE. Byte-for-byte the shape opt_sccp_branches already uses to make a branch 374// unconditional (op -> OP_BR, op0 = taken target's BLOCK ID, op1/op2 cleared, n_operands 1), so 375// the optimizer keeps exactly one spelling of that operation rather than growing a second. 376// Nothing is deleted here: the two comparisons simply become dead and opt_dce collects them, 377// and the fail block becomes unreachable and opt_sweep_unreachable_function collects that. 378// Deleting them directly would mean duplicating two passes that already do it correctly. 379func bck_rewrite_site(f: *Function, b_i: i64, lo_i: i64, ok_i: i64) -> i64 { 380 let b: *BasicBlock = block_at(f, b_i) 381 let lo: *BasicBlock = block_at(f, lo_i) 382 let ok: *BasicBlock = block_at(f, ok_i) 383 let t1: *Instr = b.tail 384 if t1 == (0 as *Instr) { return 0 } 385 let t2: *Instr = lo.tail 386 if t2 == (0 as *Instr) { return 0 } 387 t1.op = OP_BR 388 t1.op0 = lo.id 389 t1.op1 = 0 390 t1.op2 = 0 391 t1.n_operands = 1 392 t2.op = OP_BR 393 t2.op0 = ok.id 394 t2.op1 = 0 395 t2.op2 = 0 396 t2.n_operands = 1 397 return 1 398} 399 400// THE REFUSAL REPORT, to stderr, once per function that contained at least one conditional 401// branch, and only while the declared mode is live. A pass that matches nothing otherwise 402// prints exactly the zero a pass that was never reached prints -- which is the state this one 403// was in on its first run, and reading this line is what located the cause in a single run 404// instead of a guess. The per-function figures are local; every r_* figure is CUMULATIVE for 405// the whole compile, so the LAST line emitted is the whole-compile summary. 406func bck_report(fn_blocks: i64, cand_here: i64, sites_here: i64, elided_here: i64) -> i64 { 407 if g_bckelide_live != 1 { return 0 } 408 if cand_here == 0 { return 0 } 409 bck_ekv("BCK-ELIDE blocks=" as *u8, fn_blocks) 410 bck_ekv(" cand=" as *u8, cand_here) 411 bck_ekv(" sites=" as *u8, sites_here) 412 bck_ekv(" elided=" as *u8, elided_here) 413 bck_ekv(" cum_sites=" as *u8, g_bck_sites) 414 bck_ekv(" cum_elided=" as *u8, g_bck_elided) 415 bck_ereason(" r_ge_not_found=" as *u8, BCK_R_GE_NOT_FOUND) 416 bck_ereason(" r_not_ge=" as *u8, BCK_R_NOT_GE) 417 bck_ereason(" r_not_zero=" as *u8, BCK_R_NOT_ZERO) 418 bck_ereason(" r_no_lo=" as *u8, BCK_R_NO_LO) 419 bck_ereason(" r_lo_preds=" as *u8, BCK_R_LO_PREDS) 420 bck_ereason(" r_lo_not_brcond=" as *u8, BCK_R_LO_NOT_BRCOND) 421 bck_ereason(" r_fail_mismatch=" as *u8, BCK_R_FAIL_MISMATCH) 422 bck_ereason(" r_lt_not_found=" as *u8, BCK_R_LT_NOT_FOUND) 423 bck_ereason(" r_not_lt=" as *u8, BCK_R_NOT_LT) 424 bck_ereason(" r_idx_mismatch=" as *u8, BCK_R_IDX_MISMATCH) 425 bck_ereason(" r_no_fail=" as *u8, BCK_R_NO_FAIL) 426 bck_ereason(" r_fail_not_terminal=" as *u8, BCK_R_FAIL_NOT_TERM) 427 bck_ereason(" r_no_ok=" as *u8, BCK_R_NO_OK) 428 bck_ereason(" r_ok_preds=" as *u8, BCK_R_OK_PREDS) 429 bck_ereason(" r_matched=" as *u8, BCK_R_MATCHED) 430 bck_ekv(" r_nonconst_len=" as *u8, g_bck_ref_nolen) 431 bck_ekv(" r_no_dominating_twin=" as *u8, g_bck_ref_nodom) 432 bck_ekv(" declined_pred_truncated=" as *u8, g_bck_decl_preds) 433 bck_eputs("\n" as *u8) 434 return 0 435} 436 437// THE PASS. `info` must be a dominator tree computed from CURRENT edges by the caller (the 438// nx_opt.nx wrapper rebuilds them first, exactly as opt_licm does). 439// 440// WHY ONE DOMINATOR TREE IS ENOUGH FOR THE WHOLE PASS, even though each elision changes the 441// CFG: an elision only ever REMOVES edges (the two branches to `fail`). Removing edges removes 442// paths, and dominance is a statement about all paths -- so every dominance fact true of the 443// original graph is still true of the reduced one. The tree computed at entry is therefore 444// conservative, never stale in the unsafe direction. 445func bck_elide_dominated(f: *Function, info: *DomInfoFn, exit_group_nr: i64) -> i64 { 446 if g_bckelide_live != 1 { return 0 } 447 if exit_group_nr <= 0 { return 0 } 448 if f.n_blocks < 2 { return 0 } 449 450 var tb: i64 = 0 451 while tb < f.n_blocks { 452 let tbb: *BasicBlock = block_at(f, tb) 453 if tbb.n_preds > BCK_PRED_SLOTS { 454 g_bck_decl_preds = g_bck_decl_preds + 1 455 return 0 456 } 457 tb = tb + 1 458 } 459 460 let scratch: *i64 = sys_mmap(BCK_SCRATCH_BYTES) as *i64 461 let cand0: i64 = g_bck_cand 462 463 // Pass 1: COUNT, and attribute every rejection to a named reason. Allocates nothing beyond 464 // the scratch slot, so a function with no re-check to remove -- which is most of them -- 465 // costs one scan and no table at all. 466 var nsite: i64 = 0 467 var bi: i64 = 0 468 while bi < f.n_blocks { 469 if bck_match_site(f, bi, exit_group_nr, scratch, 1) == 1 { nsite = nsite + 1 } 470 bi = bi + 1 471 } 472 g_bck_sites = g_bck_sites + nsite 473 // One check cannot be a RE-check, so a single site is the common honest zero -- reported, 474 // not silent, so it can be told apart from "the pass never ran". 475 if nsite < 2 { 476 bck_report(f.n_blocks, g_bck_cand - cand0, nsite, 0) 477 return 0 478 } 479 480 let nb: i64 = nsite * 8 + 64 481 let s_b: *i64 = sys_mmap(nb) as *i64 482 let s_lo: *i64 = sys_mmap(nb) as *i64 483 let s_ok: *i64 = sys_mmap(nb) as *i64 484 let s_idx: *i64 = sys_mmap(nb) as *i64 485 let s_len: *i64 = sys_mmap(nb) as *i64 486 let s_out: *i64 = sys_mmap(nb) as *i64 487 let cflag: *i64 = sys_mmap(BCK_SCRATCH_BYTES) as *i64 488 489 // Pass 2: COLLECT, keyed by (idx value id, len CONSTANT). A site whose length is not a 490 // constant is dropped from the table entirely, so it can neither be elided nor justify an 491 // elision -- the declared slice refusal, applied in both directions. 492 var n: i64 = 0 493 var bj: i64 = 0 494 while bj < f.n_blocks { 495 if n < nsite { 496 if bck_match_site(f, bj, exit_group_nr, scratch, 0) == 1 { 497 let lc: i64 = bck_const_of(f, scratch[BCK_O_LEN], cflag) 498 if cflag[0] == 1 { 499 s_b[n] = bj 500 s_lo[n] = scratch[BCK_O_LO] 501 s_ok[n] = scratch[BCK_O_OK] 502 s_idx[n] = scratch[BCK_O_IDX] 503 s_len[n] = lc 504 s_out[n] = 0 505 n = n + 1 506 } 507 if cflag[0] != 1 { g_bck_ref_nolen = g_bck_ref_nolen + 1 } 508 } 509 } 510 bj = bj + 1 511 } 512 513 // Pass 3: DECIDE. For each site, look for a still-live site with the same key whose `ok` 514 // dominates this site's entry. 515 var elided: i64 = 0 516 var si: i64 = 0 517 while si < n { 518 var j: i64 = 0 519 var done: i64 = 0 520 while j < n { 521 if done == 0 { 522 if j != si { 523 // CIRCULAR-JUSTIFICATION GUARD. A site that has itself been elided no 524 // longer performs a comparison, so it cannot be the reason another site is 525 // safe. The dominator tree makes a genuine cycle impossible here (each 526 // site's ok is a descendant of its own b, so mutual justification would 527 // need a block to be its own strict ancestor) -- this guard makes that 528 // argument LOCAL instead of leaving the reader to reconstruct it. 529 if s_out[j] == 0 { 530 if s_idx[j] == s_idx[si] { 531 if s_len[j] == s_len[si] { 532 let okj: *BasicBlock = block_at(f, s_ok[j]) 533 let bsi: *BasicBlock = block_at(f, s_b[si]) 534 if dom_dominates(info, okj, bsi) == 1 { 535 if bck_rewrite_site(f, s_b[si], s_lo[si], s_ok[si]) == 1 { 536 s_out[si] = 1 537 elided = elided + 1 538 g_bck_elided = g_bck_elided + 1 539 done = 1 540 } 541 } 542 } 543 } 544 } 545 } 546 } 547 j = j + 1 548 } 549 if done == 0 { g_bck_ref_nodom = g_bck_ref_nodom + 1 } 550 si = si + 1 551 } 552 bck_report(f.n_blocks, g_bck_cand - cand0, n, elided) 553 return elided 554} 555 556// ============================================================================================ 557// LN7b: INDUCTION-GUARDED ELISION -- bck_elide_induction (2026-09-01, the F1 lane). 558// 559// WHAT THE DOMINANCE PASS ABOVE CANNOT SEE. Measured on the receipt loop that /compare/lang 560// publishes (a [1024]i64 summed 20,000 times): `--bckelide` ran, found 3 candidates and elided 561// NOTHING -- `BCK-ELIDE blocks=10 cand=3 sites=0 elided=0`. The loop carries ONE check per 562// iteration, so there is no "re-check of the same SSA id" for dominance to remove; the fact 563// that proves the check is the LOOP GUARD `k < 1024` in the header, which tests a DIFFERENT 564// load of the same counter. Every iteration then re-loads k, re-tests `k >= 0` and `k < 1024`, 565// and branches twice -- two compares and two branches on a body of eleven instructions. 566// 567// THE THEOREM. Let A be a stack slot (an OP_ALLOCA) such that 568// (a) A NEVER ESCAPES: its id appears only as the ADDRESS operand of a LOAD or a STORE (the 569// same rule opt_licm's licm_alloca_noescape applies). Then no other value id can name 570// that memory, and the only writes to it are the STOREs this pass can see. 571// (b) the first instruction touching A after its alloca, in the alloca's own block, is a 572// STORE of a constant c0 with 0 <= c0 <= BCK_IV_MAX_CONST (the initialiser), and no LOAD 573// of A precedes it there; 574// (c) EVERY STORE to A writes either a constant c with 0 <= c <= BCK_IV_MAX_CONST, or 575// LOAD(A) + c with 0 <= c <= BCK_IV_MAX_CONST, and every store of the second kind is 576// GUARDED: some `LOAD(A) <s L'` (L' a constant, L' <= BCK_IV_MAX_CONST) whose TRUE edge 577// dominates the store, with no store to A on any path from that edge to the store. 578// Then by induction on the stores, at every point of the function 0 <= A < 2^61: a reset writes 579// a small non-negative constant, and an increment adds at most 2^60 to a value the guard just 580// proved below 2^60, so no store ever wraps. Hence A >= 0 everywhere, which discharges the 581// site's `idx >= 0` leg for idx = LOAD(A). 582// For the `idx < len` leg: if a guard `LOAD(A) <s L` with L <= len has a TRUE edge T that 583// DOMINATES the site block, and no STORE to A can execute on any path from T's entry to the 584// site's own load of idx, then the value the guard tested is the value the site loads, and it 585// is below L <= len. Both comparisons can only answer true, so making the two branches 586// unconditional changes no observable behaviour -- the same rewrite the dominance pass uses. 587// 588// WHERE THE FIELD IS. LLVM's InductiveRangeCheckElimination splits the iteration space 589// (a pre-loop, a check-free main loop and a post-loop) so it can drop checks whose bound is not 590// implied by the guard; Go's prove pass carries per-value limit lattices through the CFG 591// (knowledge/fetched/cmp_lang_llvm_irce.cpp, cmp_lang_go_prove.go, both mirrored and pinned in 592// lang.refs). This pass is DELIBERATELY the smaller theorem: it removes a check only where the 593// loop guard's bound already implies it, adds no code, splits no loop, and its proof fits in 594// one screen. The IRCE split is the next rung, named in lang.plan, not smuggled in here. 595// 596// WRONG IN THE DIRECTION OF KEEPING THE CHECK, BY CONSTRUCTION -- every predicate below is a 597// reason to elide; every unrecognised shape keeps the check, and every refusal is NAMED and 598// tallied on stderr, because a refusing run and a run that never happened print the same zero. 599// 600// DECLARED IMPRECISION, each sound in the do-nothing direction: 601// * the site's load of idx must sit IN the site's own block (the freshly emitted shape); a 602// load hoisted elsewhere is refused (r_iv_load_not_local) rather than reasoned about; 603// * the guard must be `LOAD(A) <s CONST` at a block's terminator, with no store to A between 604// that load and the branch; `CONST >s LOAD(A)`, `<=`, unsigned and slice lengths are all 605// refused -- a slice length is loaded, not constant, and stays exactly as declared above; 606// * reachability is over succ0/succ1 and pred0..pred2, and a function whose pred lists are 607// truncated is refused whole, as the dominance pass and opt_licm already refuse it; 608// * a block on the guard-to-site path is treated as store-carrying if it stores to A 609// ANYWHERE in it, prefix or suffix -- a whole-block over-approximation that can only 610// refuse a valid elision. 611// ============================================================================================ 612const BCK_IV_MAX_CONST: i64 = 1152921504606846976 // 2^60: guard bound + increment stays far below 2^63 613const BCK_R_IV_LOAD_NOT_LOCAL: i64 = 16 614const BCK_R_IV_NOT_LOAD: i64 = 17 615const BCK_R_IV_NOT_ALLOCA: i64 = 18 616const BCK_R_IV_ESCAPES: i64 = 19 617const BCK_R_IV_NO_INIT: i64 = 20 618const BCK_R_IV_BAD_STORE: i64 = 21 619const BCK_R_IV_INC_UNGUARDED: i64 = 22 620const BCK_R_IV_NO_GUARD: i64 = 23 621const BCK_R_IV_STORE_ON_PATH: i64 = 24 622const BCK_R_IV_MATCHED: i64 = 25 623const BCK_IV_OPERAND_SLOTS: i64 = 24 624 625static g_bck_iv_sites: i64 626static g_bck_iv_elided: i64 627func bck_stat_iv_sites() -> i64 { return g_bck_iv_sites } 628func bck_stat_iv_elided() -> i64 { return g_bck_iv_elided } 629 630// operand k of an instruction. A local twin of nx_opt.nx's opt_opk: that file IMPORTS this one, 631// and NishiLang resolves names in textual order, so the optimizer's copy is not visible here. 632func bck_iv_opk(inst: *Instr, k: i64) -> i64 { 633 if k == 0 { return inst.op0 } 634 if k == 1 { return inst.op1 } 635 if k == 2 { return inst.op2 } 636 if k == 3 { return inst.op3 } 637 if k == 4 { return inst.op4 } 638 if k == 5 { return inst.op5 } 639 if k == 6 { return inst.op6 } 640 if k == 7 { return inst.op7 } 641 if k == 8 { return inst.op8 } 642 if k == 9 { return inst.op9 } 643 if k == 10 { return inst.op10 } 644 if k == 11 { return inst.op11 } 645 if k == 12 { return inst.op12 } 646 if k == 13 { return inst.op13 } 647 if k == 14 { return inst.op14 } 648 if k == 15 { return inst.op15 } 649 if k == 16 { return inst.op16 } 650 if k == 17 { return inst.op17 } 651 if k == 18 { return inst.op18 } 652 if k == 19 { return inst.op19 } 653 if k == 20 { return inst.op20 } 654 if k == 21 { return inst.op21 } 655 if k == 22 { return inst.op22 } 656 if k == 23 { return inst.op23 } 657 return 0 - 1 658} 659 660// The defining instruction of value id v anywhere in f, or null. 661func bck_iv_def(f: *Function, v: i64) -> *Instr { 662 if v < 0 { return 0 as *Instr } 663 if v >= f.n_values { return 0 as *Instr } 664 return bck_def_of(f, block_at(f, 0), v) 665} 666 667// (a) A NEVER ESCAPES -- the licm_alloca_noescape rule, restated (see bck_iv_opk for why). 668func bck_iv_noescape(f: *Function, aid: i64) -> i64 { 669 var bi: i64 = 0 670 while bi < f.n_blocks { 671 let b: *BasicBlock = block_at(f, bi) 672 var inst: *Instr = b.head 673 while inst != (0 as *Instr) { 674 var no: i64 = inst.n_operands 675 if no > BCK_IV_OPERAND_SLOTS { no = BCK_IV_OPERAND_SLOTS } 676 var k: i64 = 0 677 while k < no { 678 // A terminator's target operands are BLOCK IDS, not value ids: OP_BR op0 and 679 // OP_BR_COND op1/op2 name blocks, and a block number that happens to equal an 680 // alloca's value id is not a use of it. MEASURED on the first probe: the init 681 // loop's counter (alloca id 2) was refused as escaping because the loop's own 682 // `br_cond ... -> block 2` carried the number 2. Skipped by position, never by 683 // value, so the check stays exact for every real operand. 684 var isblk: i64 = 0 685 if inst.op == OP_BR { if k == 0 { isblk = 1 } } 686 if inst.op == OP_BR_COND { if k == 1 { isblk = 1 } if k == 2 { isblk = 1 } } 687 if isblk == 0 { if bck_iv_opk(inst, k) == aid { 688 var okpos: i64 = 0 689 if inst.op == OP_LOAD { if k == 0 { okpos = 1 } } 690 if inst.op == OP_STORE { if k == 0 { okpos = 1 } } 691 if okpos == 0 { return 0 } 692 } } 693 k = k + 1 694 } 695 inst = inst.next 696 } 697 bi = bi + 1 698 } 699 return 1 700} 701 702// v is a constant in [0, BCK_IV_MAX_CONST]; out[0] receives it 703func bck_iv_small_const(f: *Function, v: i64, out: *i64) -> i64 { 704 let fl: *i64 = sys_mmap(16) as *i64 705 let c: i64 = bck_const_of(f, v, fl) 706 if fl[0] != 1 { return 0 } 707 if c < 0 { return 0 } 708 if c > BCK_IV_MAX_CONST { return 0 } 709 out[0] = c 710 return 1 711} 712 713// v is LOAD(aid) 714func bck_iv_is_load_of(f: *Function, v: i64, aid: i64) -> i64 { 715 let d: *Instr = bck_iv_def(f, v) 716 if d == (0 as *Instr) { return 0 } 717 if d.op != OP_LOAD { return 0 } 718 if d.op0 != aid { return 0 } 719 return 1 720} 721 722// (c) classify one STORE to aid: 1 reset (small non-negative constant), 2 increment 723// (LOAD(aid) + small non-negative constant, either operand order), 0 anything else. 724func bck_iv_store_kind(f: *Function, st: *Instr, aid: i64) -> i64 { 725 let c: *i64 = sys_mmap(16) as *i64 726 if bck_iv_small_const(f, st.op1, c) == 1 { return 1 } 727 let d: *Instr = bck_iv_def(f, st.op1) 728 if d == (0 as *Instr) { return 0 } 729 if d.op != OP_ADD { return 0 } 730 if bck_iv_is_load_of(f, d.op0, aid) == 1 { if bck_iv_small_const(f, d.op1, c) == 1 { return 2 } } 731 if bck_iv_is_load_of(f, d.op1, aid) == 1 { if bck_iv_small_const(f, d.op0, c) == 1 { return 2 } } 732 return 0 733} 734 735// (b) the initialiser: after the alloca, in its own block, the first touch of aid is a reset store 736func bck_iv_init_ok(f: *Function, aid: i64) -> i64 { 737 let ad: *Instr = bck_iv_def(f, aid) 738 if ad == (0 as *Instr) { return 0 } 739 if ad.op != OP_ALLOCA { return 0 } 740 let c: *i64 = sys_mmap(16) as *i64 741 var inst: *Instr = ad.next 742 while inst != (0 as *Instr) { 743 if inst.op == OP_LOAD { if inst.op0 == aid { return 0 } } 744 if inst.op == OP_STORE { if inst.op0 == aid { 745 if bck_iv_small_const(f, inst.op1, c) == 1 { return 1 } 746 return 0 747 } } 748 inst = inst.next 749 } 750 return 0 751} 752 753func bck_iv_block_has_store(b: *BasicBlock, aid: i64) -> i64 { 754 var inst: *Instr = b.head 755 while inst != (0 as *Instr) { 756 if inst.op == OP_STORE { if inst.op0 == aid { return 1 } } 757 inst = inst.next 758 } 759 return 0 760} 761 762func bck_iv_push_succ(f: *Function, s: *BasicBlock, t_i: i64, mark: *u8, stack: *i64, sp: *i64) -> i64 { 763 if s == (0 as *BasicBlock) { return 0 } 764 let si: i64 = df_block_index(f, s) 765 if si < 0 { return 0 } 766 if si == t_i { return 0 } 767 if mark[si] != (0 as u8) { return 0 } 768 mark[si] = 1 as u8 769 stack[sp[0]] = si 770 sp[0] = sp[0] + 1 771 return 1 772} 773 774// FORWARD: every block reachable from T's successors without re-entering T. 775func bck_iv_mark_fwd(f: *Function, t_i: i64, mark: *u8, stack: *i64) -> i64 { 776 var z: i64 = 0 777 while z < f.n_blocks { mark[z] = 0 as u8; z = z + 1 } 778 let sp: *i64 = sys_mmap(16) as *i64 779 sp[0] = 0 780 let tb: *BasicBlock = block_at(f, t_i) 781 bck_iv_push_succ(f, tb.succ0, t_i, mark, stack, sp) 782 bck_iv_push_succ(f, tb.succ1, t_i, mark, stack, sp) 783 while sp[0] > 0 { 784 sp[0] = sp[0] - 1 785 let x: i64 = stack[sp[0]] 786 let xb: *BasicBlock = block_at(f, x) 787 bck_iv_push_succ(f, xb.succ0, t_i, mark, stack, sp) 788 bck_iv_push_succ(f, xb.succ1, t_i, mark, stack, sp) 789 } 790 return 0 791} 792 793func bck_iv_push_pred(f: *Function, p: *BasicBlock, t_i: i64, mark: *u8, stack: *i64, sp: *i64) -> i64 { 794 if p == (0 as *BasicBlock) { return 0 } 795 let pi: i64 = df_block_index(f, p) 796 if pi < 0 { return 0 } 797 if pi == t_i { return 0 } 798 if mark[pi] != (0 as u8) { return 0 } 799 mark[pi] = 1 as u8 800 stack[sp[0]] = pi 801 sp[0] = sp[0] + 1 802 return 1 803} 804 805// BACKWARD: every block from which dst is reachable without passing through T (dst itself marked). 806func bck_iv_mark_bwd(f: *Function, t_i: i64, dst_i: i64, mark: *u8, stack: *i64) -> i64 { 807 var z: i64 = 0 808 while z < f.n_blocks { mark[z] = 0 as u8; z = z + 1 } 809 let sp: *i64 = sys_mmap(16) as *i64 810 sp[0] = 0 811 mark[dst_i] = 1 as u8 812 stack[0] = dst_i 813 sp[0] = 1 814 while sp[0] > 0 { 815 sp[0] = sp[0] - 1 816 let x: i64 = stack[sp[0]] 817 let xb: *BasicBlock = block_at(f, x) 818 if xb.n_preds > 0 { bck_iv_push_pred(f, xb.pred0, t_i, mark, stack, sp) } 819 if xb.n_preds > 1 { bck_iv_push_pred(f, xb.pred1, t_i, mark, stack, sp) } 820 if xb.n_preds > 2 { bck_iv_push_pred(f, xb.pred2, t_i, mark, stack, sp) } 821 } 822 return 0 823} 824 825// 1 iff no STORE to aid can execute on any path from T's entry to `stop` (exclusive) in dst. 826func bck_iv_store_free(f: *Function, t_i: i64, dst_i: i64, aid: i64, stop: *Instr, fwd: *u8, bwd: *u8, stack: *i64) -> i64 { 827 let db: *BasicBlock = block_at(f, dst_i) 828 var inst: *Instr = db.head 829 while inst != (0 as *Instr) { 830 if inst == stop { break } 831 if inst.op == OP_STORE { if inst.op0 == aid { return 0 } } 832 inst = inst.next 833 } 834 // straight from the guard's true edge into this very block: only the prefix is on the path 835 if t_i == dst_i { return 1 } 836 // T is entered from the guard; a store anywhere in it is on the path (whole-block, conservative) 837 if bck_iv_block_has_store(block_at(f, t_i), aid) == 1 { return 0 } 838 bck_iv_mark_fwd(f, t_i, fwd, stack) 839 bck_iv_mark_bwd(f, t_i, dst_i, bwd, stack) 840 // dst can re-enter itself without passing T only through a successor that reaches dst without T; 841 // then dst's OWN suffix stores lie on a path to stop as well -- refuse if dst stores at all 842 var cyc: i64 = 0 843 if db.succ0 != (0 as *BasicBlock) { let s0: i64 = df_block_index(f, db.succ0); if s0 >= 0 { if s0 != t_i { if bwd[s0] != (0 as u8) { cyc = 1 } } } } 844 if db.succ1 != (0 as *BasicBlock) { let s1: i64 = df_block_index(f, db.succ1); if s1 >= 0 { if s1 != t_i { if bwd[s1] != (0 as u8) { cyc = 1 } } } } 845 if cyc == 1 { if bck_iv_block_has_store(db, aid) == 1 { return 0 } } 846 var x: i64 = 0 847 while x < f.n_blocks { 848 if x != t_i { if x != dst_i { if fwd[x] != (0 as u8) { if bwd[x] != (0 as u8) { 849 if bck_iv_block_has_store(block_at(f, x), aid) == 1 { return 0 } 850 } } } } 851 x = x + 1 852 } 853 return 1 854} 855 856// A guard: a block whose terminator is BR_COND on `LOAD(aid) <s L'` with L' <= limit, whose TRUE 857// edge dominates dst, and with no store to aid between that load and the branch. Returns the 858// TRUE-edge block index, or -1. 859func bck_iv_find_guard(f: *Function, info: *DomInfoFn, aid: i64, limit: i64, dst: *BasicBlock) -> i64 { 860 let c: *i64 = sys_mmap(16) as *i64 861 var gi: i64 = 0 862 while gi < f.n_blocks { 863 let g: *BasicBlock = block_at(f, gi) 864 let t: *Instr = g.tail 865 if t != (0 as *Instr) { if t.op == OP_BR_COND { 866 let d: *Instr = bck_def_of(f, g, t.op0) 867 if d != (0 as *Instr) { if d.op == OP_LT_S { 868 if bck_iv_is_load_of(f, d.op0, aid) == 1 { if bck_iv_small_const(f, d.op1, c) == 1 { if c[0] <= limit { 869 // the tested load must live in g, and nothing may store to aid between it and the branch 870 let ld: *Instr = bck_def_in_block(g, d.op0) 871 if ld != (0 as *Instr) { 872 var clean: i64 = 1 873 var s: *Instr = ld.next 874 while s != (0 as *Instr) { 875 if s.op == OP_STORE { if s.op0 == aid { clean = 0 } } 876 s = s.next 877 } 878 if clean == 1 { 879 let ti: i64 = bck_block_index_by_id(f, t.op1) 880 if ti >= 0 { if dom_dominates(info, block_at(f, ti), dst) == 1 { return ti } } 881 } 882 } 883 } } } 884 } } 885 } } 886 gi = gi + 1 887 } 888 return 0 - 1 889} 890 891// One site: returns BCK_R_IV_MATCHED or the named refusal. 892func bck_iv_check(f: *Function, info: *DomInfoFn, b: *BasicBlock, bi: i64, ld: *Instr, lc: i64, fwd: *u8, bwd: *u8, stack: *i64) -> i64 { 893 if ld == (0 as *Instr) { return BCK_R_IV_LOAD_NOT_LOCAL } 894 if ld.op != OP_LOAD { return BCK_R_IV_NOT_LOAD } 895 let aid: i64 = ld.op0 896 let ad: *Instr = bck_iv_def(f, aid) 897 if ad == (0 as *Instr) { return BCK_R_IV_NOT_ALLOCA } 898 if ad.op != OP_ALLOCA { return BCK_R_IV_NOT_ALLOCA } 899 if bck_iv_noescape(f, aid) != 1 { return BCK_R_IV_ESCAPES } 900 if bck_iv_init_ok(f, aid) != 1 { return BCK_R_IV_NO_INIT } 901 var sb: i64 = 0 902 while sb < f.n_blocks { 903 let xb: *BasicBlock = block_at(f, sb) 904 var inst: *Instr = xb.head 905 while inst != (0 as *Instr) { 906 if inst.op == OP_STORE { if inst.op0 == aid { 907 let k: i64 = bck_iv_store_kind(f, inst, aid) 908 if k == 0 { return BCK_R_IV_BAD_STORE } 909 if k == 2 { 910 let ti2: i64 = bck_iv_find_guard(f, info, aid, BCK_IV_MAX_CONST, xb) 911 if ti2 < 0 { return BCK_R_IV_INC_UNGUARDED } 912 if bck_iv_store_free(f, ti2, sb, aid, inst, fwd, bwd, stack) != 1 { return BCK_R_IV_INC_UNGUARDED } 913 } 914 } } 915 inst = inst.next 916 } 917 sb = sb + 1 918 } 919 let ti: i64 = bck_iv_find_guard(f, info, aid, lc, b) 920 if ti < 0 { return BCK_R_IV_NO_GUARD } 921 if bck_iv_store_free(f, ti, bi, aid, ld, fwd, bwd, stack) != 1 { return BCK_R_IV_STORE_ON_PATH } 922 return BCK_R_IV_MATCHED 923} 924 925func bck_iv_report(fn_blocks: i64, cand: i64, elided: i64) -> i64 { 926 if g_bckelide_live != 1 { return 0 } 927 if cand == 0 { return 0 } 928 bck_ekv("BCK-ELIDE-IV blocks=" as *u8, fn_blocks) 929 bck_ekv(" cand=" as *u8, cand) 930 bck_ekv(" elided=" as *u8, elided) 931 bck_ekv(" cum_iv_sites=" as *u8, g_bck_iv_sites) 932 bck_ekv(" cum_iv_elided=" as *u8, g_bck_iv_elided) 933 bck_ereason(" r_iv_load_not_local=" as *u8, BCK_R_IV_LOAD_NOT_LOCAL) 934 bck_ereason(" r_iv_not_load=" as *u8, BCK_R_IV_NOT_LOAD) 935 bck_ereason(" r_iv_not_alloca=" as *u8, BCK_R_IV_NOT_ALLOCA) 936 bck_ereason(" r_iv_escapes=" as *u8, BCK_R_IV_ESCAPES) 937 bck_ereason(" r_iv_no_init=" as *u8, BCK_R_IV_NO_INIT) 938 bck_ereason(" r_iv_bad_store=" as *u8, BCK_R_IV_BAD_STORE) 939 bck_ereason(" r_iv_inc_unguarded=" as *u8, BCK_R_IV_INC_UNGUARDED) 940 bck_ereason(" r_iv_no_guard=" as *u8, BCK_R_IV_NO_GUARD) 941 bck_ereason(" r_iv_store_on_path=" as *u8, BCK_R_IV_STORE_ON_PATH) 942 bck_ereason(" r_iv_matched=" as *u8, BCK_R_IV_MATCHED) 943 bck_eputs("\n" as *u8) 944 return 0 945} 946 947// THE PASS. Same contract as bck_elide_dominated: `info` is a dominator tree over CURRENT 948// edges; an elision only removes edges, so the tree stays conservative for the whole pass. 949// Runs on every matched site whose length is a compile-time constant; a site the dominance pass 950// already rewrote is no longer a BR_COND site and is simply not matched again. 951func bck_elide_induction(f: *Function, info: *DomInfoFn, exit_group_nr: i64) -> i64 { 952 if g_bckelide_live != 1 { return 0 } 953 if exit_group_nr <= 0 { return 0 } 954 if f.n_blocks < 2 { return 0 } 955 var tb: i64 = 0 956 while tb < f.n_blocks { 957 let tbb: *BasicBlock = block_at(f, tb) 958 if tbb.n_preds > BCK_PRED_SLOTS { g_bck_decl_preds = g_bck_decl_preds + 1; return 0 } 959 tb = tb + 1 960 } 961 let scratch: *i64 = sys_mmap(BCK_SCRATCH_BYTES) as *i64 962 let cflag: *i64 = sys_mmap(16) as *i64 963 let fwd: *u8 = sys_mmap(f.n_blocks + 16) 964 let bwd: *u8 = sys_mmap(f.n_blocks + 16) 965 let stack: *i64 = sys_mmap(f.n_blocks * 8 + 64) as *i64 966 var elided: i64 = 0 967 var cand: i64 = 0 968 var bi: i64 = 0 969 while bi < f.n_blocks { 970 if bck_match_site(f, bi, exit_group_nr, scratch, 0) == 1 { 971 let lc: i64 = bck_const_of(f, scratch[BCK_O_LEN], cflag) 972 if cflag[0] == 1 { 973 cand = cand + 1 974 let b: *BasicBlock = block_at(f, bi) 975 let ld: *Instr = bck_def_in_block(b, scratch[BCK_O_IDX]) 976 let r: i64 = bck_iv_check(f, info, b, bi, ld, lc, fwd, bwd, stack) 977 bck_reason_bump(r) 978 if r == BCK_R_IV_MATCHED { 979 if bck_rewrite_site(f, bi, scratch[BCK_O_LO], scratch[BCK_O_OK]) == 1 { 980 elided = elided + 1 981 g_bck_iv_elided = g_bck_iv_elided + 1 982 } 983 } 984 } 985 } 986 bi = bi + 1 987 } 988 g_bck_iv_sites = g_bck_iv_sites + cand 989 bck_iv_report(f.n_blocks, cand, elided) 990 sys_munmap(fwd, f.n_blocks + 16) 991 sys_munmap(bwd, f.n_blocks + 16) 992 sys_munmap(stack as *u8, f.n_blocks * 8 + 64) 993 return elided 994}