asm: arm64 support - #7465
Draft
gingerBill wants to merge 35 commits into
Draft
Conversation
The Mnemonic enum had one member per encoding form -- ADD_IMM, ADD_SR, ADD_ER, ADD_V for what an assembler just calls ADD; LDR, LDR_LIT, LDR_PRE, LDR_POST, LDR_REG, LDR_V for LDR; SVE_ADD_Z / SVE_ADD_PRED / SVE_AND_P for names SVE spells ADD and AND. The encoder never needed that: like x86, it already resolves a mnemonic by scanning its run of forms and matching operand types, so the split bought nothing and cost a printer that had to strip suffixes back off at runtime -- incompletely, so ADD_V printed "add.v", LDR_PRE "ldr.pre" and FCVT_H_S "fcvt.h.s". Collapse the enum to the names assemblers accept: 1104 -> 785 mnemonics, with the variants becoming forms under one name (ADD now has 13, LDR 15). LSLV/LSRV/ASRV/RORV fold into LSL/LSR/ASR/ROR. Form order within a run is precedence, and the original declaration order is already the order an assembler resolves: "add w0,w1,w2" takes the shifted-register form, and only the extended form can encode SP. This needed one structural change. The matcher was blind to addressing mode -- `case .MEM: return op.kind == .MEMORY` -- which is precisely why LDR/LDR_PRE/LDR_POST/LDR_REG had to be separate mnemonics; all 20 merge collisions were this and nothing else. Split Operand_Type.MEM into mode-specific types (MEM_OFFSET/PRE/POST/REG/EXT plus four SVE), matching how W_REG/W_SHIFTED/W_EXTENDED are already distinct types over one register class. The decoder derives Address_Mode from `enc`, so it is unaffected. Encodings are unchanged: the multiset of (ops, enc, bits, mask, feature, flags) over all forms is identical before and after except for two entries deliberately dropped. NOT_V_ALIAS duplicated NOT_V byte for byte, and MOV_V_ALIAS was wrong -- it encoded VN where the ORR-based MOV alias needs VN_VM_DUP, so "mov v1.8b, v2.8b" would have emitted "orr v1.8b, v2.8b, v0.8b". AMX_* keeps its prefix: Apple's coprocessor is undocumented with no assembler spelling, so there is no canonical name to collapse to and bare "set"/"clr"/"ldx" would mislead. The two-token system instructions keep theirs too and print with a space (dc zva, tlbi vae1, bti j). Verified: all 11 rexcode suites match HEAD exactly (arm64 461/461); the three generator stages round-trip idempotently; 754 of 785 mnemonics are accepted by llvm-mc, the rest being AMX (24), TME (4, no +tme in this LLVM build), B_COND/BC_COND and TBL2; and a 39-case encode/print differential against llvm-mc matches 34, with the 5 others confirmed byte-identical at HEAD (pre-existing LSR/ASR immediate and LDP pre-index packing bugs, and printer gaps for vector arrangements and MOVZ/MOVK shifts). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s exposed
Encoder
* LSR/ASR by immediate used the generic IMM12 encoding, which writes bits
10-21 -- straight into the imms field the UBFM/SBFM base pattern already
fills, so immr stayed 0 and every shift encoded as #0 (`asr x0,x1,#7`
gave 9340fc20, not 9347fc20). They need immr alone, since imms is the
constant 31/63 fixed in the form: new ENC_SHIFT_IMMR.
* LDP/STP and friends borrowed the single-register addressing encodings,
which put an UNSCALED 9-bit displacement at bits 20:12 and OR a pre/post
marker into bits 11:10. The pair forms want a SCALED 7-bit value at
21:15, and bits 11:10 are part of Rt2 -- so `ldp x0,x1,[x2,#16]!` came
back with Rt2=3. New OFFSET_PAIR_4/8/16 (the scale does not follow from
the register type: LDPSW pairs X registers but loads words, STGP scales
by 16), with the addressing mode read from bits[24:23] where the
architecture keeps it. 26 forms retargeted.
Decoder
* Vector operands came back with size=4 always, so a decoded V register
lost its arrangement and disassembly printed a bare `v0` that no
assembler would take. Reconstruct it from the form's operand type.
* Vd/Vn/Vm/Va hardcoded REG_V, but SVE forms use those same slots with
Z_REG_* operands -- `add z0.d, z0.d, z0.d` decoded as a V register.
Take the class from the operand type, as every other slot already does.
Printer
* V/Z registers now print their arrangement (`add v0.4s, v1.4s, v2.4s`,
`add z0.d, ...`). Element views (op_v_elem_*) moved from 1/2/4/8 to odd
codes 1/3/5/7, because an element-D view and an 8B arrangement were both
size 8 and could not be told apart.
* MOVZ/MOVN/MOVK print the hw index as `lsl #16`, omitted when zero.
* BC_COND folds its condition into the mnemonic like B_COND already did,
instead of printing it twice.
Table (each bit pattern re-derived from llvm-mc)
* BTI_J and BTI_C had each other's encodings.
* FCMLA's mask left size bit 22 free, so .4s and .2d were indistinguishable
and .2d decoded as .4s.
* BFDOT carried the Q=0 pattern for its .4s/.8h form; PMULLB/PMULLT were
missing the size field; TLBI PAALL/PAALLOS had the wrong CRm/op2.
* RDSVL's imm6 sits at bits 10:5, not where IMM6 puts it: ENC_IMM6_LO.
Nine test expectations that asserted the wrong values were corrected.
specgen.lua
Was already dead before the mnemonic work -- it wrote to encoding_table.odin
and spliced a SPECGEN region, neither of which survived the merge into
instruction_table.odin. Retargeted, taught the canonical names, and made it
emit Form literals (Encoding + Clobber). It can no longer own whole
`.MNEM = { ... }` blocks either, since ADD now holds integer, NEON and SVE
forms together, so it MERGES: a form is added only when no (bits, mask)
match exists, and existing rows are never rewritten -- their hand-maintained
Clobber data has to survive a regeneration.
Verified: all 11 rexcode suites match HEAD exactly (arm64 461/461); the three
generator stages stay idempotent; a 73-case differential against llvm-mc is
byte-exact for both encode and decode round-trip. Over the whole decode table,
canonical-form disassembly re-assembled by llvm-mc goes from 594 byte-exact /
1818 unassemblable to 1737 / 678. Re-running specgen re-derives 1130 forms
from llvm-mc and finds every one already present, which independently confirms
those bit patterns.
Still open: multi-vector register lists ({z0.b, z1.b}) and lane indices
(v0.s[2]) are not modelled, so those forms print without them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The arm64 pass turned up the same class of bug elsewhere: mnemonics named
after an encoding rather than after what an assembler accepts, and forms
that no caller can reach because the thing that tells them apart is not
checked.
mips
* The printer mapped every `_` to `.`, but MSA spells the sign qualifier
with an underscore and only the element size with a dot: `adds_s.b`,
`max_s.h`, `copy_u.w`. `adds.s.b` is rejected by an assembler. 91
mnemonics were printing text that would not reassemble. The name alone
cannot decide it -- MSA's ADDS_S_D and the FP convert CVT_S_D have the
same shape and want opposite treatment -- so the family is read off the
form's feature.
* `encode` now takes `features: Feature_Set = FEATURES_ALL` and skips
forms outside it, mirroring `decode`, which has had that parameter all
along. That asymmetry was the reason 12 mnemonics carried an ISA-variant
suffix: with no way to say which MIPS you were targeting, the pre-R6 and
R6 encodings of `mul` had to be two enum members. They are now one
mnemonic with two forms. Eight of the twelve did not even need the
feature filter -- pre-R6 MADD takes rs,rt while the PS2 MMI MADD takes
rd,rs,rt, so operand matching alone separates them. Verified against
llvm-mc: pre-R6 `mul` 712a4002, R6 `mul` 012a4098, `madd $t1,$t2`
712a0000. The printer's hand-written override table is gone.
arm32
* 20 `*_LANE` mnemonics folded into their base. The lane form differs from
the base in an operand TYPE already (DPR_ELEM vs DPR), so the matcher
could always tell them apart; the split only cost us the printed name,
which was the enum name verbatim -- `vqdmulh_lane`, which no assembler
takes. VMOV/VLD1-4/VST1-4 are left alone: their lane forms collide with
the base because register lists and lane indices are not modelled.
riscv
* ZEXT_H and REV8 each carry an RV32 and an RV64 encoding with identical
operands, and the forms were already tagged rv32_only / rv64_only -- the
encoder just never looked. `encode` now takes `xlen: XLEN = .RV64` and
filters, so the RV64 encodings are reachable at all: zext.h 0805c53b and
rev8 6b85d513, both confirmed against llvm-mc.
mos6502
* SAX_NMOS folded into SAX. The undocumented NMOS store-A&X and the
HuC6280 register swap share the mnemonic `sax`; one takes a memory
operand and the other takes none, so they are just two form sets.
Verified: every rexcode suite matches HEAD exactly, all 13 packages build,
and MIPS mnemonics llvm-mc does not recognise drop from 448 to 354.
Still open: arm32 has 201 form signatures no caller can select, because the
NEON data type (.i8/.i16/.f32) is not an operand -- `inst_vadd(d0,d1,d2)`
always yields the first form, and only a decoder-supplied form_id hint can
pick another. 38 arm32 mnemonics still carry encoding-shaped names
(VPADD_F, VCEQ_Z, VLDRB_GATHER, VMOV_Q_R, ...).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… spell
Of the 38 mnemonics still carrying an encoding-shaped name, 24 were simply
names no assembler accepts, and arm32's printer emits the enum name verbatim
-- so `vldrb_gather`, `vceq_z`, `vmov_q_r` and friends were the printed
output. Judged against llvm-mc in every case:
renamed (base name was free)
BFI_BR -> BFX the V8.1M Branch Future indeXed, not a bitfield
insert; llvm assembles `bfx .L, r0` to F060E001,
which is exactly the bit pattern this entry held.
VDOT_BF16 -> VDOT `vdot.bf16 d0, d1, d2`
VMMLA_BF16 -> VMMLA `vmmla.bf16 q0, q1, q2`
merged into the base mnemonic (21)
VCEQ_Z/VCGE_Z/VCGT_Z/VCLE_Z/VCLT_Z -> the compare-against-zero forms
are the same mnemonic with a literal `#0`: `vceq.i8 d0, d1, #0`.
VCVT_FIXED, VCVT_BF16 -> VCVT `vcvt.s16.f32 s0, s0, #4`
VFMA_BF16 -> VFMA
VLDR{B,H,W,D}_GATHER, VSTR{B,H,W,D}_SCATTER -> VLDR*/VSTR*: an MVE
gather is spelled `vldrb.u8 q0, [r0, q1]`; the vector offset is an
operand, not part of the mnemonic.
VMOV_Q_R, VMOV_R_Q, VMOV_2GPR_Q -> VMOV
VHCADD_SAT -> VHCADD, VCMLA_MVE -> VCMLA
kept, but printed properly (2)
PSB_CSYNC / TSB_CSYNC are written as two tokens, `psb csync`, the same
shape as arm64's DC/AT/TLBI. The underscore now prints as a space; no
other arm32 mnemonic has one.
Every merged form had an operand signature the matcher could already tell
apart from the base's, so nothing became unreachable. 631 -> 590 mnemonics,
underscore-bearing names 58 -> 14.
Test indices were re-derived by matching (bits, mask) against the rebuilt
table rather than by computing offsets -- every index the tests reference was
found, which is a check that the merge dropped no form.
Still blocked, and for the two reasons already known:
VPADD_F, VRECPE_F, VRSQRTE_F -- collide with their base because the NEON
data type (.f32 vs .i8/.u32) is not an operand.
VMOV_LANE, VLD1-4_LANE, VST1-4_LANE -- register lists and lane indices are
not modelled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NEON reuses one operand shape across every element width, so `vadd.i8` and `vadd.f32` are both DPR,DPR,DPR and only the type separates their encodings. The type existed nowhere in the data: the encoder could reach the first form of a shape and no other, and the printer reconstructed a suffix from the bit pattern at print time. 489 of 1680 forms -- 29% of the table -- were unreachable, and `inst_vadd(d0,d1,d2)` could only ever produce VFP vadd.f64. Add `Data_Type` and carry `dt: [2]Data_Type` on Instruction, Encoding and Decode_Entry. Two slots because the convert family names both ends (`vcvt.s32.f32`); everything else leaves the second .NONE. In A64 the arrangement belongs to each operand (`add v0.4s, v1.4s, v2.4s`); in A32 it belongs to the instruction, which is why it goes here and not on Operand. Instruction does not grow: it lands in bytes that were already padding, so 88 stays 88. Encoding and Decode_Entry go 21 -> 23, which is +3,360 B per table, +6.7 KB in all. The per-form type is derived from llvm-mc rather than hand-written: assemble each form's canonical word, disassemble it, take the suffix. 942 forms carry one, 38 carry two. (`.w` is the Thumb wide qualifier, not a type, and is excluded.) Effect: of 202 shape groups holding more than one form, 168 are now separated by the type -- 429 of the 489 unreachable forms become selectable. `dt` left at .NONE means "unspecified" and still takes the first matching form, so every existing caller behaves exactly as before. It also fixes printing. The old inference could only ever produce one type, so the whole convert family printed `vcvt.f32` -- 13 forms sharing one string that no assembler accepts. They now print `vcvt.f32.s32`, `vcvt.f64.f32`, `vcvta.u32.f64`, and so on. Verified: vadd.i8/i16/i32/i64/f32 encode to f2010802 / f2110802 / f2210802 / f2310802 / f2010d02, matching llvm-mc exactly; all 11 rexcode suites are identical to baseline. Still unreachable, 60 forms in 34 groups: register lists (VLD2-4/VST2-4), LDM/STM addressing modes, and a few lane-indexed and fixed-point convert forms whose element size is not captured by the type alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
x86 keeps Instruction at 64 bytes -- one cache line -- by packing its memory
operand into a bit_field u64 rather than a struct. arm32 and arm64 both used
a 12-byte Memory struct, and since Memory sits in every Operand that width is
multiplied by four in every Instruction. Adopting x86's trick, plus two
smaller things, takes both ARM ISAs under the cache line.
Instruction ops[4] Operand Memory
x86 64 48 12 8
arm32 88 -> 48 72->40 18->10 12->8
arm64 64 -> 48 56->40 14->10 12->8
Memory -> bit_field u64, both ISAs. Field syntax and composite literals are
unchanged, so callers see nothing. Registers keep their type: an arm64
Register never exceeds 0x0C1F and an arm32 one never exceeds 0x401F, but the
arm64 NONE sentinel is 0xFFFF, so arm64 gives them the full 16 bits and arm32
15. What is left goes to `disp`: 23 bits on arm64 (worst case 65,520, from
LDR Q, [Xn, #imm12*16]) and 19 on arm32 (worst case 4,095, an A32 imm12) --
64x and 32x headroom respectively.
arm32 Operand also carried four tail bytes arm64 does not. `cond` was dead:
nine builders wrote it and nothing in the package ever read it, and
Instruction.cond already exists. shift_type/shift_amt/lane now ride inside
the union alongside the register they describe -- they only ever apply to a
register operand -- via a `using` bit_field, so op.reg, op.shift_type,
op.shift_amt and op.lane still read and write exactly as before.
arm32 Instruction packs cond, operand_count, mode, length and the two flag
bits into one 16-bit word; they need 13 bits between them and were spending
six bytes. `using` again keeps the field names, with the one exception that
inst.flags.sets_flags is now inst.sets_flags (five call sites).
Verified: every rexcode suite matches baseline; both generators stay
idempotent; arm64 is 73/73 byte-exact against llvm-mc on both encode and
decode round-trip; arm32's 1680/1680 sweep still passes and a memory/shift
encode spot-check is byte-identical to what the same code produced before
this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shrinking Instruction to 48 was the wrong call, and measuring it said so.
The premise was that a sub-cache-line struct touches fewer lines. It does --
but `#packed` aligns the struct to 1, so a 48-byte stride straddles a line
boundary 75% of the time, and the heap base is not line-aligned either. The
old 64-byte packed layout was worse still: 100% straddling, getting none of
the benefit its size implied.
Measured on an i7-9750H (L1d 32K/core, L2 256K, L3 12M), best-of-5, median of
3 interleaved rounds, against the 64-byte packed layout this branch started
from:
scan encode decode
64 packed (was) 1.000x 1.000x 1.000x
48 packed 0.909x 1.040x 1.022x
64 align(64) 1.218x 1.034x 0.806x
Decode is ~19% faster aligned, and that holds at every working set including
ones that fit entirely in L1 -- so it is split-store cost at the store ports,
not cache-line fetches. Decode writes whole Instructions, and the aligned
stores are worth more than the 33% extra bytes they move. A fourth variant --
48 bytes with `#packed` removed -- was measured to rule out the obvious
confound, and tracked 48-packed within 0.5% everywhere, so the win is
alignment and not the loss of packing.
Encode is within a few percent throughout (it is compute-bound; the form scan
dominates), and the pure read traversal is slower, but that is a synthetic
loop and its regression is codegen, not cache -- it is present even at
L1-resident sizes where a standalone struct shows no such penalty.
The Operand and Memory work from the previous commit is what makes this
possible: a 45/48-byte payload now sits inside one line with room to spare,
where the original spent all 64 bytes. The 16-19 spare bytes cost nothing over
a straddling 48-byte struct and give new fields somewhere to land.
All 11 rexcode suites match baseline; arm64 is 73/73 byte-exact against
llvm-mc; arm32's 1680/1680 sweep passes and its encode spot-checks are
unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SHIFT_NAMES holds five entries, LSL..RRX, but Shift_Type has ten: the four register-shifted-register markers (LSL_REG = 6 .. ROR_REG = 9) say the shift count comes from an Rs register rather than an immediate. Both places that indexed the table used the raw enum value, and the guard in front of them only excluded NONE and RRX -- so any operand carrying a register shift indexed a 5-entry array with 6..9 and killed the printer: printer.odin(473:45) Index 6 is out of range 0..<5 Fold the register-shifted variants back onto the table and give each spelling its own case: `, lsl #3` for an immediate amount, `, lsl r3` when the count is in a register (Rs index rides in shift_amt), and a bare `, rrx`, which takes no amount. All nine now print what an assembler accepts -- verified against llvm-mc -- where three of them previously crashed and RRX printed nothing. The memory-operand site indexed the same table the same way and is routed through the same helper. Found by printing every entry in the decode table; that sweep now completes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pattern
The encoder builds a word by ORing packed operand fields onto the form's
`bits`. It can only ever set a bit that way, never clear one -- so any bit
`bits` presets that an operand is supposed to drive is stuck at 1 forever.
72 forms did that, and two families show what it cost:
* The U bit (23) on the whole A32 load/store family. U selects add vs
subtract for the displacement, and the encoder derives it from the sign of
mem.disp -- but every form had it preset, so `ldr r0, [r1, #-4]` silently
encoded as `[r1, #4]`. Every negative displacement in the family was wrong.
* The Vn high bit (7) on the NEON lane-indexed forms. That bit is the top of
the register number, so presetting it meant Vn could only ever name
d16..d31; d0..d15 were unreachable.
Which bits are operand-driven was decided by llvm-mc rather than by reading
the manual: for each of the 230 bits a form preset outside its mask, take the
form's canonical word with the bit set and cleared and disassemble both. Same
mnemonic, different operands means the bit belongs to an operand (clear it);
a different mnemonic, or an undecodable word, means the bit is genuinely fixed
for that form. The split was not per-bit -- bit 7 is a register bit for
VMUL/VMLA/VFMA but distinguishes VNEG from VABS and VCMPE from VCMP, and bit
23 is the U bit for LDR but the load/store select for VCX3 -- so every form
was classified individually.
Ten test expectations asserted the old values and were corrected; each had the
bug baked in. Verified byte-exact against llvm-mc across the load/store family
including every negative-displacement form, and the 1680/1680 decode sweep and
all other suites are unchanged.
The other half of `bits & ~mask != 0` -- 130 forms where the bit really is
fixed and the MASK is merely too loose -- is deliberately not in this commit.
Widening those masks alone breaks decode: a bit that distinguishes two
mnemonics has to be added to BOTH forms' masks in the same pass, and doing
only the ones that set it made LSL swallow MOVS, CX3 swallow VADDLVA and VABAV
swallow VRMLSLDAVH. That needs each form's true mask derived empirically
(vary the operands, see which bits move) the way specgen does it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mnemonics
LDM and STM each held five A32 forms with identical operand shapes -- the four
increment/decrement orders plus a writeback variant -- distinguished only by
their fixed bits. Nothing could tell them apart, so the encoder always took the
first and six of the eight A32 encodings were unreachable: `ldmib`, `ldmda`,
`ldmdb`, `stmib`, `stmda`, `stmdb` could not be produced at all.
They are not variants of one mnemonic in the first place. An assembler spells
them `ldmib` / `ldmda` / `ldmdb`, with plain `ldm` meaning IA, so this follows
the same rule as the rest of the enum: one member per name an assembler
accepts. LDM/STM keep the IA order and the Thumb encodings; the other three
orders become their own mnemonics, and the T32 DB encodings join them.
All eight now encode, byte-exact against llvm-mc:
ldm e8900006 stm e8800006
ldmib e9900006 stmib e9800006
ldmda e8100006 stmda e8000006
ldmdb e9100006 stmdb e9000006
Six test checks referenced these forms by index; they were re-derived by
matching (bits, mask) against the rebuilt table rather than by adjusting
offsets, and every one was found -- so no form was lost in the move.
Writeback (`ldm r0!, {...}`) is still unreachable: it is a property of the base
operand, not a separate mnemonic, and there is nowhere to put it yet. That is
one form per family rather than four.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collapsing the encoding-shaped mnemonics into their assembler names merged several form lists that already overlapped, leaving byte-identical Encoding rows inside one mnemonic. The matcher takes the first form that matches, so every duplicate was dead weight -- unreachable, and inflating both tables. VMUL, VMLA and VMLS each gained two from the *_LANE merge; VSHR, VSRA and VRSHR two each, VQSHRN, VQRSHRN and VSHLL one each. The branch base had none, so these are mine: the collision check I ran before each merge compared operand signatures, which is the right test for "can the matcher tell these apart", but says nothing about two forms being wholly identical. Comparing the full Encoding would have caught them. 1680 -> 1665 forms. Removing an unreachable exact duplicate cannot change behaviour, and does not: the load/store, LDM/STM and NEON data-type encode checks are byte-for-byte what they were, and the decode sweep still round-trips every form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…operand
B_COND was a straight contradiction of the rule the rest of the enum follows.
No assembler has a mnemonic called `b.cond`; it has `b.eq`, `b.le`, `b.ne` and
thirteen more, and x86 in this same library already models exactly this shape
the right way -- JE, JNE, JG, JLE are sixteen separate mnemonics with the
condition in the opcode.
Two things were wrong with folding it into an operand.
The condition is not an operand. It is four bits of the opcode, no different
from x86 putting Jcc's condition in the low nibble of 0x7_. Modelling it as
one has to be papered over everywhere: the printer special-cased B_COND to
rebuild `b.eq` out of operand 0, sbprint had to skip that operand so it did
not print twice, and the public mnemonic_to_string -- which has no instruction
to read the operand from -- returned "b_cond", a string no assembler takes.
All three of those are now gone; the name prints itself.
More importantly it destroyed the flag data. x86 records per condition which
status bits it consults: JE reads {ZF}, JLE reads {ZF, SF, OF}. One B_COND
entry could not say that, so it claimed nzcv_rd = {N, Z, C, V} -- all four
flags, for every condition. Every entry was wrong. Split apart they carry what
they actually read:
b.eq/b.ne Z b.hi/b.ls Z, C
b.cs/b.cc C b.ge/b.lt N, V
b.mi/b.pl N b.gt/b.le N, Z, V
b.vs/b.vc V b.al/b.nv none
which is the data the compiler's asm checker reads for flag liveness, now that
arm64 feeds it.
The mask also covers the condition field for the first time (0xFF000010 ->
0xFF00001F): with the condition in an operand, four opcode bits sat outside
the mask.
BC.cond gets the same treatment. Builders come out per condition, so `b.le` is
`inst_b_le(label)` rather than `inst_b_cond(.LE, label)`. Cond stays exactly as
it is -- CSEL, CSINC, CSINV, CSNEG, CCMP, CCMN and FCSEL take a real condition
operand, and the compiler's OP_COND handling is untouched.
All sixteen verified by disassembling our own output with llvm-mc: b.eq, b.ne,
b.hs, b.lo, b.mi, b.pl, b.vs, b.vc, b.hi, b.ls, b.ge, b.lt, b.gt, b.le, b.al,
b.nv. Rows follow the table's current column formatting. All rexcode suites
pass and the generators stay idempotent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Splitting B_COND left three places still describing the old model. The generated builders were already right -- inst_b_le(label), inst_bc_ne(label), one per condition, all 32 verified to encode and round-trip -- but the hand-written scaffolding around them was not. verify_against_llvm normalised our mnemonic by truncating at the first underscore, which turned B_COND into "b". LLVM prints b.eq/b.ne/..., so the tool carried 32 alias rows pairing "b" with each of them to stop the mismatch being reported. That truncation now collapses all sixteen B_* onto "b" and makes every condition compare equal to every other -- the check would pass whatever the table said. Keep the condition instead (B_LE -> "b.le") and the 32 alias rows are unnecessary; they are gone. specgen's canonicalizer kept B_COND and BC_COND off its rename path by name. Those names no longer exist, so replace the entry with a rule that matches the shape (BC?_%u%u), which is what the intent was. And the note in instructions.odin still pointed at inst_b_cond. It now says what is actually true: a conditional branch is one builder per condition because the condition is part of the mnemonic, while the select/compare family -- CSEL, CSINC, CSINV, CSNEG, CCMP, CCMN, FCSEL -- really does take a condition operand and keeps one. specgen still re-derives its 1130 forms from llvm-mc and finds every one already present, all suites pass, and all 13 packages build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…into bill/arm64-clobber
…icate mnemonics
Asking for a VFMA with a lane operand emitted a VMLA. NEON's fused multiply-add
has no by-element form at all -- llvm-mc rejects `vfma.f32 d0, d1, d2[0]` -- and
the four rows sitting under VFMA/VFMS held VMLA/VMLS's lane encodings, which
VMLA and VMLS already own. Their data type gave it away too: `.I32` on a
fused multiply-add, which is float-only. Deleted.
They were also the reason `bits & ~mask` looked wrong on those rows: as
authored they were F2A000C0, and clearing the Vn high bit -- correct, since
bit 7 is the top of the register number -- landed them exactly on VMLA's
F2A00040.
Found by asking which (bits, mask, mode) triples more than one mnemonic
claims. That check found 18; this commit takes it to 9.
Three of the eighteen were whole mnemonics duplicating a base:
VRECPE_F, VRSQRTE_F every form already present under VRECPE / VRSQRTE,
which carry both the U32 and F32 variants.
VPADD_F its F32 form duplicated VPADD's; its F16 form was the
only thing it owned, so that moves to VPADD, where the
data type now selects it.
All three were on the list of names no assembler spells, so that count goes
from 14 to 11 -- and nine of the remaining eleven are the *_LANE group, still
waiting on register-list and lane-index modelling. The other two are
`psb csync` and `tsb csync`, which are correct as they are.
Two smoke checks asserted the VFMA/VFMS by-element forms and are gone with
them; a third moved index.
Verified against llvm-mc: vpadd.i8/.f32/.f16, vrecpe.u32/.f32 and vrsqrte.f32
all byte-exact, 1656/1656 decode sweep, every suite at baseline.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`mrs x0, cntvct_el0` came back out of the disassembler as `mrs x0, #24322`. The 230 sysreg constants were already there and the encoding was byte-exact against llvm-mc -- the printer simply had no SYS_REG handling, so the packed 15-bit field printed as an immediate. Nothing an assembler would take back. Adds a value -> name table (sorted, binary searched) and prints that operand by name for MRS, and for the MSR form that takes one. MSR's other form holds a PSTATE field selector in the same slot, which is a different namespace; the two are told apart by whether the second operand is a register or an immediate. Six encodings carry two names, so a round-trip can come back spelled as the sibling; the first by source order wins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
These are what an assembler writes -- and what one prints back -- for
CSINC/CSINV/CSNEG with the condition inverted, and none of the five
existed. A disassembly of `cset w0, eq` came out as
`csinc w0, wzr, wzr, ne`.
Two constraints the table could not state before:
- The condition is stored inverted, so COND_HI_INV packs `cond ~ 1`
and reads it back the same way. The printer needs nothing; the
decoder hands it a plain condition operand.
- cinc/cinv/cneg are only the alias when Rn == Rm, which is a
cross-field equality no mask expresses. One operand fills both
slots on the way in (RN_RM), and decode checks the two fields agree
before accepting the entry -- reached only on a mask match, so it
costs nothing in the scan.
The aliases also require cond != 111x. That one *is* expressible: the
14 legal values are covered exactly by three masked patterns (0xxx,
10xx, 110x), so AL and NV fall through to the underlying instruction
the way llvm-mc does. COND_NOT_AL rejects them on the encode side.
Verified against llvm-mc across all five mnemonics, both widths and all
14 conditions: 140/140 of our printed strings assemble to exactly our
bytes. Disassembly agrees except for cs/hs and cc/lo, which is the
package's existing spelling of those two conditions and shows up on
CSEL and B.cond alike. AL/NV and Rn != Rm both fall through correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
Printing MRS/MSR operands by name made these visible: sweeping all 230 constants through encode -> decode -> print -> llvm-mc, 89 of them assembled to bytes we did not produce. TCR_EL1 encoded as 0x4282 where the architecture says 0x4102, FAR_EL1 as 0x5300 for 0x4300, the whole pointer-auth key block was off by a CRn, and so on. Anything reading one of them got a different register than it asked for. The field comments were right and only the packed values were wrong, so most of the file could be rebuilt from its own comments and checked against llvm-mc. Thirteen needed more: nine had bad comments too (RGSR/GCR/TFSR/TFSRE0, ID_MMFR4/5, and the three ICC SGI registers -- ICC_SGI*_EL1 are op1=0, not 3), and four carried a "historic collision" note instead of fields. Those were derived from the ARM ARM and agree with llvm-mc. Five of the six encodings that looked like duplicates were simply wrong values landing on each other; one real pair is left, DBGDTRRX_EL0 and DBGDTRTX_EL0, which genuinely share an encoding as the read and write views of one register. The name table prefers the read name, since MRS is the direction that has to print. All 230 now verified byte-exact against llvm-mc: 222 through MRS, and the 8 write-only ones through MSR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
…into bill/arm64-clobber
arm64 was the only arch carrying a second register file; registers belong in one place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
They were plain i64 constants handed to op_imm, so any integer typed as one and `inst_mrs(X0, 999999)` compiled fine. Worse, the printer could not tell a system register from an immediate and had to recover the distinction by mnemonic and slot -- MSR's other form holds a PSTATE field selector in the same position, so it keyed off whether operand 1 was a register. System_Register is now its own type with its own Operand_Kind, union member and op_sysreg constructor, exactly as Cond is. The printer's slot logic is gone: the operand knows what it is, so naming it is a case in the same switch that prints every other operand kind. MSR's PSTATE selector is typed PSTATE_FIELD, which is what it always was. It cannot join `Register` itself: that is a u16 with the class in its high byte, leaving 8 bits for the number, and a system register needs 15. Widening it would break `Memory`, which packs two registers plus a displacement and a mode into exactly 64 bits. The constants are also reorganised. They had accreted into overlapping sections -- two "ID registers" groups, three cache groups, a "Batch 5: comprehensive sysreg sweep" banner, and a "hmm let me recompute" note left in a comment. All 231 are now grouped by architectural function (18 groups, alphabetical within each) with their five fields aligned. Verified unchanged against llvm-mc: 222 registers byte-exact through MRS, 8 write-only through MSR, and the PSTATE form still decodes as an immediate rather than a register. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.