go/internal/gcimporter: add "bundled" export data formats

Export data is self-contained, which is convenient for ensuring the
compiler only needs to open export data files directly referenced in
source file import declarations. But it does have a consequence that
export data files can redundantly encode information.

This CL adds a new "bundled" export data format that allows encoding
multiple complete packages together so they can share transitive
declarations. This is intended to be useful for storing a compilation
unit's source files along with any export data they depend upon for
efficient re-type-checking and analysis later.

Change-Id: I720a12cb051767f9d7ce95d645b448043a1e2167
Reviewed-on: https://go-review.googlesource.com/c/tools/+/292352
Trust: Matthew Dempsky <mdempsky@google.com>
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go
index 0e42514..04b65b6 100644
--- a/go/gcexportdata/gcexportdata.go
+++ b/go/gcexportdata/gcexportdata.go
@@ -105,3 +105,25 @@
 	}
 	return gcimporter.IExportData(out, fset, pkg)
 }
+
+// ReadBundle reads an export bundle from in, decodes it, and returns type
+// information for the packages.
+// File position information is added to fset.
+//
+// ReadBundle may inspect and add to the imports map to ensure that references
+// within the export bundle to other packages are consistent.
+//
+// On return, the state of the reader is undefined.
+func ReadBundle(in io.Reader, fset *token.FileSet, imports map[string]*types.Package) ([]*types.Package, error) {
+	data, err := ioutil.ReadAll(in)
+	if err != nil {
+		return nil, fmt.Errorf("reading export bundle: %v", err)
+	}
+	return gcimporter.IImportBundle(fset, imports, data)
+}
+
+// WriteBundle writes encoded type information for the specified packages to out.
+// The FileSet provides file position information for named objects.
+func WriteBundle(out io.Writer, fset *token.FileSet, pkgs []*types.Package) error {
+	return gcimporter.IExportBundle(out, fset, pkgs)
+}
diff --git a/go/internal/gcimporter/iexport.go b/go/internal/gcimporter/iexport.go
index c4fae42..d2fc8b6 100644
--- a/go/internal/gcimporter/iexport.go
+++ b/go/internal/gcimporter/iexport.go
@@ -25,12 +25,25 @@
 // 0: Go1.11 encoding
 const iexportVersion = 0
 
+// Current bundled export format version. Increase with each format change.
+// 0: initial implementation
+const bundleVersion = 0
+
 // IExportData writes indexed export data for pkg to out.
 //
 // If no file set is provided, position info will be missing.
 // The package path of the top-level package will not be recorded,
 // so that calls to IImportData can override with a provided package path.
