// 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(` %s %s AST display for %s `, escName, ir.CSS, ir.JS("checked", "rangefunc"), escName) w.Print("") w.Print("

") w.Print(escName) w.Print("

") w.Print(` help

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.

`) w.Print("") w.Print("") } func (w *HTMLWriter) DeclHTML(phase string) func() { return func() { w.Print("
") // 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("") } }