Commit Graph

28 Commits

Author SHA1 Message Date
Brendan Punsky
bba65df7ee rexcode/arm64: model the NEON register lists
LD1-4/ST1-4 write their registers as a list -- `ld2 {v0.16b, v1.16b},
[x1]` -- and none of that was modelled. LD2/LD3/LD4 named a single
register where the syntax names two, three or four, so every one of
their forms disassembled to something no assembler would take.

How many registers the list holds is fixed by the instruction form, not
chosen by the caller: LD2 always names two. So it rides on the encoding
(VD_LIST1..4, VN_LIST1..4, which pack exactly like VD/VN) rather than on
the operand type. Putting it in the type would have meant a type per
count per arrangement -- 32 of them -- and would have made the matcher
check something the caller cannot vary.

The operand carries the count, and the printer walks the run from the
first register, wrapping at v31. That replaces the V_LIST_16B one-off
added with the TBL/TBX fix, which could only ever express a
single-register list; TBL/TBX now go through the same path.

Operand grows a byte for the count, which comes out of the padding
Instruction already had -- it is still exactly one 64-byte cache line,
still aligned to one.

The arrangement codes were bare numbers repeated across four files, and
the generated builders would have grown more of them, so they are now
named constants (VSHAPE_16B and friends).

Verified against llvm-mc: 50 whole-register list forms byte-exact, and
all 51 lane-indexed forms byte-exact -- `ld2 {v0.b, v1.b}[1], [x1]` and
`ld1 {v0.16b}, [x1]` included. Before this and the lane-index change,
every one of those 101 printed something that would not assemble. The
vector sweep holds at 809 byte-exact with nothing unassemblable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
2026-08-27 23:58:38 -04:00
Brendan Punsky
a99f21922c rexcode/arm64: close the last ten disassembly gaps
The three remaining shapes llvm-mc would not accept back, now that
making every NEON arrangement reachable had exposed them.

PMULL/PMULL2 wrote their destination as .2d where the architecture says
.1q. That arrangement had no operand type because the size marker is
lanes*elem-bytes and 1*16 collides with 16B, so V_1Q takes the next free
multiple of 8 instead. The encodings were already right; only the label
was wrong.

TBL/TBX write their table register as a list, `{v1.16b}`. The braces
belong to the operand rather than the mnemonic, so V_LIST_16B carries
them and the printer stays generic. Only one-register lists are modelled
-- LD1-4/ST1-4 need a count, which is still open.

SM3TT1A/1B/2A/2B were missing their lane index entirely. The mask
already left imm2 free at bits 13:12; the operand simply was not in the
table, so every one of them decoded as index 0 and printed `v2.s` with
no index at all.

Printing that index needed the piece that was never there: a lane index
is its own immediate operand, so it printed as a separate `#2` rather
than glued to the register it indexes. It now carries a marker and the
printer writes `v2.s[3]`. The marker is set from the ENCODING, not the
operand type -- EXT shares .VEC_INDEX for a byte index that really is
written `#3`.

That last part reaches further than these ten: of the 51 lane-indexed
forms, all 51 used to print an index that no assembler would take. 15
are now byte-exact against llvm-mc (SM3TT, DUP, INS) and the other 36
are LD1-4/ST1-4, which additionally need the register-list braces.

The vector sweep is now 809 byte-exact with nothing mismatched and
nothing llvm cannot assemble, from 803/10 before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
2026-08-27 23:44:13 -04:00
Brendan Punsky
1d4887ccb6 rexcode/arm64: vector builders relabelled the caller's register
inst_add(X0, X1, X2) encoded `add v0.16b, v1.16b, v2.16b`. It built the
operand as op_v_16b(u8(reg_hw(dst))), which throws away the register's
class and rebuilds a V register from the bare number -- so an X register
became a V register before the matcher, whose whole job is to reject
that, ever saw it. It encoded, round-tripped, and printed cleanly. Seven
mnemonics with both a scalar and a vector three-register form were
affected: add, and, bic, eor, orn, orr, sub.

The vector constructors now take the register the caller actually has
and put the arrangement in op.size, so the class survives. A wrong class
matches no form and encode reports it; the right class picks the right
form, which is what the matcher was always supposed to do.

