// 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 succs; u16 in_defs; u16 out_defs; u16 in_flags; u16 out_flags; u16 live_in_regs; u16 live_out_regs; PtrSet in_params; PtrSet 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 gen_params; Array 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 insts; // program-order (only for fact-carrying instrs) Array blocks; PtrMap entity_to_index; Array 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 gb_internal u16 asm_decl_resolve_pin_bit(AsmCtx *asm_ctx, Array 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 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 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 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 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 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 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 ® : 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(heap_allocator(), n); defer (slice_free(&in_regs, heap_allocator())); auto out_regs = slice_make(heap_allocator(), n); defer (slice_free(&out_regs, heap_allocator())); auto gen_regs = slice_make(heap_allocator(), n); defer (slice_free(&gen_regs, heap_allocator())); auto in_flags = slice_make(heap_allocator(), n); defer (slice_free(&in_flags, heap_allocator())); auto out_flags = slice_make(heap_allocator(), n); defer (slice_free(&out_flags, heap_allocator())); auto gen_flags = slice_make(heap_allocator(), n); defer (slice_free(&gen_flags, heap_allocator())); auto in_pm = slice_make(heap_allocator(), n); defer (slice_free(&in_pm, heap_allocator())); 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) { 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(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 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 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 & ~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; }