mirror of
https://github.com/odin-lang/Odin.git
synced 2026-08-27 23:41:31 +00:00
Implement a CFG for the assembler to improve the soundness of the checks
This commit is contained in:
@@ -821,6 +821,21 @@ main :: proc() {
|
||||
}
|
||||
""")
|
||||
|
||||
strings.write_string(&sb, """
|
||||
bool is_self_zeroing_idiom(u16 m) const {
|
||||
switch (m) {
|
||||
case M_XOR:
|
||||
case M_SUB:
|
||||
case M_SUBW:
|
||||
case M_SLT:
|
||||
case M_SLTU:
|
||||
case M_ANDN:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
""")
|
||||
|
||||
strings.write_string(&sb, "\n};\n")
|
||||
|
||||
strings.write_string(&sb, "\n\n\n")
|
||||
|
||||
@@ -824,6 +824,45 @@ main :: proc() {
|
||||
}
|
||||
""")
|
||||
|
||||
strings.write_string(&sb, """
|
||||
bool is_self_zeroing_idiom(u16 m) const {
|
||||
switch (m) {
|
||||
// integer xor / sub: x ^ x == 0, x - x == 0
|
||||
case M_XOR:
|
||||
case M_SUB:
|
||||
|
||||
// SSE/AVX bitwise xor of a register with itself
|
||||
case M_PXOR:
|
||||
case M_XORPS:
|
||||
case M_XORPD:
|
||||
case M_VPXOR:
|
||||
case M_VXORPS:
|
||||
case M_VXORPD:
|
||||
|
||||
// packed integer subtract: psub x, x == 0
|
||||
case M_PSUBB:
|
||||
case M_PSUBW:
|
||||
case M_PSUBD:
|
||||
case M_PSUBQ:
|
||||
case M_VPSUBB:
|
||||
case M_VPSUBW:
|
||||
case M_VPSUBD:
|
||||
case M_VPSUBQ:
|
||||
|
||||
// andnot of a value with itself: (~x) & x == 0
|
||||
case M_ANDN: // BMI1 GPR: andn dst, a, a
|
||||
case M_ANDNPS:
|
||||
case M_ANDNPD:
|
||||
case M_PANDN:
|
||||
case M_VANDNPS:
|
||||
case M_VANDNPD:
|
||||
case M_VPANDN:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
""")
|
||||
|
||||
strings.write_string(&sb, "\n};\n")
|
||||
|
||||
strings.write_string(&sb, "\n\n\n")
|
||||
|
||||
@@ -813,6 +813,41 @@ struct Asm_amd64 {
|
||||
break;
|
||||
}
|
||||
return {AsmOperandConstraint_None, -1};
|
||||
} bool is_self_zeroing_idiom(u16 m) const {
|
||||
switch (m) {
|
||||
// integer xor / sub: x ^ x == 0, x - x == 0
|
||||
case M_XOR:
|
||||
case M_SUB:
|
||||
|
||||
// SSE/AVX bitwise xor of a register with itself
|
||||
case M_PXOR:
|
||||
case M_XORPS:
|
||||
case M_XORPD:
|
||||
case M_VPXOR:
|
||||
case M_VXORPS:
|
||||
case M_VXORPD:
|
||||
|
||||
// packed integer subtract: psub x, x == 0
|
||||
case M_PSUBB:
|
||||
case M_PSUBW:
|
||||
case M_PSUBD:
|
||||
case M_PSUBQ:
|
||||
case M_VPSUBB:
|
||||
case M_VPSUBW:
|
||||
case M_VPSUBD:
|
||||
case M_VPSUBQ:
|
||||
|
||||
// andnot of a value with itself: (~x) & x == 0
|
||||
case M_ANDN: // BMI1 GPR: andn dst, a, a
|
||||
case M_ANDNPS:
|
||||
case M_ANDNPD:
|
||||
case M_PANDN:
|
||||
case M_VANDNPS:
|
||||
case M_VANDNPD:
|
||||
case M_VPANDN:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -755,6 +755,17 @@ struct Asm_riscv {
|
||||
break;
|
||||
}
|
||||
return {AsmOperandConstraint_None, -1};
|
||||
} bool is_self_zeroing_idiom(u16 m) const {
|
||||
switch (m) {
|
||||
case M_XOR:
|
||||
case M_SUB:
|
||||
case M_SUBW:
|
||||
case M_SLT:
|
||||
case M_SLTU:
|
||||
case M_ANDN:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,63 +1,4 @@
|
||||
struct AsmBlock {
|
||||
i32 first, last;
|
||||
Array<i32> succs;
|
||||
u16 in_defs;
|
||||
u16 out_defs;
|
||||
PtrSet<Entity *> in_params;
|
||||
PtrSet<Entity *> out_params;
|
||||
bool reachable;
|
||||
};
|
||||
|
||||
struct AsmInstructionFacts {
|
||||
AstAsmInstruction *node;
|
||||
String name;
|
||||
|
||||
u16 gen_regs;
|
||||
u16 read_regs;
|
||||
|
||||
Array<Entity *> gen_params;
|
||||
Array<Entity *> read_params;
|
||||
|
||||
bool is_control;
|
||||
bool is_conditional;
|
||||
bool is_terminal;
|
||||
|
||||
Entity *branch_target;
|
||||
i32 block_id;
|
||||
};
|
||||
|
||||
|
||||
struct AsmMnemonicAccumulator {
|
||||
u16 defined_regs;
|
||||
PtrSet<Entity *> defined_params;
|
||||
|
||||
// Union of registers implicitly clobbered by matched forms (for redundant-#clobber hints).
|
||||
u16 implicit_clobbered_regs;
|
||||
u16 explicitly_produced_regs;
|
||||
u16 stale_outputs;
|
||||
|
||||
bool straight_line;
|
||||
|
||||
// Whether the most-recently-checked instruction terminates straight-line flow.
|
||||
// Reset to false at every label (a label starts a fresh straight-line region whose
|
||||
// tail we haven't seen yet). Consulted after the loop for #diverging templates.
|
||||
bool last_is_terminal;
|
||||
|
||||
// Did the template contain any instructions at all? An empty diverging body can't diverge.
|
||||
bool saw_any_instructions;
|
||||
|
||||
// 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;
|
||||
|
||||
PtrMap<AstAsmInstruction *, AsmInstructionFacts> instruction_facts;
|
||||
};
|
||||
#include "check_asm_cfg.cpp"
|
||||
|
||||
// Bit-width the operand's Odin type occupies in a register/immediate slot.
|
||||
// Integers/floats/bools/pointers -> their size; #simd -> total vector width. 0 if unknown.
|
||||
@@ -1647,51 +1588,6 @@ gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm
|
||||
}
|
||||
}
|
||||
|
||||
if (asm_acc->straight_line) {
|
||||
u16 wants = cast(u16)clobber.implicit_rd & asm_ctx->CLOBBER_REGS_NAMED;
|
||||
u16 undefined = wants & ~asm_acc->defined_regs;
|
||||
|
||||
for (u16 bit = 1; bit != 0; bit <<= 1) {
|
||||
if ((undefined & bit) == 0) {
|
||||
continue;
|
||||
}
|
||||
char const *rname = asm_ctx->clobber_reg_bit_name(bit);
|
||||
|
||||
String owner = {};
|
||||
char const *role = nullptr;
|
||||
for (auto const &ed : tmpl_entity->AsmTemplate.decls) {
|
||||
if (ed.pin.len == 0 || ed.entity == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (asm_ctx->clobber_bit_for_reg_name(ed.pin) != 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 has written %%%s yet; write to it (e.g. into '%.*s') before this instruction",
|
||||
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; "
|
||||
"pin an input parameter to %%%s, or write %%%s before this instruction",
|
||||
LIT(name), rname, rname, rname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u16 produced = cast(u16)clobber.implicit_wr & asm_ctx->CLOBBER_REGS_NAMED;
|
||||
u16 explicit_writes = 0;
|
||||
|
||||
@@ -1762,7 +1658,28 @@ gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm
|
||||
}
|
||||
|
||||
facts.gen_regs = produced | pinned_param_writes;
|
||||
asm_acc->defined_regs |= facts.gen_regs;
|
||||
|
||||
// NOTE(bill): mnemonics such as `xor r, r` / `sub r, r` act as zeroing the destination
|
||||
// independent of its prior value: the read is architecturally dead, so it must not count as a use.
|
||||
bool self_zeroing = false;
|
||||
if (asm_ctx->is_self_zeroing_idiom(cast(u16)mnemonic) && operands.count >= 2) {
|
||||
Entity *e0 = entity_of_node(operands[0].expr);
|
||||
bool all_same = (e0 != nullptr);
|
||||
for (isize k = 1; all_same && k < operands.count; k++) {
|
||||
all_same = entity_of_node(operands[k].expr) == e0;
|
||||
}
|
||||
// also treat literal %reg == %reg as self-zeroing (no entity, compare reg bits)
|
||||
if (!all_same && operands[0].expr->kind == Ast_AsmRegister) {
|
||||
u16 b0 = asm_ctx->clobber_bit_for_reg_name(operands[0].expr->AsmRegister.name.string);
|
||||
all_same = b0 != 0;
|
||||
for (isize k = 1; all_same && k < operands.count; k++) {
|
||||
all_same = operands[k].expr->kind == Ast_AsmRegister &&
|
||||
asm_ctx->clobber_bit_for_reg_name(operands[k].expr->AsmRegister.name.string) == b0;
|
||||
}
|
||||
}
|
||||
self_zeroing = all_same;
|
||||
}
|
||||
|
||||
|
||||
for_array(i, operands) {
|
||||
int slot = user_operand_target_index(cast(int)i);
|
||||
@@ -1773,7 +1690,8 @@ gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm
|
||||
if (pe == nullptr || pe->kind != Entity_Variable) {
|
||||
continue;
|
||||
}
|
||||
if (cast(u16)clobber.read & (1u << slot)) {
|
||||
|
||||
if (!self_zeroing && (cast(u16)clobber.read & (1u << slot))) {
|
||||
array_add(&facts.read_params, pe);
|
||||
}
|
||||
if (cast(u16)clobber.written & (1u << slot)) {
|
||||
@@ -1781,40 +1699,6 @@ gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm
|
||||
}
|
||||
}
|
||||
|
||||
if (asm_acc->straight_line) {
|
||||
// NOTE(bill): check for read-before-write (use of undefined value)
|
||||
// Only really meaningful in straight-line code, so a label above means a branch
|
||||
// could have defined the value out of the textual order
|
||||
for_array(i, operands) {
|
||||
auto const &op = operands[i];
|
||||
int slot = user_operand_target_index(cast(int)i);
|
||||
if (slot < 0 || (cast(u16)clobber.read & (1u << slot)) == 0) {
|
||||
continue; // not a read slot of this form
|
||||
}
|
||||
Entity *pe = entity_of_node(op.expr);
|
||||
if (pe == nullptr || pe->kind != Entity_Variable) {
|
||||
continue; // literal %reg / immediate / memory, not a tracked param
|
||||
}
|
||||
if (!ptr_set_exists(&asm_acc->defined_params, pe)) {
|
||||
error(op.expr, "'%.*s' reads '%.*s' before it is assigned; its initial value is undefined",
|
||||
LIT(name), LIT(pe->token.string));
|
||||
ptr_set_add(&asm_acc->defined_params, pe); // warn once per param
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(bill): record the instruction's parameter writes
|
||||
for_array(i, operands) {
|
||||
int slot = user_operand_target_index(cast(int)i);
|
||||
if (slot < 0 || (cast(u16)clobber.written & (1u << slot)) == 0) {
|
||||
continue;
|
||||
}
|
||||
Entity *pe = entity_of_node(operands[i].expr);
|
||||
if (pe != nullptr && pe->kind == Entity_Variable) {
|
||||
ptr_set_add(&asm_acc->defined_params, pe);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Registers this form clobbers implicitly (RDTSC->RAX:RDX, etc.), for the
|
||||
// redundant-#clobber hint. Union across the template; pinned regs excluded
|
||||
@@ -1842,17 +1726,10 @@ gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm
|
||||
bool halt = clobber.has_halt();
|
||||
// A conditional branch reads a flag and can fall through -> not terminal.
|
||||
bool conditional = clobber.is_conditional();
|
||||
asm_acc->last_is_terminal = halt || (control && !conditional);
|
||||
|
||||
facts.is_control = control;
|
||||
facts.is_conditional = conditional;
|
||||
facts.is_terminal = halt || (control && !conditional);
|
||||
|
||||
if (control) {
|
||||
// A branch/call inside the template means subsequent instructions may be reached
|
||||
// out of textual order; stop trusting the linear def model past this point.
|
||||
asm_acc->straight_line = false;
|
||||
}
|
||||
}
|
||||
asm_ctx->clobber_implicit_regs(&tmpl_entity->AsmTemplate.clobber_registers_set, produced);
|
||||
|
||||
@@ -2426,7 +2303,6 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity
|
||||
|
||||
entity->type = type;
|
||||
|
||||
|
||||
bool is_volatile = false;
|
||||
bool is_align_stack = false;
|
||||
bool is_pure_annotated = false;
|
||||
@@ -2541,44 +2417,10 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity
|
||||
}
|
||||
|
||||
AsmMnemonicAccumulator asm_acc = {};
|
||||
ptr_set_init(&asm_acc.defined_params);
|
||||
defer (ptr_set_destroy(&asm_acc.defined_params));
|
||||
|
||||
map_init(&asm_acc.instruction_facts);
|
||||
defer (map_destroy(&asm_acc.instruction_facts));
|
||||
|
||||
|
||||
// Physical registers known to hold a defined value at the current point in the
|
||||
// straight-line instruction stream. Seeded with input-pinned registers (they
|
||||
// carry their argument at entry); grows as instructions write registers.
|
||||
for (auto const &ed : ate->decls) {
|
||||
if (ed.no_init) {
|
||||
ptr_set_add(&asm_acc.defined_params, ed.entity);
|
||||
}
|
||||
switch (ed.param_group) {
|
||||
case AsmTemplateEntityDeclParamGroup_Input:
|
||||
if (ed.pin.len != 0) {
|
||||
// Only inputs (and the input half of a tie, which is Input-group) hold a
|
||||
// value at entry. Output/scratch pins start undefined and become defined
|
||||
// when an instruction writes them.
|
||||
asm_acc.defined_regs |= asm_ctx->clobber_bit_for_reg_name(ed.pin);
|
||||
}
|
||||
ptr_set_add(&asm_acc.defined_params, ed.entity);
|
||||
break;
|
||||
case AsmTemplateEntityDeclParamGroup_Output:
|
||||
if (ed.tie >= 0) {
|
||||
ptr_set_add(&asm_acc.defined_params, ed.entity);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Linear "written earlier in the text" is only a sound proxy for "produced at
|
||||
// runtime" while control flow is straight-line. The first label is a potential
|
||||
// jump target / back-edge, after which a read can precede its textual def; from
|
||||
// there on we stop emitting the implicit-read diagnostic.
|
||||
asm_acc.straight_line = true;
|
||||
asm_acc.can_be_pure = true;
|
||||
asm_acc.can_be_pure = true;
|
||||
|
||||
// collect label decls
|
||||
for (Ast *instruction_ : at->instructions) {
|
||||
@@ -2678,10 +2520,6 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity
|
||||
case_end;
|
||||
|
||||
case_ast_node(label, AsmLabelDecl, instruction_);
|
||||
asm_acc.straight_line = false;
|
||||
// A new straight-line region begins here; its tail is unseen,
|
||||
// so the previous instruction's terminality no longer describes the body's end.
|
||||
asm_acc.last_is_terminal = false;
|
||||
if (previous_prefix != 0) {
|
||||
error(previous_prefix_instr, "A prefix must be immediately followed by an instruction, but a label declaration was found");
|
||||
previous_prefix = 0;
|
||||
@@ -2780,32 +2618,17 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity
|
||||
error(previous_prefix_instr, "A prefix must be immediately followed by an instruction, but the template ended");
|
||||
}
|
||||
|
||||
for (auto const &ed : ate->decls) {
|
||||
if (ed.param_group != AsmTemplateEntityDeclParamGroup_Output) {
|
||||
continue;
|
||||
}
|
||||
if (ed.tie >= 0) {
|
||||
continue;
|
||||
}
|
||||
if (ed.no_init) {
|
||||
continue;
|
||||
}
|
||||
if (!asm_acc.straight_line) {
|
||||
continue;
|
||||
}
|
||||
// NOTE(bill, 2026-08-24): Construct a control-flow graph (CFG) from the instructions
|
||||
// to do further analysis which is not possible with an conservative straight-line approximation
|
||||
// Using a CFG is a much sounder approach for calculating:
|
||||
// * reads before writes
|
||||
// * divergence
|
||||
// * unreachable code
|
||||
|
||||
bool written = false;
|
||||
if (ed.pin.len != 0) {
|
||||
u16 bit = asm_ctx->clobber_bit_for_reg_name(ed.pin);
|
||||
written = (bit != 0) && (asm_acc.defined_regs & bit) != 0;
|
||||
} else {
|
||||
written = ptr_set_exists(&asm_acc.defined_params, ed.entity);
|
||||
}
|
||||
|
||||
if (!written) {
|
||||
error(ed.entity->token, "'asm' output parameter '%.*s' is never assigned to in this template, thus its value is undefined", LIT(ed.entity->token.string));
|
||||
}
|
||||
}
|
||||
AsmCfg cfg = {};
|
||||
defer (asm_cfg_destroy(&cfg));
|
||||
check_asm_cfg_build(d->init_expr, &asm_acc, &cfg);
|
||||
check_asm_cfg_analyse(asm_ctx, ctx, entity, &cfg, &asm_acc);
|
||||
|
||||
|
||||
bool vet_unused = false;
|
||||
@@ -2866,7 +2689,7 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity
|
||||
}
|
||||
}
|
||||
if (ed.tie > 0) {
|
||||
// TODO(bill): Handle this edge case
|
||||
// TODO(bill): Handle this edge case?
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2893,16 +2716,6 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity
|
||||
"that would require the stack to be realigned");
|
||||
}
|
||||
|
||||
if (type->Proc.diverging) {
|
||||
if (!asm_acc.saw_any_instructions) {
|
||||
error(entity->token, "This asm template is declared as diverging (-> !) but its body is empty and cannot diverge");
|
||||
} else if (!asm_acc.last_is_terminal) {
|
||||
error(entity->token,
|
||||
"This asm template is declared diverging (-> !) but its final instruction can fall through; "
|
||||
"end it with an unconditional jump, return, or halt");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
bool declared_effects = entity->AsmTemplate.is_volatile ||
|
||||
entity->AsmTemplate.clobber_memory ||
|
||||
|
||||
541
src/check_asm_cfg.cpp
Normal file
541
src/check_asm_cfg.cpp
Normal file
@@ -0,0 +1,541 @@
|
||||
struct AsmBlock {
|
||||
i32 first, last;
|
||||
Array<i32> succs;
|
||||
u16 in_defs;
|
||||
u16 out_defs;
|
||||
PtrSet<Entity *> in_params;
|
||||
PtrSet<Entity *> out_params;
|
||||
bool reachable;
|
||||
};
|
||||
|
||||
struct AsmInstructionFacts {
|
||||
AstAsmInstruction *node;
|
||||
String name;
|
||||
|
||||
u16 gen_regs;
|
||||
u16 read_regs;
|
||||
|
||||
Array<Entity *> gen_params;
|
||||
Array<Entity *> read_params;
|
||||
|
||||
bool is_control;
|
||||
bool is_conditional;
|
||||
bool is_terminal;
|
||||
|
||||
Entity *branch_target;
|
||||
i32 block_id;
|
||||
};
|
||||
|
||||
|
||||
struct AsmMnemonicAccumulator {
|
||||
// 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;
|
||||
|
||||
PtrMap<AstAsmInstruction *, AsmInstructionFacts> instruction_facts;
|
||||
};
|
||||
|
||||
struct AsmCfg {
|
||||
Array<AstAsmInstruction *> insts; // program-order (only for fact-carrying instrs)
|
||||
Array<AsmBlock> blocks;
|
||||
PtrMap<Entity *, i32> label_block; // key: Entity_Label*
|
||||
};
|
||||
|
||||
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->label_block);
|
||||
}
|
||||
|
||||
gb_internal void check_asm_cfg_build(Ast *at_node, AsmMnemonicAccumulator *acc, AsmCfg *cfg) {
|
||||
ast_node(at, AsmTemplate, at_node);
|
||||
|
||||
cfg->insts.allocator = heap_allocator();
|
||||
cfg->blocks.allocator = heap_allocator();
|
||||
map_init(&cfg->label_block);
|
||||
|
||||
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) {
|
||||
map_set(&cfg->label_block, 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 = map_get(&acc->instruction_facts, instr);
|
||||
// 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; // the fall-through after a branch starts a new block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for_array(bi, cfg->blocks) { // Calculate the edges for the blocks
|
||||
AsmBlock *b = &cfg->blocks[bi];
|
||||
|
||||
AstAsmInstruction *last = cfg->insts[b->last];
|
||||
AsmInstructionFacts *lf = map_get(&acc->instruction_facts, last);
|
||||
|
||||
i32 branch_succ = -1;
|
||||
bool fallthrough = true;
|
||||
|
||||
if (lf != nullptr && lf->is_control) {
|
||||
if (lf->branch_target != nullptr) {
|
||||
i32 *t = map_get(&cfg->label_block, lf->branch_target);
|
||||
if (t != nullptr && *t < cast(i32)cfg->blocks.count) {
|
||||
branch_succ = *t; // in-range internal target ('jmp .l' / 'jz .l')
|
||||
}
|
||||
// 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, AsmMnemonicAccumulator *acc, i32 bi) {
|
||||
AsmBlock const *b = &cfg->blocks[bi];
|
||||
AstAsmInstruction *last = cfg->insts[b->last];
|
||||
AsmInstructionFacts *lf = map_get(&acc->instruction_facts, last);
|
||||
|
||||
if (lf != nullptr && lf->branch_target != nullptr) {
|
||||
i32 *t = map_get(&cfg->label_block, lf->branch_target);
|
||||
if (t != nullptr && *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 > 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, 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;
|
||||
for (auto const &ed : tmpl_entity->AsmTemplate.decls) {
|
||||
if (ed.pin.len == 0 || ed.entity == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (asm_ctx->clobber_bit_for_reg_name(ed.pin) != 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, CheckerContext *ctx, Entity *entity, AsmCfg *cfg,
|
||||
AsmMnemonicAccumulator *acc) {
|
||||
GB_ASSERT(entity->kind == Entity_AsmTemplate);
|
||||
auto const &decls = entity->AsmTemplate.decls;
|
||||
bool diverging = entity->type->Proc.diverging;
|
||||
|
||||
if (cfg->blocks.count == 0) {
|
||||
// With an empty body, the CFG cannot really do nothing
|
||||
if (diverging && !acc->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;
|
||||
|
||||
PtrMap<Entity *, i32> entity_to_index = {};
|
||||
map_init(&entity_to_index);
|
||||
defer (map_destroy(&entity_to_index));
|
||||
u64 universe_pm = 0;
|
||||
for_array(i, decls) {
|
||||
if (decls[i].entity != nullptr) {
|
||||
map_set(&entity_to_index, decls[i].entity, cast(i32)i);
|
||||
universe_pm |= (cast(u64)1 << i);
|
||||
}
|
||||
}
|
||||
auto bit_of = [&](Entity *e) -> u64 {
|
||||
i32 *ix = map_get(&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;
|
||||
for (auto const &ed : decls) {
|
||||
if (ed.no_init) {
|
||||
seed_pm |= bit_of(ed.entity);
|
||||
if (ed.pin.len != 0) {
|
||||
seed_regs |= asm_ctx->clobber_bit_for_reg_name(ed.pin);
|
||||
}
|
||||
}
|
||||
switch (ed.param_group) {
|
||||
case AsmTemplateEntityDeclParamGroup_Input:
|
||||
seed_pm |= bit_of(ed.entity);
|
||||
if (ed.pin.len != 0) {
|
||||
seed_regs |= asm_ctx->clobber_bit_for_reg_name(ed.pin);
|
||||
}
|
||||
break;
|
||||
case AsmTemplateEntityDeclParamGroup_Output:
|
||||
// NOTE(bill): input provides the value
|
||||
if (ed.tie >= 0) {
|
||||
seed_pm |= bit_of(ed.entity);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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_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()));
|
||||
|
||||
// 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;
|
||||
u64 gp = 0;
|
||||
AsmBlock const &b = cfg->blocks[bi];
|
||||
for (i32 ii = b.first; ii <= b.last; ii++) {
|
||||
AsmInstructionFacts *f = map_get(&acc->instruction_facts, cfg->insts[ii]);
|
||||
if (f == nullptr) {
|
||||
continue;
|
||||
}
|
||||
gr |= f->gen_regs;
|
||||
for (Entity *pe : f->gen_params) {
|
||||
gp |= bit_of(pe);
|
||||
}
|
||||
}
|
||||
gen_regs[bi] = gr;
|
||||
gen_pm[bi] = gp;
|
||||
}
|
||||
|
||||
// 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;
|
||||
} else {
|
||||
in_regs[bi] = REG_TOP;
|
||||
in_pm[bi] = universe_pm;
|
||||
}
|
||||
out_regs[bi] = in_regs[bi] | gen_regs[bi];
|
||||
out_pm[bi] = in_pm[bi] | gen_pm[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;
|
||||
u64 nin_p = seed_pm;
|
||||
if (bi != 0) {
|
||||
nin_r = REG_TOP;
|
||||
nin_p = universe_pm;
|
||||
for (i32 p : preds[bi]) {
|
||||
nin_r &= out_regs[p];
|
||||
nin_p &= out_pm[p];
|
||||
}
|
||||
}
|
||||
u16 nout_r = nin_r | gen_regs[bi];
|
||||
u64 nout_p = nin_p | gen_pm[bi];
|
||||
|
||||
if (nin_r != in_regs[bi] ||
|
||||
nin_p != in_pm[bi] ||
|
||||
nout_r != out_regs[bi] ||
|
||||
nout_p != out_pm[bi]) {
|
||||
in_regs[bi] = nin_r;
|
||||
in_pm[bi] = nin_p;
|
||||
out_regs[bi] = nout_r;
|
||||
out_pm[bi] = nout_p;
|
||||
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];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(bill): unreachable code
|
||||
for (AsmBlock &block : cfg->blocks) {
|
||||
if (block.reachable) {
|
||||
continue;
|
||||
}
|
||||
AstAsmInstruction *first = cfg->insts[block.first];
|
||||
if (block.first == block.last) {
|
||||
warning(first->name, "The asm instruction is unreachable within this block");
|
||||
} else {
|
||||
warning(first->name, "The asm instructions are unreachable within this block");
|
||||
}
|
||||
}
|
||||
|
||||
{ // 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;
|
||||
|
||||
for_array(bi, cfg->blocks) {
|
||||
AsmBlock const &b = cfg->blocks[bi];
|
||||
if (!b.reachable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
u16 run_regs = in_regs[bi];
|
||||
u64 run_pm = in_pm[bi];
|
||||
|
||||
for (i32 ii = b.first; ii <= b.last; ii++) {
|
||||
AstAsmInstruction *instr = cfg->insts[ii];
|
||||
AsmInstructionFacts *f = map_get(&acc->instruction_facts, instr);
|
||||
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;
|
||||
}
|
||||
check_asm_cfg_report_undef_reg(asm_ctx, entity, instr, f->name, bit);
|
||||
reported_regs |= bit;
|
||||
}
|
||||
|
||||
for (Entity *pe : f->read_params) {
|
||||
i32 *ix = map_get(&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;
|
||||
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;
|
||||
bool any_exit = false;
|
||||
for_array(bi, cfg->blocks) {
|
||||
if (!cfg->blocks[bi].reachable) {
|
||||
continue;
|
||||
}
|
||||
if (!check_asm_cfg_block_leaves(cfg, acc, cast(i32)bi)) {
|
||||
continue;
|
||||
}
|
||||
any_exit = true;
|
||||
exit_regs &= out_regs[bi];
|
||||
exit_pm &= out_pm[bi];
|
||||
}
|
||||
|
||||
// NOTE(bill): Outputs must be assigned on every path that returns
|
||||
if (any_exit && !diverging) {
|
||||
for (auto const &ed : decls) {
|
||||
if (ed.param_group != AsmTemplateEntityDeclParamGroup_Output) {
|
||||
continue;
|
||||
}
|
||||
if (ed.tie >= 0 || ed.no_init) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool written;
|
||||
if (ed.pin.len != 0) {
|
||||
u16 bit = asm_ctx->clobber_bit_for_reg_name(ed.pin);
|
||||
written = (bit != 0) && (exit_regs & 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, acc, 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,6 +342,8 @@ struct Entity {
|
||||
String name;
|
||||
Ast *node;
|
||||
Ast *parent;
|
||||
|
||||
i32 asm_block_index;
|
||||
} Label;
|
||||
struct {
|
||||
Ast *node;
|
||||
@@ -577,6 +579,7 @@ gb_internal Entity *alloc_entity_label(Scope *scope, Token token, Type *type, As
|
||||
Entity *entity = alloc_entity(Entity_Label, scope, token, type);
|
||||
entity->Label.node = node;
|
||||
entity->Label.parent = parent;
|
||||
entity->Label.asm_block_index = -1;
|
||||
entity->state = EntityState_Resolved;
|
||||
return entity;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user