1// Copyright 2024 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
5package dwarf
6
7import (
8	"bytes"
9	"flag"
10	"fmt"
11	"go/ast"
12	"go/format"
13	"go/parser"
14	"go/printer"
15	"go/token"
16	"os"
17	"strconv"
18	"strings"
19	"testing"
20)
21
22const pvagenfile = "./putvarabbrevgen.go"
23
24var pvaDoGenerate bool
25
26func TestMain(m *testing.M) {
27	flag.BoolVar(&pvaDoGenerate, "generate", false, "regenerates "+pvagenfile)
28	flag.Parse()
29	os.Exit(m.Run())
30
31}
32
33// TestPutVarAbbrevGenerator checks that putvarabbrevgen.go is kept in sync
34// with the contents of functions putvar and putAbstractVar. If test flag -generate
35// is specified the file is regenerated instead.
36//
37// The block of code in putvar and putAbstractVar that picks the correct
38// abbrev is also generated automatically by this function by looking at all
39// the possible paths in their CFG and the order in which putattr is called.
40//
41// There are some restrictions on how putattr can be used in putvar and
42// putAbstractVar:
43//
44//  1. it shouldn't appear inside a for or switch statements
45//  2. it can appear within any number of nested if/else statements but the
46//     conditionals must not change after putvarAbbrev/putAbstractVarAbbrev
47//     are called
48//  3. the form argument of putattr must be a compile time constant
49//  4. each putattr call must be followed by a comment containing the name of
50//     the attribute it is setting
51//
52// TestPutVarAbbrevGenerator will fail if (1) or (4) are not respected and
53// the generated code will not compile if (3) is violated. Violating (2)
54// will result in code silently wrong code (which will usually be detected
55// by one of the tests that parse debug_info).
56func TestPutVarAbbrevGenerator(t *testing.T) {
57	spvagenfile := pvagenerate(t)
58
59	if pvaDoGenerate {
60		err := os.WriteFile(pvagenfile, []byte(spvagenfile), 0660)
61		if err != nil {
62			t.Fatal(err)
63		}
64		return
65	}
66
67	slurp := func(name string) string {
68		out, err := os.ReadFile(name)
69		if err != nil {
70			t.Fatal(err)
71		}
72		return string(out)
73	}
74
75	if spvagenfile != slurp(pvagenfile) {
76		t.Error(pvagenfile + " is out of date")
77	}
78
79}
80
81func pvagenerate(t *testing.T) string {
82	var fset token.FileSet
83	f, err := parser.ParseFile(&fset, "./dwarf.go", nil, parser.ParseComments)
84	if err != nil {
85		t.Fatal(err)
86	}
87	cm := ast.NewCommentMap(&fset, f, f.Comments)
88	abbrevs := make(map[string]int)
89	funcs := map[string]ast.Stmt{}
90	for _, decl := range f.Decls {
91		decl, ok := decl.(*ast.FuncDecl)
92		if !ok || decl.Body == nil {
93			continue
94		}
95		if decl.Name.Name == "putvar" || decl.Name.Name == "putAbstractVar" {
96			// construct the simplified CFG
97			pvagraph, _ := pvacfgbody(t, &fset, cm, decl.Body.List)
98			funcs[decl.Name.Name+"Abbrev"] = pvacfgvisit(pvagraph, abbrevs)
99		}
100	}
101	abbrevslice := make([]string, len(abbrevs))
102	for abbrev, n := range abbrevs {
103		abbrevslice[n] = abbrev
104	}
105
106	buf := new(bytes.Buffer)
107	fmt.Fprint(buf, `// Code generated by TestPutVarAbbrevGenerator. DO NOT EDIT.
108// Regenerate using go test -run TestPutVarAbbrevGenerator -generate instead.
109
110package dwarf
111
112var putvarAbbrevs = []dwAbbrev{
113`)
114
115	for _, abbrev := range abbrevslice {
116		fmt.Fprint(buf, abbrev+",\n")
117	}
118
119	fmt.Fprint(buf, "\n}\n\n")
120
121	fmt.Fprint(buf, "func putAbstractVarAbbrev(v *Var) int {\n")
122	format.Node(buf, &token.FileSet{}, funcs["putAbstractVarAbbrev"])
123	fmt.Fprint(buf, "}\n\n")
124
125	fmt.Fprint(buf, "func putvarAbbrev(v *Var, concrete, withLoclist bool) int {\n")
126	format.Node(buf, &token.FileSet{}, funcs["putvarAbbrev"])
127	fmt.Fprint(buf, "}\n")
128
129	out, err := format.Source(buf.Bytes())
130	if err != nil {
131		t.Log(string(buf.Bytes()))
132		t.Fatal(err)
133	}
134
135	return string(out)
136}
137
138type pvacfgnode struct {
139	attr, form string
140
141	cond      ast.Expr
142	then, els *pvacfgnode
143}
144
145// pvacfgbody generates a simplified CFG for a slice of statements,
146// containing only calls to putattr and the if statements affecting them.
147func pvacfgbody(t *testing.T, fset *token.FileSet, cm ast.CommentMap, body []ast.Stmt) (start, end *pvacfgnode) {
148	add := func(n *pvacfgnode) {
149		if start == nil || end == nil {
150			start = n
151			end = n
152		} else {
153			end.then = n
154			end = n
155		}
156	}
157	for _, stmt := range body {
158		switch stmt := stmt.(type) {
159		case *ast.ExprStmt:
160			if x, _ := stmt.X.(*ast.CallExpr); x != nil {
161				funstr := exprToString(x.Fun)
162				if funstr == "putattr" {
163					form, _ := x.Args[3].(*ast.Ident)
164					if form == nil {
165						t.Fatalf("%s invalid use of putattr", fset.Position(x.Pos()))
166					}
167					cmt := findLineComment(cm, stmt)
168					if cmt == nil {
169						t.Fatalf("%s invalid use of putattr (no comment containing the attribute name)", fset.Position(x.Pos()))
170					}
171					add(&pvacfgnode{attr: strings.TrimSpace(cmt.Text[2:]), form: form.Name})
172				}
173			}
174		case *ast.IfStmt:
175			ifStart, ifEnd := pvacfgif(t, fset, cm, stmt)
176			if ifStart != nil {
177				add(ifStart)
178				end = ifEnd
179			}
180		default:
181			// check that nothing under this contains a putattr call
182			ast.Inspect(stmt, func(n ast.Node) bool {
183				if call, _ := n.(*ast.CallExpr); call != nil {
184					if exprToString(call.Fun) == "putattr" {
185						t.Fatalf("%s use of putattr in unsupported block", fset.Position(call.Pos()))
186					}
187				}
188				return true
189			})
190		}
191	}
192	return start, end
193}
194
195func pvacfgif(t *testing.T, fset *token.FileSet, cm ast.CommentMap, ifstmt *ast.IfStmt) (start, end *pvacfgnode) {
196	thenStart, thenEnd := pvacfgbody(t, fset, cm, ifstmt.Body.List)
197	var elseStart, elseEnd *pvacfgnode
198	if ifstmt.Else != nil {
199		switch els := ifstmt.Else.(type) {
200		case *ast.IfStmt:
201			elseStart, elseEnd = pvacfgif(t, fset, cm, els)
202		case *ast.BlockStmt:
203			elseStart, elseEnd = pvacfgbody(t, fset, cm, els.List)
204		default:
205			t.Fatalf("%s: unexpected statement %T", fset.Position(els.Pos()), els)
206		}
207	}
208
209	if thenStart != nil && elseStart != nil && thenStart == thenEnd && elseStart == elseEnd && thenStart.form == elseStart.form && thenStart.attr == elseStart.attr {
210		return thenStart, thenEnd
211	}
212
213	if thenStart != nil || elseStart != nil {
214		start = &pvacfgnode{cond: ifstmt.Cond}
215		end = &pvacfgnode{}
216		if thenStart != nil {
217			start.then = thenStart
218			thenEnd.then = end
219		} else {
220			start.then = end
221		}
222		if elseStart != nil {
223			start.els = elseStart
224			elseEnd.then = end
225		} else {
226			start.els = end
227		}
228	}
229	return start, end
230}
231
232func exprToString(t ast.Expr) string {
233	var buf bytes.Buffer
234	printer.Fprint(&buf, token.NewFileSet(), t)
235	return buf.String()
236}
237
238// findLineComment finds the line comment for statement stmt.
239func findLineComment(cm ast.CommentMap, stmt *ast.ExprStmt) *ast.Comment {
240	var r *ast.Comment
241	for _, cmtg := range cm[stmt] {
242		for _, cmt := range cmtg.List {
243			if cmt.Slash > stmt.Pos() {
244				if r != nil {
245					return nil
246				}
247				r = cmt
248			}
249		}
250	}
251	return r
252}
253
254// pvacfgvisit visits the CFG depth first, populates abbrevs with all
255// possible dwAbbrev definitions and returns a tree of if/else statements
256// that picks the correct abbrev.
257func pvacfgvisit(pvacfg *pvacfgnode, abbrevs map[string]int) ast.Stmt {
258	r := &ast.IfStmt{Cond: &ast.BinaryExpr{
259		Op: token.EQL,
260		X:  &ast.SelectorExpr{X: &ast.Ident{Name: "v"}, Sel: &ast.Ident{Name: "Tag"}},
261		Y:  &ast.Ident{Name: "DW_TAG_variable"}}}
262	r.Body = &ast.BlockStmt{List: []ast.Stmt{
263		pvacfgvisitnode(pvacfg, "DW_TAG_variable", []*pvacfgnode{}, abbrevs),
264	}}
265	r.Else = &ast.BlockStmt{List: []ast.Stmt{
266		pvacfgvisitnode(pvacfg, "DW_TAG_formal_parameter", []*pvacfgnode{}, abbrevs),
267	}}
268	return r
269}
270
271func pvacfgvisitnode(pvacfg *pvacfgnode, tag string, path []*pvacfgnode, abbrevs map[string]int) ast.Stmt {
272	if pvacfg == nil {
273		abbrev := toabbrev(tag, path)
274		if _, ok := abbrevs[abbrev]; !ok {
275			abbrevs[abbrev] = len(abbrevs)
276		}
277		return &ast.ReturnStmt{
278			Results: []ast.Expr{&ast.BinaryExpr{
279				Op: token.ADD,
280				X:  &ast.Ident{Name: "DW_ABRV_PUTVAR_START"},
281				Y:  &ast.BasicLit{Kind: token.INT, Value: strconv.Itoa(abbrevs[abbrev])}}}}
282	}
283	if pvacfg.attr != "" {
284		return pvacfgvisitnode(pvacfg.then, tag, append(path, pvacfg), abbrevs)
285	} else if pvacfg.cond != nil {
286		if bx, _ := pvacfg.cond.(*ast.BinaryExpr); bx != nil && bx.Op == token.EQL && exprToString(bx.X) == "v.Tag" {
287			// this condition is "v.Tag == Xxx", check the value of 'tag'
288			y := exprToString(bx.Y)
289			if y == tag {
290				return pvacfgvisitnode(pvacfg.then, tag, path, abbrevs)
291			} else {
292				return pvacfgvisitnode(pvacfg.els, tag, path, abbrevs)
293			}
294		} else {
295			r := &ast.IfStmt{Cond: pvacfg.cond}
296			r.Body = &ast.BlockStmt{List: []ast.Stmt{pvacfgvisitnode(pvacfg.then, tag, path, abbrevs)}}
297			r.Else = &ast.BlockStmt{List: []ast.Stmt{pvacfgvisitnode(pvacfg.els, tag, path, abbrevs)}}
298			return r
299		}
300	} else {
301		return pvacfgvisitnode(pvacfg.then, tag, path, abbrevs)
302	}
303}
304
305func toabbrev(tag string, path []*pvacfgnode) string {
306	buf := new(bytes.Buffer)
307	fmt.Fprintf(buf, "{\n%s,\nDW_CHILDREN_no,\n[]dwAttrForm{\n", tag)
308	for _, node := range path {
309		if node.cond == nil {
310			fmt.Fprintf(buf, "{%s, %s},\n", node.attr, node.form)
311
312		}
313	}
314	fmt.Fprint(buf, "},\n}")
315	return buf.String()
316}
317