Source file src/cmd/compile/internal/ir/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 ir
     6  
     7  import (
     8  	"bufio"
     9  	"cmd/compile/internal/base"
    10  	"cmd/compile/internal/types"
    11  	"cmd/internal/src"
    12  	"crypto/sha256"
    13  	"encoding/hex"
    14  	"fmt"
    15  	"html"
    16  	"io"
    17  	"os"
    18  	"path/filepath"
    19  	"reflect"
    20  	"regexp"
    21  	"strings"
    22  )
    23  
    24  // An HTMLWriter dumps IR to multicolumn HTML, similar to what the
    25  // ssa backend does for GOSSAFUNC.  This is not the format used for
    26  // the ast column in GOSSAFUNC output.
    27  type HTMLWriter struct {
    28  	HTMLWriterBase
    29  	Func *Func
    30  }
    31  
    32  type HTMLWriterBase struct {
    33  	w             *BufferedWriterCloser
    34  	canonIdMap    map[any]int
    35  	prevCanonId   int
    36  	path          string
    37  	prevHash      []byte
    38  	pendingPhases []string
    39  	pendingTitles []string
    40  	doDump        func(string) func()
    41  }
    42  
    43  func (h *HTMLWriterBase) Init(out io.WriteCloser, reportPath string, doDump func(string) func()) {
    44  	h.w = NewBufferedWriterCloser(out)
    45  	h.canonIdMap = make(map[any]int)
    46  	h.path = reportPath
    47  	h.doDump = doDump
    48  }
    49  
    50  // BufferedWriterCloser is here to help avoid pre-buffering the whole
    51  // rendered HTML in memory, which can cause problems for large inputs.
    52  type BufferedWriterCloser struct {
    53  	file io.Closer
    54  	w    *bufio.Writer
    55  }
    56  
    57  func (b *BufferedWriterCloser) Write(p []byte) (n int, err error) {
    58  	return b.w.Write(p)
    59  }
    60  
    61  func (b *BufferedWriterCloser) Close() error {
    62  	b.w.Flush()
    63  	b.w = nil
    64  	return b.file.Close()
    65  }
    66  
    67  func NewBufferedWriterCloser(f io.WriteCloser) *BufferedWriterCloser {
    68  	return &BufferedWriterCloser{file: f, w: bufio.NewWriter(f)}
    69  }
    70  
    71  func NewHTMLWriter(path string, f *Func, cfgMask string) *HTMLWriter {
    72  	path = strings.ReplaceAll(path, "/", string(filepath.Separator))
    73  	out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
    74  	if err != nil {
    75  		base.Fatalf("%v", err)
    76  	}
    77  	reportPath := path
    78  	if !filepath.IsAbs(reportPath) {
    79  		pwd, err := os.Getwd()
    80  		if err != nil {
    81  			base.Fatalf("%v", err)
    82  		}
    83  		reportPath = filepath.Join(pwd, path)
    84  	}
    85  	h := HTMLWriter{
    86  		Func: f,
    87  	}
    88  	h.Init(out, reportPath, h.FuncHTML)
    89  	h.start()
    90  	return &h
    91  }
    92  
    93  func (h *HTMLWriterBase) Path() string {
    94  	return h.path
    95  }
    96  
    97  // CanonId assigns indices to nodes based on pointer identity.
    98  // this helps ensure that output html files don't gratuitously
    99  // differ from run to run.
   100  func (h *HTMLWriterBase) CanonId(n any) int {
   101  	if id := h.canonIdMap[n]; id > 0 {
   102  		return id
   103  	}
   104  	h.prevCanonId++
   105  	h.canonIdMap[n] = h.prevCanonId
   106  	return h.prevCanonId
   107  }
   108  
   109  // Fatalf reports an error and exits.
   110  func (w *HTMLWriterBase) Fatalf(msg string, args ...any) {
   111  	base.FatalfAt(src.NoXPos, msg, args...)
   112  }
   113  
   114  const (
   115  	RightArrow = "►" // \u25BA click-to-open (is closed)
   116  	DownArrow  = "▼" // \u25BC click-to-close (is open)
   117  )
   118  
   119  func (w *HTMLWriter) start() {
   120  	if w == nil {
   121  		return
   122  	}
   123  	escName := html.EscapeString(PkgFuncName(w.Func))
   124  	w.Print("<!DOCTYPE html>")
   125  	w.Print("<html>")
   126  	w.Printf(`<head>
   127  <meta name="generator" content="AST display for %s">
   128  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
   129  %s
   130  %s
   131  <title>AST display for %s</title>
   132  </head>`, escName, CSS, JS("bloop", "loopvar", "escape", "slice", "walk"), escName)
   133  	w.Print("<body>")
   134  	w.Print("<h1>")
   135  	w.Print(html.EscapeString(w.Func.Sym().Name))
   136  	w.Print("</h1>")
   137  	w.Print(`
   138  <a href="#" onclick="toggle_visibility('help');return false;" id="helplink">help</a>
   139  <div id="help">
   140  
   141  <p>
   142  Click anywhere on a node (with "cell" cursor) to outline a node and all of its subtrees.
   143  </p>
   144  <p>
   145  Click on a name (with "crosshair" cursor) to highlight every occurrence of a name.
   146  (Note that all the name nodes are the same node, so those also all outline together).
   147  </p>
   148  <p>
   149  Click on a file, line, or column (with "crosshair" cursor) to highlight positions
   150  in that file, at that file:line, or at that file:line:column, respectively.<br>Inlined
   151  locations are not treated as a single location, but as a sequence of locations that
   152  can be independently highlighted.
   153  </p>
   154  <p>
   155  Click on a ` + DownArrow + ` to collapse a subtree, or on a ` + RightArrow + ` to expand a subtree.
   156  </p>
   157  
   158  
   159  </div>
   160  <label for="dark-mode-button" style="margin-left: 15px; cursor: pointer;">darkmode</label>
   161  <input type="checkbox" onclick="toggleDarkMode();" id="dark-mode-button" style="cursor: pointer" />
   162  `)
   163  	w.Print("<table>")
   164  	w.Print("<tr>")
   165  }
   166  
   167  func (w *HTMLWriterBase) Close(format string, args ...any) {
   168  	if w == nil {
   169  		return
   170  	}
   171  	w.Print("</tr>")
   172  	w.Print("</table>")
   173  	w.Print("</body>")
   174  	w.Print("</html>\n")
   175  	w.w.Close()
   176  	fmt.Fprintf(os.Stderr, format, args...)
   177  }
   178  
   179  // WritePhase writes f in a column headed by title.
   180  // phase is used for collapsing columns and should be unique across the table.
   181  func (w *HTMLWriterBase) WritePhase(phase, title string) {
   182  	if w == nil {
   183  		return // avoid generating HTML just to discard it
   184  	}
   185  	w.pendingPhases = append(w.pendingPhases, phase)
   186  	w.pendingTitles = append(w.pendingTitles, title)
   187  	w.flushPhases()
   188  }
   189  
   190  // flushPhases collects any pending phases and titles, writes them to the html, and resets the pending slices.
   191  func (w *HTMLWriterBase) flushPhases() {
   192  	phaseLen := len(w.pendingPhases)
   193  	if phaseLen == 0 {
   194  		return
   195  	}
   196  	phases := strings.Join(w.pendingPhases, "  +  ")
   197  	w.WriteMultiTitleColumn(
   198  		phases,
   199  		w.pendingTitles,
   200  		"allow-x-scroll",
   201  		w.doDump(w.pendingPhases[phaseLen-1]),
   202  	)
   203  	w.pendingPhases = w.pendingPhases[:0]
   204  	w.pendingTitles = w.pendingTitles[:0]
   205  }
   206  
   207  func (w *HTMLWriterBase) WriteMultiTitleColumn(phase string, titles []string, class string, writeContent func()) {
   208  	if w == nil {
   209  		return
   210  	}
   211  	id := strings.ReplaceAll(phase, " ", "-")
   212  	// collapsed column
   213  	w.Printf("<td id=\"%v-col\" class=\"collapsed\"><div>%v</div></td>", id, phase)
   214  
   215  	if class == "" {
   216  		w.Printf("<td id=\"%v-exp\">", id)
   217  	} else {
   218  		w.Printf("<td id=\"%v-exp\" class=\"%v\">", id, class)
   219  	}
   220  	for _, title := range titles {
   221  		w.Print("<h2>" + title + "</h2>")
   222  	}
   223  	writeContent()
   224  	w.Print("<div class=\"resizer\"></div>")
   225  	w.Print("</td>\n")
   226  }
   227  
   228  func (w *HTMLWriterBase) Printf(msg string, v ...any) {
   229  	if _, err := fmt.Fprintf(w.w, msg, v...); err != nil {
   230  		w.Fatalf("%v", err)
   231  	}
   232  }
   233  
   234  func (w *HTMLWriterBase) Print(s string) {
   235  	if _, err := fmt.Fprint(w.w, s); err != nil {
   236  		w.Fatalf("%v", err)
   237  	}
   238  }
   239  
   240  func (w *HTMLWriterBase) indent(n int) {
   241  	indent(w.w, n)
   242  }
   243  
   244  func (w *HTMLWriter) FuncHTML(phase string) func() {
   245  	return func() {
   246  		w.Print("<pre>") // use pre for formatting to preserve indentation
   247  		w.dumpNodesHTML(w.Func.Body, 1)
   248  		w.Print("</pre>")
   249  	}
   250  }
   251  
   252  func (h *HTMLWriter) dumpNodesHTML(list Nodes, depth int) {
   253  	if len(list) == 0 {
   254  		h.Print(" <nil>")
   255  		return
   256  	}
   257  
   258  	for _, n := range list {
   259  		h.dumpNodeHTML(n, depth)
   260  	}
   261  }
   262  
   263  const indentString = ".   "
   264  
   265  // indent prints indentation to w.
   266  func (h *HTMLWriterBase) indentForToggle(depth int, hasChildren bool) {
   267  	h.Print("\n")
   268  	if depth == 0 {
   269  		return
   270  	}
   271  	for i := 0; i < depth-1; i++ {
   272  		h.Print(indentString)
   273  	}
   274  	if hasChildren {
   275  		// Remove 2 spaces, which have similar rendered width to
   276  		// leading ir.DownArrow and trailing space.
   277  		h.Print(indentString[:len(indentString)-2])
   278  	} else {
   279  		h.Print(indentString)
   280  	}
   281  }
   282  
   283  func (h *HTMLWriter) dumpNodeHTML(n Node, depth int) {
   284  	hasChildren := nodeHasChildren(n)
   285  	h.indentForToggle(depth, hasChildren)
   286  
   287  	if depth > 40 {
   288  		h.Print("...")
   289  		return
   290  	}
   291  
   292  	if n == nil {
   293  		h.Print("NilIrNode")
   294  		return
   295  	}
   296  
   297  	// For HTML, we want to wrap the node and its details in a span that can be highlighted
   298  	// across all occurrences of the span in all columns, so it has to be linked to the node ID,
   299  	// which is its address. Canonicalize the address to a counter so that repeated compiler
   300  	// runs yield the same html.
   301  	//
   302  	// JS Equivalence logic:
   303  	//   var c = elem.classList.item(0);
   304  	//   var x = document.getElementsByClassName(c);
   305  	//
   306  	// Tag each class with its canonicalized index.
   307  
   308  	h.Printf("<span class=\"n%d outline-node\">", h.CanonId(n))
   309  	defer h.Printf("</span>")
   310  
   311  	if hasChildren {
   312  		h.Print(`<span class="toggle" onclick="toggle_node(this)">` + DownArrow + `</span> `) // NOTE TRAILING SPACE after </span>!
   313  	}
   314  
   315  	if len(n.Init()) != 0 {
   316  		h.Print(`<span class="node-body">`)
   317  		h.Printf("%+v-init", n.Op())
   318  		h.dumpNodesHTML(n.Init(), depth+1)
   319  		h.indent(depth)
   320  		h.Print(`</span>`)
   321  	}
   322  
   323  	switch n.Op() {
   324  	default:
   325  		h.Printf("%+v", n.Op())
   326  		h.dumpNodeHeaderHTML(n)
   327  
   328  	case OLITERAL:
   329  		h.Printf("%+v-%v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Val())))
   330  		h.dumpNodeHeaderHTML(n)
   331  		return
   332  
   333  	case ONAME, ONONAME:
   334  		if n.Sym() != nil {
   335  			// Name highlighting:
   336  			// Create a hash for the symbol name to use as a class
   337  			// We use the same irValueClicked logic which uses the first class as the identifier
   338  			name := fmt.Sprintf("%v", n.Sym())
   339  			hash := sha256.Sum256([]byte(name))
   340  			symID := "sym-" + hex.EncodeToString(hash[:6])
   341  			h.Printf("%+v-<span class=\"%s variable-name\">%+v</span>", n.Op(), symID, html.EscapeString(name))
   342  		} else {
   343  			h.Printf("%+v", n.Op())
   344  		}
   345  		h.dumpNodeHeaderHTML(n)
   346  		return
   347  
   348  	case OLINKSYMOFFSET:
   349  		n := n.(*LinksymOffsetExpr)
   350  		h.Printf("%+v-%v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Linksym)))
   351  		if n.Offset_ != 0 {
   352  			h.Printf("%+v", n.Offset_)
   353  		}
   354  		h.dumpNodeHeaderHTML(n)
   355  
   356  	case OASOP:
   357  		n := n.(*AssignOpStmt)
   358  		h.Printf("%+v-%+v", n.Op(), n.AsOp)
   359  		h.dumpNodeHeaderHTML(n)
   360  
   361  	case OTYPE:
   362  		h.Printf("%+v %+v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Sym())))
   363  		h.dumpNodeHeaderHTML(n)
   364  		return
   365  
   366  	case OCLOSURE:
   367  		h.Printf("%+v", n.Op())
   368  		h.dumpNodeHeaderHTML(n)
   369  
   370  	case ODCLFUNC:
   371  		n := n.(*Func)
   372  		h.Printf("%+v", n.Op())
   373  		h.dumpNodeHeaderHTML(n)
   374  		if hasChildren {
   375  			h.Print(`<span class="node-body">`)
   376  			defer h.Print(`</span>`)
   377  		}
   378  		fn := n
   379  		if len(fn.Dcl) > 0 {
   380  			h.indent(depth)
   381  			h.Printf("%+v-Dcl", n.Op())
   382  			for _, dcl := range n.Dcl {
   383  				h.dumpNodeHTML(dcl, depth+1)
   384  			}
   385  		}
   386  		if len(fn.ClosureVars) > 0 {
   387  			h.indent(depth)
   388  			h.Printf("%+v-ClosureVars", n.Op())
   389  			for _, cv := range fn.ClosureVars {
   390  				h.dumpNodeHTML(cv, depth+1)
   391  			}
   392  		}
   393  		if len(fn.Body) > 0 {
   394  			h.indent(depth)
   395  			h.Printf("%+v-body", n.Op())
   396  			h.dumpNodesHTML(fn.Body, depth+1)
   397  		}
   398  		return
   399  	}
   400  	if hasChildren {
   401  		h.Print(`<span class="node-body">`)
   402  		defer h.Print(`</span>`)
   403  	}
   404  
   405  	v := reflect.ValueOf(n).Elem()
   406  	t := reflect.TypeOf(n).Elem()
   407  	nf := t.NumField()
   408  	for i := 0; i < nf; i++ {
   409  		tf := t.Field(i)
   410  		vf := v.Field(i)
   411  		if tf.PkgPath != "" {
   412  			continue
   413  		}
   414  		switch tf.Type.Kind() {
   415  		case reflect.Interface, reflect.Ptr, reflect.Slice:
   416  			if vf.IsNil() {
   417  				continue
   418  			}
   419  		}
   420  		name := strings.TrimSuffix(tf.Name, "_")
   421  		switch name {
   422  		case "X", "Y", "Index", "Chan", "Value", "Call":
   423  			name = ""
   424  		}
   425  		switch val := vf.Interface().(type) {
   426  		case Node:
   427  			if name != "" {
   428  				h.indent(depth)
   429  				h.Printf("%+v-%s", n.Op(), name)
   430  			}
   431  			h.dumpNodeHTML(val, depth+1)
   432  		case Nodes:
   433  			if len(val) == 0 {
   434  				continue
   435  			}
   436  			if name != "" {
   437  				h.indent(depth)
   438  				h.Printf("%+v-%s", n.Op(), name)
   439  			}
   440  			h.dumpNodesHTML(val, depth+1)
   441  		default:
   442  			if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) {
   443  				if vf.Len() == 0 {
   444  					continue
   445  				}
   446  				if name != "" {
   447  					h.indent(depth)
   448  					h.Printf("%+v-%s", n.Op(), name)
   449  				}
   450  				for i, n := 0, vf.Len(); i < n; i++ {
   451  					h.dumpNodeHTML(vf.Index(i).Interface().(Node), depth+1)
   452  				}
   453  			}
   454  		}
   455  	}
   456  }
   457  
   458  func nodeHasChildren(n Node) bool {
   459  	if n == nil {
   460  		return false
   461  	}
   462  	if len(n.Init()) != 0 {
   463  		return true
   464  	}
   465  	switch n.Op() {
   466  	case OLITERAL, ONAME, ONONAME, OTYPE:
   467  		return false
   468  	case ODCLFUNC:
   469  		n := n.(*Func)
   470  		return len(n.Dcl) > 0 || len(n.ClosureVars) > 0 || len(n.Body) > 0
   471  	}
   472  
   473  	v := reflect.ValueOf(n).Elem()
   474  	t := reflect.TypeOf(n).Elem()
   475  	nf := t.NumField()
   476  	for i := 0; i < nf; i++ {
   477  		tf := t.Field(i)
   478  		vf := v.Field(i)
   479  		if tf.PkgPath != "" {
   480  			continue
   481  		}
   482  		switch tf.Type.Kind() {
   483  		case reflect.Interface, reflect.Ptr, reflect.Slice:
   484  			if vf.IsNil() {
   485  				continue
   486  			}
   487  		}
   488  		switch val := vf.Interface().(type) {
   489  		case Node:
   490  			return true
   491  		case Nodes:
   492  			if len(val) > 0 {
   493  				return true
   494  			}
   495  		default:
   496  			if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) {
   497  				if vf.Len() > 0 {
   498  					return true
   499  				}
   500  			}
   501  		}
   502  	}
   503  	return false
   504  }
   505  
   506  func (h *HTMLWriter) dumpNodeHeaderHTML(n Node) {
   507  	// print pointer to be able to see identical nodes
   508  	if base.Debug.DumpPtrs != 0 {
   509  		h.Printf(" p(%p)", n)
   510  	}
   511  
   512  	if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Defn != nil {
   513  		h.Printf(" defn(%p)", n.Name().Defn)
   514  	}
   515  
   516  	if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Curfn != nil {
   517  		h.Printf(" curfn(%p)", n.Name().Curfn)
   518  	}
   519  	if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Outer != nil {
   520  		h.Printf(" outer(%p)", n.Name().Outer)
   521  	}
   522  
   523  	if EscFmt != nil {
   524  		if esc := EscFmt(n); esc != "" {
   525  			h.Printf(" %s", html.EscapeString(esc))
   526  		}
   527  	}
   528  
   529  	if n.Sym() != nil && n.Op() != ONAME && n.Op() != ONONAME && n.Op() != OTYPE {
   530  		h.Printf(" %+v", html.EscapeString(fmt.Sprintf("%v", n.Sym())))
   531  	}
   532  
   533  	v := reflect.ValueOf(n).Elem()
   534  	t := v.Type()
   535  	nf := t.NumField()
   536  	for i := 0; i < nf; i++ {
   537  		tf := t.Field(i)
   538  		if tf.PkgPath != "" {
   539  			continue
   540  		}
   541  		k := tf.Type.Kind()
   542  		if reflect.Bool <= k && k <= reflect.Complex128 {
   543  			name := strings.TrimSuffix(tf.Name, "_")
   544  			vf := v.Field(i)
   545  			vfi := vf.Interface()
   546  			if name == "Offset" && vfi == types.BADWIDTH || name != "Offset" && vf.IsZero() {
   547  				continue
   548  			}
   549  			if vfi == true {
   550  				h.Printf(" %s", name)
   551  			} else {
   552  				h.Printf(" %s:%+v", name, html.EscapeString(fmt.Sprintf("%v", vf.Interface())))
   553  			}
   554  		}
   555  	}
   556  
   557  	v = reflect.ValueOf(n)
   558  	t = v.Type()
   559  	nm := t.NumMethod()
   560  	for i := 0; i < nm; i++ {
   561  		tm := t.Method(i)
   562  		if tm.PkgPath != "" {
   563  			continue
   564  		}
   565  		m := v.Method(i)
   566  		mt := m.Type()
   567  		if mt.NumIn() == 0 && mt.NumOut() == 1 && mt.Out(0).Kind() == reflect.Bool {
   568  			func() {
   569  				defer func() { recover() }()
   570  				if m.Call(nil)[0].Bool() {
   571  					name := strings.TrimSuffix(tm.Name, "_")
   572  					h.Printf(" %s", name)
   573  				}
   574  			}()
   575  		}
   576  	}
   577  
   578  	if n.Op() == OCLOSURE {
   579  		n := n.(*ClosureExpr)
   580  		if fn := n.Func; fn != nil && fn.Nname.Sym() != nil {
   581  			h.Printf(" fnName(%+v)", html.EscapeString(fmt.Sprintf("%v", fn.Nname.Sym())))
   582  		}
   583  	}
   584  
   585  	if n.Type() != nil {
   586  		if n.Op() == OTYPE {
   587  			h.Printf(" type")
   588  		}
   589  		h.Printf(" %+v", html.EscapeString(fmt.Sprintf("%v", n.Type())))
   590  	}
   591  	if n.Typecheck() != 0 {
   592  		h.Printf(" tc(%d)", n.Typecheck())
   593  	}
   594  
   595  	if n.Pos().IsKnown() {
   596  		h.Print(" <span class=\"line-number\">")
   597  		switch n.Pos().IsStmt() {
   598  		case src.PosNotStmt:
   599  			h.Print("_")
   600  		case src.PosIsStmt:
   601  			h.Print("+")
   602  		}
   603  		sep := ""
   604  		base.Ctxt.AllPos(n.Pos(), func(pos src.Pos) {
   605  			h.Print(sep)
   606  			sep = " "
   607  			// Hierarchical highlighting:
   608  			// Click file -> highlight all ranges in this file
   609  			// Click line -> highlight all ranges at this line (in this file)
   610  			// Click col  -> highlight this specific range
   611  
   612  			file := pos.Filename()
   613  			// Create a hash for the filename to use as a class
   614  			hash := sha256.Sum256([]byte(file))
   615  			fileID := "loc-" + hex.EncodeToString(hash[:6])
   616  			lineID := fmt.Sprintf("%s-L%d", fileID, pos.Line())
   617  			colID := fmt.Sprintf("%s-C%d", lineID, pos.Col())
   618  
   619  			// File part: triggers fileID
   620  			h.Printf("<span class=\"%s line-number\">%s</span>:", fileID, html.EscapeString(filepath.Base(file)))
   621  			// Line part: triggers lineID (and fileID via class list)
   622  			h.Printf("<span class=\"%s %s line-number\">%d</span>:", lineID, fileID, pos.Line())
   623  			// Col part: triggers colID (and lineID, fileID)
   624  			h.Printf("<span class=\"%s %s %s line-number\">%d</span>", colID, lineID, fileID, pos.Col())
   625  		})
   626  		h.Print("</span>")
   627  	}
   628  }
   629  
   630  const CSS = `<style>
   631  
   632  body {
   633      font-size: 14px;
   634      font-family: Arial, sans-serif;
   635  }
   636  
   637  h1 {
   638      font-size: 18px;
   639      display: inline-block;
   640      margin: 0 1em .5em 0;
   641  }
   642  
   643  #helplink {
   644      display: inline-block;
   645  }
   646  
   647  #help {
   648      display: none;
   649  }
   650  
   651  table {
   652      border: 1px solid black;
   653      table-layout: fixed;
   654      width: 300px;
   655  }
   656  
   657  th, td {
   658      border: 1px solid black;
   659      overflow: hidden;
   660      width: 400px;
   661      vertical-align: top;
   662      padding: 5px;
   663      position: relative;
   664  }
   665  
   666  .resizer {
   667      display: inline-block;
   668      background: transparent;
   669      width: 10px;
   670      height: 100%;
   671      position: absolute;
   672      right: 0;
   673      top: 0;
   674      cursor: col-resize;
   675      z-index: 100;
   676  }
   677  
   678  td > h2 {
   679      cursor: pointer;
   680      font-size: 120%;
   681      margin: 5px 0px 5px 0px;
   682  }
   683  
   684  td.collapsed {
   685      font-size: 12px;
   686      width: 12px;
   687      border: 1px solid white;
   688      padding: 2px;
   689      cursor: pointer;
   690      background: #fafafa;
   691  }
   692  
   693  td.collapsed div {
   694      text-align: right;
   695      transform: rotate(180deg);
   696      writing-mode: vertical-lr;
   697      white-space: pre;
   698  }
   699  
   700  pre {
   701      font-family: Menlo, monospace;
   702      font-size: 12px;
   703  }
   704  
   705  pre {
   706      -moz-tab-size: 4;
   707      -o-tab-size:   4;
   708      tab-size:      4;
   709  }
   710  
   711  .allow-x-scroll {
   712      overflow-x: scroll;
   713  }
   714  
   715  .outline-node {
   716      cursor: cell;
   717  }
   718  
   719  .variable-name {
   720      cursor: crosshair;
   721  }
   722  
   723  .line-number {
   724      font-size: 11px;
   725      cursor: crosshair;
   726  }
   727  
   728  body.darkmode {
   729      background-color: rgb(21, 21, 21);
   730      color: rgb(230, 255, 255);
   731      opacity: 100%;
   732  }
   733  
   734  td.darkmode {
   735      background-color: rgb(21, 21, 21);
   736      border: 1px solid gray;
   737  }
   738  
   739  body.darkmode table, th {
   740      border: 1px solid gray;
   741  }
   742  
   743  body.darkmode text {
   744      fill: white;
   745  }
   746  
   747  .highlight-aquamarine     { background-color: aquamarine; color: black; }
   748  .highlight-coral          { background-color: coral; color: black; }
   749  .highlight-lightpink      { background-color: lightpink; color: black; }
   750  .highlight-lightsteelblue { background-color: lightsteelblue; color: black; }
   751  .highlight-palegreen      { background-color: palegreen; color: black; }
   752  .highlight-skyblue        { background-color: skyblue; color: black; }
   753  .highlight-lightgray      { background-color: lightgray; color: black; }
   754  .highlight-yellow         { background-color: yellow; color: black; }
   755  .highlight-lime           { background-color: lime; color: black; }
   756  .highlight-khaki          { background-color: khaki; color: black; }
   757  .highlight-aqua           { background-color: aqua; color: black; }
   758  .highlight-salmon         { background-color: salmon; color: black; }
   759  
   760  
   761  .outline-blue           { outline: #2893ff solid 2px; }
   762  .outline-red            { outline: red solid 2px; }
   763  .outline-blueviolet     { outline: blueviolet solid 2px; }
   764  .outline-darkolivegreen { outline: darkolivegreen solid 2px; }
   765  .outline-fuchsia        { outline: fuchsia solid 2px; }
   766  .outline-sienna         { outline: sienna solid 2px; }
   767  .outline-gold           { outline: gold solid 2px; }
   768  .outline-orangered      { outline: orangered solid 2px; }
   769  .outline-teal           { outline: teal solid 2px; }
   770  .outline-maroon         { outline: maroon solid 2px; }
   771  .outline-black          { outline: black solid 2px; }
   772  
   773  /* Capture alternative for outline-black and ellipse.outline-black when in dark mode */
   774  body.darkmode .outline-black        { outline: gray solid 2px; }
   775  
   776  .toggle {
   777      cursor: pointer;
   778      display: inline-block;
   779      text-align: center;
   780      user-select: none;
   781      font-size: 12px; // hand-tweaked
   782  }
   783  
   784  </style>
   785  `
   786  
   787  // safePhaseNameString is a very conservative limit on phase names
   788  // that can safely be encoded as JavaScript strings by wrapping with
   789  // double-quotes.
   790  var safePhaseNameString = regexp.MustCompile("^[a-zA-Z0-9_ .]+$")
   791  
   792  func JS(opened ...string) string {
   793  	var middle strings.Builder
   794  
   795  	// "bloop",
   796  	// "loopvar",
   797  	// "escape",
   798  	// "slice",
   799  	// "walk",
   800  
   801  	// This is only for default-display purposes, and the expected strings
   802  	// are the names of compiler phases. If a wonky name is rejected, the
   803  	// "harm" is that a pane in the debugging display is not pre-opened.
   804  	for _, s := range opened {
   805  		if !safePhaseNameString.MatchString(s) {
   806  			continue
   807  		}
   808  		middle.WriteString("\t\"")
   809  		middle.WriteString(s)
   810  		middle.WriteString("\",\n")
   811  	}
   812  	return JS1 + middle.String() + JS2
   813  }
   814  
   815  const (
   816  	JS1 = `<script type="text/javascript">
   817  
   818  // Contains phase names which are expanded by default. Other columns are collapsed.
   819  let expandedDefault = [
   820  `
   821  	JS2 = `];
   822  if (history.state === null) {
   823      history.pushState({expandedDefault}, "", location.href);
   824  }
   825  
   826  // ordered list of all available highlight colors
   827  var highlights = [
   828      "highlight-aquamarine",
   829      "highlight-coral",
   830      "highlight-lightpink",
   831      "highlight-lightsteelblue",
   832      "highlight-palegreen",
   833      "highlight-skyblue",
   834      "highlight-lightgray",
   835      "highlight-yellow",
   836      "highlight-lime",
   837      "highlight-khaki",
   838      "highlight-aqua",
   839      "highlight-salmon"
   840  ];
   841  
   842  // state: which value is highlighted this color?
   843  var highlighted = {};
   844  for (var i = 0; i < highlights.length; i++) {
   845      highlighted[highlights[i]] = "";
   846  }
   847  
   848  // ordered list of all available outline colors
   849  var outlines = [
   850      "outline-blue",
   851      "outline-red",
   852      "outline-blueviolet",
   853      "outline-darkolivegreen",
   854      "outline-fuchsia",
   855      "outline-sienna",
   856      "outline-gold",
   857      "outline-orangered",
   858      "outline-teal",
   859      "outline-maroon",
   860      "outline-black"
   861  ];
   862  
   863  // state: which value is outlined this color?
   864  var outlined = {};
   865  for (var i = 0; i < outlines.length; i++) {
   866      outlined[outlines[i]] = "";
   867  }
   868  
   869  window.onload = function() {
   870      if (history.state !== null) {
   871          expandedDefault = history.state.expandedDefault;
   872      }
   873      if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
   874          toggleDarkMode();
   875          document.getElementById("dark-mode-button").checked = true;
   876      }
   877  
   878      var irElemClicked = function(elem, event, selections, selected) {
   879          event.stopPropagation();
   880  
   881          // find all values with the same name
   882          var c = elem.classList.item(0);
   883          var x = document.getElementsByClassName(c);
   884  
   885          // if selected, remove selections from all of them
   886          // otherwise, attempt to add
   887  
   888          var remove = "";
   889          for (var i = 0; i < selections.length; i++) {
   890              var color = selections[i];
   891              if (selected[color] == c) {
   892                  remove = color;
   893                  break;
   894              }
   895          }
   896  
   897          if (remove != "") {
   898              for (var i = 0; i < x.length; i++) {
   899                  x[i].classList.remove(remove);
   900              }
   901              selected[remove] = "";
   902              return;
   903          }
   904  
   905          // we're adding a selection
   906          // find first available color
   907          var avail = "";
   908          for (var i = 0; i < selections.length; i++) {
   909              var color = selections[i];
   910              if (selected[color] == "") {
   911                  avail = color;
   912                  break;
   913              }
   914          }
   915          if (avail == "") {
   916              alert("out of selection colors; go add more");
   917              return;
   918          }
   919  
   920          // set that as the selection
   921          for (var i = 0; i < x.length; i++) {
   922              x[i].classList.add(avail);
   923          }
   924          selected[avail] = c;
   925      };
   926  
   927      var irValueClicked = function(event) {
   928          irElemClicked(this, event, highlights, highlighted);
   929      };
   930  
   931      var irTreeClicked = function(event) {
   932          irElemClicked(this, event, outlines, outlined);
   933      };
   934  
   935      var irValues = document.getElementsByClassName("outline-node");
   936      for (var i = 0; i < irValues.length; i++) {
   937          irValues[i].addEventListener('click', irTreeClicked);
   938      }
   939  
   940      var lines = document.getElementsByClassName("line-number");
   941      for (var i = 0; i < lines.length; i++) {
   942          lines[i].addEventListener('click', irValueClicked);
   943      }
   944  
   945      var variableNames = document.getElementsByClassName("variable-name");
   946      for (var i = 0; i < variableNames.length; i++) {
   947          variableNames[i].addEventListener('click', irValueClicked);
   948      }
   949  
   950      function toggler(phase) {
   951          return function() {
   952              toggle_cell(phase+'-col');
   953              toggle_cell(phase+'-exp');
   954              const i = expandedDefault.indexOf(phase);
   955              if (i !== -1) {
   956                  expandedDefault.splice(i, 1);
   957              } else {
   958                  expandedDefault.push(phase);
   959              }
   960              history.pushState({expandedDefault}, "", location.href);
   961          };
   962      }
   963  
   964      function toggle_cell(id) {
   965          var e = document.getElementById(id);
   966          if (e.style.display == 'table-cell') {
   967              e.style.display = 'none';
   968          } else {
   969              e.style.display = 'table-cell';
   970          }
   971      }
   972  
   973      // Go through all columns and collapse needed phases.
   974      const td = document.getElementsByTagName("td");
   975      for (let i = 0; i < td.length; i++) {
   976          const id = td[i].id;
   977          const phase = id.substr(0, id.length-4);
   978          let show = expandedDefault.indexOf(phase) !== -1
   979  
   980          // If show == false, check to see if this is a combined column (multiple phases).
   981          // If combined, check each of the phases to see if they are in our expandedDefaults.
   982          // If any are found, that entire combined column gets shown.
   983          if (!show) {
   984              const combined = phase.split('--+--');
   985              const len = combined.length;
   986              if (len > 1) {
   987                  for (let i = 0; i < len; i++) {
   988                      const num = expandedDefault.indexOf(combined[i]);
   989                      if (num !== -1) {
   990                          expandedDefault.splice(num, 1);
   991                          if (expandedDefault.indexOf(phase) === -1) {
   992                              expandedDefault.push(phase);
   993                              show = true;
   994                          }
   995                      }
   996                  }
   997              }
   998          }
   999          if (id.endsWith("-exp")) {
  1000              const h2Els = td[i].getElementsByTagName("h2");
  1001              const len = h2Els.length;
  1002              if (len > 0) {
  1003                  for (let i = 0; i < len; i++) {
  1004                      h2Els[i].addEventListener('click', toggler(phase));
  1005                  }
  1006              }
  1007          } else {
  1008              td[i].addEventListener('click', toggler(phase));
  1009          }
  1010          if (id.endsWith("-col") && show || id.endsWith("-exp") && !show) {
  1011              td[i].style.display = 'none';
  1012              continue;
  1013          }
  1014          td[i].style.display = 'table-cell';
  1015      }
  1016  
  1017      var resizers = document.getElementsByClassName("resizer");
  1018      for (var i = 0; i < resizers.length; i++) {
  1019          var resizer = resizers[i];
  1020          resizer.addEventListener('mousedown', initDrag, false);
  1021      }
  1022  };
  1023  
  1024  var startX, startWidth, resizableCol;
  1025  
  1026  function initDrag(e) {
  1027      resizableCol = this.parentElement;
  1028      startX = e.clientX;
  1029      startWidth = parseInt(document.defaultView.getComputedStyle(resizableCol).width, 10);
  1030      document.documentElement.addEventListener('mousemove', doDrag, false);
  1031      document.documentElement.addEventListener('mouseup', stopDrag, false);
  1032  }
  1033  
  1034  function doDrag(e) {
  1035      resizableCol.style.width = (startWidth + e.clientX - startX) + 'px';
  1036  }
  1037  
  1038  function stopDrag(e) {
  1039      document.documentElement.removeEventListener('mousemove', doDrag, false);
  1040      document.documentElement.removeEventListener('mouseup', stopDrag, false);
  1041  }
  1042  
  1043  function toggle_visibility(id) {
  1044      var e = document.getElementById(id);
  1045      if (e.style.display == 'block') {
  1046          e.style.display = 'none';
  1047      } else {
  1048          e.style.display = 'block';
  1049      }
  1050  }
  1051  
  1052  function toggleDarkMode() {
  1053      document.body.classList.toggle('darkmode');
  1054  
  1055      // Collect all of the "collapsed" elements and apply dark mode on each collapsed column
  1056      const collapsedEls = document.getElementsByClassName('collapsed');
  1057      const len = collapsedEls.length;
  1058  
  1059      for (let i = 0; i < len; i++) {
  1060          collapsedEls[i].classList.toggle('darkmode');
  1061      }
  1062  }
  1063  
  1064  function toggle_node(e) {
  1065      event.stopPropagation();
  1066      var parent = e.parentNode;
  1067      var children = parent.children;
  1068      for (var i = 0; i < children.length; i++) {
  1069          if (children[i].classList.contains("node-body")) {
  1070              if (children[i].style.display == "none") {
  1071                  children[i].style.display = "";
  1072              } else {
  1073                  children[i].style.display = "none";
  1074              }
  1075          }
  1076      }
  1077      if (e.innerText == "` + RightArrow + `") {
  1078          e.innerText = "` + DownArrow + `";
  1079      } else {
  1080          e.innerText = "` + RightArrow + `";
  1081      }
  1082  }
  1083  
  1084  </script>
  1085  `
  1086  )
  1087  

View as plain text