golang.org/x/arch@v0.17.0/internal/unify/html.go (about) 1 // Copyright 2025 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 unify 6 7 import ( 8 "fmt" 9 "html" 10 "io" 11 "strings" 12 ) 13 14 func (t *tracer) writeHTML(w io.Writer) { 15 if !t.saveTree { 16 panic("writeHTML called without tracer.saveTree") 17 } 18 19 fmt.Fprintf(w, "<html><head><style>%s</style></head>", htmlCSS) 20 for _, root := range t.trees { 21 dot := newDotEncoder() 22 html := htmlTracer{w: w, dot: dot} 23 html.writeTree(root) 24 } 25 fmt.Fprintf(w, "</html>\n") 26 } 27 28 const htmlCSS = ` 29 .unify { 30 display: grid; 31 grid-auto-columns: min-content; 32 text-align: center; 33 } 34 35 .header { 36 grid-row: 1; 37 font-weight: bold; 38 padding: 0.25em; 39 position: sticky; 40 top: 0; 41 background: white; 42 } 43 44 .envFactor { 45 display: grid; 46 grid-auto-rows: min-content; 47 grid-template-columns: subgrid; 48 text-align: center; 49 } 50 ` 51 52 type htmlTracer struct { 53 w io.Writer 54 dot *dotEncoder 55 svgs map[*Value]string 56 } 57 58 func (t *htmlTracer) writeTree(node *traceTree) { 59 // TODO: This could be really nice. 60 // 61 // - Put nodes that were unified on the same rank with {rank=same; a; b} 62 // 63 // - On hover, highlight nodes that node was unified with and the result. If 64 // it's a variable, highlight it in the environment, too. 65 // 66 // - On click, show the details of unifying that node. 67 // 68 // This could be the only way to navigate, without necessarily needing the 69 // whole nest of <detail> nodes. 70 71 // TODO: It might be possible to write this out on the fly. 72 73 t.emit([]*Value{node.v, node.w}, []string{"v", "w"}, node.envIn) 74 75 // Render children. 76 for i, child := range node.children { 77 if i >= 10 { 78 fmt.Fprintf(t.w, `<div style="margin-left: 4em">...</div>`) 79 break 80 } 81 fmt.Fprintf(t.w, `<details style="margin-left: 4em"><summary>%s</summary>`, html.EscapeString(child.label)) 82 t.writeTree(child) 83 fmt.Fprintf(t.w, "</details>\n") 84 } 85 86 // Render result. 87 if node.err != nil { 88 fmt.Fprintf(t.w, "Error: %s\n", html.EscapeString(node.err.Error())) 89 } else { 90 t.emit([]*Value{node.res}, []string{"res"}, node.env) 91 } 92 } 93 94 func (t *htmlTracer) svg(v *Value) string { 95 if s, ok := t.svgs[v]; ok { 96 return s 97 } 98 var buf strings.Builder 99 t.dot.subgraph(v) 100 t.dot.writeSvg(&buf) 101 t.dot.clear() 102 svg := buf.String() 103 if t.svgs == nil { 104 t.svgs = make(map[*Value]string) 105 } 106 t.svgs[v] = svg 107 buf.Reset() 108 return svg 109 } 110 111 func (t *htmlTracer) emit(vs []*Value, labels []string, env nonDetEnv) { 112 fmt.Fprintf(t.w, `<div class="unify">`) 113 for i, v := range vs { 114 fmt.Fprintf(t.w, `<div class="header" style="grid-column: %d">%s</div>`, i+1, html.EscapeString(labels[i])) 115 fmt.Fprintf(t.w, `<div style="grid-area: 2 / %d">%s</div>`, i+1, t.svg(v)) 116 } 117 118 t.emitEnv(env, len(vs)) 119 120 fmt.Fprintf(t.w, `</div>`) 121 } 122 123 func (t *htmlTracer) emitEnv(env nonDetEnv, colStart int) { 124 if env.isBottom() { 125 fmt.Fprintf(t.w, `<div class="header" style="grid-column: %d">_|_</div>`, colStart+1) 126 return 127 } 128 129 colLimit := 10 130 col := colStart 131 for i, f := range env.factors { 132 if i > 0 { 133 // Print * between each factor. 134 fmt.Fprintf(t.w, `<div class="header" style="grid-column: %d">×</div>`, col+1) 135 col++ 136 } 137 138 var idCols []int 139 for i, id := range f.ids { 140 var str string 141 if i == 0 && len(f.ids) > 1 { 142 str = "(" 143 } 144 if colLimit <= 0 { 145 str += "..." 146 } else { 147 str += html.EscapeString(t.dot.idp.unique(id)) 148 } 149 if (i == len(f.ids)-1 || colLimit <= 0) && len(f.ids) > 1 { 150 str += ")" 151 } 152 153 fmt.Fprintf(t.w, `<div class="header" style="grid-column: %d">%s</div>`, col+1, str) 154 idCols = append(idCols, col) 155 156 col++ 157 if colLimit <= 0 { 158 break 159 } 160 colLimit-- 161 } 162 163 fmt.Fprintf(t.w, `<div class="envFactor" style="grid-area: 2 / %d / 3 / %d">`, idCols[0]+1, col+1) 164 rowLimit := 10 165 row := 0 166 for _, term := range f.terms { 167 // TODO: Print + between rows? With some horizontal something to 168 // make it clear what it applies across? 169 170 for i, val := range term.vals { 171 fmt.Fprintf(t.w, `<div style="grid-area: %d / %d">`, row+1, idCols[i]-idCols[0]+1) 172 if i < len(term.vals)-1 && i == len(idCols)-1 { 173 fmt.Fprintf(t.w, `...</div>`) 174 break 175 } else if rowLimit <= 0 { 176 fmt.Fprintf(t.w, `...</div>`) 177 } else { 178 fmt.Fprintf(t.w, `%s</div>`, t.svg(val)) 179 } 180 } 181 182 row++ 183 if rowLimit <= 0 { 184 break 185 } 186 rowLimit-- 187 } 188 fmt.Fprintf(t.w, `</div>`) 189 } 190 }