Re-add check_asm_cfg_liveness

This commit is contained in:
gingerBill
2026-08-25 09:43:31 +01:00
parent b1a81cf6f3
commit 6f65d775c2
2 changed files with 125 additions and 0 deletions

View File

@@ -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;
{

View File

@@ -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 <typename AsmCtx>
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 &reg : entity->AsmTemplate.clobber_registers_set) {
exit_live |= asm_ctx->clobber_bit_for_reg_name(reg);
}
auto live_in = slice_make<u16>(heap_allocator(), n); defer (slice_free(&live_in, heap_allocator()));
auto live_out = slice_make<u16>(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;
}