From: David Chase Date: Fri, 30 Jan 2026 06:46:05 +0000 (+0200) Subject: cmd/compile: enhance astdump flag to also generate HTML X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=ae7b257f24736ec13100870c4dee59c5eb57f062;p=gostls13.git cmd/compile: enhance astdump flag to also generate HTML AI-generated code, 3 merged commits, plus a LOT of hand cleanups and tweaks, including removing cargo-culted dead code from the SSA example, reorganizing CSS and JS out of a single giant comment, using defer appropriately to ensure balanced open/close tags, running output through tidy to check compliance. Prompts are included for reference. This is intended to produce an HTML file in the style of "ssa.html", but for AST. The result of various phases appears in columns, which can be scrolled side to side, and also dragged sideways to make them wider (because AST tends wide). This supports three kinds of highlighting, which I tweaked along the way to make them (in my opinion) more useful. 1) Node outlining. This outlines a node and all of its subtrees. When the cursor is a "cell" (outlined cross) node highlighting is available. Note that "NAME" nodes are repeated within the tree, so click on one of these will outline every occurrence. This is actually done with pointer identity. 2) Name highlighting, available with a "crosshair" cursor. This highlights a name, e.g. "autotmp_1". 3) Position highlighting, available with a "crosshair" cursor. This highlights either a file (all occurrences of that file's positions), a line within a file (all occurrences of that file:line combination), or a column (all occurrences of that particular file:line:column). Inlined positions are treated as a sequence of positions, not a single position. Prompts: ``` The file cmd/compile/internal/ir/dump.go contains a function AstDump that calls FDump to generate a textual representation of the AST from several phases within the compiler. The file cmd/compile/internal/ir/fmt.go contains the definition of FDump. The SSA phases of the compiler use code in cmd/compile/internal/ssa/html.go to render its textual representation into an html display that allows hiding phases and highlighting blocks, identifiers and line numbers. Please write a similar HTML-generating phase for AST that produces an output that will allow hiding phases and perhaps hiding subtrees, and highlighting identifiers and line numbers. The idioms and hacks used in the SSA html display have worked well, if you want to copy them. ``` ``` Not bad, but the output contains a strike-through beginning on a line that contains "TYPE type *testing.B tc(1)". The strike through begins after "TYPE " and continues for the rest of the output. Can you find that bug and fix it? ``` ``` The AST output is often wider than the statically sized columns in the HTML output. Either the columns need to have a width that can be resized (dragged wider, for example) or the AST needs to be draggable, side-to-side, within the columns. Resizable columns seems like the preferable choice, if it is possible. ``` ``` The highlighting for file name and line number is not quite right -- all the lines in the same file are grouped together, where what I want, is that each different file:line:column gets its own number. There's also the issue of inlining, in some cases the location is described as more than one file:line:column, where the first is the call site and the second is the inlined function. I think it makes sense to treat each single file:line:column as its own item for highlighting, instead of trying to treat the sequence of file:line:column as a single distinct location. One thing that might be interesting, but I am not sure how hard it would be, is to distinguish between clicks to the file part, the line part, and the column part -- click on file means highlight all that matches file, click on line means all that machines file:line (not just the line numner, since there may be different files, with inlining) and click on the column means to highlight the specific file:line:column triple. That is, if it is possible. ``` ``` Lovely. Can you implement highlighting for names, strings like "NAME-testing.b" so that all uses of a variable or a temporary can easily be seen? ``` Change-Id: I1ed97cd92cdae16d556e3334e543af37973799e5 Reviewed-on: https://go-review.googlesource.com/c/go/+/740563 LUCI-TryBot-Result: Go LUCI Reviewed-by: t hepudds Reviewed-by: Keith Randall --- diff --git a/src/cmd/compile/internal/gc/main.go b/src/cmd/compile/internal/gc/main.go index 66b7ca35b4..9c090f2a36 100644 --- a/src/cmd/compile/internal/gc/main.go +++ b/src/cmd/compile/internal/gc/main.go @@ -47,6 +47,7 @@ import ( // already been some compiler errors). It may also be invoked from the explicit panic in // hcrash(), in which case, we pass the panic on through. func handlePanic() { + ir.CloseHTMLWriters() if err := recover(); err != nil { if err == "-h" { // Force real panic now with -h option (hcrash) - the error @@ -243,6 +244,12 @@ func Main(archInit func(*ssagen.ArchInfo)) { } } + for _, fn := range typecheck.Target.Funcs { + if ir.MatchAstDump(fn, "start") { + ir.AstDump(fn, "start, "+ir.FuncName(fn)) + } + } + // Apply bloop markings. bloop.Walk(typecheck.Target) @@ -250,6 +257,12 @@ func Main(archInit func(*ssagen.ArchInfo)) { base.Timer.Start("fe", "devirtualize-and-inline") interleaved.DevirtualizeAndInlinePackage(typecheck.Target, profile) + for _, fn := range typecheck.Target.Funcs { + if ir.MatchAstDump(fn, "devirtualize-and-inline") { + ir.AstDump(fn, "devirtualize-and-inline, "+ir.FuncName(fn)) + } + } + noder.MakeWrappers(typecheck.Target) // must happen after inlining // Get variable capture right in for loops. diff --git a/src/cmd/compile/internal/ir/dump.go b/src/cmd/compile/internal/ir/dump.go index e31b9e468a..13599d7c50 100644 --- a/src/cmd/compile/internal/ir/dump.go +++ b/src/cmd/compile/internal/ir/dump.go @@ -143,6 +143,12 @@ func AstDump(fn *Func, why string) { FDump(w, why, fn) }, ) + // strip text following comma, for phase names. + comma := strings.Index(why, ",") + if comma > 0 { + why = why[:comma] + } + DumpNodeHTML(fn, why, fn) if err != nil { fmt.Fprintf(os.Stderr, "Dump returned error %v\n", err) } @@ -189,6 +195,37 @@ func withLockAndFile(fn *Func, dump func(io.Writer)) (err error) { return } +var htmlWriters = make(map[*Func]*HTMLWriter) +var orderedFuncs = []*Func{} + +// DumpNodeHTML dumps the node n to the HTML writer for fn. +// It uses the same phase name as the text dump. +func DumpNodeHTML(fn *Func, why string, n Node) { + mu.Lock() + defer mu.Unlock() + w, ok := htmlWriters[fn] + if !ok { + name := escapedFileName(fn, ".html") + w = NewHTMLWriter(name, fn, "") + htmlWriters[fn] = w + orderedFuncs = append(orderedFuncs, fn) + } + w.WritePhase(why, why) +} + +// CloseHTMLWriter closes the HTML writer for fn, if one exists. +func CloseHTMLWriters() { + mu.Lock() + defer mu.Unlock() + for _, fn := range orderedFuncs { + if w, ok := htmlWriters[fn]; ok { + w.Close() + delete(htmlWriters, fn) + } + } + orderedFuncs = nil +} + type dumper struct { output io.Writer fieldrx *regexp.Regexp // field name filter diff --git a/src/cmd/compile/internal/ir/html.go b/src/cmd/compile/internal/ir/html.go new file mode 100644 index 0000000000..c17d4655ec --- /dev/null +++ b/src/cmd/compile/internal/ir/html.go @@ -0,0 +1,926 @@ +// 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 ir + +import ( + "bufio" + "cmd/compile/internal/base" + "cmd/compile/internal/types" + "cmd/internal/src" + "crypto/sha256" + "encoding/hex" + "fmt" + "html" + "io" + "os" + "path/filepath" + "reflect" + "strings" +) + +// An HTMLWriter dumps IR to multicolumn HTML, similar to what the +// ssa backend does for GOSSAFUNC. This is not the format used for +// the ast column in GOSSAFUNC output. +type HTMLWriter struct { + w *BufferedWriterCloser + Func *Func + canonIdMap map[Node]int + prevCanonId int + path string + prevHash []byte + pendingPhases []string + pendingTitles []string +} + +// BufferedWriterCloser is here to help avoid pre-buffering the whole +// rendered HTML in memory, which can cause problems for large inputs. +type BufferedWriterCloser struct { + file io.Closer + w *bufio.Writer +} + +func (b *BufferedWriterCloser) Write(p []byte) (n int, err error) { + return b.w.Write(p) +} + +func (b *BufferedWriterCloser) Close() error { + b.w.Flush() + b.w = nil + return b.file.Close() +} + +func NewBufferedWriterCloser(f io.WriteCloser) *BufferedWriterCloser { + return &BufferedWriterCloser{file: f, w: bufio.NewWriter(f)} +} + +func NewHTMLWriter(path string, f *Func, 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 { + base.Fatalf("%v", err) + } + reportPath := path + if !filepath.IsAbs(reportPath) { + pwd, err := os.Getwd() + if err != nil { + base.Fatalf("%v", err) + } + reportPath = filepath.Join(pwd, path) + } + h := HTMLWriter{ + w: NewBufferedWriterCloser(out), + Func: f, + path: reportPath, + canonIdMap: make(map[Node]int), + } + h.start() + return &h +} + +// canonId assigns indices to nodes based on pointer identity. +// this helps ensure that output html files don't gratuitously +// differ from run to run. +func (h *HTMLWriter) canonId(n Node) int { + if id := h.canonIdMap[n]; id > 0 { + return id + } + h.prevCanonId++ + h.canonIdMap[n] = h.prevCanonId + return h.prevCanonId +} + +// Fatalf reports an error and exits. +func (w *HTMLWriter) Fatalf(msg string, args ...any) { + base.FatalfAt(src.NoXPos, msg, args...) +} + +func (w *HTMLWriter) start() { + if w == nil { + return + } + escName := html.EscapeString(PkgFuncName(w.Func)) + w.Print("") + w.Print("") + w.Printf(` + + +%s +%s +AST display for %s +`, escName, CSS, JS, escName) + w.Print("") + w.Print("

") + w.Print(html.EscapeString(w.Func.Sym().Name)) + w.Print("

") + w.Print(` +help +
+ +

+Click anywhere on a node (with "cell" cursor) to outline a node and all of its subtrees. +

+

+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). +

