feat(lsp): offer and document the registered extended mnemonics
Completion merges the extension registry's mnemonics beside the toolchain entries, with operand shapes measured against the layer's own encoder, and hover documents a registered mnemonic from its metadata: the extension notice, the forms, the features, the fixed encodings and the manual references. Assisted-by: GLM 5.3 Flash
This commit is contained in:
1 parent
a90ec84bee
commit
dadeda144a
3 files changed
+644
No files matched your search
@@ -0,0 +1,348 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The language-server side of the extended-instruction layer. The registry
|
||||
// in asm carries the mnemonics the generated architecture tables do not know;
|
||||
// this file lets completion and hover document those mnemonics from the
|
||||
// layer's own metadata, and lets diagnostics leave the layer's verdicts to
|
||||
// it. Nothing the server states about a registered mnemonic is invented:
|
||||
// the summaries, forms, features, encodings and references are read from the
|
||||
// metadata, and the operand spellings are measured against the layer's own
|
||||
// encoder, so the editor surface can never promise a spelling the assembler
|
||||
// would refuse.
|
||||
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
|
||||
)
|
||||
|
||||
// extFormDoc is the rendering of one registered form: everything hover and
|
||||
// completion state about it.
|
||||
type extFormDoc struct {
|
||||
summary string // the metadata's one-line description ("" when absent)
|
||||
form string // the operand shape's label, from ExtForm.String
|
||||
shape string // the operand spellings the layer's encoder accepts
|
||||
feature string // the architecture feature, from ExtFeature
|
||||
word uint32 // the fixed encoding bits
|
||||
ref string // the manual entry the encoding is transcribed from
|
||||
}
|
||||
|
||||
// extDocsKey addresses one mnemonic's renderings for one architecture.
|
||||
type extDocsKey struct {
|
||||
a arch.Arch
|
||||
name string
|
||||
}
|
||||
|
||||
// extDocsCache memoises the per-form renderings: the probe walks the layer's
|
||||
// encoder once per mnemonic and architecture, and every later completion or
|
||||
// hover reuses the answer.
|
||||
var extDocsCache sync.Map // map[extDocsKey][]extFormDoc
|
||||
|
||||
// The probe candidates. They cover every operand kind the layer defines
|
||||
// today; a kind arriving later joins through its case in extBuild, and an
|
||||
// unknown kind degrades to a bare label rather than a wrong spelling.
|
||||
var extProbeArrangements = []arch.ExtArrangement{
|
||||
arch.ExtArrB, arch.ExtArrH, arch.ExtArrS, arch.ExtArrD, arch.ExtArrQ, arch.ExtArrNone,
|
||||
}
|
||||
|
||||
var extProbeQualifiers = []arch.ExtQualifier{
|
||||
arch.ExtQualMerging, arch.ExtQualZeroing, arch.ExtQualNone,
|
||||
}
|
||||
|
||||
var extProbeImmediates = []int64{1, 0, 255, -1}
|
||||
|
||||
// extForms returns the renderings of one mnemonic's registered forms on a,
|
||||
// computing and memoising them on first use. It returns nil when the layer
|
||||
// registers nothing for the mnemonic.
|
||||
func extForms(a arch.Arch, mnemonic string) []extFormDoc {
|
||||
key := extDocsKey{a: a, name: strings.ToUpper(mnemonic)}
|
||||
if v, ok := extDocsCache.Load(key); ok {
|
||||
docs, _ := v.([]extFormDoc)
|
||||
return docs
|
||||
}
|
||||
var docs []extFormDoc
|
||||
if cands, ok := asm.LookupExtension(a, mnemonic); ok {
|
||||
docs = make([]extFormDoc, 0, len(cands))
|
||||
for _, in := range cands {
|
||||
docs = append(docs, extFormDoc{
|
||||
summary: in.Summary,
|
||||
form: in.Form.String(),
|
||||
shape: extFormShape(in),
|
||||
feature: string(in.Feature),
|
||||
word: in.Word,
|
||||
ref: in.Ref,
|
||||
})
|
||||
}
|
||||
}
|
||||
extDocsCache.Store(key, docs)
|
||||
return docs
|
||||
}
|
||||
|
||||
// extBuild builds one candidate operand list for the form: every vector
|
||||
// position at reg with the arrangement arr, every predicate position at reg
|
||||
// with the qualifier qual, every immediate at imm.
|
||||
func extBuild(in arch.ExtInstr, reg int, arr arch.ExtArrangement, qual arch.ExtQualifier, imm int64) []arch.ExtOperand {
|
||||
kinds := in.Form.Kinds()
|
||||
ops := make([]arch.ExtOperand, len(kinds))
|
||||
for i, k := range kinds {
|
||||
switch k {
|
||||
case arch.ExtZReg:
|
||||
ops[i] = arch.ExtVector(reg, arr)
|
||||
case arch.ExtPReg:
|
||||
ops[i] = arch.ExtPredicate(reg, qual)
|
||||
default: // arch.ExtImm and anything the probe does not model
|
||||
ops[i] = arch.ExtImmediate(imm)
|
||||
}
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
// extEncodes reports whether the form's encoder accepts the operand list.
|
||||
func extEncodes(in arch.ExtInstr, ops []arch.ExtOperand) bool {
|
||||
_, err := in.Encode(ops)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// extAnchor finds one operand list the form encodes, to anchor the probes on;
|
||||
// ok is false when no probe combination encodes, and the shape then renders
|
||||
// from the operand kinds alone.
|
||||
func extAnchor(in arch.ExtInstr) (base []arch.ExtOperand, arr arch.ExtArrangement, qual arch.ExtQualifier, imm int64, ok bool) {
|
||||
for _, a := range extProbeArrangements {
|
||||
for _, q := range extProbeQualifiers {
|
||||
for _, v := range extProbeImmediates {
|
||||
ops := extBuild(in, 0, a, q, v)
|
||||
if extEncodes(in, ops) {
|
||||
return ops, a, q, v, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, arch.ExtArrNone, arch.ExtQualNone, 0, false
|
||||
}
|
||||
|
||||
// extAcceptedArrangements returns the arrangements the form's vector
|
||||
// positions accept, probed together: the encoder requires the vector
|
||||
// positions of a form to agree, so one position alone cannot carry the probe.
|
||||
func extAcceptedArrangements(in arch.ExtInstr, qual arch.ExtQualifier, imm int64) []arch.ExtArrangement {
|
||||
var out []arch.ExtArrangement
|
||||
for _, a := range extProbeArrangements {
|
||||
if extEncodes(in, extBuild(in, 0, a, qual, imm)) {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// extAcceptedQualifiers returns the qualifiers the form's predicate
|
||||
// positions accept, probed together for the same reason.
|
||||
func extAcceptedQualifiers(in arch.ExtInstr, arr arch.ExtArrangement, imm int64) []arch.ExtQualifier {
|
||||
var out []arch.ExtQualifier
|
||||
for _, q := range extProbeQualifiers {
|
||||
if extEncodes(in, extBuild(in, 0, arr, q, imm)) {
|
||||
out = append(out, q)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// extRegRange walks the register numbers one operand position accepts and
|
||||
// returns a "Z0-Z31" style span when the accepted set is one contiguous run
|
||||
// from its low end. ok is false when no register encodes there or the set
|
||||
// is not contiguous, and the caller falls back to a bare label.
|
||||
func extRegRange(in arch.ExtInstr, base []arch.ExtOperand, pos int, letter string) (span string, ok bool) {
|
||||
const limit = 32 // both register files the layer defines sit inside 32
|
||||
lo, hi, n := -1, -1, 0
|
||||
for r := range limit {
|
||||
ops := slices.Clone(base)
|
||||
ops[pos].Reg = r
|
||||
if !extEncodes(in, ops) {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
if lo < 0 {
|
||||
lo = r
|
||||
}
|
||||
hi = r
|
||||
}
|
||||
if n == 0 || hi-lo+1 != n {
|
||||
return "", false
|
||||
}
|
||||
return fmt.Sprintf("%s%d-%s%d", letter, lo, letter, hi), true
|
||||
}
|
||||
|
||||
// extFormShape measures the operand spellings the form accepts: the register
|
||||
// spans, the arrangement suffixes and the predicate qualifiers its own
|
||||
// encoder takes, rendered the way the source writes them. Where a probe
|
||||
// finds nothing to anchor on, the shape degrades to the operand kinds and
|
||||
// states nothing the encoder has not proven.
|
||||
func extFormShape(in arch.ExtInstr) string {
|
||||
kinds := in.Form.Kinds()
|
||||
if len(kinds) == 0 {
|
||||
return ""
|
||||
}
|
||||
base, arr, qual, imm, anchored := extAnchor(in)
|
||||
|
||||
// The arrangement suffixes and the qualifiers, rendered in enum order.
|
||||
suffix := ""
|
||||
quals := []arch.ExtQualifier{qual}
|
||||
if anchored {
|
||||
var spelled []string
|
||||
for _, a := range extAcceptedArrangements(in, qual, imm) {
|
||||
if a != arch.ExtArrNone {
|
||||
spelled = append(spelled, a.String())
|
||||
}
|
||||
}
|
||||
if len(spelled) > 0 {
|
||||
suffix = strings.Join(spelled, "/")
|
||||
}
|
||||
quals = extAcceptedQualifiers(in, arr, imm)
|
||||
}
|
||||
|
||||
var parts []string
|
||||
for i, k := range kinds {
|
||||
switch k {
|
||||
case arch.ExtZReg:
|
||||
label := "Z<n>"
|
||||
if anchored {
|
||||
if span, ok := extRegRange(in, base, i, "Z"); ok {
|
||||
label = span
|
||||
}
|
||||
}
|
||||
parts = append(parts, label+suffix)
|
||||
case arch.ExtPReg:
|
||||
label := "P<n>"
|
||||
if anchored {
|
||||
if span, ok := extRegRange(in, base, i, "P"); ok {
|
||||
label = span
|
||||
}
|
||||
}
|
||||
var spelled []string
|
||||
for _, q := range quals {
|
||||
if q != arch.ExtQualNone {
|
||||
spelled = append(spelled, q.String())
|
||||
}
|
||||
}
|
||||
if len(spelled) > 0 {
|
||||
label += strings.Join(spelled, " or ")
|
||||
}
|
||||
parts = append(parts, label)
|
||||
default:
|
||||
parts = append(parts, "$imm")
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// extFeatureList names the architecture features the forms belong to, in
|
||||
// first-occurrence order, "" when the metadata carries none.
|
||||
func extFeatureList(docs []extFormDoc) string {
|
||||
var out []string
|
||||
for _, d := range docs {
|
||||
if d.feature != "" && !slices.Contains(out, d.feature) {
|
||||
out = append(out, d.feature)
|
||||
}
|
||||
}
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// renderExtensionHover renders the hover documentation of a registered
|
||||
// extended mnemonic: the fact that it is an extension above the toolchain,
|
||||
// then one entry per registered form. A form with no summary in the
|
||||
// metadata states the encoding facts alone: the shape label, the feature,
|
||||
// the fixed encoding bits and the manual reference, and nothing more.
|
||||
func renderExtensionHover(mnemonic string, docs []extFormDoc) string {
|
||||
if len(docs) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("**" + mnemonic + "**: extension above the Go toolchain")
|
||||
if f := extFeatureList(docs); f != "" {
|
||||
b.WriteString(" (" + f + ")")
|
||||
}
|
||||
for _, d := range docs {
|
||||
b.WriteString("\n\n- ")
|
||||
// The summary leads where the metadata carries one; without it the
|
||||
// form's own label stands in, and the bullet states the encoding
|
||||
// facts and nothing more.
|
||||
lead := d.summary
|
||||
if lead == "" {
|
||||
lead = d.form
|
||||
}
|
||||
if lead != "" {
|
||||
b.WriteString(lead + ": ")
|
||||
}
|
||||
b.WriteString(d.shape)
|
||||
var clauses []string
|
||||
if d.summary != "" && d.form != "" {
|
||||
clauses = append(clauses, d.form)
|
||||
}
|
||||
if d.feature != "" {
|
||||
clauses = append(clauses, d.feature)
|
||||
}
|
||||
if d.word != 0 {
|
||||
clauses = append(clauses, fmt.Sprintf("encoding 0x%08x", d.word))
|
||||
}
|
||||
if len(clauses) > 0 {
|
||||
b.WriteString(" (" + strings.Join(clauses, ", ") + ")")
|
||||
}
|
||||
if d.ref != "" {
|
||||
b.WriteString("\n " + d.ref)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// extensionHover returns the hover documentation of a registered extended
|
||||
// mnemonic, "" when the layer registers nothing for it.
|
||||
func extensionHover(a arch.Arch, word string) string {
|
||||
return renderExtensionHover(strings.ToUpper(word), extForms(a, word))
|
||||
}
|
||||
|
||||
// applyExtensionCompletion merges the registered extended mnemonics into the
|
||||
// completion list, so they are offered exactly where the toolchain
|
||||
// mnemonics are: a mnemonic the base table also knows enriches its existing
|
||||
// item, a registry-only one gains its own. The detail carries the operand
|
||||
// shapes the layer's encoder accepts.
|
||||
func applyExtensionCompletion(items []CompletionItem, a arch.Arch) []CompletionItem {
|
||||
byLabel := make(map[string]int, len(items))
|
||||
for i, it := range items {
|
||||
if _, ok := byLabel[it.Label]; !ok {
|
||||
byLabel[it.Label] = i
|
||||
}
|
||||
}
|
||||
for _, name := range asm.ExtensionNames(a) {
|
||||
docs := extForms(a, name)
|
||||
if len(docs) == 0 {
|
||||
continue
|
||||
}
|
||||
shapes := make([]string, 0, len(docs))
|
||||
for _, d := range docs {
|
||||
shapes = append(shapes, d.shape)
|
||||
}
|
||||
hover := extensionHover(a, name)
|
||||
if i, ok := byLabel[name]; ok {
|
||||
items[i].Detail += "; extension: " + strings.Join(shapes, "; ")
|
||||
if items[i].Documentation == "" {
|
||||
items[i].Documentation = hover
|
||||
} else {
|
||||
items[i].Documentation += "\n\n" + hover
|
||||
}
|
||||
continue
|
||||
}
|
||||
byLabel[name] = len(items)
|
||||
items = append(items, CompletionItem{
|
||||
Label: name,
|
||||
Kind: ciFunction,
|
||||
Detail: "extension: " + strings.Join(shapes, "; "),
|
||||
Documentation: hover,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,9 @@ func (s *Server) completion(p completionParams) []CompletionItem {
|
||||
}
|
||||
items = append(items, CompletionItem{Label: r.Name, Kind: kind, Detail: r.Desc})
|
||||
}
|
||||
// The extended-instruction layer, beside the toolchain entries: the
|
||||
// registered mnemonics with the operand shapes the registry accepts.
|
||||
items = applyExtensionCompletion(items, a.Arch)
|
||||
// Local labels defined in the document.
|
||||
if f, _ := parser.Parse("", s.docs[p.TextDocument.URI]); f != nil {
|
||||
for _, name := range labelNames(f) {
|
||||
@@ -134,6 +137,7 @@ func (s *Server) hover(p hoverParams) *Hover {
|
||||
a := arch.ForArch(arch.FromFilename(uriPath(p.TextDocument.URI)))
|
||||
|
||||
var md string
|
||||
ext := extensionHover(a.Arch, word)
|
||||
// Directives first: several of their names (TEXT, PCALIGN, FUNCDATA,
|
||||
// the BYTE family) also sit in the generated instruction tables with
|
||||
// empty summaries, and the directive documentation is the answer a
|
||||
@@ -142,6 +146,13 @@ func (s *Server) hover(p hoverParams) *Hover {
|
||||
md = doc
|
||||
} else if in, ok := a.Lookup(word); ok {
|
||||
md = "**" + in.Name + "**: " + in.Summary
|
||||
// A mnemonic the extension layer registers documents its registered
|
||||
// forms beside the table entry.
|
||||
if ext != "" {
|
||||
md += "\n\n" + ext
|
||||
}
|
||||
} else if ext != "" {
|
||||
md = ext
|
||||
} else if r, ok := a.Register(word); ok {
|
||||
md = "**" + r.Name + "**: " + r.Class.String() + " register. " + r.Desc
|
||||
} else if desc, ok := arch.PseudoRegDesc(word); ok {
|
||||
|
||||
Reference in new issue
Block a user