From b4cf9b14bd1d22413ad4a8877faed996a283bd5f Mon Sep 17 00:00:00 2001 From: gingerBill Date: Tue, 25 Aug 2026 12:17:06 +0100 Subject: [PATCH] asm: Implement sub-register width analysis --- src/check_asm.cpp | 36 ++++++++++ src/check_asm_cfg.cpp | 162 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 189 insertions(+), 9 deletions(-) diff --git a/src/check_asm.cpp b/src/check_asm.cpp index 38e9604d5..4a7e0d282 100644 --- a/src/check_asm.cpp +++ b/src/check_asm.cpp @@ -1656,6 +1656,42 @@ gb_internal bool check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm } + for_array(i, operands) { // Sub-register widths for explicit named-register operands + auto const &op = operands[i]; + Ast *e = op.expr; + if (e == nullptr || e->kind != Ast_AsmRegister) { + continue; + } + u16 bit = asm_ctx->clobber_bit_for_reg_name(e->AsmRegister.name.string); + i32 idx = asm_reg_index_from_bit(bit); + if (idx < 0) { + continue; + } + i32 w = check_asm_operand_bit_width(op.type); + if (w <= 0) { + w = 0; + // Fallback to the register's intrinsic width for a bare %reg. + auto r = asm_ctx->register_lookup(e->AsmRegister.name.string); + if (r) { + w = asm_ctx->reg_size(r); + } + } + if (w == 0) { + continue; + } + int tslot = user_operand_target_index(cast(int)i); + if (0 <= tslot && tslot < 4) { + if (!self_zeroing && cast(u16)clobber.read & (1u << tslot)) { + facts->read_reg_w.e[idx] = cast(u8)w; + facts->read_regs |= bit; // NOTE(bill): let liveness + coarse rbw see the explicit read + } + if (cast(u16)clobber.written & (1u << tslot)) { + facts->gen_reg_w.e [idx] = cast(u8)w; + } + } + } + + for_array(i, operands) { int slot = user_operand_target_index(cast(int)i); if (slot < 0) { diff --git a/src/check_asm_cfg.cpp b/src/check_asm_cfg.cpp index 579d71e40..d2b423e23 100644 --- a/src/check_asm_cfg.cpp +++ b/src/check_asm_cfg.cpp @@ -1,3 +1,32 @@ + +enum { + ASM_WIDTH_REG_COUNT = 16 +}; + +// Sub-register width tracking +// 0 means "this instruction does not read/write that named register" +struct AsmRegW { + u8 e[ASM_WIDTH_REG_COUNT]; +}; + +gb_internal u8 asm_reg_def_width_after(u8 prev, u8 w) { + bool x86 = build_context.metrics.arch == TargetArch_amd64 || + build_context.metrics.arch == TargetArch_i386; // TODO(bill): actually support x86 32-bit :P + if (x86 && w == 32) { + return 64; + } + return gb_max(prev, w); +} + +gb_internal i32 asm_reg_index_from_bit(u16 bit) { + for (i32 i = 0; i < ASM_WIDTH_REG_COUNT; i++) { + if (bit == cast(u16)(1u << i)) { + return i; + } + } + return -1; +} + struct AsmBlock { i32 first; // instruction index i32 last; @@ -28,6 +57,10 @@ struct AsmInstructionFacts { u16 gen_regs; u16 read_regs; + // Sub-register widths, indexed by reg bit-index. + AsmRegW read_reg_w; + AsmRegW gen_reg_w; + Array gen_params; Array read_params; @@ -39,6 +72,25 @@ struct AsmInstructionFacts { i32 block_id; }; +gb_internal u16 asm_full_kill_mask(AsmInstructionFacts *f) { + u16 full = cast(u16)(build_context.metrics.ptr_size*8); + u16 kill = 0; + for (u16 bit = 1; bit != 0; bit <<= 1) { + if ((f->gen_regs & bit) == 0) { + continue; + } + i32 idx = asm_reg_index_from_bit(bit); + u8 w = 0; + if (idx >= 0) { + w = f->gen_reg_w.e[idx]; + } + if (w == 0 || asm_reg_def_width_after(0, w) >= full) { + kill |= bit; + } + } + return kill; +} + struct AsmCfg { // Union of registers implicitly clobbered by matched forms (for redundant-#clobber hints). u16 implicit_clobbered_regs; @@ -381,6 +433,11 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont auto out_pm = slice_make(heap_allocator(), n); defer (slice_free(&out_pm, heap_allocator())); auto gen_pm = slice_make(heap_allocator(), n); defer (slice_free(&gen_pm, heap_allocator())); + // Sub-register width lattice + auto in_w = slice_make(heap_allocator(), n); defer (slice_free(&in_w, heap_allocator())); + auto out_w = slice_make(heap_allocator(), n); defer (slice_free(&out_w, heap_allocator())); + auto gen_w = slice_make(heap_allocator(), n); defer (slice_free(&gen_w, heap_allocator())); + // predecessors, restricted to reachable blocks auto preds = slice_make>(heap_allocator(), n); for_array(i, preds) { @@ -409,6 +466,8 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont u16 gr = 0; u16 gf = 0; u64 gp = 0; + AsmRegW gw = {}; + AsmBlock const &b = cfg->blocks[bi]; for (i32 ii = b.first; ii <= b.last; ii++) { AsmInstructionFacts *f = cfg->insts[ii]->facts; @@ -420,10 +479,17 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont for (Entity *pe : f->gen_params) { gp |= bit_of(pe); } + for (int w_idx = 0; w_idx < ASM_WIDTH_REG_COUNT; w_idx++) { + u8 w = f->gen_reg_w.e[w_idx]; + if (w != 0) { + gw.e[w_idx] = asm_reg_def_width_after(gw.e[w_idx], w); + } + } } gen_regs[bi] = gr; gen_flags[bi] = gf; gen_pm[bi] = gp; + gen_w[bi] = gw; } // NOTE(bill): initialize the blocks @@ -489,6 +555,54 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont } } + for_array(bi, cfg->blocks) { + if (!cfg->blocks[bi].reachable) { + continue; + } + in_w[bi] = {}; + for (i32 idx = 0; idx < ASM_WIDTH_REG_COUNT; idx++) { + out_w[bi].e[idx] = gb_max(in_w[bi].e[idx], gen_w[bi].e[idx]); + } + } + changed = true; + while (changed) { + changed = false; + for_array(bi, cfg->blocks) { + if (!cfg->blocks[bi].reachable) { + continue; + } + AsmRegW nin = {}; + AsmRegW nout = {}; + + auto const &pred = preds[bi]; + + if (bi != 0) { + for (i32 idx = 0; idx < ASM_WIDTH_REG_COUNT; idx++) { + nin.e[idx] = 64; + } + if (pred.count == 0) { + // unreachable-by-preds guard + // just zero it out + nin = {}; + } + for (i32 p : pred) { + for (i32 idx = 0; idx < ASM_WIDTH_REG_COUNT; idx++) { + nin.e[idx] = gb_min(nin.e[idx], out_w[p].e[idx]); + } + } + } + for (i32 idx = 0; idx < ASM_WIDTH_REG_COUNT; idx++) { + nout.e[idx] = gb_max(nin.e[idx], gen_w[bi].e[idx]); + } + if (gb_memcompare(&nin, &in_w [bi], gb_size_of(AsmRegW)) != 0 || + gb_memcompare(&nout, &out_w[bi], gb_size_of(AsmRegW)) != 0) { + in_w [bi] = nin; + out_w[bi] = nout; + changed = true; + } + } + } + // NOTE(bill): publish the register masks and materialise the parameter sets onto the blocks for_array(bi, cfg->blocks) { AsmBlock *b = &cfg->blocks[bi]; @@ -556,9 +670,11 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont continue; } - u16 run_regs = in_regs[bi]; - u16 run_flags = in_flags[bi]; - u64 run_pm = in_pm[bi]; + u16 run_regs = in_regs[bi]; + u16 run_flags = in_flags[bi]; + u64 run_pm = in_pm[bi]; + AsmRegW run_w = in_w[bi]; + for (i32 ii = b.first; ii <= b.last; ii++) { AstAsmInstruction *instr = cfg->insts[ii]; @@ -572,8 +688,8 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont if ((undef & bit) == 0) { continue; } - check_asm_cfg_report_undef_reg(asm_ctx, cfg, entity, instr, f->name, bit); reported_regs |= bit; + check_asm_cfg_report_undef_reg(asm_ctx, cfg, entity, instr, f->name, bit); } u16 undef_flags = f->read_flags & ~run_flags & ~reported_flags; @@ -606,6 +722,27 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont LIT(f->name), plural, flag_strs); } + // Sub-register width + for (i32 w_idx = 0; w_idx < ASM_WIDTH_REG_COUNT; w_idx++) { + u8 rw = f->read_reg_w.e[w_idx]; + if (rw == 0) { + continue; + } + u16 bit = cast(u16)(1u << w_idx); + if ((reported_regs & bit) != 0) { + continue; + } + if (run_w.e[w_idx] < rw) { + reported_regs |= bit; + + char const *rname = asm_ctx->clobber_reg_bit_name(bit); + error(instr->name, + "'%.*s' reads %s at %u-bit width but only its low %u bits are defined on all paths here; " + "write the full register first (e.g. zero-extend, or a full-width or 32-bit-zeroing write)", + LIT(f->name), rname, cast(unsigned)rw, cast(unsigned)run_w.e[w_idx]); + } + } + for (Entity *pe : f->read_params) { i32 *ix = map_get(&cfg->entity_to_index, pe); if (ix == nullptr) { @@ -626,9 +763,16 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont run_regs |= f->gen_regs; run_flags |= f->gen_flags; + for (i32 w_idx = 0; w_idx < ASM_WIDTH_REG_COUNT; w_idx++) { + u8 gw = f->gen_reg_w.e[w_idx]; + if (gw != 0) { + run_w.e[w_idx] = asm_reg_def_width_after(run_w.e[w_idx], gw); + } + } for (Entity *pe : f->gen_params) { run_pm |= bit_of(pe); } + } } } @@ -799,7 +943,7 @@ gb_internal bool check_asm_cfg_liveness(AsmCtx *asm_ctx, AsmCfg *cfg, Entity *en if (f == nullptr) { continue; } - live = (live & ~f->gen_regs) | (f->read_regs & REG_TOP); + live = (live & ~asm_full_kill_mask(f)) | (f->read_regs & REG_TOP); } if (lo != live_out[bi] || live != live_in[bi]) { @@ -841,9 +985,9 @@ gb_internal bool check_asm_cfg_liveness(AsmCtx *asm_ctx, AsmCfg *cfg, Entity *en } 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; + // and that it does not itself read (self-use like `xor r,r` or `add r,x`) is a dead write. + u16 kill = asm_full_kill_mask(f); + u16 dead = kill & ~live_after & ~f->read_regs; for (u16 bit = 1; bit != 0; bit <<= 1) { if ((dead & bit) == 0) { continue; @@ -859,7 +1003,7 @@ gb_internal bool check_asm_cfg_liveness(AsmCtx *asm_ctx, AsmCfg *cfg, Entity *en LIT(f->name), asm_ctx->clobber_reg_bit_name(bit)); } } - live = (live & ~f->gen_regs) | (f->read_regs & REG_TOP); + live = (live & ~kill) | (f->read_regs & REG_TOP); } }