/
instantiate.go
203 lines (191 loc) · 6.45 KB
/
instantiate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package gen
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/types"
"path"
"path/filepath"
"strings"
"golang.org/x/tools/go/ast/astutil"
)
// Instantiate instantiates the generic package, giving the resulting package the given name.
// The bindings are a map from generic parameter name to the instantiated type of the parameter.
// After Instantiate returns, the AST of pkg has been altered, the FileSet has been updated to
// match and the new package has been typechecked.
// Note: the AST may be altered even if Instantiate returns an error.
func Instantiate(pkg *Package, pkgName string, bindings map[string]types.Type) error {
bindingList, err := newBindingList(bindings, pkg)
if err != nil {
return err
}
if err := substitutePackage(pkg, bindingList, pkgName); err != nil {
return err
}
for _, b := range bindingList {
if !b.found {
return fmt.Errorf("no type parameter %s in %s", b.param, pkg.Path)
}
}
if err := pkg.Apkg.reload(); err != nil {
return err
}
tpkg, info, err := typecheckPackage("dummy_import_path/"+pkgName, pkg.Apkg, theImporter)
if err != nil {
return err
}
pkg.Tpkg = tpkg
pkg.info = info
return nil
}
func newBindingList(bindings map[string]types.Type, pkg *Package) ([]*Binding, error) {
var bindingList []*Binding
for param, argType := range bindings {
b, err := newBinding(param, argType, pkg)
if err != nil {
return nil, err
}
bindingList = append(bindingList, b)
}
return bindingList, nil
}
// Modifies the asts in pkg. pkgName is the new package name.
func substitutePackage(pkg *Package, bindings []*Binding, pkgName string) error {
rws := makeRewriteRules(bindings)
pkg.Apkg.pkg.Name = pkgName
for _, file := range pkg.Apkg.pkg.Files {
if err := substituteFile(file, bindings, rws, pkg, pkgName, true); err != nil {
return err
}
}
return nil
}
func makeRewriteRules(bindings []*Binding) []rewrite {
var rws []rewrite
for _, b := range bindings {
for _, am := range augmentedMethods(b.arg) {
rws = append(rws, rewrite{
argType: b.param.Type(),
methodName: am.name,
op: am.tok,
})
}
}
return rws
}
// substituteFile changes the AST of file to reflect the bindings and rewrites.
func substituteFile(file *ast.File, bindings []*Binding, rewrites []rewrite, pkg *Package, pkgName string, addImports bool) error {
for _, b := range bindings {
typeSpec := findTypeDecl(b.param, file)
if typeSpec == nil {
continue
}
b.found = true
if addImports {
// Add an import for the argument type if necessary.
// TODO: the named type from another package might be embedded in the type, like map[int]geo.Point.
if named, ok := b.arg.(*types.Named); ok {
tn := named.Obj()
name := tn.Pkg().Name()
ipath := tn.Pkg().Path()
if name == path.Base(ipath) {
name = ""
}
astutil.AddNamedImport(pkg.Apkg.fset, file, name, ipath)
}
}
// Turn the type spec into an alias if it isn't already.
if !typeSpec.Assign.IsValid() {
typeSpec.Assign = typeSpec.Type.Pos()
}
typeSpec.Type = typeToExpr(b.arg, pkg.Tpkg)
}
file.Name.Name = pkgName
if err := replaceCode(file, bindings, rewrites, pkg); err != nil {
return err
}
trimImports(pkg.Apkg.fset, file)
return nil
}
// Return the declaration of param in file, or nil if not present.
func findTypeDecl(param *types.TypeName, file *ast.File) *ast.TypeSpec {
paramPath, _ := astutil.PathEnclosingInterval(file, param.Pos(), param.Pos())
if len(paramPath) < 2 {
// Type decl for param is not in this file.
return nil
}
return paramPath[1].(*ast.TypeSpec)
}
// instantiateInto instantiates generic package gpkg into the package dest.
// The filenames and symbols of pkg are prefixed with the generic import's name.
// gpkg's AST is messed up afterwards.
// TODO: name collisions between the new names we create and existing names in dest (including filenames).
func instantiateInto(gpkg *Package, gimp genericImport, bindings map[string]types.Type, dest *astPackage) error {
bindingList, err := newBindingList(bindings, gpkg)
if err != nil {
return err
}
if err := substitutePackageInto(gpkg, bindingList, gimp, dest); err != nil {
return err
}
for _, b := range bindingList {
if !b.found {
return fmt.Errorf("no type parameter %s in %s", b.param, gpkg.Path)
}
}
return dest.reload()
}
func substitutePackageInto(src *Package, bindings []*Binding, gimp genericImport, dest *astPackage) error {
prefix := gimp.name + "_"
// Remove filenames with prefix from dest, because they are from an earlier instantiation.
for filename := range dest.pkg.Files {
if strings.HasPrefix(filename, prefix) {
delete(dest.pkg.Files, filename)
}
}
// For each file in dest that imports src, replace references to the import identifier with
// prefixed symbols.
// We already checked that all generic imports matching gimp use the same name.
for _, gi := range dest.genericImports {
if gi.name == gimp.name {
replaceImportWithPrefix(gi.file, gimp.name, prefix)
cmap := ast.NewCommentMap(dest.fset, gi.file, gi.file.Comments)
// TODO: Instead of deleting the import, comment it out. That will
// preserve line numbers in error messages.
astutil.DeleteNamedImport(dest.fset, gi.file, gi.name, gi.path)
gi.file.Comments = cmap.Filter(gi.file).Comments()
}
}
// Perform normal substitution on the files of src, and also prefix all top-level symbols.
// Add the modified files of src to dest.
rws := makeRewriteRules(bindings)
for filename, file := range src.Apkg.pkg.Files {
if err := substituteFileInto(filename, file, bindings, rws, src, dest, prefix); err != nil {
return err
}
}
return nil
}
func substituteFileInto(filename string, file *ast.File, bindings []*Binding, rewrites []rewrite, src *Package, dest *astPackage, prefix string) error {
if err := substituteFile(file, bindings, rewrites, src, dest.pkg.Name, false); err != nil {
return err
}
prefixTopLevelSymbols(file, prefix)
// Format file with the src FileSet, then parse it with the dest FileSet. If
// we don't do this before we add file to dest, then its positions are
// interpreted relative to the wrong FileSet, and that can actually result in
// syntactically invalid code.
var buf bytes.Buffer
if err := format.Node(&buf, src.Apkg.fset, file); err != nil {
return err
}
newFilename := filepath.Join(dest.dir, prefix+filepath.Base(filename))
newFile, err := parser.ParseFile(dest.fset, newFilename, &buf, parser.ParseComments)
if err != nil {
return err
}
dest.pkg.Files[newFilename] = newFile
return nil
}