Source file src/cmd/compile/internal/noder/html.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package noder
     6  
     7  import (
     8  	"cmd/compile/internal/base"
     9  	"cmd/compile/internal/ir"
    10  	"cmd/compile/internal/syntax"
    11  	"cmd/compile/internal/types2"
    12  	"crypto/sha256"
    13  	"encoding/hex"
    14  	"fmt"
    15  	"html"
    16  	"os"
    17  	"path/filepath"
    18  	"reflect"
    19  	"strings"
    20  )
    21  
    22  // An HTMLWriter dumps syntax nodes to multicolumn HTML, similar to what the
    23  // ssa backend does for GOSSAFUNC.
    24  type HTMLWriter struct {
    25  	ir.HTMLWriterBase
    26  
    27  	Decl *syntax.FuncDecl
    28  	pkg  *types2.Package
    29  	file *syntax.File
    30  	info *types2.Info
    31  }
    32  
    33  func NewHTMLWriter(pkg *types2.Package, file *syntax.File, info *types2.Info, path string, decl *syntax.FuncDecl, cfgMask string) *HTMLWriter {
    34  	path = strings.ReplaceAll(path, "/", string(filepath.Separator))
    35  	out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
    36  	if err != nil {
    37  		panic(err)
    38  	}
    39  	reportPath := path
    40  	if !filepath.IsAbs(reportPath) {
    41  		pwd, err := os.Getwd()
    42  		if err != nil {
    43  			panic(err)
    44  		}
    45  		reportPath = filepath.Join(pwd, path)
    46  	}
    47  	h := HTMLWriter{
    48  		pkg:  pkg,
    49  		file: file,
    50  		info: info,
    51  		Decl: decl,
    52  	}
    53  	h.Init(out, reportPath, h.DeclHTML)
    54  	h.start()
    55  	return &h
    56  }
    57  
    58  func (w *HTMLWriter) pkgFuncName() string {
    59  	p := w.pkg.Path()
    60  	if p == "" {
    61  		p = base.Ctxt.Pkgpath
    62  	}
    63  	return p + "." + w.Decl.Name.Value
    64  }
    65  
    66  func (w *HTMLWriter) start() {
    67  	if w == nil {
    68  		return
    69  	}
    70  	escName := html.EscapeString(w.pkgFuncName())
    71  	w.Print("<!DOCTYPE html>")
    72  	w.Print("<html>")
    73  	w.Printf(`<head>
    74  <meta name="generator" content="AST display for %s">
    75  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    76  %s
    77  %s
    78  <title>AST display for %s</title>
    79  </head>`, escName, ir.CSS, ir.JS("checked", "rangefunc"), escName)
    80  	w.Print("<body>")
    81  	w.Print("<h1>")
    82  	w.Print(escName)
    83  	w.Print("</h1>")
    84  	w.Print(`
    85  <a href="#" onclick="toggle_visibility('help');return false;" id="helplink">help</a>
    86  <div id="help">
    87  
    88  <p>
    89  Click anywhere on a node (with "cell" cursor) to outline a node and all of its subtrees.
    90  </p>
    91  <p>
    92  Click on a name (with "crosshair" cursor) to highlight every occurrence of a name.
    93  (Note that all the name nodes are the same node, so those also all outline together).
    94  </p>
    95  <p>
    96  Click on a file, line, or column (with "crosshair" cursor) to highlight positions
    97  in that file, at that file:line, or at that file:line:column, respectively.<br>Inlined
    98  locations are not treated as a single location, but as a sequence of locations that
    99  can be independently highlighted.
   100  </p>
   101  <p>
   102  Click on a ` + ir.DownArrow + ` to collapse a subtree, or on a ` + ir.RightArrow + ` to expand a subtree.
   103  </p>
   104  <p>
   105  Non-tree attributes, like scope and type lookups, are displayed in italics.  Those may
   106  also be clicked to highlight identity relationships within and between phases.
   107  </p>
   108  
   109  </div>
   110  <label for="dark-mode-button" style="margin-left: 15px; cursor: pointer;">darkmode</label>
   111  <input type="checkbox" onclick="toggleDarkMode();" id="dark-mode-button" style="cursor: pointer" />
   112  `)
   113  	w.Print("<table>")
   114  	w.Print("<tr>")
   115  }
   116  
   117  func (w *HTMLWriter) DeclHTML(phase string) func() {
   118  	return func() {
   119  		w.Print("<pre>") // use pre for formatting to preserve indentation
   120  		w.dumpScopeHTML(w.pkg.Scope(), 1, false)
   121  		w.dumpScopeHTML(w.info.Scopes[w.file], 1, false)
   122  		w.dumpNodeHTML(w.Decl, 1, "")
   123  		w.Print("</pre>")
   124  	}
   125  }
   126  
   127  func (h *HTMLWriter) dumpNodesHTML(list []syntax.Node, depth int) {
   128  	if len(list) == 0 {
   129  		h.Print(" <nil>")
   130  		return
   131  	}
   132  
   133  	for _, n := range list {
   134  		h.dumpNodeHTML(n, depth, "")
   135  	}
   136  }
   137  
   138  func isValid(t types2.Type) bool {
   139  	return t != nil && types2.Unalias(t) != types2.Typ[types2.Invalid]
   140  }
   141  
   142  const indentString = ".  "
   143  
   144  func (w *HTMLWriter) indent(n int) {
   145  	w.Print("\n")
   146  	for range n {
   147  		w.Print(indentString)
   148  	}
   149  }
   150  
   151  // indentForToggle prints indentation to w.
   152  func (h *HTMLWriter) indentForToggle(depth int, hasChildren bool) {
   153  	h.Print("\n")
   154  	if depth == 0 {
   155  		return
   156  	}
   157  	for range depth - 1 {
   158  		h.Print(indentString)
   159  	}
   160  	if hasChildren {
   161  		// Remove 2 spaces, which have similar rendered width to
   162  		// leading ir.DownArrow and trailing space.
   163  		h.Print(indentString[:len(indentString)-2])
   164  	} else {
   165  		h.Print(indentString)
   166  	}
   167  }
   168  
   169  // dumpScopeHTML writes a string representation of the scope to w,
   170  // with the scope elements sorted by name.
   171  // The level of indentation is controlled by n >= 0, with
   172  // n == 0 for no indentation.
   173  // If recurse is set, it also writes nested (children) scopes.
   174  func (h *HTMLWriter) dumpScopeHTML(s *types2.Scope, depth int, recur bool) {
   175  	hasChildren := true // TODO detect empty scopes
   176  	h.indentForToggle(depth, hasChildren)
   177  	if hasChildren {
   178  		h.Printf("<span class=\"n%d scope\">", h.CanonId(s))
   179  		defer h.Printf("</span>")
   180  		// NOTE TRAILING SPACE after </span>! See indentForToggle above.
   181  		h.Print(`<span class="toggle" onclick="toggle_node(this)">` + ir.DownArrow + `</span> `)
   182  	}
   183  	h.Printf("scope %s %p", html.EscapeString(s.Comment()), s)
   184  
   185  	if hasChildren {
   186  		h.Print(`<span class="node-body">`)
   187  		defer h.Print(`</span>`)
   188  
   189  		for _, name := range s.Names() {
   190  			obj := s.Lookup(name)
   191  			h.dumpOutlineNodeHTML(depth+1, "", obj)
   192  		}
   193  
   194  		if recur {
   195  			for i := range s.NumChildren() {
   196  				c := s.Child(i)
   197  				h.dumpScopeHTML(c, depth+1, recur)
   198  			}
   199  		}
   200  	}
   201  }
   202  
   203  func (h *HTMLWriter) dumpOutlineNodeHTML(depth int, pfx string, obj fmt.Stringer) {
   204  	h.indentForToggle(depth, false)
   205  	h.Printf("<span class=\"n%d outline-node\" style=\"font-style: italic;\">%s%s</span>",
   206  		h.CanonId(obj), pfx, html.EscapeString(obj.String()))
   207  }
   208  
   209  func (h *HTMLWriter) dumpNodeHTML(n syntax.Node, depth int, prefix string) {
   210  	hasChildren := h.nodeHasChildren(n)
   211  	h.indentForToggle(depth, hasChildren)
   212  
   213  	if depth > 40 {
   214  		h.Print("...")
   215  		return
   216  	}
   217  
   218  	if n == nil {
   219  		h.Print("NilSyntaxNode")
   220  		return
   221  	}
   222  
   223  	h.Printf("<span class=\"n%d outline-node\">", h.CanonId(n))
   224  	defer h.Printf("</span>")
   225  
   226  	if hasChildren {
   227  		// NOTE TRAILING SPACE after </span>! See indentForToggle above.
   228  		h.Print(`<span class="toggle" onclick="toggle_node(this)">` + ir.DownArrow + `</span> `) // NOTE TRAILING SPACE after </span>!
   229  	}
   230  
   231  	opName := strings.TrimPrefix(fmt.Sprintf("%T", n), "*syntax.")
   232  
   233  	if prefix != "" {
   234  		h.Printf("%s", html.EscapeString(prefix))
   235  	}
   236  
   237  	switch n := n.(type) {
   238  	case *syntax.BasicLit:
   239  		h.Printf("%s-%v", opName, html.EscapeString(n.Value))
   240  		h.dumpNodeHeaderHTML(n)
   241  		return
   242  
   243  	case *syntax.Name:
   244  		name := n.Value
   245  		hash := sha256.Sum256([]byte(name))
   246  		symID := "sym-" + hex.EncodeToString(hash[:6])
   247  		h.Printf("%s-<span class=\"%s variable-name\">%s</span>", opName, symID, html.EscapeString(name))
   248  		h.dumpNodeHeaderHTML(n)
   249  		if hasChildren {
   250  			h.Print(`<span class="node-body">`)
   251  			defer h.Print(`</span>`)
   252  
   253  			if obj := h.info.ObjectOf(n); obj != nil {
   254  				h.dumpOutlineNodeHTML(depth+1, "objectOf=", obj)
   255  			}
   256  			if typ := h.info.TypeOf(n); isValid(typ) {
   257  				h.dumpOutlineNodeHTML(depth+1, "typeOf=", typ)
   258  			}
   259  		}
   260  		return
   261  
   262  	case syntax.Expr:
   263  		h.Printf("%s", opName)
   264  		h.dumpNodeHeaderHTML(n)
   265  		if hasChildren {
   266  			h.Print(`<span class="node-body">`)
   267  			defer h.Print(`</span>`)
   268  
   269  			if typ := h.info.TypeOf(n); isValid(typ) {
   270  				h.dumpOutlineNodeHTML(depth+1, "typeOf=", typ)
   271  			}
   272  		}
   273  
   274  	default:
   275  		h.Printf("%s", opName)
   276  		h.dumpNodeHeaderHTML(n)
   277  		if hasChildren {
   278  			h.Print(`<span class="node-body">`)
   279  			defer h.Print(`</span>`)
   280  		}
   281  	}
   282  
   283  	if s := h.info.Scopes[n]; s != nil && s.Len() > 0 {
   284  		h.dumpScopeHTML(s, depth+1, false)
   285  	}
   286  
   287  	v := reflect.ValueOf(n).Elem()
   288  	t := v.Type()
   289  	nf := t.NumField()
   290  	for i := 0; i < nf; i++ {
   291  		tf := t.Field(i)
   292  		vf := v.Field(i)
   293  		if tf.PkgPath != "" {
   294  			continue
   295  		}
   296  		switch tf.Type.Kind() {
   297  		case reflect.Interface, reflect.Ptr, reflect.Slice:
   298  			if vf.IsNil() {
   299  				continue
   300  			}
   301  		}
   302  		name := strings.TrimSuffix(tf.Name, "_")
   303  
   304  		switch val := vf.Interface().(type) {
   305  		case syntax.Node:
   306  			if name != "" {
   307  				h.dumpNodeHTML(val, depth+1, name+": ")
   308  			} else {
   309  				h.dumpNodeHTML(val, depth+1, "")
   310  			}
   311  		default:
   312  			if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) {
   313  				if vf.Len() == 0 {
   314  					continue
   315  				}
   316  				if name != "" {
   317  					for i := range vf.Len() {
   318  						h.dumpNodeHTML(vf.Index(i).Interface().(syntax.Node), depth+1,
   319  							fmt.Sprintf("%s[%d]: ", name, i))
   320  					}
   321  				} else {
   322  					for i := range vf.Len() {
   323  						h.dumpNodeHTML(vf.Index(i).Interface().(syntax.Node), depth+1, "")
   324  					}
   325  				}
   326  			}
   327  		}
   328  	}
   329  }
   330  
   331  var nodeType = reflect.TypeFor[syntax.Node]()
   332  
   333  func (h *HTMLWriter) nodeHasChildren(n syntax.Node) bool {
   334  	if n == nil {
   335  		return false
   336  	}
   337  	switch x := n.(type) {
   338  	case *syntax.BasicLit:
   339  		return false
   340  	case *syntax.Name:
   341  		return h.info.ObjectOf(x) != nil || isValid(h.info.TypeOf(x))
   342  	case syntax.Expr:
   343  		if isValid(h.info.TypeOf(x)) {
   344  			return true
   345  		}
   346  	}
   347  
   348  	v := reflect.ValueOf(n).Elem()
   349  	t := reflect.TypeOf(n).Elem()
   350  	nf := t.NumField()
   351  	for i := 0; i < nf; i++ {
   352  		tf := t.Field(i)
   353  		vf := v.Field(i)
   354  		if tf.PkgPath != "" {
   355  			continue
   356  		}
   357  		switch tf.Type.Kind() {
   358  		case reflect.Interface, reflect.Ptr, reflect.Slice:
   359  			if vf.IsNil() {
   360  				continue
   361  			}
   362  		}
   363  		switch vf.Interface().(type) {
   364  		case syntax.Node:
   365  			return true
   366  		default:
   367  			if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) && vf.Len() > 0 {
   368  				return true
   369  			}
   370  		}
   371  	}
   372  	return false
   373  }
   374  
   375  func (h *HTMLWriter) dumpNodeHeaderHTML(n syntax.Node) {
   376  	v := reflect.ValueOf(n).Elem()
   377  	t := v.Type()
   378  	nf := t.NumField()
   379  	for i := 0; i < nf; i++ {
   380  		tf := t.Field(i)
   381  		if tf.PkgPath != "" {
   382  			continue
   383  		}
   384  		k := tf.Type.Kind()
   385  		if reflect.Bool <= k && k <= reflect.Complex128 || k == reflect.String {
   386  			name := strings.TrimSuffix(tf.Name, "_")
   387  			if name == "Value" {
   388  				continue
   389  			}
   390  			vf := v.Field(i)
   391  			vfi := vf.Interface()
   392  			if vf.IsZero() {
   393  				continue
   394  			}
   395  			if vfi == true {
   396  				h.Printf(" %s", name)
   397  			} else {
   398  				h.Printf(" %s:%+v", name, html.EscapeString(fmt.Sprint(vf.Interface())))
   399  			}
   400  		}
   401  	}
   402  
   403  	if n.Pos().IsKnown() {
   404  		h.Print(" <span class=\"line-number\">")
   405  		file := n.Pos().Base().Filename()
   406  		if file != "" {
   407  			hash := sha256.Sum256([]byte(file))
   408  			fileID := "loc-" + hex.EncodeToString(hash[:6])
   409  			lineID := fmt.Sprintf("%s-L%d", fileID, n.Pos().Line())
   410  			colID := fmt.Sprintf("%s-C%d", lineID, n.Pos().Col())
   411  
   412  			h.Printf("<span class=\"%s line-number\">%s</span>:", fileID, html.EscapeString(filepath.Base(file)))
   413  			h.Printf("<span class=\"%s %s line-number\">%d</span>:", lineID, fileID, n.Pos().Line())
   414  			h.Printf("<span class=\"%s %s %s line-number\">%d</span>", colID, lineID, fileID, n.Pos().Col())
   415  		} else {
   416  			h.Printf("%v", html.EscapeString(n.Pos().String()))
   417  		}
   418  		h.Print("</span>")
   419  	}
   420  }
   421  

View as plain text