golang.org/x/arch@v0.17.0/internal/unify/dot.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  	"bytes"
     9  	"fmt"
    10  	"html"
    11  	"io"
    12  	"os"
    13  	"os/exec"
    14  	"strings"
    15  )
    16  
    17  const maxNodes = 30
    18  
    19  type dotEncoder struct {
    20  	w *bytes.Buffer
    21  
    22  	idGen    int // Node name generation
    23  	valLimit int // Limit the number of Values in a subgraph
    24  
    25  	idp identPrinter
    26  }
    27  
    28  func newDotEncoder() *dotEncoder {
    29  	return &dotEncoder{
    30  		w: new(bytes.Buffer),
    31  	}
    32  }
    33  
    34  func (enc *dotEncoder) clear() {
    35  	enc.w.Reset()
    36  	enc.idGen = 0
    37  }
    38  
    39  func (enc *dotEncoder) writeTo(w io.Writer) {
    40  	fmt.Fprintln(w, "digraph {")
    41  	// Use the "new" ranking algorithm, which lets us put nodes from different
    42  	// clusters in the same rank.
    43  	fmt.Fprintln(w, "newrank=true;")
    44  	fmt.Fprintln(w, "node [shape=box, ordering=out];")
    45  
    46  	w.Write(enc.w.Bytes())
    47  	fmt.Fprintln(w, "}")
    48  }
    49  
    50  func (enc *dotEncoder) writeSvg(w io.Writer) error {
    51  	cmd := exec.Command("dot", "-Tsvg")
    52  	in, err := cmd.StdinPipe()
    53  	if err != nil {
    54  		return err
    55  	}
    56  	var out bytes.Buffer
    57  	cmd.Stdout = &out
    58  	cmd.Stderr = os.Stderr
    59  	if err := cmd.Start(); err != nil {
    60  		return err
    61  	}
    62  	enc.writeTo(in)
    63  	in.Close()
    64  	if err := cmd.Wait(); err != nil {
    65  		return err
    66  	}
    67  	// Trim SVG header so the result can be embedded
    68  	//
    69  	// TODO: In Graphviz 10.0.1, we could use -Tsvg_inline.
    70  	svg := out.Bytes()
    71  	if i := bytes.Index(svg, []byte("<svg ")); i >= 0 {
    72  		svg = svg[i:]
    73  	}
    74  	_, err = w.Write(svg)
    75  	return err
    76  }
    77  
    78  func (enc *dotEncoder) newID(f string) string {
    79  	id := fmt.Sprintf(f, enc.idGen)
    80  	enc.idGen++
    81  	return id
    82  }
    83  
    84  func (enc *dotEncoder) node(label, sublabel string) string {
    85  	id := enc.newID("n%d")
    86  	l := html.EscapeString(label)
    87  	if sublabel != "" {
    88  		l += fmt.Sprintf("<BR ALIGN=\"CENTER\"/><FONT POINT-SIZE=\"10\">%s</FONT>", html.EscapeString(sublabel))
    89  	}
    90  	fmt.Fprintf(enc.w, "%s [label=<%s>];\n", id, l)
    91  	return id
    92  }
    93  
    94  func (enc *dotEncoder) edge(from, to string, label string, args ...any) {
    95  	l := fmt.Sprintf(label, args...)
    96  	fmt.Fprintf(enc.w, "%s -> %s [label=%q];\n", from, to, l)
    97  }
    98  
    99  func (enc *dotEncoder) subgraph(v *Value) (vID, cID string) {
   100  	enc.valLimit = maxNodes
   101  	cID = enc.newID("cluster_%d")
   102  	fmt.Fprintf(enc.w, "subgraph %s {\n", cID)
   103  	fmt.Fprintf(enc.w, "style=invis;")
   104  	vID = enc.value(v)
   105  	fmt.Fprintf(enc.w, "}\n")
   106  	return
   107  }
   108  
   109  func (enc *dotEncoder) value(v *Value) string {
   110  	if enc.valLimit <= 0 {
   111  		id := enc.newID("n%d")
   112  		fmt.Fprintf(enc.w, "%s [label=\"...\", shape=triangle];\n", id)
   113  		return id
   114  	}
   115  	enc.valLimit--
   116  
   117  	switch vd := v.Domain.(type) {
   118  	default:
   119  		panic(fmt.Sprintf("unknown domain type %T", vd))
   120  
   121  	case nil:
   122  		return enc.node("_|_", "")
   123  
   124  	case Top:
   125  		return enc.node("_", "")
   126  
   127  		// TODO: Like in YAML, figure out if this is just a sum. In dot, we
   128  		// could say any unentangled variable is a sum, and if it has more than
   129  		// one reference just share the node.
   130  
   131  	// case Sum:
   132  	// 	node := enc.node("Sum", "")
   133  	// 	for i, elt := range vd.vs {
   134  	// 		enc.edge(node, enc.value(elt), "%d", i)
   135  	// 		if enc.valLimit <= 0 {
   136  	// 			break
   137  	// 		}
   138  	// 	}
   139  	// 	return node
   140  
   141  	case Def:
   142  		node := enc.node("Def", "")
   143  		for k, v := range vd.All() {
   144  			enc.edge(node, enc.value(v), "%s", k)
   145  			if enc.valLimit <= 0 {
   146  				break
   147  			}
   148  		}
   149  		return node
   150  
   151  	case Tuple:
   152  		if vd.repeat == nil {
   153  			label := "Tuple"
   154  			node := enc.node(label, "")
   155  			for i, elt := range vd.vs {
   156  				enc.edge(node, enc.value(elt), "%d", i)
   157  				if enc.valLimit <= 0 {
   158  					break
   159  				}
   160  			}
   161  			return node
   162  		} else {
   163  			// TODO
   164  			return enc.node("TODO: Repeat", "")
   165  		}
   166  
   167  	case String:
   168  		switch vd.kind {
   169  		case stringExact:
   170  			return enc.node(fmt.Sprintf("%q", vd.exact), "")
   171  		case stringRegex:
   172  			var parts []string
   173  			for _, re := range vd.re {
   174  				parts = append(parts, fmt.Sprintf("%q", re))
   175  			}
   176  			return enc.node(strings.Join(parts, "&"), "")
   177  		}
   178  		panic("bad String kind")
   179  
   180  	case Var:
   181  		return enc.node(fmt.Sprintf("Var %s", enc.idp.unique(vd.id)), "")
   182  	}
   183  }