Files
Odin/src/check_asm_cfg.cpp
T
gingerBill 654b3a8895 asm: Generalize memory operand syntax to a flat term list
Parse `[...]` as `term { (+|-) term }` where each term is `operand [ (*|<<|>>) operand ]`.

This allows for constant-only terms to be folded in-place, especially for things like displacement.

e.g. `[p + 8*32]` means `[p + 256]`.

Term order is also free: `[8 + p]` == `[p + 8]`
2026-09-07 18:39:59 +01:00

1058 lines
31 KiB
C++

// check_asm_cfg.cpp
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;
Array<i32> succs;
u16 in_defs;
u16 out_defs;
u16 in_flags;
u16 out_flags;
u16 live_in_regs;
u16 live_out_regs;
PtrSet<Entity *> in_params;
PtrSet<Entity *> out_params;
bool reachable;
};
struct AsmInstructionFacts {
AstAsmInstruction *node;
String name;
u16 gen_flags; // flag bits this instruction defines
u16 read_flags;
u16 gen_regs;
u16 read_regs;
// Sub-register widths, indexed by reg bit-index.
AsmRegW read_reg_w;
AsmRegW gen_reg_w;
Array<Entity *> gen_params;
Array<Entity *> read_params;
bool is_control;
bool is_conditional;
bool is_terminal;
Entity *branch_target;
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;
u16 explicitly_produced_regs;
u16 stale_outputs;
bool saw_any_instructions; // NOTE(bill): An empty diverging body cannot diverge.
// NOTE(bill): Related to #align_stack
// any call/branch (CONTROL) or memory effect that could require the stack
// to be realigned. If none occurred, #align_stack is redundant.
bool saw_call_or_mem;
// Purity test
bool can_be_pure;
char const *impure_reason;
Ast * impure_reason_node;
Array<AstAsmInstruction *> insts; // program-order (only for fact-carrying instrs)
Array<AsmBlock> blocks;
PtrMap<Entity *, i32> entity_to_index;
Array<u16> decl_pin_bit;
u64 universe_pm;
};
gb_internal void asm_cfg_init(AsmCfg *cfg) {
map_init(&cfg->entity_to_index);
cfg->decl_pin_bit.allocator = heap_allocator();
cfg->can_be_pure = true;
};
gb_internal void asm_cfg_destroy(AsmCfg *cfg) {
for (auto &block : cfg->blocks) {
array_free(&block.succs);
ptr_set_destroy(&block.in_params);
ptr_set_destroy(&block.out_params);
}
array_free(&cfg->blocks);
array_free(&cfg->insts);
map_destroy(&cfg->entity_to_index);
array_free(&cfg->decl_pin_bit);
}
gb_internal i32 asm_cfg_label_block_index(Entity *entity) {
if (entity != nullptr && entity->kind == Entity_Label) {
return entity->Label.asm_block_index;
}
return -1;
}
gb_internal bool asm_cfg_label_block_index_set(Entity *entity, i32 index) {
if (entity != nullptr && entity->kind == Entity_Label) {
GB_ASSERT(entity->Label.asm_block_index < 0);
entity->Label.asm_block_index = index;
}
return false;
}
// The physical-register bit a decl is pinned to. A width-view carries no pin of
// its own; it inherits its source decl's pin. Returns 0 for unpinned decls.
template <typename AsmCtx>
gb_internal u16 asm_decl_resolve_pin_bit(AsmCtx *asm_ctx, Array<AsmTemplateEntityDecl> const &decls, i32 di) {
if (di < 0 || di >= cast(i32)decls.count) {
return 0;
}
auto const &ed = decls[di];
if (ed.pin.len != 0) {
return asm_ctx->clobber_bit_for_reg_name(ed.pin);
}
if (ed.view_of >= 0 && ed.view_of < cast(i32)decls.count) {
String src_pin = decls[ed.view_of].pin;
if (src_pin.len != 0) {
return asm_ctx->clobber_bit_for_reg_name(src_pin);
}
}
return 0;
}
template <typename AsmCtx>
gb_internal u16 asm_decl_resolve_flag_bit(AsmCtx *asm_ctx, AsmTemplateEntityDecl const &ed) {
if (ed.pin_flag.len == 0) {
return 0;
}
auto flags = asm_ctx->flag_from_name(ed.pin_flag);
return cast(u16)flags;
}
template <typename AsmCtx>
gb_internal void asm_cfg_populate_decls(AsmCtx *asm_ctx, AsmCfg *cfg, Entity *entity) {
auto const &decls = entity->AsmTemplate.decls;
cfg->universe_pm = 0;
if (decls.count > 64) {
// NOTE(bill): check_asm_cfg_analyse will err on this since this is exceed the maximum number of declarations
return;
}
array_resize(&cfg->decl_pin_bit, decls.count);
for_array(i, decls) {
Entity *e = decls[i].entity;
cfg->decl_pin_bit[i] = asm_decl_resolve_pin_bit(asm_ctx, decls, cast(i32)i);
if (e != nullptr) {
if (decls[i].view_of >= 0) {
// NOTE(bill): A view shares its source's lattice bit, as it is not an independent value.
i32 src_i = decls[i].view_of;
Entity *src_e = decls[src_i].entity;
if (src_e != nullptr) {
map_set(&cfg->entity_to_index, e, src_i);
}
// NOTE(bill): No need to set a universe bit for the view as the source already has one
} else {
map_set(&cfg->entity_to_index, e, cast(i32)i);
cfg->universe_pm |= (cast(u64)1 << i);
}
}
}
}
template <typename AsmCtx>
gb_internal void check_asm_cfg_build(AsmCtx *asm_ctx, AsmCfg *cfg, Ast *at_node, Entity *entity) {
ast_node(at, AsmTemplate, at_node);
asm_cfg_populate_decls(asm_ctx, cfg, entity);
cfg->insts.allocator = heap_allocator();
cfg->blocks.allocator = heap_allocator();
bool need_leader = true;
// Build basic blocks over the template body. A leader is: the first instruction, any
// instruction preceded by a label, and any instruction following a control transfer.
for (Ast *node : at->instructions) {
if (node->kind == Ast_AsmLabelDecl) {
// Every label between two instructions names the block the *next* instruction
// opens; consecutive labels share it. A trailing label maps to blocks.count.
Entity *le = node->AsmLabelDecl.name->Ident.entity;
if (le != nullptr) {
asm_cfg_label_block_index_set(le, cast(i32)cfg->blocks.count);
}
need_leader = true;
continue;
}
if (node->kind != Ast_AsmInstruction) {
continue; // directives are straight-line filler; no CFG effect
}
AstAsmInstruction *instr = &node->AsmInstruction;
AsmInstructionFacts *facts = instr->facts;
// Prefixes and pseudo-macro ops (li/la) carry no facts and never branch.
if (need_leader || cfg->blocks.count == 0) {
AsmBlock b = {};
b.first = cast(i32)cfg->insts.count;
b.last = cast(i32)cfg->insts.count;
b.succs.allocator = heap_allocator();
array_add(&cfg->blocks, b);
need_leader = false;
}
i32 bi = cast(i32)cfg->blocks.count - 1;
i32 ii = cast(i32)cfg->insts.count;
array_add(&cfg->insts, instr);
cfg->blocks[bi].last = ii;
if (facts != nullptr) {
facts->block_id = bi;
if (facts->is_control) {
need_leader = true;
}
}
}
for_array(bi, cfg->blocks) { // Calculate the edges for the blocks
AsmBlock *b = &cfg->blocks[bi];
AstAsmInstruction *last = cfg->insts[b->last];
AsmInstructionFacts *lf = last->facts;
i32 branch_succ = -1;
bool fallthrough = true;
if (lf != nullptr && lf->is_control) {
if (lf->branch_target != nullptr) {
i32 t = asm_cfg_label_block_index(lf->branch_target);
if (0 <= t && t < cast(i32)cfg->blocks.count) {
branch_succ = t;
}
// For `t == blocks.count`, this implies a jump to the implicit end, and is handled as "leaves" below
}
// e.g. jmp/ret/hlt (and, conservatively, call) do not fall through in this model.
if (lf->is_terminal) {
fallthrough = false;
}
}
if (branch_succ >= 0) {
array_add(&b->succs, branch_succ);
}
if (fallthrough) {
i32 next = cast(i32)bi + 1;
if (next < cast(i32)cfg->blocks.count) {
array_add(&b->succs, next);
}
}
}
if (cfg->blocks.count != 0) { // Reachability determination
Array<i32> stack = {};
stack.allocator = heap_allocator();
defer (array_free(&stack));
cfg->blocks[0].reachable = true;
array_add(&stack, cast(i32)0);
while (stack.count > 0) {
i32 bi = stack[stack.count-1];
stack.count -= 1;
for (i32 s : cfg->blocks[bi].succs) {
if (s >= 0 && s < cast(i32)cfg->blocks.count && !cfg->blocks[s].reachable) {
cfg->blocks[s].reachable = true;
array_add(&stack, s);
}
}
}
}
}
gb_internal bool check_asm_cfg_block_leaves(AsmCfg *cfg, i32 bi) {
AsmBlock const *b = &cfg->blocks[bi];
AstAsmInstruction *last = cfg->insts[b->last];
AsmInstructionFacts *lf = last->facts;
if (lf != nullptr && lf->branch_target != nullptr) {
i32 t = asm_cfg_label_block_index(lf->branch_target);
if (t >= 0 && t >= cast(i32)cfg->blocks.count) {
return true; // 'jmp .end' — falls into the implicit return
}
}
bool terminal = (lf != nullptr) && lf->is_terminal;
if (!terminal && (bi+1 >= cast(i32)cfg->blocks.count)) {
return true; // straight-line/conditional-tail with nothing after it
}
return false;
}
template <typename AsmCtx>
gb_internal void check_asm_cfg_report_undef_reg(AsmCtx *asm_ctx, AsmCfg *cfg, Entity *tmpl_entity,
AstAsmInstruction *instr, String name, u16 bit) {
char const *rname = asm_ctx->clobber_reg_bit_name(bit);
String owner = {};
char const *role = nullptr;
auto const &decls = tmpl_entity->AsmTemplate.decls;
for_array(i, decls) {
auto const &ed = decls[i];
if (ed.entity == nullptr || cfg->decl_pin_bit[i] != bit) {
continue;
}
if (ed.param_group == AsmTemplateEntityDeclParamGroup_Output && ed.tie < 0) {
owner = ed.entity->token.string;
role = "output";
break;
}
if (ed.param_group == AsmTemplateEntityDeclParamGroup_Scratch && ed.view_of < 0) {
owner = ed.entity->token.string;
role = "scratch";
break;
}
}
if (role != nullptr) {
error(instr->name,
"'%.*s' implicitly reads %%%s, which is bound to the %s parameter '%.*s', "
"but nothing writes %%%s on all paths reaching here; write to it (e.g. into '%.*s') first",
LIT(name), rname, role, LIT(owner), rname, LIT(owner));
} else {
error(instr->name,
"'%.*s' implicitly reads %%%s, but nothing in this template produces a value for it "
"on all paths reaching here; pin an input to %%%s, or write %%%s first",
LIT(name), rname, rname, rname);
}
}
template <typename AsmCtx>
gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerContext *ctx, Entity *entity) {
GB_ASSERT(entity->kind == Entity_AsmTemplate);
auto *ate = &entity->AsmTemplate;
auto const &decls = ate->decls;
bool diverging = entity->type->Proc.diverging;
if (cfg->blocks.count == 0) {
// With an empty body, the CFG cannot really do nothing
if (diverging && !cfg->saw_any_instructions) {
error(entity->token, "This 'asm' template is declared as diverging (-> !) but its body is empty and cannot diverge");
}
return;
}
if (decls.count > 64) {
error(entity->token, "'asm' templates cannot have more than 64 total parameter declarations, got %td", decls.count);
return;
}
u16 const REG_TOP = asm_ctx->CLOBBER_REGS_NAMED;
u16 const FLAG_TOP = cast(u16)~cast(u16)0;
u64 const universe_pm = cfg->universe_pm;
auto bit_of = [&](Entity *e) -> u64 {
i32 *ix = map_get(&cfg->entity_to_index, e);
return ix ? (cast(u64)1 << *ix) : cast(u64)0;
};
// NOTE(bill): entry seed intiailization which mirrors the linear seeding of defined_regs
u16 seed_regs = 0;
u64 seed_pm = 0;
AsmRegW entry_seed_w = {};
for_array(i, decls) {
auto const &ed = decls[i];
u16 pin_bit = cfg->decl_pin_bit[i];
bool is_input_pin = false;
if (ed.no_init) {
seed_pm |= bit_of(ed.entity);
seed_regs |= pin_bit;
is_input_pin = true;
}
switch (ed.param_group) {
case AsmTemplateEntityDeclParamGroup_Input:
seed_pm |= bit_of(ed.entity);
seed_regs |= pin_bit;
is_input_pin = true;
break;
case AsmTemplateEntityDeclParamGroup_Output:
if (ed.tie >= 0) {
seed_pm |= bit_of(ed.entity);
}
break;
}
if (is_input_pin && pin_bit != 0 && ed.entity != nullptr) {
i32 wi = asm_reg_index_from_bit(pin_bit);
if (wi >= 0) {
i32 tw = cast(i32)(type_size_of(ed.entity->type) * 8);
if (tw > 255) tw = 255;
entry_seed_w.e[wi] = gb_max(entry_seed_w.e[wi], cast(u8)tw);
}
}
}
// NOTE(bill): #preserve
u16 preserve_bits = 0;
for (String const &reg : ate->preserve_registers_set) {
preserve_bits |= asm_ctx->clobber_bit_for_reg_name(reg);
}
seed_regs |= preserve_bits;
{
u8 full = cast(u8)(build_context.metrics.ptr_size * 8);
for (i32 idx = 0; idx < ASM_WIDTH_REG_COUNT; idx++) {
if (preserve_bits & cast(u16)(1u << idx)) {
entry_seed_w.e[idx] = gb_max(entry_seed_w.e[idx], full);
}
}
}
isize const n = cfg->blocks.count;
auto in_regs = slice_make<u16>(heap_allocator(), n); defer (slice_free(&in_regs, heap_allocator()));
auto out_regs = slice_make<u16>(heap_allocator(), n); defer (slice_free(&out_regs, heap_allocator()));
auto gen_regs = slice_make<u16>(heap_allocator(), n); defer (slice_free(&gen_regs, heap_allocator()));
auto in_flags = slice_make<u16>(heap_allocator(), n); defer (slice_free(&in_flags, heap_allocator()));
auto out_flags = slice_make<u16>(heap_allocator(), n); defer (slice_free(&out_flags, heap_allocator()));
auto gen_flags = slice_make<u16>(heap_allocator(), n); defer (slice_free(&gen_flags, heap_allocator()));
auto in_pm = slice_make<u64>(heap_allocator(), n); defer (slice_free(&in_pm, heap_allocator()));
auto out_pm = slice_make<u64>(heap_allocator(), n); defer (slice_free(&out_pm, heap_allocator()));
auto gen_pm = slice_make<u64>(heap_allocator(), n); defer (slice_free(&gen_pm, heap_allocator()));
// Sub-register width lattice
auto in_w = slice_make<AsmRegW>(heap_allocator(), n); defer (slice_free(&in_w, heap_allocator()));
auto out_w = slice_make<AsmRegW>(heap_allocator(), n); defer (slice_free(&out_w, heap_allocator()));
auto gen_w = slice_make<AsmRegW>(heap_allocator(), n); defer (slice_free(&gen_w, heap_allocator()));
// predecessors, restricted to reachable blocks
auto preds = slice_make<Array<i32>>(heap_allocator(), n);
for_array(i, preds) {
preds[i].allocator = heap_allocator();
}
defer ({
for_array(i, preds) {
array_free(&preds[i]);
}
slice_free(&preds, heap_allocator());
});
for_array(bi, cfg->blocks) {
AsmBlock *block = &cfg->blocks[bi];
if (!block->reachable) {
continue;
}
for (i32 s : block->succs) {
if (0 <= s && s < cast(i32)n &&
cfg->blocks[s].reachable) {
array_add(&preds[s], cast(i32)bi);
}
}
}
for_array(bi, cfg->blocks) {
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;
if (f == nullptr) {
continue;
}
gr |= f->gen_regs;
gf |= f->gen_flags;
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
// entry is from the seeds and every other reachable block from TOP (intersection)
for_array(bi, cfg->blocks) {
if (!cfg->blocks[bi].reachable) {
continue;
}
if (bi == 0) {
in_regs [bi] = seed_regs;
in_pm [bi] = seed_pm;
in_flags[bi] = 0; // no flag is defined at the template entry point
} else {
in_regs [bi] = REG_TOP;
in_pm [bi] = universe_pm;
in_flags[bi] = FLAG_TOP;
}
out_regs [bi] = in_regs [bi] | gen_regs [bi];
out_pm [bi] = in_pm [bi] | gen_pm [bi];
out_flags[bi] = in_flags[bi] | gen_flags[bi];
}
// forward must-analysis: in = AND(preds.out); out = in | gen. Iterate to fixpoint.
bool changed = true;
while (changed) {
changed = false;
for_array(bi, cfg->blocks) {
if (!cfg->blocks[bi].reachable) {
continue;
}
u16 nin_r = seed_regs;
u16 nin_f = 0;
u64 nin_p = seed_pm;
if (bi != 0) {
nin_r = REG_TOP;
nin_f = FLAG_TOP;
nin_p = universe_pm;
for (i32 p : preds[bi]) {
nin_r &= out_regs[p];
nin_f &= out_flags[p];
nin_p &= out_pm[p];
}
}
u16 nout_r = nin_r | gen_regs[bi];
u64 nout_p = nin_p | gen_pm[bi];
u16 nout_f = nin_f | gen_flags[bi];
if (nin_r != in_regs [bi] ||
nin_p != in_pm [bi] ||
nin_f != in_flags [bi] ||
nout_r != out_regs [bi] ||
nout_p != out_pm [bi] ||
nout_f != out_flags[bi]) {
in_regs [bi] = nin_r;
in_pm [bi] = nin_p;
in_flags [bi] = nin_f;
out_regs [bi] = nout_r;
out_pm [bi] = nout_p;
out_flags[bi] = nout_f;
changed = true;
}
}
}
for_array(bi, cfg->blocks) {
if (!cfg->blocks[bi].reachable) {
continue;
}
in_w[bi] = {};
if (bi == 0) {
in_w[bi] = entry_seed_w;
}
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]);
}
}
} else {
nin = entry_seed_w;
}
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];
b->in_defs = in_regs [bi];
b->out_defs = out_regs [bi];
b->in_flags = in_flags [bi];
b->out_flags = out_flags[bi];
if (!b->reachable) {
continue;
}
for_array(i, decls) {
Entity *e = decls[i].entity;
if (e == nullptr) {
continue;
}
if (((in_pm[bi] >> i) & 1) != 0) {
ptr_set_add(&b->in_params, e);
}
if (((out_pm[bi] >> i) & 1) != 0) {
ptr_set_add(&b->out_params, e);
}
}
}
{
auto block_is_targeted = slice_make<bool>(heap_allocator(), cfg->blocks.count);
defer (slice_free(&block_is_targeted, heap_allocator()));
for_array(bi, cfg->blocks) {
AstAsmInstruction *last = cfg->insts[cfg->blocks[bi].last];
AsmInstructionFacts *lf = last->facts;
if (lf != nullptr && lf->branch_target != nullptr) {
i32 t = asm_cfg_label_block_index(lf->branch_target);
if (0 <= t && t < cast(i32)cfg->blocks.count) {
block_is_targeted[t] = true;
}
}
}
// NOTE(bill): unreachable code
for_array(bi, cfg->blocks) {
AsmBlock const &block = cfg->blocks[bi];
if (block.reachable) {
continue;
}
AstAsmInstruction *first = cfg->insts[block.first];
char const *plural = (block.first == block.last) ? "instruction is" : "instructions are";
if (block_is_targeted[bi]) {
warning(first->name, "The asm %s unreachable: this block is only reached from code that is itself unreachable", plural);
} else {
warning(first->name, "The asm %s unreachable: no branch targets it and control cannot fall through from above (e.g. it follows an unconditional jump, return, or halt)", plural);
}
}
}
{ // NOTE(bill): read-before-write, definite-assignment across the whole CFG
PtrSet<Entity *> reported_params = {};
defer (ptr_set_destroy(&reported_params));
u16 reported_regs = 0;
u16 reported_flags = 0;
for_array(bi, cfg->blocks) {
AsmBlock const &b = cfg->blocks[bi];
if (!b.reachable) {
continue;
}
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];
AsmInstructionFacts *f = instr->facts;
if (f == nullptr) {
continue;
}
u16 undef = f->read_regs & REG_TOP & ~run_regs & ~reported_regs;
for (u16 bit = 1; bit != 0; bit <<= 1) {
if ((undef & bit) == 0) {
continue;
}
reported_regs |= bit;
check_asm_cfg_report_undef_reg(asm_ctx, cfg, entity, instr, f->name, bit);
}
bool is_status_snapshot = false;
if (instr->mnemonic && instr->valid_form_index >= 0) {
auto clobber_forms = asm_ctx->clobber_forms(instr->mnemonic);
if (clobber_forms.count > 0 && instr->valid_form_index < clobber_forms.count) {
auto &clobber = clobber_forms[instr->valid_form_index];
is_status_snapshot = clobber.is_status_snapshot();
}
}
u16 undef_flags = f->read_flags & ~run_flags & ~reported_flags;
if (is_status_snapshot) {
undef_flags = 0;
}
if (undef_flags != 0) {
reported_flags |= undef_flags;
gbString flag_strs = gb_string_make(heap_allocator(), "");
defer (gb_string_free(flag_strs));
isize count = 0;
for (u16 bit = 1; bit != 0; bit <<= 1) {
if (bit & undef_flags) {
if (count++ > 0) {
flag_strs = gb_string_appendc(flag_strs, ", ");
}
char const *f = asm_ctx->clobber_flag_bit_name(bit);
flag_strs = gb_string_appendc(flag_strs, "%flags.");
flag_strs = gb_string_appendc(flag_strs, f);
}
}
char const *plural = "a status flag";
if (count > 0) {
plural = "status flags";
}
error(instr->name,
"'%.*s' reads %s (%s) that is not set on all paths reaching here; "
"a flag-setting instruction (e.g. 'cmp', 'test') must precede it on every path",
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) {
continue;
}
if (((run_pm >> *ix) & 1) == 0 && !ptr_set_exists(&reported_params, pe)) {
Ast *loc = instr->name;
for (Ast *op : instr->operands) {
if (entity_of_node(op) == pe) {
loc = op;
break;
}
}
error(loc, "'%.*s' reads '%.*s' before it is assigned; its initial value is undefined", LIT(f->name), LIT(pe->token.string));
ptr_set_add(&reported_params, pe);
}
}
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);
}
}
}
}
{ // NOTE(bill): Collect the template's return points reachable blocks that leave via the end
u16 exit_regs = REG_TOP;
u64 exit_pm = universe_pm;
u16 exit_flags = FLAG_TOP;
bool any_exit = false;
for_array(bi, cfg->blocks) {
if (!cfg->blocks[bi].reachable) {
continue;
}
if (!check_asm_cfg_block_leaves(cfg, cast(i32)bi)) {
continue;
}
any_exit = true;
exit_regs &= out_regs [bi];
exit_pm &= out_pm [bi];
exit_flags &= out_flags[bi];
}
// NOTE(bill): Outputs must be assigned on every path that returns
if (any_exit && !diverging) {
for_array(i, decls) {
auto const &ed = decls[i];
if (ed.param_group != AsmTemplateEntityDeclParamGroup_Output) {
continue;
}
if (ed.tie >= 0 || ed.no_init) {
continue;
}
bool written = false;
u16 flag_bit = asm_decl_resolve_flag_bit(asm_ctx, ed);
u16 reg_bit = cfg->decl_pin_bit[i];
if (flag_bit != 0) {
// Flag-pinned output: defined iff the pinned flag is set on every returning path.
written = (exit_flags & flag_bit) != 0;
} else if (reg_bit != 0) {
written = (exit_regs & reg_bit) != 0;
} else {
written = (exit_pm & bit_of(ed.entity)) != 0;
}
if (!written) {
error(ed.entity->token,
"'asm' output parameter '%.*s' is not assigned on all paths through this template; "
"its value is undefined",
LIT(ed.entity->token.string));
}
}
}
}
if (diverging) { // No reachable path may return / fall off the end
bool any_leak = false;
for_array(bi, cfg->blocks) {
if (cfg->blocks[bi].reachable && check_asm_cfg_block_leaves(cfg, cast(i32)bi)) {
any_leak = true;
break;
}
}
if (any_leak) {
error(entity->token,
"This 'asm' template is declared diverging (-> !) but a reachable path can fall through the end; "
"end every path with an unconditional jump, return, or halt");
}
} else {
// Not declared diverging: no reachable block leaves => control never returns
bool any_exit = false;
for_array(bi, cfg->blocks) {
auto const &block = cfg->blocks[bi];
if (block.reachable && check_asm_cfg_block_leaves(cfg, cast(i32)bi)) {
any_exit = true;
break;
}
}
if (!any_exit) {
// Distinguish an unconditional self-loop (jmp to own block, no other exit edge)
// from the general no-returning-path case.
i32 self_loop_bi = -1;
for_array(bi, cfg->blocks) {
AsmBlock const &b = cfg->blocks[bi];
if (!b.reachable) {
continue;
}
bool loops_to_self = false;
for (i32 s : b.succs) {
if (s == cast(i32)bi) {
loops_to_self = true;
break;
}
}
if (loops_to_self && b.succs.count == 1) {
self_loop_bi = cast(i32)bi;
break;
}
}
if (self_loop_bi >= 0) {
AstAsmInstruction *first = cfg->insts[cfg->blocks[self_loop_bi].first];
error(first->name,
"This asm instruction forms an unconditional self-loop and can never exit; "
"if the template is meant never to return, declare it diverging (-> !)");
} else {
error(entity->token,
"This 'asm' template has no reachable path that returns or falls through the end; "
"if this is intended, declare it diverging (-> !)");
}
}
}
}
// 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 & ~asm_full_kill_mask(f)) | (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 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;
}
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 & ~kill) | (f->read_regs & REG_TOP);
}
}
return true;
}