// Copyright 2024 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 dwarf import ( "bytes" "flag" "fmt" "go/ast" "go/format" "go/parser" "go/printer" "go/token" "os" "strconv" "strings" "testing" ) const pvagenfile = "./putvarabbrevgen.go" var pvaDoGenerate bool func TestMain(m *testing.M) { flag.BoolVar(&pvaDoGenerate, "generate", false, "regenerates "+pvagenfile) flag.Parse() os.Exit(m.Run()) } // TestPutVarAbbrevGenerator checks that putvarabbrevgen.go is kept in sync // with the contents of functions putvar and putAbstractVar. If test flag -generate // is specified the file is regenerated instead. // // The block of code in putvar and putAbstractVar that picks the correct // abbrev is also generated automatically by this function by looking at all // the possible paths in their CFG and the order in which putattr is called. // // There are some restrictions on how putattr can be used in putvar and // putAbstractVar: // // 1. it shouldn't appear inside a for or switch statements // 2. it can appear within any number of nested if/else statements but the // conditionals must not change after putvarAbbrev/putAbstractVarAbbrev // are called // 3. the form argument of putattr must be a compile time constant // 4. each putattr call must be followed by a comment containing the name of // the attribute it is setting // // TestPutVarAbbrevGenerator will fail if (1) or (4) are not respected and // the generated code will not compile if (3) is violated. Violating (2) // will result in code silently wrong code (which will usually be detected // by one of the tests that parse debug_info). func TestPutVarAbbrevGenerator(t *testing.T) { spvagenfile := pvagenerate(t) if pvaDoGenerate { err := os.WriteFile(pvagenfile, []byte(spvagenfile), 0660) if err != nil { t.Fatal(err) } return } slurp := func(name string) string { out, err := os.ReadFile(name) if err != nil { t.Fatal(err) } return string(out) } if spvagenfile != slurp(pvagenfile) { t.Error(pvagenfile + " is out of date") } } func pvagenerate(t *testing.T) string { var fset token.FileSet f, err := parser.ParseFile(&fset, "./dwarf.go", nil, parser.ParseComments) if err != nil { t.Fatal(err) } cm := ast.NewCommentMap(&fset, f, f.Comments) abbrevs := make(map[string]int) funcs := map[string]ast.Stmt{} for _, decl := range f.Decls { decl, ok := decl.(*ast.FuncDecl) if !ok || decl.Body == nil { continue } if decl.Name.Name == "putvar" || decl.Name.Name == "putAbstractVar" { // construct the simplified CFG pvagraph, _ := pvacfgbody(t, &fset, cm, decl.Body.List) funcs[decl.Name.Name+"Abbrev"] = pvacfgvisit(pvagraph, abbrevs) } } abbrevslice := make([]string, len(abbrevs)) for abbrev, n := range abbrevs { abbrevslice[n] = abbrev } buf := new(bytes.Buffer) fmt.Fprint(buf, `// Code generated by TestPutVarAbbrevGenerator. DO NOT EDIT. // Regenerate using go test -run TestPutVarAbbrevGenerator -generate instead. package dwarf var putvarAbbrevs = []dwAbbrev{ `) for _, abbrev := range abbrevslice { fmt.Fprint(buf, abbrev+",\n") } fmt.Fprint(buf, "\n}\n\n") fmt.Fprint(buf, "func putAbstractVarAbbrev(v *Var) int {\n") format.Node(buf, &token.FileSet{}, funcs["putAbstractVarAbbrev"]) fmt.Fprint(buf, "}\n\n") fmt.Fprint(buf, "func putvarAbbrev(v *Var, concrete, withLoclist bool) int {\n") format.Node(buf, &token.FileSet{}, funcs["putvarAbbrev"]) fmt.Fprint(buf, "}\n") out, err := format.Source(buf.Bytes()) if err != nil { t.Log(string(buf.Bytes())) t.Fatal(err) } return string(out) } type pvacfgnode struct { attr, form string cond ast.Expr then, els *pvacfgnode } // pvacfgbody generates a simplified CFG for a slice of statements, // containing only calls to putattr and the if statements affecting them. func pvacfgbody(t *testing.T, fset *token.FileSet, cm ast.CommentMap, body []ast.Stmt) (start, end *pvacfgnode) { add := func(n *pvacfgnode) { if start == nil || end == nil { start = n end = n } else { end.then = n end = n } } for _, stmt := range body { switch stmt := stmt.(type) { case *ast.ExprStmt: if x, _ := stmt.X.(*ast.CallExpr); x != nil { funstr := exprToString(x.Fun) if funstr == "putattr" { form, _ := x.Args[3].(*ast.Ident) if form == nil { t.Fatalf("%s invalid use of putattr", fset.Position(x.Pos())) } cmt := findLineComment(cm, stmt) if cmt == nil { t.Fatalf("%s invalid use of putattr (no comment containing the attribute name)", fset.Position(x.Pos())) } add(&pvacfgnode{attr: strings.TrimSpace(cmt.Text[2:]), form: form.Name}) } } case *ast.IfStmt: ifStart, ifEnd := pvacfgif(t, fset, cm, stmt) if ifStart != nil { add(ifStart) end = ifEnd } default: // check that nothing under this contains a putattr call ast.Inspect(stmt, func(n ast.Node) bool { if call, _ := n.(*ast.CallExpr); call != nil { if exprToString(call.Fun) == "putattr" { t.Fatalf("%s use of putattr in unsupported block", fset.Position(call.Pos())) } } return true }) } } return start, end } func pvacfgif(t *testing.T, fset *token.FileSet, cm ast.CommentMap, ifstmt *ast.IfStmt) (start, end *pvacfgnode) { thenStart, thenEnd := pvacfgbody(t, fset, cm, ifstmt.Body.List) var elseStart, elseEnd *pvacfgnode if ifstmt.Else != nil { switch els := ifstmt.Else.(type) { case *ast.IfStmt: elseStart, elseEnd = pvacfgif(t, fset, cm, els) case *ast.BlockStmt: elseStart, elseEnd = pvacfgbody(t, fset, cm, els.List) default: t.Fatalf("%s: unexpected statement %T", fset.Position(els.Pos()), els) } } if thenStart != nil && elseStart != nil && thenStart == thenEnd && elseStart == elseEnd && thenStart.form == elseStart.form && thenStart.attr == elseStart.attr { return thenStart, thenEnd } if thenStart != nil || elseStart != nil { start = &pvacfgnode{cond: ifstmt.Cond} end = &pvacfgnode{} if thenStart != nil { start.then = thenStart thenEnd.then = end } else { start.then = end } if elseStart != nil { start.els = elseStart elseEnd.then = end } else { start.els = end } } return start, end } func exprToString(t ast.Expr) string { var buf bytes.Buffer printer.Fprint(&buf, token.NewFileSet(), t) return buf.String() } // findLineComment finds the line comment for statement stmt. func findLineComment(cm ast.CommentMap, stmt *ast.ExprStmt) *ast.Comment { var r *ast.Comment for _, cmtg := range cm[stmt] { for _, cmt := range cmtg.List { if cmt.Slash > stmt.Pos() { if r != nil { return nil } r = cmt } } } return r } // pvacfgvisit visits the CFG depth first, populates abbrevs with all // possible dwAbbrev definitions and returns a tree of if/else statements // that picks the correct abbrev. func pvacfgvisit(pvacfg *pvacfgnode, abbrevs map[string]int) ast.Stmt { r := &ast.IfStmt{Cond: &ast.BinaryExpr{ Op: token.EQL, X: &ast.SelectorExpr{X: &ast.Ident{Name: "v"}, Sel: &ast.Ident{Name: "Tag"}}, Y: &ast.Ident{Name: "DW_TAG_variable"}}} r.Body = &ast.BlockStmt{List: []ast.Stmt{ pvacfgvisitnode(pvacfg, "DW_TAG_variable", []*pvacfgnode{}, abbrevs), }} r.Else = &ast.BlockStmt{List: []ast.Stmt{ pvacfgvisitnode(pvacfg, "DW_TAG_formal_parameter", []*pvacfgnode{}, abbrevs), }} return r } func pvacfgvisitnode(pvacfg *pvacfgnode, tag string, path []*pvacfgnode, abbrevs map[string]int) ast.Stmt { if pvacfg == nil { abbrev := toabbrev(tag, path) if _, ok := abbrevs[abbrev]; !ok { abbrevs[abbrev] = len(abbrevs) } return &ast.ReturnStmt{ Results: []ast.Expr{&ast.BinaryExpr{ Op: token.ADD, X: &ast.Ident{Name: "DW_ABRV_PUTVAR_START"}, Y: &ast.BasicLit{Kind: token.INT, Value: strconv.Itoa(abbrevs[abbrev])}}}} } if pvacfg.attr != "" { return pvacfgvisitnode(pvacfg.then, tag, append(path, pvacfg), abbrevs) } else if pvacfg.cond != nil { if bx, _ := pvacfg.cond.(*ast.BinaryExpr); bx != nil && bx.Op == token.EQL && exprToString(bx.X) == "v.Tag" { // this condition is "v.Tag == Xxx", check the value of 'tag' y := exprToString(bx.Y) if y == tag { return pvacfgvisitnode(pvacfg.then, tag, path, abbrevs) } else { return pvacfgvisitnode(pvacfg.els, tag, path, abbrevs) } } else { r := &ast.IfStmt{Cond: pvacfg.cond} r.Body = &ast.BlockStmt{List: []ast.Stmt{pvacfgvisitnode(pvacfg.then, tag, path, abbrevs)}} r.Else = &ast.BlockStmt{List: []ast.Stmt{pvacfgvisitnode(pvacfg.els, tag, path, abbrevs)}} return r } } else { return pvacfgvisitnode(pvacfg.then, tag, path, abbrevs) } } func toabbrev(tag string, path []*pvacfgnode) string { buf := new(bytes.Buffer) fmt.Fprintf(buf, "{\n%s,\nDW_CHILDREN_no,\n[]dwAttrForm{\n", tag) for _, node := range path { if node.cond == nil { fmt.Fprintf(buf, "{%s, %s},\n", node.attr, node.form) } } fmt.Fprint(buf, "},\n}") return buf.String() }