// Copyright 2015 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 ssa import ( "bytes" "cmd/internal/src" "fmt" "html" exec "internal/execabs" "io" "os" "path/filepath" "strconv" "strings" ) type HTMLWriter struct { w io.WriteCloser Func *Func path string dot *dotWriter prevHash []byte pendingPhases []string pendingTitles []string } func NewHTMLWriter(path string, f *Func, cfgMask string) *HTMLWriter { path = strings.Replace(path, "/", string(filepath.Separator), -1) out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { f.Fatalf("%v", err) } reportPath := path if !filepath.IsAbs(reportPath) { pwd, err := os.Getwd() if err != nil { f.Fatalf("%v", err) } reportPath = filepath.Join(pwd, path) } html := HTMLWriter{ w: out, Func: f, path: reportPath, dot: newDotWriter(cfgMask), } html.start() return &html } // Fatalf reports an error and exits. func (w *HTMLWriter) Fatalf(msg string, args ...interface{}) { fe := w.Func.Frontend() fe.Fatalf(src.NoXPos, msg, args...) } // Logf calls the (w *HTMLWriter).Func's Logf method passing along a msg and args. func (w *HTMLWriter) Logf(msg string, args ...interface{}) { w.Func.Logf(msg, args...) } func (w *HTMLWriter) start() { if w == nil { return } w.WriteString("") w.WriteString(` `) w.WriteString("") w.WriteString("

") w.WriteString(html.EscapeString(w.Func.Name)) w.WriteString("

") w.WriteString(` help

Click on a value or block to toggle highlighting of that value/block and its uses. (Values and blocks are highlighted by ID, and IDs of dead items may be reused, so not all highlights necessarily correspond to the clicked item.)

Faded out values and blocks are dead code that has not been eliminated.

Values printed in italics have a dependency cycle.

CFG: Dashed edge is for unlikely branches. Blue color is for backward edges. Edge with a dot means that this edge follows the order in which blocks were laidout.

`) w.WriteString("") w.WriteString("") } func (w *HTMLWriter) Close() { if w == nil { return } io.WriteString(w.w, "") io.WriteString(w.w, "
") io.WriteString(w.w, "") io.WriteString(w.w, "") w.w.Close() fmt.Printf("dumped SSA to %v\n", w.path) } // WritePhase writes f in a column headed by title. // phase is used for collapsing columns and should be unique across the table. func (w *HTMLWriter) WritePhase(phase, title string) { if w == nil { return // avoid generating HTML just to discard it } hash := hashFunc(w.Func) w.pendingPhases = append(w.pendingPhases, phase) w.pendingTitles = append(w.pendingTitles, title) if !bytes.Equal(hash, w.prevHash) { w.flushPhases() } w.prevHash = hash } // flushPhases collects any pending phases and titles, writes them to the html, and resets the pending slices. func (w *HTMLWriter) flushPhases() { phaseLen := len(w.pendingPhases) if phaseLen == 0 { return } phases := strings.Join(w.pendingPhases, " + ") w.WriteMultiTitleColumn( phases, w.pendingTitles, fmt.Sprintf("hash-%x", w.prevHash), w.Func.HTML(w.pendingPhases[phaseLen-1], w.dot), ) w.pendingPhases = w.pendingPhases[:0] w.pendingTitles = w.pendingTitles[:0] } // FuncLines contains source code for a function to be displayed // in sources column. type FuncLines struct { Filename string StartLineno uint Lines []string } // ByTopo sorts topologically: target function is on top, // followed by inlined functions sorted by filename and line numbers. type ByTopo []*FuncLines func (x ByTopo) Len() int { return len(x) } func (x ByTopo) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x ByTopo) Less(i, j int) bool { a := x[i] b := x[j] if a.Filename == b.Filename { return a.StartLineno < b.StartLineno } return a.Filename < b.Filename } // WriteSources writes lines as source code in a column headed by title. // phase is used for collapsing columns and should be unique across the table. func (w *HTMLWriter) WriteSources(phase string, all []*FuncLines) { if w == nil { return // avoid generating HTML just to discard it } var buf bytes.Buffer fmt.Fprint(&buf, "
") filename := "" for _, fl := range all { fmt.Fprint(&buf, "
 
") if filename != fl.Filename { fmt.Fprint(&buf, "
 
") filename = fl.Filename } for i := range fl.Lines { ln := int(fl.StartLineno) + i fmt.Fprintf(&buf, "
%v
", ln, ln) } } fmt.Fprint(&buf, "
")
	filename = ""
	for _, fl := range all {
		fmt.Fprint(&buf, "
 
") if filename != fl.Filename { fmt.Fprintf(&buf, "
%v
", fl.Filename) filename = fl.Filename } for i, line := range fl.Lines { ln := int(fl.StartLineno) + i var escaped string if strings.TrimSpace(line) == "" { escaped = " " } else { escaped = html.EscapeString(line) } fmt.Fprintf(&buf, "
%v
", ln, escaped) } } fmt.Fprint(&buf, "
") w.WriteColumn(phase, phase, "allow-x-scroll", buf.String()) } func (w *HTMLWriter) WriteAST(phase string, buf *bytes.Buffer) { if w == nil { return // avoid generating HTML just to discard it } lines := strings.Split(buf.String(), "\n") var out bytes.Buffer fmt.Fprint(&out, "
") for _, l := range lines { l = strings.TrimSpace(l) var escaped string var lineNo string if l == "" { escaped = " " } else { if strings.HasPrefix(l, "buildssa") { escaped = fmt.Sprintf("%v", l) } else { // Parse the line number from the format file:line:col. // See the implementation in ir/fmt.go:dumpNodeHeader. sl := strings.Split(l, ":") if len(sl) >= 3 { if _, err := strconv.Atoi(sl[len(sl)-2]); err == nil { lineNo = sl[len(sl)-2] } } escaped = html.EscapeString(l) } } if lineNo != "" { fmt.Fprintf(&out, "
%v
", lineNo, escaped) } else { fmt.Fprintf(&out, "
%v
", escaped) } } fmt.Fprint(&out, "
") w.WriteColumn(phase, phase, "allow-x-scroll", out.String()) } // WriteColumn writes raw HTML in a column headed by title. // It is intended for pre- and post-compilation log output. func (w *HTMLWriter) WriteColumn(phase, title, class, html string) { w.WriteMultiTitleColumn(phase, []string{title}, class, html) } func (w *HTMLWriter) WriteMultiTitleColumn(phase string, titles []string, class, html string) { if w == nil { return } id := strings.Replace(phase, " ", "-", -1) // collapsed column w.Printf("
%v
", id, phase) if class == "" { w.Printf("", id) } else { w.Printf("", id, class) } for _, title := range titles { w.WriteString("

" + title + "

") } w.WriteString(html) w.WriteString("\n") } func (w *HTMLWriter) Printf(msg string, v ...interface{}) { if _, err := fmt.Fprintf(w.w, msg, v...); err != nil { w.Fatalf("%v", err) } } func (w *HTMLWriter) WriteString(s string) { if _, err := io.WriteString(w.w, s); err != nil { w.Fatalf("%v", err) } } func (v *Value) HTML() string { // TODO: Using the value ID as the class ignores the fact // that value IDs get recycled and that some values // are transmuted into other values. s := v.String() return fmt.Sprintf("%s", s, s) } func (v *Value) LongHTML() string { // TODO: Any intra-value formatting? // I'm wary of adding too much visual noise, // but a little bit might be valuable. // We already have visual noise in the form of punctuation // maybe we could replace some of that with formatting. s := fmt.Sprintf("", v.String()) linenumber := "(?)" if v.Pos.IsKnown() { linenumber = fmt.Sprintf("(%s)", v.Pos.LineNumber(), v.Pos.LineNumberHTML()) } s += fmt.Sprintf("%s %s = %s", v.HTML(), linenumber, v.Op.String()) s += " <" + html.EscapeString(v.Type.String()) + ">" s += html.EscapeString(v.auxString()) for _, a := range v.Args { s += fmt.Sprintf(" %s", a.HTML()) } r := v.Block.Func.RegAlloc if int(v.ID) < len(r) && r[v.ID] != nil { s += " : " + html.EscapeString(r[v.ID].String()) } var names []string for name, values := range v.Block.Func.NamedValues { for _, value := range values { if value == v { names = append(names, name.String()) break // drop duplicates. } } } if len(names) != 0 { s += " (" + strings.Join(names, ", ") + ")" } s += "" return s } func (b *Block) HTML() string { // TODO: Using the value ID as the class ignores the fact // that value IDs get recycled and that some values // are transmuted into other values. s := html.EscapeString(b.String()) return fmt.Sprintf("%s", s, s) } func (b *Block) LongHTML() string { // TODO: improve this for HTML? s := fmt.Sprintf("%s", html.EscapeString(b.String()), html.EscapeString(b.Kind.String())) if b.Aux != nil { s += html.EscapeString(fmt.Sprintf(" {%v}", b.Aux)) } if t := b.AuxIntString(); t != "" { s += html.EscapeString(fmt.Sprintf(" [%v]", t)) } for _, c := range b.ControlValues() { s += fmt.Sprintf(" %s", c.HTML()) } if len(b.Succs) > 0 { s += " →" // right arrow for _, e := range b.Succs { c := e.b s += " " + c.HTML() } } switch b.Likely { case BranchUnlikely: s += " (unlikely)" case BranchLikely: s += " (likely)" } if b.Pos.IsKnown() { // TODO does not begin to deal with the full complexity of line numbers. // Maybe we want a string/slice instead, of outer-inner when inlining. s += fmt.Sprintf(" (%s)", b.Pos.LineNumber(), b.Pos.LineNumberHTML()) } return s } func (f *Func) HTML(phase string, dot *dotWriter) string { buf := new(bytes.Buffer) if dot != nil { dot.writeFuncSVG(buf, phase, f) } fmt.Fprint(buf, "") p := htmlFuncPrinter{w: buf} fprintFunc(p, f) // fprintFunc(&buf, f) // TODO: HTML, not text,
for line breaks, etc. fmt.Fprint(buf, "
") return buf.String() } func (d *dotWriter) writeFuncSVG(w io.Writer, phase string, f *Func) { if d.broken { return } if _, ok := d.phases[phase]; !ok { return } cmd := exec.Command(d.path, "-Tsvg") pipe, err := cmd.StdinPipe() if err != nil { d.broken = true fmt.Println(err) return } buf := new(bytes.Buffer) cmd.Stdout = buf bufErr := new(bytes.Buffer) cmd.Stderr = bufErr err = cmd.Start() if err != nil { d.broken = true fmt.Println(err) return } fmt.Fprint(pipe, `digraph "" { margin=0; ranksep=.2; `) id := strings.Replace(phase, " ", "-", -1) fmt.Fprintf(pipe, `id="g_graph_%s";`, id) fmt.Fprintf(pipe, `node [style=filled,fillcolor=white,fontsize=16,fontname="Menlo,Times,serif",margin="0.01,0.03"];`) fmt.Fprintf(pipe, `edge [fontsize=16,fontname="Menlo,Times,serif"];`) for i, b := range f.Blocks { if b.Kind == BlockInvalid { continue } layout := "" if f.laidout { layout = fmt.Sprintf(" #%d", i) } fmt.Fprintf(pipe, `%v [label="%v%s\n%v",id="graph_node_%v_%v",tooltip="%v"];`, b, b, layout, b.Kind.String(), id, b, b.LongString()) } indexOf := make([]int, f.NumBlocks()) for i, b := range f.Blocks { indexOf[b.ID] = i } layoutDrawn := make([]bool, f.NumBlocks()) ponums := make([]int32, f.NumBlocks()) _ = postorderWithNumbering(f, ponums) isBackEdge := func(from, to ID) bool { return ponums[from] <= ponums[to] } for _, b := range f.Blocks { for i, s := range b.Succs { style := "solid" color := "black" arrow := "vee" if b.unlikelyIndex() == i { style = "dashed" } if f.laidout && indexOf[s.b.ID] == indexOf[b.ID]+1 { // Red color means ordered edge. It overrides other colors. arrow = "dotvee" layoutDrawn[s.b.ID] = true } else if isBackEdge(b.ID, s.b.ID) { color = "#2893ff" } fmt.Fprintf(pipe, `%v -> %v [label=" %d ",style="%s",color="%s",arrowhead="%s"];`, b, s.b, i, style, color, arrow) } } if f.laidout { fmt.Fprintln(pipe, `edge[constraint=false,color=gray,style=solid,arrowhead=dot];`) colors := [...]string{"#eea24f", "#f38385", "#f4d164", "#ca89fc", "gray"} ci := 0 for i := 1; i < len(f.Blocks); i++ { if layoutDrawn[f.Blocks[i].ID] { continue } fmt.Fprintf(pipe, `%s -> %s [color="%s"];`, f.Blocks[i-1], f.Blocks[i], colors[ci]) ci = (ci + 1) % len(colors) } } fmt.Fprint(pipe, "}") pipe.Close() err = cmd.Wait() if err != nil { d.broken = true fmt.Printf("dot: %v\n%v\n", err, bufErr.String()) return } svgID := "svg_graph_" + id fmt.Fprintf(w, `
`, svgID, svgID) // For now, an awful hack: edit the html as it passes through // our fingers, finding '", b, dead) fmt.Fprintf(p.w, "
  • %s:", b.HTML()) if len(b.Preds) > 0 { io.WriteString(p.w, " ←") // left arrow for _, e := range b.Preds { pred := e.b fmt.Fprintf(p.w, " %s", pred.HTML()) } } if len(b.Values) > 0 { io.WriteString(p.w, ``) } io.WriteString(p.w, "
  • ") if len(b.Values) > 0 { // start list of values io.WriteString(p.w, "
  • ") io.WriteString(p.w, "") io.WriteString(p.w, "
  • ") } io.WriteString(p.w, "
  • ") fmt.Fprint(p.w, b.LongHTML()) io.WriteString(p.w, "
  • ") io.WriteString(p.w, "") } func (p htmlFuncPrinter) value(v *Value, live bool) { var dead string if !live { dead = "dead-value" } fmt.Fprintf(p.w, "
  • ", dead) fmt.Fprint(p.w, v.LongHTML()) io.WriteString(p.w, "
  • ") } func (p htmlFuncPrinter) startDepCycle() { fmt.Fprintln(p.w, "") } func (p htmlFuncPrinter) endDepCycle() { fmt.Fprintln(p.w, "") } func (p htmlFuncPrinter) named(n LocalSlot, vals []*Value) { fmt.Fprintf(p.w, "
  • name %s: ", n) for _, val := range vals { fmt.Fprintf(p.w, "%s ", val.HTML()) } fmt.Fprintf(p.w, "
  • ") } type dotWriter struct { path string broken bool phases map[string]bool // keys specify phases with CFGs } // newDotWriter returns non-nil value when mask is valid. // dotWriter will generate SVGs only for the phases specified in the mask. // mask can contain following patterns and combinations of them: // * - all of them; // x-y - x through y, inclusive; // x,y - x and y, but not the passes between. func newDotWriter(mask string) *dotWriter { if mask == "" { return nil } // User can specify phase name with _ instead of spaces. mask = strings.Replace(mask, "_", " ", -1) ph := make(map[string]bool) ranges := strings.Split(mask, ",") for _, r := range ranges { spl := strings.Split(r, "-") if len(spl) > 2 { fmt.Printf("range is not valid: %v\n", mask) return nil } var first, last int if mask == "*" { first = 0 last = len(passes) - 1 } else { first = passIdxByName(spl[0]) last = passIdxByName(spl[len(spl)-1]) } if first < 0 || last < 0 || first > last { fmt.Printf("range is not valid: %v\n", r) return nil } for p := first; p <= last; p++ { ph[passes[p].name] = true } } path, err := exec.LookPath("dot") if err != nil { fmt.Println(err) return nil } return &dotWriter{path: path, phases: ph} } func passIdxByName(name string) int { for i, p := range passes { if p.name == name { return i } } return -1 }