feat(lsp): leave the extension verdicts to the registry

Diagnostics no longer repeat the generated table's ignorance of a
registered mnemonic: unknown-instruction and unencodable-instruction
against a statement the registry encodes are filtered from the server's
own presentation, driven by asm.LookupExtension directly.  The filter is
a no-op once lint learns the registry, so the two compose unchanged.

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 00:06:05 +02:00
1 parent dadeda144a
commit 2d803e38d8
3 files changed
+116

No files matched your search

+38
View File
@@ -21,6 +21,8 @@ import (
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
"sourcedock.dev/petrbalvin/gasm-sdk/ast"
"sourcedock.dev/petrbalvin/gasm-sdk/lint"
)
// extFormDoc is the rendering of one registered form: everything hover and
@@ -346,3 +348,39 @@ func applyExtensionCompletion(items []CompletionItem, a arch.Arch) []CompletionI
}
return items
}
// extOwnedCode reports whether a lint code carries a verdict the extension
// layer owns: the two that accuse a mnemonic of not existing or of not being
// encodable. The operand-count rule stays with the generated table, whose
// bounds the relaxed architectures never fire against a registered form.
func extOwnedCode(code string) bool {
return code == lint.CodeUnknownInstr || code == lint.CodeUnencodable
}
// extensionMnemonicSites collects the source positions of the statements
// whose mnemonic the architecture's extension layer registers. The
// diagnostics pass consults it to leave the layer's verdicts to the layer:
// a registry mnemonic is neither unknown nor unencodable, the registry
// encodes it.
func extensionMnemonicSites(f *ast.File, a arch.Arch) map[[2]int]bool {
out := make(map[[2]int]bool)
if f == nil {
return out
}
for _, d := range f.Decls {
t, ok := d.(*ast.Text)
if !ok {
continue
}
for _, s := range t.Body {
in, ok := s.(*ast.Instr)
if !ok {
continue
}
if _, ok := asm.LookupExtension(a, in.Mnemonic.Text); ok {
out[[2]int{in.Mnemonic.Pos.Line, in.Mnemonic.Pos.Column}] = true
}
}
}
return out
}
+68
View File
@@ -283,3 +283,71 @@ func TestExtensionHoverWithoutProse(t *testing.T) {
}
}
}
// 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)
}
}
}
+10
View File
@@ -1127,6 +1127,13 @@ func (s *Server) diagnosticsFor(uri string) []Diagnostic {
cfg := lint.Config{Arch: arch.FromFilename(uriPath(uri))}
diags := lint.File(f, cfg)
// The extension layer owns the verdict on its own mnemonics: the registry
// encodes them, so a report of unknown or unencodable against a
// registered mnemonic is the generated table's ignorance, not the file's
// defect. The filter consults the registry directly and is a no-op once
// lint learns the registry itself, so the two compose unchanged.
owned := extensionMnemonicSites(f, cfg.Arch)
out := make([]Diagnostic, 0, len(diags)+len(errs))
for _, e := range errs {
pos := token.Position{Line: 1, Column: 1}
@@ -1142,6 +1149,9 @@ func (s *Server) diagnosticsFor(uri string) []Diagnostic {
})
}
for _, d := range diags {
if extOwnedCode(d.Code) && owned[[2]int{d.Pos.Line, d.Pos.Column}] {
continue
}
out = append(out, Diagnostic{
Range: clientRange(text, toRange(d.Pos.Line, d.Pos.Column, d.End)),
Severity: lintSeverity(d.Severity),