Files
Odin/src/check_asm.cpp

2774 lines
95 KiB
C++

// 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.
gb_internal i32 check_asm_operand_bit_width(Type *type) {
if (type == nullptr || type == t_invalid) {
return 0;
}
if (is_type_untyped(type)) {
return -1;
}
if (is_type_boolean(type)) {
return 1;
}
i64 sz = type_size_of(base_type(type));
if (sz <= 0) {
return 0;
}
return cast(i32)(sz * 8);
}
gb_internal bool is_valid_asm_parameter_type(Type *type) {
if (is_type_integer(type)) {
return true;
}
if (is_type_float(type)) {
return true;
}
if (is_type_boolean(type)) {
return true;
}
if (is_type_internally_pointer_like(type)) {
return true;
}
if (is_type_simd_vector(type)) {
return true;
}
return false;
}
gb_internal AsmRegClass check_asm_reg_class_from_type(Type *type) {
if (is_type_integer(type)) {
return AsmRegClass_Integer;
}
if (is_type_float(type)) {
return AsmRegClass_Float;
}
if (is_type_boolean(type)) {
return AsmRegClass_Integer;
}
if (is_type_internally_pointer_like(type)) {
return AsmRegClass_Integer;
}
if (is_type_simd_vector(type)) {
return AsmRegClass_Vector;
}
return AsmRegClass_Unknown;
}
gb_internal AsmOperandKind determine_asm_operand_kind(Operand const *operand) {
if (operand->mode == Addressing_Constant) {
return AsmOperand_Immediate;
}
Ast *expr = operand->expr;
switch (expr->kind) {
case_ast_node(label, AsmLabelDecl, expr);
return AsmOperand_Label;
case_end;
case_ast_node(reg, AsmRegister, expr);
return AsmOperand_Register;
case_end;
case_ast_node(reg, AsmMemoryOperand, expr);
return AsmOperand_Memory;
case_end;
case_ast_node(ident, Ident, expr);
// TODO(bill): Is this correct?
if (expr->tav.mode == Addressing_Constant) {
return AsmOperand_Immediate;
}
Entity *e = entity_of_node(expr);
if (e != nullptr && e->kind == Entity_Variable && (e->flags & EntityFlag_PolyConst) != 0) {
return AsmOperand_Immediate;
}
return AsmOperand_Register;
case_end;
}
return AsmOperand_Invalid;
}
gb_internal void check_asm_pin_type_compat(AsmRegClass reg_class, i32 reg_w, Type *decl_type,
Ast *at, String pin_name, String param_name) {
if (reg_class == AsmRegClass_Unknown ||
reg_w == 0 ||
decl_type == nullptr || decl_type == t_invalid) {
return;
}
AsmRegClass got_class = check_asm_reg_class_from_type(decl_type);
i32 got_w = check_asm_operand_bit_width(decl_type);
bool class_ok;
switch (reg_class) {
case AsmRegClass_Integer: class_ok = (got_class == AsmRegClass_Integer); break;
case AsmRegClass_Vector: class_ok = (got_class == AsmRegClass_Vector || got_class == AsmRegClass_Float); break;
case AsmRegClass_Mask: class_ok = (got_class == AsmRegClass_Mask); break;
default: class_ok = true; break;
}
if (!class_ok) {
error(at, "Parameter '%.*s' is pinned to %%%.*s, but its type is in the wrong register class for that register",
LIT(param_name), LIT(pin_name));
return;
}
// got_w < 0 == untyped constant: skip. Otherwise the value must fit the register.
if (got_w > 0 && got_w > reg_w) {
error(at, "Parameter '%.*s' (%d-bit) is wider than its pinned register %%%.*s (%d-bit)",
LIT(param_name), cast(int)got_w, LIT(pin_name), cast(int)reg_w);
}
}
// Collect param/immediate entities referenced anywhere in an operand expr (incl.
// nested memory sub-operands). Reads Ident.entity, which check_asm_instruction_operand
// populates during operand checking.
template <typename AsmCtx>
gb_internal void check_asm_collect_refs(AsmCtx *asm_ctx, PtrSet<Entity *> *refs, Ast *expr, u16 *touched_regs_) {
if (expr == nullptr) {
return;
}
switch (expr->kind) {
case Ast_Ident:
if (expr->Ident.entity != nullptr) {
ptr_set_add(refs, cast(Entity *)expr->Ident.entity);
}
return;
case Ast_AsmRegister:
// A literal %reg touches a physical register. A pinned scratch/immediate is
// referenced in the body via its pinned register, not its identifier, so record
// the bit; the unused check maps decl pins back through this mask.
if (touched_regs_) *touched_regs_ |= asm_ctx->clobber_bit_for_reg_name(expr->AsmRegister.name.string);
return;
case Ast_AsmMemoryOperand: {
auto *m = &expr->AsmMemoryOperand;
check_asm_collect_refs(asm_ctx, refs, m->segment_override, touched_regs_);
check_asm_collect_refs(asm_ctx, refs, m->base, touched_regs_);
check_asm_collect_refs(asm_ctx, refs, m->index, touched_regs_);
check_asm_collect_refs(asm_ctx, refs, m->scale, touched_regs_);
check_asm_collect_refs(asm_ctx, refs, m->disp, touched_regs_);
return;
}
}
}
enum AsmMismatch : u8 {
AsmMismatch_None,
AsmMismatch_Size, // register / vector width mismatch
AsmMismatch_Class, // register class mismatch
AsmMismatch_ImmRange, // constant immediate does not fit the slot width
AsmMismatch_ImmType, // non-integer constant where an integer immediate is required
AsmMismatch_NamedReg, // slot only a named hardware register can fill
};
// Does a constant immediate value fit a slot of `bits` width (0 == unconstrained)?
// Accepts either a signed or an unsigned interpretation of the bit pattern, which
// matches how the assembler treats imm fields (e.g. both 200 and -56 fit imm8).
gb_internal bool check_asm_immediate_value_fits(ExactValue ev, i32 bits, i32 *needed_, AsmMismatch *mismatch_) {
if (ev.kind == ExactValue_Float) {
// Try to convert it if possible to an integer
ev = exact_value_to_integer(ev);
}
switch (ev.kind) {
case ExactValue_Bool:
// Encodes as 0 or 1; fits any immediate slot with a non-zero width.
if (needed_) *needed_ = 1;
return true;
case ExactValue_Integer: {
mp_int const *v = &ev.value_integer;
i32 mag_bits = cast(i32)mp_count_bits(v);
if (needed_) *needed_ = mag_bits;
if (bits == 0) {
// TODO(bill): is this a decent width?!
bits = 64; // slot does not pin a width, just set a decent default
}
if (mp_iszero(v)) {
return true;
}
if (!mp_isneg(v)) {
// Non-negative: fits if the unsigned bit pattern is <= `bits` wide.
if (mag_bits <= bits) {
return true;
}
} else {
// Negative: fits signed in `bits` iff mp_count_bits(-v - 1) <= bits-1.
// (-v-1 ranges 0 .. 2^(bits-1)-1 for the representable negatives.)
mp_int tmp = {};
mp_init(&tmp);
defer (mp_clear(&tmp));
mp_neg(v, &tmp); // tmp = -v (positive magnitude)
mp_sub_d(&tmp, 1, &tmp); // tmp = -v - 1
i32 nb = cast(i32)mp_count_bits(&tmp);
if (needed_) *needed_ = nb + 1; // signed bit-width, for the diagnostic
if (nb <= bits-1) {
return true;
}
}
if (mismatch_) *mismatch_ = AsmMismatch_ImmRange;
return false;
}
case ExactValue_Float:
// TODO(bill): does any architecture support floating-point immediates?
// amd64 has no floating-point instruction immediates.
if (needed_) *needed_ = 0;
if (mismatch_) *mismatch_ = AsmMismatch_ImmType;
return false;
}
if (mismatch_) *mismatch_ = AsmMismatch_ImmType;
return false;
}
// Returns true if the operand's Odin type is size/class-compatible with the form's slot.
// On mismatch, fills *mismatch_ for a precise diagnostic. `slot` here is the
// resolved OperandType at the correct (implicit-skipped) slot.
template <typename AsmCtx>
gb_internal bool check_asm_operand_size_class(AsmCtx *asm_ctx, typename AsmCtx::OperandType slot, Operand const *operand,
AsmMismatch *mismatch_, i32 *want_bits_, i32 *got_bits_) {
if (mismatch_) *mismatch_ = AsmMismatch_None;
AsmOperandKind slot_kind = asm_ctx->kind_from_operand_type(slot);
if (slot_kind == AsmOperand_Immediate) {
i32 want_w = asm_ctx->operand_type_bit_width(slot); // 32 for OP_IMM32
if (want_bits_) *want_bits_ = want_w;
if (operand->mode != Addressing_Constant) {
return true; // $-immediate, bound per instantiation; defer
}
i32 needed = 0;
ExactValue ev = operand->value;
bool ok = check_asm_immediate_value_fits(ev, want_w, &needed, mismatch_);
if (got_bits_) *got_bits_ = needed;
return ok;
}
// Register / memory-sized slots
AsmRegClass want_class = asm_ctx->operand_type_reg_class(slot);
i32 want_w = asm_ctx->operand_type_bit_width(slot);
// A slot only a named hardware register can fill (segment/control/debug/x87/MMX)
// carries no class and, apart from MMX, no width either. Nothing below would
// reject a template parameter standing in for one.
u16 want_named = asm_ctx->operand_type_named_reg_class(slot);
if (want_named != 0) {
bool ok = false;
if (operand->expr != nullptr && operand->expr->kind == Ast_AsmRegister) {
auto r = asm_ctx->register_lookup(operand->expr->AsmRegister.name.string);
ok = r && asm_ctx->reg_class(asm_ctx->register_codes[r]) == want_named;
}
if (!ok) {
if (mismatch_) *mismatch_ = AsmMismatch_NamedReg;
return false;
}
return true;
}
// A pure-label / sizeless slot imposes no reg width/class.
if (want_class == AsmRegClass_Unknown && want_w == 0) {
return true;
}
// Determine the type whose width/class we actually measure.
//
// Memory operands encode their *access* type as a pointer: `[p]:u8` -> `^u8`,
// with a bare `rawptr` meaning "unsized" (no explicit `:type` annotation). A
// register/immediate/parameter operand measures its own type directly.
Type *measured = operand->type;
bool is_memory = (determine_asm_operand_kind(operand) == AsmOperand_Memory);
if (is_memory) {
if (are_types_identical(measured, t_rawptr)) {
// Unsized memory operand: the width is inferred elsewhere (from the
// register operand or deferred), so nothing to check against here.
if (want_bits_) *want_bits_ = want_w;
return true;
}
measured = type_deref(measured); // ^u8 -> u8
}
AsmRegClass got_class = check_asm_reg_class_from_type(measured);
i32 got_w = check_asm_operand_bit_width(measured);
if (got_w < 0) {
// Untyped constant: width is a property of the value, not the type.
if (operand->mode == Addressing_Constant && operand->value.kind == ExactValue_Integer) {
got_w = cast(i32)mp_count_bits(&operand->value.value_integer);
if (got_w == 0) {
got_w = 1; // zero still occupies a slot
}
} else {
got_w = 0; // unknown; skip the width comparison rather than fake a pass
}
}
if (want_bits_) *want_bits_ = want_w;
if (got_bits_) *got_bits_ = got_w;
// Class check (only when the slot constrains a class).
//
// A *memory* operand against a register-or-memory slot (e.g. OP_XMM_M64) has no
// lane semantics -- it is just N bytes of memory -- so its integer/vector class
// must not be held against the slot's register class. Only width matters for the
// memory interpretation. Register operands still get the full class check.
if (want_class != AsmRegClass_Unknown && !is_memory) {
bool class_ok;
switch (want_class) {
case AsmRegClass_Integer:
class_ok = (got_class == AsmRegClass_Integer);
break;
case AsmRegClass_Vector:
// A scalar float uses only the low lane, so it is valid in any vector
// register slot; a #simd vector matches the vector class exactly.
class_ok = (got_class == AsmRegClass_Vector || got_class == AsmRegClass_Float);
break;
case AsmRegClass_Mask:
class_ok = (got_class == AsmRegClass_Mask);
break;
default:
class_ok = true;
break;
}
if (!class_ok) {
if (mismatch_) *mismatch_ = AsmMismatch_Class;
return false;
}
}
// Width check.
if (want_w != 0 && got_w != 0) {
if (want_class == AsmRegClass_Vector && !is_memory) {
// A scalar float uses only the low lane, so it may be narrower than the
// slot; a #simd vector must match the vector width exactly.
bool width_ok = (got_class == AsmRegClass_Float) ? (got_w <= want_w) : (got_w == want_w);
if (!width_ok) {
if (mismatch_) *mismatch_ = AsmMismatch_Size;
return false;
}
} else if (want_class == AsmRegClass_Float &&
got_class == AsmRegClass_Float && !is_memory &&
!asm_ctx->float_reg_width_is_exact()) {
// NOTE(bill): architectures such as RISC-V have registers which are
// always the architecture width
if (got_w > want_w) {
if (mismatch_) *mismatch_ = AsmMismatch_Size;
return false;
}
} else if (want_class == AsmRegClass_Integer && !is_memory &&
!asm_ctx->integer_reg_width_is_exact()) {
// NOTE(bill): architectures such as RISC-V have registers which are
// always the architecture width
if (got_w > want_w) {
if (mismatch_) *mismatch_ = AsmMismatch_Size;
return false;
}
} else {
// Integer/mask registers on exact-width targets, and all memory operands.
if (want_w != got_w) {
if (mismatch_) *mismatch_ = AsmMismatch_Size;
return false;
}
}
}
return true;
}
enum AsmAddrRole {
AsmAddr_Base,
AsmAddr_Index,
};
// Validate that a resolved base/index operand is a 32- or 64-bit integer register.
// `reg_name` is the literal register string when the operand was an AstAsmRegister
// (so rsp/esp-as-index can be caught), else the empty string.
gb_internal bool check_asm_addr_register(Operand const *operand, AsmAddrRole role, String reg_name, i32 *width_) {
char const *role_name = (role == AsmAddr_Base) ? "base" : "index";
AsmRegClass cls = check_asm_reg_class_from_type(operand->type);
i32 w = check_asm_operand_bit_width(operand->type);
if (width_) *width_ = w;
if (cls != AsmRegClass_Integer) {
char const *got = "non-integer";
if (cls == AsmRegClass_Vector) {
got = "vector";
} else if (cls == AsmRegClass_Mask) {
got = "mask";
}
error(operand->expr, "A memory operand's %s must be an integer register, got a %s value", role_name, got);
return false;
}
if (w != 32 && w != 64) {
error(operand->expr, "A memory operand's %s must be a 32-bit or 64-bit register, got a %d-bit register", role_name, cast(int)w);
return false;
}
if (role == AsmAddr_Index && reg_name.len != 0) {
// rsp/esp cannot be encoded as an index register.
if (reg_name == "rsp" || reg_name == "esp") {
error(operand->expr, "%%%.*s cannot be used as an index register", LIT(reg_name));
return false;
}
}
return true;
}
gb_internal Type *check_asm_template_signature_params(CheckerContext *ctx, Scope *scope, Ast *_params, bool input_parameters, Array<AsmTemplateEntityDecl> *asm_template_entity_decls) {
Type *tuple = alloc_type_tuple();
if (_params == nullptr) {
return tuple;
}
ast_node(field_list, FieldList, _params);
Slice<Ast *> params = field_list->list;
Array<Entity *> variables = {};
variables.allocator = heap_allocator();
i32 param_index = 0;
for (Ast *param : params) {
ast_node(field, Field, param);
bool prev = ctx->allow_polymorphic_types;
ctx->allow_polymorphic_types = false;
Type *type = check_type(ctx, field->type);
ctx->allow_polymorphic_types = prev;
if (!is_valid_asm_parameter_type(type)) {
gbString s = type_to_string(type);
error(field->type, "Invalid type for an asm template. It must be an integer, float, boolean, pointer, multi-pointer, or #simd vector, got '%s'", s);
gb_string_free(s);
continue;
}
for_array(j, field->names) {
Ast *name = field->names[j];
bool is_poly_name = false;
switch (name->kind) {
case Ast_Ident:
break;
case Ast_PolyType:
GB_ASSERT(name->PolyType.specialization == nullptr);
is_poly_name = true;
name = name->PolyType.type;
break;
}
if (!ast_node_expect(name, Ast_Ident)) {
continue;
}
if (is_blank_ident(name)) {
error(name, "All parameters must have a name in an asm template");
continue;
}
Token name_token = name->Ident.token;
Entity *entity = alloc_entity_param(scope, name_token, type, false, /*is_value*/true);
entity->flags |= EntityFlag_Used;
if (is_poly_name) {
entity->flags |= EntityFlag_PolyConst;
if (is_type_internally_pointer_like(type)) {
error(name, "Parameters with a pointer-like type cannot be used as $ immediates");
}
}
Entity *found = scope_insert(scope, entity);
if (found == nullptr) {
array_add(&variables, entity);
AsmTemplateEntityDecl ed = asm_template_entity_decl_default(entity);
if (is_poly_name) {
ed.kind = AsmTemplateEntityDecl_Immediate;
}
if (input_parameters) {
ed.param_group = AsmTemplateEntityDeclParamGroup_Input;
ed.param_index = param_index++;
ed.result_index = -1;
} else {
ed.param_group = AsmTemplateEntityDeclParamGroup_Output;
ed.param_index = -1;
ed.result_index = param_index++;
}
ed.total_index = cast(i32)asm_template_entity_decls->count;
array_add(asm_template_entity_decls, ed);
} else {
TokenPos pos = found->token.pos;
error(name_token,
"Redeclaration of '%.*s' in this scope\n"
"\tat %s",
LIT(name_token.string), token_pos_to_string(pos));
entity = found;
}
}
}
tuple->Tuple.variables = slice_from_array(variables);
return tuple;
}
gb_internal AsmTemplateEntityDeclParamGroup check_asm_find_group(Entity *entity, Array<AsmTemplateEntityDecl> const &asm_template_entity_decls, i32 *index_) {
for_array(i, asm_template_entity_decls) {
auto const &ed = asm_template_entity_decls[i];
if (ed.entity == entity) {
if (index_) *index_ = cast(i32)i;
return ed.param_group;
}
}
if (index_) *index_ = -1;
return AsmTemplateEntityDeclParamGroup_Unknown;
};
gb_internal AsmTemplateEntityDeclKind check_asm_find_kind(Entity *entity, Array<AsmTemplateEntityDecl> const &asm_template_entity_decls) {
for (auto const &ed : asm_template_entity_decls) {
if (ed.entity == entity) {
return ed.kind;
}
}
return AsmTemplateEntityDecl_Invalid;
};
gb_internal bool check_asm_is_immediate_param(Entity *tmpl_entity, Operand const *o) {
Entity *pe = entity_of_node(o->expr);
if (pe != nullptr && pe->kind == Entity_Variable) {
return check_asm_find_kind(pe, tmpl_entity->AsmTemplate.decls) == AsmTemplateEntityDecl_Immediate;
}
return false;
}
template <typename AsmCtx>
gb_internal void check_asm_specs(AsmCtx *asm_ctx, CheckerContext *ctx, Scope *scope, Slice<Ast *> const &specs, Array<AsmTemplateEntityDecl> *asm_template_entity_decls) {
StringSet pin_set = {};
string_set_init(&pin_set, specs.count);
defer (string_set_destroy(&pin_set));
StringSet pin_flag_set = {};
string_set_init(&pin_flag_set, specs.count);
defer (string_set_destroy(&pin_flag_set));
for (Ast *spec_ : specs) {
if (spec_->kind != Ast_AsmSpec) {
continue;
}
ast_node(spec, AsmSpec, spec_);
GB_ASSERT(spec->name->kind == Ast_Ident);
Entity *input = scope_lookup_current(scope, spec->name->Ident.interned, spec->name->Ident.hash);
Entity *other_scratch = nullptr;
String pin = {};
String pin_flag = {};
AsmRegClass pin_reg_class = AsmRegClass_Unknown;
i32 pin_reg_w = 0;
if (spec->value != nullptr) {
if (spec->value->kind == Ast_Ident) {
other_scratch = scope_lookup_current(scope, spec->value->Ident.interned, spec->value->Ident.hash);
if (other_scratch) {
auto group = check_asm_find_group(other_scratch, *asm_template_entity_decls, nullptr);
if (!group) {
error(spec->value, "This must be another parameter, got %.*s", LIT(other_scratch->token.string));
}
} else {
error(spec->value, "Undefined parameter declaration '%.*s'", LIT(spec->value->Ident.token.string));
}
} else {
if (spec->value->kind != Ast_AsmRegister) {
gbString s = expr_to_string(spec->value);
error(spec->value, "Expected an asm register or scratch parameter, got %s", s);
gb_string_free(s);
continue;
}
ast_node(reg, AsmRegister, spec->value);
pin = reg->name.string;
if (pin.len != 0) {
Operand op = {};
if (check_register(asm_ctx, &op, reg)) {
if (reg->flag.string.len) {
GB_ASSERT(pin == "flags");
pin_flag = reg->flag.string;
if (string_set_update(&pin_flag_set, pin_flag)) {
error(spec->value, "Pinned register flag %%%.*s.%.*s has already been assigned", LIT(pin), LIT(pin_flag));
}
}
if (string_set_update(&pin_set, pin) && pin != "flags") {
error(spec->value, "Pinned register %%%.*s has already been assigned", LIT(pin));
}
if (reg->flag.string.len == 0) {
pin_reg_class = check_asm_reg_class_from_type(op.type);
pin_reg_w = check_asm_operand_bit_width(op.type);
}
}
}
}
}
if (spec->tied_name == nullptr) {
if (spec->type != nullptr) {
Type *type = check_type(ctx, spec->type);
if (!is_valid_asm_parameter_type(type)) {
gbString s = type_to_string(type);
error(spec->type, "Invalid type for an asm template. It must be an integer, float, boolean, pointer, multi-pointer, or #simd vector, got '%s'", s);
gb_string_free(s);
continue;
}
Token name_token = spec->name->Ident.token;
Entity *entity = alloc_entity_param(scope, name_token, type, false, /*is_value*/true);
entity->flags |= EntityFlag_Used;
Entity *found = scope_insert(scope, entity);
if (found == nullptr) {
AsmTemplateEntityDecl ed = asm_template_entity_decl_default(entity);
ed.param_group = AsmTemplateEntityDeclParamGroup_Scratch;
ed.total_index = cast(i32)asm_template_entity_decls->count;
ed.pin = pin;
ed.pin_flag = pin_flag;
if (pin.len != 0) {
check_asm_pin_type_compat(pin_reg_class, pin_reg_w, type, spec->value, pin,
spec->name->Ident.token.string);
}
if (other_scratch != nullptr) {
// Width-view of another operand: `p0b: u8 = p0`.
// p0b shares p0's register, viewed at p0b's declared width.
GB_ASSERT(spec->value != nullptr);
i32 src_index = -1;
auto src_group = check_asm_find_group(other_scratch, *asm_template_entity_decls, &src_index);
// 1. The source must already exist and be a register-class operand
// (you cannot take a width-view of an immediate or memory operand).
if (src_index < 0) {
error(spec->value, "'%.*s' must refer to a previously declared parameter", LIT(other_scratch->token.string));
} else {
auto &src = (*asm_template_entity_decls)[src_index];
bool src_is_reg = src_group == AsmTemplateEntityDeclParamGroup_Input ||
src_group == AsmTemplateEntityDeclParamGroup_Output ||
src_group == AsmTemplateEntityDeclParamGroup_Scratch;
if (src.kind == AsmTemplateEntityDecl_Immediate || src.kind == AsmTemplateEntityDecl_Memory) {
src_is_reg = false;
}
if (!src_is_reg) {
error(spec->value, "A width-view can only be taken of a register operand, not '%.*s'", LIT(other_scratch->token.string));
}
// 2. The view width must be a legal sub-register width and no wider
// than the source (only narrowing views exist).
i32 view_w = check_asm_operand_bit_width(type); // this decl's type (u8 -> 8)
i32 src_w = check_asm_operand_bit_width(src.entity->type);
AsmRegClass view_class = check_asm_reg_class_from_type(type);
AsmRegClass src_class = check_asm_reg_class_from_type(src.entity->type);
if (view_class != AsmRegClass_Integer || src_class != AsmRegClass_Integer) {
error(spec->type, "Width-views are only supported for integer registers");
} else {
switch (view_w) {
case 8: case 16: case 32: case 64:
if (view_w > src_w) {
error(spec->type, "A width-view (%d-bit) cannot be wider than its source '%.*s' (%d-bit)",
cast(int)view_w, LIT(other_scratch->token.string), cast(int)src_w);
}
break;
default:
error(spec->type, "A width-view must be an 8, 16, 32, or 64-bit integer type, got a %d-bit type", cast(int)view_w);
break;
}
}
// 3. A view does not carry its own pin; it inherits the source's register.
if (pin.len != 0) {
error(spec->value, "A width-view cannot also be pinned to a register; it inherits the source operand's register");
}
ed.kind = AsmTemplateEntityDecl_Register;
ed.view_of = src_index;
ed.view_bits = view_w;
// A view is not itself an input/output/scratch slot for allocation:
// mark it so the lowering passes skip it. Reuse the Scratch group but
// with view_of >= 0 as the discriminator (see lowering note).
}
}
if (other_scratch == nullptr && check_asm_reg_class_from_type(type) != AsmRegClass_Unknown) {
ed.kind = AsmTemplateEntityDecl_Register;
}
array_add(asm_template_entity_decls, ed);
} else {
TokenPos pos = found->token.pos;
error(name_token,
"Redeclaration of '%.*s' in this scope\n"
"\tat %s",
LIT(name_token.string), token_pos_to_string(pos));
entity = found;
continue;
}
} else if (input == nullptr) {
error(spec->name, "Undefined parameter declaration '%.*s'", LIT(spec->name->Ident.token.string));
continue;
} else {
i32 index = -1;
auto group = check_asm_find_group(input, *asm_template_entity_decls, &index);
gb_unused(group);
GB_ASSERT(index >= 0);
auto *i = &(*asm_template_entity_decls)[index];
if (i->pin.len == 0) {
i->pin = pin;
i->pin_flag = pin_flag;
if (pin_flag.len != 0 && group != AsmTemplateEntityDeclParamGroup_Output) {
error(spec->value, "Input parameters cannot be pinned to a flag style register");
} else if (pin.len != 0 && pin_flag.len == 0) {
check_asm_pin_type_compat(pin_reg_class, pin_reg_w, input->type, spec->value, pin,
input->token.string);
}
} else {
error(spec_, "Asm register has already been pinned");
}
if (other_scratch != nullptr) {
GB_ASSERT(spec->value != nullptr);
error(spec->value, "Another parameter must be assigned/paired with a scratch parameter declaration");
}
}
} else {
GB_ASSERT(spec->tied_name->kind == Ast_Ident);
if (spec->type != nullptr) {
error(spec->type, "Tied register definitions cannot have a defined type since the values are already defined");
}
if (input == nullptr) {
error(spec->name, "Undefined parameter declaration '%.*s'", LIT(spec->name->Ident.token.string));
continue;
}
Entity *output = scope_lookup_current(scope, spec->tied_name->Ident.interned, spec->tied_name->Ident.hash);
if (output == nullptr) {
error(spec->name, "Undefined parameter declaration '%.*s'", LIT(spec->name->Ident.token.string));
continue;
}
i32 input_index = -1;
i32 output_index = -1;
auto input_group = check_asm_find_group(input, *asm_template_entity_decls, &input_index);
auto output_group = check_asm_find_group(output, *asm_template_entity_decls, &output_index);
if (input_group != AsmTemplateEntityDeclParamGroup_Input) {
error(input->token, "Parameter tied with '%.*s' must be an input parameter", LIT(output->token.string));
continue;
}
if (output_group != AsmTemplateEntityDeclParamGroup_Output) {
error(output->token, "Parameter tied with '%.*s' must be an output parameter", LIT(input->token.string));
continue;
}
GB_ASSERT(input_index >= 0);
GB_ASSERT(output_index >= 0);
auto *i = &(*asm_template_entity_decls)[input_index];
auto *o = &(*asm_template_entity_decls)[output_index];
i->tie = output_index;
o->tie = input_index;
i->pin = pin;
o->pin = pin;
if (pin.len != 0) {
check_asm_pin_type_compat(pin_reg_class, pin_reg_w, input->type, spec->value, pin, input->token.string);
check_asm_pin_type_compat(pin_reg_class, pin_reg_w, output->type, spec->value, pin, output->token.string);
}
// Tied parameters share one physical register, so they must be the same register family (both integer, or both vector/float).
// Width may legitimately differ (a narrow read feeding a wide write), so width is intentionally NOT checked.
{
AsmRegClass ic = check_asm_reg_class_from_type(input->type);
AsmRegClass oc = check_asm_reg_class_from_type(output->type);
bool i_int = (ic == AsmRegClass_Integer);
bool o_int = (oc == AsmRegClass_Integer);
bool i_vec = (ic == AsmRegClass_Vector || ic == AsmRegClass_Float);
bool o_vec = (oc == AsmRegClass_Vector || oc == AsmRegClass_Float);
if ((i_int && o_vec) || (i_vec && o_int)) {
error(spec->name, "Tied parameters '%.*s' and '%.*s' share a register but are in different register classes",
LIT(input->token.string), LIT(output->token.string));
}
}
if (other_scratch != nullptr) {
GB_ASSERT(spec->value != nullptr);
error(spec->value, "Another parameter must be assigned/paired with a scratch parameter declaration, not a tie");
}
if (pin_flag.len != 0) {
error(spec->value, "Input parameters, and thus tied parameters, cannot be pinned to a flag style register");
}
}
}
}
template <typename AsmCtx>
gb_internal bool check_register(AsmCtx *asm_ctx, Operand *operand, AstAsmRegister *asm_reg) {
String name = asm_reg->name.string;
if (asm_reg->flag.kind == Token_Ident) {
bool ok = true;
i32 width = 0;
String flag = asm_reg->flag.string;
if (name != "flags") {
error(asm_reg->name, "Register flags can only be called on %%flags");
ok = false;
} else {
i32 bit = asm_ctx->flag_bit_from_name(flag, &width);
if (bit < 0) {
error(asm_reg->flag, "Unknown register %%flags name: %.*s", LIT(flag));
ok = false;
}
}
operand->type = t_bool;
if (width > 1) {
operand->type = t_u8;
}
return ok;
}
auto r = asm_ctx->register_lookup(name);
if (r) {
operand->mode = Addressing_Value;
u16 reg_class = asm_ctx->reg_class(r);
if (reg_class == asm_ctx->REG_CLASS_K) {
// Opmask register: classify as a mask, not a 64-bit integer.
// operand->type = t_asm_mask; // see note if this type does not yet exist
// return true;
}
u16 width_in_bits = asm_ctx->reg_size(r);
switch (width_in_bits) {
case 0:
// a register whose class the width table cannot describe, `%rip` being the only one.
// anchored on the name rather than the operand, which clobbers and pins do not have
error(asm_reg->name, "Asm registers with no operand width are not supported: %%%.*s", LIT(name));
return false;
case 8:
operand->type = t_u8;
break;
case 16:
operand->type = t_u16;
break;
case 32:
operand->type = t_u32;
break;
case 64:
operand->type = t_u64;
break;
case 80:
error(asm_reg->name, "80-bit width asm registers are not supported");
return false;
case 128:
operand->type = alloc_type_simd_vector(4, t_f32);
break;
case 256:
operand->type = alloc_type_simd_vector(8, t_f32);
break;
case 512:
operand->type = alloc_type_simd_vector(16, t_f32);
break;
default:
GB_PANIC("Unhandled register width size: %d", width_in_bits);
break;
}
return true;
}
ERROR_BLOCK();
error(asm_reg->name, "Unknown register for this target platform: %%%.*s", LIT(name));
{
auto dym = did_you_mean_make(heap_allocator(), asm_ctx->register_map.count, name);
defer (did_you_mean_destroy(&dym));
for (auto const &entry : asm_ctx->register_map) {
did_you_mean_append(&dym, entry.key);
}
check_did_you_mean_print(&dym);
}
return false;
}
enum CheckMnemomicResult {
CheckMnemomic_Invalid,
CheckMnemomic_Mnemonic,
CheckMnemomic_PseudoMnemonic,
CheckMnemomic_PseudoMacroMnemonic,
CheckMnemomic_Prefix,
};
template <typename AsmCtx>
gb_internal bool check_pseudo_macro_mnemonic(AsmCtx *asm_ctx, Entity *tmpl_entity,
AstAsmInstruction *instr, Slice<Operand> const &operands) {
if (build_context.metrics.arch != TargetArch_riscv64) {
return false;
}
/*
NOTE(bill): this is probably not even a complete list when it comes to
of the pseudo macro mnemonics, but this currently covers most of them.
It just handles those edge cases directly as the LLVM assembler will
handle them directly any way.
*/
int const XLEN = cast(int)(build_context.metrics.ptr_size*8);
String name = instr->name->Ident.token.string;
auto want_int_reg = [&](Operand const *o, char const *role) {
if (determine_asm_operand_kind(o) != AsmOperand_Register ||
check_asm_reg_class_from_type(o->type) != AsmRegClass_Integer) {
error(o->expr, "'%.*s' %s must be an integer register", LIT(name), role);
return false;
}
int width = check_asm_operand_bit_width(o->type);
if (width > XLEN) {
error(o->expr, "'%.*s' %s is wider than the %d-bit register width, got %d-bits", LIT(name), role, XLEN, width);
return false;
}
return true;
};
if (name == "li") { // li rd, imm — dest reg + assemble-time integer that fits XLEN (or a $-immediate)
if (operands.count != 2) {
error(instr->name, "'%.*s' expects 2 operands, got %td", LIT(name), operands.count);
return true; // it exists but incorrectly handled
}
want_int_reg(&operands[0], "destination");
Operand const *imm = &operands[1];
if (imm->mode == Addressing_Constant) {
ExactValue ev = exact_value_to_integer(imm->value);
if (ev.kind != ExactValue_Integer) {
error(imm->expr, "'%.*s' immediate must be an integer constant", LIT(name));
return true;
}
AsmMismatch m = AsmMismatch_None; i32 needed = 0;
if (!check_asm_immediate_value_fits(ev, XLEN, &needed, &m)) {
gbString vs = exact_value_to_string(ev);
error(imm->expr, "'%.*s' immediate %s does not fit in a %d-bit register (needs %d bits)", LIT(name), vs, XLEN, needed);
gb_string_free(vs);
}
} else if (!check_asm_is_immediate_param(tmpl_entity, imm)) {
error(imm->expr, "'li' source must be a constant integer or a $ immediate parameter");
}
return true;
} else if (name == "la" || name == "lla") { // la / lla rd, symbol — dest reg + a label (or symbol, once representable)
if (operands.count != 2) {
error(instr->name, "'%.*s' expects 2 operands, got %td", LIT(name), operands.count);
// NOTE(bill): it exists but incorrectly handled
return true;
}
want_int_reg(&operands[0], "destination");
if (determine_asm_operand_kind(&operands[1]) != AsmOperand_Label) {
error(operands[1].expr, "'%.*s' source must be a label", LIT(name));
}
return true;
}
return false;
}
template <typename AsmCtx>
gb_internal CheckMnemomicResult check_mnemonic_name(AsmCtx *asm_ctx, AstAsmInstruction *instr, u16 *mnemonic_, u8 *suffix_flags_) {
Token token = instr->name->Ident.token;
GB_ASSERT_MSG(token.kind == Token_Ident || token_is_keyword(token.kind), "got %.*s of kind %.*s", LIT(token.string), LIT(token_strings[token.kind]));
String name = token.string;
auto p = asm_ctx->prefix_lookup(name);
if (p) {
if (mnemonic_) *mnemonic_ = cast(u16)p;
return CheckMnemomic_Prefix;
}
auto m = asm_ctx->mnemonic_lookup(name);
if (m) {
if (mnemonic_) *mnemonic_ = cast(u16)m;
return CheckMnemomic_Mnemonic;
}
auto pm = asm_ctx->pseudo_mnemonic_lookup(name);
if (pm) {
if (mnemonic_) *mnemonic_ = pm;
return CheckMnemomic_PseudoMnemonic;
}
u8 suffix_flags = 0;
auto om = asm_ctx->mnemonic_lookup_ordered(name, &suffix_flags);
if (om) {
if (mnemonic_) *mnemonic_ = cast(u16)om;
if (suffix_flags_) *suffix_flags_ = suffix_flags;
return CheckMnemomic_Mnemonic;
}
auto pmm = asm_ctx->pseudo_macro_mnemonic_lookup(name);
if (pmm) {
if (mnemonic_) *mnemonic_ = cast(u16)pmm;
return CheckMnemomic_PseudoMacroMnemonic;
}
ERROR_BLOCK();
if (instr->operands.count == 0) {
error(instr->name, "Unknown mnemonic/prefix for this target platform: %.*s", LIT(name));
} else {
error(instr->name, "Unknown mnemonic for this target platform: %.*s", LIT(name));
}
auto dym = did_you_mean_make(heap_allocator(), asm_ctx->MNEMONIC_COUNT, name);
defer (did_you_mean_destroy(&dym));
for (u16 i = asm_ctx->M_INVALID+1; i < asm_ctx->MNEMONIC_COUNT; i++) {
String str = asm_ctx->mnemonic_strings[i];
did_you_mean_append(&dym, str);
}
if (instr->operands.count == 0) {
for (u16 i = asm_ctx->PREFIX_INVALID+1; i < asm_ctx->PREFIX_COUNT; i++) {
String str = asm_ctx->prefix_strings[i];
did_you_mean_append(&dym, str);
}
}
check_did_you_mean_print(&dym);
return CheckMnemomic_Invalid;
}
struct AsmMnemonicAccumulator {
u16 defined_regs;
// Union of registers implicitly clobbered by matched forms (for redundant-#clobber hints).
u16 implicit_clobbered_regs;
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;
u16 explicitly_produced_regs;
u16 stale_outputs;
// 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;
};
gb_internal bool check_asm_instr_targets_internal_label(AstAsmInstruction *instr) {
bool saw_label = false;
for (Ast *op : instr->operands) {
if (op->kind == Ast_AsmLabelDecl) {
saw_label = true; // resolved against label_scope during operand checking
}
}
return saw_label;
}
template <typename AsmCtx>
gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tmpl_entity, AstAsmInstruction *instr,
u16 mnemonic, u16 pseudo_mnemonic, Slice<Operand> const &operands,
u8 previous_prefix, Ast *previous_prefix_instr,
AsmMnemonicAccumulator *asm_acc) {
GB_ASSERT(mnemonic > 0);
auto forms = asm_ctx->encoding_forms(mnemonic);
auto clobber_forms = asm_ctx->clobber_forms(mnemonic);
String name = asm_ctx->mnemonic_strings[mnemonic];
auto alias = asm_ctx->pseudo_alias(cast(u16)pseudo_mnemonic);
if (pseudo_mnemonic) {
name = asm_ctx->pseudo_mnemonic_strings[pseudo_mnemonic];
}
bool is_pseudo = pseudo_mnemonic != 0;
int target_explicit_count = is_pseudo ? alias.nargs : -1;
auto form_user_operand_count = [&](typename AsmCtx::Encoding const &form) -> int {
int count = is_pseudo ? target_explicit_count : cast(int)form.explicit_count();
return gb_max(count, 0);
};
auto pseudo_alias_arg_operand_index = [](AsmCtx *asm_ctx, auto a, int arg_index) -> int {
if (arg_index < 0 || arg_index > 2) {
return -1;
}
auto want = cast(typename AsmCtx::AliasSrc)(asm_ctx->AliasSrc_ARG0 + arg_index);
for (int i = 0; i < gb_count_of(a.src); i++) {
if (a.src[i] == want) {
return i;
}
}
return -1;
};
auto user_operand_target_index = [&](int user_i) -> int {
if (!is_pseudo) {
return user_i;
}
return pseudo_alias_arg_operand_index(asm_ctx, alias, user_i);
};
auto operand_slot_type = [&](typename AsmCtx::Encoding const &form, int user_index) -> typename AsmCtx::OperandType {
int raw_slot = -1;
if (is_pseudo) {
raw_slot = user_operand_target_index(user_index);
} else {
raw_slot = asm_ctx->form_explicit_slot(form, user_index);
}
if (0 <= raw_slot && raw_slot < cast(int)gb_count_of(form.ops)) {
return form.ops[raw_slot];
}
return asm_ctx->OP_NONE;
};
auto describe_form = [&](typename AsmCtx::Encoding const &form, typename AsmCtx::Clobber const &clobber) -> gbString {
gbString s = gb_string_make(heap_allocator(), "");
int count = form_user_operand_count(form);
for (int i = 0; i < count; i++) {
auto slot = operand_slot_type(form, i);
AsmOperandKind k = asm_ctx->kind_from_operand_type(slot);
AsmRegClass c = asm_ctx->operand_type_reg_class(slot);
i32 w = asm_ctx->operand_type_bit_width(slot);
if (k == AsmOperand_Label) {
s = gb_string_appendc(s, "label");
} else if (k == AsmOperand_Immediate) {
s = (w > 0) ? gb_string_append_fmt(s, "imm%d", cast(int)w)
: gb_string_appendc(s, "imm");
} else {
char const *reg = "reg";
switch (c) {
case AsmRegClass_Integer: reg = "r"; break;
case AsmRegClass_Float: reg = "f"; break;
case AsmRegClass_Vector: reg = "v"; break;
case AsmRegClass_Mask: reg = "k"; break;
default: reg = "reg"; break;
}
switch (k) {
case AsmOperand_Register:
s = (w > 0) ? gb_string_append_fmt(s, "%s%d", reg, cast(int)w)
: gb_string_appendc(s, reg);
break;
case AsmOperand_Memory:
s = (w > 0) ? gb_string_append_fmt(s, "m%d", cast(int)w)
: gb_string_appendc(s, "m");
break;
case AsmOperand_Register_Or_Memory:
s = (w > 0) ? gb_string_append_fmt(s, "%s/m%d", reg, cast(int)w)
: gb_string_append_fmt(s, "%s/m", reg);
break;
default:
s = gb_string_appendc(s, "operand");
break;
}
}
if (i+1 < count) {
s = gb_string_appendc(s, ", ");
}
switch (k) {
case AsmOperand_Label: // 5 characters
break;
case AsmOperand_Immediate: // 3+ characters
case AsmOperand_Register_Or_Memory:
if (w == 0) {
s = gb_string_appendc(s, " ");
} else if (w < 10) {
s = gb_string_appendc(s, " ");
}
break;
case AsmOperand_Register: // 1+ chacracters
case AsmOperand_Memory:
if (w == 0) {
s = gb_string_appendc(s, " ");
} else if (w < 10) {
s = gb_string_appendc(s, " ");
} else if (w < 100) {
s = gb_string_appendc(s, " ");
}
break;
}
}
bool all_implicit = true;
for (int i = 0; i < count; i++) {
auto slot = operand_slot_type(form, i);
AsmOperandKind k = asm_ctx->kind_from_operand_type(slot);
switch (k) {
case AsmOperand_Label:
case AsmOperand_Register:
case AsmOperand_Memory:
case AsmOperand_Register_Or_Memory:
all_implicit = false;
break;
case AsmOperand_Immediate:
break;
}
if (!all_implicit) {
break;
}
}
if (all_implicit) {
auto print_set = [&](char const *name, u16 bits) {
if (bits == 0) {
return;
}
int count = 0;
s = gb_string_appendc(s, " ");
s = gb_string_appendc(s, name);
s = gb_string_appendc(s, "={");
for (u16 bit = 1; bit != 0; bit <<= 1) {
if ((bits & bit) == 0) {
continue;
}
char const *rname = asm_ctx->clobber_reg_bit_name(bit);
if (count++ > 0) {
s = gb_string_appendc(s, ", ");
}
s = gb_string_appendc(s, rname);
}
s = gb_string_appendc(s, "}");
};
u16 implicit_wr = clobber.implicit_wr & asm_ctx->CLOBBER_REGS_NAMED;
u16 implicit_rd = clobber.implicit_rd & asm_ctx->CLOBBER_REGS_NAMED;
u16 implicit_rw = implicit_wr | implicit_rd;
if (implicit_rw != 0) {
s = gb_string_appendc(s, " //");
}
implicit_wr &= ~implicit_rw;
implicit_rd &= ~implicit_rw;
print_set("read/writes", implicit_rw);
print_set("writes", implicit_wr);
print_set("reads", implicit_rd);
}
return s;
};
auto print_closest_form = [&](isize form_index) {
if (form_index < 0) {
return;
}
gbString desc = describe_form(forms[form_index], clobber_forms[form_index]);
defer (gb_string_free(desc));
String line = make_string(cast(u8 const *)desc, gb_string_length(desc));
line = string_trim_trailing_whitespace(line);
if (line.len == 0) {
error_line("\tClosest form: '%.*s'\n", LIT(name));
} else {
error_line("\tClosest form: '%.*s %.*s'\n", LIT(name), LIT(line));
}
};
auto print_possible_forms = [&]() {
Array<gbString> lines = {};
lines.allocator = heap_allocator();
defer (array_free(&lines));
defer (
for_array(i, lines) {
gb_string_free(lines[i]);
}
);
for_array(fi, forms) {
gbString desc = describe_form(forms[fi], clobber_forms[fi]);
bool dup = false;
for (auto const &l : lines) {
if (gb_string_are_equal(l, desc)) {
dup = true;
break;
}
}
if (dup) {
gb_string_free(desc);
} else {
array_add(&lines, desc);
}
}
if (lines.count == 0) {
return;
}
error_line("\tPossible forms for '%.*s':\n", LIT(name));
error_line("\t(r: int, v: vector, f: float, k: mask, m: memory,\n");
error_line("\t r/m: reg-or-mem, imm: immediate; number: bit=width)\n");
isize const MAX_SHOWN = 32;
isize shown = gb_min(lines.count, MAX_SHOWN);
for (isize i = 0; i < shown; i++) {
String line = make_string(cast(u8 const *)lines[i], gb_string_length(lines[i]));
if (line.len == 0) {
error_line("\t\t%.*s\n", LIT(name)); // zero-operand form
} else {
error_line("\t\t%.*s %.*s\n", LIT(name), LIT(line));
}
}
if (lines.count > shown) {
isize rest = lines.count - shown;
error_line("\t\t... and %td more form%s\n", rest, rest == 1 ? "" : "s");
}
};
int min_count = I32_MAX;
int max_count = -1;
for (auto form : forms) {
int explicit_count = cast(int)form.explicit_count();
min_count = gb_min(min_count, explicit_count);
max_count = gb_max(max_count, explicit_count);
}
min_count = gb_max(min_count, 0);
max_count = gb_max(max_count, 0);
if (is_pseudo) {
min_count = gb_min(min_count, target_explicit_count);
max_count = gb_min(max_count, target_explicit_count);
}
// A prefix that none of this mnemonic's forms can take is unconditionally wrong,
// independent of whether the operands match — catch it even on a match failure.
if (previous_prefix > 0) {
bool any_form_accepts = false;
for (auto &form : forms) {
bool req_mem = false;
if (asm_ctx->prefix_kind_okay(previous_prefix, form, &req_mem)) {
any_form_accepts = true;
break;
}
}
if (!any_form_accepts) {
error(previous_prefix_instr ? previous_prefix_instr : instr->name,
"Asm prefix cannot be applied to '%.*s'", LIT(name));
}
}
auto valid_spots = slice_make<bool>(heap_allocator(), max_count);
defer (slice_free(&valid_spots, heap_allocator()));
auto possible_kinds = slice_make<AsmOperandKind>(heap_allocator(), max_count);
defer (slice_free(&possible_kinds, heap_allocator()));
auto possible_class_kinds = slice_make<AsmRegClass>(heap_allocator(), max_count);
defer (slice_free(&possible_class_kinds, heap_allocator()));
bool matched = false;
isize valid_form_index = -1;
isize best_form = -1;
int best_score = -1;
int best_dist = I32_MAX; // secondary: prefer smaller width distance
int best_pref = -1; // tertiary: prefer wider slots (r64 over r32)
for_array(form_index, forms) {
auto &form = forms[form_index];
if (is_pseudo) {
if (cast(int)form.explicit_count() < target_explicit_count) {
continue;
}
} else {
if (operands.count != cast(int)form.explicit_count()) {
continue;
}
}
int score = 0;
int width_dist = 0;
int width_pref = 0;
for_array(i, operands) {
auto type = operand_slot_type(form, cast(int)i);
Operand const *operand = &operands[i];
AsmOperandKind dst = asm_ctx->kind_from_operand_type(type);
AsmOperandKind src = determine_asm_operand_kind(operand);
bool kind_ok = (dst == src) ||
(dst == AsmOperand_Register_Or_Memory && (src == AsmOperand_Register || src == AsmOperand_Memory));
// Tertiary key: bias toward wider register slots so an r64 form outranks
// an otherwise-equal r32 form.
width_pref += cast(int)asm_ctx->operand_type_bit_width(type);
bool spot_ok = false;
AsmMismatch m = AsmMismatch_None;
if (kind_ok) {
bool mem_unsized = (src == AsmOperand_Memory) && are_types_identical(operand->type, t_rawptr);
if (dst == AsmOperand_Register_Or_Memory && src == AsmOperand_Memory && mem_unsized) {
spot_ok = true; // memory form accepts memory; no size check
} else {
i32 wb_ = 0, gb_ = 0;
spot_ok = check_asm_operand_size_class(asm_ctx, type, operand, &m, &wb_, &gb_);
if (!spot_ok && (m == AsmMismatch_Size || m == AsmMismatch_ImmRange) && wb_ > 0 && gb_ > 0) {
int d = cast(int)wb_ - cast(int)gb_;
width_dist += (d < 0) ? -d : d;
}
}
}
if (spot_ok) {
score += 2;
valid_spots[i] = true;
} else if (kind_ok && m != AsmMismatch_NamedReg) {
// A slot wanting a named hardware register is not a near miss for anything
// else, so it must not outrank a form that merely has the widths wrong.
score += 1; // kind matched, only value/size/class failed
}
}
if (score == operands.count * 2) {
matched = true;
valid_form_index = form_index;
break;
}
// Lexicographic rank: score desc, then width_dist asc, then width_pref desc.
bool better;
if (score != best_score) {
better = score > best_score;
} else if (width_dist != best_dist) {
better = width_dist < best_dist;
} else {
better = width_pref > best_pref;
}
if (better) {
best_score = score;
best_dist = width_dist;
best_pref = width_pref;
best_form = form_index;
}
}
if (operands.count < min_count || operands.count > max_count) {
ERROR_BLOCK();
if (min_count == max_count) {
error(instr->name, "The asm instruction '%.*s' expects %d operands, got %td", LIT(name), max_count, operands.count);
} else {
error(instr->name, "The asm instruction '%.*s' expects %d..=%d operands, got %td", LIT(name), min_count, max_count, operands.count);
}
print_possible_forms();
return;
}
if (matched) {
if (valid_form_index >= 0 && previous_prefix > 0) {
auto &form = forms[valid_form_index];
bool requires_memory_dest = false;
if (asm_ctx->prefix_kind_okay(previous_prefix, form, &requires_memory_dest)) {
if (operands.count != 0 && determine_asm_operand_kind(&operands[0]) != AsmOperand_Memory) {
error(previous_prefix_instr ? previous_prefix_instr : instr->name,
"Asm prefix requires '%.*s' to have a memory destination operand", LIT(name));
}
}
}
GB_ASSERT(tmpl_entity->kind == Entity_AsmTemplate);
GB_ASSERT(valid_form_index >= 0);
instr->mnemonic = mnemonic;
instr->valid_form_index = cast(i32)valid_form_index;
// Handle clobbering from mnemonic
auto clobber = clobber_forms[valid_form_index];
// NOTE(bill): reads_mem/writes_mem are per-FORM capability bits.
// A form with an r/m slot (e.g. add r/m32, imm32) carries them even
// when the operand resolved to a register, e.g. `add x, 123`.
// Count a real access only when an operand actually resolved to memory,
// or the access is implicit (no r/m slot exists to carry the bit: movs/stos/...).
bool has_mem_operand = false;
bool has_rm_slot = false;
auto const &valid_form = forms[valid_form_index];
for_array(i, operands) {
if (determine_asm_operand_kind(&operands[i]) == AsmOperand_Memory) {
has_mem_operand = true;
}
AsmOperandKind op_kind = asm_ctx->kind_from_operand_type(operand_slot_type(valid_form, cast(int)i));
if (op_kind == AsmOperand_Memory || op_kind == AsmOperand_Register_Or_Memory) {
has_rm_slot = true;
}
}
bool mem_is_real = has_mem_operand || !has_rm_slot;
bool internal_branch = clobber.has_control() && check_asm_instr_targets_internal_label(instr);
bool effective_side_effects = clobber.implies_side_effects() && !internal_branch;
tmpl_entity->AsmTemplate.clobber_flags |= clobber.implies_clobber_flags();
tmpl_entity->AsmTemplate.clobber_memory |= clobber.implies_clobber_memory() && mem_is_real;
tmpl_entity->AsmTemplate.is_volatile |= effective_side_effects;
tmpl_entity->AsmTemplate.has_observable_side_effect |= effective_side_effects;
tmpl_entity->AsmTemplate.has_observable_side_effect |= clobber.writes_mem && mem_is_real;
// #align_stack only matters if the body makes a call (which requires the stack
// aligned at the call boundary) or manipulates RSP directly. Plain memory access
// through a parameter pointer does NOT require stack realignment, so
// implies_clobber_memory() is intentionally NOT used here.
if (clobber.is_call_or_mem()) {
asm_acc->saw_call_or_mem = true;
}
u16 pinned_mask = 0;
for (auto const &ed : tmpl_entity->AsmTemplate.decls) {
if (ed.pin.len != 0) {
pinned_mask |= asm_ctx->clobber_bit_for_reg_name(ed.pin);
}
}
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;
u16 written_ops = cast(u16)clobber.written;
u16 pinned_param_writes = 0;
for_array(i, operands) {
int tslot = user_operand_target_index(cast(int)i);
if (tslot < 0 || tslot >= 4 || (written_ops & (1u << tslot)) == 0) {
continue;
}
auto const &op = operands[i];
Ast *e = op.expr;
if (e && e->kind == Ast_AsmRegister) {
u16 b = asm_ctx->clobber_bit_for_reg_name(e->AsmRegister.name.string);
produced |= b;
explicit_writes |= b;
} else {
// NOTE(bill): A write through a pinned parameter (or a width-view of one)
// defines that parameter's physical register for the read-before-write check only
auto written_pinned_reg_bit = [&](Operand const &op) -> u16 {
Entity *pe = entity_of_node(op.expr);
if (pe == nullptr || pe->kind != Entity_Variable) {
return 0;
}
auto const &decls = tmpl_entity->AsmTemplate.decls;
for_array(di, decls) {
auto const &ed = decls[di];
if (ed.entity != pe) {
continue;
}
if (ed.pin.len != 0) {
return asm_ctx->clobber_bit_for_reg_name(ed.pin);
}
// NOTE(bill): A width-view carries no pin of its own and thus it aliases its source's register.
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;
}
return 0;
};
pinned_param_writes |= written_pinned_reg_bit(operands[i]);
}
}
if (is_pseudo) {
// Synthesized register sources (e.g. ra in `jal off` -> `jal ra, off`) also
// write a physical register; record them so ra is treated as produced/clobbered.
u16 synth = 0;
for (int i = 0; i < gb_count_of(alias.src); i++) {
if ((written_ops & (1u << i)) == 0) {
continue;
}
if (alias.src[i] == asm_ctx->AliasSrc_LINK) {
GB_ASSERT(build_context.metrics.arch == TargetArch_riscv64);
synth |= 1<<0; // ClobberReg_RA
}
}
produced |= synth;
explicit_writes |= synth;
}
asm_acc->defined_regs |= produced | pinned_param_writes;
// Registers this form clobbers implicitly (RDTSC->RAX:RDX, etc.), for the
// redundant-#clobber hint. Union across the template; pinned regs excluded
// so a legitimate output pin is never called "redundant".
{
u16 implicit_wr = cast(u16)clobber.implicit_wr & asm_ctx->CLOBBER_REGS_NAMED;
asm_acc->implicit_clobbered_regs |= implicit_wr & ~pinned_mask;
}
// Approximate staleness. An output that was explicitly produced (literal %reg write)
// and is later implicitly clobbered — without this same instruction re-producing it —
// is marked stale. Explicit re-production clears it. Implicitly-produced outputs
// (RDTSC->RDX) are never tracked, so they never false-fire.
{
u16 implicit_clobber = cast(u16)clobber.implicit_wr & asm_ctx->CLOBBER_REGS_NAMED;
asm_acc->explicitly_produced_regs |= explicit_writes;
asm_acc->stale_outputs &= ~explicit_writes;
asm_acc->stale_outputs |= implicit_clobber & asm_acc->explicitly_produced_regs & ~explicit_writes;
}
// Terminality for a #diverging template: this instruction ends straight-line
// flow off the end (jmp/ret/etc. -> CONTROL, hlt/ud2 -> HALT). A conditional
// branch does NOT terminate (it can fall through), so require that the form
// is not merely CONTROL-with-fallthrough. We approximate "unconditional" as
// CONTROL|HALT with no explicit label/operand fallthrough below.
{
bool control = clobber.has_control();
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);
}
// 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.
if (clobber.has_control()) {
asm_acc->straight_line = false;
}
asm_ctx->clobber_implicit_regs(&tmpl_entity->AsmTemplate.clobber_registers_set, produced);
// Purity inference
if (asm_acc->can_be_pure) {
// NOTE(bill): Only the first violating instruction is recorded
// The later ones don't overwrite the reason.
char const *why = nullptr;
if (clobber.writes_mem && mem_is_real) {
why = "it writes to memory";
} else if (clobber.reads_mem && mem_is_real) {
// A load's result depends on memory, which is not a value input.
why = "it reads from memory";
} else if (clobber.is_nondeterministic() ||
(pseudo_mnemonic > 0 && alias.is_nondeterministic())) {
// rdtsc / rdrand / cpuid on x86
// counter/entropy CSR reads on RISC-V.
why = "it is nondeterministic";
} else if (clobber.implies_clobber_memory() && mem_is_real) {
why = "it accesses memory the compiler cannot see";
} else if (effective_side_effects) {
why = "it has an observable side effect";
} else if (!internal_branch && clobber.has_control()) {
// Internal jmp/jcc/ret over the template's own labels stays pure; a call
// or an indirect/external transfer does not.
why = "it possibly transfers control outside the inline 'asm' template";
}
if (why != nullptr) {
asm_acc->can_be_pure = false;
asm_acc->impure_reason = why;
asm_acc->impure_reason_node = instr->name;
}
}
return;
}
// failure path
enum { MAX_VARIANT_COUNT = 32 };
AsmMismatch mismatch[MAX_VARIANT_COUNT] = {};
i32 want_bits[MAX_VARIANT_COUNT] = {};
i32 got_bits[MAX_VARIANT_COUNT] = {};
if (best_form >= 0) {
auto &form = forms[best_form];
for_array(i, operands) {
auto type = operand_slot_type(form, cast(int)i);
AsmOperandKind dst = asm_ctx->kind_from_operand_type(type);
AsmOperandKind src = determine_asm_operand_kind(&operands[i]);
possible_kinds[i] = dst;
possible_class_kinds[i] = asm_ctx->reg_class_from_operand_type(type);
bool kind_ok = (dst == src) ||
(dst == AsmOperand_Register_Or_Memory && (src == AsmOperand_Register || src == AsmOperand_Memory));
if (!kind_ok) {
valid_spots[i] = false;
} else {
AsmMismatch m = AsmMismatch_None;
i32 wb_ = 0;
i32 gb_ = 0;
bool ok = check_asm_operand_size_class(asm_ctx, type, &operands[i], &m, &wb_, &gb_);
valid_spots[i] = ok;
if (!ok && i < MAX_VARIANT_COUNT) {
mismatch[i] = m;
want_bits[i] = wb_;
got_bits[i] = gb_;
}
}
}
}
{
begin_error_block();
bool nearly = best_score >= gb_max(operands.count*2 - 2, 0);
if (nearly) {
error(instr->name, "'%.*s' operands nearly matched the expected encoding forms", LIT(name));
} else {
error(instr->name, "'%.*s' operands matched none of the expected encoding forms", LIT(name));
}
for_array(i, valid_spots) {
if (valid_spots[i] || i >= operands.count) {
continue;
}
auto dst = possible_kinds[i];
AsmOperandKind src = determine_asm_operand_kind(&operands[i]);
AsmRegClass dst_reg_class = possible_class_kinds[i];
AsmRegClass src_reg_class = check_asm_reg_class_from_type(operands[i].type);
AsmMismatch m = (i < MAX_VARIANT_COUNT) ? mismatch[i] : AsmMismatch_None;
end_error_block();
begin_error_block();
if (m == AsmMismatch_ImmRange) {
ExactValue ev = operands[i].value;
gbString vs = exact_value_to_string(ev);
i32 bits_required = 0;
check_asm_immediate_value_fits(ev, want_bits[i], &bits_required, nullptr);
if (bits_required > 0) {
error(operands[i].expr, "'%.*s' operand-%td is a %d-bit immediate value, but the value %s does not fit in the %d-bit immediate this form encodes",
LIT(name), i, bits_required, vs, cast(int)want_bits[i]);
} else {
error(operands[i].expr, "'%.*s' operand-%td is an immediate value, but the value %s does not fit in the %d-bit immediate this form encodes",
LIT(name), i, vs, cast(int)want_bits[i]);
}
gb_string_free(vs);
} else if (m == AsmMismatch_ImmType) {
error(operands[i].expr, "'%.*s' operand-%td: a floating-point constant cannot be used as an immediate",
LIT(name), i);
} else if (m == AsmMismatch_Size && want_bits[i] && got_bits[i]) {
error(operands[i].expr, "'%.*s' operand-%td has the wrong size: expected a %u-bit %.*s operand, got %u-bit",
LIT(name), i,
cast(unsigned)want_bits[i], LIT(asm_reg_class_strings[dst_reg_class]),
cast(unsigned)got_bits[i]);
} else if (m == AsmMismatch_Class) {
error(operands[i].expr, "'%.*s' operand-%td is in the wrong register class, expected %d-bit %.*s %.*s, got %d-bit %.*s %.*s",
LIT(name), i,
want_bits[i], LIT(asm_reg_class_strings[dst_reg_class]), LIT(asm_operand_kind_strings[dst]),
got_bits[i], LIT(asm_reg_class_strings[src_reg_class]), LIT(asm_operand_kind_strings[src]));
} else if (m == AsmMismatch_NamedReg) {
auto slot = operand_slot_type(forms[best_form], cast(int)i);
error(operands[i].expr, "'%.*s' operand-%td must be a named %.*s register, got a %.*s",
LIT(name), i,
LIT(asm_ctx->named_reg_class_string(asm_ctx->operand_type_named_reg_class(slot))),
LIT(asm_operand_kind_strings[src]));
} else if (dst == AsmOperand_Immediate) {
error(operands[i].expr, "'%.*s' operand-%td must be an assemble-time constant or a $ immediate parameter, got a %.*s",
LIT(name), i, LIT(asm_operand_kind_strings[src]));
} else if (dst) {
error(operands[i].expr, "'%.*s' operand-%td has an invalid kind, expected %.*s operand",
LIT(name), i, LIT(asm_operand_kind_expected_strings[dst]));
} else {
error(operands[i].expr, "'%.*s' operand-%td has an invalid kind", LIT(name), i);
}
}
if (nearly && best_form >= 0) {
print_closest_form(best_form);
} else {
print_possible_forms();
}
end_error_block();
}
}
template <typename AsmCtx>
gb_internal void check_asm_instruction_operand(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *entity, Operand *operand, Ast *expr, bool allow_memory_operands) {
if (expr == nullptr) {
return;
}
operand->expr = expr;
operand->mode = Addressing_Invalid;
operand->type = t_invalid;
GB_ASSERT(entity->kind == Entity_AsmTemplate);
auto *ate = &entity->AsmTemplate;
Scope *param_scope = ate->param_scope;
Scope *label_scope = ate->label_scope;
gb_unused(param_scope);
gb_unused(label_scope);
switch (expr->kind) {
case_ast_node(ue, UnaryExpr, expr);
check_expr(ctx, operand, expr);
if (operand->mode != Addressing_Constant) {
error(expr, "Asm operands within unary operands (+ - ~) can only compile time constants");
}
return;
case_end;
case_ast_node(pe, ParenExpr, expr);
check_expr(ctx, operand, expr);
if (operand->mode != Addressing_Constant) {
error(expr, "Asm operands within parentheses can only compile time constants, if they were supported");
} else {
error(expr, "Asm operands with parentheses are not currently supported");
}
return;
case_end;
case_ast_node(i, Ident, expr);
Entity *found = scope_lookup_current(param_scope, i->interned, i->hash);
if (found != nullptr) {
i->entity = found;
operand->mode = Addressing_Value;
operand->type = found->type;
return;
}
found = scope_lookup(param_scope->parent, i->interned, i->hash);
if (found != nullptr) {
if (found->kind == Entity_Constant) {
i->entity = found;
operand->mode = Addressing_Constant;
operand->value = found->Constant.value;
operand->type = found->type;
add_type_and_value(ctx, expr, operand->mode, operand->type, operand->value);
} else {
error(expr, "Only asm parameters or constants are allowed to be used within an 'asm' template");
}
} else {
error(expr, "Undeclared asm parameter or constant '%.*s'", LIT(i->token.string));
}
return;
case_end;
case_ast_node(bl, BasicLit, expr);
check_expr(ctx, operand, expr);
return;
case_end;
case_ast_node(asm_reg, AsmRegister, expr);
check_register(asm_ctx, operand, asm_reg);
return;
case_end;
case_ast_node(mem_op, AsmMemoryOperand, expr);
operand->type = t_rawptr;
operand->mode = Addressing_Value;
if (!allow_memory_operands) {
break;
}
Operand segment_override = {};
check_asm_instruction_operand(asm_ctx, ctx, entity, &segment_override, mem_op->segment_override, false);
if (segment_override.expr == nullptr) {
// okay
} else if (segment_override.expr->kind == Ast_AsmRegister) {
String reg_name = segment_override.expr->AsmRegister.name.string;
auto reg = asm_ctx->register_lookup(reg_name);
auto reg_class = asm_ctx->reg_class(asm_ctx->register_codes[reg]);
if (reg_class != asm_ctx->REG_CLASS_SEG) {
gbString s = expr_to_string(segment_override.expr);
error(segment_override.expr, "A segment override must be a selector register parameter, got %s", s);
gb_string_free(s);
}
} else {
gbString s = expr_to_string(segment_override.expr);
error(segment_override.expr, "A segment override must be a selector register parameter, got %s", s);
gb_string_free(s);
}
Operand base = {};
Operand index = {};
Operand scale = {};
Operand disp = {};
check_asm_instruction_operand(asm_ctx, ctx, entity, &base, mem_op->base, false);
check_asm_instruction_operand(asm_ctx, ctx, entity, &index, mem_op->index, false);
check_asm_instruction_operand(asm_ctx, ctx, entity, &scale, mem_op->scale, false);
check_asm_instruction_operand(asm_ctx, ctx, entity, &disp, mem_op->disp, false);
// NOTE(bill): if the base/index is actually an immediate and there is no scale nor disp,
// then treat it as a disp, and modify the AST too
if (index.expr != nullptr && scale.expr == nullptr && disp.expr == nullptr) {
bool do_swap = index.mode == Addressing_Constant;
if (!do_swap) {
Entity *param_entity = entity_of_node(index.expr);
if (param_entity != nullptr && param_entity->kind == Entity_Variable) {
auto kind = check_asm_find_kind(param_entity, ate->decls);
do_swap = kind == AsmTemplateEntityDecl_Immediate;
}
}
if (do_swap) {
disp = index;
index = {};
mem_op->disp = mem_op->index;
mem_op->index = nullptr;
mem_op->disp_op = mem_op->index_op;
mem_op->index_op = {};
}
}
if (base.expr != nullptr && index.expr == nullptr && scale.expr == nullptr && disp.expr == nullptr) {
bool do_swap = base.mode == Addressing_Constant;
if (!do_swap) {
Entity *param_entity = entity_of_node(base.expr);
if (param_entity != nullptr && param_entity->kind == Entity_Variable) {
auto kind = check_asm_find_kind(param_entity, ate->decls);
do_swap = kind == AsmTemplateEntityDecl_Immediate;
}
}
if (do_swap) {
disp = base;
base = {};
mem_op->disp = mem_op->base;
mem_op->base = nullptr;
}
}
i32 base_w = 0;
i32 index_w = 0;
bool have_base = false;
bool have_index = false;
// base: must resolve to a 32/64-bit integer register
if (base.expr) {
String reg_name = {};
bool ok_kind = true;
if (base.expr->kind == Ast_AsmRegister) {
reg_name = base.expr->AsmRegister.name.string;
ok_kind = check_register(asm_ctx, &base, &base.expr->AsmRegister);
} else {
Entity *param_entity = entity_of_node(base.expr);
if (param_entity == nullptr || param_entity->kind != Entity_Variable) {
gbString s = expr_to_string(base.expr);
error(base.expr, "A base value must be a register parameter, got %s", s);
gb_string_free(s);
ok_kind = false;
} else {
auto kind = check_asm_find_kind(param_entity, ate->decls);
// A pointer/integer parameter used as an address base lowers to a
// register operand, so accept both Register and Memory kinds here.
if (kind != AsmTemplateEntityDecl_Register && kind != AsmTemplateEntityDecl_Memory) {
gbString s = expr_to_string(base.expr);
error(base.expr, "A base value must be a register parameter, got %s", s);
gb_string_free(s);
ok_kind = false;
}
}
}
if (ok_kind) {
have_base = check_asm_addr_register(&base, AsmAddr_Base, reg_name, &base_w);
}
}
// index: must resolve to a 32/64-bit integer register, and not rsp/esp
if (index.expr) {
String reg_name = {};
bool ok_kind = true;
if (index.expr->kind == Ast_AsmRegister) {
reg_name = index.expr->AsmRegister.name.string;
ok_kind = check_register(asm_ctx, &index, &index.expr->AsmRegister);
} else {
Entity *param_entity = entity_of_node(index.expr);
if (param_entity == nullptr || param_entity->kind != Entity_Variable) {
gbString s = expr_to_string(index.expr);
error(index.expr, "An index value must be an integer register, got %s", s);
gb_string_free(s);
ok_kind = false;
} else {
auto kind = check_asm_find_kind(param_entity, ate->decls);
switch (kind) {
case AsmTemplateEntityDecl_Register:
case AsmTemplateEntityDecl_Immediate:
// okay
break;
default:
{
gbString s = expr_to_string(index.expr);
gbString t = type_to_string(index.type);
error(index.expr, "An index must be an integer register, got %s of type %s", s, t);
gb_string_free(t);
gb_string_free(s);
ok_kind = false;
}
break;
}
}
}
if (ok_kind) {
have_index = check_asm_addr_register(&index, AsmAddr_Index, reg_name, &index_w);
}
}
// base and index must be the same width
if (have_base && have_index && base_w != index_w) {
Ast *at = mem_op->base ? mem_op->base : expr;
error(at, "A memory operand's base and index registers must be the same width, got a %d-bit base and a %d-bit index",
cast(int)base_w, cast(int)index_w);
}
// a scale factor is meaningless without an index
if (scale.expr && !index.expr) {
error(scale.expr, "A scale factor requires an index register");
}
// scale: constant 1/2/4/8, or an immediate parameter
for (int i = 0; scale.expr && i == 0; i++) {
if (!is_type_integer(scale.type)) {
gbString s = expr_to_string(scale.expr);
error(scale.expr, "A scale must be a constant integer or an immediate, got %s", s);
gb_string_free(s);
break;
}
if (scale.mode == Addressing_Constant) {
gbString s = exact_value_to_string(scale.value);
defer (gb_string_free(s));
if (scale.value.kind != ExactValue_Integer) {
error(scale.expr, "A scale must be a constant integer or an immediate, got %s", s);
break;
} else {
i64 v = exact_value_to_i64(scale.value);
Token op = mem_op->scale_op;
switch (op.kind) {
case Token_Mul:
switch (v) {
case 1: case 2: case 4: case 8:
// okay
break;
default:
error(scale.expr, "A scale using '*' must be a constant integer or an immediate with the value 1, 2, 4, or 8, got %s", s);
break;
}
break;
case Token_Shl:
case Token_Shr:
switch (v) {
case 0: case 1: case 2: case 3:
// okay
break;
default:
error(scale.expr, "A shifting scale using '%.*s' must be a constant integer or an immediate with the value 0, 1, 2, or 3, got %s", LIT(op.string), s);
break;
}
break;
default:
error(op, "Unknown/unhandled scaling operator '%.*s'", LIT(op.string));
break;
}
if (op.kind == Token_Shr) {
if (build_context.metrics.arch != TargetArch_arm64) {
error(op, "The target platform does not support '%.*s' for shifting scale parameters in memory operands", LIT(op.string));
}
}
}
} else {
Entity *param_entity = entity_of_node(scale.expr);
if (param_entity == nullptr || param_entity->kind != Entity_Variable) {
gbString s = expr_to_string(scale.expr);
error(scale.expr, "A scale must be a constant integer or an immediate, got %s", s);
gb_string_free(s);
break;
}
auto kind = check_asm_find_kind(param_entity, ate->decls);
if (kind != AsmTemplateEntityDecl_Immediate) {
gbString s = expr_to_string(scale.expr);
error(scale.expr, "A scale must be a constant integer or an immediate, got %s", s);
gb_string_free(s);
break;
}
}
}
// displacement: an integer that fits a signed 32-bit value
for (int i = 0; disp.expr && i == 0; i++) {
if (disp.expr->kind == Ast_AsmRegister) {
error(disp.expr, "A displacement must be a constant integer value, got a register");
break;
}
// A displacement must be assemble-time constant. A register-valued
// parameter belongs in the index slot, not the displacement.
if (disp.mode == Addressing_Constant && disp.value.kind == ExactValue_Integer) {
AsmMismatch m = AsmMismatch_None;
i32 needed = 0;
if (!check_asm_immediate_value_fits(disp.value, 32, &needed, &m)) {
gbString vs = exact_value_to_string(disp.value);
error(disp.expr, "A memory displacement must fit in a signed 32-bit value, got %s (needs %d bits)", vs, cast(int)needed);
gb_string_free(vs);
}
break;
}
Entity *param_entity = entity_of_node(disp.expr);
if (param_entity != nullptr && param_entity->kind == Entity_Variable) {
auto kind = check_asm_find_kind(param_entity, ate->decls);
if (kind == AsmTemplateEntityDecl_Immediate) {
// A $-immediate parameter is a legal (assemble-time) displacement.
break;
}
if (kind == AsmTemplateEntityDecl_Register) {
error(disp.expr, "A register parameter cannot be a displacement; use it as an index, e.g. [base + %.*s]", LIT(disp.expr->Ident.token.string));
break;
}
}
gbString s = expr_to_string(disp.expr);
error(disp.expr, "A displacement must be a constant integer or immediate, got %s", s);
gb_string_free(s);
}
if (index.expr != nullptr) {
if (!asm_ctx->supports_memory_index_not_just_disp()) {
error(index.expr, "The target platform does not support memory indexing within memory operands, only displacements");
}
}
if (scale.expr != nullptr) {
if (!asm_ctx->supports_memory_index_not_just_disp()) {
error(scale.expr, "The target platform does not support memory index scaling within memory operands");
}
}
if (mem_op->type) {
Type *t = check_type(ctx, mem_op->type);
if (t != nullptr && t != t_invalid) {
if (is_valid_asm_parameter_type(t) && !is_type_internally_pointer_like(t)) {
operand->type = alloc_type_pointer(t);
} else {
gbString s = type_to_string(t);
error(mem_op->type, "Asm memory operands type interpretation must be either an integer, boolean, float, or #simd vector, got %s", s);
gb_string_free(s);
// leave operand->type == t_rawptr ("unsized")
}
}
}
return;
case_end;
case_ast_node(label, AsmLabelDecl, expr);
ast_node(name, Ident, label->name);
Entity *found = scope_lookup_current(label_scope, name->interned, name->hash);
if (found == nullptr) {
error(expr, "Undeclared asm label '.%.*s'", LIT(name->token.string));
}
name->entity = found;
if (found != nullptr) {
found->flags |= EntityFlag_Used;
add_type_and_value(ctx, expr, Addressing_Value, found->type, {});
}
return;
case_end;
}
{
gbString s = expr_to_string(expr);
error(expr, "Invalid asm operand, got %s", s);
gb_string_free(s);
}
return;
}
template <typename AsmCtx>
gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *entity, DeclInfo *d) {
GB_ASSERT(entity->kind == Entity_AsmTemplate);
auto *ate = &entity->AsmTemplate;
String asm_template_name = entity->token.string;
gb_unused(asm_template_name);
ast_node(at, AsmTemplate, d->init_expr);
GB_ASSERT(at->signature != nullptr);
if (at->signature->kind != Ast_ProcType) {
error(at->signature, "Expected a valid signature, got %.*s", LIT(ast_strings[at->signature->kind]));
return;
}
AstProcType *pt = &at->signature->ProcType;
ate->param_scope = create_scope(ctx->info, ctx->scope);
ate->label_scope = create_scope(ctx->info, ctx->scope);
ate->decls.allocator = heap_allocator();
Type *params = check_asm_template_signature_params(ctx, ate->param_scope, pt->params, true, &ate->decls);
Type *results = check_asm_template_signature_params(ctx, ate->param_scope, pt->results, false, &ate->decls);
Type *type = alloc_type_proc(ate->param_scope, params, params->Tuple.variables.count, results, results->Tuple.variables.count, false, pt->calling_convention);
type->Proc.diverging = pt->diverging;
if (!type->Proc.diverging && results->Tuple.variables.count != 0) {
// always require the results of `asm` templates
type->Proc.require_results = true;
}
entity->type = type;
bool is_volatile = false;
bool is_align_stack = false;
bool is_pure_annotated = false;
auto *clobber_registers_set = &entity->AsmTemplate.clobber_registers_set;
check_asm_specs(asm_ctx, ctx, ate->param_scope, at->specs, &ate->decls);
{ // check clobbers
bool clobber_flags = false;
bool clobber_memory = false;
for (Ast *clobber_ : at->clobbers) {
ast_node(clobber, AsmClobber, clobber_);
if (clobber->value == nullptr) {
if (clobber->name.string == "volatile") {
if (is_volatile) {
error(clobber->name, "#volatile has already been defined as an asm specification");
}
is_volatile = true;
} else if (clobber->name.string == "align_stack") {
if (is_align_stack) {
error(clobber->name, "#align_stack has already been defined as an asm specification");
}
is_align_stack = true;
} else if (clobber->name.string == "pure") {
if (is_pure_annotated) {
error(clobber->name, "#pure has already been defined as an asm specification");
}
is_pure_annotated = true;
} else {
error(clobber->name, "Unknown clobber directive '#%.*s'", LIT(clobber->name.string));
}
continue;
}
switch (clobber->value->kind) {
case_ast_node(asm_reg, AsmRegister, clobber->value)
String reg = asm_reg->name.string;
if (asm_reg->flag.string != "") {
error(asm_reg->flag, "#clobber on specific flags is not allowed");
}
Operand operand = {};
if (check_register(asm_ctx, &operand, asm_reg)) {
if (string_set_update(clobber_registers_set, reg)) {
error(clobber->value, "#clobber %%%.*s has already been defined", LIT(reg));
}
}
case_end;
case_ast_node(ident, Ident, clobber->value);
String str = ident->token.string;
if (str == "flags") {
if (clobber_flags) {
error(clobber->value, "#clobber flags has already been defined");
}
clobber_flags = true;
} else if (str == "memory") {
if (clobber_memory) {
error(clobber->value, "#clobber memory has already been defined");
}
clobber_memory = true;
} else {
error(clobber->value, "Expected either a register, 'flags', or 'memory' for a '#clobber' specification, got '%.*s'", LIT(str));
}
case_end;
default:
error(clobber->value, "Expected either a register, 'flags', or 'memory' for a '#clobber' specification");
break;
}
}
entity->AsmTemplate.clobber_flags = clobber_flags;
entity->AsmTemplate.clobber_memory = clobber_memory;
entity->AsmTemplate.is_volatile = is_volatile;
entity->AsmTemplate.is_align_stack = is_align_stack;
}
// add normalizations for the reigsters too
for (String const &reg : *clobber_registers_set) {
u16 bit = asm_ctx->clobber_bit_for_reg_name(reg);
String rname = make_string_c(asm_ctx->clobber_reg_bit_name(bit));
if (rname != reg) {
string_set_update(clobber_registers_set, rname);
}
}
// Two distinct operands pinned to the same physical register only makes sense when
// they are tied (they intentionally share one register). Compared by bit so %eax
// and %rax collide. Flag pins ("flags") yield bit 0 and are skipped.
for_array(i, ate->decls) {
auto const &a = ate->decls[i];
if (a.pin.len == 0) {
continue;
}
u16 abit = asm_ctx->clobber_bit_for_reg_name(a.pin);
if (abit == 0) {
continue;
}
for (isize j = i+1; j < ate->decls.count; j++) {
auto const &b = ate->decls[j];
if (b.pin.len == 0 || asm_ctx->clobber_bit_for_reg_name(b.pin) != abit) {
continue;
}
bool tied = (a.tie == cast(i32)j) || (b.tie == cast(i32)i);
if (tied) {
continue;
}
error(b.entity ? b.entity->token : entity->token,
"Parameters '%.*s' and '%.*s' are both pinned to %%%s but are not tied",
LIT(a.entity->token.string), LIT(b.entity->token.string),
asm_ctx->clobber_reg_bit_name(abit));
}
}
AsmMnemonicAccumulator asm_acc = {};
// 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.pin.len == 0) {
continue;
}
// 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.
if (ed.param_group == AsmTemplateEntityDeclParamGroup_Input) {
asm_acc.defined_regs |= asm_ctx->clobber_bit_for_reg_name(ed.pin);
}
}
// 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;
// collect label decls
for (Ast *instruction_ : at->instructions) {
switch (instruction_->kind) {
case_ast_node(label, AsmLabelDecl, instruction_);
GB_ASSERT(label->name->kind == Ast_Ident);
Ast *name = label->name;
if (is_blank_ident(name)) {
error(name, "Asm label definition cannot be '_'");
continue;
}
Entity *label_entity = alloc_entity_label(ate->label_scope, name->Ident.token, nullptr, instruction_, nullptr);
Entity *found = scope_insert(ate->label_scope, label_entity);
if (found != nullptr) {
TokenPos pos = found->token.pos;
error(name,
"Redeclaration of the label '%.*s' in this scope\n"
"\tat %s",
LIT(name->Ident.token.string), token_pos_to_string(pos));
continue;
}
name->Ident.entity = label_entity;
case_end;
}
}
Array<Operand> operands = {};
operands.allocator = heap_allocator();
array_reserve(&operands, 16);
defer (array_free(&operands));
u8 previous_prefix = 0;
Ast *previous_prefix_instr = nullptr; // for a good error location
for (Ast *instruction_ : at->instructions) {
switch (instruction_->kind) {
case_ast_node(instr, AsmInstruction, instruction_);
GB_ASSERT(instr->name->kind == Ast_Ident);
u16 mnemonic = 0;
u8 suffix_flags = 0;
CheckMnemomicResult res = check_mnemonic_name(asm_ctx, instr, &mnemonic, &suffix_flags);
array_clear(&operands);
for (Ast *expr : instr->operands) {
Operand operand = {};
check_asm_instruction_operand(asm_ctx, ctx, entity, &operand, expr, /*allow_memory_operands*/true);
array_add(&operands, operand);
}
if (res == CheckMnemomic_Prefix) {
if (instr->operands.count != 0) {
error(instr->name, "A prefix must not have any operands, and be separate from the instruction it is prefixing");
}
if (previous_prefix != 0) {
error(instr->name, "A prefix cannot immediately follow another prefix");
}
previous_prefix = cast(u8)mnemonic;
previous_prefix_instr = instruction_;
} else if (res == CheckMnemomic_Mnemonic) {
instr->suffix_flags = suffix_flags;
check_mnemonic(asm_ctx, ctx, entity, instr, mnemonic, 0, slice_from_array(operands),
previous_prefix, previous_prefix_instr,
&asm_acc);
asm_acc.saw_any_instructions = true;
previous_prefix = 0;
previous_prefix_instr = nullptr;
} else if (res == CheckMnemomic_PseudoMnemonic) {
instr->suffix_flags = suffix_flags;
u16 pseudo_mnemonic = cast(u16)mnemonic;
auto alias = asm_ctx->pseudo_alias(cast(u16)pseudo_mnemonic);
u16 target_mnemonic = cast(u16)alias.target;
check_mnemonic(asm_ctx, ctx, entity, instr, target_mnemonic, pseudo_mnemonic, slice_from_array(operands),
previous_prefix, previous_prefix_instr,
&asm_acc);
asm_acc.saw_any_instructions = true;
previous_prefix = 0;
previous_prefix_instr = nullptr;
} else if (res == CheckMnemomic_PseudoMacroMnemonic) {
instr->suffix_flags = suffix_flags;
check_pseudo_macro_mnemonic(asm_ctx, entity, instr, slice_from_array(operands));
asm_acc.saw_any_instructions = true;
previous_prefix = 0;
previous_prefix_instr = nullptr;
} else {
// invalid mnemonic already reported; a pending prefix now has no target
previous_prefix = 0;
previous_prefix_instr = nullptr;
}
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;
previous_prefix_instr = nullptr;
}
case_end;
case_ast_node(dir, AsmDirective, instruction_);
String name = dir->name.string;
if (name == "byte") {
if (dir->operands.count == 0) {
error(dir->name, "Expected 1 or more integers for the asm directive #%.*s", LIT(name));
break;
}
array_clear(&operands);
for (Ast *expr : dir->operands) {
Operand operand = {};
check_asm_instruction_operand(asm_ctx, ctx, entity, &operand, expr, /*allow_memory_operands*/true);
array_add(&operands, operand);
}
for (auto const &op : operands) {
if (op.mode != Addressing_Constant) {
error(op.expr, "Expected an integer for the asm directive #%.*s", LIT(name));
continue;
}
ExactValue ev = exact_value_to_integer(op.value);
if (ev.kind != ExactValue_Integer) {
error(op.expr, "Expected an integer for the asm directive #%.*s", LIT(name));
continue;
}
i64 i = exact_value_to_i64(ev);
if (i < 0 || i > 255) {
error(op.expr, "Expected an integer within 0..<256 for the asm directive #%.*s, got %lld", LIT(name), cast(long long)i);
continue;
}
}
} else if (name == "align") {
if (dir->operands.count != 1) {
error(dir->name, "Expected 1 integer for the asm directive #%.*s", LIT(name));
break;
}
array_clear(&operands);
for (Ast *expr : dir->operands) {
Operand operand = {};
check_asm_instruction_operand(asm_ctx, ctx, entity, &operand, expr, /*allow_memory_operands*/true);
array_add(&operands, operand);
}
for (auto const &op : operands) {
if (op.mode != Addressing_Constant) {
error(op.expr, "Expected a power-of-two integer for the asm directive #%.*s", LIT(name));
continue;
}
ExactValue ev = exact_value_to_integer(op.value);
if (ev.kind != ExactValue_Integer) {
error(op.expr, "Expected a power-of-two integer for the asm directive #%.*s", LIT(name));
continue;
}
i64 i = exact_value_to_i64(ev);
if (i < 0 || !is_power_of_two(i)) {
error(op.expr, "Expected a power-of-two integer for the asm directive #%.*s, got %lld", LIT(name), cast(long long)i);
continue;
}
}
} else if (name == "skip" ||
name == "nop") {
if (dir->operands.count != 1) {
error(dir->name, "Expected 1 integer for the asm directive #%.*s", LIT(name));
break;
}
array_clear(&operands);
for (Ast *expr : dir->operands) {
Operand operand = {};
check_asm_instruction_operand(asm_ctx, ctx, entity, &operand, expr, /*allow_memory_operands*/true);
array_add(&operands, operand);
}
for (auto const &op : operands) {
if (op.mode != Addressing_Constant) {
error(op.expr, "Expected an integer >0 for the asm directive #%.*s", LIT(name));
continue;
}
ExactValue ev = exact_value_to_integer(op.value);
if (ev.kind != ExactValue_Integer) {
error(op.expr, "Expected an integer >0 for the asm directive #%.*s", LIT(name));
continue;
}
i64 i = exact_value_to_i64(ev);
if (i < 0) {
error(op.expr, "Expected an integer >0 for the asm directive #%.*s, got %lld", LIT(name), cast(long long)i);
continue;
}
}
} else {
error(dir->name, "Unknown asm directive: #%.*s", LIT(name));
}
case_end;
default:
error(instruction_, "Unexpected instruction in asm template");
break;
}
}
if (previous_prefix != 0) {
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 && ed.pin.len != 0)) {
// continue;
// }
// u16 bit = asm_ctx->clobber_bit_for_reg_name(ed.pin);
// if (bit && (asm_acc.defined_regs & bit) == 0 && asm_acc.straight_line) {
// error(ed.entity->token,
// "Output '%.*s' is pinned to %%%.*s but nothing in this template writes it",
// LIT(ed.entity->token.string), LIT(ed.pin));
// }
// }
bool vet_unused = false;
{
AstFile *file = ctx->file;
if (file == nullptr) {
file = entity->file;
}
vet_unused = (ast_file_vet_flags(file) & VetFlag_UnusedVariables) != 0;
}
if (vet_unused) {
for (auto const &entry : ate->label_scope->elements) {
Entity *le = entry.value;
GB_ASSERT(le != nullptr);
if ((le->flags & EntityFlag_Used) == 0) {
error(le->token, "'asm' label '.%.*s' is declared but never reference by any instruction", LIT(le->token.string));
}
}
}
if (vet_unused) {
PtrSet<Entity *> refs = {};
ptr_set_init(&refs);
defer (ptr_set_destroy(&refs));
u16 touched_regs = 0;
for (Ast *instruction_ : at->instructions) {
if (instruction_->kind == Ast_AsmInstruction) {
for (Ast *op : instruction_->AsmInstruction.operands) {
check_asm_collect_refs(asm_ctx, &refs, op, &touched_regs);
}
} else if (instruction_->kind == Ast_AsmDirective) {
for (Ast *op : instruction_->AsmDirective.operands) {
check_asm_collect_refs(asm_ctx, &refs, op, &touched_regs);
}
}
}
for (auto const &ed : ate->decls) {
bool is_scratch = ed.param_group == AsmTemplateEntityDeclParamGroup_Scratch && ed.view_of < 0;
bool is_immediate = ed.kind == AsmTemplateEntityDecl_Immediate;
if ((!is_scratch && !is_immediate) || ed.entity == nullptr) {
continue;
}
// Used if its identifier is referenced OR (for a pinned scratch) its pinned
// register is touched in the body. Immediates are never register-touched, so
// they fall through to the entity check as before.
if (ptr_set_exists(&refs, ed.entity)) {
continue;
}
if (ed.pin.len != 0) {
u16 pin_bit = asm_ctx->clobber_bit_for_reg_name(ed.pin);
if (pin_bit != 0 && (touched_regs & pin_bit) != 0) {
continue;
}
}
error(ed.entity->token, "'asm' %s '%.*s' is declared but never used",
is_immediate ? "immediate parameter" : "scratch parameter",
LIT(ed.entity->token.string));
}
}
GB_ASSERT(entity->kind == Entity_AsmTemplate);
if (results->Tuple.variables.count == 0 && !entity->AsmTemplate.is_volatile &&
!entity->AsmTemplate.clobber_memory &&
entity->AsmTemplate.has_observable_side_effect) {
warning(entity->token,
"This asm template has an observable effect but declares no outputs "
"and does not #volatile in the specification block; it may be optimized away. "
"Please add #volatile if the effect is intended.");
}
if (entity->AsmTemplate.is_align_stack && !asm_acc.saw_call_or_mem) {
warning(entity->token,
"#align_stack is redundant; this template makes no call and touches no memory "
"that would require the stack to be realigned");
}
if (false) {
// TODO(bill): is this even a good idea? The programmer might have just added it for the reason so that he can
// tell if an asm template clobbers something specific or if it is #volatile.
// I'll leave this in an `if (false)` block for the time being just in case it might be useful in the future.
// Redundant #clobber hint
for (Ast *clobber_ : at->clobbers) {
ast_node(clobber, AsmClobber, clobber_);
if (clobber->value == nullptr || clobber->value->kind != Ast_AsmRegister) {
continue;
}
String reg = clobber->value->AsmRegister.name.string;
u16 bit = asm_ctx->clobber_bit_for_reg_name(reg);
if (bit && (asm_acc.implicit_clobbered_regs & bit) != 0) {
warning(clobber->value, "#clobber %%%.*s is redundant; an instruction in this template already clobbers it implicitly", LIT(reg));
}
}
// Redundant #volatile hint
if (entity->AsmTemplate.is_volatile && entity->AsmTemplate.has_observable_side_effect) {
for (Ast *clobber_ : at->clobbers) {
ast_node(clobber, AsmClobber, clobber_);
if (clobber->value == nullptr && clobber->name.string == "volatile") {
warning(clobber->name, "#volatile is redundant; an instruction in this template already has an observable side effect");
break;
}
}
}
}
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 ||
entity->AsmTemplate.has_observable_side_effect;
bool is_pure = asm_acc.can_be_pure && !declared_effects && !type->Proc.diverging;
entity->AsmTemplate.is_pure = is_pure;
if (is_pure_annotated && !is_pure) {
Ast *node = asm_acc.impure_reason_node;
char const *why = asm_acc.impure_reason;
if (why == nullptr) {
if (type->Proc.diverging) {
why = "it is declared diverging (-> !) and computes no outputs";
} else if (entity->AsmTemplate.clobber_memory) {
why = "it declares '#clobber memory'";
} else if (entity->AsmTemplate.is_volatile) {
why = "it is declared '#volatile'";
} else {
why = "it declares an observable effect";
}
}
Token tok = node ? ast_token(node): entity->token;
error(tok, "'asm' template is marked #pure but it is not pure: %s", why);
}
}
}
gb_internal void check_asm_template_from_entity(CheckerContext *c, Entity *e, DeclInfo *d) {
if (build_context.metrics.arch == TargetArch_amd64) {
check_asm_template(&g_asm_amd64, c, e, d);
} else if (build_context.metrics.arch == TargetArch_riscv64) {
check_asm_template(&g_asm_riscv, c, e, d);
} else {
error(e->token, "asm templates are not currently supported for this target");
}
}