cue: implement recursive marshalling

This allows a complete Instance, with all its
dependancies to be marshalled.

The API changed to allow multiple instances
to be marshalled at once, so that they can
share dependencies.

Also fixes exporting bug that this exposes.

Change-Id: I5daf1dd577fabb5fbd31978c417f73b759a83c91
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2740
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/build.go b/cue/build.go
index a76be73..4ac5214 100644
--- a/cue/build.go
+++ b/cue/build.go
@@ -15,9 +15,6 @@
 package cue
 
 import (
-	"bytes"
-	"compress/gzip"
-	"encoding/gob"
 	"path"
 	"strconv"
 	"sync"
@@ -25,7 +22,6 @@
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/build"
 	"cuelang.org/go/cue/errors"
-	"cuelang.org/go/cue/format"
 	"cuelang.org/go/cue/token"
 	"cuelang.org/go/internal"
 )
@@ -54,6 +50,14 @@
 	return r.idx
 }
 
+func (r *Runtime) buildContext() *build.Context {
+	ctx := r.ctx
+	if r.ctx == nil {
+		ctx = build.NewContext()
+	}
+	return ctx
+}
+
 func (r *Runtime) complete(p *build.Instance) (*Instance, error) {
 	idx := r.index()
 	if err := p.Complete(); err != nil {
@@ -67,82 +71,12 @@
 	return inst, nil
 }
 
-type instanceData struct {
-	Filename string
-	PkgPath  string
-	Data     []byte
-}
-
-// Unmarshal creates an Instance from bytes generated by the MarshalBinary
-// method of an instance.
-func (r *Runtime) Unmarshal(b []byte) (*Instance, error) {
-	reader, err := gzip.NewReader(bytes.NewReader(b))
-	if err != nil {
-		return nil, errors.Newf(token.NoPos, "restore failed: %v", err)
-	}
-	version := [1]byte{}
-	_, _ = reader.Read(version[:])
-
-	data := &instanceData{}
-	if err = gob.NewDecoder(reader).Decode(data); err != nil {
-		return nil, errors.Newf(token.NoPos, "restore failed: %v", err)
-	}
-
-	inst, err := r.Compile(data.Filename, data.Data)
-	if inst != nil {
-		inst.ImportPath = data.PkgPath
-	}
-	return inst, err
-}
-
-// MarshalBinary marshals c into binary data.
-func (inst *Instance) MarshalBinary() (b []byte, err error) {
-	ctx := inst.index.newContext()
-	n := export(ctx, inst.rootValue, options{raw: true})
-
-	file, ok := n.(*ast.File)
-	if !ok {
-		file = &ast.File{}
-		if obj, ok := n.(*ast.StructLit); ok {
-			file.Decls = append(file.Decls, obj.Elts...)
-		} else {
-			file.Decls = append(file.Decls, &ast.EmbedDecl{Expr: n.(ast.Expr)})
-		}
-	}
-	if inst.Name != "" {
-		file.Name = ast.NewIdent(inst.Name)
-	}
-
-	b, err = format.Node(file)
-	if err != nil {
-		return nil, err
-	}
-
-	buf := &bytes.Buffer{}
-	zw := gzip.NewWriter(buf)
-	_, _ = zw.Write([]byte{0}) // version marker
-	enc := gob.NewEncoder(zw)
-	err = enc.Encode(&instanceData{
-		Filename: inst.Value().Pos().Filename(),
-		PkgPath:  inst.ImportPath,
-		Data:     b,
-	})
-	if err != nil {
-		return nil, err
-	}
-	err = zw.Close()
-	return buf.Bytes(), err
-}
-
 // Compile compiles the given source into an Instance. The source code may be
 // provided as a string, byte slice, io.Reader. The name is used as the file
 // name in position information. The source may import builtin packages. Use
 // Build to allow importing non-builtin packages.
 func (r *Runtime) Compile(filename string, source interface{}) (*Instance, error) {
-	ctx := r.ctx
-	if ctx == nil {
-		ctx = build.NewContext()
-	}
+	ctx := r.buildContext()
 	p := ctx.NewInstance(filename, dummyLoad)
 	if err := p.AddFile(filename, source); err != nil {
 		return nil, p.Err
@@ -153,10 +87,7 @@
 // CompileFile compiles the given source file into an Instance. The source may
 // import builtin packages. Use Build to allow importing non-builtin packages.
 func (r *Runtime) CompileFile(file *ast.File) (*Instance, error) {
-	ctx := r.ctx
-	if ctx == nil {
-		ctx = build.NewContext()
-	}
+	ctx := r.buildContext()
 	p := ctx.NewInstance(file.Filename, dummyLoad)
 	err := p.AddSyntax(file)
 	if err != nil {
@@ -172,10 +103,7 @@
 // may import builtin packages. Use Build to allow importing non-builtin
 // packages.
 func (r *Runtime) CompileExpr(expr ast.Expr) (*Instance, error) {
-	ctx := r.ctx
-	if ctx == nil {
-		ctx = build.NewContext()
-	}
+	ctx := r.buildContext()
 	p := ctx.NewInstance("", dummyLoad)
 	switch x := expr.(type) {
 	case *ast.StructLit:
@@ -217,17 +145,28 @@
 		panic("cue: list of instances must not be empty")
 	}
 	var r Runtime
+	a, _ := r.build(instances)
+	return a
+}
+
+func (r *Runtime) build(instances []*build.Instance) ([]*Instance, error) {
 	index := r.index()
 
 	loaded := []*Instance{}
 
-	for _, p := range instances {
-		p.Complete()
+	var errs errors.Error
 
-		loaded = append(loaded, index.loadInstance(p))
+	for _, p := range instances {
+		_ = p.Complete()
+		errs = errors.Append(errs, p.Err)
+
+		i := index.loadInstance(p)
+		errs = errors.Append(errs, i.Err)
+		loaded = append(loaded, i)
 	}
+
 	// TODO: insert imports
-	return loaded
+	return loaded, errs
 }
 
 // FromExpr creates an instance from an expression.
@@ -252,8 +191,9 @@
 	labelMap map[string]label
 	labels   []string
 
-	loaded  map[*build.Instance]*Instance
-	imports map[value]*Instance // key is always a *structLit
+	loaded        map[*build.Instance]*Instance
+	imports       map[value]*Instance // key is always a *structLit
+	importsByPath map[string]*Instance
 
 	offset label
 	parent *index
@@ -276,9 +216,10 @@
 	// machine implementation for storing graphs.
 	token.NewFile("dummy", sharedOffset, 0)
 	i := &index{
-		labelMap: map[string]label{"": 0},
-		labels:   []string{""},
-		imports:  map[value]*Instance{},
+		labelMap:      map[string]label{"": 0},
+		labels:        []string{""},
+		imports:       map[value]*Instance{},
+		importsByPath: map[string]*Instance{},
 	}
 	return i
 }
@@ -287,11 +228,12 @@
 func newIndex() *index {
 	parent := sharedIndex
 	i := &index{
-		labelMap: map[string]label{},
-		loaded:   map[*build.Instance]*Instance{},
-		imports:  map[value]*Instance{},
-		offset:   label(len(parent.labels)) + parent.offset,
-		parent:   parent,
+		labelMap:      map[string]label{},
+		loaded:        map[*build.Instance]*Instance{},
+		imports:       map[value]*Instance{},
+		importsByPath: map[string]*Instance{},
+		offset:        label(len(parent.labels)) + parent.offset,
+		parent:        parent,
 	}
 	parent.freeze = true
 	return i
diff --git a/cue/build_test.go b/cue/build_test.go
index 772ba9a..fd8e239 100644
--- a/cue/build_test.go
+++ b/cue/build_test.go
@@ -24,73 +24,6 @@
 	"cuelang.org/go/cue/token"
 )
 
-func TestMarshalling(t *testing.T) {
-	testCases := []struct {
-		filename string
-		input    string
-		pkg      string
-	}{{
-		filename: "foo.cue",
-		pkg:      "foo",
-		input: `package foo
-
-		A: int
-		B: string
-		`,
-	}, {
-		filename: "bar.cue",
-		pkg:      "bar",
-		input: `package bar
-
-		"Hello world!"
-		`,
-	}, {
-		filename: "qux.cue",
-		input: `
-			"Hello world!"
-		`,
-	}, {
-		filename: "baz.cue",
-		pkg:      "baz",
-		input: `package baz
-
-		import "strings"
-
-		a: strings.TrimSpace("  Hello world!  ")
-		`}}
-	for _, tc := range testCases {
-		t.Run(tc.filename, func(t *testing.T) {
-			r := &Runtime{}
-			inst, err := r.Compile(tc.filename, tc.input)
-			if err != nil {
-				t.Fatal(err)
-			}
-			inst.ImportPath = "test/pkg"
-			got := fmt.Sprint(inst.Value())
-
-			b, err := inst.MarshalBinary()
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			r2 := &Runtime{}
-			inst, err = r2.Unmarshal(b)
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			if inst.ImportPath != "test/pkg" {
-				t.Error("import path was not restored")
-			}
-			want := fmt.Sprint(inst.Value())
-
-			if got != want {
-				t.Errorf("\ngot:  %q;\nwant: %q", got, want)
-			}
-		})
-	}
-}
-
 func TestFromExpr(t *testing.T) {
 	testCases := []struct {
 		expr ast.Expr
@@ -139,7 +72,6 @@
 		Number: 12
 		`),
 	}
-	insts(pkg1, pkg2)
 
 	testCases := []struct {
 		instances []*bimport
@@ -251,13 +183,11 @@
 }
 
 func (b *builder) load(pos token.Pos, path string) *build.Instance {
-	p := b.ctxt.NewInstance(path, b.load)
 	bi := b.imports[path]
 	if bi == nil {
 		return nil
 	}
-	buildInstance(b.imports[path], p)
-	return p
+	return b.build(bi)
 }
 
 type bimport struct {
@@ -277,17 +207,21 @@
 	}
 	for _, bi := range insts {
 		if bi.path == "" {
-			p := b.ctxt.NewInstance("dir", b.load)
-			buildInstance(bi, p)
-			instances = append(instances, p)
+			instances = append(instances, b.build(bi))
 		}
 	}
 	return
 }
 
-func buildInstance(bi *bimport, p *build.Instance) {
+func (b *builder) build(bi *bimport) *build.Instance {
+	path := bi.path
+	if path == "" {
+		path = "dir"
+	}
+	p := b.ctxt.NewInstance(path, b.load)
 	for i, f := range bi.files {
 		_ = p.AddFile(fmt.Sprintf("file%d.cue", i), f)
 	}
-	p.Complete()
+	_ = p.Complete()
+	return p
 }
diff --git a/cue/builtin.go b/cue/builtin.go
index 2b2672a..af15465 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -274,7 +274,7 @@
 	return c.builtin.name(c.ctx)
 }
 
-var builtins = map[string]*structLit{}
+var builtins = map[string]*Instance{}
 
 func initBuiltins(pkgs map[string]*builtinPkg) {
 	ctx := sharedIndex.newContext()
@@ -286,15 +286,16 @@
 	for _, k := range keys {
 		b := pkgs[k]
 		e := mustCompileBuiltins(ctx, b, k)
-		builtins[k] = e
-		builtins["-/"+path.Base(k)] = e
 
-		sharedIndex.addInst(&Instance{
+		i := sharedIndex.addInst(&Instance{
 			ImportPath: k,
 			Name:       path.Base(k),
 			rootStruct: e,
 			rootValue:  e,
 		})
+
+		builtins[k] = i
+		builtins["-/"+path.Base(k)] = i
 	}
 }
 
@@ -307,7 +308,7 @@
 	if !ok {
 		return nil
 	}
-	return p
+	return p.rootStruct
 }
 
 func init() {
diff --git a/cue/export.go b/cue/export.go
index 5454c68..e8616e6 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -32,7 +32,7 @@
 	return !m.raw
 }
 
-func export(ctx *context, v value, m options) ast.Node {
+func export(ctx *context, v value, m options) (n ast.Node, imports []string) {
 	e := exporter{ctx, m, nil, map[label]bool{}, map[string]importInfo{}}
 	top, ok := v.evalPartial(ctx).(*structLit)
 	if ok {
@@ -45,9 +45,9 @@
 	value := e.expr(v)
 	if len(e.imports) == 0 {
 		// TODO: unwrap structs?
-		return value
+		return value, nil
 	}
-	imports := make([]string, 0, len(e.imports))
+	imports = make([]string, 0, len(e.imports))
 	for k := range e.imports {
 		imports = append(imports, k)
 	}
@@ -81,7 +81,7 @@
 	}
 
 	// resolve the file.
-	return file
+	return file, imports
 }
 
 type exporter struct {
@@ -164,14 +164,11 @@
 	panic(fmt.Sprintf("unsupported clause type %T", v))
 }
 
-func (p *exporter) shortName(preferred, pkg string) string {
+func (p *exporter) shortName(inst *Instance, preferred, pkg string) string {
 	info, ok := p.imports[pkg]
 	short := info.short
 	if !ok {
-		short = pkg
-		if i := strings.LastIndexByte(pkg, '.'); i >= 0 {
-			short = pkg[i+1:]
-		}
+		short = inst.Name
 		if _, ok := p.top[p.ctx.label(short, true)]; ok && preferred != "" {
 			short = preferred
 			info.name = short
@@ -226,7 +223,9 @@
 		if x.pkg == 0 {
 			return name
 		}
-		short := p.shortName("", p.ctx.labelStr(x.pkg))
+		pkg := p.ctx.labelStr(x.pkg)
+		inst := builtins[pkg]
+		short := p.shortName(inst, "", pkg)
 		return &ast.SelectorExpr{X: ast.NewIdent(short), Sel: name}
 
 	case *nodeRef:
@@ -238,7 +237,7 @@
 			return nil // should not happen!
 		}
 		short := p.ctx.labelStr(x.short)
-		return ast.NewIdent(p.shortName(short, inst.ImportPath))
+		return ast.NewIdent(p.shortName(inst, short, inst.ImportPath))
 
 	case *selectorExpr:
 		n := p.expr(x.x)
diff --git a/cue/export_test.go b/cue/export_test.go
index 4aa01e7..51ad59e 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -322,7 +322,8 @@
 			v := newValueRoot(ctx, n)
 
 			opts := options{raw: !tc.eval}
-			b, err := format.Node(export(ctx, v.eval(ctx), opts), format.Simplify())
+			node, _ := export(ctx, v.eval(ctx), opts)
+			b, err := format.Node(node, format.Simplify())
 			if err != nil {
 				log.Fatal(err)
 			}
diff --git a/cue/instance.go b/cue/instance.go
index 39dc797..1083cd0 100644
--- a/cue/instance.go
+++ b/cue/instance.go
@@ -53,6 +53,9 @@
 	}
 	p.index = x
 	x.imports[p.rootStruct] = p
+	if p.ImportPath != "" {
+		x.importsByPath[p.ImportPath] = p
+	}
 	return p
 }
 
@@ -67,11 +70,11 @@
 // newInstance creates a new instance. Use Insert to populate the instance.
 func (x *index) newInstance(p *build.Instance) *Instance {
 	st := &structLit{baseValue: baseValue{nil}}
-	i := x.addInst(&Instance{
+	i := &Instance{
 		rootStruct: st,
 		rootValue:  st,
 		inst:       p,
-	})
+	}
 	if p != nil {
 		i.ImportPath = p.ImportPath
 		i.Dir = p.Dir
@@ -80,7 +83,7 @@
 			i.setListOrError(p.Err)
 		}
 	}
-	return i
+	return x.addInst(i)
 }
 
 func (inst *Instance) setListOrError(err errors.Error) {
diff --git a/cue/load/import.go b/cue/load/import.go
index e0618cb..7fe1df1 100644
--- a/cue/load/import.go
+++ b/cue/load/import.go
@@ -78,28 +78,13 @@
 		parentPath = filepath.Join(srcDir, filepath.FromSlash(path))
 	}
 	p := cfg.Context.NewInstance(path, l.loadFunc(parentPath))
-	p.DisplayPath = path
-
-	isLocal := isLocalImport(path)
-
-	if cfg.Module != "" && isLocal {
-		p.ImportPath = filepath.Join(cfg.Module, path)
-	}
-
-	var modDir string
-	// var modErr error
-	if !isLocal {
-		// TODO(mpvl): support module lookup
-	}
-
-	p.Local = isLocal
 
 	if err := updateDirs(cfg, p, path, srcDir, 0); err != nil {
 		p.ReportError(err)
 		return p
 	}
 
-	if modDir == "" && path != cleanImport(path) {
+	if path != cleanImport(path) {
 		report(p, l.errPkgf(nil,
 			"non-canonical import path: %q should be %q", path, pathpkg.Clean(path)))
 		p.Incomplete = true
@@ -108,7 +93,7 @@
 
 	fp := newFileProcessor(cfg, p)
 
-	root := p.Dir
+	root := srcDir
 
 	for dir := p.Dir; ctxt.isDir(dir); {
 		files, err := ctxt.readDir(dir)
@@ -145,6 +130,10 @@
 		dir = parent
 	}
 
+	if strings.HasPrefix(root, srcDir) {
+		root = srcDir
+	}
+
 	rewriteFiles(p, root, false)
 	if errs := fp.finalize(); errs != nil {
 		for _, e := range errors.Errors(errs) {
@@ -208,8 +197,13 @@
 }
 
 func updateDirs(c *Config, p *build.Instance, path, srcDir string, mode importMode) errors.Error {
+	p.DisplayPath = path
+
+	isLocal := isLocalImport(path)
+	p.Local = isLocal
+
 	ctxt := &c.fileSystem
-	// path := p.ImportPath
+
 	if path == "" {
 		return errors.Newf(token.NoPos, "import %q: invalid import path", path)
 	}
@@ -218,17 +212,19 @@
 		return errors.Newf(token.NoPos, "absolute import path %q not allowed", path)
 	}
 
-	if isLocalImport(path) {
+	if isLocal {
+		if c.Module != "" {
+			p.ImportPath = filepath.Join(c.Module, path)
+		}
+
 		if srcDir == "" {
 			return errors.Newf(token.NoPos, "import %q: import relative to unknown directory", path)
 		}
-		if !ctxt.isAbsPath(path) {
-			p.Dir = ctxt.joinPath(srcDir, path)
-		}
+		p.Dir = ctxt.joinPath(srcDir, path)
 		return nil
 	}
 	dir := ctxt.joinPath(srcDir, path)
-	info, err := ctxt.stat(filepath.Join(srcDir, path))
+	info, err := ctxt.stat(dir)
 	if err == nil && info.IsDir() {
 		p.Dir = dir
 		return nil
@@ -239,14 +235,9 @@
 }
 
 func normPrefix(root, path string, isLocal bool) string {
-	root = filepath.Clean(root)
-	prefix := ""
-	if isLocal {
-		prefix = "." + string(filepath.Separator)
-	}
-	if !strings.HasSuffix(root, string(filepath.Separator)) &&
-		strings.HasPrefix(path, root) {
-		path = prefix + path[len(root)+1:]
+	path, err := filepath.Rel(root, path)
+	if err == nil && isLocal {
+		path = "." + string(filepath.Separator) + path
 	}
 	return path
 }
diff --git a/cue/marshal.go b/cue/marshal.go
new file mode 100644
index 0000000..43174d0
--- /dev/null
+++ b/cue/marshal.go
@@ -0,0 +1,202 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cue
+
+import (
+	"bytes"
+	"compress/gzip"
+	"encoding/gob"
+	"path/filepath"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/build"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/format"
+	"cuelang.org/go/cue/token"
+)
+
+// root.
+type instanceData struct {
+	Root  bool
+	Path  string
+	Files []fileData
+}
+
+type fileData struct {
+	Name string
+	Data []byte
+}
+
+const version = 1
+
+type unmarshaller struct {
+	ctxt    *build.Context
+	imports map[string]*instanceData
+}
+
+func (b *unmarshaller) load(pos token.Pos, path string) *build.Instance {
+	bi := b.imports[path]
+	if bi == nil {
+		return nil
+	}
+	return b.build(bi)
+}
+
+func (b *unmarshaller) build(bi *instanceData) *build.Instance {
+	p := b.ctxt.NewInstance(bi.Path, b.load)
+	p.ImportPath = bi.Path
+	for _, f := range bi.Files {
+		_ = p.AddFile(f.Name, f.Data)
+	}
+	p.Complete()
+	return p
+}
+
+func compileInstances(r *Runtime, data []*instanceData) (instances []*Instance, err error) {
+	b := unmarshaller{
+		ctxt:    r.buildContext(),
+		imports: map[string]*instanceData{},
+	}
+	for _, i := range data {
+		if i.Path == "" {
+			if !i.Root {
+				return nil, errors.Newf(token.NoPos,
+					"data contains non-root package without import path")
+			}
+			continue
+		}
+		b.imports[i.Path] = i
+	}
+
+	builds := []*build.Instance{}
+	for _, i := range data {
+		if !i.Root {
+			continue
+		}
+		builds = append(builds, b.build(i))
+	}
+
+	return r.build(builds)
+}
+
+// Unmarshal creates an Instance from bytes generated by the MarshalBinary
+// method of an instance.
+func (r *Runtime) Unmarshal(b []byte) ([]*Instance, error) {
+	if len(b) == 0 {
+		return nil, errors.Newf(token.NoPos, "unmarshal failed: empty buffer")
+	}
+
+	switch b[0] {
+	case version:
+	default:
+		return nil, errors.Newf(token.NoPos,
+			"unmarshal failed: unsupported version %d, regenerate data", b[0])
+	}
+
+	reader, err := gzip.NewReader(bytes.NewReader(b[1:]))
+	if err != nil {
+		return nil, errors.Newf(token.NoPos, "unmarshal failed: %v", err)
+	}
+
+	data := []*instanceData{}
+	err = gob.NewDecoder(reader).Decode(&data)
+	if err != nil {
+		return nil, errors.Newf(token.NoPos, "unmarshal failed: %v", err)
+	}
+
+	return compileInstances(r, data)
+}
+
+// Marshal creates bytes from a group of instances. Imported instances will
+// be included in the emission.
+//
+// The stored instances are functionally the same, but preserving of file
+// information is only done on a best-effort basis.
+func (r *Runtime) Marshal(instances ...*Instance) (b []byte, err error) {
+	ctx := r.index().newContext()
+
+	staged := []instanceData{}
+	done := map[string]int{}
+
+	var errs errors.Error
+
+	var stageInstance func(i *Instance) (pos int)
+	stageInstance = func(i *Instance) (pos int) {
+		if p, ok := done[i.ImportPath]; ok {
+			return p
+		}
+		n, imports := export(ctx, i.rootValue, options{raw: true})
+
+		file, ok := n.(*ast.File)
+		if !ok {
+			file = &ast.File{}
+			if obj, ok := n.(*ast.StructLit); ok {
+				file.Decls = append(file.Decls, obj.Elts...)
+			} else {
+				file.Decls = append(file.Decls, &ast.EmbedDecl{Expr: n.(ast.Expr)})
+			}
+		}
+		if i.Name != "" {
+			file.Name = ast.NewIdent(i.Name)
+		}
+
+		b, err := format.Node(file)
+		errs = errors.Append(errs, errors.Promote(err, "marshal"))
+
+		filename := "unmarshal"
+		if i.inst != nil && len(i.inst.Files) == 1 {
+			filename = i.inst.Files[0].Filename
+			if i.Dir != "" {
+				filename, _ = filepath.Rel(i.Dir, filename)
+			}
+		}
+
+		staged = append(staged, instanceData{
+			Path:  i.ImportPath,
+			Files: []fileData{{filename, b}},
+		})
+
+		p := len(staged) - 1
+
+		for _, imp := range imports {
+			i := ctx.importsByPath[imp]
+			if i == nil {
+				continue // a builtin package.
+			}
+			stageInstance(i)
+		}
+
+		return p
+	}
+
+	for _, i := range instances {
+		staged[stageInstance(i)].Root = true
+	}
+
+	buf := &bytes.Buffer{}
+	buf.WriteByte(version)
+
+	zw := gzip.NewWriter(buf)
+	if err := gob.NewEncoder(zw).Encode(staged); err != nil {
+		return nil, err
+	}
+
+	if err := zw.Close(); err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+
+}
diff --git a/cue/marshal_test.go b/cue/marshal_test.go
new file mode 100644
index 0000000..b380b1a
--- /dev/null
+++ b/cue/marshal_test.go
@@ -0,0 +1,202 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cue
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestMarshalling(t *testing.T) {
+	testCases := []struct {
+		filename string
+		input    string
+		pkg      string
+	}{{
+		filename: "foo.cue",
+		pkg:      "foo",
+		input: `package foo
+
+		A: int
+		B: string
+		`,
+	}, {
+		filename: "bar.cue",
+		pkg:      "bar",
+		input: `package bar
+
+		"Hello world!"
+		`,
+	}, {
+		filename: "qux.cue",
+		input: `
+			"Hello world!"
+		`,
+	}, {
+		filename: "baz.cue",
+		pkg:      "baz",
+		input: `package baz
+
+		import "strings"
+
+		a: strings.TrimSpace("  Hello world!  ")
+		`}}
+	for _, tc := range testCases {
+		t.Run(tc.filename, func(t *testing.T) {
+			r := &Runtime{}
+			inst, err := r.Compile(tc.filename, tc.input)
+			if err != nil {
+				t.Fatal(err)
+			}
+			inst.ImportPath = "test/pkg"
+			want := fmt.Sprint(inst.Value())
+
+			b, err := r.Marshal(inst)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			r2 := &Runtime{}
+			instances, err := r2.Unmarshal(b)
+			if err != nil {
+				t.Fatal(err)
+			}
+			inst = instances[0]
+
+			if inst.ImportPath != "test/pkg" {
+				t.Error("import path was not restored")
+			}
+			got := fmt.Sprint(inst.Value())
+
+			if got != want {
+				t.Errorf("\ngot:  %q;\nwant: %q", got, want)
+			}
+		})
+	}
+}
+
+func TestMarshalMultiPackage(t *testing.T) {
+	files := func(s ...string) (a []fileData) {
+		for i, s := range s {
+			a = append(a, fileData{fmt.Sprintf("file%d.cue", i), []byte(s)})
+		}
+		return a
+	}
+	insts := func(i ...*instanceData) []*instanceData { return i }
+	pkg1 := &instanceData{
+		true,
+		"pkg1",
+		files(`
+		package pkg1
+
+		Object: "World"
+		`),
+	}
+	pkg2 := &instanceData{
+		true,
+		"example.com/foo/pkg2",
+		files(`
+		package pkg
+
+		Number: 12
+		`),
+	}
+
+	testCases := []struct {
+		instances []*instanceData
+		emit      string
+	}{{
+		insts(&instanceData{true, "", files(`test: "ok"`)}),
+		`{test: "ok"}`,
+	}, {
+		insts(&instanceData{true, "",
+			files(
+				`package test
+
+		import math2 "math"
+
+		"Pi: \(math2.Pi)!"`)}),
+		`"Pi: 3.14159265358979323846264338327950288419716939937510582097494459!"`,
+	}, {
+		insts(pkg1, &instanceData{true, "",
+			files(
+				`package test
+
+			import "pkg1"
+
+			"Hello \(pkg1.Object)!"`),
+		}),
+		`"Hello World!"`,
+	}, {
+		insts(pkg1, &instanceData{true, "",
+			files(
+				`package test
+
+		import pkg2 "pkg1"
+		pkg1: pkg2.Object
+
+		"Hello \(pkg1)!"`),
+		}),
+		`"Hello World!"`,
+	}, {
+		insts(pkg2, &instanceData{true, "",
+			files(
+				`package test
+
+		import "example.com/foo/pkg2"
+
+		"Hello \(pkg.Number)!"`),
+		}),
+		`"Hello 12!"`,
+	}}
+
+	strValue := func(a []*Instance) (ret []string) {
+		for _, i := range a {
+			ret = append(ret, strings.TrimSpace((fmt.Sprint(i.Value()))))
+		}
+		return ret
+	}
+
+	for _, tc := range testCases {
+		t.Run("", func(t *testing.T) {
+			r := &Runtime{}
+
+			insts, err := compileInstances(r, tc.instances)
+			if err != nil {
+				t.Fatal(err)
+			}
+			want := strValue(insts)
+
+			b, err := r.Marshal(insts...)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			r2 := &Runtime{}
+			insts, err = r2.Unmarshal(b)
+			if err != nil {
+				t.Fatal(err)
+			}
+			got := strValue(insts)
+
+			if !cmp.Equal(got, want) {
+				t.Error(cmp.Diff(got, want))
+			}
+		})
+	}
+}
diff --git a/cue/types.go b/cue/types.go
index dc1fcb1..c1c9e3a 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -743,9 +743,11 @@
 	ctx := v.ctx()
 	o := getOptions(opts)
 	if o.raw {
-		return export(ctx, v.path.v, o)
+		n, _ := export(ctx, v.path.v, o)
+		return n
 	}
-	return export(ctx, v.path.cache, o)
+	n, _ := export(ctx, v.path.cache, o)
+	return n
 }
 
 // Decode initializes x with Value v. If x is a struct, it will validate the