// Copyright (c) 2026 Petr BalvĂ­n (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) } } }