diff --git a/lsp/extensions.go b/lsp/extensions.go index d72c3db..f06a3a0 100644 --- a/lsp/extensions.go +++ b/lsp/extensions.go @@ -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 +} diff --git a/lsp/extensions_test.go b/lsp/extensions_test.go index cb1007a..66e1c7b 100644 --- a/lsp/extensions_test.go +++ b/lsp/extensions_test.go @@ -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) + } + } +} diff --git a/lsp/handlers.go b/lsp/handlers.go index 9017f88..66a7c4f 100644 --- a/lsp/handlers.go +++ b/lsp/handlers.go @@ -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),