diff --git a/src/check_asm.cpp b/src/check_asm.cpp index cb535a5d3..c03b081fd 100644 --- a/src/check_asm.cpp +++ b/src/check_asm.cpp @@ -2644,6 +2644,7 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity check_asm_cfg_build(asm_ctx, &cfg, d->init_expr, entity); check_asm_cfg_analyse(asm_ctx, &cfg, ctx, entity); + check_asm_cfg_liveness(asm_ctx, &cfg, entity, /*emit_dead_writes*/all_instructions_good); bool vet_unused = false; { diff --git a/src/check_asm_cfg.cpp b/src/check_asm_cfg.cpp index 3a4981c27..b846264a1 100644 --- a/src/check_asm_cfg.cpp +++ b/src/check_asm_cfg.cpp @@ -665,3 +665,127 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont } } } + +// Backward liveness: a value is live at a point if some path from there reads it before overwriting it. +// Dual of the forward definite-assignment pass. +// Used to find dead writes (a definition never read before being overwritten or before template exit). +// +// NOTE(bill): only set `emit_dead_writes` to be true when all instructions are "good". +template +gb_internal bool check_asm_cfg_liveness(AsmCtx *asm_ctx, AsmCfg *cfg, Entity *entity, bool emit_dead_writes) { + isize const n = cfg->blocks.count; + if (n == 0) { + return false; + } + + u16 const REG_TOP = asm_ctx->CLOBBER_REGS_NAMED; + + u16 exit_live = 0; + u16 output_regs = 0; + for_array(i, entity->AsmTemplate.decls) { + auto const &ed = entity->AsmTemplate.decls[i]; + if (ed.param_group == AsmTemplateEntityDeclParamGroup_Output) { + exit_live |= cfg->decl_pin_bit[i]; // pinned/view-inherited output reg, if any + output_regs |= cfg->decl_pin_bit[i]; + } + } + for (String const ® : entity->AsmTemplate.clobber_registers_set) { + exit_live |= asm_ctx->clobber_bit_for_reg_name(reg); + } + + auto live_in = slice_make(heap_allocator(), n); defer (slice_free(&live_in, heap_allocator())); + auto live_out = slice_make(heap_allocator(), n); defer (slice_free(&live_out, heap_allocator())); + + bool changed = true; + while (changed) { + changed = false; + // Iterate in reverse for faster convergence + for (isize bi = n - 1; bi >= 0; bi--) { + if (!cfg->blocks[bi].reachable) { + continue; + } + AsmBlock const &b = cfg->blocks[bi]; + + // live_out = union of successors' live_in, plus exit_live if this block leaves. + u16 lo = 0; + for (i32 s : b.succs) { + if (0 <= s && s < cast(i32)n && cfg->blocks[s].reachable) { + lo |= live_in[s]; + } + } + if (check_asm_cfg_block_leaves(cfg, cast(i32)bi)) { + lo |= exit_live; + } + + u16 live = lo; + for (i32 ii = b.last; ii >= b.first; ii--) { + AsmInstructionFacts *f = cfg->insts[ii]->facts; + if (f == nullptr) { + continue; + } + live = (live & ~f->gen_regs) | (f->read_regs & REG_TOP); + } + + if (lo != live_out[bi] || live != live_in[bi]) { + live_out[bi] = lo; + live_in[bi] = live; + changed = true; + } + } + } + + for_array(bi, cfg->blocks) { + cfg->blocks[bi].live_in_regs = live_in[bi]; + cfg->blocks[bi].live_out_regs = live_out[bi]; + } + + if (!emit_dead_writes) { + // Lattice has been computed but no diagnostic until the read facts are validated + return false; + } + + // Dead-write detection: walk each block forward, tracking live-out per instruction. + // A written reg that is not live immediately after the write (and not re-read in + // this same instruction) is dead. + for_array(bi, cfg->blocks) { + AsmBlock const &b = cfg->blocks[bi]; + if (!b.reachable) { + continue; + } + // recompute per-instruction live-out by replaying the transfer from block live_out + // (cheap: block is short). Build an array of live-after-each-instruction. + u16 live = live_out[bi]; + if (check_asm_cfg_block_leaves(cfg, cast(i32)bi)) { + live |= exit_live; // already folded above, but harmless + } + for (i32 ii = b.last; ii >= b.first; ii--) { + AsmInstructionFacts *f = cfg->insts[ii]->facts; + if (f == nullptr) { + continue; + } + u16 live_after = live; + // A register this instruction writes but that is not live afterward, + // and that it does not itself read (self-use like `xor r,r` or `add r,x`), + // is a dead write. + u16 dead = f->gen_regs & ~live_after & ~f->read_regs; + for (u16 bit = 1; bit != 0; bit <<= 1) { + if ((dead & bit) == 0) { + continue; + } + if ((bit & output_regs) != 0) { + warning(f->node->name, + "'%.*s' writes output register %%%s, but that value is overwritten before the template returns; " + "the output's final value does not come from this instruction", + LIT(f->name), asm_ctx->clobber_reg_bit_name(bit)); + } else { + warning(f->node->name, + "'%.*s' writes %%%s but its value is never read before being overwritten or the template ends", + LIT(f->name), asm_ctx->clobber_reg_bit_name(bit)); + } + } + live = (live & ~f->gen_regs) | (f->read_regs & REG_TOP); + } + } + + return true; +} \ No newline at end of file