Work on trying to get the arm64 asm backend working

This commit is contained in:
gingerBill
2026-08-31 11:02:06 +01:00
parent 3d8437d152
commit af0842343d
3 changed files with 301 additions and 22 deletions

View File

@@ -959,6 +959,32 @@ struct Asm_arm64 {
return false;
}
bool operand_type_is_cond_code(OperandType t) const {
switch (t){
case OP_COND:
case OP_COND_NOT_AL:
return true;
}
return false;
}
// Does this slot's register spell as `vN.<T>` (an arrangement) rather than a
// scalar view (bN/hN/sN/dN/qN), a lane (vN.<T>[i]), or a bare vN? Drives the
// arrangement-suffix emission in the AArch64 backend. Element-indexed and
// scalar-view slots are deliberately excluded: those carry their width via the
// register-name modifier (${N:s|d|q}) or the explicit lane syntax instead.
bool operand_type_wants_arrangement(OperandType t) const {
switch (t) {
case OP_V_8B: case OP_V_16B:
case OP_V_4H: case OP_V_8H:
case OP_V_2S: case OP_V_4S:
case OP_V_1D: case OP_V_2D:
case OP_V_4H_FP16: case OP_V_8H_FP16:
return true;
}
return false;
}
AsmRegClass operand_type_reg_class(OperandType t) const {
// Same mapping as reg_class_from_operand_type.
return reg_class_from_operand_type(t);

View File

@@ -2323,6 +2323,11 @@ gb_internal void check_asm_instruction_operand(AsmCtx *asm_ctx, CheckerContext *
case_end;
case_ast_node(ie, IndexExpr, expr);
if (!allow_memory_operands) {
error(expr, "Invalid use of an index expression in an asm operand");
return;
}
Operand lhs = {};
Operand rhs = {};
check_asm_instruction_operand(asm_ctx, ctx, entity, &lhs, ie->expr, false);

View File

@@ -1,4 +1,4 @@
#define LLVM_ASM_DEBUG_PRINT false
#define LLVM_ASM_DEBUG_PRINT true
struct lbAsmGenerate {
Entity * tmpl_entity;
@@ -8,6 +8,16 @@ struct lbAsmGenerate {
gbString asm_string;
gbString constraints;
// Stable per-template numbering for internal labels (assigned on first sight).
PtrMap<Entity *, i32> label_numbers;
i32 next_label_number;
PtrMap<Entity *, i32> label_def_pos;
AstAsmInstruction * curr_instr;
isize curr_operand_index;
i32 curr_instr_pos;
Array<lbValue> const *curr_args;
enum WriteOperandFlags : u32 {
WriteOperandFlag_PrintPrefixes = 1<<0,
WriteOperandFlag_IsScale = 1<<1,
@@ -34,11 +44,14 @@ struct lbAsmGenerate {
this->asm_string = gb_string_make_reserve(heap_allocator(), 256);
this->constraints = gb_string_make_reserve(heap_allocator(), 64);
map_init(&this->label_numbers);
}
void destroy() {
gb_string_free(this->asm_string);
gb_string_free(this->constraints);
map_destroy(&this->label_numbers);
map_destroy(&this->label_def_pos);
}
void write_cstr(char const *cstr) { asm_string = gb_string_appendc (asm_string, cstr); }
@@ -48,16 +61,31 @@ struct lbAsmGenerate {
void write_u64(u64 val) { asm_string = gb_string_append_fmt (asm_string, "%llu", cast(unsigned long long)val); }
void write_i64(i64 val) { asm_string = gb_string_append_fmt (asm_string, "%lld", cast(long long)val); }
void write_label(AstIdent *label_ident) {
// AArch64's assembler requires conditional-branch targets to be assembler-local.
// Named .L symbols are treated as external within an inline-asm string, so use
// numeric locals: each label entity gets a small integer, a definition prints
// `N:`, and a reference prints `Nf` (forward) or `Nb` (backward). For targets
// that don't need this (amd64), the named form is still fine; this base method
// is overridden per target.
virtual void write_label_def(AstIdent *label_ident) {
String name = label_ident->token.string;
write_cstr(".L_");
write_string(tmpl_entity->token.string);
write_cstr("_");
write_string(name);
// ${:uid} expands to a per-instantiation unique integer, so repeated
// inlining of the same template can't collide on the label symbol.
write_cstr("${:uid}");
}
virtual void write_label_ref(AstIdent *label_ident) {
this->write_label_def(label_ident); // default: same spelling for def and ref
}
void write_label(AstIdent *label_ident) {
this->write_label_ref(label_ident);
}
virtual void prescan_label_positions() {
// do nothing by default
}
AsmTemplateEntityDecl *entity_op(Entity *parameter) {
for (AsmTemplateEntityDecl &op : *ops) {
@@ -109,6 +137,8 @@ struct lbAsmGenerate {
gb_string_clear(this->asm_string);
gb_string_clear(this->constraints);
this->curr_args = &args;
TEMPORARY_ALLOCATOR_GUARD();
auto param_types = array_make<LLVMTypeRef> (temporary_allocator(), 0, ops->count);
@@ -247,10 +277,16 @@ struct lbAsmGenerate {
op_number[i] = next_op++;
}
// AArch64 uses numeric local labels for internal branches; resolve each label's
// definition position up front so references can pick f/b correctly.
this->prescan_label_positions();
// Build the template text
u32 op_flags = this->default_operand_write_flags();
bool reverse = this->reverse_operand_order();
for_array(i, tmpl_node->instructions) {
this->curr_instr_pos = cast(i32)i;
if (i > 0) {
write_cstr("\n");
}
@@ -269,12 +305,13 @@ struct lbAsmGenerate {
if (k > 0) { write_cstr(", "); }
u32 f = op_flags;
if (indirect) f |= WriteOperandFlag_IndirectBranch;
this->curr_instr = instr;
this->curr_operand_index = j;
this->write_operand(op_number, instr->operands[j], f);
}
case_end;
case_ast_node(label, AsmLabelDecl, instr_);
this->write_label(&label->name->Ident);
write_cstr(":");
this->write_label_def(&label->name->Ident);
case_end;
case_ast_node(dir, AsmDirective, instr_);
String name = dir->name.string;
@@ -976,6 +1013,148 @@ struct lbAsmGenerate_riscv64 : lbAsmGenerate {
};
struct lbAsmGenerate_arm64 : lbAsmGenerate {
void prescan_label_positions() override {
map_init(&label_def_pos);
i32 pos = 0;
for (Ast *node : tmpl_node->instructions) {
if (node->kind == Ast_AsmLabelDecl) {
Entity *le = node->AsmLabelDecl.name->Ident.entity;
if (le != nullptr) {
map_set(&label_def_pos, le, pos);
}
}
pos += 1; // count every node so refs can compare positions consistently
}
}
i32 arm64_label_number(AstIdent *label_ident) {
Entity *le = label_ident->entity;
GB_ASSERT(le != nullptr);
if (i32 *n = map_get(&label_numbers, le)) {
return *n;
}
if (next_label_number == 0) {
map_init(&label_numbers);
}
i32 n = ++next_label_number; // 1-based; 0 reserved as "unassigned"
map_set(&label_numbers, le, n);
return n;
}
void write_label_def(AstIdent *label_ident) override {
// Numeric local definition: `N:` — never an f/b suffix.
asm_string = gb_string_append_fmt(asm_string, "%d:", this->arm64_label_number(label_ident));
}
void write_label_ref(AstIdent *label_ident) override {
Entity *le = label_ident->entity;
i32 n = this->arm64_label_number(label_ident);
i32 def_pos = -1;
if (i32 *p = map_get(&label_def_pos, le)) {
def_pos = *p;
}
// Forward if the definition comes at or after the referencing instruction.
// (A self/loop-top reference at the same position is backward once emitted;
// a branch to a label defined later is forward.)
bool forward = def_pos > this->curr_instr_pos;
asm_string = gb_string_append_fmt(asm_string, "%d%c", n, forward ? 'f' : 'b');
}
// ARM64 condition-code encodings -> mnemonic. csel/cset/ccmp/b.<cc> take the
// bare mnemonic, NOT '#<n>'; the frontend resolves the cond to its 0..15 encoding.
static char const *arm64_cond_name(i64 e) {
static char const *n[16] = {
"eq","ne","hs","lo","mi","pl","vs","vc",
"hi","ls","ge","lt","gt","le","al","nv",
};
return (0 <= e && e < 16) ? n[e] : nullptr;
}
// Is user-operand `i` of this instruction the condition-code slot?
bool arm64_is_cond_slot(AstAsmInstruction *instr, isize i) {
if (instr->mnemonic == 0 || instr->valid_form_index < 0) {
return false;
}
auto forms = g_asm_arm64.encoding_forms(instr->mnemonic);
if (instr->valid_form_index >= forms.count) {
return false;
}
auto const &form = forms[instr->valid_form_index];
int slot = this->reverse_operand_order() ? cast(int)i : cast(int)i; // A64: no flip
if (slot < 0 || slot >= cast(int)gb_count_of(form.ops)) {
return false;
}
return g_asm_arm64.operand_type_is_cond_code(form.ops[slot]);
}
// The register-name modifier this *form slot* mandates, independent of the
// operand's Odin type. ldrb/strb want W even for an i64 param; a 128-bit vector
// load wants the Q name; scalar FP wants s/d. Returns 0 when bare $N is correct.
char arm64_slot_reg_modifier(AstAsmInstruction *instr, isize i) {
if (instr->mnemonic == 0 || instr->valid_form_index < 0) {
return 0;
}
auto forms = g_asm_arm64.encoding_forms(instr->mnemonic);
if (instr->valid_form_index >= forms.count) {
return 0;
}
auto const &form = forms[instr->valid_form_index];
if (i < 0 || i >= cast(isize)gb_count_of(form.ops)) {
return 0;
}
auto slot = form.ops[i];
AsmOperandKind k = g_asm_arm64.kind_from_operand_type(slot);
if (k != AsmOperand_Register && k != AsmOperand_Register_Or_Memory) {
return 0;
}
AsmRegClass cls = g_asm_arm64.operand_type_reg_class(slot);
i32 w = g_asm_arm64.operand_type_bit_width(slot);
if (cls == AsmRegClass_Integer) {
return (w == 32) ? 'w' : (w == 64) ? 'x' : 0;
}
if (cls == AsmRegClass_Float || cls == AsmRegClass_Vector) {
switch (w) {
case 8: return 'b';
case 16: return 'h';
case 32: return 's';
case 64: return 'd';
case 128: return 'q';
}
}
return 0;
}
// NEON arrangement suffix (".4s", ".2d", ...) for a #simd operand in a slot that
// wants a vector arrangement rather than a scalar name. Empty when not applicable.
String arm64_arrangement_suffix(AstAsmInstruction *instr, isize i, Type *operand_type) {
if (instr->mnemonic == 0 || instr->valid_form_index < 0) {
return {};
}
auto forms = g_asm_arm64.encoding_forms(instr->mnemonic);
if (instr->valid_form_index >= forms.count) {
return {};
}
auto const &form = forms[instr->valid_form_index];
if (i < 0 || i >= cast(isize)gb_count_of(form.ops)) {
return {};
}
if (!g_asm_arm64.operand_type_wants_arrangement(form.ops[i])) {
return {};
}
Type *bt = base_type(operand_type);
if (bt->kind != Type_SimdVector) {
return {};
}
i64 lanes = bt->SimdVector.count;
i64 esz = type_size_of(base_type(bt->SimdVector.elem));
switch (esz) {
case 1: return (lanes == 8) ? str_lit(".8b") : (lanes == 16) ? str_lit(".16b") : String{};
case 2: return (lanes == 4) ? str_lit(".4h") : (lanes == 8) ? str_lit(".8h") : String{};
case 4: return (lanes == 2) ? str_lit(".2s") : (lanes == 4) ? str_lit(".4s") : String{};
case 8: return (lanes == 1) ? str_lit(".1d") : (lanes == 2) ? str_lit(".2d") : String{};
}
return {};
}
bool reverse_operand_order() override {
return false;
}
@@ -1055,7 +1234,19 @@ struct lbAsmGenerate_arm64 : lbAsmGenerate {
}
void write_operand(Slice<i32> const &op_number, Ast *op, u32 flags) override {
AstAsmInstruction *instr = this->curr_instr;
isize opi = this->curr_operand_index;
if (op->tav.mode == Addressing_Constant) {
// A condition-code slot is a constant in the frontend but must print as a
// mnemonic (gt/lt/...), never '#<n>'.
if (instr != nullptr && this->arm64_is_cond_slot(instr, opi)) {
i64 e = exact_value_to_i64(exact_value_to_integer(op->tav.value));
char const *cc = arm64_cond_name(e);
GB_ASSERT_MSG(cc != nullptr, "asm: bad ARM64 condition encoding %lld", cast(long long)e);
write_cstr(cc);
return;
}
this->write_constant_operand(op, flags);
return;
}
@@ -1097,7 +1288,25 @@ struct lbAsmGenerate_arm64 : lbAsmGenerate {
}
} else {
GB_ASSERT(!negate); // only immediates/displacements negate
asm_string = gb_string_append_fmt(asm_string, "$%d", idx);
// An arrangement operand prints `vN.<T>` — bare $N (which lowers to
// the full vN) plus the ".4s"/".2d"/... suffix. It must NOT also take
// a register-name modifier: ${N:q} + .4s yields the invalid `q1.4s`.
// Only non-arrangement slots take the w/x/q/d/s modifier.
String arr = {};
if (instr != nullptr) {
arr = this->arm64_arrangement_suffix(instr, opi, ed->entity->type);
}
if (arr.len != 0) {
asm_string = gb_string_append_fmt(asm_string, "$%d", idx);
write_string(arr);
} else {
char mod = (instr != nullptr) ? this->arm64_slot_reg_modifier(instr, opi) : 0;
if (mod != 0) {
asm_string = gb_string_append_fmt(asm_string, "${%d:%c}", idx, mod);
} else {
asm_string = gb_string_append_fmt(asm_string, "$%d", idx);
}
}
}
}
case_end;
@@ -1119,19 +1328,41 @@ struct lbAsmGenerate_arm64 : lbAsmGenerate {
Ast *base_op = ie->expr;
Ast *index = ie->index;
// The lane index is an assemble-time constant, validated and folded by the
// checker onto ie->index. If it isn't present here the checker already
// reported the error (non-constant / out-of-range lane), so bail rather than
// assert — lowering must not be the thing that crashes on a rejected program.
if (index->tav.mode != Addressing_Constant) {
return;
// Resolve the lane to a concrete i64. Two spellings reach here:
// v[0] — literal lane, folded onto the index node's tav
// v[idx] — $-immediate parameter; its value is the operand argument,
// looked up via the decl exactly like any other immediate.
i64 lane = -1;
bool have_lane = false;
if (index->kind == Ast_Ident) {
// Immediate-parameter lane: find its decl, confirm it's an immediate, read value.
Entity *ie_ = entity_of_node(index);
if (ie_ != nullptr) {
auto *ed = entity_op(ie_); // the AsmTemplateEntityDecl for this parameter
if (ed != nullptr && ed->kind == AsmTemplateEntityDecl_Immediate) {
GB_ASSERT(ed->param_index >= 0);
lbValue v = (*this->curr_args)[ed->param_index];
GB_ASSERT_MSG(LLVMIsAConstantInt(v.value),
"asm: lane immediate '%.*s' is not a constant",
LIT(ed->entity->token.string));
lane = cast(i64)LLVMConstIntGetSExtValue(v.value);
have_lane = true;
}
}
}
ExactValue lane_ev = exact_value_to_integer(index->tav.value);
if (lane_ev.kind != ExactValue_Integer) {
return;
if (!have_lane) {
// Literal lane: value folded onto the index node by the checker.
GB_ASSERT_MSG(index->tav.mode == Addressing_Constant,
"asm: AArch64 lane index reached lowering unfolded");
ExactValue ev = exact_value_to_integer(index->tav.value);
GB_ASSERT(ev.kind == ExactValue_Integer);
lane = exact_value_to_i64(ev);
have_lane = true;
}
i64 lane = exact_value_to_i64(lane_ev);
GB_ASSERT(lane >= 0); // checker guarantees non-negative
GB_ASSERT(lane >= 0);
switch (base_op->kind) {
case_ast_node(reg, AsmRegister, base_op);
@@ -1147,7 +1378,7 @@ struct lbAsmGenerate_arm64 : lbAsmGenerate {
}
}
GB_ASSERT_MSG(has_arrangement,
"asm: AArch64 lane access on register '%.*s' needs an element qualifier "
"asm: ARM64 lane access on register '%.*s' needs an element qualifier "
"(e.g. '%.*s.d[%lld]')",
LIT(rname), LIT(rname), cast(long long)lane);
this->write_string(rname);
@@ -1164,14 +1395,31 @@ struct lbAsmGenerate_arm64 : lbAsmGenerate {
GB_ASSERT(idx >= 0);
char q = this->arm64_lane_qualifier_for_type(ed->entity->type);
GB_ASSERT_MSG(q != 0, "asm: cannot determine AArch64 lane element size for '%.*s'",
GB_ASSERT_MSG(q != 0, "asm: cannot determine ARM64 lane element size for '%.*s'",
LIT(e->token.string));
asm_string = gb_string_append_fmt(asm_string, "$%d.%c[%lld]", idx, q, cast(long long)lane);
// NOTE(bill): fmov exception: fmov has no `vN.<T>[i]` lane form for S/D elements — lane i
// of an S/D element aliases the scalar view, so emit the scalar name (${N:s} /
// ${N:d}) with no lane suffix. (The `.d[1]` high-half form is the only real
// fmov lane form; if you need it, special-case lane==1 d-element separately.)
bool is_fmov = instr != nullptr && instr->mnemonic == Asm_arm64::M_FMOV;
if (is_fmov && (q == 's' || q == 'd')) {
GB_ASSERT_MSG(lane == 0 || (q == 'd' && lane == 1),
"asm: fmov has no lane form for %c[%lld]; only s[0]/d[0] (scalar) and d[1] exist",
q, cast(long long)lane);
if (q == 'd' && lane == 1) {
// the one genuine fmov lane form: fmov Xd, Vn.d[1]
asm_string = gb_string_append_fmt(asm_string, "$%d.d[1]", idx);
} else {
asm_string = gb_string_append_fmt(asm_string, "${%d:%c}", idx, q);
}
} else {
asm_string = gb_string_append_fmt(asm_string, "$%d.%c[%lld]", idx, q, cast(long long)lane);
}
case_end;
default:
GB_PANIC("asm: AArch64 lane base must be an operand or explicit register, got '%s'",
GB_PANIC("asm: ARM64 lane base must be an operand or explicit register, got '%s'",
expr_to_string(base_op));
break;
}