2026-10-06 19:53:59 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package lint
|
|
|
|
|
|
|
|
|
|
import (
|
2026-10-07 20:48:22 +02:00
|
|
|
"strings"
|
2026-10-06 19:53:59 +02:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
2026-10-07 20:48:22 +02:00
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
|
2026-10-06 19:53:59 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TestExtensionInstructionsAreKnown pins the lint side of the
|
|
|
|
|
// extended-instruction layer: every mnemonic the arm64 registry carries
|
|
|
|
|
// lints without unknown-instruction, and a kernel the encoder accepts lints
|
|
|
|
|
// clean, because the wired layer is the acceptance rather than a gap of the
|
|
|
|
|
// generated table.
|
|
|
|
|
func TestExtensionInstructionsAreKnown(t *testing.T) {
|
|
|
|
|
src := `#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
ADD Z1.S, Z2.S, Z0.S
|
|
|
|
|
SUBR Z1.S, P0/M, Z0.S
|
|
|
|
|
SQADD Z0.D, Z1.D, Z2.D
|
|
|
|
|
UQADD Z0.D, Z1.D, Z2.D
|
|
|
|
|
SQSUB Z0.D, Z1.D, Z2.D
|
|
|
|
|
UQSUB Z0.D, Z1.D, Z2.D
|
|
|
|
|
SMULH Z3.D, Z4.D, Z5.D
|
|
|
|
|
ADD $255, Z0.S
|
|
|
|
|
MUL $-128, Z0.B
|
|
|
|
|
RET
|
|
|
|
|
`
|
|
|
|
|
diags := lintArchFile(t, "ext_arm64.s", src, arch.ARM64, asm.AssembleFileARM64)
|
|
|
|
|
for _, code := range []string{CodeUnknownInstr, CodeUnencodable} {
|
|
|
|
|
if n := codes(diags)[code]; n != 0 {
|
|
|
|
|
t.Errorf("%s fired %d times on the wired extension: %+v", code, n, diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(diags) != 0 {
|
|
|
|
|
t.Errorf("a kernel the encoder accepts must lint clean, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The registry consult is the layer's own, not a loosening of the rule:
|
|
|
|
|
// a mnemonic neither table nor registry carries still fires.
|
|
|
|
|
diags = lintSrcArch(t, "ext_arm64.s", `#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
FROBNICATE Z0.S, Z1.S, Z2.S
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUnknownInstr] != 1 {
|
|
|
|
|
t.Errorf("an unregistered mnemonic must fire unknown-instruction once: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-07 20:48:22 +02:00
|
|
|
|
|
|
|
|
// amd64ExtKernel spells one register-form statement per distinct mnemonic the
|
|
|
|
|
// amd64 extension layer registers, decorations beside them where the entries
|
|
|
|
|
// carry them, and the memory, broadcast and scaled-index spellings at the
|
|
|
|
|
// tail. TestAmd64ExtensionInstructionsAreKnown lints it; the completeness
|
|
|
|
|
// guard inside fails the moment a registered mnemonic lacks its row, so a
|
|
|
|
|
// family added to the registry lands here in the same change.
|
|
|
|
|
const amd64ExtKernel = `#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
VCVTNE2PS2BF16 Z1, Z2, Z3{K5}
|
|
|
|
|
VCVTNEPS2BF16 Z1, Y2{K5}
|
|
|
|
|
VDPBF16PS Z1, Z2, Z3{K5}
|
|
|
|
|
VP2INTERSECTD Z1, Z2, K3
|
|
|
|
|
VP2INTERSECTQ Z1, Z2, K3
|
|
|
|
|
VMOVSH X1, X2, X3
|
|
|
|
|
VMOVW R12, X1
|
|
|
|
|
VADDSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VSUBSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VMULSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VDIVSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VMINSH X1, X2, X3{SAE}
|
|
|
|
|
VMAXSH X1, X2, X3{SAE}
|
|
|
|
|
VSQRTSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VSCALEFSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VGETEXPSH X1, X2, X3{SAE}
|
|
|
|
|
VCOMISH X1, X2{SAE}
|
|
|
|
|
VUCOMISH X1, X2{SAE}
|
|
|
|
|
VCVTSS2SH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VCVTSH2SS X1, X2, X3
|
|
|
|
|
VCVTSH2SD X1, X2, X3
|
|
|
|
|
VCVTSD2SH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VCVTSI2SH X1, R12D, X2
|
|
|
|
|
VCVTUSI2SH X1, R12D, X2
|
|
|
|
|
VCVTSH2SI X1, R12D
|
|
|
|
|
VCVTSH2USI X1, R12D
|
|
|
|
|
VADDPH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VSUBPH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VMULPH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VDIVPH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VMINPH Z1, Z2, Z3{K5}{SAE}
|
|
|
|
|
VMAXPH Z1, Z2, Z3{K5}{SAE}
|
|
|
|
|
VSQRTPH Z1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCMPSH $0x7b, X1, X2, K3
|
|
|
|
|
VGETMANTSH $0x0b, X1, X2, X4
|
|
|
|
|
VREDUCESH $0x7b, X1, X2, X4
|
|
|
|
|
VRNDSCALESH $0x7b, X1, X2, X4
|
|
|
|
|
VCVTPH2W Z1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCVTPH2UW Z1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCVTW2PH Z1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCVTUW2PH Z1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCVTPH2DQ Y1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCVTPH2UDQ Y1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCVTDQ2PH Z1, Y2{K5}{RZ-SAE}
|
|
|
|
|
VCVTUDQ2PH Z1, Y2{K5}{RZ-SAE}
|
|
|
|
|
VCVTPH2QQ X1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCVTPH2UQQ X1, Z2{K5}{RZ-SAE}
|
|
|
|
|
VCVTQQ2PH Z1, X2{K5}{RZ-SAE}
|
|
|
|
|
VCVTUQQ2PH Z1, X2{K5}{RZ-SAE}
|
|
|
|
|
VCVTPH2PD X1, Z2{K5}
|
|
|
|
|
VCVTPD2PH Z1, X2{K5}{RZ-SAE}
|
|
|
|
|
VRNDSCALEPH $0x7b, Z1, Z3{K5}
|
|
|
|
|
VREDUCEPH $0x7b, Z1, Z3{K5}
|
|
|
|
|
VGETMANTPH $0x0b, Z1, Z3{K5}
|
|
|
|
|
VFMADD132PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMADD213PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMADD231PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMSUB132PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMSUB213PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMSUB231PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMADDSUB132PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMADDSUB213PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMADDSUB231PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMSUBADD132PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMSUBADD213PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMSUBADD231PH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMADD132SH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFMADD213SH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFMADD231SH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFMSUB132SH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFMSUB213SH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFMSUB231SH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFMULCPH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFCMULCPH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMULCSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFCMULCSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFMADDCPH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFCMADDCPH Z1, Z2, Z3{K5}{RZ-SAE}
|
|
|
|
|
VFMADDCSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VFCMADDCSH X1, X2, X3{RZ-SAE}
|
|
|
|
|
VMINMAXPH $0x7b, Z1, Z2, Z4{K5}
|
|
|
|
|
VMINMAXSH $0x7b, X1, X2, X4
|
|
|
|
|
VPDPWSUD X1, X2, X3
|
|
|
|
|
VPDPWSUDS X1, X2, X3
|
|
|
|
|
VPDPWUSD X1, X2, X3
|
|
|
|
|
VPDPWUSDS X1, X2, X3
|
|
|
|
|
VMOVSH (R9), X30
|
|
|
|
|
VMOVSH X30, (R9)
|
|
|
|
|
VADDPH Z28, (R9), Z30{K7}{Z}
|
|
|
|
|
VCVTDQ2PH (R9){1TO8}, Y30
|
|
|
|
|
VADDPH Z29, (R9)(R12*2), Z30
|
|
|
|
|
VPDPWSUD X2, 127(R9), X1
|
|
|
|
|
RET
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
// TestAmd64ExtensionInstructionsAreKnown pins the lint side of the amd64
|
|
|
|
|
// extension layer: every distinct mnemonic the registry carries lints without
|
|
|
|
|
// unknown-instruction, and the whole kernel, whose statements the encoder
|
|
|
|
|
// accepts one and all, lints clean, because the wired layer is the acceptance
|
|
|
|
|
// rather than a gap of the generated table.
|
|
|
|
|
func TestAmd64ExtensionInstructionsAreKnown(t *testing.T) {
|
|
|
|
|
// The completeness guard: a registered mnemonic without a row here is a
|
|
|
|
|
// gap in the kernel, not a pass.
|
|
|
|
|
for _, name := range asm.ExtensionNames(arch.AMD64) {
|
|
|
|
|
if !strings.Contains(amd64ExtKernel, "\t"+name+" ") {
|
|
|
|
|
t.Errorf("the amd64 extension kernel has no row for %q", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
diags := lintArchFile(t, "ext_amd64.s", amd64ExtKernel, arch.AMD64, func(f *ast.File) (*asm.Image, error) {
|
|
|
|
|
return asm.AssembleFile(f)
|
|
|
|
|
})
|
|
|
|
|
for _, code := range []string{CodeUnknownInstr, CodeUnencodable, CodeExtensionForm,
|
|
|
|
|
CodeNonportableRegister, CodeOperandCount, CodeRegisterWidthMismatch} {
|
|
|
|
|
if n := codes(diags)[code]; n != 0 {
|
|
|
|
|
t.Errorf("%s fired %d times on the wired extension: %+v", code, n, diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(diags) != 0 {
|
|
|
|
|
t.Errorf("a kernel the encoder accepts must lint clean, got %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The registry consult is the layer's own, not a loosening of the rule:
|
|
|
|
|
// a mnemonic neither table nor registry carries still fires.
|
|
|
|
|
diags = lintSrcArch(t, "ext_amd64.s", `#include "textflag.h"
|
|
|
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
|
|
|
FROBNICATE Z1, Z2, Z3
|
|
|
|
|
RET
|
|
|
|
|
`)
|
|
|
|
|
if codes(diags)[CodeUnknownInstr] != 1 {
|
|
|
|
|
t.Errorf("an unregistered mnemonic must fire unknown-instruction once: %+v", diags)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestAmd64ExtensionRefusals pins the sharper diagnostic the amd64 layer
|
|
|
|
|
// carries: a statement the registry refuses at assembly time is a lint error
|
|
|
|
|
// here, under the same text the assembler prints, and one diagnostic alone.
|
|
|
|
|
// The rows are the layer's own validator refusals: the decorations a form
|
|
|
|
|
// does not take, the registers outside a class, and the immediate layouts
|
|
|
|
|
// with their reserved bits.
|
|
|
|
|
func TestAmd64ExtensionRefusals(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
stmt string
|
|
|
|
|
want string
|
|
|
|
|
}{
|
|
|
|
|
{"VADDSH X5, X4, X6{K3}", "the entry's destination takes none"},
|
|
|
|
|
{"VADDPH Y5, Y4, Y6{RZ-SAE}", "wants a ZMM register"},
|
|
|
|
|
{"VADDPH Z5, Z4, Z6{K0}", "outside the masking registers k1-k7"},
|
|
|
|
|
{"VADDPH Z5, Z4, Z6{Z}", "zeroing without a write mask"},
|
|
|
|
|
{"VADDPH Z5, Z4, Z6{SAE}", "spells {sae} without a mode"},
|
|
|
|
|
{"VADDPH Z29, (R9){RZ-SAE}, Z30", "the memory operand takes none"},
|
|
|
|
|
{"VFMULCPH Z5, (R9){1TO8}, Z6", "the entry's memory operand takes none"},
|
|
|
|
|
{"VADDPH Z29, Z28, Z30{BOGUS}", "is not a decoration the layer reads"},
|
|
|
|
|
{"VADDPH Z29, Z28, Z30{K7}{K3}", "carries two write masks"},
|
|
|
|
|
{"VCMPSH $300, X29, X28, K5", "outside the unsigned byte range"},
|
|
|
|
|
{"VGETMANTSH $0x20, X29, X28, X30", "the upper nibble of the mantissa control is reserved"},
|
|
|
|
|
{"VCVTSI2SH X29, X12, X30", "general register"},
|
|
|
|
|
{"VADDPH Z5, Z4", "got 2 operands"},
|
|
|
|
|
{"VMOVSH (Z4), X30", "not an extended-layer operand"},
|
|
|
|
|
{"VMOVSH foo+4(SB), X30", "not an extended-layer operand"},
|
|
|
|
|
{"VMOVSH 8(R9)(DX*3), X30", "outside the byte multipliers"},
|
|
|
|
|
}
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
src := "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\t" + tt.stmt + "\n\tRET\n"
|
|
|
|
|
diags := lintSrcArch(t, "ext_amd64.s", src)
|
|
|
|
|
if len(diags) != 1 {
|
|
|
|
|
t.Errorf("%s: got %d diagnostics, want the one extension-form error: %+v", tt.stmt, len(diags), diags)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
d := diags[0]
|
|
|
|
|
if d.Code != CodeExtensionForm || d.Severity != Error {
|
|
|
|
|
t.Errorf("%s: got %s %s, want an error-severity %s: %+v", tt.stmt, d.Severity, d.Code, CodeExtensionForm, d)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(d.Message, tt.want) {
|
|
|
|
|
t.Errorf("%s: message %q does not name %q", tt.stmt, d.Message, tt.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|