-func IExportData(out io.Writer, fset *token.FileSet, pkg *types.Package) (err error) {
+func IExportData(out io.Writer, fset *token.FileSet, pkg *types.Package) error {
+	return iexportCommon(out, fset, false, []*types.Package{pkg})
+}
+
+// IExportBundle writes an indexed export bundle for pkgs to out.
+func IExportBundle(out io.Writer, fset *token.FileSet, pkgs []*types.Package) error {
+	return iexportCommon(out, fset, true, pkgs)
+}
+
+func iexportCommon(out io.Writer, fset *token.FileSet, bundle bool, pkgs []*types.Package) (err error) {
 	defer func() {
 		if e := recover(); e != nil {
 			if ierr, ok := e.(internalError); ok {
@@ -48,7 +61,9 @@
 		stringIndex: map[string]uint64{},
 		declIndex:   map[types.Object]uint64{},
 		typIndex:    map[types.Type]uint64{},
-		localpkg:    pkg,
+	}
+	if !bundle {
+		p.localpkg = pkgs[0]
 	}
 
 	for i, pt := range predeclared() {
@@ -59,10 +74,20 @@
 	}
 
 	// Initialize work queue with exported declarations.
-	scope := pkg.Scope()
-	for _, name := range scope.Names() {
-		if ast.IsExported(name) {
-			p.pushDecl(scope.Lookup(name))
+	for _, pkg := range pkgs {
+		scope := pkg.Scope()
+		for _, name := range scope.Names() {
+			if ast.IsExported(name) {
+				p.pushDecl(scope.Lookup(name))
+			}
+		}
+
+		if bundle {
+			// Ensure pkg and its imports are included in the index.
+			p.allPkgs[pkg] = true
+			for _, imp := range pkg.Imports() {
+				p.allPkgs[imp] = true
+			}
 		}
 	}
 
@@ -75,10 +100,25 @@
 	dataLen := uint64(p.data0.Len())
 	w := p.newWriter()
 	w.writeIndex(p.declIndex)
+
+	if bundle {
+		w.uint64(uint64(len(pkgs)))
+		for _, pkg := range pkgs {
+			w.pkg(pkg)
+			imps := pkg.Imports()
+			w.uint64(uint64(len(imps)))
+			for _, imp := range imps {
+				w.pkg(imp)
+			}
+		}
+	}
 	w.flush()
 
 	// Assemble header.
 	var hdr intWriter
+	if bundle {
+		hdr.uint64(bundleVersion)
+	}
 	hdr.uint64(iexportVersion)
 	hdr.uint64(uint64(p.strings.Len()))
 	hdr.uint64(dataLen)
@@ -102,7 +142,9 @@
 	// For the main index, make sure to include every package that
 	// we reference, even if we're not exporting (or reexporting)
 	// any symbols from it.
-	pkgObjs[w.p.localpkg] = nil
+	if w.p.localpkg != nil {
+		pkgObjs[w.p.localpkg] = nil
+	}
 	for pkg := range w.p.allPkgs {
 		pkgObjs[pkg] = nil
 	}
diff --git a/go/internal/gcimporter/iexport_test.go b/go/internal/gcimporter/iexport_test.go
index 8284ede..b6b4001 100644
--- a/go/internal/gcimporter/iexport_test.go
+++ b/go/internal/gcimporter/iexport_test.go
@@ -134,6 +134,21 @@
 			testPkgData(t, conf.Fset, pkg, exportdata)
 		}
 	}
+
+	var bundle bytes.Buffer
+	if err := gcimporter.IExportBundle(&bundle, conf.Fset, sorted); err != nil {
+		t.Fatal(err)
+	}
+	fset2 := token.NewFileSet()
+	imports := make(map[string]*types.Package)
+	pkgs2, err := gcimporter.IImportBundle(fset2, imports, bundle.Bytes())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for i, pkg := range sorted {
+		testPkg(t, conf.Fset, pkg, fset2, pkgs2[i])
+	}
 }
 
 func testPkgData(t *testing.T, fset *token.FileSet, pkg *types.Package, exportdata []byte) {
diff --git a/go/internal/gcimporter/iimport.go b/go/internal/gcimporter/iimport.go
index a31a880..b236deb 100644
--- a/go/internal/gcimporter/iimport.go
+++ b/go/internal/gcimporter/iimport.go
@@ -59,10 +59,23 @@
 )
 
 // IImportData imports a package from the serialized package data
-// and returns the number of bytes consumed and a reference to the package.
+// and returns 0 and a reference to the package.
 // If the export data version is not recognized or the format is otherwise
 // compromised, an error is returned.
-func IImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (_ int, pkg *types.Package, err error) {
+func IImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (int, *types.Package, error) {
+	pkgs, err := iimportCommon(fset, imports, data, false, path)
+	if err != nil {
+		return 0, nil, err
+	}
+	return 0, pkgs[0], nil
+}
+
+// IImportBundle imports a set of packages from the serialized package bundle.
+func IImportBundle(fset *token.FileSet, imports map[string]*types.Package, data []byte) ([]*types.Package, error) {
+	return iimportCommon(fset, imports, data, true, "")
+}
+
+func iimportCommon(fset *token.FileSet, imports map[string]*types.Package, data []byte, bundle bool, path string) (pkgs []*types.Package, err error) {
 	const currentVersion = 1
 	version := int64(-1)
 	defer func() {
@@ -77,6 +90,15 @@
 
 	r := &intReader{bytes.NewReader(data), path}
 
+	if bundle {
+		bundleVersion := r.uint64()
+		switch bundleVersion {
+		case bundleVersion:
+		default:
+			errorf("unknown bundle format version %d", bundleVersion)
+		}
+	}
+
 	version = int64(r.uint64())
 	switch version {
 	case currentVersion, 0:
@@ -143,39 +165,58 @@
 		p.pkgIndex[pkg] = nameIndex
 		pkgList[i] = pkg
 	}
-	if len(pkgList) == 0 {
-		errorf("no packages found for %s", path)
-		panic("unreachable")
+
+	if bundle {
+		pkgs = make([]*types.Package, r.uint64())
+		for i := range pkgs {
+			pkg := p.pkgAt(r.uint64())
+			imps := make([]*types.Package, r.uint64())
+			for j := range imps {
+				imps[j] = p.pkgAt(r.uint64())
+			}
+			pkg.SetImports(imps)
+			pkgs[i] = pkg
+		}
+	} else {
+		if len(pkgList) == 0 {
+			errorf("no packages found for %s", path)
+			panic("unreachable")
+		}
+		pkgs = pkgList[:1]
+
+		// record all referenced packages as imports
+		list := append(([]*types.Package)(nil), pkgList[1:]...)
+		sort.Sort(byPath(list))
+		pkgs[0].SetImports(list)
 	}
-	p.ipkg = pkgList[0]
-	names := make([]string, 0, len(p.pkgIndex[p.ipkg]))
-	for name := range p.pkgIndex[p.ipkg] {
-		names = append(names, name)
-	}
-	sort.Strings(names)
-	for _, name := range names {
-		p.doDecl(p.ipkg, name)
+
+	for _, pkg := range pkgs {
+		if pkg.Complete() {
+			continue
+		}
+
+		names := make([]string, 0, len(p.pkgIndex[pkg]))
+		for name := range p.pkgIndex[pkg] {
+			names = append(names, name)
+		}
+		sort.Strings(names)
+		for _, name := range names {
+			p.doDecl(pkg, name)
+		}
+
+		// package was imported completely and without errors
+		pkg.MarkComplete()
 	}
 
 	for _, typ := range p.interfaceList {
 		typ.Complete()
 	}
 
-	// record all referenced packages as imports
-	list := append(([]*types.Package)(nil), pkgList[1:]...)
-	sort.Sort(byPath(list))
-	p.ipkg.SetImports(list)
-
-	// package was imported completely and without errors
-	p.ipkg.MarkComplete()
-
-	consumed, _ := r.Seek(0, io.SeekCurrent)
-	return int(consumed), p.ipkg, nil
+	return pkgs, nil
 }
 
 type iimporter struct {
 	ipath   string
-	ipkg    *types.Package
 	version int
 
 	stringData  []byte
@@ -227,9 +268,6 @@
 		return pkg
 	}
 	path := p.stringAt(off)
-	if path == p.ipath {
-		return p.ipkg
-	}
 	errorf("missing package %q in %q", path, p.ipath)
 	return nil
 }