mirror of
https://github.com/golang/go.git
synced 2026-02-06 18:00:01 +00:00
cmd/compile: enhance astdump flag to also generate HTML
AI-generated code, 3 merged commits, plus a LOT of hand cleanups and tweaks, including removing cargo-culted dead code from the SSA example, reorganizing CSS and JS out of a single giant comment, using defer appropriately to ensure balanced open/close tags, running output through tidy to check compliance. Prompts are included for reference. This is intended to produce an HTML file in the style of "ssa.html", but for AST. The result of various phases appears in columns, which can be scrolled side to side, and also dragged sideways to make them wider (because AST tends wide). This supports three kinds of highlighting, which I tweaked along the way to make them (in my opinion) more useful. 1) Node outlining. This outlines a node and all of its subtrees. When the cursor is a "cell" (outlined cross) node highlighting is available. Note that "NAME" nodes are repeated within the tree, so click on one of these will outline every occurrence. This is actually done with pointer identity. 2) Name highlighting, available with a "crosshair" cursor. This highlights a name, e.g. "autotmp_1". 3) Position highlighting, available with a "crosshair" cursor. This highlights either a file (all occurrences of that file's positions), a line within a file (all occurrences of that file:line combination), or a column (all occurrences of that particular file:line:column). Inlined positions are treated as a sequence of positions, not a single position. Prompts: ``` The file cmd/compile/internal/ir/dump.go contains a function AstDump that calls FDump to generate a textual representation of the AST from several phases within the compiler. The file cmd/compile/internal/ir/fmt.go contains the definition of FDump. The SSA phases of the compiler use code in cmd/compile/internal/ssa/html.go to render its textual representation into an html display that allows hiding phases and highlighting blocks, identifiers and line numbers. Please write a similar HTML-generating phase for AST that produces an output that will allow hiding phases and perhaps hiding subtrees, and highlighting identifiers and line numbers. The idioms and hacks used in the SSA html display have worked well, if you want to copy them. ``` ``` Not bad, but the output contains a strike-through beginning on a line that contains "TYPE type *testing.B tc(1)". The strike through begins after "TYPE " and continues for the rest of the output. Can you find that bug and fix it? ``` ``` The AST output is often wider than the statically sized columns in the HTML output. Either the columns need to have a width that can be resized (dragged wider, for example) or the AST needs to be draggable, side-to-side, within the columns. Resizable columns seems like the preferable choice, if it is possible. ``` ``` The highlighting for file name and line number is not quite right -- all the lines in the same file are grouped together, where what I want, is that each different file:line:column gets its own number. There's also the issue of inlining, in some cases the location is described as more than one file:line:column, where the first is the call site and the second is the inlined function. I think it makes sense to treat each single file:line:column as its own item for highlighting, instead of trying to treat the sequence of file:line:column as a single distinct location. One thing that might be interesting, but I am not sure how hard it would be, is to distinguish between clicks to the file part, the line part, and the column part -- click on file means highlight all that matches file, click on line means all that machines file:line (not just the line numner, since there may be different files, with inlining) and click on the column means to highlight the specific file:line:column triple. That is, if it is possible. ``` ``` Lovely. Can you implement highlighting for names, strings like "NAME-testing.b" so that all uses of a variable or a temporary can easily be seen? ``` Change-Id: I1ed97cd92cdae16d556e3334e543af37973799e5 Reviewed-on: https://go-review.googlesource.com/c/go/+/740563 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: t hepudds <thepudds1460@gmail.com> Reviewed-by: Keith Randall <khr@google.com>
This commit is contained in:
parent
835d6d42c4
commit
ae7b257f24
4 changed files with 1080 additions and 0 deletions
|
|
@ -47,6 +47,7 @@ import (
|
|||
// already been some compiler errors). It may also be invoked from the explicit panic in
|
||||
// hcrash(), in which case, we pass the panic on through.
|
||||
func handlePanic() {
|
||||
ir.CloseHTMLWriters()
|
||||
if err := recover(); err != nil {
|
||||
if err == "-h" {
|
||||
// Force real panic now with -h option (hcrash) - the error
|
||||
|
|
@ -243,6 +244,12 @@ func Main(archInit func(*ssagen.ArchInfo)) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, fn := range typecheck.Target.Funcs {
|
||||
if ir.MatchAstDump(fn, "start") {
|
||||
ir.AstDump(fn, "start, "+ir.FuncName(fn))
|
||||
}
|
||||
}
|
||||
|
||||
// Apply bloop markings.
|
||||
bloop.Walk(typecheck.Target)
|
||||
|
||||
|
|
@ -250,6 +257,12 @@ func Main(archInit func(*ssagen.ArchInfo)) {
|
|||
base.Timer.Start("fe", "devirtualize-and-inline")
|
||||
interleaved.DevirtualizeAndInlinePackage(typecheck.Target, profile)
|
||||
|
||||
for _, fn := range typecheck.Target.Funcs {
|
||||
if ir.MatchAstDump(fn, "devirtualize-and-inline") {
|
||||
ir.AstDump(fn, "devirtualize-and-inline, "+ir.FuncName(fn))
|
||||
}
|
||||
}
|
||||
|
||||
noder.MakeWrappers(typecheck.Target) // must happen after inlining
|
||||
|
||||
// Get variable capture right in for loops.
|
||||
|
|
|
|||
|
|
@ -143,6 +143,12 @@ func AstDump(fn *Func, why string) {
|
|||
FDump(w, why, fn)
|
||||
},
|
||||
)
|
||||
// strip text following comma, for phase names.
|
||||
comma := strings.Index(why, ",")
|
||||
if comma > 0 {
|
||||
why = why[:comma]
|
||||
}
|
||||
DumpNodeHTML(fn, why, fn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Dump returned error %v\n", err)
|
||||
}
|
||||
|
|
@ -189,6 +195,37 @@ func withLockAndFile(fn *Func, dump func(io.Writer)) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
var htmlWriters = make(map[*Func]*HTMLWriter)
|
||||
var orderedFuncs = []*Func{}
|
||||
|
||||
// DumpNodeHTML dumps the node n to the HTML writer for fn.
|
||||
// It uses the same phase name as the text dump.
|
||||
func DumpNodeHTML(fn *Func, why string, n Node) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
w, ok := htmlWriters[fn]
|
||||
if !ok {
|
||||
name := escapedFileName(fn, ".html")
|
||||
w = NewHTMLWriter(name, fn, "")
|
||||
htmlWriters[fn] = w
|
||||
orderedFuncs = append(orderedFuncs, fn)
|
||||
}
|
||||
w.WritePhase(why, why)
|
||||
}
|
||||
|
||||
// CloseHTMLWriter closes the HTML writer for fn, if one exists.
|
||||
func CloseHTMLWriters() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, fn := range orderedFuncs {
|
||||
if w, ok := htmlWriters[fn]; ok {
|
||||
w.Close()
|
||||
delete(htmlWriters, fn)
|
||||
}
|
||||
}
|
||||
orderedFuncs = nil
|
||||
}
|
||||
|
||||
type dumper struct {
|
||||
output io.Writer
|
||||
fieldrx *regexp.Regexp // field name filter
|
||||
|
|
|
|||
926
src/cmd/compile/internal/ir/html.go
Normal file
926
src/cmd/compile/internal/ir/html.go
Normal file
|
|
@ -0,0 +1,926 @@
|
|||
// Copyright 2026 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 ir
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cmd/compile/internal/base"
|
||||
"cmd/compile/internal/types"
|
||||
"cmd/internal/src"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An HTMLWriter dumps IR to multicolumn HTML, similar to what the
|
||||
// ssa backend does for GOSSAFUNC. This is not the format used for
|
||||
// the ast column in GOSSAFUNC output.
|
||||
type HTMLWriter struct {
|
||||
w *BufferedWriterCloser
|
||||
Func *Func
|
||||
canonIdMap map[Node]int
|
||||
prevCanonId int
|
||||
path string
|
||||
prevHash []byte
|
||||
pendingPhases []string
|
||||
pendingTitles []string
|
||||
}
|
||||
|
||||
// BufferedWriterCloser is here to help avoid pre-buffering the whole
|
||||
// rendered HTML in memory, which can cause problems for large inputs.
|
||||
type BufferedWriterCloser struct {
|
||||
file io.Closer
|
||||
w *bufio.Writer
|
||||
}
|
||||
|
||||
func (b *BufferedWriterCloser) Write(p []byte) (n int, err error) {
|
||||
return b.w.Write(p)
|
||||
}
|
||||
|
||||
func (b *BufferedWriterCloser) Close() error {
|
||||
b.w.Flush()
|
||||
b.w = nil
|
||||
return b.file.Close()
|
||||
}
|
||||
|
||||
func NewBufferedWriterCloser(f io.WriteCloser) *BufferedWriterCloser {
|
||||
return &BufferedWriterCloser{file: f, w: bufio.NewWriter(f)}
|
||||
}
|
||||
|
||||
func NewHTMLWriter(path string, f *Func, cfgMask string) *HTMLWriter {
|
||||
path = strings.ReplaceAll(path, "/", string(filepath.Separator))
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
base.Fatalf("%v", err)
|
||||
}
|
||||
reportPath := path
|
||||
if !filepath.IsAbs(reportPath) {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
base.Fatalf("%v", err)
|
||||
}
|
||||
reportPath = filepath.Join(pwd, path)
|
||||
}
|
||||
h := HTMLWriter{
|
||||
w: NewBufferedWriterCloser(out),
|
||||
Func: f,
|
||||
path: reportPath,
|
||||
canonIdMap: make(map[Node]int),
|
||||
}
|
||||
h.start()
|
||||
return &h
|
||||
}
|
||||
|
||||
// canonId assigns indices to nodes based on pointer identity.
|
||||
// this helps ensure that output html files don't gratuitously
|
||||
// differ from run to run.
|
||||
func (h *HTMLWriter) canonId(n Node) int {
|
||||
if id := h.canonIdMap[n]; id > 0 {
|
||||
return id
|
||||
}
|
||||
h.prevCanonId++
|
||||
h.canonIdMap[n] = h.prevCanonId
|
||||
return h.prevCanonId
|
||||
}
|
||||
|
||||
// Fatalf reports an error and exits.
|
||||
func (w *HTMLWriter) Fatalf(msg string, args ...any) {
|
||||
base.FatalfAt(src.NoXPos, msg, args...)
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) start() {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
escName := html.EscapeString(PkgFuncName(w.Func))
|
||||
w.Print("<!DOCTYPE html>")
|
||||
w.Print("<html>")
|
||||
w.Printf(`<head>
|
||||
<meta name="generator" content="AST display for %s">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
%s
|
||||
%s
|
||||
<title>AST display for %s</title>
|
||||
</head>`, escName, CSS, JS, escName)
|
||||
w.Print("<body>")
|
||||
w.Print("<h1>")
|
||||
w.Print(html.EscapeString(w.Func.Sym().Name))
|
||||
w.Print("</h1>")
|
||||
w.Print(`
|
||||
<a href="#" onclick="toggle_visibility('help');return false;" id="helplink">help</a>
|
||||
<div id="help">
|
||||
|
||||
<p>
|
||||
Click anywhere on a node (with "cell" cursor) to outline a node and all of its subtrees.
|
||||
</p>
|
||||
<p>
|
||||
Click on a name (with "crosshair" cursor) to highlight every occurrence of a name.
|
||||
(Note that all the name nodes are the same node, so those also all outline together).
|
||||
</p>
|
||||
<p>
|
||||
Click on a file, line, or column (with "crosshair" cursor) to highlight positions
|
||||
in that file, at that file:line, or at that file:line:column, respectively.<br>Inlined
|
||||
locations are not treated as a single location, but as a sequence of locations that
|
||||
can be independently highlighted.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<label for="dark-mode-button" style="margin-left: 15px; cursor: pointer;">darkmode</label>
|
||||
<input type="checkbox" onclick="toggleDarkMode();" id="dark-mode-button" style="cursor: pointer" />
|
||||
`)
|
||||
w.Print("<table>")
|
||||
w.Print("<tr>")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) Close() {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
w.Print("</tr>")
|
||||
w.Print("</table>")
|
||||
w.Print("</body>")
|
||||
w.Print("</html>\n")
|
||||
w.w.Close()
|
||||
fmt.Fprintf(os.Stderr, "Writing html ast output for %s to %s\n", PkgFuncName(w.Func), w.path)
|
||||
}
|
||||
|
||||
// WritePhase writes f in a column headed by title.
|
||||
// phase is used for collapsing columns and should be unique across the table.
|
||||
func (w *HTMLWriter) WritePhase(phase, title string) {
|
||||
if w == nil {
|
||||
return // avoid generating HTML just to discard it
|
||||
}
|
||||
w.pendingPhases = append(w.pendingPhases, phase)
|
||||
w.pendingTitles = append(w.pendingTitles, title)
|
||||
w.flushPhases()
|
||||
}
|
||||
|
||||
// flushPhases collects any pending phases and titles, writes them to the html, and resets the pending slices.
|
||||
func (w *HTMLWriter) flushPhases() {
|
||||
phaseLen := len(w.pendingPhases)
|
||||
if phaseLen == 0 {
|
||||
return
|
||||
}
|
||||
phases := strings.Join(w.pendingPhases, " + ")
|
||||
w.WriteMultiTitleColumn(
|
||||
phases,
|
||||
w.pendingTitles,
|
||||
"allow-x-scroll",
|
||||
w.FuncHTML(w.pendingPhases[phaseLen-1]),
|
||||
)
|
||||
w.pendingPhases = w.pendingPhases[:0]
|
||||
w.pendingTitles = w.pendingTitles[:0]
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) WriteMultiTitleColumn(phase string, titles []string, class string, writeContent func()) {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
id := strings.ReplaceAll(phase, " ", "-")
|
||||
// collapsed column
|
||||
w.Printf("<td id=\"%v-col\" class=\"collapsed\"><div>%v</div></td>", id, phase)
|
||||
|
||||
if class == "" {
|
||||
w.Printf("<td id=\"%v-exp\">", id)
|
||||
} else {
|
||||
w.Printf("<td id=\"%v-exp\" class=\"%v\">", id, class)
|
||||
}
|
||||
for _, title := range titles {
|
||||
w.Print("<h2>" + title + "</h2>")
|
||||
}
|
||||
writeContent()
|
||||
w.Print("<div class=\"resizer\"></div>")
|
||||
w.Print("</td>\n")
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) Printf(msg string, v ...any) {
|
||||
if _, err := fmt.Fprintf(w.w, msg, v...); err != nil {
|
||||
w.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) Print(s string) {
|
||||
if _, err := fmt.Fprint(w.w, s); err != nil {
|
||||
w.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) indent(n int) {
|
||||
indent(w.w, n)
|
||||
}
|
||||
|
||||
func (w *HTMLWriter) FuncHTML(phase string) func() {
|
||||
return func() {
|
||||
w.Print("<pre>") // use pre for formatting to preserve indentation
|
||||
w.dumpNodesHTML(w.Func.Body, 1)
|
||||
w.Print("</pre>")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTMLWriter) dumpNodesHTML(list Nodes, depth int) {
|
||||
if len(list) == 0 {
|
||||
h.Print(" <nil>")
|
||||
return
|
||||
}
|
||||
|
||||
for _, n := range list {
|
||||
h.dumpNodeHTML(n, depth)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTMLWriter) dumpNodeHTML(n Node, depth int) {
|
||||
indent(h.w, depth)
|
||||
if depth > 40 {
|
||||
h.Print("...")
|
||||
return
|
||||
}
|
||||
|
||||
if n == nil {
|
||||
h.Print("NilIrNode")
|
||||
return
|
||||
}
|
||||
|
||||
// For HTML, we want to wrap the node and its details in a span that can be highlighted
|
||||
// across all occurrences of the span in all columns, so it has to be linked to the node ID,
|
||||
// which is its address. Canonicalize the address to a counter so that repeated compiler
|
||||
// runs yield the same html.
|
||||
//
|
||||
// JS Equivalence logic:
|
||||
// var c = elem.classList.item(0);
|
||||
// var x = document.getElementsByClassName(c);
|
||||
//
|
||||
// Tag each class with its canonicalized index.
|
||||
|
||||
h.Printf("<span class=\"n%d ir-node\">", h.canonId(n))
|
||||
defer h.Printf("</span>")
|
||||
|
||||
if len(n.Init()) != 0 {
|
||||
h.Printf("%+v-init", n.Op())
|
||||
h.dumpNodesHTML(n.Init(), depth+1)
|
||||
h.indent(depth)
|
||||
}
|
||||
|
||||
switch n.Op() {
|
||||
default:
|
||||
h.Printf("%+v", n.Op())
|
||||
h.dumpNodeHeaderHTML(n)
|
||||
|
||||
case OLITERAL:
|
||||
h.Printf("%+v-%v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Val())))
|
||||
h.dumpNodeHeaderHTML(n)
|
||||
return
|
||||
|
||||
case ONAME, ONONAME:
|
||||
if n.Sym() != nil {
|
||||
// Name highlighting:
|
||||
// Create a hash for the symbol name to use as a class
|
||||
// We use the same irValueClicked logic which uses the first class as the identifier
|
||||
name := fmt.Sprintf("%v", n.Sym())
|
||||
hash := sha256.Sum256([]byte(name))
|
||||
symID := "sym-" + hex.EncodeToString(hash[:6])
|
||||
h.Printf("%+v-<span class=\"%s variable-name\">%+v</span>", n.Op(), symID, html.EscapeString(name))
|
||||
} else {
|
||||
h.Printf("%+v", n.Op())
|
||||
}
|
||||
h.dumpNodeHeaderHTML(n)
|
||||
return
|
||||
|
||||
case OLINKSYMOFFSET:
|
||||
n := n.(*LinksymOffsetExpr)
|
||||
h.Printf("%+v-%v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Linksym)))
|
||||
if n.Offset_ != 0 {
|
||||
h.Printf("%+v", n.Offset_)
|
||||
}
|
||||
h.dumpNodeHeaderHTML(n)
|
||||
|
||||
case OASOP:
|
||||
n := n.(*AssignOpStmt)
|
||||
h.Printf("%+v-%+v", n.Op(), n.AsOp)
|
||||
h.dumpNodeHeaderHTML(n)
|
||||
|
||||
case OTYPE:
|
||||
h.Printf("%+v %+v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Sym())))
|
||||
h.dumpNodeHeaderHTML(n)
|
||||
return
|
||||
|
||||
case OCLOSURE:
|
||||
h.Printf("%+v", n.Op())
|
||||
h.dumpNodeHeaderHTML(n)
|
||||
|
||||
case ODCLFUNC:
|
||||
n := n.(*Func)
|
||||
h.Printf("%+v", n.Op())
|
||||
h.dumpNodeHeaderHTML(n)
|
||||
fn := n
|
||||
if len(fn.Dcl) > 0 {
|
||||
h.indent(depth)
|
||||
h.Printf("%+v-Dcl", n.Op())
|
||||
for _, dcl := range n.Dcl {
|
||||
h.dumpNodeHTML(dcl, depth+1)
|
||||
}
|
||||
}
|
||||
if len(fn.ClosureVars) > 0 {
|
||||
h.indent(depth)
|
||||
h.Printf("%+v-ClosureVars", n.Op())
|
||||
for _, cv := range fn.ClosureVars {
|
||||
h.dumpNodeHTML(cv, depth+1)
|
||||
}
|
||||
}
|
||||
if len(fn.Body) > 0 {
|
||||
h.indent(depth)
|
||||
h.Printf("%+v-body", n.Op())
|
||||
h.dumpNodesHTML(fn.Body, depth+1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(n).Elem()
|
||||
t := reflect.TypeOf(n).Elem()
|
||||
nf := t.NumField()
|
||||
for i := 0; i < nf; i++ {
|
||||
tf := t.Field(i)
|
||||
vf := v.Field(i)
|
||||
if tf.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
switch tf.Type.Kind() {
|
||||
case reflect.Interface, reflect.Ptr, reflect.Slice:
|
||||
if vf.IsNil() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
name := strings.TrimSuffix(tf.Name, "_")
|
||||
switch name {
|
||||
case "X", "Y", "Index", "Chan", "Value", "Call":
|
||||
name = ""
|
||||
}
|
||||
switch val := vf.Interface().(type) {
|
||||
case Node:
|
||||
if name != "" {
|
||||
h.indent(depth)
|
||||
h.Printf("%+v-%s", n.Op(), name)
|
||||
}
|
||||
h.dumpNodeHTML(val, depth+1)
|
||||
case Nodes:
|
||||
if len(val) == 0 {
|
||||
continue
|
||||
}
|
||||
if name != "" {
|
||||
h.indent(depth)
|
||||
h.Printf("%+v-%s", n.Op(), name)
|
||||
}
|
||||
h.dumpNodesHTML(val, depth+1)
|
||||
default:
|
||||
if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) {
|
||||
if vf.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
if name != "" {
|
||||
h.indent(depth)
|
||||
h.Printf("%+v-%s", n.Op(), name)
|
||||
}
|
||||
for i, n := 0, vf.Len(); i < n; i++ {
|
||||
h.dumpNodeHTML(vf.Index(i).Interface().(Node), depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTMLWriter) dumpNodeHeaderHTML(n Node) {
|
||||
// print pointer to be able to see identical nodes
|
||||
if base.Debug.DumpPtrs != 0 {
|
||||
h.Printf(" p(%p)", n)
|
||||
}
|
||||
|
||||
if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Defn != nil {
|
||||
h.Printf(" defn(%p)", n.Name().Defn)
|
||||
}
|
||||
|
||||
if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Curfn != nil {
|
||||
h.Printf(" curfn(%p)", n.Name().Curfn)
|
||||
}
|
||||
if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Outer != nil {
|
||||
h.Printf(" outer(%p)", n.Name().Outer)
|
||||
}
|
||||
|
||||
if EscFmt != nil {
|
||||
if esc := EscFmt(n); esc != "" {
|
||||
h.Printf(" %s", html.EscapeString(esc))
|
||||
}
|
||||
}
|
||||
|
||||
if n.Sym() != nil && n.Op() != ONAME && n.Op() != ONONAME && n.Op() != OTYPE {
|
||||
h.Printf(" %+v", html.EscapeString(fmt.Sprintf("%v", n.Sym())))
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(n).Elem()
|
||||
t := v.Type()
|
||||
nf := t.NumField()
|
||||
for i := 0; i < nf; i++ {
|
||||
tf := t.Field(i)
|
||||
if tf.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
k := tf.Type.Kind()
|
||||
if reflect.Bool <= k && k <= reflect.Complex128 {
|
||||
name := strings.TrimSuffix(tf.Name, "_")
|
||||
vf := v.Field(i)
|
||||
vfi := vf.Interface()
|
||||
if name == "Offset" && vfi == types.BADWIDTH || name != "Offset" && vf.IsZero() {
|
||||
continue
|
||||
}
|
||||
if vfi == true {
|
||||
h.Printf(" %s", name)
|
||||
} else {
|
||||
h.Printf(" %s:%+v", name, html.EscapeString(fmt.Sprintf("%v", vf.Interface())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v = reflect.ValueOf(n)
|
||||
t = v.Type()
|
||||
nm := t.NumMethod()
|
||||
for i := 0; i < nm; i++ {
|
||||
tm := t.Method(i)
|
||||
if tm.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
m := v.Method(i)
|
||||
mt := m.Type()
|
||||
if mt.NumIn() == 0 && mt.NumOut() == 1 && mt.Out(0).Kind() == reflect.Bool {
|
||||
func() {
|
||||
defer func() { recover() }()
|
||||
if m.Call(nil)[0].Bool() {
|
||||
name := strings.TrimSuffix(tm.Name, "_")
|
||||
h.Printf(" %s", name)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if n.Op() == OCLOSURE {
|
||||
n := n.(*ClosureExpr)
|
||||
if fn := n.Func; fn != nil && fn.Nname.Sym() != nil {
|
||||
h.Printf(" fnName(%+v)", html.EscapeString(fmt.Sprintf("%v", fn.Nname.Sym())))
|
||||
}
|
||||
}
|
||||
|
||||
if n.Type() != nil {
|
||||
if n.Op() == OTYPE {
|
||||
h.Printf(" type")
|
||||
}
|
||||
h.Printf(" %+v", html.EscapeString(fmt.Sprintf("%v", n.Type())))
|
||||
}
|
||||
if n.Typecheck() != 0 {
|
||||
h.Printf(" tc(%d)", n.Typecheck())
|
||||
}
|
||||
|
||||
if n.Pos().IsKnown() {
|
||||
h.Print(" <span class=\"line-number\">")
|
||||
switch n.Pos().IsStmt() {
|
||||
case src.PosNotStmt:
|
||||
h.Print("_")
|
||||
case src.PosIsStmt:
|
||||
h.Print("+")
|
||||
}
|
||||
sep := ""
|
||||
base.Ctxt.AllPos(n.Pos(), func(pos src.Pos) {
|
||||
h.Print(sep)
|
||||
sep = " "
|
||||
// Hierarchical highlighting:
|
||||
// Click file -> highlight all ranges in this file
|
||||
// Click line -> highlight all ranges at this line (in this file)
|
||||
// Click col -> highlight this specific range
|
||||
|
||||
file := pos.Filename()
|
||||
// Create a hash for the filename to use as a class
|
||||
hash := sha256.Sum256([]byte(file))
|
||||
fileID := "loc-" + hex.EncodeToString(hash[:6])
|
||||
lineID := fmt.Sprintf("%s-L%d", fileID, pos.Line())
|
||||
colID := fmt.Sprintf("%s-C%d", lineID, pos.Col())
|
||||
|
||||
// File part: triggers fileID
|
||||
h.Printf("<span class=\"%s line-number\">%s</span>:", fileID, html.EscapeString(filepath.Base(file)))
|
||||
// Line part: triggers lineID (and fileID via class list)
|
||||
h.Printf("<span class=\"%s %s line-number\">%d</span>:", lineID, fileID, pos.Line())
|
||||
// Col part: triggers colID (and lineID, fileID)
|
||||
h.Printf("<span class=\"%s %s %s line-number\">%d</span>", colID, lineID, fileID, pos.Col())
|
||||
})
|
||||
h.Print("</span>")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
CSS = `<style>
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
display: inline-block;
|
||||
margin: 0 1em .5em 0;
|
||||
}
|
||||
|
||||
#helplink {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#help {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid black;
|
||||
table-layout: fixed;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid black;
|
||||
overflow: hidden;
|
||||
width: 400px;
|
||||
vertical-align: top;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
display: inline-block;
|
||||
background: transparent;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: col-resize;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
td > h2 {
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
margin: 5px 0px 5px 0px;
|
||||
}
|
||||
|
||||
td.collapsed {
|
||||
font-size: 12px;
|
||||
width: 12px;
|
||||
border: 1px solid white;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
td.collapsed div {
|
||||
text-align: right;
|
||||
transform: rotate(180deg);
|
||||
writing-mode: vertical-lr;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: Menlo, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
pre {
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.allow-x-scroll {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.ir-node {
|
||||
cursor: cell;
|
||||
}
|
||||
|
||||
.variable-name {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
font-size: 11px;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
body.darkmode {
|
||||
background-color: rgb(21, 21, 21);
|
||||
color: rgb(230, 255, 255);
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
td.darkmode {
|
||||
background-color: rgb(21, 21, 21);
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
body.darkmode table, th {
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
body.darkmode text {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.highlight-aquamarine { background-color: aquamarine; color: black; }
|
||||
.highlight-coral { background-color: coral; color: black; }
|
||||
.highlight-lightpink { background-color: lightpink; color: black; }
|
||||
.highlight-lightsteelblue { background-color: lightsteelblue; color: black; }
|
||||
.highlight-palegreen { background-color: palegreen; color: black; }
|
||||
.highlight-skyblue { background-color: skyblue; color: black; }
|
||||
.highlight-lightgray { background-color: lightgray; color: black; }
|
||||
.highlight-yellow { background-color: yellow; color: black; }
|
||||
.highlight-lime { background-color: lime; color: black; }
|
||||
.highlight-khaki { background-color: khaki; color: black; }
|
||||
.highlight-aqua { background-color: aqua; color: black; }
|
||||
.highlight-salmon { background-color: salmon; color: black; }
|
||||
|
||||
|
||||
.outline-blue { outline: #2893ff solid 2px; }
|
||||
.outline-red { outline: red solid 2px; }
|
||||
.outline-blueviolet { outline: blueviolet solid 2px; }
|
||||
.outline-darkolivegreen { outline: darkolivegreen solid 2px; }
|
||||
.outline-fuchsia { outline: fuchsia solid 2px; }
|
||||
.outline-sienna { outline: sienna solid 2px; }
|
||||
.outline-gold { outline: gold solid 2px; }
|
||||
.outline-orangered { outline: orangered solid 2px; }
|
||||
.outline-teal { outline: teal solid 2px; }
|
||||
.outline-maroon { outline: maroon solid 2px; }
|
||||
.outline-black { outline: black solid 2px; }
|
||||
|
||||
/* Capture alternative for outline-black and ellipse.outline-black when in dark mode */
|
||||
body.darkmode .outline-black { outline: gray solid 2px; }
|
||||
|
||||
</style>
|
||||
`
|
||||
|
||||
JS = `<script type="text/javascript">
|
||||
|
||||
// Contains phase names which are expanded by default. Other columns are collapsed.
|
||||
let expandedDefault = [
|
||||
"bloop",
|
||||
"loopvar",
|
||||
"escape",
|
||||
"slice",
|
||||
"walk",
|
||||
];
|
||||
if (history.state === null) {
|
||||
history.pushState({expandedDefault}, "", location.href);
|
||||
}
|
||||
|
||||
// ordered list of all available highlight colors
|
||||
var highlights = [
|
||||
"highlight-aquamarine",
|
||||
"highlight-coral",
|
||||
"highlight-lightpink",
|
||||
"highlight-lightsteelblue",
|
||||
"highlight-palegreen",
|
||||
"highlight-skyblue",
|
||||
"highlight-lightgray",
|
||||
"highlight-yellow",
|
||||
"highlight-lime",
|
||||
"highlight-khaki",
|
||||
"highlight-aqua",
|
||||
"highlight-salmon"
|
||||
];
|
||||
|
||||
// state: which value is highlighted this color?
|
||||
var highlighted = {};
|
||||
for (var i = 0; i < highlights.length; i++) {
|
||||
highlighted[highlights[i]] = "";
|
||||
}
|
||||
|
||||
// ordered list of all available outline colors
|
||||
var outlines = [
|
||||
"outline-blue",
|
||||
"outline-red",
|
||||
"outline-blueviolet",
|
||||
"outline-darkolivegreen",
|
||||
"outline-fuchsia",
|
||||
"outline-sienna",
|
||||
"outline-gold",
|
||||
"outline-orangered",
|
||||
"outline-teal",
|
||||
"outline-maroon",
|
||||
"outline-black"
|
||||
];
|
||||
|
||||
// state: which value is outlined this color?
|
||||
var outlined = {};
|
||||
for (var i = 0; i < outlines.length; i++) {
|
||||
outlined[outlines[i]] = "";
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
if (history.state !== null) {
|
||||
expandedDefault = history.state.expandedDefault;
|
||||
}
|
||||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
toggleDarkMode();
|
||||
document.getElementById("dark-mode-button").checked = true;
|
||||
}
|
||||
|
||||
var irElemClicked = function(elem, event, selections, selected) {
|
||||
event.stopPropagation();
|
||||
|
||||
// find all values with the same name
|
||||
var c = elem.classList.item(0);
|
||||
var x = document.getElementsByClassName(c);
|
||||
|
||||
// if selected, remove selections from all of them
|
||||
// otherwise, attempt to add
|
||||
|
||||
var remove = "";
|
||||
for (var i = 0; i < selections.length; i++) {
|
||||
var color = selections[i];
|
||||
if (selected[color] == c) {
|
||||
remove = color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (remove != "") {
|
||||
for (var i = 0; i < x.length; i++) {
|
||||
x[i].classList.remove(remove);
|
||||
}
|
||||
selected[remove] = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// we're adding a selection
|
||||
// find first available color
|
||||
var avail = "";
|
||||
for (var i = 0; i < selections.length; i++) {
|
||||
var color = selections[i];
|
||||
if (selected[color] == "") {
|
||||
avail = color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (avail == "") {
|
||||
alert("out of selection colors; go add more");
|
||||
return;
|
||||
}
|
||||
|
||||
// set that as the selection
|
||||
for (var i = 0; i < x.length; i++) {
|
||||
x[i].classList.add(avail);
|
||||
}
|
||||
selected[avail] = c;
|
||||
};
|
||||
|
||||
var irValueClicked = function(event) {
|
||||
irElemClicked(this, event, highlights, highlighted);
|
||||
};
|
||||
|
||||
var irTreeClicked = function(event) {
|
||||
irElemClicked(this, event, outlines, outlined);
|
||||
};
|
||||
|
||||
var irValues = document.getElementsByClassName("ir-node");
|
||||
for (var i = 0; i < irValues.length; i++) {
|
||||
irValues[i].addEventListener('click', irTreeClicked);
|
||||
}
|
||||
|
||||
var lines = document.getElementsByClassName("line-number");
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
lines[i].addEventListener('click', irValueClicked);
|
||||
}
|
||||
|
||||
var variableNames = document.getElementsByClassName("variable-name");
|
||||
for (var i = 0; i < variableNames.length; i++) {
|
||||
variableNames[i].addEventListener('click', irValueClicked);
|
||||
}
|
||||
|
||||
function toggler(phase) {
|
||||
return function() {
|
||||
toggle_cell(phase+'-col');
|
||||
toggle_cell(phase+'-exp');
|
||||
const i = expandedDefault.indexOf(phase);
|
||||
if (i !== -1) {
|
||||
expandedDefault.splice(i, 1);
|
||||
} else {
|
||||
expandedDefault.push(phase);
|
||||
}
|
||||
history.pushState({expandedDefault}, "", location.href);
|
||||
};
|
||||
}
|
||||
|
||||
function toggle_cell(id) {
|
||||
var e = document.getElementById(id);
|
||||
if (e.style.display == 'table-cell') {
|
||||
e.style.display = 'none';
|
||||
} else {
|
||||
e.style.display = 'table-cell';
|
||||
}
|
||||
}
|
||||
|
||||
// Go through all columns and collapse needed phases.
|
||||
const td = document.getElementsByTagName("td");
|
||||
for (let i = 0; i < td.length; i++) {
|
||||
const id = td[i].id;
|
||||
const phase = id.substr(0, id.length-4);
|
||||
let show = expandedDefault.indexOf(phase) !== -1
|
||||
|
||||
// If show == false, check to see if this is a combined column (multiple phases).
|
||||
// If combined, check each of the phases to see if they are in our expandedDefaults.
|
||||
// If any are found, that entire combined column gets shown.
|
||||
if (!show) {
|
||||
const combined = phase.split('--+--');
|
||||
const len = combined.length;
|
||||
if (len > 1) {
|
||||
for (let i = 0; i < len; i++) {
|
||||
const num = expandedDefault.indexOf(combined[i]);
|
||||
if (num !== -1) {
|
||||
expandedDefault.splice(num, 1);
|
||||
if (expandedDefault.indexOf(phase) === -1) {
|
||||
expandedDefault.push(phase);
|
||||
show = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id.endsWith("-exp")) {
|
||||
const h2Els = td[i].getElementsByTagName("h2");
|
||||
const len = h2Els.length;
|
||||
if (len > 0) {
|
||||
for (let i = 0; i < len; i++) {
|
||||
h2Els[i].addEventListener('click', toggler(phase));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
td[i].addEventListener('click', toggler(phase));
|
||||
}
|
||||
if (id.endsWith("-col") && show || id.endsWith("-exp") && !show) {
|
||||
td[i].style.display = 'none';
|
||||
continue;
|
||||
}
|
||||
td[i].style.display = 'table-cell';
|
||||
}
|
||||
|
||||
var resizers = document.getElementsByClassName("resizer");
|
||||
for (var i = 0; i < resizers.length; i++) {
|
||||
var resizer = resizers[i];
|
||||
resizer.addEventListener('mousedown', initDrag, false);
|
||||
}
|
||||
};
|
||||
|
||||
var startX, startWidth, resizableCol;
|
||||
|
||||
function initDrag(e) {
|
||||
resizableCol = this.parentElement;
|
||||
startX = e.clientX;
|
||||
startWidth = parseInt(document.defaultView.getComputedStyle(resizableCol).width, 10);
|
||||
document.documentElement.addEventListener('mousemove', doDrag, false);
|
||||
document.documentElement.addEventListener('mouseup', stopDrag, false);
|
||||
}
|
||||
|
||||
function doDrag(e) {
|
||||
resizableCol.style.width = (startWidth + e.clientX - startX) + 'px';
|
||||
}
|
||||
|
||||
function stopDrag(e) {
|
||||
document.documentElement.removeEventListener('mousemove', doDrag, false);
|
||||
document.documentElement.removeEventListener('mouseup', stopDrag, false);
|
||||
}
|
||||
|
||||
function toggle_visibility(id) {
|
||||
var e = document.getElementById(id);
|
||||
if (e.style.display == 'block') {
|
||||
e.style.display = 'none';
|
||||
} else {
|
||||
e.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
document.body.classList.toggle('darkmode');
|
||||
|
||||
// Collect all of the "collapsed" elements and apply dark mode on each collapsed column
|
||||
const collapsedEls = document.getElementsByClassName('collapsed');
|
||||
const len = collapsedEls.length;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
collapsedEls[i].classList.toggle('darkmode');
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
`
|
||||
)
|
||||
104
src/cmd/compile/internal/ir/html_test.go
Normal file
104
src/cmd/compile/internal/ir/html_test.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright 2026 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 ir
|
||||
|
||||
import (
|
||||
"cmd/compile/internal/base"
|
||||
"cmd/compile/internal/types"
|
||||
"cmd/internal/obj"
|
||||
"cmd/internal/src"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTMLWriter(t *testing.T) {
|
||||
// Initialize base.Ctxt to avoid panics
|
||||
base.Ctxt = new(obj.Link)
|
||||
|
||||
// Setup a temporary directory for output
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Mock func
|
||||
fn := &Func{
|
||||
Nname: &Name{
|
||||
sym: &types.Sym{Name: "TestFunc"},
|
||||
},
|
||||
}
|
||||
// Func embeds miniExpr, so we might need to set op if checked
|
||||
fn.op = ODCLFUNC
|
||||
|
||||
// Create HTMLWriter
|
||||
outFile := filepath.Join(tmpDir, "test.html")
|
||||
w := NewHTMLWriter(outFile, fn, "")
|
||||
if w == nil {
|
||||
t.Fatalf("Failed to create HTMLWriter")
|
||||
}
|
||||
|
||||
// Write a phase
|
||||
w.WritePhase("phase1", "Phase 1")
|
||||
|
||||
// Register a file/line
|
||||
posBase := src.NewFileBase("test.go", "test.go")
|
||||
// base.Ctxt.PosTable.Register(posBase) -- Not needed/doesn't exist
|
||||
pos := src.MakePos(posBase, 10, 1)
|
||||
|
||||
// Create a dummy node
|
||||
n := &Name{
|
||||
sym: &types.Sym{Name: "VarX"},
|
||||
Class: PAUTO,
|
||||
}
|
||||
n.op = ONAME
|
||||
n.pos = base.Ctxt.PosTable.XPos(pos)
|
||||
|
||||
// Add another phase which actually dumps something interesting
|
||||
fn.Body = []Node{n}
|
||||
w.WritePhase("phase2", "Phase 2")
|
||||
|
||||
// Test escaping
|
||||
n2 := &Name{
|
||||
sym: &types.Sym{Name: "<Bad>"},
|
||||
Class: PAUTO,
|
||||
}
|
||||
n2.op = ONAME
|
||||
fn.Body = []Node{n2}
|
||||
w.WritePhase("phase3", "Phase 3")
|
||||
|
||||
w.Close()
|
||||
|
||||
// Verify file exists and has content
|
||||
content, err := os.ReadFile(outFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read output file: %v", err)
|
||||
}
|
||||
|
||||
s := string(content)
|
||||
if len(s) == 0 {
|
||||
t.Errorf("Output file is empty")
|
||||
}
|
||||
|
||||
// Check for Expected strings
|
||||
expected := []string{
|
||||
"<html>",
|
||||
"Phase 1",
|
||||
"Phase 2",
|
||||
"Phase 2",
|
||||
"VarX",
|
||||
"NAME",
|
||||
"<Bad>",
|
||||
"resizer",
|
||||
"loc-",
|
||||
"line-number",
|
||||
"sym-",
|
||||
"variable-name",
|
||||
}
|
||||
|
||||
for _, e := range expected {
|
||||
if !strings.Contains(s, e) {
|
||||
t.Errorf("Output missing %q", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue