2026-10-06 19:52:20 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// The assembler's side of the extended-instruction layer: this file turns a
// parsed arm64 statement into the operand form arch.ExtInstr.Encode consumes
// and routes statements only the layer can encode through the registry. It
// sits beside the main arm64 encoders, never inside them: the generated
// tables and the scalar, NEON and FP paths are untouched, and a statement
// reaches this file only when the mnemonic is registered in the extension
// layer and at least one operand is a scalable vector or predicate register.
//
// The spellings are the layer's own Plan 9 forms, the ones its metadata
// documents: Zn, Zm, Zd for the unpredicated three-vector class, Zm, Pg/M,
2026-10-07 00:11:40 +02:00
// Zdn for the predicated class, imm{, LSL #8}, Zdn for the immediate
// classes, and for the predicate family Pm.B, Pn.B, Pg/Z (or Pg.Z), Pd.B
// for the logical operations, Pn.B, Pg.Z, Pd.B for the breaks, Pm.T, Pn.T,
// Pd.T for the permutations, Rm, Rn, Pd.T for the while compares, PN8-PN15
2026-10-07 12:59:42 +02:00
// for the counter destinations, and the bare SETFFR. Stage three adds the
// crypto family (Zn.T, Zd.T, Zd.T read-back and the in-place Zd.T, Zd.T),
// the predicate counters (Pn.T, Pg, Rd; Pn.T, ZR; Rd, Pn.T, Rd; ZR and R
// terminators) and the reductions (Zn.T, Pg, Vd over the SIMD register
// V0-V31, with ZR and RSP accepted where the classes take them).
2026-10-06 19:52:20 +02:00
package asm
import (
"fmt"
"strconv"
"strings"
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
)
// arm64ExtStatement converts one instruction's operands into the extended
// layer's operand form. pinned reports that the statement belongs to the
// layer: the mnemonic is registered in the registry and the operand list
2026-10-07 00:11:40 +02:00
// carries at least one scalable vector, predicate or predicate-as-counter
// register, or no operands at all (the zero-operand forms such as SETFFR,
// which no scalar path could mean instead). A pinned statement can only
// encode through the layer, so every operand is read here and its
// diagnostic replaces whatever the scalar paths would have said about
// operands they cannot read; err is non-nil for a pinned statement whose
// operands the layer refuses, and extops is complete only when err is nil.
// Unpinned means the statement is nobody's: the caller falls through to the
// ordinary arm64 encoders, which keep their exact behaviour for every
// scalar, NEON and FP operand list.
2026-10-06 19:52:20 +02:00
func arm64ExtStatement ( mnem string , ops [] * ast . Operand ) ( extops [] arch . ExtOperand , pinned bool , err error ) {
if _ , ok := LookupExtension ( arch . ARM64 , mnem ); ! ok {
return nil , false , nil
}
2026-10-07 12:59:42 +02:00
if ! arm64ExtPinned ( mnem , ops ) {
2026-10-06 19:52:20 +02:00
return nil , false , nil
}
out := make ([] arch . ExtOperand , 0 , len ( ops ))
for i , op := range ops {
text := strings . Join ( strings . Fields ( op . Raw ), "" )
// The spelled shift of an immediate class: the shift is an attribute
// of the preceding immediate operand (imm{, LSL #8}, Zdn), never an
// operand of its own.
if amount , ok := strings . CutPrefix ( text , "LSL#" ); ok {
if len ( out ) == 0 || out [ len ( out ) - 1 ]. Kind != arch . ExtImm || out [ len ( out ) - 1 ]. HasShift {
return nil , true , fmt . Errorf ( "%s: operand %d (%s): LSL belongs straight after an immediate" , mnem , i + 1 , op . Raw )
}
n , convErr := strconv . Atoi ( amount )
if convErr != nil {
return nil , true , fmt . Errorf ( "%s: operand %d (%s): %q is not an LSL amount" , mnem , i + 1 , op . Raw , amount )
}
out [ len ( out ) - 1 ]. Shift , out [ len ( out ) - 1 ]. HasShift = n , true
continue
}
if op . Kind == ast . OpImmediate {
ext , ok := arm64ExtImmediate ( op )
if ! ok {
return nil , true , fmt . Errorf ( "%s: operand %d (%s) is not an immediate the layer can read" , mnem , i + 1 , op . Raw )
}
out = append ( out , ext )
continue
}
2026-10-07 13:24:20 +02:00
// The gather/scatter destination list, [Z13.B]: one scalable vector
// in brackets, its arrangement part of the instruction's identity.
if strings . HasPrefix ( text , "[" ) && strings . HasSuffix ( text , "]" ) {
if ext , ok := arm64ExtVector ( strings . Trim ( text , "[]" )); ok {
out = append ( out , ext )
continue
}
}
// The gather/scatter memory operand: a parenthesised register pair,
// an immediate-offset base or a lone vector base.
if ext , ok := arm64ExtSveMem ( text ); ok {
out = append ( out , ext )
continue
}
2026-10-06 19:52:20 +02:00
if ext , ok := arm64ExtVector ( text ); ok {
out = append ( out , ext )
continue
}
if ext , ok := arm64ExtPredicate ( text ); ok {
out = append ( out , ext )
continue
}
2026-10-07 00:11:40 +02:00
if ext , ok := arm64ExtCounter ( text ); ok {
out = append ( out , ext )
continue
}
2026-10-07 12:59:42 +02:00
if text == "ZR" {
out = append ( out , arch . ExtZeroRegister ())
continue
}
if text == "RSP" {
out = append ( out , arch . ExtStackPointer ())
continue
}
if ext , ok := arm64ExtSIMD ( text ); ok {
out = append ( out , ext )
continue
}
2026-10-07 00:11:40 +02:00
if ext , ok := arm64ExtGeneral ( text ); ok {
out = append ( out , ext )
continue
}
return nil , true , fmt . Errorf ( "%s: operand %d (%s) is not an extended-layer operand: want a scalable vector, predicate, general or counter register, or an immediate" , mnem , i + 1 , op . Raw )
2026-10-06 19:52:20 +02:00
}
return out , true , nil
}
2026-10-07 12:59:42 +02:00
// arm64ExtPinned reports whether the statement belongs to the layer. A
// mnemonic the extension layer registers on its own, one the generated
// arm64 table does not know, owns every one of its statements: no scalar
// path could mean it instead, and the layer's diagnostics replace the
// unsupported-instruction complaint. A mnemonic both tables carry (the
// SVE aliases of ADD, SUB and MUL) keeps the operand-shape test: any
// operand is a scalable vector, predicate or predicate-as-counter
// register, the shapes only the extension layer reads, or the statement
// carries no operands at all and the mnemonic's zero-operand forms claim
// it. The shape test is deliberately loose about the suffixes: P0/B is
// not a spelling the layer takes, but the P of it makes the statement the
// layer's, and the conversion then diagnoses the operand precisely
// instead of leaving it to a scalar path that would report an unrelated
// register error.
func arm64ExtPinned ( mnem string , ops [] * ast . Operand ) bool {
2026-10-07 00:11:40 +02:00
if len ( ops ) == 0 {
return true
}
2026-10-07 12:59:42 +02:00
if _ , shared := a64InstrTable [ mnem ]; ! shared {
return true
}
2026-10-06 19:52:20 +02:00
for _ , op := range ops {
if op . Kind == ast . OpImmediate {
continue
}
text := strings . Join ( strings . Fields ( op . Raw ), "" )
if _ , ok := arm64ExtVector ( text ); ok {
return true
}
if arm64ExtPredicateShape ( text ) {
return true
}
}
return false
}
2026-10-07 00:11:40 +02:00
// arm64ExtPredicateShape reports whether text spells a predicate or
// predicate-as-counter register at all: PN or P, digits, an optional
// arrangement suffix and an optional qualifier after a slash, whatever the
// qualifier says. The strict parses in arm64ExtPredicate and
// arm64ExtCounter judge the suffix; this shape only decides who the operand
// belongs to.
2026-10-06 19:52:20 +02:00
func arm64ExtPredicateShape ( text string ) bool {
if text == "" || text [ 0 ] != 'P' {
return false
}
text = text [ 1 :]
2026-10-07 00:11:40 +02:00
if rest , found := strings . CutPrefix ( text , "N" ); found {
text = rest
}
2026-10-06 19:52:20 +02:00
if i := strings . IndexByte ( text , '/' ); i >= 0 {
text = text [: i ]
}
if i := strings . IndexByte ( text , '.' ); i >= 0 {
text = text [: i ]
}
_ , err := strconv . Atoi ( text )
return err == nil && text != ""
}
// arm64ExtImmediate converts a $ immediate into the layer's form. The
// parser folds a parenthesised constant expression in full ($(255<<8)) and
// reads a bare literal greedily, dropping any trailing operator tokens:
// $255<<8 parses as 255 with the shift silently gone. Encoding that silent
// prefix would assemble what the text did not say, so an unparenthesised
// immediate is accepted only when its whole text reads back as one integer
// carrying the parser's value.
func arm64ExtImmediate ( op * ast . Operand ) ( arch . ExtOperand , bool ) {
if op . Kind != ast . OpImmediate || ! op . Imm . HasVal {
return arch . ExtOperand {}, false
}
text := strings . Join ( strings . Fields ( strings . TrimPrefix ( op . Raw , "$" )), "" )
if ! strings . HasPrefix ( text , "(" ) {
if _ , parseErr := strconv . ParseInt ( text , 0 , 64 ); parseErr != nil {
return arch . ExtOperand {}, false
}
}
v := op . Imm . Val
if op . Imm . Neg {
v = - v
}
return arch . ExtOperand { Kind : arch . ExtImm , Imm : v }, true
}
// arm64ExtVector parses a scalable vector register operand: Z0..Z31 with an
// optional element-size suffix, Z0.S. The arrangement is carried as written
// and the encoding validates it against the form.
func arm64ExtVector ( text string ) ( arch . ExtOperand , bool ) {
reg , arr , ok := arm64ExtReg ( text , 'Z' )
if ! ok {
return arch . ExtOperand {}, false
}
return arch . ExtOperand { Kind : arch . ExtZReg , Reg : reg , Arr : arr }, true
}
// arm64ExtPredicate parses a predicate register operand: P0..P15 with an
2026-10-07 00:11:40 +02:00
// optional element-size suffix (P0.B) and an optional qualifier in either
// spelling the corpus and the wired forms use, P0/M and P0.Z.
2026-10-06 19:52:20 +02:00
func arm64ExtPredicate ( text string ) ( arch . ExtOperand , bool ) {
qual := arch . ExtQualNone
if base , suffix , found := strings . Cut ( text , "/" ); found {
switch suffix {
case "M" :
qual = arch . ExtQualMerging
case "Z" :
qual = arch . ExtQualZeroing
default :
return arch . ExtOperand {}, false
}
text = base
2026-10-07 00:11:40 +02:00
} else if base , suffix , found := strings . Cut ( text , "." ); found &&
( suffix == "Z" || suffix == "M" ) {
// The dot qualifier stands in place of an arrangement, the spelling
// the toolchain's corpus writes (P1.Z, P14.M).
qual = arch . ExtQualMerging
if suffix == "Z" {
qual = arch . ExtQualZeroing
}
text = base
2026-10-06 19:52:20 +02:00
}
reg , arr , ok := arm64ExtReg ( text , 'P' )
if ! ok {
return arch . ExtOperand {}, false
}
return arch . ExtOperand { Kind : arch . ExtPReg , Reg : reg , Arr : arr , Qual : qual }, true
}
2026-10-07 00:11:40 +02:00
// arm64ExtCounter parses a predicate-as-counter register operand: PN8..PN15
// with an optional element-size suffix, PN14.S. The register range is the
// counter range the layer's convention carries; the encoding validates it.
func arm64ExtCounter ( text string ) ( arch . ExtOperand , bool ) {
rest , ok := strings . CutPrefix ( text , "PN" )
if ! ok {
return arch . ExtOperand {}, false
}
reg , arr , ok := arm64ExtRegDigits ( rest )
if ! ok {
return arch . ExtOperand {}, false
}
return arch . ExtOperand { Kind : arch . ExtPNReg , Reg : reg , Arr : arr }, true
}
// arm64ExtGeneral parses a general register operand: R0..R30, the plain
2026-10-07 12:59:42 +02:00
// spelling the while-compare forms take, beside the ZR and RSP spellings of
// the thirty-first slot the conversion above reads. The register range is
// left to the encoding, whose diagnostics name it.
2026-10-07 00:11:40 +02:00
func arm64ExtGeneral ( text string ) ( arch . ExtOperand , bool ) {
rest , ok := strings . CutPrefix ( text , "R" )
if ! ok {
return arch . ExtOperand {}, false
}
reg , arr , ok := arm64ExtRegDigits ( rest )
if ! ok || arr != arch . ExtArrNone {
return arch . ExtOperand {}, false
}
return arch . ExtOperand { Kind : arch . ExtGReg , Reg : reg }, true
}
2026-10-07 12:59:42 +02:00
// arm64ExtSIMD parses a 128-bit SIMD register operand: V0..V31, written
// bare, the scalar destination the reductions and the crypto read-back
2026-10-07 19:17:20 +02:00
// forms take, or with the counted quadword suffix of the SVE2.1 QV class,
// V5.S4 reading four 32-bit lanes (the spellings .B16, .H8, .S4 and .D2;
// no other suffix parses). The register range is left to the encoding.
2026-10-07 12:59:42 +02:00
func arm64ExtSIMD ( text string ) ( arch . ExtOperand , bool ) {
rest , ok := strings . CutPrefix ( text , "V" )
if ! ok {
return arch . ExtOperand {}, false
}
2026-10-07 19:17:20 +02:00
arr := arch . ExtArrNone
for _ , q := range [] struct {
suffix string
arr arch . ExtArrangement
}{
{ "B16" , arch . ExtArrB },
{ "H8" , arch . ExtArrH },
{ "S4" , arch . ExtArrS },
{ "D2" , arch . ExtArrD },
} {
if s := "." + q . suffix ; strings . HasSuffix ( rest , s ) {
arr = q . arr
rest = rest [: len ( rest ) - len ( s )]
break
}
}
reg , bare , ok := arm64ExtRegDigits ( rest )
if ! ok || bare != arch . ExtArrNone {
2026-10-07 12:59:42 +02:00
return arch . ExtOperand {}, false
}
2026-10-07 19:17:20 +02:00
return arch . ExtOperand { Kind : arch . ExtVReg , Reg : reg , Arr : arr }, true
2026-10-07 12:59:42 +02:00
}
2026-10-07 13:24:20 +02:00
// arm64ExtSveMem parses the gather/scatter memory operand off a normalised
// operand text: the parenthesised pair (R6)(R14), (Z23.D<<1)(R24) and
// (Z4.S.UXTW)(R3), the immediate-offset base 6(Z7.S), and the lone vector
// base (Z5.D) of the stores. The second parenthesis accepts the
// stack-pointer spelling RSP; the ranges and the mode's own rules are left
// to the encoding, whose diagnostics name them.
func arm64ExtSveMem ( text string ) ( arch . ExtOperand , bool ) {
// The immediate-offset spelling: digits straight before the parenthesis.
if i := strings . IndexByte ( text , '(' ); i > 0 && i == strings . LastIndexByte ( text , '(' ) {
disp , err := strconv . ParseUint ( text [: i ], 10 , 32 )
if err == nil && strings . HasSuffix ( text , ")" ) {
op , ok := arm64ExtSveMemGroup ( text [ i + 1 : len ( text ) - 1 ])
if ! ok {
return arch . ExtOperand {}, false
}
if ! op . BaseVec || op . Extend != 0 || op . Shift != 0 {
return arch . ExtOperand {}, false
}
op . Imm = int64 ( disp )
return op , true
}
}
// The parenthesised forms: one group or two.
rest , ok := strings . CutPrefix ( text , "(" )
if ! ok || ! strings . HasSuffix ( text , ")" ) {
return arch . ExtOperand {}, false
}
rest = rest [: len ( rest ) - 1 ]
first := rest
op := arch . ExtOperand { Off : - 1 }
if base , second , found := strings . Cut ( rest , ")(" ); found {
first = base
off , ok := arm64ExtSveMemOffset ( second )
if ! ok {
return arch . ExtOperand {}, false
}
op = off
}
group , ok := arm64ExtSveMemGroup ( first )
if ! ok {
return arch . ExtOperand {}, false
}
group . Off = op . Off
group . Reg31 = op . Reg31
return group , true
}
// arm64ExtSveMemGroup parses one parenthesised memory register: R6, R6<<3,
// Z23.D, Z23.D<<1, Z4.S.UXTW or Z7.D.SXTW. The general registers run
// R0-R30 and the scalable vectors Z0-Z31 with an .S or .D element size and
// an optional UXTW or SXTW extension; the ranges are left to the encoding.
func arm64ExtSveMemGroup ( text string ) ( arch . ExtOperand , bool ) {
op := arch . ExtOperand { Kind : arch . ExtSveMem , Off : - 1 }
if base , shift , found := strings . Cut ( text , "<<" ); found {
n , err := strconv . Atoi ( shift )
if err != nil || n < 0 {
return arch . ExtOperand {}, false
}
op . Shift = n
text = base
}
parts := strings . Split ( text , "." )
switch parts [ 0 ][ 0 ] {
case 'R' :
reg , err := strconv . Atoi ( parts [ 0 ][ 1 :])
if err != nil || len ( parts ) != 1 {
return arch . ExtOperand {}, false
}
op . Reg = reg
case 'Z' :
reg , err := strconv . Atoi ( parts [ 0 ][ 1 :])
if err != nil || len ( parts ) < 2 || len ( parts ) > 3 {
return arch . ExtOperand {}, false
}
switch parts [ 1 ] {
case "S" :
op . Arr = arch . ExtArrS
case "D" :
op . Arr = arch . ExtArrD
default :
return arch . ExtOperand {}, false
}
op . Reg = reg
op . BaseVec = true
if len ( parts ) == 3 {
switch parts [ 2 ] {
case "UXTW" :
op . Extend = 1
case "SXTW" :
op . Extend = 2
default :
return arch . ExtOperand {}, false
}
}
default :
return arch . ExtOperand {}, false
}
return op , true
}
// arm64ExtSveMemOffset parses the second parenthesis of a gather/scatter
// memory operand: a plain R0-R30 or the stack-pointer spelling RSP.
func arm64ExtSveMemOffset ( text string ) ( arch . ExtOperand , bool ) {
if text == "RSP" {
return arch . ExtOperand { Off : 31 , Reg31 : 2 }, true
}
if len ( text ) < 2 || text [ 0 ] != 'R' {
return arch . ExtOperand {}, false
}
reg , err := strconv . Atoi ( text [ 1 :])
if err != nil {
return arch . ExtOperand {}, false
}
return arch . ExtOperand { Off : reg }, true
}
2026-10-07 00:11:40 +02:00
// arm64ExtRegDigits parses the digits and optional arrangement suffix of a
// register spelling once the letter prefix is gone.
func arm64ExtRegDigits ( text string ) ( reg int , arr arch . ExtArrangement , ok bool ) {
if base , suffix , found := strings . Cut ( text , "." ); found {
switch suffix {
case "B" :
arr = arch . ExtArrB
case "H" :
arr = arch . ExtArrH
case "S" :
arr = arch . ExtArrS
case "D" :
arr = arch . ExtArrD
case "Q" :
arr = arch . ExtArrQ
default :
return 0 , 0 , false
}
text = base
}
n , err := strconv . Atoi ( text )
if err != nil || n < 0 {
return 0 , 0 , false
}
return n , arr , true
}
2026-10-06 19:52:20 +02:00
// arm64ExtReg parses Pn or Zn with an optional arrangement suffix off a
// normalised operand text. The register range is left to the encoding: the
// layer's own diagnostics name the range a form carries.
func arm64ExtReg ( text string , letter byte ) ( reg int , arr arch . ExtArrangement , ok bool ) {
if len ( text ) < 2 || text [ 0 ] != letter {
return 0 , 0 , false
}
digits := text [ 1 :]
if base , suffix , found := strings . Cut ( digits , "." ); found {
switch suffix {
case "B" :
arr = arch . ExtArrB
case "H" :
arr = arch . ExtArrH
case "S" :
arr = arch . ExtArrS
case "D" :
arr = arch . ExtArrD
case "Q" :
arr = arch . ExtArrQ
default :
return 0 , 0 , false
}
digits = base
}
n , err := strconv . Atoi ( digits )
if err != nil || n < 0 {
return 0 , 0 , false
}
return n , arr , true
}