// 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...)
}
const (
RIGHT_ARROW = "\u25BA" // click-to-open (is closed)
DOWN_ARROW = "\u25BC" // click-to-close (is open)
)
func (w *HTMLWriter) start() {
if w == nil {
return
}
escName := html.EscapeString(PkgFuncName(w.Func))
w.Print("")
w.Print("")
w.Printf(`
Click anywhere on a node (with "cell" cursor) to outline a node and all of its subtrees.
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).
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. Inlined
locations are not treated as a single location, but as a sequence of locations that
can be independently highlighted.
Click on a ` + DOWN_ARROW + ` to collapse a subtree, or on a ` + RIGHT_ARROW + ` to expand a subtree.
`)
w.Print("
")
w.Print("
")
}
func (w *HTMLWriter) Close() {
if w == nil {
return
}
w.Print("
")
w.Print("
")
w.Print("")
w.Print("\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("
%v
", id, phase)
if class == "" {
w.Printf("
", id)
} else {
w.Printf("
", id, class)
}
for _, title := range titles {
w.Print("
" + title + "
")
}
writeContent()
w.Print("")
w.Print("
\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("
") // use pre for formatting to preserve indentation
w.dumpNodesHTML(w.Func.Body, 1)
w.Print("
")
}
}
func (h *HTMLWriter) dumpNodesHTML(list Nodes, depth int) {
if len(list) == 0 {
h.Print(" ")
return
}
for _, n := range list {
h.dumpNodeHTML(n, depth)
}
}
// indent prints indentation to w.
func (h *HTMLWriter) indentForToggle(depth int, hasChildren bool) {
h.Print("\n")
if depth == 0 {
return
}
for i := 0; i < depth-1; i++ {
h.Print(". ")
}
if hasChildren {
h.Print(". ")
} else {
h.Print(". ")
}
}
func (h *HTMLWriter) dumpNodeHTML(n Node, depth int) {
hasChildren := nodeHasChildren(n)
h.indentForToggle(depth, hasChildren)
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("", h.canonId(n))
defer h.Printf("")
if hasChildren {
h.Print(`` + DOWN_ARROW + ` `) // NOTE TRAILING SPACE after !
}
if len(n.Init()) != 0 {
h.Print(``)
h.Printf("%+v-init", n.Op())
h.dumpNodesHTML(n.Init(), depth+1)
h.indent(depth)
h.Print(``)
}
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-%+v", 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)
if hasChildren {
h.Print(``)
defer h.Print(``)
}
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
}
if hasChildren {
h.Print(``)
defer h.Print(``)
}
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 nodeHasChildren(n Node) bool {
if n == nil {
return false
}
if len(n.Init()) != 0 {
return true
}
switch n.Op() {
case OLITERAL, ONAME, ONONAME, OTYPE:
return false
case ODCLFUNC:
n := n.(*Func)
return len(n.Dcl) > 0 || len(n.ClosureVars) > 0 || len(n.Body) > 0
}
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
}
}
switch val := vf.Interface().(type) {
case Node:
return true
case Nodes:
if len(val) > 0 {
return true
}
default:
if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) {
if vf.Len() > 0 {
return true
}
}
}
}
return false
}
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(" ")
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("%s:", fileID, html.EscapeString(filepath.Base(file)))
// Line part: triggers lineID (and fileID via class list)
h.Printf("%d:", lineID, fileID, pos.Line())
// Col part: triggers colID (and lineID, fileID)
h.Printf("%d", colID, lineID, fileID, pos.Col())
})
h.Print("")
}
}
const (
CSS = `
`
JS = `
`
)