gopls: add assembly DocumentHighlight, fix label scoping
This adds DocumentHighlight for Go assembly files, and fixes a bug where
label definitions were not properly scoped to their enclosing TEXT function.
- Dispatch file.Asm in DocumentHighlight to goasm.Highlight.
- For a symbol or label under the cursor, same-name occurrences are
highlighted: definitions (TEXT, GLOBL, labels) as Write, references as Read.
- For a register, all occurrences within the enclosing TEXT function are
highlighted, approximating its def/use chain.
- Scope label definitions to the enclosing TEXT function, so Definition on
a label reference no longer jumps to a same-named label in a different
function.
For golang/go#71754
Change-Id: I450d689f4cfc7052736dd5c5957375d2ae16484b
GitHub-Last-Rev: 1e43c451da62f148f49408f4aec3c8a0056abc11
GitHub-Pull-Request: golang/tools#661
Reviewed-on: https://go-review.googlesource.com/c/tools/+/804760
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alex Putman <aputman@golang.org>
diff --git a/gopls/doc/features/assembly.md b/gopls/doc/features/assembly.md
index 7302999..f6074e8 100644
--- a/gopls/doc/features/assembly.md
+++ b/gopls/doc/features/assembly.md
@@ -11,21 +11,39 @@
directory containing at least one `*.go` file, then the `.s` file is
Go assembly, and its appropriate language server is gopls.
-Only Definition (`textDocument/definition`) requests are currently
-supported. For example, a Definition request on the `sigpanic`
-symbol in this file in GOROOT/src/runtime/asm.s:
+The following requests are currently supported:
-```asm
- JMP ·sigpanic<ABIInternal>(SB)
-```
+- Definition (`textDocument/definition`): on a reference to a symbol,
+ returns the location of its declaration. For example, a Definition
+ request on the `sigpanic` symbol in this file in
+ GOROOT/src/runtime/asm.s:
-returns the location of the function declaration in
-GOROOT/src/runtime/signal_unix.go:
+ ```asm
+ JMP ·sigpanic<ABIInternal>(SB)
+ ```
-```go
-//go:linkname sigpanic
-func sigpanic() {
-```
+ returns the location of the function declaration in
+ GOROOT/src/runtime/signal_unix.go:
+
+ ```go
+ //go:linkname sigpanic
+ func sigpanic() {
+ ```
+
+- References (`textDocument/references`): finds all references to the
+ symbol under the cursor, in both Go and assembly files within the
+ same package.
+
+- Hover (`textDocument/hover`): reports the signature and doc comment
+ of the symbol's Go declaration.
+
+- DocumentHighlight (`textDocument/documentHighlight`): highlights all
+ occurrences of the symbol, control label, or machine register under
+ the cursor. Labels and registers are scoped to the enclosing TEXT
+ function, and occurrences are classified as reads or writes.
+ Register highlighting requires the file name to carry a GOARCH
+ suffix (e.g. `foo_amd64.s`) and currently supports x86 (amd64, 386)
+ and arm64.
See also issue https://go.dev/issue/71754, which tracks the development of LSP
-features in Go assembly files.
\ No newline at end of file
+features in Go assembly files.
diff --git a/gopls/doc/release/v0.24.0.md b/gopls/doc/release/v0.24.0.md
index 9951ac8..f4d0db3 100644
--- a/gopls/doc/release/v0.24.0.md
+++ b/gopls/doc/release/v0.24.0.md
@@ -41,6 +41,12 @@
over a symbol reports the signature and doc comment of its Go
declaration.
+Gopls now supports the `textDocument/documentHighlight` request in Go
+assembly files: all occurrences of the symbol, label, or machine
+register under the cursor are highlighted within the enclosing TEXT
+function, with definitions classified as writes and references as
+reads.
+
## Analysis features
<!-- TODO Gopls is now using staticcheck [v0.8.0-rc1](https://github.com/dominikh/go-tools/releases/tag/2026.2rc1). -->
diff --git a/gopls/internal/goasm/highlight.go b/gopls/internal/goasm/highlight.go
new file mode 100644
index 0000000..ad0b7c5
--- /dev/null
+++ b/gopls/internal/goasm/highlight.go
@@ -0,0 +1,483 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package goasm
+
+import (
+ "bytes"
+ "context"
+ "strings"
+
+ "golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/protocol"
+ "golang.org/x/tools/gopls/internal/util/asm"
+ "golang.org/x/tools/internal/event"
+)
+
+// Highlight handles the textDocument/documentHighlight request for Go
+// assembly files.
+//
+// If the cursor is on a symbol identifier, all occurrences of the same
+// name in the file are highlighted: definitions (TEXT, GLOBL) as Write,
+// references as Read. Control labels are function-scoped, so for a label
+// only occurrences within the enclosing TEXT function are highlighted.
+//
+// If the cursor is on a machine register, all occurrences of that
+// register within the enclosing TEXT function are highlighted,
+// approximating its def/use chain: occurrences classified as
+// definitions are Write, uses are Read. Register highlighting requires
+// a GOARCH file name suffix (e.g. *_amd64.s, *_arm64.s).
+func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.DocumentHighlight, error) {
+ ctx, done := event.Start(ctx, "goasm.Highlight")
+ defer done()
+
+ content, err := fh.Content()
+ if err != nil {
+ return nil, err
+ }
+
+ asmFile := asm.Parse(fh.URI(), content)
+
+ start, end, err := asmFile.Mapper.RangeOffsets(rng)
+ if err != nil {
+ return nil, err
+ }
+
+ // Identifier (symbol or label) under the cursor?
+ if found := asmFile.IdentAt(start, end); found != nil {
+ return highlightIdents(asmFile, found)
+ }
+
+ // Register under the cursor?
+ return highlightRegister(asmFile, start)
+}
+
+// highlightIdents highlights every identifier with the same name as
+// found. Definitions are Write; references are Read. If the name denotes
+// a control label (it has a label definition in the file), only
+// occurrences within the enclosing TEXT function are highlighted, since
+// labels are function-scoped and the same label name may be reused in
+// different functions.
+func highlightIdents(file *asm.File, found *asm.Ident) ([]protocol.DocumentHighlight, error) {
+ // Heuristic: if the name is used as a label anywhere in the file,
+ // assume every occurrence is a label. A label and a global symbol
+ // sharing a name is implausible in practice, so per-occurrence
+ // disambiguation is not worth the cost.
+ lo, hi := 0, len(file.Mapper.Content)
+ for _, id := range file.Idents {
+ if id.Kind == asm.Label && id.Name == found.Name {
+ lo, hi = file.FunctionRange(found.Offset)
+ break
+ }
+ }
+
+ var highlights []protocol.DocumentHighlight
+ for _, id := range file.Idents {
+ if id.Name != found.Name || !(lo <= id.Offset && id.Offset < hi) {
+ continue
+ }
+ idRange, err := file.IdentRange(id)
+ if err != nil {
+ return nil, err
+ }
+ kind := protocol.Read
+ if id.Kind == asm.Text || id.Kind == asm.Global || id.Kind == asm.Label {
+ kind = protocol.Write
+ }
+ highlights = append(highlights, protocol.DocumentHighlight{
+ Range: idRange,
+ Kind: kind,
+ })
+ }
+ return highlights, nil
+}
+
+// highlightRegister highlights all occurrences of the register under the
+// cursor within the enclosing TEXT function.
+func highlightRegister(file *asm.File, offset int) ([]protocol.DocumentHighlight, error) {
+ content := file.Mapper.Content
+ arch := fileArch(file.Mapper.URI.Base())
+ if arch == "" {
+ return nil, nil
+ }
+ word, wordStart := wordAt(content, offset)
+ if word == "" || !isRegisterWord(word) || inComment(content, offset) {
+ return nil, nil
+ }
+ // The first word on a line is the mnemonic, not a register.
+ if isLineStart(content, wordStart) {
+ return nil, nil
+ }
+
+ funcStart, funcEnd := file.FunctionRange(offset)
+ var highlights []protocol.DocumentHighlight
+ pos := funcStart
+ for pos < funcEnd {
+ i := bytes.Index(content[pos:funcEnd], []byte(word))
+ if i < 0 {
+ break
+ }
+ absOff := pos + i
+ pos = absOff + len(word)
+ // Skip occurrences inside comments, within a larger word
+ // (e.g. "AX" in "MAX"), or at the start of a line (mnemonic).
+ if inComment(content, absOff) ||
+ !isWordBoundary(content, absOff, absOff+len(word)) ||
+ isLineStart(content, absOff) {
+ continue
+ }
+ rng, err := file.Mapper.OffsetRange(absOff, absOff+len(word))
+ if err != nil {
+ return nil, err
+ }
+ highlights = append(highlights, protocol.DocumentHighlight{
+ Range: rng,
+ Kind: registerKind(content, absOff, arch),
+ })
+ }
+ return highlights, nil
+}
+
+// fileArch returns the GOARCH suffix of an assembly file's base name. It
+// returns "" for unsupported architectures and for file names without a
+// GOARCH suffix (which select their architecture using build constraints);
+// register highlighting is not yet supported for those files.
+func fileArch(base string) string {
+ name, ok := strings.CutSuffix(base, ".s")
+ if !ok {
+ return ""
+ }
+ if i := strings.LastIndexByte(name, '_'); i >= 0 {
+ arch := name[i+1:]
+ switch arch {
+ case "386", "amd64", "arm64":
+ return arch
+ }
+ }
+ return ""
+}
+
+// registerKind classifies the register occurrence at offset (a byte
+// offset within content) as Read or Write, where arch is the file's
+// GOARCH ("386", "amd64", or "arm64"). It follows the Plan 9
+// assembly convention that the destination operand is the last
+// operand: a register in the last operand is a definition (Write), and a
+// register in any earlier operand is a use (Read), with three exceptions:
+//
+// - A register inside parentheses is part of a memory address operand
+// (as in (AX) or 8(AX)(BX*4)) and is always Read, even in the
+// destination operand of a store. A parenthesized register pair,
+// such as the (R4, R5) of arm64 LDP/STP, is not a memory address
+// but a single operand in its own right, and is classified by its
+// position like any other operand; see enclosingGroup.
+// - Comparison and test instructions (x86: CMP, TEST, COMIS*, UCOMIS*,
+// BT*; arm64: CMP, CMN, TST) have no destination, so all their
+// operands are Read.
+// - Single-operand instructions write their operand only in a few
+// cases, all x86-specific: POP stores into it, INC/DEC/NEG/NOT/BSWAP
+// update it in place, and SETcc sets it to 0 or 1. For all others
+// (PUSH, MUL/DIV, ...) the operand is a source and is Read.
+//
+// Implicit register operands are not modeled — for example MUL/DIV
+// clobber DX:AX, CALL may clobber CX, and the post-increment forms such
+// as arm64 LDP.P update their base register — so occurrences in such
+// instructions may be misclassified.
+//
+// TODO(golang/go#71754): model implicit operands.
+//
+// TODO(golang/go#71754): consider linking instruction mnemonics to
+// their CPU documentation (e.g. https://www.felixcloutier.com/x86/movzx).
+func registerKind(content []byte, offset int, arch string) protocol.DocumentHighlightKind {
+ // Find the line containing offset.
+ lineStart := offset
+ for lineStart > 0 && content[lineStart-1] != '\n' {
+ lineStart--
+ }
+ lineEnd := offset
+ for lineEnd < len(content) && content[lineEnd] != '\n' {
+ lineEnd++
+ }
+ line := content[lineStart:lineEnd]
+ // Strip a trailing comment so its commas and parentheses are not
+ // mistaken for operand syntax.
+ if i := bytes.Index(line, []byte("//")); i >= 0 {
+ line = line[:i]
+ }
+
+ // A register inside parentheses is a memory address: always Read,
+ // unless the parentheses enclose a register pair, in which case the
+ // pair is classified by its position as a whole.
+ rel := offset - lineStart
+ if open, isPair := enclosingGroup(line, rel); open >= 0 {
+ if !isPair {
+ return protocol.Read
+ }
+ rel = open // classify the pair by the position of its '('
+ }
+
+ // Identify the mnemonic: the first non-space token on the line.
+ i := 0
+ for i < len(line) && (line[i] == ' ' || line[i] == '\t') {
+ i++
+ }
+ mnStart := i
+ for i < len(line) && line[i] != ' ' && line[i] != '\t' && line[i] != ',' {
+ i++
+ }
+ mnemonic := string(line[mnStart:i])
+
+ if isCompareMnemonic(arch, mnemonic) {
+ return protocol.Read
+ }
+
+ // The operand list starts after the mnemonic. Count top-level commas to
+ // determine which operand the occurrence is in; the last operand is the
+ // destination.
+ operandArea := line[i:]
+ relMatch := min(max(rel-i, 0), len(operandArea))
+ commaBefore := topLevelCommas(operandArea[:relMatch])
+ totalCommas := topLevelCommas(operandArea)
+ if totalCommas == 0 {
+ // Single-operand instruction. The write cases below are
+ // all x86-specific; on other architectures the operand is
+ // always a source.
+ if arch != "386" && arch != "amd64" {
+ return protocol.Read
+ }
+ m := strings.ToUpper(mnemonic)
+ if strings.HasPrefix(m, "SET") { // SETcc; also avoids trimSizeSuffix("SETEQ") = "SETE"
+ return protocol.Write
+ }
+ switch trimSizeSuffix(m) {
+ case "POP", "INC", "DEC", "NEG", "NOT", "BSWAP":
+ return protocol.Write
+ }
+ return protocol.Read
+ }
+ if commaBefore >= totalCommas {
+ return protocol.Write
+ }
+ return protocol.Read
+}
+
+// enclosingGroup reports whether index rel of line is inside a
+// parenthesized group, returning the index of the group's '(', or -1 if
+// there is none, and whether the group is a register pair such as the
+// (R4, R5) of arm64 LDP/STP.
+//
+// A group is taken to be a register pair if it contains a comma at its
+// own nesting level and its '(' does not immediately follow an
+// identifier. Memory address operands contain no comma at that level (as
+// in (AX) or 8(AX)(BX*4)), and in a macro invocation the '(' is glued to
+// the macro name (as in QR(V0, V4, V8, V12)).
+func enclosingGroup(line []byte, rel int) (int, bool) {
+ // Find the innermost unclosed '(' before rel.
+ open := -1
+ for depth, j := 0, rel-1; j >= 0 && open < 0; j-- {
+ switch line[j] {
+ case ')':
+ depth++
+ case '(':
+ if depth == 0 {
+ open = j
+ } else {
+ depth--
+ }
+ }
+ }
+ if open < 0 {
+ return -1, false
+ }
+ if open > 0 && isWordByte(line[open-1]) {
+ return open, false // macro invocation
+ }
+ depth := 0
+ for j := open; j < len(line); j++ {
+ switch line[j] {
+ case '(':
+ depth++
+ case ')':
+ if depth--; depth == 0 {
+ return open, false // closed with no comma of its own
+ }
+ case ',':
+ if depth == 1 {
+ return open, true
+ }
+ }
+ }
+ return open, false // unterminated
+}
+
+// topLevelCommas counts the commas of s that are not nested within
+// parentheses, that is, the operand separators of an instruction.
+func topLevelCommas(s []byte) int {
+ n, depth := 0, 0
+ for _, b := range s {
+ switch b {
+ case '(':
+ depth++
+ case ')':
+ if depth > 0 {
+ depth--
+ }
+ case ',':
+ if depth == 0 {
+ n++
+ }
+ }
+ }
+ return n
+}
+
+// isCompareMnemonic reports whether mnemonic is a comparison or test
+// instruction on arch, whose operands are all reads (no destination).
+// On arm64 these are CMP/CMN/TST (the prefixes cover the W width
+// variants). On x86, BT (bit test) only reads its destination operand
+// to set flags, so it is a comparison; BTS/BTR/BTC are read-modify-
+// write and are not — their destination is classified as Write by the
+// default rule. CMPXCHG/CMPXCHG8B/CMPXCHG16B are read-modify-write and
+// are excluded from CMP prefix matching for the same reason.
+//
+// The x86 predicate comparisons CMPPD/CMPPS/CMPSD/CMPSS do have a
+// destination, and it is their second operand rather than their last
+// (as in CMPPD X1, X2, $7), so both this function and the default rule
+// misclassify it as Read. Their mnemonics are also ambiguous: CMPSD and
+// CMPSS name both these instructions and the operand-free string
+// comparisons.
+func isCompareMnemonic(arch, mnemonic string) bool {
+ m := strings.ToUpper(mnemonic)
+ switch arch {
+ case "arm64":
+ return strings.HasPrefix(m, "CMP") ||
+ strings.HasPrefix(m, "CMN") ||
+ strings.HasPrefix(m, "TST")
+ case "386", "amd64":
+ // Continue below with the x86 cases.
+ default:
+ return false
+ }
+ if trimSizeSuffix(m) == "BT" {
+ return true
+ }
+ // CMPXCHG has CMP prefix but is read-modify-write, not a compare.
+ if strings.HasPrefix(m, "CMPXCHG") {
+ return false
+ }
+ switch {
+ case strings.HasPrefix(m, "CMP"),
+ strings.HasPrefix(m, "TEST"),
+ strings.HasPrefix(m, "COM"),
+ strings.HasPrefix(m, "UCOM"):
+ return true
+ }
+ return false
+}
+
+// trimSizeSuffix strips a single trailing size suffix (B/W/L/Q) from an
+// instruction mnemonic, e.g. "CMPQ" -> "CMP", "BTB" -> "BT", "BTS" -> "BTS".
+func trimSizeSuffix(m string) string {
+ if len(m) > 0 {
+ switch m[len(m)-1] {
+ case 'B', 'W', 'L', 'Q':
+ return m[:len(m)-1]
+ }
+ }
+ return m
+}
+
+// inComment reports whether offset falls within a // line comment.
+// Like [asm.Parse], it does not recognize /* */ block comments.
+func inComment(content []byte, offset int) bool {
+ lineStart := offset
+ for lineStart > 0 && content[lineStart-1] != '\n' {
+ lineStart--
+ }
+ return bytes.Contains(content[lineStart:offset], []byte("//"))
+}
+
+// isWordBoundary reports whether content[start:end], which contains
+// only word bytes and is nonempty, is a whole word: the bytes
+// immediately before start and after end are not word bytes.
+func isWordBoundary(content []byte, start, end int) bool {
+ if start > 0 && isWordByte(content[start-1]) {
+ return false
+ }
+ if end < len(content) && isWordByte(content[end]) {
+ return false
+ }
+ return true
+}
+
+// wordAt returns the maximal run of ASCII word bytes ([A-Za-z0-9])
+// containing pos, together with its start offset. If the run is empty
+// (pos is on a non-word byte whose left neighbor is also a non-word
+// byte), wordAt returns ("", pos).
+//
+// Precondition: 0 <= pos <= len(content).
+func wordAt(content []byte, pos int) (string, int) {
+ start := pos
+ for start > 0 && isWordByte(content[start-1]) {
+ start--
+ }
+ end := pos
+ for end < len(content) && isWordByte(content[end]) {
+ end++
+ }
+ return string(content[start:end]), start
+}
+
+func isWordByte(b byte) bool {
+ return (b >= 'A' && b <= 'Z') ||
+ (b >= 'a' && b <= 'z') ||
+ (b >= '0' && b <= '9') ||
+ b == '_'
+}
+
+// isRegisterWord reports whether word looks like a machine register name:
+// 2-3 ASCII uppercase letters/digits with at least one letter. (Requiring
+// a letter excludes numeric immediates such as "123".) The pseudo-
+// registers SB, SP, FP, and PC are excluded because they appear in almost
+// every operand, so highlighting them would be noise rather than signal.
+func isRegisterWord(word string) bool {
+ if len(word) < 2 || len(word) > 3 {
+ return false
+ }
+ switch word {
+ case "SB", "SP", "FP", "PC":
+ return false
+ }
+ hasLetter := false
+ for i := 0; i < len(word); i++ {
+ c := word[i]
+ switch {
+ case 'A' <= c && c <= 'Z':
+ hasLetter = true
+ case !('0' <= c && c <= '9'):
+ return false
+ }
+ }
+ return hasLetter
+}
+
+// isLineStart reports whether offset begins a line, i.e. it is preceded
+// only by whitespace or the start of the file. Callers use it to reject
+// instruction mnemonics, which assumes each line holds at most one
+// instruction and that it is not preceded by a label; neither holds for
+// "label: RET" or for instructions separated by ';', though no such line
+// appears in GOROOT for the architectures supported by fileArch.
+func isLineStart(content []byte, offset int) bool {
+ for i := offset - 1; i >= 0; i-- {
+ switch content[i] {
+ case '\n':
+ return true
+ case ' ', '\t':
+ continue
+ default:
+ return false
+ }
+ }
+ return true // beginning of file
+}
diff --git a/gopls/internal/goasm/resolve.go b/gopls/internal/goasm/resolve.go
index 9067bf5..fe9fb8e 100644
--- a/gopls/internal/goasm/resolve.go
+++ b/gopls/internal/goasm/resolve.go
@@ -35,7 +35,8 @@
// localDef is the defining identifier in the assembly file for a local
// symbol — a label, a bare TEXT/GLOBL symbol, or a current-package
- // symbol without a Go declaration. It is nil if none was found.
+ // symbol without a Go declaration. It is nil if none was found. For a
+ // label, only the enclosing TEXT function is searched.
localDef *asm.Ident
}
@@ -73,12 +74,7 @@
// Find the identifier under the cursor.
// Use the selection range so that haphazard selections that
// happen to start in an identifier don't produce spurious matches.
- for _, id := range res.file.Idents {
- if id.Offset <= start && end <= id.End() {
- res.found = &id
- break
- }
- }
+ res.found = res.file.IdentAt(start, end)
if res.found == nil {
return res, nil
}
@@ -121,10 +117,23 @@
// symbol, or a package-qualified symbol without a Go declaration — in
// the assembly file.
if res.obj == nil {
+ // Labels are function-scoped: a label definition matches only
+ // within the enclosing TEXT function, so that a jump doesn't land
+ // on a same-named label in another function.
+ lo, hi := res.file.FunctionRange(res.found.Offset)
for _, id := range res.file.Idents {
- if id.Name == res.found.Name &&
- (id.Kind == asm.Text || id.Kind == asm.Global || id.Kind == asm.Label) {
+ if id.Name != res.found.Name {
+ continue
+ }
+ switch id.Kind {
+ case asm.Text, asm.Global:
res.localDef = &id
+ case asm.Label:
+ if lo <= id.Offset && id.Offset < hi {
+ res.localDef = &id
+ }
+ }
+ if res.localDef != nil {
break
}
}
diff --git a/gopls/internal/server/highlight.go b/gopls/internal/server/highlight.go
index 83b2660..b5aa4bc 100644
--- a/gopls/internal/server/highlight.go
+++ b/gopls/internal/server/highlight.go
@@ -8,6 +8,7 @@
"context"
"golang.org/x/tools/gopls/internal/file"
+ "golang.org/x/tools/gopls/internal/goasm"
"golang.org/x/tools/gopls/internal/golang"
"golang.org/x/tools/gopls/internal/label"
"golang.org/x/tools/gopls/internal/protocol"
@@ -26,6 +27,8 @@
defer release()
switch snapshot.FileKind(fh) {
+ case file.Asm:
+ return goasm.Highlight(ctx, snapshot, fh, params.Range)
case file.Tmpl:
return template.Highlight(ctx, snapshot, fh, params.Range)
case file.Go:
diff --git a/gopls/internal/test/marker/testdata/definition/asm.txt b/gopls/internal/test/marker/testdata/definition/asm.txt
index da42b5e..9a45d84 100644
--- a/gopls/internal/test/marker/testdata/definition/asm.txt
+++ b/gopls/internal/test/marker/testdata/definition/asm.txt
@@ -35,3 +35,19 @@
package b
func B() {} //@ loc(bB, "B")
+
+-- a/asm2.s --
+// Labels are function-scoped: the same label name in different TEXT
+// functions denotes different labels.
+
+TEXT ·f1(SB), $0-0
+onlyf1:
+loop: //@ loc(f1loop, "loop")
+ JMP loop //@ def("loop", f1loop)
+ RET
+
+TEXT ·f2(SB), $0-0
+loop: //@ loc(f2loop, "loop")
+ JMP loop //@ def("loop", f2loop)
+ JMP onlyf1 //@ def("onlyf1") // defined only in f1: no definition here
+ RET
diff --git a/gopls/internal/test/marker/testdata/highlight/asm.txt b/gopls/internal/test/marker/testdata/highlight/asm.txt
new file mode 100644
index 0000000..ffb3aca
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/highlight/asm.txt
@@ -0,0 +1,147 @@
+Test of documentHighlight for assembly files.
+
+-- example/highlight_text.s --
+TEXT ·foo(SB), $0-0 //@hiloc(defFoo, "·foo", write)
+ CALL ·foo(SB) //@hiloc(refFoo, "·foo", read)
+//@highlightall(defFoo, refFoo)
+
+-- example/highlight_global.s --
+GLOBL ·bar(SB), $8 //@hiloc(defBar, "·bar", write)
+
+TEXT ·useBar(SB), $0-0
+ MOVQ ·bar(SB), AX //@hiloc(refBar, "·bar", read)
+//@highlightall(defBar, refBar)
+
+-- example/highlight_label.s --
+TEXT ·loopDemo(SB), $0-0
+loop: //@hiloc(defLoop, "loop", write)
+ JMP loop //@hiloc(refLoop, "loop", read)
+//@highlightall(defLoop, refLoop)
+
+-- example/highlight_register_amd64.s --
+TEXT ·useReg(SB), $0-0
+ MOVQ AX, BX //@hiloc(ax1, "AX", read)
+ ADDQ CX, AX //@hiloc(ax2, "AX", write)
+//@highlightall(ax1, ax2)
+
+-- example/highlight_register_scope_amd64.s --
+TEXT ·f1(SB), $0-0
+ MOVQ AX, BX //@hiloc(axF1, "AX", read)
+
+TEXT ·f2(SB), $0-0
+ MOVQ AX, CX
+//@highlightall(axF1)
+
+-- example/highlight_compare_amd64.s --
+TEXT ·cmpDemo(SB), $0-0
+ CMPQ AX, BX //@hiloc(cmpAX, "AX", read)
+//@highlightall(cmpAX)
+
+-- example/highlight_label_scope.s --
+TEXT ·f1(SB), $0-0
+loop: //@hiloc(f1def, "loop", write)
+ JMP loop //@hiloc(f1ref, "loop", read)
+//@highlightall(f1def, f1ref)
+
+TEXT ·f2(SB), $0-0
+loop: //@hiloc(f2def, "loop", write)
+ JMP loop //@hiloc(f2ref, "loop", read)
+//@highlightall(f2def, f2ref)
+
+-- example/highlight_mem_operand_amd64.s --
+TEXT ·storeLoad(SB), $0-0
+ MOVQ AX, (BX) //@hiloc(bxStore, "BX", read)
+ MOVQ (AX), BX //@hiloc(bxLoad, "BX", write)
+//@highlightall(bxStore, bxLoad)
+
+-- example/highlight_stack_amd64.s --
+TEXT ·stack(SB), $0-0
+ PUSHQ AX //@hiloc(axPush, "AX", read)
+ POPQ AX //@hiloc(axPop, "AX", write)
+//@highlightall(axPush, axPop)
+
+-- example/highlight_register_386.s --
+TEXT ·reg386(SB), $0-0
+ POPL AX //@hiloc(axPop386, "AX", write)
+ CMPL CX, AX //@hiloc(axCmp386, "AX", read)
+//@highlightall(axPop386, axCmp386)
+
+-- example/highlight_immediate_amd64.s --
+TEXT ·imm(SB), $0-0
+ MOVQ $123, AX //@hiloc(imm, "123", read)
+//@highlight(imm)
+
+-- example/highlight_comment_amd64.s --
+TEXT ·c(SB), $0-0
+ MOVQ AX, BX
+ // ADDQ AX, CX //@hiloc(commentAX, "AX", read)
+//@highlight(commentAX)
+
+-- example/highlight_mnemonic_amd64.s --
+TEXT ·mn(SB), $0-0
+ MOVQ AX, BX //@hiloc(mnemonic, "MOVQ", read)
+//@highlight(mnemonic)
+
+-- example/highlight_bt_amd64.s --
+TEXT ·btDemo(SB), $0-0
+ BTQ $3, AX //@hiloc(btAX, "AX", read)
+//@highlightall(btAX)
+
+-- example/highlight_setcc_amd64.s --
+TEXT ·setcc(SB), $0-0
+ SETEQ AL //@hiloc(alSet, "AL", write)
+//@highlightall(alSet)
+
+-- example/highlight_bswap_amd64.s --
+TEXT ·bswap(SB), $0-0
+ BSWAPL AX //@hiloc(axBswap, "AX", write)
+//@highlightall(axBswap)
+
+-- example/highlight_cmpxchg_amd64.s --
+TEXT ·xchg(SB), $0-0
+ CMPXCHGQ AX, BX //@hiloc(bxXchg, "BX", write)
+//@highlightall(bxXchg)
+
+-- example/highlight_bare_text_amd64.s --
+TEXT ·f(SB), $0-0
+ MOVQ AX, BX //@hiloc(axBare1, "AX", read)
+TEXT
+ MOVQ AX, CX //@hiloc(axBare2, "AX", read)
+//@highlightall(axBare1, axBare2)
+
+-- example/highlight_register_noarch.s --
+TEXT ·noArch(SB), $0-0
+ MOVQ AX, BX //@hiloc(axNoArch, "AX", read)
+//@highlight(axNoArch)
+
+-- example/highlight_arm64.s --
+TEXT ·armDemo(SB), $0-0
+ MOVD R1, R0 //@hiloc(r0mov, "R0", write)
+ CMP R0, R1 //@hiloc(r0cmp, "R0", read)
+ TST $1, R0 //@hiloc(r0tst, "R0", read)
+//@highlightall(r0mov, r0cmp, r0tst)
+
+-- example/highlight_regpair_arm64.s --
+TEXT ·pair(SB), $0-0
+ LDP (0*8)(R0), (R4, R5) //@hiloc(r4load, "R4", write)
+ STP (R4, R5), (0*8)(R0) //@hiloc(r4store, "R4", read)
+//@highlightall(r4load, r4store)
+
+-- example/highlight_macro_arm64.s --
+#define QR(a, b, c, d) VADD a, b, c
+TEXT ·macro(SB), $0-0
+ QR(V0, V4, V8, V12) //@hiloc(v12, "V12", read)
+//@highlightall(v12)
+
+-- example/highlight_macro_amd64.s --
+#define QR(a, b, c, d) PADDL a, b
+TEXT ·macro(SB), $0-0
+ QR(X0, X4, X8, X12) //@hiloc(x12, "X12", read)
+//@highlightall(x12)
+
+-- example/highlight_xmm_amd64.s --
+TEXT ·xmm(SB), $0-0
+ MOVOU X0, X1 //@hiloc(x1mov, "X1", write)
+ PXOR X1, X0 //@hiloc(x1xor, "X1", read)
+//@highlightall(x1mov, x1xor)
+
diff --git a/gopls/internal/util/asm/parse.go b/gopls/internal/util/asm/parse.go
index 233c0c4..765eb5f 100644
--- a/gopls/internal/util/asm/parse.go
+++ b/gopls/internal/util/asm/parse.go
@@ -9,6 +9,7 @@
"bufio"
"bytes"
"fmt"
+ "sort"
"strings"
"unicode"
@@ -45,6 +46,8 @@
// A file represents a parsed file of Go assembly language.
type File struct {
+ // Idents holds the identifiers of the file, ordered by Offset;
+ // [File.IdentAt] relies on that order for its binary search.
Idents []Ident
Mapper *protocol.Mapper
@@ -63,6 +66,55 @@
return f.Mapper.OffsetLocation(ident.Offset, ident.Offset+ident.OrigLen)
}
+// IdentAt returns the identifier containing the byte range [start, end),
+// or nil if none. Because [File.Idents] are ordered by Offset, the
+// lookup uses a binary search.
+func (f *File) IdentAt(start, end int) *Ident {
+ // Find the last identifier whose Offset <= start.
+ idx := sort.Search(len(f.Idents), func(i int) bool {
+ return f.Idents[i].Offset > start
+ })
+ if idx == 0 {
+ return nil
+ }
+ id := &f.Idents[idx-1]
+ if end <= id.End() {
+ return id
+ }
+ return nil
+}
+
+// FunctionRange returns the byte range [start, end) of the TEXT function
+// enclosing offset: start is the beginning of the line containing the
+// enclosing TEXT directive, end is the beginning of the line containing
+// the next TEXT directive, or len(content) if there is none. If offset
+// precedes the first TEXT directive, the range covers from 0 to the
+// first TEXT directive.
+//
+// TEXT directives are taken from the parsed file rather than re-detected
+// here, so that scoping stays consistent with the identifiers the parser
+// reports (e.g. a bare "TEXT" line with no symbol is not a boundary).
+func (f *File) FunctionRange(offset int) (int, int) {
+ content := f.Mapper.Content
+ funcStart, funcEnd := 0, len(content)
+ for i := range f.Idents {
+ id := &f.Idents[i]
+ if id.Kind != Text {
+ continue
+ }
+ lineStart := id.Offset
+ for lineStart > 0 && content[lineStart-1] != '\n' {
+ lineStart--
+ }
+ if lineStart > offset {
+ funcEnd = lineStart
+ break
+ }
+ funcStart = lineStart
+ }
+ return funcStart, funcEnd
+}
+
// Ident represents an identifier in an assembly file.
type Ident struct {
Name string // symbol name (after correcting [·∕]); Name[0]='.' => current package