2026-10-07 20:20:06 +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 amd64 extension layer: this file turns a parsed
// amd64 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 amd64 encoders, never inside them: the generated table, the
// legacy SSE paths and the VEX and EVEX mechanisms are untouched, and a
// statement reaches this file only when the mnemonic is registered in the
// extension layer and the scalar paths cannot encode it.
//
// The spellings are the layer's own Plan 9 forms, the ones its metadata
// documents: the vector registers carry the house names X0, Y0 and Z0 (the
// EVEX 128, 256 and 512-bit classes, registers 16 to 31 included), the general
// registers the width their spelling fixes (RAX through R15, EAX through EDI
// and R8D through R15D), the opmask registers K0 through K7, and the memory
// operand the base-relative form off(base) with the optional scaled index
// off(base)(index*scale) the SIB byte carries. The decorations ride the
// operand in braces: the write mask {k1} through {k7} and zeroing {z} on the
// destination, the {1toN} broadcast on the memory source, and {sae} and
// {rn-sae} through {rz-sae} beside the rounding-capable destinations. The
// imm8-control forms take their control byte as the leading $ immediate the
// reference listings write first.
//
// The Feature field stays metadata at assembly time: the assembler has no CPU,
// the toolchain does not gate assembly on CPU features, and every registered
// feature assembles, the behaviour the arm64 wiring established
// (asm/arm64_ext.go). The field remains for the linter and the listing.
package asm
import (
"fmt"
"strconv"
"strings"
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
)
// amd64ExtStatement 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 amd64 registry and the scalar
// paths cannot encode it. 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 amd64 encoders, which
// keep their exact behaviour for every statement they knew before.
func amd64ExtStatement ( mnem string , ops [] * ast . Operand ) ( extops [] arch . ExtOperand , pinned bool , err error ) {
if _ , ok := LookupExtension ( arch . AMD64 , mnem ); ! ok {
return nil , false , nil
}
if Encodable ( mnem ) {
// A mnemonic the main encoder knows is never the layer's, whatever
// the registry carries: the scalar paths keep the statement. No
// registered mnemonic trips this today (the layer is sealed by
// test), but the guard keeps the fall-through promise exact should
// the toolchain ever learn one of these names.
return nil , false , nil
}
out := make ([] arch . ExtOperand , 0 , len ( ops ))
for i , op := range ops {
ext , convErr := amd64ExtOperand ( mnem , op , i + 1 )
if convErr != nil {
return nil , true , convErr
}
out = append ( out , ext )
}
return out , true , nil
}
2026-10-07 20:48:22 +02:00
// EncodeAmd64Statement runs one parsed amd64 statement through the layer
// exactly as the assembler does: the operands convert the amd64ExtOperand way
// and the mnemonic resolves and encodes through the registry. pinned reports
// that the statement belongs to the layer alone (the mnemonic is registered
// and the scalar paths cannot encode it); err is the layer's own refusal of
// the operands, the same text the assembler prints, so the linter surfaces
// one diagnostic where assembly would fail. A statement the scalar paths own
// returns pinned false, with nothing to report.
func EncodeAmd64Statement ( mnem string , ops [] * ast . Operand ) ( code [] byte , pinned bool , err error ) {
extops , pinned , err := amd64ExtStatement ( mnem , ops )
if ! pinned || err != nil {
return nil , pinned , err
}
code , err = EncodeExtension ( arch . AMD64 , mnem , extops ... )
if err != nil {
return nil , true , err
}
return code , true , nil
}
2026-10-07 20:20:06 +02:00
// amd64ExtOperand converts one parsed operand into the layer's form: a $ immediate,
// a vector, general or opmask register, or a base-relative memory operand, each
// with the brace decorations the spelling carries.
func amd64ExtOperand ( mnem string , op * ast . Operand , pos int ) ( arch . ExtOperand , error ) {
if op . Kind == ast . OpImmediate {
return amd64ExtImmediate ( mnem , op , pos )
}
body , dec , err := amd64ExtDecorations ( mnem , op , pos )
if err != nil {
return arch . ExtOperand {}, err
}
if strings . ContainsRune ( body , '(' ) {
ext , ok := amd64ExtMemory ( mnem , op , pos )
if ! ok {
return arch . ExtOperand {}, fmt . Errorf ( "%s: operand %d (%s) is not an extended-layer operand: want a base-relative memory operand, off(base)(index*scale) shape" , mnem , pos , op . Raw )
}
ext . Broadcast = dec . broadcast
if dec . hasMask {
ext . Mask , ext . HasMask = dec . mask , true
}
ext . Zeroing = dec . zeroing
ext . Round = dec . round
return ext , nil
}
ext , ok := amd64ExtRegister ( mnem , op , pos , body )
if ! ok {
return arch . ExtOperand {}, fmt . Errorf ( "%s: operand %d (%s) is not an extended-layer operand: want a vector, general or opmask register, a base-relative memory operand or an immediate" , mnem , pos , op . Raw )
}
if dec . hasMask {
ext . Mask , ext . HasMask = dec . mask , true
}
ext . Zeroing = dec . zeroing
ext . Round = dec . round
return ext , nil
}
// amd64ExtImmediate converts a $ immediate into the layer's form. The
// parser folds a parenthesised constant expression in full 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 amd64ExtImmediate ( mnem string , op * ast . Operand , pos int ) ( arch . ExtOperand , error ) {
if ! op . Imm . HasVal {
return arch . ExtOperand {}, fmt . Errorf ( "%s: operand %d (%s) is not an immediate the layer can read" , mnem , pos , op . Raw )
}
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 {}, fmt . Errorf ( "%s: operand %d (%s) is not an immediate the layer can read" , mnem , pos , op . Raw )
}
}
v := op . Imm . Val
if op . Imm . Neg {
v = - v
}
return arch . ExtOperand { Kind : arch . ExtImm , Imm : v }, nil
}
// amd64ExtRegister parses a register operand off a normalised operand body:
// the vector classes X0-X31, Y0-Y31 and Z0-Z31, the width-fixed general
// spellings RAX through R15 and EAX through R15D, and the opmask registers
// K0-K7. The register ranges are left to the encoding, whose diagnostics
// name them.
func amd64ExtRegister ( mnem string , op * ast . Operand , pos int , body string ) ( arch . ExtOperand , bool ) {
if body == "" {
return arch . ExtOperand {}, false
}
r , ok := ParseReg ( body )
if ! ok {
return arch . ExtOperand {}, false
}
switch {
case r . mask :
return arch . ExtOperand { Kind : arch . ExtKReg , Reg : r . idx }, true
case r . isVec ():
kind := arch . ExtXMM
switch r . size {
case 32 :
kind = arch . ExtYMM
case 64 :
kind = arch . ExtZMM
}
return arch . ExtOperand { Kind : kind , Reg : r . idx }, true
case r . size == 8 :
return arch . ExtOperand { Kind : arch . ExtR64 , Reg : r . idx }, true
case r . size == 4 :
return arch . ExtOperand { Kind : arch . ExtR32 , Reg : r . idx }, true
}
return arch . ExtOperand {}, false
}
// amd64ExtMemory parses a base-relative memory operand off the parsed
// address: off(base) and off(base)(index*scale), the SIB shapes the layer's
// entries carry. The base and the index are general registers spelled in any
// width the house names offer, the displacement the leading signed term, and
// a group whose scale is not written scales by one, the choice the main
// amd64 paths make for the same spelling. Vector, opmask and segment
// registers are refused as base and index, and so is every frame form: the
// layer's memory operand is hardware addressing alone.
func amd64ExtMemory ( mnem string , op * ast . Operand , pos int ) ( arch . ExtOperand , bool ) {
a := op . Addr
if a . Range != nil || a . Base == "" {
return arch . ExtOperand {}, false
}
if a . Sym != nil && a . Sym . Pseudo != "" {
return arch . ExtOperand {}, false
}
base , ok := amd64ExtGprNumber ( a . Base )
if ! ok {
return arch . ExtOperand {}, false
}
ext := arch . ExtOperand { Kind : arch . ExtMem , Reg : base , Imm : a . Offset }
if a . Index != "" {
index , ok := amd64ExtGprNumber ( a . Index )
if ! ok {
return arch . ExtOperand {}, false
}
scale := a . Scale
if scale == 0 {
scale = 1
}
ext . Index , ext . Scale , ext . HasIndex = index , scale , true
}
return ext , true
}
// amd64ExtGprNumber resolves one general-register spelling to its number:
// whatever the register table carries for indices 0-15, the vector, opmask,
// x87, MMX, segment and control-debug classes refused, so a vector register
// in a base or index position names itself rather than encoding as its
// same-numbered general register.
func amd64ExtGprNumber ( name string ) ( int , bool ) {
r , ok := ParseReg ( name )
if ! ok || r . mask || r . fp || r . mmx || r . seg != 0 || r . ctl != 0 || r . size > 8 {
return 0 , false
}
return r . idx , true
}
// amd64ExtDecorations splits the brace decorations off a normalised operand
// text and returns the body before the first brace and the decorations they
// spell: the write mask {k1} through {k7}, zeroing {z}, the {1toN} broadcast
// and the rounding controls {sae} and {rn-sae} through {rz-sae}, matched
// case-insensitively the way the register spellings are. The mask, zeroing
// and rounding fields land on the operand the conversion builds; whether the
// position takes them is the encoding's judgement, whose diagnostics name the
// entry. The broadcast factor N is checked as a number and otherwise left to
// the entry: the layer's model carries the spelling, not the lane count.
func amd64ExtDecorations ( mnem string , op * ast . Operand , pos int ) ( body string , dec amd64ExtDecor , err error ) {
compact := strings . Join ( strings . Fields ( op . Raw ), "" )
i := strings . IndexByte ( compact , '{' )
if i < 0 {
return compact , dec , nil
}
body = compact [: i ]
for i < len ( compact ) {
if compact [ i ] != '{' {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s): text between brace decorations" , mnem , pos , op . Raw )
}
end := strings . IndexByte ( compact [ i :], '}' )
if end < 0 {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s): brace decoration without a closing brace" , mnem , pos , op . Raw )
}
content := strings . ToUpper ( compact [ i + 1 : i + end ])
switch {
case content == "Z" :
if dec . zeroing {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s) carries two zeroing decorations" , mnem , pos , op . Raw )
}
dec . zeroing = true
case content == "SAE" :
if dec . round != arch . ExtRoundNone {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s) carries two rounding controls" , mnem , pos , op . Raw )
}
dec . round = arch . ExtRoundSAE
case content == "RN-SAE" :
if dec . round != arch . ExtRoundNone {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s) carries two rounding controls" , mnem , pos , op . Raw )
}
dec . round = arch . ExtRoundNearest
case content == "RD-SAE" :
if dec . round != arch . ExtRoundNone {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s) carries two rounding controls" , mnem , pos , op . Raw )
}
dec . round = arch . ExtRoundDown
case content == "RU-SAE" :
if dec . round != arch . ExtRoundNone {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s) carries two rounding controls" , mnem , pos , op . Raw )
}
dec . round = arch . ExtRoundUp
case content == "RZ-SAE" :
if dec . round != arch . ExtRoundNone {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s) carries two rounding controls" , mnem , pos , op . Raw )
}
dec . round = arch . ExtRoundTruncate
case strings . HasPrefix ( content , "K" ) && content != "K" :
n , convErr := strconv . Atoi ( content [ 1 :])
if convErr != nil || n < 0 {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s): %q is not a mask decoration, want {k1} through {k7}" , mnem , pos , op . Raw , content )
}
if dec . hasMask {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s) carries two write masks" , mnem , pos , op . Raw )
}
dec . mask , dec . hasMask = n , true
case strings . HasPrefix ( content , "1TO" ):
if _ , convErr := strconv . Atoi ( content [ 3 :]); convErr != nil {
return "" , dec , fmt . Errorf ( "%s: operand %d (%s): %q is not a broadcast decoration, want {1toN}" , mnem , pos , op . Raw , content )
}
dec . broadcast = true
default :
return "" , dec , fmt . Errorf ( "%s: operand %d (%s): {%s} is not a decoration the layer reads: want {k1} through {k7}, {z}, {1toN}, {sae} or {rn-sae} through {rz-sae}" , mnem , pos , op . Raw , content )
}
i += end + 1
}
return body , dec , nil
}
// amd64ExtDecor carries the brace decorations one operand's spelling names.
type amd64ExtDecor struct {
mask int
hasMask bool
zeroing bool
broadcast bool
round arch . ExtRounding
}