// Copyright (c) 2026 Petr BalvĂ­n (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, // 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 // 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). 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 // 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. 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 } if !arm64ExtPinned(mnem, ops) { 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 } // 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 } if ext, ok := arm64ExtVector(text); ok { out = append(out, ext) continue } if ext, ok := arm64ExtPredicate(text); ok { out = append(out, ext) continue } if ext, ok := arm64ExtCounter(text); ok { out = append(out, ext) continue } 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 } 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) } return out, true, nil } // 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 { if len(ops) == 0 { return true } if _, shared := a64InstrTable[mnem]; !shared { return true } 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 } // 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. func arm64ExtPredicateShape(text string) bool { if text == "" || text[0] != 'P' { return false } text = text[1:] if rest, found := strings.CutPrefix(text, "N"); found { text = rest } 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 // 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. 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 } 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 } 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 } // 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 // 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. 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 } // arm64ExtSIMD parses a 128-bit SIMD register operand: V0..V31, written // bare, the scalar destination the reductions and the crypto read-back // 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. func arm64ExtSIMD(text string) (arch.ExtOperand, bool) { rest, ok := strings.CutPrefix(text, "V") if !ok { return arch.ExtOperand{}, false } 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 { return arch.ExtOperand{}, false } return arch.ExtOperand{Kind: arch.ExtVReg, Reg: reg, Arr: arr}, true } // 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 } // 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 } // 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 }