// 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 noder
import (
"cmd/compile/internal/base"
"cmd/compile/internal/ir"
"cmd/compile/internal/syntax"
"cmd/compile/internal/types2"
"crypto/sha256"
"encoding/hex"
"fmt"
"html"
"os"
"path/filepath"
"reflect"
"strings"
)
// An HTMLWriter dumps syntax nodes to multicolumn HTML, similar to what the
// ssa backend does for GOSSAFUNC.
type HTMLWriter struct {
ir.HTMLWriterBase
Decl *syntax.FuncDecl
pkg *types2.Package
file *syntax.File
info *types2.Info
}
func NewHTMLWriter(pkg *types2.Package, file *syntax.File, info *types2.Info, path string, decl *syntax.FuncDecl, 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 {
panic(err)
}
reportPath := path
if !filepath.IsAbs(reportPath) {
pwd, err := os.Getwd()
if err != nil {
panic(err)
}
reportPath = filepath.Join(pwd, path)
}
h := HTMLWriter{
pkg: pkg,
file: file,
info: info,
Decl: decl,
}
h.Init(out, reportPath, h.DeclHTML)
h.start()
return &h
}
func (w *HTMLWriter) pkgFuncName() string {
p := w.pkg.Path()
if p == "" {
p = base.Ctxt.Pkgpath
}
return p + "." + w.Decl.Name.Value
}
func (w *HTMLWriter) start() {
if w == nil {
return
}
escName := html.EscapeString(w.pkgFuncName())
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 ` + ir.DownArrow + ` to collapse a subtree, or on a ` + ir.RightArrow + ` to expand a subtree.
Non-tree attributes, like scope and type lookups, are displayed in italics. Those may
also be clicked to highlight identity relationships within and between phases.
") // use pre for formatting to preserve indentation
w.dumpScopeHTML(w.pkg.Scope(), 1, false)
w.dumpScopeHTML(w.info.Scopes[w.file], 1, false)
w.dumpNodeHTML(w.Decl, 1, "")
w.Print("
")
}
}
func (h *HTMLWriter) dumpNodesHTML(list []syntax.Node, depth int) {
if len(list) == 0 {
h.Print(" ")
return
}
for _, n := range list {
h.dumpNodeHTML(n, depth, "")
}
}
func isValid(t types2.Type) bool {
return t != nil && types2.Unalias(t) != types2.Typ[types2.Invalid]
}
const indentString = ". "
func (w *HTMLWriter) indent(n int) {
w.Print("\n")
for range n {
w.Print(indentString)
}
}
// indentForToggle prints indentation to w.
func (h *HTMLWriter) indentForToggle(depth int, hasChildren bool) {
h.Print("\n")
if depth == 0 {
return
}
for range depth - 1 {
h.Print(indentString)
}
if hasChildren {
// Remove 2 spaces, which have similar rendered width to
// leading ir.DownArrow and trailing space.
h.Print(indentString[:len(indentString)-2])
} else {
h.Print(indentString)
}
}
// dumpScopeHTML writes a string representation of the scope to w,
// with the scope elements sorted by name.
// The level of indentation is controlled by n >= 0, with
// n == 0 for no indentation.
// If recurse is set, it also writes nested (children) scopes.
func (h *HTMLWriter) dumpScopeHTML(s *types2.Scope, depth int, recur bool) {
hasChildren := true // TODO detect empty scopes
h.indentForToggle(depth, hasChildren)
if hasChildren {
h.Printf("", h.CanonId(s))
defer h.Printf("")
// NOTE TRAILING SPACE after ! See indentForToggle above.
h.Print(`` + ir.DownArrow + ` `)
}
h.Printf("scope %s %p", html.EscapeString(s.Comment()), s)
if hasChildren {
h.Print(``)
defer h.Print(``)
for _, name := range s.Names() {
obj := s.Lookup(name)
h.dumpOutlineNodeHTML(depth+1, "", obj)
}
if recur {
for i := range s.NumChildren() {
c := s.Child(i)
h.dumpScopeHTML(c, depth+1, recur)
}
}
}
}
func (h *HTMLWriter) dumpOutlineNodeHTML(depth int, pfx string, obj fmt.Stringer) {
h.indentForToggle(depth, false)
h.Printf("%s%s",
h.CanonId(obj), pfx, html.EscapeString(obj.String()))
}
func (h *HTMLWriter) dumpNodeHTML(n syntax.Node, depth int, prefix string) {
hasChildren := h.nodeHasChildren(n)
h.indentForToggle(depth, hasChildren)
if depth > 40 {
h.Print("...")
return
}
if n == nil {
h.Print("NilSyntaxNode")
return
}
h.Printf("", h.CanonId(n))
defer h.Printf("")
if hasChildren {
// NOTE TRAILING SPACE after ! See indentForToggle above.
h.Print(`` + ir.DownArrow + ` `) // NOTE TRAILING SPACE after !
}
opName := strings.TrimPrefix(fmt.Sprintf("%T", n), "*syntax.")
if prefix != "" {
h.Printf("%s", html.EscapeString(prefix))
}
switch n := n.(type) {
case *syntax.BasicLit:
h.Printf("%s-%v", opName, html.EscapeString(n.Value))
h.dumpNodeHeaderHTML(n)
return
case *syntax.Name:
name := n.Value
hash := sha256.Sum256([]byte(name))
symID := "sym-" + hex.EncodeToString(hash[:6])
h.Printf("%s-%s", opName, symID, html.EscapeString(name))
h.dumpNodeHeaderHTML(n)
if hasChildren {
h.Print(``)
defer h.Print(``)
if obj := h.info.ObjectOf(n); obj != nil {
h.dumpOutlineNodeHTML(depth+1, "objectOf=", obj)
}
if typ := h.info.TypeOf(n); isValid(typ) {
h.dumpOutlineNodeHTML(depth+1, "typeOf=", typ)
}
}
return
case syntax.Expr:
h.Printf("%s", opName)
h.dumpNodeHeaderHTML(n)
if hasChildren {
h.Print(``)
defer h.Print(``)
if typ := h.info.TypeOf(n); isValid(typ) {
h.dumpOutlineNodeHTML(depth+1, "typeOf=", typ)
}
}
default:
h.Printf("%s", opName)
h.dumpNodeHeaderHTML(n)
if hasChildren {
h.Print(``)
defer h.Print(``)
}
}
if s := h.info.Scopes[n]; s != nil && s.Len() > 0 {
h.dumpScopeHTML(s, depth+1, false)
}
v := reflect.ValueOf(n).Elem()
t := v.Type()
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 val := vf.Interface().(type) {
case syntax.Node:
if name != "" {
h.dumpNodeHTML(val, depth+1, name+": ")
} else {
h.dumpNodeHTML(val, depth+1, "")
}
default:
if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) {
if vf.Len() == 0 {
continue
}
if name != "" {
for i := range vf.Len() {
h.dumpNodeHTML(vf.Index(i).Interface().(syntax.Node), depth+1,
fmt.Sprintf("%s[%d]: ", name, i))
}
} else {
for i := range vf.Len() {
h.dumpNodeHTML(vf.Index(i).Interface().(syntax.Node), depth+1, "")
}
}
}
}
}
}
var nodeType = reflect.TypeFor[syntax.Node]()
func (h *HTMLWriter) nodeHasChildren(n syntax.Node) bool {
if n == nil {
return false
}
switch x := n.(type) {
case *syntax.BasicLit:
return false
case *syntax.Name:
return h.info.ObjectOf(x) != nil || isValid(h.info.TypeOf(x))
case syntax.Expr:
if isValid(h.info.TypeOf(x)) {
return true
}
}
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 vf.Interface().(type) {
case syntax.Node:
return true
default:
if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) && vf.Len() > 0 {
return true
}
}
}
return false
}
func (h *HTMLWriter) dumpNodeHeaderHTML(n syntax.Node) {
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 || k == reflect.String {
name := strings.TrimSuffix(tf.Name, "_")
if name == "Value" {
continue
}
vf := v.Field(i)
vfi := vf.Interface()
if vf.IsZero() {
continue
}
if vfi == true {
h.Printf(" %s", name)
} else {
h.Printf(" %s:%+v", name, html.EscapeString(fmt.Sprint(vf.Interface())))
}
}
}
if n.Pos().IsKnown() {
h.Print(" ")
file := n.Pos().Base().Filename()
if file != "" {
hash := sha256.Sum256([]byte(file))
fileID := "loc-" + hex.EncodeToString(hash[:6])
lineID := fmt.Sprintf("%s-L%d", fileID, n.Pos().Line())
colID := fmt.Sprintf("%s-C%d", lineID, n.Pos().Col())
h.Printf("%s:", fileID, html.EscapeString(filepath.Base(file)))
h.Printf("%d:", lineID, fileID, n.Pos().Line())
h.Printf("%d", colID, lineID, fileID, n.Pos().Col())
} else {
h.Printf("%v", html.EscapeString(n.Pos().String()))
}
h.Print("")
}
}