Files
gasm-sdk/lsp/extensions_test.go
T

353 lines
13 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package lsp
import (
"encoding/json"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
)
// sveDoc is an arm64 kernel over the registered extended-instruction layer:
// every SVE form of ADD, a registry-only mnemonic, the signed-immediate MUL,
// and one mnemonic no layer registers, which must stay reported wherever the
// registry is silent.
const sveDoc = "#include \"textflag.h\"\n" +
"TEXT \u00b7f(SB), NOSPLIT, $0\n" +
"\tADD Z0.S, Z1.S, Z2.S\n" +
"\tADD Z1.S, P0/M, Z0.S\n" +
"\tADD $255, Z0.S\n" +
"\tSQADD Z3.S, Z4.S, Z5.S\n" +
"\tMUL $-128, Z0.B\n" +
"\tNOSUCHTHING Z0.S, Z1.S\n" +
"\tRET\n"
// completionItems runs one completion request and indexes the answers by
// label. The base surface predates this change and carries a few duplicate
// labels (a directive or pseudo-register can also sit in the generated
// tables); callers that care about a specific label count it in the slice.
func completionItems(t *testing.T, uri, text string, id int) ([]CompletionItem, map[string]CompletionItem) {
t.Helper()
in := session(uri, text) +
frame(id, "textDocument/completion", map[string]any{
"textDocument": map[string]any{"uri": uri},
"position": map[string]any{"line": 2, "character": 1},
}) + frame(nil, "exit", nil)
msgs := run(t, in)
resp := findByID(msgs, id)
if resp == nil {
t.Fatal("no completion response")
}
var items []CompletionItem
if err := json.Unmarshal(mustResult(t, resp), &items); err != nil {
t.Fatal(err)
}
out := make(map[string]CompletionItem, len(items))
for _, it := range items {
if _, dup := out[it.Label]; !dup {
out[it.Label] = it
}
}
return items, out
}
// countLabel counts the items carrying one label.
func countLabel(items []CompletionItem, label string) int {
n := 0
for _, it := range items {
if it.Label == label {
n++
}
}
return n
}
// hoverAt runs one hover request and returns the rendered markdown.
func hoverAt(t *testing.T, uri, text string, id, line, char int) *Hover {
t.Helper()
in := session(uri, text) +
frame(id, "textDocument/hover", map[string]any{
"textDocument": map[string]any{"uri": uri},
"position": map[string]any{"line": line, "character": char},
}) + frame(nil, "exit", nil)
msgs := run(t, in)
resp := findByID(msgs, id)
if resp == nil {
t.Fatal("no hover response")
}
var h *Hover
if err := json.Unmarshal(mustResult(t, resp), &h); err != nil {
t.Fatal(err)
}
return h
}
// publishedDiagnostics runs one didOpen and returns the pushed diagnostics.
func publishedDiagnostics(t *testing.T, uri, text string) []Diagnostic {
t.Helper()
msgs := run(t, session(uri, text)+frame(nil, "exit", nil))
pub := findMethod(msgs, "textDocument/publishDiagnostics")
if pub == nil {
t.Fatal("no publishDiagnostics notification")
}
var p publishDiagnosticsParams
json.Unmarshal(pub.Params, &p)
return p.Diagnostics
}
// TestCompletionExtensionMnemonicsARM64 checks that the registered extended
// mnemonics are offered where the toolchain mnemonics are, with the operand
// shapes the registry accepts, and that a mnemonic both layers know merges
// into one item instead of two.
func TestCompletionExtensionMnemonicsARM64(t *testing.T) {
items, byLabel := completionItems(t, "file:///v_arm64.s", sveDoc, 40)
sqadd, ok := byLabel["SQADD"]
if !ok {
t.Fatal("completion missing the registry mnemonic SQADD")
}
if sqadd.Kind != ciFunction {
t.Errorf("SQADD kind = %d, want the instruction kind", sqadd.Kind)
}
for _, want := range []string{"extension: ", "Z0-Z31.B/.H/.S/.D", "$imm"} {
if !strings.Contains(sqadd.Detail, want) {
t.Errorf("SQADD detail = %q, want it to carry %q", sqadd.Detail, want)
}
}
if !strings.Contains(sqadd.Documentation, "extension above the Go toolchain") {
t.Errorf("SQADD documentation = %q, want the extension notice", sqadd.Documentation)
}
subr, ok := byLabel["SUBR"]
if !ok {
t.Fatal("completion missing the registry mnemonic SUBR")
}
if !strings.Contains(subr.Detail, "P0-P7/M") {
t.Errorf("SUBR detail = %q, want the predicate spelling P0-P7/M", subr.Detail)
}
// ADD sits in both layers: one item, the table entry carrying the
// extension shapes beside its own summary.
if n := countLabel(items, "ADD"); n != 1 {
t.Errorf("ADD offered %d times, want one merged item", n)
}
add := byLabel["ADD"]
if !strings.Contains(add.Detail, "64-bit") || !strings.Contains(add.Detail, "extension: ") {
t.Errorf("ADD detail = %q, want the merged table and extension text", add.Detail)
}
if !strings.Contains(add.Documentation, "extension above the Go toolchain") {
t.Errorf("ADD documentation = %q, want the extension notice beside the table summary", add.Documentation)
}
// The toolchain surface is untouched beside the additions.
for _, want := range []string{"TEXT", "MOVD", "NOSPLIT"} {
if _, ok := byLabel[want]; !ok {
t.Errorf("completion lost the toolchain entry %q", want)
}
}
// No extended mnemonic arrives as a second item beside its merge.
for _, name := range asm.ExtensionNames(arch.ARM64) {
if n := countLabel(items, name); n != 1 {
t.Errorf("extended mnemonic %q offered %d times, want once", name, n)
}
}
}
// TestCompletionNoExtensionMnemonicsAMD64 is the per-architecture negative
// case: the extension layer registers nothing on amd64, so no extended
// mnemonic may be invented into the list and the toolchain surface stands.
func TestCompletionNoExtensionMnemonicsAMD64(t *testing.T) {
_, byLabel := completionItems(t, "file:///v_amd64.s", sveDoc, 41)
for _, banned := range []string{"SQADD", "SUBR", "UQADD", "UQSUB", "SMULH"} {
if _, ok := byLabel[banned]; ok {
t.Errorf("completion invented %q on amd64, whose layer registers nothing", banned)
}
}
if _, ok := byLabel["MOVQ"]; !ok {
t.Error("completion lost MOVQ on amd64")
}
if add, ok := byLabel["ADD"]; ok && strings.Contains(add.Detail, "extension: ") {
t.Errorf("ADD detail = %q, want no extension text where the layer is absent", add.Detail)
}
}
// TestHoverExtensionMnemonicARM64 checks that a registered extended mnemonic
// documents itself from the layer's metadata: the extension notice, the
// family, the operand shapes, the fixed encoding and the manual reference.
func TestHoverExtensionMnemonicARM64(t *testing.T) {
h := hoverAt(t, "file:///v_arm64.s", sveDoc, 42, 5, 3) // on SQADD
if h == nil {
t.Fatal("hover over SQADD returned nothing")
}
for _, want := range []string{
"**SQADD**: extension above the Go toolchain (sve)",
"Add signed saturating scalable vector elements, unpredicated: Z0-Z31.B/.H/.S/.D, Z0-Z31.B/.H/.S/.D, Z0-Z31.B/.H/.S/.D",
"unpredicated vectors, sve, encoding 0x04201000",
"$imm, Z0-Z31.B/.H/.S/.D",
"DDI 0487J",
} {
if !strings.Contains(h.Contents.Value, want) {
t.Errorf("hover = %q, want it to carry %q", h.Contents.Value, want)
}
}
if strings.Contains(h.Contents.Value, ".Q") {
t.Errorf("hover = %q, states an arrangement the layer's encoder refuses", h.Contents.Value)
}
}
// TestHoverSharedMnemonicARM64 checks the merge on a mnemonic both layers
// know: the table entry keeps its summary and the registered forms attach
// beside it.
func TestHoverSharedMnemonicARM64(t *testing.T) {
h := hoverAt(t, "file:///v_arm64.s", sveDoc, 43, 2, 3) // on ADD
if h == nil {
t.Fatal("hover over ADD returned nothing")
}
if !strings.Contains(h.Contents.Value, "**ADD**: ADD (64-bit)") {
t.Errorf("hover = %q, want the generated table summary", h.Contents.Value)
}
if !strings.Contains(h.Contents.Value, "extension above the Go toolchain") {
t.Errorf("hover = %q, want the registered forms beside the summary", h.Contents.Value)
}
if !strings.Contains(h.Contents.Value, "P0-P7/M") {
t.Errorf("hover = %q, want the predicated form's spelling", h.Contents.Value)
}
}
// TestHoverNoExtensionMnemonicAMD64 is the per-architecture negative case:
// with no registered layer on amd64, a hover over a registry-only mnemonic
// answers null exactly as before.
func TestHoverNoExtensionMnemonicAMD64(t *testing.T) {
if h := hoverAt(t, "file:///v_amd64.s", sveDoc, 44, 5, 3); h != nil {
t.Errorf("hover over SQADD on amd64 = %q, want null", h.Contents.Value)
}
}
// TestExtensionShapesMatchRegistry pins the measuring: the rendered shapes
// are exactly the spellings the registry's encoder accepts, checked by
// round-tripping the boundary spellings through asm.EncodeExtension.
func TestExtensionShapesMatchRegistry(t *testing.T) {
docs := extForms(arch.ARM64, "ADD")
if len(docs) != 3 {
t.Fatalf("ADD renders %d forms, want 3", len(docs))
}
wantShapes := []string{
"Z0-Z31.B/.H/.S/.D, Z0-Z31.B/.H/.S/.D, Z0-Z31.B/.H/.S/.D",
"Z0-Z31.B/.H/.S/.D, P0-P7/M, Z0-Z31.B/.H/.S/.D",
"$imm, Z0-Z31.B/.H/.S/.D",
}
for i, want := range wantShapes {
if docs[i].shape != want {
t.Errorf("form %d shape = %q, want %q", i, docs[i].shape, want)
}
}
// The predicate span ends where the encoder's three-bit field ends.
if _, err := asm.EncodeExtension(arch.ARM64, "ADD",
arch.ExtVector(0, arch.ExtArrS), arch.ExtPredicate(7, arch.ExtQualMerging), arch.ExtVector(1, arch.ExtArrS)); err != nil {
t.Errorf("P7/M refused: %v", err)
}
if _, err := asm.EncodeExtension(arch.ARM64, "ADD",
arch.ExtVector(0, arch.ExtArrS), arch.ExtPredicate(8, arch.ExtQualMerging), arch.ExtVector(1, arch.ExtArrS)); err == nil {
t.Error("P8/M encoded, the span overstates the registry")
}
// The arrangement suffixes stop at .D: .Q refuses while the shape omits it.
if _, err := asm.EncodeExtension(arch.ARM64, "ADD",
arch.ExtVector(0, arch.ExtArrQ), arch.ExtVector(1, arch.ExtArrQ), arch.ExtVector(2, arch.ExtArrQ)); err == nil {
t.Error(".Q encoded, the suffixes overstate the registry")
}
// And .B, which the shape spells, encodes.
if _, err := asm.EncodeExtension(arch.ARM64, "ADD",
arch.ExtVector(0, arch.ExtArrB), arch.ExtVector(1, arch.ExtArrB), arch.ExtVector(2, arch.ExtArrB)); err != nil {
t.Errorf(".B refused: %v", err)
}
}
// TestExtensionHoverWithoutProse checks the no-prose contract: a form whose
// metadata carries no summary states the encoding facts it has and nothing
// invented in their place.
func TestExtensionHoverWithoutProse(t *testing.T) {
docs := []extFormDoc{
{form: "unsigned immediate", shape: "$imm, Z0-Z31.B/.H/.S/.D", feature: "sve", word: 0x2520c000},
}
md := renderExtensionHover("TESTMNEM", docs)
for _, want := range []string{
"**TESTMNEM**: extension above the Go toolchain (sve)",
"- unsigned immediate: $imm, Z0-Z31.B/.H/.S/.D (sve, encoding 0x2520c000)",
} {
if !strings.Contains(md, want) {
t.Errorf("hover = %q, want it to carry %q", md, want)
}
}
}
// TestDiagnosticsExtensionOwnedARM64 pins the pass-through on arm64: the
// lint on this base still reports a registered mnemonic as unknown, and the
// server filters its own presentation through the registry, while a mnemonic
// no layer registers stays reported.
func TestDiagnosticsExtensionOwnedARM64(t *testing.T) {
diags := publishedDiagnostics(t, "file:///v_arm64.s", sveDoc)
for _, d := range diags {
if strings.Contains(d.Message, "SQADD") {
t.Errorf("registry mnemonic still reported: %+v", d)
}
}
unknown := 0
for _, d := range diags {
if d.Code == "unknown-instruction" {
unknown++
if !strings.Contains(d.Message, "NOSUCHTHING") {
t.Errorf("unknown-instruction against the wrong statement: %+v", d)
}
}
}
if unknown != 1 {
t.Errorf("unknown-instruction count = %d, want the one unregistered mnemonic", unknown)
}
}
// TestDiagnosticsExtensionOwnedFocused is the focused filter test: the
// server's own diagnostic computation, without the protocol, answers nothing
// against the registered mnemonics and keeps everything else.
func TestDiagnosticsExtensionOwnedFocused(t *testing.T) {
srv := &Server{docs: map[string]string{"file:///v_arm64.s": sveDoc}}
diags := srv.diagnosticsFor("file:///v_arm64.s")
if len(diags) == 0 {
t.Fatal("diagnostics empty, want the unregistered mnemonic")
}
for _, d := range diags {
switch {
case strings.Contains(d.Message, "SQADD"):
t.Errorf("registry mnemonic still reported: %+v", d)
case d.Code == "unknown-instruction":
if !strings.Contains(d.Message, "NOSUCHTHING") {
t.Errorf("unexpected unknown-instruction: %+v", d)
}
}
}
}
// TestDiagnosticsExtensionAMD64StillReported is the per-architecture
// negative case: on amd64 the registry owns nothing, so the same source
// keeps every unknown-instruction report, the SVE mnemonics included.
func TestDiagnosticsExtensionAMD64StillReported(t *testing.T) {
diags := publishedDiagnostics(t, "file:///v_amd64.s", sveDoc)
found := map[string]bool{}
for _, d := range diags {
if d.Code == "unknown-instruction" {
for _, mnem := range []string{"SQADD", "NOSUCHTHING"} {
if strings.Contains(d.Message, mnem) {
found[mnem] = true
}
}
}
}
for _, mnem := range []string{"SQADD", "NOSUCHTHING"} {
if !found[mnem] {
t.Errorf("amd64 lost the unknown-instruction report for %q: %+v", mnem, diags)
}
}
}