The same laundering hid a second bug. Because every arrangement built
the same Odin signature, all of a mnemonic's arrangements collapsed onto
one builder name and only the first survived -- ADD has seven NEON forms
and six were unreachable. 229 mnemonics were in that state. The
arrangement is now part of the builder name (inst_add_v8b_v8b_v8b), so
they are all reachable: 992 builders becomes 1847. The overload group is
unchanged, since it still dedups by Odin signature, so inst_add(V0, V1,
V2) still means .16b as before.

Making them reachable exposed two pre-existing bugs, both fixed here:
CMLE/CMLT/FCMLE/FCMLT compare against zero and the zero is part of the
syntax rather than an encoded operand, so their disassembly was missing
the trailing `#0`/`#0.0` and no assembler would take it; and BFCVTN was
typed .8h at the destination where the architecture says .4h (BFCVTN2 is
the .8h one, and was already right).

Verified against llvm-mc: of the 874 all-register vector builders, 803
are byte-exact and none disagree. The 10 that llvm cannot assemble are
the already-known modelling gaps -- SM3TT lane indices, TBL/TBX register
lists, and PMULL's .1q destination, which the arrangement encoding
cannot represent since 1 lane * 16 bytes collides with 16B. The other 61
are the harness passing V registers where a scalar B/H/S/D/Q view is
required, which is the class check doing its job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UmHLRF11EoWwNWCJ7JGaA
2026-08-27 21:26:11 -04:00
Brendan Punsky
dcaab1aa85 rexcode/arm64: system registers get a type instead of being bare i64
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
2026-08-27 21:13:12 -04:00
Brendan Punsky
47b637e862 rexcode/arm64: fold sysregs.odin into registers.odin
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
2026-08-27 21:13:12 -04:00
gingerBill
b89ae20337 Merge branch 'bill/arm64-clobber' of https://github.com/odin-lang/Odin into bill/arm64-clobber 2026-08-27 15:06:23 +01:00
Brendan Punsky
ba5d1dab00 rexcode/arm64: 89 system registers had the wrong encoding
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
2026-08-27 09:43:17 -04:00
Brendan Punsky
5627cc94d6 rexcode/arm64: add the cset/csetm/cinc/cinv/cneg aliases
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
2026-08-27 08:59:59 -04:00
Brendan Punsky
01bdf7522c rexcode/arm64: MRS/MSR printed the system register as a number
`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
2026-08-27 08:59:47 -04:00
gingerBill
d90c175a63 Merge branch 'bill/arm64-clobber' of https://github.com/odin-lang/Odin into bill/arm64-clobber 2026-08-27 13:39:36 +01:00
Brendan Punsky
a2cd94f406 rexcode/arm64: catch the tooling up to per-condition branch mnemonics
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>
2026-08-27 08:33:23 -04:00
Brendan Punsky
4406ce69db rexcode/arm64: B.cond is sixteen mnemonics, not one with a condition 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>
2026-08-27 08:28:39 -04:00
gingerBill
d8cea6ceb0 asm: semantic checking for v[idx] (arm style) 2026-08-27 13:02:10 +01:00
gingerBill
87b9b39749 Begin integrating arm64 into the Odin compiler 2026-08-27 11:39:23 +01:00
gingerBill
038e8a51dd Format INSTRUCTION_TABLE 2026-08-27 10:41:26 +01:00
Brendan Punsky
5a9a7e9f49 rexcode: pad arm32/arm64 Instruction back to 64 bytes and align it
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>
2026-08-27 01:13:06 -04:00
Brendan Punsky
0d614419e0 rexcode: arm32 88 -> 48 bytes, arm64 64 -> 48
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>
2026-08-27 00:40:20 -04:00
Brendan Punsky
dd925287ad rexcode/arm64: fix the encode, decode and print bugs the mnemonic pass 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>
2026-08-26 18:24:26 -04:00
Brendan Punsky
f4bd6d74f4 rexcode/arm64: mnemonics are assembler mnemonics, not per-encoding names
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>
2026-08-26 17:51:36 -04:00
gingerBill
ab1ef45c3f Remove unneeded comments 2026-08-26 14:18:13 +01:00
gingerBill
82fd8791bf Use INSTRUCTION_TABLE now 2026-08-26 14:14:02 +01:00
gingerBill
02f7bf5634 Merge into INSTRUCTION_TABLE 2026-08-26 14:03:45 +01:00
gingerBill
f354406a79 Add Clobber alias 2026-08-26 12:54:15 +01:00
gingerBill
f2615fd527 Add clobber_table.odin 2026-08-26 11:56:53 +01:00
gingerBill
d909a1293a Add clobber_types.odin 2026-08-26 11:56:14 +01:00
Flāvius
2e8d3b9e10 rexcode/isa: display-side label naming — address order, offset-keyed names
Internal label ids are allocation-order handles: the encoder's creation
order, or the decoder's branch-DISCOVERY order (a loop's latch names the
header before an earlier forward target). Printing labels by raw id leaked
that accident into listings — label numbers appeared out of order down the
page — and the printers' `label_names: ^map[u32]string` keyed the caller's
names by those synthesized ids, which a decode consumer cannot know without
re-deriving them (the practical result: naming "label 0" could caption a
random interior branch target).

Naming is now derived at the presentation seam, shared by every ISA
(`isa.Label_Display` in isa/print.odin):

  - display numbers are assigned in ASCENDING ADDRESS order, so a listing
    reads L0, L1, L2 … top to bottom regardless of id allocation;
  - caller names are keyed by BYTE OFFSET (`isa.Label_Names`, with a
    `distinct` Label_Offset key so an id-keyed map from the old contract
    fails to compile instead of silently mis-naming);
  - a named offset is guaranteed a label row even when no Label_Definition
    points at it — `names[0] = "factorial"` heads a function's listing.

All ten ISA printers (x86, mips, rsp, arm32, arm64, riscv, ppc, ppc_vle,
mos6502, mos65816) drop their per-printer offset_to_label maps and
write_label helpers for the shared display; each arch re-exports
Label_Offset/Label_Names beside Label_Definition. En route this fixes an
arm32/ppc/ppc_vle bug where passing ANY names map suppressed the default
L<n> label rows for unnamed labels. Decode-side id assignment is untouched:
the reloc round-trip contract (encoder ids surviving decode) and the
sparse-id padding it relies on stay exactly as they were.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Riok9vMpkLmo78wsVKJHhz
2026-08-03 20:52:11 -04:00
Brendan Punsky
fae15847a3 rexcode: buffer-sizing helpers across all ISAs + naming-contract doc
Roll the encode/decode buffer-sizing helpers (added for x86 in 49787b7de) out
to every other ISA, and document them in the cross-arch naming contract.

Per arch (arm32, arm64, mips, riscv, ppc, ppc_vle, rsp, mos6502, mos65816):
  - encode_max_code_size / encode_max_relocation_count now key off the
    []Instruction slice (were int counts); bodies unchanged (* MAX_INST_SIZE).
  - encode_reserve(code, relocs, instructions): grows the caller's code []u8 by
    length and reserves relocs by capacity; allocates no new buffers.
  - decode_max_instruction_count / decode_estimate_instruction_count: exact
    ceiling and typical estimate, keyed off the min/avg instruction size per
    arch (fixed-4: arm64/mips/ppc/rsp; min-2: arm32/riscv/ppc_vle; min-1: mos).
  - decode_reserve(instructions, inst_info, label_defs, data, exact=false).

docs/cross_arch_design.md: helpers added to the naming contract.

No behavior change to the existing size helpers (signature only). All 10 ISAs
check + test green (x86 2282, arm32 600, arm64 461, mips 281, riscv 154, ppc 31,
ppc_vle 281, rsp 70, mos6502 148, mos65816 53).
2026-06-19 04:11:30 -04:00
Brendan Punsky
95df04fbe1 rexcode: re-house ISA packages under core:rexcode/isa/<arch>
Move all ten ISA packages (x86, arm32, arm64, mips, riscv, ppc, ppc_vle,
rsp, mos6502, mos65816) from core/rexcode/<arch> to core/rexcode/isa/<arch>,
so the import pattern is now `import "core:rexcode/isa/x86"`. The shared
core stays at core:rexcode/isa.

Mechanical: relative `import "../isa"` / "../../isa" -> absolute
"core:rexcode/isa" (the only path that survives the move; the "../" and
"../.." self/generated imports move with their packages). build.lua now
builds paths as <root>/isa/<name>; stale `cd <arch>` hints in the verify
tools and the doc.odin paths updated.

WASM stays at core/rexcode/wasm for now -- it is an IR, not an ISA, and
will move under the forthcoming core:rexcode/ir once that layer lands.

All 10 arches gen/builders/check/test green; import core:rexcode/isa/x86
verified working; wasm still compiles.
2026-06-18 19:03:27 -04:00