2026-10-06 23:55:04 +02:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-06 23:56:23 +02:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|