// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package lint import ( "strings" "testing" "sourcedock.dev/petrbalvin/gasm-sdk/arch" "sourcedock.dev/petrbalvin/gasm-sdk/asm" "sourcedock.dev/petrbalvin/gasm-sdk/ast" ) // 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) } } // 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) } } }