blob: 63d8e82d9fd1e305afae0652d31a9f2112ba0d6e [file] [log] [blame]
// Copyright 2024 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 golang
// This file produces the "Browse GOARCH assembly of f" HTML report.
//
// See also:
// - ./codeaction.go - computes the symbol and offers the CodeAction command.
// - ../server/command.go - handles the command by opening a web page.
// - ../server/server.go - handles the HTTP request and calls this function.
import (
"bytes"
"context"
"fmt"
"html"
"path/filepath"
"regexp"
"strconv"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/internal/gocommand"
)
// AssemblyHTML returns an HTML document containing an assembly listing of the selected function.
//
// TODO(adonovan):
// - display a "Compiling..." message as a cold build can be slow.
// - cross-link jumps and block labels, like github.com/aclements/objbrowse.
func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string, web Web) ([]byte, error) {
// Compile the package with -S, and capture its stderr stream.
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{
Verb: "build",
Args: []string{"-gcflags=-S", "."},
WorkingDir: filepath.Dir(pkg.Metadata().CompiledGoFiles[0].Path()),
})
if err != nil {
return nil, err // e.g. failed to write overlays (rare)
}
defer cleanupInvocation()
_, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv)
if err != nil {
return nil, err // e.g. won't compile
}
content := stderr.String()
escape := html.EscapeString
// Produce the report.
title := fmt.Sprintf("%s assembly for %s",
escape(snapshot.View().GOARCH()),
escape(symbol))
var buf bytes.Buffer
buf.WriteString(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>` + escape(title) + `</title>
<link rel="stylesheet" href="/assets/common.css">
<script src="/assets/common.js"></script>
</head>
<body>
<h1>` + title + `</h1>
<p>
<a href='https://go.dev/doc/asm'>A Quick Guide to Go's Assembler</a>
</p>
<p>
Experimental. <a href='https://github.com/golang/go/issues/67478'>Contributions welcome!</a>
</p>
<p>
Click on a source line marker <code>L1234</code> to navigate your editor there.
(VS Code users: please upvote <a href='https://github.com/microsoft/vscode/issues/208093'>#208093</a>)
</p>
<p>
Reload the page to recompile.
</p>
<pre>
`)
// insnRx matches an assembly instruction line.
// Submatch groups are: (offset-hex-dec, file-line-column, instruction).
insnRx := regexp.MustCompile(`^(\s+0x[0-9a-f ]+)\(([^)]*)\)\s+(.*)$`)
// Parse the functions of interest out of the listing.
// Each function is of the form:
//
// symbol STEXT k=v...
// 0x0000 00000 (/file.go:123) NOP...
// ...
//
// Allow matches of symbol, symbol.func1, symbol.deferwrap, etc.
on := false
for _, line := range strings.Split(content, "\n") {
// start of function symbol?
if strings.Contains(line, " STEXT ") {
on = strings.HasPrefix(line, symbol) &&
(line[len(symbol)] == ' ' || line[len(symbol)] == '.')
}
if !on {
continue // within uninteresting symbol
}
// In lines of the form
// "\t0x0000 00000 (/file.go:123) NOP..."
// replace the "(/file.go:123)" portion with an "L0123" source link.
// Skip filenames of the form "<foo>".
if parts := insnRx.FindStringSubmatch(line); parts != nil {
link := " " // if unknown
if file, linenum, ok := cutLast(parts[2], ":"); ok && !strings.HasPrefix(file, "<") {
if linenum, err := strconv.Atoi(linenum); err == nil {
text := fmt.Sprintf("L%04d", linenum)
link = sourceLink(text, web.SrcURL(file, linenum, 1))
}
}
fmt.Fprintf(&buf, "%s\t%s\t%s", escape(parts[1]), link, escape(parts[3]))
} else {
buf.WriteString(escape(line))
}
buf.WriteByte('\n')
}
return buf.Bytes(), nil
}
// cutLast is the "last" analogue of [strings.Cut].
func cutLast(s, sep string) (before, after string, ok bool) {
if i := strings.LastIndex(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}