+

+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.
Inlined +locations are not treated as a single location, but as a sequence of locations that +can be independently highlighted. +

+ +
+ + +`) + w.Print("") + w.Print("") +} + +func (w *HTMLWriter) Close() { + if w == nil { + return + } + w.Print("") + w.Print("
") + w.Print("") + w.Print("\n") + w.w.Close() + fmt.Fprintf(os.Stderr, "Writing html ast output for %s to %s\n", PkgFuncName(w.Func), 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 + } + w.pendingPhases = append(w.pendingPhases, phase) + w.pendingTitles = append(w.pendingTitles, title) + w.flushPhases() +} + +// 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, + "allow-x-scroll", + w.FuncHTML(w.pendingPhases[phaseLen-1]), + ) + w.pendingPhases = w.pendingPhases[:0] + w.pendingTitles = w.pendingTitles[:0] +} + +func (w *HTMLWriter) WriteMultiTitleColumn(phase string, titles []string, class string, writeContent func()) { + if w == nil { + return + } + id := strings.ReplaceAll(phase, " ", "-") + // collapsed column + w.Printf("
%v
", id, phase) + + if class == "" { + w.Printf("", id) + } else { + w.Printf("", id, class) + } + for _, title := range titles { + w.Print("

" + title + "

") + } + writeContent() + w.Print("
") + w.Print("\n") +} + +func (w *HTMLWriter) Printf(msg string, v ...any) { + if _, err := fmt.Fprintf(w.w, msg, v...); err != nil { + w.Fatalf("%v", err) + } +} + +func (w *HTMLWriter) Print(s string) { + if _, err := fmt.Fprint(w.w, s); err != nil { + w.Fatalf("%v", err) + } +} + +func (w *HTMLWriter) indent(n int) { + indent(w.w, n) +} + +func (w *HTMLWriter) FuncHTML(phase string) func() { + return func() { + w.Print("
") // use pre for formatting to preserve indentation
+		w.dumpNodesHTML(w.Func.Body, 1)
+		w.Print("
") + } +} + +func (h *HTMLWriter) dumpNodesHTML(list Nodes, depth int) { + if len(list) == 0 { + h.Print(" ") + return + } + + for _, n := range list { + h.dumpNodeHTML(n, depth) + } +} + +func (h *HTMLWriter) dumpNodeHTML(n Node, depth int) { + indent(h.w, depth) + if depth > 40 { + h.Print("...") + return + } + + if n == nil { + h.Print("NilIrNode") + return + } + + // For HTML, we want to wrap the node and its details in a span that can be highlighted + // across all occurrences of the span in all columns, so it has to be linked to the node ID, + // which is its address. Canonicalize the address to a counter so that repeated compiler + // runs yield the same html. + // + // JS Equivalence logic: + // var c = elem.classList.item(0); + // var x = document.getElementsByClassName(c); + // + // Tag each class with its canonicalized index. + + h.Printf("", h.canonId(n)) + defer h.Printf("") + + if len(n.Init()) != 0 { + h.Printf("%+v-init", n.Op()) + h.dumpNodesHTML(n.Init(), depth+1) + h.indent(depth) + } + + switch n.Op() { + default: + h.Printf("%+v", n.Op()) + h.dumpNodeHeaderHTML(n) + + case OLITERAL: + h.Printf("%+v-%v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Val()))) + h.dumpNodeHeaderHTML(n) + return + + case ONAME, ONONAME: + if n.Sym() != nil { + // Name highlighting: + // Create a hash for the symbol name to use as a class + // We use the same irValueClicked logic which uses the first class as the identifier + name := fmt.Sprintf("%v", n.Sym()) + hash := sha256.Sum256([]byte(name)) + symID := "sym-" + hex.EncodeToString(hash[:6]) + h.Printf("%+v-%+v", n.Op(), symID, html.EscapeString(name)) + } else { + h.Printf("%+v", n.Op()) + } + h.dumpNodeHeaderHTML(n) + return + + case OLINKSYMOFFSET: + n := n.(*LinksymOffsetExpr) + h.Printf("%+v-%v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Linksym))) + if n.Offset_ != 0 { + h.Printf("%+v", n.Offset_) + } + h.dumpNodeHeaderHTML(n) + + case OASOP: + n := n.(*AssignOpStmt) + h.Printf("%+v-%+v", n.Op(), n.AsOp) + h.dumpNodeHeaderHTML(n) + + case OTYPE: + h.Printf("%+v %+v", n.Op(), html.EscapeString(fmt.Sprintf("%v", n.Sym()))) + h.dumpNodeHeaderHTML(n) + return + + case OCLOSURE: + h.Printf("%+v", n.Op()) + h.dumpNodeHeaderHTML(n) + + case ODCLFUNC: + n := n.(*Func) + h.Printf("%+v", n.Op()) + h.dumpNodeHeaderHTML(n) + fn := n + if len(fn.Dcl) > 0 { + h.indent(depth) + h.Printf("%+v-Dcl", n.Op()) + for _, dcl := range n.Dcl { + h.dumpNodeHTML(dcl, depth+1) + } + } + if len(fn.ClosureVars) > 0 { + h.indent(depth) + h.Printf("%+v-ClosureVars", n.Op()) + for _, cv := range fn.ClosureVars { + h.dumpNodeHTML(cv, depth+1) + } + } + if len(fn.Body) > 0 { + h.indent(depth) + h.Printf("%+v-body", n.Op()) + h.dumpNodesHTML(fn.Body, depth+1) + } + return + } + + 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 + } + } + name := strings.TrimSuffix(tf.Name, "_") + switch name { + case "X", "Y", "Index", "Chan", "Value", "Call": + name = "" + } + switch val := vf.Interface().(type) { + case Node: + if name != "" { + h.indent(depth) + h.Printf("%+v-%s", n.Op(), name) + } + h.dumpNodeHTML(val, depth+1) + case Nodes: + if len(val) == 0 { + continue + } + if name != "" { + h.indent(depth) + h.Printf("%+v-%s", n.Op(), name) + } + h.dumpNodesHTML(val, depth+1) + default: + if vf.Kind() == reflect.Slice && vf.Type().Elem().Implements(nodeType) { + if vf.Len() == 0 { + continue + } + if name != "" { + h.indent(depth) + h.Printf("%+v-%s", n.Op(), name) + } + for i, n := 0, vf.Len(); i < n; i++ { + h.dumpNodeHTML(vf.Index(i).Interface().(Node), depth+1) + } + } + } + } +} + +func (h *HTMLWriter) dumpNodeHeaderHTML(n Node) { + // print pointer to be able to see identical nodes + if base.Debug.DumpPtrs != 0 { + h.Printf(" p(%p)", n) + } + + if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Defn != nil { + h.Printf(" defn(%p)", n.Name().Defn) + } + + if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Curfn != nil { + h.Printf(" curfn(%p)", n.Name().Curfn) + } + if base.Debug.DumpPtrs != 0 && n.Name() != nil && n.Name().Outer != nil { + h.Printf(" outer(%p)", n.Name().Outer) + } + + if EscFmt != nil { + if esc := EscFmt(n); esc != "" { + h.Printf(" %s", html.EscapeString(esc)) + } + } + + if n.Sym() != nil && n.Op() != ONAME && n.Op() != ONONAME && n.Op() != OTYPE { + h.Printf(" %+v", html.EscapeString(fmt.Sprintf("%v", n.Sym()))) + } + + 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 { + name := strings.TrimSuffix(tf.Name, "_") + vf := v.Field(i) + vfi := vf.Interface() + if name == "Offset" && vfi == types.BADWIDTH || name != "Offset" && vf.IsZero() { + continue + } + if vfi == true { + h.Printf(" %s", name) + } else { + h.Printf(" %s:%+v", name, html.EscapeString(fmt.Sprintf("%v", vf.Interface()))) + } + } + } + + v = reflect.ValueOf(n) + t = v.Type() + nm := t.NumMethod() + for i := 0; i < nm; i++ { + tm := t.Method(i) + if tm.PkgPath != "" { + continue + } + m := v.Method(i) + mt := m.Type() + if mt.NumIn() == 0 && mt.NumOut() == 1 && mt.Out(0).Kind() == reflect.Bool { + func() { + defer func() { recover() }() + if m.Call(nil)[0].Bool() { + name := strings.TrimSuffix(tm.Name, "_") + h.Printf(" %s", name) + } + }() + } + } + + if n.Op() == OCLOSURE { + n := n.(*ClosureExpr) + if fn := n.Func; fn != nil && fn.Nname.Sym() != nil { + h.Printf(" fnName(%+v)", html.EscapeString(fmt.Sprintf("%v", fn.Nname.Sym()))) + } + } + + if n.Type() != nil { + if n.Op() == OTYPE { + h.Printf(" type") + } + h.Printf(" %+v", html.EscapeString(fmt.Sprintf("%v", n.Type()))) + } + if n.Typecheck() != 0 { + h.Printf(" tc(%d)", n.Typecheck()) + } + + if n.Pos().IsKnown() { + h.Print(" ") + switch n.Pos().IsStmt() { + case src.PosNotStmt: + h.Print("_") + case src.PosIsStmt: + h.Print("+") + } + sep := "" + base.Ctxt.AllPos(n.Pos(), func(pos src.Pos) { + h.Print(sep) + sep = " " + // Hierarchical highlighting: + // Click file -> highlight all ranges in this file + // Click line -> highlight all ranges at this line (in this file) + // Click col -> highlight this specific range + + file := pos.Filename() + // Create a hash for the filename to use as a class + hash := sha256.Sum256([]byte(file)) + fileID := "loc-" + hex.EncodeToString(hash[:6]) + lineID := fmt.Sprintf("%s-L%d", fileID, pos.Line()) + colID := fmt.Sprintf("%s-C%d", lineID, pos.Col()) + + // File part: triggers fileID + h.Printf("%s:", fileID, html.EscapeString(filepath.Base(file))) + // Line part: triggers lineID (and fileID via class list) + h.Printf("%d:", lineID, fileID, pos.Line()) + // Col part: triggers colID (and lineID, fileID) + h.Printf("%d", colID, lineID, fileID, pos.Col()) + }) + h.Print("") + } +} + +const ( + CSS = ` +` + + JS = ` +` +) diff --git a/src/cmd/compile/internal/ir/html_test.go b/src/cmd/compile/internal/ir/html_test.go new file mode 100644 index 0000000000..2d88d5089b --- /dev/null +++ b/src/cmd/compile/internal/ir/html_test.go @@ -0,0 +1,104 @@ +// 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 ir + +import ( + "cmd/compile/internal/base" + "cmd/compile/internal/types" + "cmd/internal/obj" + "cmd/internal/src" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestHTMLWriter(t *testing.T) { + // Initialize base.Ctxt to avoid panics + base.Ctxt = new(obj.Link) + + // Setup a temporary directory for output + tmpDir := t.TempDir() + + // Mock func + fn := &Func{ + Nname: &Name{ + sym: &types.Sym{Name: "TestFunc"}, + }, + } + // Func embeds miniExpr, so we might need to set op if checked + fn.op = ODCLFUNC + + // Create HTMLWriter + outFile := filepath.Join(tmpDir, "test.html") + w := NewHTMLWriter(outFile, fn, "") + if w == nil { + t.Fatalf("Failed to create HTMLWriter") + } + + // Write a phase + w.WritePhase("phase1", "Phase 1") + + // Register a file/line + posBase := src.NewFileBase("test.go", "test.go") + // base.Ctxt.PosTable.Register(posBase) -- Not needed/doesn't exist + pos := src.MakePos(posBase, 10, 1) + + // Create a dummy node + n := &Name{ + sym: &types.Sym{Name: "VarX"}, + Class: PAUTO, + } + n.op = ONAME + n.pos = base.Ctxt.PosTable.XPos(pos) + + // Add another phase which actually dumps something interesting + fn.Body = []Node{n} + w.WritePhase("phase2", "Phase 2") + + // Test escaping + n2 := &Name{ + sym: &types.Sym{Name: ""}, + Class: PAUTO, + } + n2.op = ONAME + fn.Body = []Node{n2} + w.WritePhase("phase3", "Phase 3") + + w.Close() + + // Verify file exists and has content + content, err := os.ReadFile(outFile) + if err != nil { + t.Fatalf("Failed to read output file: %v", err) + } + + s := string(content) + if len(s) == 0 { + t.Errorf("Output file is empty") + } + + // Check for Expected strings + expected := []string{ + "", + "Phase 1", + "Phase 2", + "Phase 2", + "VarX", + "NAME", + "<Bad>", + "resizer", + "loc-", + "line-number", + "sym-", + "variable-name", + } + + for _, e := range expected { + if !strings.Contains(s, e) { + t.Errorf("Output missing %q", e) + } + } +}