| // 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("<!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, ir.CSS, ir.JS("checked", "rangefunc"), escName) | |
| w.Print("<body>") | |
| w.Print("<h1>") | |
| w.Print(escName) | |
| 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> | |
| <p> | |
| Click on a ` + ir.DownArrow + ` to collapse a subtree, or on a ` + ir.RightArrow + ` to expand a subtree. | |
| </p> | |
| <p> | |
| 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. | |
| </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) DeclHTML(phase string) func() { | |
| return func() { | |
| w.Print("<pre>") // 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("</pre>") | |
| } | |
| } | |
| func (h *HTMLWriter) dumpNodesHTML(list []syntax.Node, depth int) { | |
| if len(list) == 0 { | |
| h.Print(" <nil>") | |
| 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("<span class=\"n%d scope\">", h.CanonId(s)) | |
| defer h.Printf("</span>") | |
| // NOTE TRAILING SPACE after </span>! See indentForToggle above. | |
| h.Print(`<span class="toggle" onclick="toggle_node(this)">` + ir.DownArrow + `</span> `) | |
| } | |
| h.Printf("scope %s %p", html.EscapeString(s.Comment()), s) | |
| if hasChildren { | |
| h.Print(`<span class="node-body">`) | |
| defer h.Print(`</span>`) | |
| 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("<span class=\"n%d outline-node\" style=\"font-style: italic;\">%s%s</span>", | |
| 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("<span class=\"n%d outline-node\">", h.CanonId(n)) | |
| defer h.Printf("</span>") | |
| if hasChildren { | |
| // NOTE TRAILING SPACE after </span>! See indentForToggle above. | |
| h.Print(`<span class="toggle" onclick="toggle_node(this)">` + ir.DownArrow + `</span> `) // NOTE TRAILING SPACE after </span>! | |
| } | |
| 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-<span class=\"%s variable-name\">%s</span>", opName, symID, html.EscapeString(name)) | |
| h.dumpNodeHeaderHTML(n) | |
| if hasChildren { | |
| h.Print(`<span class="node-body">`) | |
| defer h.Print(`</span>`) | |
| 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(`<span class="node-body">`) | |
| defer h.Print(`</span>`) | |
| 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(`<span class="node-body">`) | |
| defer h.Print(`</span>`) | |
| } | |
| } | |
| 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(" <span class=\"line-number\">") | |
| 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("<span class=\"%s line-number\">%s</span>:", fileID, html.EscapeString(filepath.Base(file))) | |
| h.Printf("<span class=\"%s %s line-number\">%d</span>:", lineID, fileID, n.Pos().Line()) | |
| h.Printf("<span class=\"%s %s %s line-number\">%d</span>", colID, lineID, fileID, n.Pos().Col()) | |
| } else { | |
| h.Printf("%v", html.EscapeString(n.Pos().String())) | |
| } | |
| h.Print("</span>") | |
| } | |
| } |