Repository: openllb/hlb
Pull request #296 (2022-02-11 18:13:57)

Refactor parser into parser and ast packages
Sorry for the big diff, I keep running into module import cycles so I moved some parts of the `parser` package into `ast`, and `diagnostic.WithSources` to `filebuffer.WithBuffers`.

Mostly mechanical via `rg --files-with-matches "pattern" | xargs sd "pattern" "replace"`


Patch:
diff --git a/builtin/builtin.go b/builtin/builtin.go
index bcaaee0..0943f9d 100644
--- a/builtin/builtin.go
+++ b/builtin/builtin.go
@@ -4,14 +4,14 @@ import (
 	"context"
 	"strings"
 
-	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/pkg/filebuffer"
 	"github.com/pkg/errors"
 )
 
 var (
-	Module *parser.Module
+	Module *ast.Module
 
 	FileBuffer *filebuffer.FileBuffer
 )
@@ -24,7 +24,7 @@ func init() {
 }
 
 func initSources() (err error) {
-	ctx := diagnostic.WithSources(context.Background(), filebuffer.NewSources())
+	ctx := filebuffer.WithBuffers(context.Background(), filebuffer.NewBuffers())
 	Module, err = parser.Parse(ctx, &parser.NamedReader{
 		Reader: strings.NewReader(Reference),
 		Value:  "<builtin>",
@@ -32,12 +32,12 @@ func initSources() (err error) {
 	if err != nil {
 		return errors.Wrapf(err, "failed to initialize filebuffer for builtins")
 	}
-	FileBuffer = diagnostic.Sources(ctx).Get(Module.Pos.Filename)
+	FileBuffer = filebuffer.Buffers(ctx).Get(Module.Pos.Filename)
 	return
 }
 
-func Sources() *filebuffer.Sources {
-	sources := filebuffer.NewSources()
-	sources.Set(FileBuffer.Filename(), FileBuffer)
-	return sources
+func Buffers() *filebuffer.BufferLookup {
+	buffers := filebuffer.NewBuffers()
+	buffers.Set(FileBuffer.Filename(), FileBuffer)
+	return buffers
 }
diff --git a/builtin/gen/builtins.go b/builtin/gen/builtins.go
index 188ada3..020c718 100644
--- a/builtin/gen/builtins.go
+++ b/builtin/gen/builtins.go
@@ -14,39 +14,39 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/pkg/filebuffer"
 )
 
 type BuiltinData struct {
 	Command     string
-	FuncsByKind map[parser.Kind][]ParsedFunc
+	FuncsByKind map[ast.Kind][]ParsedFunc
 	Reference   string
 }
 
 type ParsedFunc struct {
 	Name    string
-	Params  []*parser.Field
-	Effects []*parser.Field
+	Params  []*ast.Field
+	Effects []*ast.Field
 }
 
 func GenerateBuiltins(ctx context.Context, r io.Reader) ([]byte, error) {
-	sources := filebuffer.NewSources()
-	ctx = diagnostic.WithSources(ctx, sources)
+	files := filebuffer.NewBuffers()
+	ctx = filebuffer.WithBuffers(ctx, files)
 	mod, err := parser.Parse(ctx, r)
 	if err != nil {
 		return nil, err
 	}
 
-	funcsByKind := make(map[parser.Kind][]ParsedFunc)
+	funcsByKind := make(map[ast.Kind][]ParsedFunc)
 	for _, decl := range mod.Decls {
 		fun := decl.Func
 		if fun == nil {
 			continue
 		}
 
-		var effects []*parser.Field
+		var effects []*ast.Field
 		if fun.Effects != nil && fun.Effects.Effects != nil {
 			effects = fun.Effects.Effects.Fields()
 		}
@@ -59,7 +59,7 @@ func GenerateBuiltins(ctx context.Context, r io.Reader) ([]byte, error) {
 		})
 	}
 
-	fb := sources.Get(mod.Pos.Filename)
+	fb := files.Get(mod.Pos.Filename)
 	data := BuiltinData{
 		Command:     fmt.Sprintf("builtingen %s", strings.Join(os.Args[1:], " ")),
 		FuncsByKind: funcsByKind,
@@ -83,16 +83,16 @@ func GenerateBuiltins(ctx context.Context, r io.Reader) ([]byte, error) {
 }
 
 var tmplFunctions = template.FuncMap{
-	"kind": func(kind parser.Kind) template.HTML {
+	"kind": func(kind ast.Kind) template.HTML {
 		switch kind {
-		case parser.String:
-			return template.HTML("parser.String")
-		case parser.Int:
-			return template.HTML("parser.Int")
-		case parser.Bool:
-			return template.HTML("parser.Bool")
-		case parser.Filesystem:
-			return template.HTML("parser.Filesystem")
+		case ast.String:
+			return template.HTML("ast.String")
+		case ast.Int:
+			return template.HTML("ast.Int")
+		case ast.Bool:
+			return template.HTML("ast.Bool")
+		case ast.Filesystem:
+			return template.HTML("ast.Filesystem")
 		default:
 			return template.HTML(strconv.Quote(string(kind)))
 		}
@@ -104,10 +104,10 @@ var referenceTmpl = template.Must(template.New("reference").Funcs(tmplFunctions)
 
 package builtin
 
-import "github.com/openllb/hlb/parser"
+import "github.com/openllb/hlb/parser/ast"
 
 type BuiltinLookup struct {
-	ByKind map[parser.Kind]LookupByKind
+	ByKind map[ast.Kind]LookupByKind
 }
 
 type LookupByKind struct {
@@ -115,22 +115,22 @@ type LookupByKind struct {
 }
 
 type FuncLookup struct {
-	Params []*parser.Field
-	Effects []*parser.Field
+	Params []*ast.Field
+	Effects []*ast.Field
 }
 
 var (
 	Lookup = BuiltinLookup{
-		ByKind: map[parser.Kind]LookupByKind{
+		ByKind: map[ast.Kind]LookupByKind{
 			{{range $kind, $funcs := .FuncsByKind}}{{kind $kind}}: {
 				Func: map[string]FuncLookup{
 					{{range $i, $func := $funcs}}"{{$func.Name}}": {
-						Params:  []*parser.Field{
-							{{range $i, $param := $func.Params}}parser.NewField({{kind $param.Type.Kind}}, "{{$param.Name}}", {{if $param.Modifier}}true{{else}}false{{end}}),
+						Params:  []*ast.Field{
+							{{range $i, $param := $func.Params}}ast.NewField({{kind $param.Type.Kind}}, "{{$param.Name}}", {{if $param.Modifier}}true{{else}}false{{end}}),
 							{{end}}
 						},
-						Effects: []*parser.Field{
-							{{range $i, $effect := $func.Effects}}parser.NewField({{kind $effect.Type.Kind}}, "{{$effect.Name}}", false),
+						Effects: []*ast.Field{
+							{{range $i, $effect := $func.Effects}}ast.NewField({{kind $effect.Type.Kind}}, "{{$effect.Name}}", false),
 							{{end}}
 						},
 					},
diff --git a/builtin/gen/documentation.go b/builtin/gen/documentation.go
index f386c33..de8bc97 100644
--- a/builtin/gen/documentation.go
+++ b/builtin/gen/documentation.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/openllb/doxygen-parser/doxygen"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 )
 
 // Documentation contains all the builtin functions defined for HLB.
@@ -126,7 +127,7 @@ func GenerateDocumentation(ctx context.Context, r io.Reader) (*Documentation, er
 			funcDoc.Doc = strings.TrimSpace(group.Doc)
 		}
 
-		if fun.Type.Kind.Primary() == parser.Option {
+		if fun.Type.Kind.Primary() == ast.Option {
 			subtype := string(fun.Type.Kind.Secondary())
 			optionsByFunc[subtype] = append(optionsByFunc[subtype], funcDoc)
 		}
diff --git a/builtin/lookup.go b/builtin/lookup.go
index 7240bf2..e7f77ca 100644
--- a/builtin/lookup.go
+++ b/builtin/lookup.go
@@ -2,10 +2,10 @@
 
 package builtin
 
-import "github.com/openllb/hlb/parser"
+import "github.com/openllb/hlb/parser/ast"
 
 type BuiltinLookup struct {
-	ByKind map[parser.Kind]LookupByKind
+	ByKind map[ast.Kind]LookupByKind
 }
 
 type LookupByKind struct {
@@ -13,678 +13,678 @@ type LookupByKind struct {
 }
 
 type FuncLookup struct {
-	Params  []*parser.Field
-	Effects []*parser.Field
+	Params  []*ast.Field
+	Effects []*ast.Field
 }
 
 var (
 	Lookup = BuiltinLookup{
-		ByKind: map[parser.Kind]LookupByKind{
-			parser.Filesystem: {
+		ByKind: map[ast.Kind]LookupByKind{
+			ast.Filesystem: {
 				Func: map[string]FuncLookup{
 					"scratch": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"image": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "ref", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "ref", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"http": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "url", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "url", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"git": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "remote", false),
-							parser.NewField(parser.String, "ref", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "remote", false),
+							ast.NewField(ast.String, "ref", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"local": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"frontend": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "source", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "source", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"shell": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "arg", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "arg", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"run": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "arg", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "arg", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"env": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "key", false),
-							parser.NewField(parser.String, "value", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "key", false),
+							ast.NewField(ast.String, "value", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"dir": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"user": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "name", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "name", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"mkdir": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", false),
-							parser.NewField(parser.Int, "filemode", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", false),
+							ast.NewField(ast.Int, "filemode", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"mkfile": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", false),
-							parser.NewField(parser.Int, "filemode", false),
-							parser.NewField(parser.String, "content", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", false),
+							ast.NewField(ast.Int, "filemode", false),
+							ast.NewField(ast.String, "content", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"rm": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"copy": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Filesystem, "input", false),
-							parser.NewField(parser.String, "src", false),
-							parser.NewField(parser.String, "dst", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Filesystem, "input", false),
+							ast.NewField(ast.String, "src", false),
+							ast.NewField(ast.String, "dst", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"merge": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Filesystem, "inputs", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.Filesystem, "inputs", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"diff": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Filesystem, "base", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Filesystem, "base", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"dockerPush": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "ref", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "ref", false),
 						},
-						Effects: []*parser.Field{
-							parser.NewField(parser.String, "digest", false),
+						Effects: []*ast.Field{
+							ast.NewField(ast.String, "digest", false),
 						},
 					},
 					"dockerLoad": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "ref", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "ref", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"download": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "localPath", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "localPath", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"downloadTarball": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "localPath", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "localPath", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"downloadOCITarball": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "localPath", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "localPath", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"downloadDockerTarball": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "localPath", false),
-							parser.NewField(parser.String, "ref", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "localPath", false),
+							ast.NewField(ast.String, "ref", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"entrypoint": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "args", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "args", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"cmd": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "args", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "args", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"label": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "key", false),
-							parser.NewField(parser.String, "value", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "key", false),
+							ast.NewField(ast.String, "value", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"expose": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "ports", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "ports", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"volumes": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "mountpoints", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "mountpoints", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"stopSignal": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "signal", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "signal", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"breakpoint": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "command", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "command", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::copy": {
 				Func: map[string]FuncLookup{
 					"followSymlinks": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"contentsOnly": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"unpack": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"createDestPath": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"allowWildcard": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"allowEmptyWildcard": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"chown": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "owner", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "owner", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"chmod": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Int, "filemode", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Int, "filemode", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"createdTime": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "created", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "created", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"includePatterns": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "pattern", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "pattern", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"excludePatterns": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "pattern", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "pattern", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::frontend": {
 				Func: map[string]FuncLookup{
 					"input": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "key", false),
-							parser.NewField(parser.Filesystem, "value", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "key", false),
+							ast.NewField(ast.Filesystem, "value", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"opt": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "key", false),
-							parser.NewField(parser.String, "value", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "key", false),
+							ast.NewField(ast.String, "value", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::git": {
 				Func: map[string]FuncLookup{
 					"keepGitDir": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::http": {
 				Func: map[string]FuncLookup{
 					"checksum": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "digest", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "digest", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"chmod": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Int, "filemode", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Int, "filemode", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"filename": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "name", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "name", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::image": {
 				Func: map[string]FuncLookup{
 					"resolve": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"platform": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "os", false),
-							parser.NewField(parser.String, "arch", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "os", false),
+							ast.NewField(ast.String, "arch", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::local": {
 				Func: map[string]FuncLookup{
 					"includePatterns": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "pattern", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "pattern", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"excludePatterns": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "pattern", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "pattern", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"followPaths": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::localRun": {
 				Func: map[string]FuncLookup{
 					"ignoreError": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"includeStderr": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"onlyStderr": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"shlex": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::manifest": {
 				Func: map[string]FuncLookup{
 					"platform": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "os", false),
-							parser.NewField(parser.String, "arch", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "os", false),
+							ast.NewField(ast.String, "arch", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::mkdir": {
 				Func: map[string]FuncLookup{
 					"createParents": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"chown": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "owner", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "owner", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"createdTime": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "created", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "created", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::mkfile": {
 				Func: map[string]FuncLookup{
 					"chown": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "owner", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "owner", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"createdTime": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "created", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "created", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::mount": {
 				Func: map[string]FuncLookup{
 					"readonly": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"tmpfs": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"sourcePath": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"cache": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "cacheid", false),
-							parser.NewField(parser.String, "sharingmode", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "cacheid", false),
+							ast.NewField(ast.String, "sharingmode", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::rm": {
 				Func: map[string]FuncLookup{
 					"allowNotFound": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"allowWildcard": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::run": {
 				Func: map[string]FuncLookup{
 					"readonlyRootfs": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"env": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "key", false),
-							parser.NewField(parser.String, "value", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "key", false),
+							ast.NewField(ast.String, "value", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"dir": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"user": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "name", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "name", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"ignoreCache": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"network": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "networkmode", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "networkmode", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"security": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "securitymode", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "securitymode", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"shlex": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"host": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "hostname", false),
-							parser.NewField(parser.String, "address", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "hostname", false),
+							ast.NewField(ast.String, "address", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"ssh": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"forward": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "src", false),
-							parser.NewField(parser.String, "dest", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "src", false),
+							ast.NewField(ast.String, "dest", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"secret": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "localPath", false),
-							parser.NewField(parser.String, "mountPoint", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "localPath", false),
+							ast.NewField(ast.String, "mountPoint", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"mount": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Filesystem, "input", false),
-							parser.NewField(parser.String, "mountPoint", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Filesystem, "input", false),
+							ast.NewField(ast.String, "mountPoint", false),
 						},
-						Effects: []*parser.Field{
-							parser.NewField(parser.Filesystem, "target", false),
+						Effects: []*ast.Field{
+							ast.NewField(ast.Filesystem, "target", false),
 						},
 					},
 					"breakpoint": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "command", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "command", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::secret": {
 				Func: map[string]FuncLookup{
 					"uid": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Int, "id", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Int, "id", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"gid": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Int, "id", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Int, "id", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"mode": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Int, "filemode", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Int, "filemode", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"includePatterns": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "pattern", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "pattern", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"excludePatterns": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "pattern", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "pattern", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::ssh": {
 				Func: map[string]FuncLookup{
 					"target": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "mountPoint", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "mountPoint", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"localPaths": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "path", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "path", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"uid": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Int, "id", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Int, "id", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"gid": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Int, "id", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Int, "id", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"mode": {
-						Params: []*parser.Field{
-							parser.NewField(parser.Int, "filemode", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.Int, "filemode", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"option::template": {
 				Func: map[string]FuncLookup{
 					"stringField": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "name", false),
-							parser.NewField(parser.String, "value", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "name", false),
+							ast.NewField(ast.String, "value", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
 			"pipeline": {
 				Func: map[string]FuncLookup{
 					"stage": {
-						Params: []*parser.Field{
-							parser.NewField("pipeline", "pipelines", true),
+						Params: []*ast.Field{
+							ast.NewField("pipeline", "pipelines", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
-			parser.String: {
+			ast.String: {
 				Func: map[string]FuncLookup{
 					"format": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "formatString", false),
-							parser.NewField(parser.String, "values", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "formatString", false),
+							ast.NewField(ast.String, "values", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"localArch": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"localCwd": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"localEnv": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "key", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "key", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"localOs": {
-						Params:  []*parser.Field{},
-						Effects: []*parser.Field{},
+						Params:  []*ast.Field{},
+						Effects: []*ast.Field{},
 					},
 					"localRun": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "command", false),
-							parser.NewField(parser.String, "args", true),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "command", false),
+							ast.NewField(ast.String, "args", true),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 					"manifest": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "ref", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "ref", false),
 						},
-						Effects: []*parser.Field{
-							parser.NewField(parser.String, "digest", false),
-							parser.NewField(parser.String, "config", false),
-							parser.NewField(parser.String, "index", false),
+						Effects: []*ast.Field{
+							ast.NewField(ast.String, "digest", false),
+							ast.NewField(ast.String, "config", false),
+							ast.NewField(ast.String, "index", false),
 						},
 					},
 					"template": {
-						Params: []*parser.Field{
-							parser.NewField(parser.String, "text", false),
+						Params: []*ast.Field{
+							ast.NewField(ast.String, "text", false),
 						},
-						Effects: []*parser.Field{},
+						Effects: []*ast.Field{},
 					},
 				},
 			},
diff --git a/checker/builtin.go b/checker/builtin.go
index 43155c1..2cdbae8 100644
--- a/checker/builtin.go
+++ b/checker/builtin.go
@@ -2,7 +2,7 @@ package checker
 
 import (
 	"github.com/openllb/hlb/builtin"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 )
 
 // GlobalScope is a scope containing references to all builtins.
@@ -14,23 +14,23 @@ const (
 
 // NewBuiltinScope returns a new scope containing synthetic FuncDecl Objects for
 // builtins.
-func NewBuiltinScope(builtins builtin.BuiltinLookup) *parser.Scope {
-	scope := parser.NewScope(nil, nil)
-	parser.Match(builtin.Module, parser.MatchOpts{},
-		func(fun *parser.FuncDecl) {
+func NewBuiltinScope(builtins builtin.BuiltinLookup) *ast.Scope {
+	scope := ast.NewScope(nil, nil)
+	ast.Match(builtin.Module, ast.MatchOpts{},
+		func(fun *ast.FuncDecl) {
 			obj := scope.Lookup(fun.Name.Text)
 			if obj == nil {
-				obj = &parser.Object{
+				obj = &ast.Object{
 					Ident: fun.Name,
-					Node: &parser.BuiltinDecl{
+					Node: &ast.BuiltinDecl{
 						Module:         builtin.Module,
 						Name:           fun.Name.String(),
-						FuncDeclByKind: make(map[parser.Kind]*parser.FuncDecl),
+						FuncDeclByKind: make(map[ast.Kind]*ast.FuncDecl),
 					},
 				}
 			}
 
-			decl := obj.Node.(*parser.BuiltinDecl)
+			decl := obj.Node.(*ast.BuiltinDecl)
 			decl.Kinds = append(decl.Kinds, fun.Type.Kind)
 			decl.FuncDeclByKind[fun.Type.Kind] = fun
 			scope.Insert(obj)
diff --git a/checker/checker.go b/checker/checker.go
index c380bf4..1aace19 100644
--- a/checker/checker.go
+++ b/checker/checker.go
@@ -11,12 +11,12 @@ import (
 
 	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/errdefs"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 )
 
-func SemanticPass(mod *parser.Module) error {
+func SemanticPass(mod *ast.Module) error {
 	c := &checker{
-		dups: make(map[string][]parser.Node),
+		dups: make(map[string][]ast.Node),
 	}
 	return c.SemanticPass(mod)
 }
@@ -25,16 +25,16 @@ func SemanticPass(mod *parser.Module) error {
 //
 // References that refer to imported identifiers are checked with
 // CheckReferences after imports have been resolved.
-func Check(mod *parser.Module) error {
+func Check(mod *ast.Module) error {
 	return new(checker).Check(mod)
 }
 
 // CheckReferences checks for semantic errors for references. Imported modules
 // are assumed to be reachable through the given module.
-func CheckReferences(mod *parser.Module, name string) error {
+func CheckReferences(mod *ast.Module, name string) error {
 	c := &checker{
 		checkRefs: true,
-		dups:      make(map[string][]parser.Node),
+		dups:      make(map[string][]ast.Node),
 	}
 	return c.CheckReferences(mod, name)
 }
@@ -42,10 +42,10 @@ func CheckReferences(mod *parser.Module, name string) error {
 type checker struct {
 	checkRefs bool
 	errs      []error
-	dups      map[string][]parser.Node
+	dups      map[string][]ast.Node
 }
 
-func (c *checker) SemanticPass(mod *parser.Module) error {
+func (c *checker) SemanticPass(mod *ast.Module) error {
 	// Create a module-level scope.
 	//
 	// HLB is module-scoped, so HLBs in the same directory will have separate
@@ -53,35 +53,35 @@ func (c *checker) SemanticPass(mod *parser.Module) error {
 	//
 	// A module scope is a child of the global scope where builtin functions are
 	// defined.
-	mod.Scope = parser.NewScope(mod, GlobalScope)
+	mod.Scope = ast.NewScope(mod, GlobalScope)
 
 	// (1) Build lexical scopes and memoize semantic data into the CST.
-	parser.Match(mod, parser.MatchOpts{},
+	ast.Match(mod, ast.MatchOpts{},
 		// Register imports identifiers.
-		func(id *parser.ImportDecl) {
+		func(id *ast.ImportDecl) {
 			if id.Name != nil {
 				if id.Expr != nil {
 					c.registerDecl(mod.Scope, id.Name, id.Expr.Kind(), id)
 				} else if id.DeprecatedPath != nil {
-					c.registerDecl(mod.Scope, id.Name, parser.String, id)
+					c.registerDecl(mod.Scope, id.Name, ast.String, id)
 				}
 			}
 		},
 		// Register function identifiers and construct lexical scopes.
-		func(fun *parser.FuncDecl) {
+		func(fun *ast.FuncDecl) {
 			if fun.Name != nil {
 				c.registerDecl(mod.Scope, fun.Name, fun.Kind(), fun)
 			}
 
 			// Create a lexical scope for this function.
-			fun.Scope = parser.NewScope(fun, mod.Scope)
+			fun.Scope = ast.NewScope(fun, mod.Scope)
 
 			if fun.Params != nil {
 				// Create entries for call parameters to the function. Later at code
 				// generation time, functions are called by value so each argument's value
 				// will be inserted into their respective fields.
 				for _, param := range fun.Params.Fields() {
-					fun.Scope.Insert(&parser.Object{
+					fun.Scope.Insert(&ast.Object{
 						Kind:  param.Kind(),
 						Ident: param.Name,
 						Node:  param,
@@ -93,7 +93,7 @@ func (c *checker) SemanticPass(mod *parser.Module) error {
 			// side effect has a register that binded values can be written to.
 			if fun.Effects != nil && fun.Effects.Effects != nil {
 				for _, effect := range fun.Effects.Effects.Fields() {
-					fun.Scope.Insert(&parser.Object{
+					fun.Scope.Insert(&ast.Object{
 						Kind:  effect.Kind(),
 						Ident: effect.Name,
 						Node:  effect,
@@ -107,35 +107,35 @@ func (c *checker) SemanticPass(mod *parser.Module) error {
 				fun.Body.Type = fun.Type
 
 				// BindClause rule (1): Option blocks do not have a closure for bindings.
-				if fun.Type.Kind.Primary() != parser.Option {
+				if fun.Type.Kind.Primary() != ast.Option {
 					fun.Body.Closure = fun
 				}
 			}
 		},
 		// Function literals propagate its return type to its BlockStmt.
-		func(lit *parser.FuncLit) {
-			if lit.Type.Kind != parser.Option {
+		func(lit *ast.FuncLit) {
+			if lit.Type.Kind != ast.Option {
 				lit.Body.Type = lit.Type
 			}
 		},
 		// ImportDecl's BlockStmts have module-level scope.
-		func(_ *parser.ImportDecl, lit *parser.FuncLit) {
+		func(_ *ast.ImportDecl, lit *ast.FuncLit) {
 			lit.Body.Scope = mod.Scope
 		},
 		// FuncDecl's BlockStmts have function-level scope.
-		func(fun *parser.FuncDecl, lit *parser.FuncLit) {
+		func(fun *ast.FuncDecl, lit *ast.FuncLit) {
 			lit.Body.Scope = fun.Scope
 		},
 		// Function literals propagate its scope to its children.
-		func(parentLit *parser.FuncLit, lit *parser.FuncLit) {
+		func(parentLit *ast.FuncLit, lit *ast.FuncLit) {
 			lit.Body.Scope = parentLit.Body.Scope
 		},
 		// WithClause's function literals need to infer its secondary type from its
 		// parent call statement. For example, `run with option { ... }` has a
 		// `option` type function literal, but infers its type as `option::run`.
-		func(call *parser.CallStmt, with *parser.WithClause, lit *parser.FuncLit) {
-			if lit.Type.Kind == parser.Option {
-				lit.Type.Kind = parser.Kind(fmt.Sprintf("%s::%s", lit.Type.Kind, call.Name.Ident))
+		func(call *ast.CallStmt, with *ast.WithClause, lit *ast.FuncLit) {
+			if lit.Type.Kind == ast.Option {
+				lit.Type.Kind = ast.Kind(fmt.Sprintf("%s::%s", lit.Type.Kind, call.Name.Ident))
 			}
 			lit.Body.Type = lit.Type
 		},
@@ -146,7 +146,7 @@ func (c *checker) SemanticPass(mod *parser.Module) error {
 	c.checkBinds(mod)
 
 	if len(c.dups) > 0 {
-		var nodes []parser.Node
+		var nodes []ast.Node
 		for _, dups := range c.dups {
 			nodes = append(nodes, dups[0])
 		}
@@ -164,10 +164,10 @@ func (c *checker) SemanticPass(mod *parser.Module) error {
 	return nil
 }
 
-func (c *checker) checkBinds(mod *parser.Module) {
-	parser.Match(mod, parser.MatchOpts{},
+func (c *checker) checkBinds(mod *ast.Module) {
+	ast.Match(mod, ast.MatchOpts{},
 		// BindClause rule (2): `with` provides access to parent closure.
-		func(fun *parser.FuncDecl, _ *parser.WithClause, block *parser.BlockStmt) {
+		func(fun *ast.FuncDecl, _ *ast.WithClause, block *ast.BlockStmt) {
 			block.Closure = fun
 		},
 		// Register bind clauses in the parent function body.
@@ -175,7 +175,7 @@ func (c *checker) checkBinds(mod *parser.Module) {
 		// 1. Option blocks do not have a closure for bindings.
 		// 2. `with` provides access to parent closure.
 		// 3. Binds are only allowed with a closure.
-		func(block *parser.BlockStmt, call *parser.CallStmt, binds *parser.BindClause) {
+		func(block *ast.BlockStmt, call *ast.CallStmt, binds *ast.BindClause) {
 			// BindClause rule (3): Binds are only allowed with a closure.
 			if block.Closure == nil {
 				return
@@ -188,7 +188,7 @@ func (c *checker) checkBinds(mod *parser.Module) {
 			}
 		},
 		// Binds without closure should error.
-		func(block *parser.BlockStmt, binds *parser.BindClause) {
+		func(block *ast.BlockStmt, binds *ast.BindClause) {
 			if binds.Closure == nil {
 				c.err(errdefs.WithNoBindClosure(binds.As, block.Type))
 			}
@@ -196,18 +196,18 @@ func (c *checker) checkBinds(mod *parser.Module) {
 	)
 }
 
-func (c *checker) Check(mod *parser.Module) error {
+func (c *checker) Check(mod *ast.Module) error {
 	// Second pass over the CST.
 	// (2) Type checking and other semantic checks.
-	parser.Match(mod, parser.MatchOpts{},
-		func(id *parser.ImportDecl) {
-			kset := parser.NewKindSet(parser.String, parser.Filesystem)
+	ast.Match(mod, ast.MatchOpts{},
+		func(id *ast.ImportDecl) {
+			kset := ast.NewKindSet(ast.String, ast.Filesystem)
 			err := c.checkExpr(mod.Scope, kset, id.Expr)
 			if err != nil {
 				c.err(err)
 			}
 		},
-		func(ed *parser.ExportDecl) {
+		func(ed *ast.ExportDecl) {
 			if ed.Name == nil {
 				return
 			}
@@ -219,7 +219,7 @@ func (c *checker) Check(mod *parser.Module) error {
 				obj.Exported = true
 			}
 		},
-		func(fun *parser.FuncDecl) {
+		func(fun *ast.FuncDecl) {
 			if fun.Params != nil {
 				err := c.checkFieldList(fun.Params.Fields())
 				if err != nil {
@@ -251,23 +251,23 @@ func (c *checker) Check(mod *parser.Module) error {
 	return nil
 }
 
-func (c *checker) CheckReferences(mod *parser.Module, name string) error {
+func (c *checker) CheckReferences(mod *ast.Module, name string) error {
 	// Third pass over the CST.
 	// 3. After imports have resolved, semantic checks of imported identifiers.
-	parser.Match(mod, parser.MatchOpts{},
-		func(id *parser.ImportDecl) {
-			kset := parser.NewKindSet(parser.String, parser.Filesystem)
+	ast.Match(mod, ast.MatchOpts{},
+		func(id *ast.ImportDecl) {
+			kset := ast.NewKindSet(ast.String, ast.Filesystem)
 			err := c.checkExpr(mod.Scope, kset, id.Expr)
 			if err != nil {
 				c.err(err)
 			}
 		},
-		func(block *parser.BlockStmt, call *parser.CallStmt) {
+		func(block *ast.BlockStmt, call *ast.CallStmt) {
 			if call.Name.Ident.Text != name {
 				return
 			}
 
-			kset := parser.NewKindSet(block.Kind())
+			kset := ast.NewKindSet(block.Kind())
 			err := c.checkCallStmt(block.Scope, kset, call)
 			if err != nil {
 				c.err(err)
@@ -286,9 +286,9 @@ func (c *checker) err(err error) {
 }
 
 // checkFieldList checks for duplicate fields.
-func (c *checker) checkFieldList(fields []*parser.Field) error {
-	var dups []parser.Node
-	fieldSet := make(map[string]*parser.Field)
+func (c *checker) checkFieldList(fields []*ast.Field) error {
+	var dups []ast.Node
+	fieldSet := make(map[string]*ast.Field)
 	for _, field := range fields {
 		if field.Name == nil {
 			continue
@@ -308,9 +308,9 @@ func (c *checker) checkFieldList(fields []*parser.Field) error {
 	return errdefs.WithDuplicates(dups)
 }
 
-func (c *checker) checkBlock(block *parser.BlockStmt) error {
+func (c *checker) checkBlock(block *ast.BlockStmt) error {
 	for _, stmt := range block.Stmts() {
-		kset := parser.NewKindSet(block.Kind())
+		kset := ast.NewKindSet(block.Kind())
 
 		var err error
 		switch {
@@ -327,10 +327,10 @@ func (c *checker) checkBlock(block *parser.BlockStmt) error {
 	return nil
 }
 
-func (c *checker) checkType(node parser.Node, kset *parser.KindSet, actual parser.Kind, opts ...diagnostic.Option) error {
+func (c *checker) checkType(node ast.Node, kset *ast.KindSet, actual ast.Kind, opts ...diagnostic.Option) error {
 	if !kset.Has(actual) {
 		expected := kset.Kinds()
-		if expected[0] == parser.Option {
+		if expected[0] == ast.Option {
 			expected = expected[1:]
 		}
 		return errdefs.WithWrongType(node, expected, actual, opts...)
@@ -338,12 +338,12 @@ func (c *checker) checkType(node parser.Node, kset *parser.KindSet, actual parse
 	return nil
 }
 
-func (c *checker) checkCallStmt(scope *parser.Scope, kset *parser.KindSet, call *parser.CallStmt) error {
+func (c *checker) checkCallStmt(scope *ast.Scope, kset *ast.KindSet, call *ast.CallStmt) error {
 	signature, err := c.checkCall(scope, kset, call.Name, call.Args, call.WithClause)
 	if err != nil {
 		return err
 	}
-	var kinds []parser.Kind
+	var kinds []ast.Kind
 	for _, field := range signature {
 		kinds = append(kinds, field.Kind())
 	}
@@ -351,12 +351,12 @@ func (c *checker) checkCallStmt(scope *parser.Scope, kset *parser.KindSet, call
 	return nil
 }
 
-func (c *checker) checkCallExpr(scope *parser.Scope, kset *parser.KindSet, call *parser.CallExpr) error {
+func (c *checker) checkCallExpr(scope *ast.Scope, kset *ast.KindSet, call *ast.CallExpr) error {
 	signature, err := c.checkCall(scope, kset, call.Name, call.Args(), nil)
 	if err != nil {
 		return err
 	}
-	var kinds []parser.Kind
+	var kinds []ast.Kind
 	for _, field := range signature {
 		kinds = append(kinds, field.Kind())
 	}
@@ -364,7 +364,7 @@ func (c *checker) checkCallExpr(scope *parser.Scope, kset *parser.KindSet, call
 	return nil
 }
 
-func (c *checker) skip(ie *parser.IdentExpr) bool {
+func (c *checker) skip(ie *ast.IdentExpr) bool {
 	// If not checking references, skip if IdentExpr has a reference.
 	if !c.checkRefs {
 		return ie.Reference != nil
@@ -372,7 +372,7 @@ func (c *checker) skip(ie *parser.IdentExpr) bool {
 	return false
 }
 
-func (c *checker) checkCall(scope *parser.Scope, kset *parser.KindSet, ie *parser.IdentExpr, args []*parser.Expr, with *parser.WithClause) ([]*parser.Field, error) {
+func (c *checker) checkCall(scope *ast.Scope, kset *ast.KindSet, ie *ast.IdentExpr, args []*ast.Expr, with *ast.WithClause) ([]*ast.Field, error) {
 	decl, signature, err := c.checkIdentExpr(scope, kset, ie)
 	if err != nil {
 		return nil, err
@@ -395,7 +395,7 @@ func (c *checker) checkCall(scope *parser.Scope, kset *parser.KindSet, ie *parse
 
 	for i, arg := range args {
 		kind := params[i].Type.Kind
-		err := c.checkExpr(scope, parser.NewKindSet(kind), arg)
+		err := c.checkExpr(scope, ast.NewKindSet(kind), arg)
 		if err != nil {
 			return nil, err
 		}
@@ -403,8 +403,8 @@ func (c *checker) checkCall(scope *parser.Scope, kset *parser.KindSet, ie *parse
 
 	if with != nil {
 		// Inherit the secondary type from the calling function name.
-		kind := parser.Kind(fmt.Sprintf("%s::%s", parser.Option, ie.Ident))
-		err := c.checkExpr(scope, parser.NewKindSet(kind), with.Expr)
+		kind := ast.Kind(fmt.Sprintf("%s::%s", ast.Option, ie.Ident))
+		err := c.checkExpr(scope, ast.NewKindSet(kind), with.Expr)
 		if err != nil {
 			return nil, err
 		}
@@ -413,14 +413,14 @@ func (c *checker) checkCall(scope *parser.Scope, kset *parser.KindSet, ie *parse
 	return params, nil
 }
 
-func (c *checker) checkExpr(scope *parser.Scope, kset *parser.KindSet, expr *parser.Expr) error {
-	if kset.Has(parser.Pipeline) {
-		kset = parser.NewKindSet(append(
+func (c *checker) checkExpr(scope *ast.Scope, kset *ast.KindSet, expr *ast.Expr) error {
+	if kset.Has(ast.Pipeline) {
+		kset = ast.NewKindSet(append(
 			kset.Kinds(),
-			parser.String,
-			parser.Int,
-			parser.Bool,
-			parser.Filesystem,
+			ast.String,
+			ast.Int,
+			ast.Bool,
+			ast.Filesystem,
 		)...)
 	}
 	switch {
@@ -447,7 +447,7 @@ func (c *checker) checkExpr(scope *parser.Scope, kset *parser.KindSet, expr *par
 	return errdefs.WithInternalErrorf(expr, "invalid expr")
 }
 
-func (c *checker) checkFuncLit(kset *parser.KindSet, lit *parser.FuncLit) error {
+func (c *checker) checkFuncLit(kset *ast.KindSet, lit *ast.FuncLit) error {
 	err := c.checkType(lit.Type, kset, lit.Type.Kind)
 	if err != nil {
 		return err
@@ -455,11 +455,11 @@ func (c *checker) checkFuncLit(kset *parser.KindSet, lit *parser.FuncLit) error
 	return c.checkBlock(lit.Body)
 }
 
-func (c *checker) checkBasicLit(scope *parser.Scope, kind parser.Kind, lit *parser.BasicLit) error {
+func (c *checker) checkBasicLit(scope *ast.Scope, kind ast.Kind, lit *ast.BasicLit) error {
 	switch kind {
-	case parser.String:
+	case ast.String:
 		if lit.Str == nil && lit.RawString == nil && lit.Heredoc == nil && lit.RawHeredoc == nil {
-			return errdefs.WithWrongType(lit, []parser.Kind{kind}, lit.Kind())
+			return errdefs.WithWrongType(lit, []ast.Kind{kind}, lit.Kind())
 		}
 		switch {
 		case lit.Str != nil:
@@ -469,22 +469,22 @@ func (c *checker) checkBasicLit(scope *parser.Scope, kind parser.Kind, lit *pars
 		case lit.RawHeredoc != nil:
 			return c.checkHeredocFragments(scope, lit.RawHeredoc.Fragments)
 		}
-	case parser.Int:
+	case ast.Int:
 		if lit.Decimal == nil && lit.Numeric == nil {
-			return errdefs.WithWrongType(lit, []parser.Kind{kind}, lit.Kind())
+			return errdefs.WithWrongType(lit, []ast.Kind{kind}, lit.Kind())
 		}
-	case parser.Bool:
+	case ast.Bool:
 		if lit.Bool == nil {
-			return errdefs.WithWrongType(lit, []parser.Kind{kind}, lit.Kind())
+			return errdefs.WithWrongType(lit, []ast.Kind{kind}, lit.Kind())
 		}
 	default:
-		return errdefs.WithWrongType(lit, []parser.Kind{kind}, lit.Kind())
+		return errdefs.WithWrongType(lit, []ast.Kind{kind}, lit.Kind())
 	}
 	return nil
 }
 
-func (c *checker) checkStringFragments(scope *parser.Scope, fragments []*parser.StringFragment) error {
-	kset := parser.NewKindSet(parser.String, parser.Int, parser.Bool)
+func (c *checker) checkStringFragments(scope *ast.Scope, fragments []*ast.StringFragment) error {
+	kset := ast.NewKindSet(ast.String, ast.Int, ast.Bool)
 	for _, f := range fragments {
 		if f.Interpolated == nil {
 			continue
@@ -497,8 +497,8 @@ func (c *checker) checkStringFragments(scope *parser.Scope, fragments []*parser.
 	return nil
 }
 
-func (c *checker) checkHeredocFragments(scope *parser.Scope, fragments []*parser.HeredocFragment) error {
-	kset := parser.NewKindSet(parser.String, parser.Int, parser.Bool)
+func (c *checker) checkHeredocFragments(scope *ast.Scope, fragments []*ast.HeredocFragment) error {
+	kset := ast.NewKindSet(ast.String, ast.Int, ast.Bool)
 	for _, f := range fragments {
 		if f.Interpolated == nil {
 			continue
@@ -511,11 +511,11 @@ func (c *checker) checkHeredocFragments(scope *parser.Scope, fragments []*parser
 	return nil
 }
 
-func (c *checker) checkIdentExpr(scope *parser.Scope, kset *parser.KindSet, ie *parser.IdentExpr) (ident *parser.Ident, signature []*parser.Field, err error) {
+func (c *checker) checkIdentExpr(scope *ast.Scope, kset *ast.KindSet, ie *ast.IdentExpr) (ident *ast.Ident, signature []*ast.Field, err error) {
 	return c.checkIdentExprHelper(scope, kset, ie, ie.Ident)
 }
 
-func (c *checker) checkIdentExprHelper(scope *parser.Scope, kset *parser.KindSet, ie *parser.IdentExpr, lookup *parser.Ident, opts ...diagnostic.Option) (ident *parser.Ident, signature []*parser.Field, err error) {
+func (c *checker) checkIdentExprHelper(scope *ast.Scope, kset *ast.KindSet, ie *ast.IdentExpr, lookup *ast.Ident, opts ...diagnostic.Option) (ident *ast.Ident, signature []*ast.Field, err error) {
 	obj := scope.Lookup(lookup.Text)
 	if obj == nil {
 		err = errdefs.WithUndefinedIdent(lookup, scope.Suggestion(lookup.Text, kset), opts...)
@@ -524,7 +524,7 @@ func (c *checker) checkIdentExprHelper(scope *parser.Scope, kset *parser.KindSet
 
 	if ie.Reference != nil {
 		if lookup == ie.Ident {
-			if _, ok := obj.Node.(*parser.ImportDecl); !ok {
+			if _, ok := obj.Node.(*ast.ImportDecl); !ok {
 				err = errdefs.WithNotImport(ie, obj.Ident)
 				return
 			}
@@ -539,34 +539,34 @@ func (c *checker) checkIdentExprHelper(scope *parser.Scope, kset *parser.KindSet
 	}
 
 	switch n := obj.Node.(type) {
-	case *parser.BuiltinDecl:
-		var fun *parser.FuncDecl
+	case *ast.BuiltinDecl:
+		var fun *ast.FuncDecl
 		fun, err = c.lookupBuiltin(ie.Ident, kset, n)
 		if err != nil {
 			return
 		}
 		opts = append(opts, errdefs.Defined(fun.Name))
 		return fun.Name, fun.Params.Fields(), c.checkType(lookup, kset, fun.Type.Kind, opts...)
-	case *parser.FuncDecl:
+	case *ast.FuncDecl:
 		opts = append(opts, errdefs.Defined(obj.Ident))
 		return obj.Ident, n.Params.Fields(), c.checkType(lookup, kset, n.Type.Kind, opts...)
-	case *parser.BindClause:
+	case *ast.BindClause:
 		typ := n.TargetBinding(lookup.Text).Field.Type
 		opts = append(opts, errdefs.Defined(obj.Ident))
 		return obj.Ident, n.Closure.Params.Fields(), c.checkType(lookup, kset, typ.Kind, opts...)
-	case *parser.ImportDecl:
+	case *ast.ImportDecl:
 		if ie.Reference == nil {
 			err = errdefs.WithCallImport(ie.Ident, n.Name)
 			return
 		}
-		imod, ok := obj.Data.(*parser.Module)
+		imod, ok := obj.Data.(*ast.Module)
 		if !ok {
 			err = errdefs.WithInternalErrorf(ie.Ident, "import scope is not set")
 			return
 		}
 		opts = append(opts, errdefs.Imported(obj.Ident))
 		return c.checkIdentExprHelper(imod.Scope, kset, ie, ie.Reference.Ident, opts...)
-	case *parser.Field:
+	case *ast.Field:
 		opts = append(opts, errdefs.Defined(obj.Ident))
 		return obj.Ident, nil, c.checkType(lookup, kset, n.Type.Kind, opts...)
 	default:
@@ -575,7 +575,7 @@ func (c *checker) checkIdentExprHelper(scope *parser.Scope, kset *parser.KindSet
 	}
 }
 
-func (c *checker) registerDecl(scope *parser.Scope, ident *parser.Ident, kind parser.Kind, node parser.Node) {
+func (c *checker) registerDecl(scope *ast.Scope, ident *ast.Ident, kind ast.Kind, node ast.Node) {
 	// Ensure that this identifier is not already defined in the module scope.
 	obj := scope.Lookup(ident.Text)
 	if obj != nil {
@@ -586,14 +586,14 @@ func (c *checker) registerDecl(scope *parser.Scope, ident *parser.Ident, kind pa
 		return
 	}
 
-	scope.Insert(&parser.Object{
+	scope.Insert(&ast.Object{
 		Kind:  kind,
 		Ident: ident,
 		Node:  node,
 	})
 }
 
-func (c *checker) registerBinds(scope *parser.Scope, kind parser.Kind, fun *parser.FuncDecl, call *parser.CallStmt, binds *parser.BindClause) error {
+func (c *checker) registerBinds(scope *ast.Scope, kind ast.Kind, fun *ast.FuncDecl, call *ast.CallStmt, binds *ast.BindClause) error {
 	// Bind to its lexical scope.
 	binds.Closure = fun
 	err := c.bindEffects(scope, kind, call)
@@ -614,7 +614,7 @@ func (c *checker) registerBinds(scope *parser.Scope, kind parser.Kind, fun *pars
 	return nil
 }
 
-func (c *checker) bindEffects(scope *parser.Scope, kind parser.Kind, call *parser.CallStmt) error {
+func (c *checker) bindEffects(scope *ast.Scope, kind ast.Kind, call *ast.CallStmt) error {
 	binds := call.BindClause
 	if binds == nil {
 		return nil
@@ -629,7 +629,7 @@ func (c *checker) bindEffects(scope *parser.Scope, kind parser.Kind, call *parse
 	}
 
 	var (
-		kset = parser.NewKindSet(kind)
+		kset = ast.NewKindSet(kind)
 		ie   = call.Name
 	)
 
@@ -638,7 +638,7 @@ func (c *checker) bindEffects(scope *parser.Scope, kind parser.Kind, call *parse
 		return err
 	}
 
-	bd, ok := scope.Lookup(ie.Ident.Text).Node.(*parser.BuiltinDecl)
+	bd, ok := scope.Lookup(ie.Ident.Text).Node.(*ast.BuiltinDecl)
 	if !ok {
 		return errdefs.WithNoBindEffects(
 			call.Name, binds.As,
@@ -666,7 +666,7 @@ func (c *checker) bindEffects(scope *parser.Scope, kind parser.Kind, call *parse
 	// Match each Bind to a Field on call's EffectsClause.
 	if binds.Binds != nil {
 		for _, b := range binds.Binds.Binds() {
-			var field *parser.Field
+			var field *ast.Field
 			for _, f := range binds.Effects.Fields() {
 				if f.Name.String() == b.Source.String() {
 					field = f
@@ -683,8 +683,8 @@ func (c *checker) bindEffects(scope *parser.Scope, kind parser.Kind, call *parse
 	return nil
 }
 
-func (c *checker) lookupBuiltin(node parser.Node, kset *parser.KindSet, bd *parser.BuiltinDecl) (*parser.FuncDecl, error) {
-	var fun *parser.FuncDecl
+func (c *checker) lookupBuiltin(node ast.Node, kset *ast.KindSet, bd *ast.BuiltinDecl) (*ast.FuncDecl, error) {
+	var fun *ast.FuncDecl
 	for _, kind := range kset.Kinds() {
 		fun = bd.FuncDecl(kind)
 		if fun != nil {
@@ -692,7 +692,7 @@ func (c *checker) lookupBuiltin(node parser.Node, kset *parser.KindSet, bd *pars
 		}
 	}
 	if fun == nil {
-		var kinds []parser.Kind
+		var kinds []ast.Kind
 		for kind := range bd.FuncDeclByKind {
 			kinds = append(kinds, kind)
 		}
@@ -710,19 +710,19 @@ func (c *checker) lookupBuiltin(node parser.Node, kset *parser.KindSet, bd *pars
 	return fun, nil
 }
 
-func extendSignatureWithVariadic(fields []*parser.Field, args []*parser.Expr) []*parser.Field {
+func extendSignatureWithVariadic(fields []*ast.Field, args []*ast.Expr) []*ast.Field {
 	if len(fields) == 0 {
 		return fields
 	}
 
-	params := make([]*parser.Field, len(fields))
+	params := make([]*ast.Field, len(fields))
 	copy(params, fields)
 
 	lastParam := params[len(params)-1]
 	if lastParam.Modifier != nil && lastParam.Modifier.Variadic != nil {
 		params = params[:len(params)-1]
 		for i := range args[len(params):] {
-			params = append(params, parser.NewField(
+			params = append(params, ast.NewField(
 				lastParam.Type.Kind,
 				fmt.Sprintf("%s[%d]", lastParam.Name, i),
 				false,
diff --git a/cmd/hlb/command/lint.go b/cmd/hlb/command/lint.go
index bfc1bc3..16256b0 100644
--- a/cmd/hlb/command/lint.go
+++ b/cmd/hlb/command/lint.go
@@ -14,6 +14,7 @@ import (
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/linter"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	cli "github.com/urfave/cli/v2"
 )
 
@@ -46,7 +47,7 @@ type LintInfo struct {
 }
 
 func Lint(ctx context.Context, r io.Reader, info LintInfo) error {
-	ctx = diagnostic.WithSources(ctx, builtin.Sources())
+	ctx = filebuffer.WithBuffers(ctx, builtin.Buffers())
 	mod, err := parser.Parse(ctx, r)
 	if err != nil {
 		return err
diff --git a/cmd/hlb/command/module.go b/cmd/hlb/command/module.go
index 48d9ee9..cecaa17 100644
--- a/cmd/hlb/command/module.go
+++ b/cmd/hlb/command/module.go
@@ -18,6 +18,8 @@ import (
 	"github.com/openllb/hlb/linter"
 	"github.com/openllb/hlb/module"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	"github.com/openllb/hlb/solver"
 	cli "github.com/urfave/cli/v2"
 	"github.com/xlab/treeprint"
@@ -138,7 +140,7 @@ func Vendor(ctx context.Context, cln *client.Client, info VendorInfo) (err error
 		err = errdefs.WithAbort(err, len(spans))
 	}()
 
-	ctx = diagnostic.WithSources(ctx, builtin.Sources())
+	ctx = filebuffer.WithBuffers(ctx, builtin.Buffers())
 	mod, err := parser.Parse(ctx, rc)
 	if err != nil {
 		return err
@@ -157,8 +159,8 @@ func Vendor(ctx context.Context, cln *client.Client, info VendorInfo) (err error
 	}
 
 	hasImports := false
-	parser.Match(mod, parser.MatchOpts{},
-		func(imp *parser.ImportDecl) {
+	ast.Match(mod, ast.MatchOpts{},
+		func(imp *ast.ImportDecl) {
 			hasImports = true
 		},
 	)
@@ -249,7 +251,7 @@ func Tree(ctx context.Context, cln *client.Client, info TreeInfo) (err error) {
 		err = errdefs.WithAbort(err, len(spans))
 	}()
 
-	ctx = diagnostic.WithSources(ctx, builtin.Sources())
+	ctx = filebuffer.WithBuffers(ctx, builtin.Buffers())
 	mod, err := parser.Parse(ctx, rc)
 	if err != nil {
 		return err
diff --git a/cmd/hlb/command/run.go b/cmd/hlb/command/run.go
index 603fbaa..54305c9 100644
--- a/cmd/hlb/command/run.go
+++ b/cmd/hlb/command/run.go
@@ -22,6 +22,7 @@ import (
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/local"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	"github.com/openllb/hlb/solver"
 	cli "github.com/urfave/cli/v2"
 	"github.com/xlab/treeprint"
@@ -209,7 +210,7 @@ func Run(ctx context.Context, cln *client.Client, rc io.ReadCloser, info RunInfo
 	// store Progress in context in case we need to synchronize output later
 	ctx = codegen.WithProgress(ctx, p)
 	ctx = codegen.WithMultiWriter(ctx, p.MultiWriter())
-	ctx = diagnostic.WithSources(ctx, builtin.Sources())
+	ctx = filebuffer.WithBuffers(ctx, builtin.Buffers())
 
 	defer func() {
 		if err == nil {
diff --git a/codegen/builtin.go b/codegen/builtin.go
index 20ec892..422348b 100644
--- a/codegen/builtin.go
+++ b/codegen/builtin.go
@@ -5,12 +5,12 @@ import (
 	"reflect"
 	"strings"
 
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 )
 
 var (
-	Callables = map[parser.Kind]map[string]interface{}{
-		parser.Filesystem: {
+	Callables = map[ast.Kind]map[string]interface{}{
+		ast.Filesystem: {
 			"scratch":               Scratch{},
 			"image":                 Image{},
 			"http":                  HTTP{},
@@ -41,7 +41,7 @@ var (
 			"downloadDockerTarball": DownloadDockerTarball{},
 			"breakpoint":            SetBreakpoint{},
 		},
-		parser.String: {
+		ast.String: {
 			"format":    Format{},
 			"template":  Template{},
 			"manifest":  Manifest{},
@@ -51,7 +51,7 @@ var (
 			"localEnv":  LocalEnv{},
 			"localRun":  LocalRun{},
 		},
-		parser.Pipeline: {
+		ast.Pipeline: {
 			"stage":    Stage{},
 			"parallel": Stage{},
 		},
diff --git a/codegen/builtin_option.go b/codegen/builtin_option.go
index ddbf1b7..e64395b 100644
--- a/codegen/builtin_option.go
+++ b/codegen/builtin_option.go
@@ -23,6 +23,7 @@ import (
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/pkg/llbutil"
 	"github.com/openllb/hlb/pkg/sockproxy"
 	"github.com/openllb/hlb/solver"
@@ -869,7 +870,7 @@ func (sp SourcePath) Call(ctx context.Context, cln *client.Client, val Value, op
 }
 
 type Cache struct {
-	parser.Node
+	ast.Node
 }
 
 func (c Cache) Call(ctx context.Context, cln *client.Client, val Value, opts Option, id, mode string) (Value, error) {
diff --git a/codegen/codegen.go b/codegen/codegen.go
index d596df4..0863e9c 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -14,6 +14,7 @@ import (
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/linter"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/solver"
 	"github.com/pkg/errors"
 )
@@ -61,7 +62,7 @@ type Target struct {
 	Name string
 }
 
-func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []Target) (solver.Request, error) {
+func (cg *CodeGen) Generate(ctx context.Context, mod *ast.Module, targets []Target) (solver.Request, error) {
 	var requests []solver.Request
 
 	for i, target := range targets {
@@ -78,7 +79,7 @@ func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []T
 		}
 
 		// Build expression for target.
-		ie := parser.NewIdentExpr(target.Name)
+		ie := ast.NewIdentExpr(target.Name)
 		ie.Pos.Filename = "target"
 		ie.Pos.Line = i
 
@@ -99,7 +100,7 @@ func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []T
 	return solver.Parallel(requests...), nil
 }
 
-func (cg *CodeGen) EmitExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []Value, opts Option, b *parser.Binding, ret Register) error {
+func (cg *CodeGen) EmitExpr(ctx context.Context, scope *ast.Scope, expr *ast.Expr, args []Value, opts Option, b *ast.Binding, ret Register) error {
 	ctx = WithProgramCounter(ctx, expr)
 
 	switch {
@@ -124,11 +125,11 @@ func (cg *CodeGen) EmitExpr(ctx context.Context, scope *parser.Scope, expr *pars
 	}
 }
 
-func (cg *CodeGen) EmitFuncLit(ctx context.Context, scope *parser.Scope, lit *parser.FuncLit, b *parser.Binding, ret Register) error {
+func (cg *CodeGen) EmitFuncLit(ctx context.Context, scope *ast.Scope, lit *ast.FuncLit, b *ast.Binding, ret Register) error {
 	return cg.EmitBlock(ctx, scope, lit.Body, b, ret)
 }
 
-func (cg *CodeGen) EmitBasicLit(ctx context.Context, scope *parser.Scope, lit *parser.BasicLit, ret Register) error {
+func (cg *CodeGen) EmitBasicLit(ctx context.Context, scope *ast.Scope, lit *ast.BasicLit, ret Register) error {
 	switch {
 	case lit.Decimal != nil:
 		return ret.Set(*lit.Decimal)
@@ -149,7 +150,7 @@ func (cg *CodeGen) EmitBasicLit(ctx context.Context, scope *parser.Scope, lit *p
 	}
 }
 
-func (cg *CodeGen) EmitStringLit(ctx context.Context, scope *parser.Scope, str *parser.StringLit, ret Register) error {
+func (cg *CodeGen) EmitStringLit(ctx context.Context, scope *ast.Scope, str *ast.StringLit, ret Register) error {
 	var pieces []string
 	for _, f := range str.Fragments {
 		switch {
@@ -184,7 +185,7 @@ func (cg *CodeGen) EmitStringLit(ctx context.Context, scope *parser.Scope, str *
 	return ret.Set(strings.Join(pieces, ""))
 }
 
-func (cg *CodeGen) EmitHeredoc(ctx context.Context, scope *parser.Scope, heredoc *parser.Heredoc, ret Register) error {
+func (cg *CodeGen) EmitHeredoc(ctx context.Context, scope *ast.Scope, heredoc *ast.Heredoc, ret Register) error {
 	var pieces []string
 	for _, f := range heredoc.Fragments {
 		switch {
@@ -239,7 +240,7 @@ func emitHeredocPieces(start, terminate string, pieces []string, ret Register) e
 	}
 }
 
-func (cg *CodeGen) EmitRawHeredoc(ctx context.Context, scope *parser.Scope, heredoc *parser.RawHeredoc, ret Register) error {
+func (cg *CodeGen) EmitRawHeredoc(ctx context.Context, scope *ast.Scope, heredoc *ast.RawHeredoc, ret Register) error {
 	var pieces []string
 	for _, f := range heredoc.Fragments {
 		switch {
@@ -254,7 +255,7 @@ func (cg *CodeGen) EmitRawHeredoc(ctx context.Context, scope *parser.Scope, here
 	return emitHeredocPieces(heredoc.Start, terminate, pieces, ret)
 }
 
-func (cg *CodeGen) EmitCallExpr(ctx context.Context, scope *parser.Scope, call *parser.CallExpr, ret Register) error {
+func (cg *CodeGen) EmitCallExpr(ctx context.Context, scope *ast.Scope, call *ast.CallExpr, ret Register) error {
 	ctx = WithFrame(ctx, Frame{call.Name})
 
 	// Yield before executing call expression.
@@ -274,7 +275,7 @@ func (cg *CodeGen) EmitCallExpr(ctx context.Context, scope *parser.Scope, call *
 	return cg.EmitIdentExpr(ctx, scope, call.Name, call.Name.Ident, args, nil, nil, ret)
 }
 
-func (cg *CodeGen) EmitIdentExpr(ctx context.Context, scope *parser.Scope, ie *parser.IdentExpr, lookup *parser.Ident, args []Value, opts Option, b *parser.Binding, ret Register) error {
+func (cg *CodeGen) EmitIdentExpr(ctx context.Context, scope *ast.Scope, ie *ast.IdentExpr, lookup *ast.Ident, args []Value, opts Option, b *ast.Binding, ret Register) error {
 	ctx = WithProgramCounter(ctx, ie)
 
 	obj := scope.Lookup(lookup.Text)
@@ -283,26 +284,26 @@ func (cg *CodeGen) EmitIdentExpr(ctx context.Context, scope *parser.Scope, ie *p
 	}
 
 	switch n := obj.Node.(type) {
-	case *parser.BuiltinDecl:
+	case *ast.BuiltinDecl:
 		return ret.SetAsync(func(v Value) (Value, error) {
 			return cg.EmitBuiltinDecl(ctx, scope, n, args, opts, b, v)
 		})
-	case *parser.FuncDecl:
+	case *ast.FuncDecl:
 		return cg.EmitFuncDecl(ctx, n, args, nil, ret)
-	case *parser.BindClause:
+	case *ast.BindClause:
 		return cg.EmitBinding(ctx, n.TargetBinding(lookup.Text), args, ret)
-	case *parser.ImportDecl:
-		imod, ok := obj.Data.(*parser.Module)
+	case *ast.ImportDecl:
+		imod, ok := obj.Data.(*ast.Module)
 		if !ok {
 			return errdefs.WithInternalErrorf(ProgramCounter(ctx), "expected imported module to be resolved")
 		}
 		return cg.EmitIdentExpr(ctx, imod.Scope, ie, ie.Reference.Ident, args, opts, nil, ret)
-	case *parser.Field:
+	case *ast.Field:
 		val, err := NewValue(ctx, obj.Data)
 		if err != nil {
 			return err
 		}
-		if val.Kind() != parser.Option || ret.Value().Kind() != parser.Option {
+		if val.Kind() != ast.Option || ret.Value().Kind() != ast.Option {
 			return ret.Set(val)
 		} else {
 			retOpts, err := ret.Value().Option()
@@ -320,9 +321,9 @@ func (cg *CodeGen) EmitIdentExpr(ctx context.Context, scope *parser.Scope, ie *p
 	}
 }
 
-func (cg *CodeGen) EmitImport(ctx context.Context, mod *parser.Module, id *parser.ImportDecl) (imod *parser.Module, filename string, err error) {
+func (cg *CodeGen) EmitImport(ctx context.Context, mod *ast.Module, id *ast.ImportDecl) (imod *ast.Module, filename string, err error) {
 	// Import expression can be string or fs.
-	ctx = WithReturnType(ctx, parser.None)
+	ctx = WithReturnType(ctx, ast.None)
 
 	ret := NewRegister(ctx)
 	err = cg.EmitExpr(ctx, mod.Scope, id.Expr, nil, nil, nil, ret)
@@ -333,7 +334,7 @@ func (cg *CodeGen) EmitImport(ctx context.Context, mod *parser.Module, id *parse
 
 	dir := mod.Directory
 	switch val.Kind() {
-	case parser.Filesystem:
+	case ast.Filesystem:
 		var fs Filesystem
 		fs, err = val.Filesystem()
 		if err != nil {
@@ -345,7 +346,7 @@ func (cg *CodeGen) EmitImport(ctx context.Context, mod *parser.Module, id *parse
 		if err != nil {
 			return
 		}
-	case parser.String:
+	case ast.String:
 		filename, err = val.String()
 		if err != nil {
 			return
@@ -388,9 +389,9 @@ func (cg *CodeGen) EmitImport(ctx context.Context, mod *parser.Module, id *parse
 	return
 }
 
-func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *parser.Scope, bd *parser.BuiltinDecl, args []Value, opts Option, b *parser.Binding, v Value) (Value, error) {
+func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *ast.Scope, bd *ast.BuiltinDecl, args []Value, opts Option, b *ast.Binding, v Value) (Value, error) {
 	var callable interface{}
-	if ReturnType(ctx) != parser.None {
+	if ReturnType(ctx) != ast.None {
 		callable = Callables[ReturnType(ctx)][bd.Name]
 	} else {
 		for _, kind := range bd.Kinds {
@@ -467,7 +468,7 @@ func (cg *CodeGen) EmitBuiltinDecl(ctx context.Context, scope *parser.Scope, bd
 	return outs[0].Interface().(Value), nil
 }
 
-func (cg *CodeGen) EmitFuncDecl(ctx context.Context, fun *parser.FuncDecl, args []Value, b *parser.Binding, ret Register) error {
+func (cg *CodeGen) EmitFuncDecl(ctx context.Context, fun *ast.FuncDecl, args []Value, b *ast.Binding, ret Register) error {
 	ctx = WithProgramCounter(ctx, fun.Name)
 
 	params := fun.Params.Fields()
@@ -479,13 +480,13 @@ func (cg *CodeGen) EmitFuncDecl(ctx context.Context, fun *parser.FuncDecl, args
 		return errdefs.WithInternalErrorf(ProgramCounter(ctx), "`%s` expected %d args, got %d", name, len(params), len(args))
 	}
 
-	scope := parser.NewScope(fun, fun.Scope)
+	scope := ast.NewScope(fun, fun.Scope)
 	for i, param := range params {
 		if param.Modifier != nil {
 			continue
 		}
 
-		scope.Insert(&parser.Object{
+		scope.Insert(&ast.Object{
 			Kind:  param.Kind(),
 			Ident: param.Name,
 			Node:  param,
@@ -502,24 +503,24 @@ func (cg *CodeGen) EmitFuncDecl(ctx context.Context, fun *parser.FuncDecl, args
 	return cg.EmitBlock(ctx, scope, fun.Body, b, ret)
 }
 
-func (cg *CodeGen) EmitBinding(ctx context.Context, b *parser.Binding, args []Value, ret Register) error {
+func (cg *CodeGen) EmitBinding(ctx context.Context, b *ast.Binding, args []Value, ret Register) error {
 	return cg.EmitFuncDecl(ctx, b.Bind.Closure, args, b, ret)
 }
 
-func (cg *CodeGen) lookupCall(ctx context.Context, scope *parser.Scope, lookup *parser.Ident) error {
+func (cg *CodeGen) lookupCall(ctx context.Context, scope *ast.Scope, lookup *ast.Ident) error {
 	obj := scope.Lookup(lookup.Text)
 	if obj == nil {
 		return errors.WithStack(errdefs.WithUndefinedIdent(lookup, nil))
 	}
 
 	switch n := obj.Node.(type) {
-	case *parser.ImportDecl:
-		_, ok := obj.Data.(*parser.Module)
+	case *ast.ImportDecl:
+		_, ok := obj.Data.(*ast.Module)
 		if ok {
 			break
 		}
 
-		mod := scope.Root().Node.(*parser.Module)
+		mod := scope.Root().Node.(*ast.Module)
 		imod, _, err := cg.EmitImport(ctx, mod, n)
 		if err != nil {
 			return err
@@ -535,7 +536,7 @@ func (cg *CodeGen) lookupCall(ctx context.Context, scope *parser.Scope, lookup *
 	return nil
 }
 
-func (cg *CodeGen) EmitBlock(ctx context.Context, scope *parser.Scope, block *parser.BlockStmt, b *parser.Binding, ret Register) error {
+func (cg *CodeGen) EmitBlock(ctx context.Context, scope *ast.Scope, block *ast.BlockStmt, b *ast.Binding, ret Register) error {
 	if block == nil {
 		return nil
 	}
@@ -570,7 +571,7 @@ func (cg *CodeGen) EmitBlock(ctx context.Context, scope *parser.Scope, block *pa
 	return nil
 }
 
-func (cg *CodeGen) EmitCallStmt(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, b *parser.Binding, ret Register) error {
+func (cg *CodeGen) EmitCallStmt(ctx context.Context, scope *ast.Scope, call *ast.CallStmt, b *ast.Binding, ret Register) error {
 	ctx = WithFrame(ctx, Frame{call.Name})
 
 	args, err := cg.Evaluate(ctx, scope, nil, call.Signature, call.Args...)
@@ -584,8 +585,8 @@ func (cg *CodeGen) EmitCallStmt(ctx context.Context, scope *parser.Scope, call *
 	var opts Option
 	if call.WithClause != nil {
 		// Provide a type hint to avoid ambgiuous lookups.
-		kinds := []parser.Kind{
-			parser.Kind(fmt.Sprintf("%s::%s", parser.Option, call.Name)),
+		kinds := []ast.Kind{
+			ast.Kind(fmt.Sprintf("%s::%s", ast.Option, call.Name)),
 		}
 
 		// WithClause provides option expressions access to the binding.
@@ -604,7 +605,7 @@ func (cg *CodeGen) EmitCallStmt(ctx context.Context, scope *parser.Scope, call *
 	if call.Breakpoint(ReturnType(ctx)) {
 		var command []string
 		for _, arg := range args {
-			if arg.Kind() != parser.String {
+			if arg.Kind() != ast.String {
 				return errors.New("breakpoint args must be strings")
 			}
 			argStr, err := arg.String()
@@ -623,7 +624,7 @@ func (cg *CodeGen) EmitCallStmt(ctx context.Context, scope *parser.Scope, call *
 	}
 
 	// Pass the binding if this is the matching CallStmt.
-	var binding *parser.Binding
+	var binding *ast.Binding
 	if b != nil && call.BindClause == b.Bind {
 		binding = b
 	}
@@ -633,7 +634,7 @@ func (cg *CodeGen) EmitCallStmt(ctx context.Context, scope *parser.Scope, call *
 
 type breakpointCommand []string
 
-func (cg *CodeGen) Evaluate(ctx context.Context, scope *parser.Scope, b *parser.Binding, kinds []parser.Kind, exprs ...*parser.Expr) (values []Value, err error) {
+func (cg *CodeGen) Evaluate(ctx context.Context, scope *ast.Scope, b *ast.Binding, kinds []ast.Kind, exprs ...*ast.Expr) (values []Value, err error) {
 	if len(kinds) != len(exprs) {
 		return nil, errdefs.WithInternalErrorf(ProgramCounter(ctx), "expected %d kinds but got %d", len(exprs), len(kinds))
 	}
diff --git a/codegen/context.go b/codegen/context.go
index 967f0f4..361e84b 100644
--- a/codegen/context.go
+++ b/codegen/context.go
@@ -12,8 +12,8 @@ import (
 	"github.com/moby/buildkit/solver/errdefs"
 	"github.com/moby/buildkit/solver/pb"
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
-	"github.com/openllb/hlb/diagnostic"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	"github.com/openllb/hlb/pkg/llbutil"
 	"github.com/openllb/hlb/solver"
 	"github.com/pkg/errors"
@@ -34,23 +34,23 @@ type (
 	dockerAPIKey      struct{}
 )
 
-func WithProgramCounter(ctx context.Context, node parser.Node) context.Context {
+func WithProgramCounter(ctx context.Context, node ast.Node) context.Context {
 	return context.WithValue(ctx, programCounterKey{}, node)
 }
 
-func ProgramCounter(ctx context.Context) parser.Node {
-	node, _ := ctx.Value(programCounterKey{}).(parser.Node)
+func ProgramCounter(ctx context.Context) ast.Node {
+	node, _ := ctx.Value(programCounterKey{}).(ast.Node)
 	return node
 }
 
-func WithReturnType(ctx context.Context, kind parser.Kind) context.Context {
+func WithReturnType(ctx context.Context, kind ast.Kind) context.Context {
 	return context.WithValue(ctx, returnTypeKey{}, kind)
 }
 
-func ReturnType(ctx context.Context) parser.Kind {
-	kind, ok := ctx.Value(returnTypeKey{}).(parser.Kind)
+func ReturnType(ctx context.Context) ast.Kind {
+	kind, ok := ctx.Value(returnTypeKey{}).(ast.Kind)
 	if !ok {
-		return parser.None
+		return ast.None
 	}
 	return kind
 }
@@ -63,24 +63,24 @@ func ModuleDir(ctx context.Context) string {
 	return filepath.Dir(node.Position().Filename)
 }
 
-func WithBinding(ctx context.Context, binding *parser.Binding) context.Context {
+func WithBinding(ctx context.Context, binding *ast.Binding) context.Context {
 	return context.WithValue(ctx, bindingKey{}, binding)
 }
 
-func Binding(ctx context.Context) *parser.Binding {
-	binding, ok := ctx.Value(bindingKey{}).(*parser.Binding)
+func Binding(ctx context.Context) *ast.Binding {
+	binding, ok := ctx.Value(bindingKey{}).(*ast.Binding)
 	if !ok {
-		return &parser.Binding{}
+		return &ast.Binding{}
 	}
 	return binding
 }
 
-func WithArg(ctx context.Context, n int, arg parser.Node) context.Context {
+func WithArg(ctx context.Context, n int, arg ast.Node) context.Context {
 	return context.WithValue(ctx, argKey{n}, arg)
 }
 
-func Arg(ctx context.Context, n int) parser.Node {
-	arg, _ := ctx.Value(argKey{n}).(parser.Node)
+func Arg(ctx context.Context, n int) ast.Node {
+	arg, _ := ctx.Value(argKey{n}).(ast.Node)
 	return arg
 }
 
@@ -121,7 +121,7 @@ func ImageResolver(ctx context.Context) llb.ImageMetaResolver {
 }
 
 type Frame struct {
-	parser.Node
+	ast.Node
 }
 
 func WithFrame(ctx context.Context, frame Frame) context.Context {
@@ -164,13 +164,13 @@ func WithCallbackErrgroup(ctx context.Context, g *errgroup.Group) solver.SolveOp
 
 func SourceMap(ctx context.Context) (opts []llb.ConstraintsOpt) {
 	var (
-		sources   = diagnostic.Sources(ctx)
+		files     = filebuffer.Buffers(ctx)
 		backtrace = Backtrace(ctx)
 	)
 
 	for i := len(backtrace) - 1; i >= 0; i-- {
 		node := backtrace[i].Node
-		fb := sources.Get(node.Position().Filename)
+		fb := files.Get(node.Position().Filename)
 		if fb == nil {
 			continue
 		}
diff --git a/codegen/debug.go b/codegen/debug.go
index 4cd763d..0595d53 100644
--- a/codegen/debug.go
+++ b/codegen/debug.go
@@ -22,6 +22,7 @@ import (
 	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/pkg/llbutil"
 	"github.com/openllb/hlb/solver"
 	"github.com/pkg/errors"
@@ -33,17 +34,17 @@ var (
 	ErrDebugExit = errors.Errorf("exiting debugger")
 )
 
-type Debugger func(ctx context.Context, scope *parser.Scope, node parser.Node, ret Register, opts Option) error
+type Debugger func(ctx context.Context, scope *ast.Scope, node ast.Node, ret Register, opts Option) error
 
 func NewNoopDebugger() Debugger {
-	return func(ctx context.Context, _ *parser.Scope, _ parser.Node, _ Register, _ Option) error {
+	return func(ctx context.Context, _ *ast.Scope, _ ast.Node, _ Register, _ Option) error {
 		return nil
 	}
 }
 
 type snapshot struct {
-	scope *parser.Scope
-	node  parser.Node
+	scope *ast.Scope
+	node  ast.Node
 	val   Value
 	opts  Option
 }
@@ -57,8 +58,8 @@ func (s snapshot) fs() (Filesystem, error) {
 
 func NewDebugger(c *client.Client, w io.Writer, inputSteerer *InputSteerer, promptReader *bufio.Reader) Debugger {
 	var (
-		fun               *parser.FuncDecl
-		next              *parser.FuncDecl
+		fun               *ast.FuncDecl
+		next              *ast.FuncDecl
 		history           []*snapshot
 		historyIndex      = -1
 		reverseStep       bool
@@ -69,7 +70,7 @@ func NewDebugger(c *client.Client, w io.Writer, inputSteerer *InputSteerer, prom
 
 	var previousCommand string
 
-	return func(ctx context.Context, scope *parser.Scope, node parser.Node, ret Register, opts Option) error {
+	return func(ctx context.Context, scope *ast.Scope, node ast.Node, ret Register, opts Option) error {
 		// Store a snapshot of the current debug step so we can backtrack.
 		historyIndex++
 		history = append(history, &snapshot{scope, node, ret.Value(), opts})
@@ -79,14 +80,14 @@ func NewDebugger(c *client.Client, w io.Writer, inputSteerer *InputSteerer, prom
 
 			// Keep track of whether we're in global scope or a lexical scope.
 			switch n := node.(type) {
-			case *parser.Module:
+			case *ast.Module:
 				staticBreakpoints = FindStaticBreakpoints(ctx, n)
 				breakpoints = staticBreakpoints
 
 				// Don't print source code on the first debug section.
 				showList = false
 			default:
-				fun = scope.Node.(*parser.FuncDecl)
+				fun = scope.Node.(*ast.FuncDecl)
 				if AtBreakpoint(node, fun, breakpoints, opts) {
 					cont = false
 				}
@@ -141,11 +142,11 @@ func NewDebugger(c *client.Client, w io.Writer, inputSteerer *InputSteerer, prom
 
 					if len(args) == 1 {
 						switch n := s.node.(type) {
-						case *parser.FuncDecl:
+						case *ast.FuncDecl:
 							bp = &Breakpoint{
 								Func: n,
 							}
-						case *parser.CallStmt:
+						case *ast.CallStmt:
 							if n.Name.Ident.Text == "breakpoint" {
 								fmt.Fprintf(w, "%s cannot break at breakpoint\n", parser.FormatPos(n.Pos))
 								continue
@@ -263,7 +264,7 @@ func NewDebugger(c *client.Client, w io.Writer, inputSteerer *InputSteerer, prom
 				case "funcs":
 					for _, obj := range s.scope.Defined() {
 						switch obj.Node.(type) {
-						case *parser.FuncDecl, *parser.BindClause:
+						case *ast.FuncDecl, *ast.BindClause:
 							fmt.Fprintln(w, obj.Ident.String())
 						}
 					}
@@ -399,7 +400,7 @@ func NewDebugger(c *client.Client, w io.Writer, inputSteerer *InputSteerer, prom
 	}
 }
 
-func PrintList(ctx context.Context, w io.Writer, node parser.Node) {
+func PrintList(ctx context.Context, w io.Writer, node ast.Node) {
 	err := node.WithError(nil, node.Spanf(diagnostic.Primary, ""))
 	for _, span := range diagnostic.Spans(err) {
 		fmt.Fprintln(w, span.Pretty(ctx, diagnostic.WithNumContext(3)))
@@ -407,15 +408,15 @@ func PrintList(ctx context.Context, w io.Writer, node parser.Node) {
 }
 
 type Breakpoint struct {
-	Func *parser.FuncDecl
-	Call *parser.CallStmt
+	Func *ast.FuncDecl
+	Call *ast.CallStmt
 }
 
-func FindStaticBreakpoints(ctx context.Context, mod *parser.Module) []*Breakpoint {
+func FindStaticBreakpoints(ctx context.Context, mod *ast.Module) []*Breakpoint {
 	var breakpoints []*Breakpoint
 
-	parser.Match(mod, parser.MatchOpts{},
-		func(fun *parser.FuncDecl, call *parser.CallStmt) {
+	ast.Match(mod, ast.MatchOpts{},
+		func(fun *ast.FuncDecl, call *ast.CallStmt) {
 			if fun.Kind() == "option::run" {
 				return
 			}
@@ -433,7 +434,7 @@ func FindStaticBreakpoints(ctx context.Context, mod *parser.Module) []*Breakpoin
 	return breakpoints
 }
 
-func AtBreakpoint(node parser.Node, fun *parser.FuncDecl, breakpoints []*Breakpoint, opts Option) bool {
+func AtBreakpoint(node ast.Node, fun *ast.FuncDecl, breakpoints []*Breakpoint, opts Option) bool {
 	for _, opt := range opts {
 		if _, hasBreakpointCommand := opt.(breakpointCommand); hasBreakpointCommand {
 			return true
diff --git a/codegen/resolver.go b/codegen/resolver.go
index 295b09d..6214ea1 100644
--- a/codegen/resolver.go
+++ b/codegen/resolver.go
@@ -9,7 +9,7 @@ import (
 	"github.com/moby/buildkit/client/llb"
 	gateway "github.com/moby/buildkit/frontend/gateway/client"
 	digest "github.com/opencontainers/go-digest"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/pkg/llbutil"
 	"github.com/openllb/hlb/solver"
 	"golang.org/x/sync/errgroup"
@@ -24,7 +24,7 @@ const (
 // Resolver resolves imports into a reader ready for parsing and checking.
 type Resolver interface {
 	// Resolve returns a reader for the HLB module and its compiled LLB.
-	Resolve(ctx context.Context, id *parser.ImportDecl, fs Filesystem) (parser.Directory, error)
+	Resolve(ctx context.Context, id *ast.ImportDecl, fs Filesystem) (ast.Directory, error)
 }
 
 func NewCachedImageResolver(cln *client.Client) llb.ImageMetaResolver {
diff --git a/codegen/value.go b/codegen/value.go
index 31b8403..0a088a4 100644
--- a/codegen/value.go
+++ b/codegen/value.go
@@ -14,7 +14,7 @@ import (
 	"github.com/moby/buildkit/client/llb"
 	digest "github.com/opencontainers/go-digest"
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/pkg/llbutil"
 	"github.com/openllb/hlb/solver"
 	"github.com/xlab/treeprint"
@@ -100,7 +100,7 @@ func (r *register) Value() Value {
 }
 
 type Value interface {
-	Kind() parser.Kind
+	Kind() ast.Kind
 	Filesystem() (Filesystem, error)
 	String() (string, error)
 	Int() (int, error)
@@ -147,8 +147,8 @@ func validateState(st llb.State) error {
 
 type nilValue struct{}
 
-func (v *nilValue) Kind() parser.Kind {
-	return parser.None
+func (v *nilValue) Kind() ast.Kind {
+	return ast.None
 }
 
 func (v *nilValue) Filesystem() (Filesystem, error) {
@@ -179,8 +179,8 @@ type errorValue struct {
 	err error
 }
 
-func (v *errorValue) Kind() parser.Kind {
-	return parser.None
+func (v *errorValue) Kind() ast.Kind {
+	return ast.None
 }
 
 func (v *errorValue) Filesystem() (Filesystem, error) {
@@ -217,8 +217,8 @@ func ZeroValue(ctx context.Context) Value {
 	}
 }
 
-func (v *zeroValue) Kind() parser.Kind {
-	return parser.None
+func (v *zeroValue) Kind() ast.Kind {
+	return ast.None
 }
 
 func (v *zeroValue) Filesystem() (Filesystem, error) {
@@ -278,8 +278,8 @@ type fsValue struct {
 	fs Filesystem
 }
 
-func (v *fsValue) Kind() parser.Kind {
-	return parser.Filesystem
+func (v *fsValue) Kind() ast.Kind {
+	return ast.Filesystem
 }
 
 func (v *fsValue) Filesystem() (Filesystem, error) {
@@ -321,8 +321,8 @@ type stringValue struct {
 	str string
 }
 
-func (v *stringValue) Kind() parser.Kind {
-	return parser.String
+func (v *stringValue) Kind() ast.Kind {
+	return ast.String
 }
 
 func (v *stringValue) String() (string, error) {
@@ -346,8 +346,8 @@ type intValue struct {
 	i int
 }
 
-func (v *intValue) Kind() parser.Kind {
-	return parser.Int
+func (v *intValue) Kind() ast.Kind {
+	return ast.Int
 }
 
 func (v *intValue) Int() (int, error) {
@@ -367,8 +367,8 @@ type optValue struct {
 	opt Option
 }
 
-func (v *optValue) Kind() parser.Kind {
-	return parser.Option
+func (v *optValue) Kind() ast.Kind {
+	return ast.Option
 }
 
 func (v *optValue) Option() (Option, error) {
@@ -380,8 +380,8 @@ type reqValue struct {
 	req solver.Request
 }
 
-func (v *reqValue) Kind() parser.Kind {
-	return parser.Pipeline
+func (v *reqValue) Kind() ast.Kind {
+	return ast.Pipeline
 }
 
 func (v *reqValue) Request() (solver.Request, error) {
diff --git a/diagnostic/context.go b/diagnostic/context.go
index 407ce9f..8070114 100644
--- a/diagnostic/context.go
+++ b/diagnostic/context.go
@@ -4,25 +4,9 @@ import (
 	"context"
 
 	"github.com/logrusorgru/aurora"
-	"github.com/openllb/hlb/pkg/filebuffer"
 )
 
-type (
-	sourcesKey struct{}
-	colorKey   struct{}
-)
-
-func WithSources(ctx context.Context, sources *filebuffer.Sources) context.Context {
-	return context.WithValue(ctx, sourcesKey{}, sources)
-}
-
-func Sources(ctx context.Context) *filebuffer.Sources {
-	sources, ok := ctx.Value(sourcesKey{}).(*filebuffer.Sources)
-	if !ok {
-		return filebuffer.NewSources()
-	}
-	return sources
-}
+type colorKey struct{}
 
 func WithColor(ctx context.Context, color aurora.Aurora) context.Context {
 	return context.WithValue(ctx, colorKey{}, color)
diff --git a/diagnostic/error.go b/diagnostic/error.go
index da5a0c7..e3d1046 100644
--- a/diagnostic/error.go
+++ b/diagnostic/error.go
@@ -6,6 +6,7 @@ import (
 	"strings"
 
 	"github.com/moby/buildkit/solver/errdefs"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	perrors "github.com/pkg/errors"
 )
 
@@ -46,7 +47,7 @@ func Spans(err error) (spans []*SpanError) {
 func Backtrace(ctx context.Context, err error) (spans []*SpanError) {
 	srcs := errdefs.Sources(err)
 	for i, src := range srcs {
-		fb := Sources(ctx).Get(src.Info.Filename)
+		fb := filebuffer.Buffers(ctx).Get(src.Info.Filename)
 		if fb != nil {
 			var msg string
 			if i == len(srcs)-1 {
diff --git a/diagnostic/span.go b/diagnostic/span.go
index 300b490..28acaf4 100644
--- a/diagnostic/span.go
+++ b/diagnostic/span.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/alecthomas/participle/lexer"
 	"github.com/logrusorgru/aurora"
+	"github.com/openllb/hlb/pkg/filebuffer"
 )
 
 type Type int
@@ -81,7 +82,7 @@ func (se *SpanError) Pretty(ctx context.Context, opts ...PrettyOption) string {
 	var (
 		info    PrettyInfo
 		reports []string
-		sources = Sources(ctx)
+		files   = filebuffer.Buffers(ctx)
 		color   = Color(ctx)
 	)
 	for _, opt := range opts {
@@ -92,7 +93,7 @@ func (se *SpanError) Pretty(ctx context.Context, opts ...PrettyOption) string {
 
 	filenames, spansByFilename := se.groupAnnnotations()
 	for _, filename := range filenames {
-		fb := sources.Get(filename)
+		fb := files.Get(filename)
 
 		// Sort spans in the same module by line number.
 		spans := spansByFilename[filename]
@@ -258,7 +259,7 @@ func (se *SpanError) Pretty(ctx context.Context, opts ...PrettyOption) string {
 func (se *SpanError) maxLn(ctx context.Context, numContext int) int {
 	maxLn := 0
 	for _, span := range se.Spans {
-		fb := Sources(ctx).Get(span.Start.Filename)
+		fb := filebuffer.Buffers(ctx).Get(span.Start.Filename)
 		line := span.Start.Line + numContext
 		if line > fb.Len() {
 			line = fb.Len()
diff --git a/errdefs/errors.go b/errdefs/errors.go
index d2c0454..053b148 100644
--- a/errdefs/errors.go
+++ b/errdefs/errors.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"github.com/openllb/hlb/diagnostic"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/pkg/errors"
 )
 
@@ -35,7 +35,7 @@ func WithAbort(err error, numErrs int) *ErrAbort {
 }
 
 type ErrModule struct {
-	Module *parser.Module
+	Module *ast.Module
 	Err    error
 }
 
@@ -47,28 +47,28 @@ func (e *ErrModule) Error() string {
 	return e.Err.Error()
 }
 
-func WithDeprecated(mod *parser.Module, node parser.Node, format string, a ...interface{}) error {
+func WithDeprecated(mod *ast.Module, node ast.Node, format string, a ...interface{}) error {
 	return node.WithError(
 		&ErrModule{mod, fmt.Errorf(format, a...)},
 		node.Spanf(diagnostic.Primary, format, a...),
 	)
 }
 
-func WithInternalErrorf(node parser.Node, format string, a ...interface{}) error {
+func WithInternalErrorf(node ast.Node, format string, a ...interface{}) error {
 	return node.WithError(
 		fmt.Errorf(format, a...),
 		node.Spanf(diagnostic.Primary, format, a...),
 	)
 }
 
-func WithInvalidCompileTarget(ident parser.Node) error {
+func WithInvalidCompileTarget(ident ast.Node) error {
 	return ident.WithError(
 		fmt.Errorf("invalid compile target %s", ident),
 		ident.Spanf(diagnostic.Primary, "cannot compile target"),
 	)
 }
 
-func WithWrongType(expr parser.Node, expected []parser.Kind, actual parser.Kind, opts ...diagnostic.Option) error {
+func WithWrongType(expr ast.Node, expected []ast.Kind, actual ast.Kind, opts ...diagnostic.Option) error {
 	opts = append(opts, expr.Spanf(
 		diagnostic.Primary,
 		"cannot use %s as %s", actual, OneOfKinds(expected),
@@ -79,7 +79,7 @@ func WithWrongType(expr parser.Node, expected []parser.Kind, actual parser.Kind,
 	)
 }
 
-func WithCallImport(ident parser.Node, decl parser.Node) error {
+func WithCallImport(ident ast.Node, decl ast.Node) error {
 	return ident.WithError(
 		fmt.Errorf("cannot call an imported module"),
 		ident.Spanf(diagnostic.Primary, "cannot use import directly"),
@@ -87,14 +87,14 @@ func WithCallImport(ident parser.Node, decl parser.Node) error {
 	)
 }
 
-func WithImportPathNotExist(err error, expr parser.Node, filename string) error {
+func WithImportPathNotExist(err error, expr ast.Node, filename string) error {
 	return expr.WithError(
 		err,
 		expr.Spanf(diagnostic.Primary, "no such file %q", filename),
 	)
 }
 
-func WithUndefinedIdent(ident parser.Node, suggested *parser.Object, opts ...diagnostic.Option) error {
+func WithUndefinedIdent(ident ast.Node, suggested *ast.Object, opts ...diagnostic.Option) error {
 	opts = append(opts, ident.Spanf(diagnostic.Primary, "undefined or not in scope"))
 	if suggested != nil {
 		opts = append(opts, suggested.Ident.Spanf(diagnostic.Secondary, "did you mean `%s`?", suggested.Ident))
@@ -105,7 +105,7 @@ func WithUndefinedIdent(ident parser.Node, suggested *parser.Object, opts ...dia
 	)
 }
 
-func WithNotImport(ie *parser.IdentExpr, decl parser.Node) error {
+func WithNotImport(ie *ast.IdentExpr, decl ast.Node) error {
 	return ie.Reference.WithError(
 		fmt.Errorf("cannot use dot notation with non-import"),
 		ie.Reference.Spanf(diagnostic.Primary, "`%s` is not an import", ie.Ident),
@@ -113,7 +113,7 @@ func WithNotImport(ie *parser.IdentExpr, decl parser.Node) error {
 	)
 }
 
-func WithCallUnexported(ref parser.Node, opts ...diagnostic.Option) error {
+func WithCallUnexported(ref ast.Node, opts ...diagnostic.Option) error {
 	opts = append(opts, ref.Spanf(
 		diagnostic.Primary,
 		"cannot call unexported function",
@@ -124,7 +124,7 @@ func WithCallUnexported(ref parser.Node, opts ...diagnostic.Option) error {
 	)
 }
 
-func WithNumArgs(callee parser.Node, expected, actual int, opts ...diagnostic.Option) error {
+func WithNumArgs(callee ast.Node, expected, actual int, opts ...diagnostic.Option) error {
 	opts = append(opts, callee.Spanf(
 		diagnostic.Primary,
 		"expected %d args, found %d", expected, actual,
@@ -135,7 +135,7 @@ func WithNumArgs(callee parser.Node, expected, actual int, opts ...diagnostic.Op
 	)
 }
 
-func WithDuplicates(dups []parser.Node) error {
+func WithDuplicates(dups []ast.Node) error {
 	if len(dups) == 0 {
 		return nil
 	}
@@ -157,14 +157,14 @@ func WithDuplicates(dups []parser.Node) error {
 	)
 }
 
-func WithNoBindTarget(as parser.Node) error {
+func WithNoBindTarget(as ast.Node) error {
 	return as.WithError(
 		fmt.Errorf("cannot bind, has no target"),
 		as.Spanf(diagnostic.Primary, "no bind target"),
 	)
 }
 
-func WithNoBindClosure(as, option parser.Node) error {
+func WithNoBindClosure(as, option ast.Node) error {
 	return as.WithError(
 		fmt.Errorf("cannot bind, no closure in option blocks"),
 		as.Spanf(diagnostic.Primary, "no closure for binding"),
@@ -172,7 +172,7 @@ func WithNoBindClosure(as, option parser.Node) error {
 	)
 }
 
-func WithNoBindEffects(callee, as parser.Node, opts ...diagnostic.Option) error {
+func WithNoBindEffects(callee, as ast.Node, opts ...diagnostic.Option) error {
 	opts = append(opts, as.Spanf(
 		diagnostic.Primary,
 		"`%s` has no effects to bind", callee,
@@ -183,21 +183,21 @@ func WithNoBindEffects(callee, as parser.Node, opts ...diagnostic.Option) error
 	)
 }
 
-func WithUndefinedBindTarget(callee, target parser.Node) error {
+func WithUndefinedBindTarget(callee, target ast.Node) error {
 	return target.WithError(
 		fmt.Errorf("cannot bind, `%s` is an undefined effect of `%s`", target, callee),
 		target.Spanf(diagnostic.Primary, "undefined bind"),
 	)
 }
 
-func WithInvalidImageRef(err error, arg parser.Node, ref string) error {
+func WithInvalidImageRef(err error, arg ast.Node, ref string) error {
 	return arg.WithError(
 		errors.Wrapf(err, "failed to parse `%s`", ref),
 		arg.Spanf(diagnostic.Primary, "failed to parse `%s`\n%s", ref, err),
 	)
 }
 
-func WithInvalidNetworkMode(arg parser.Node, mode string, modes []string) error {
+func WithInvalidNetworkMode(arg ast.Node, mode string, modes []string) error {
 	suggestion := diagnostic.Suggestion(mode, modes)
 	if suggestion != "" {
 		suggestion = fmt.Sprintf("\ndid you mean `%s`?", suggestion)
@@ -208,7 +208,7 @@ func WithInvalidNetworkMode(arg parser.Node, mode string, modes []string) error
 	)
 }
 
-func WithInvalidSecurityMode(arg parser.Node, mode string, modes []string) error {
+func WithInvalidSecurityMode(arg ast.Node, mode string, modes []string) error {
 	suggestion := diagnostic.Suggestion(mode, modes)
 	if suggestion != "" {
 		suggestion = fmt.Sprintf("\ndid you mean `%s`?", suggestion)
@@ -219,7 +219,7 @@ func WithInvalidSecurityMode(arg parser.Node, mode string, modes []string) error
 	)
 }
 
-func WithInvalidSharingMode(arg parser.Node, mode string, modes []string) error {
+func WithInvalidSharingMode(arg ast.Node, mode string, modes []string) error {
 	suggestion := diagnostic.Suggestion(mode, modes)
 	if suggestion != "" {
 		suggestion = fmt.Sprintf("\ndid you mean `%s`?", suggestion)
@@ -230,7 +230,7 @@ func WithInvalidSharingMode(arg parser.Node, mode string, modes []string) error
 	)
 }
 
-func WithBindCacheMount(as, cache parser.Node) error {
+func WithBindCacheMount(as, cache ast.Node) error {
 	return as.WithError(
 		fmt.Errorf("cannot bind a cache mount"),
 		as.Spanf(diagnostic.Primary, "cannot bind a cache mount"),
@@ -238,29 +238,29 @@ func WithBindCacheMount(as, cache parser.Node) error {
 	)
 }
 
-func WithDockerEngineUnsupported(decl parser.Node) error {
+func WithDockerEngineUnsupported(decl ast.Node) error {
 	return decl.WithError(
 		fmt.Errorf("not supported by buildkit embedded in docker engine, use standalone buildkit"),
 		decl.Spanf(diagnostic.Primary, "not supported by docker engine"),
 	)
 }
 
-func OneOfKinds(kinds []parser.Kind) string {
+func OneOfKinds(kinds []ast.Kind) string {
 	if len(kinds) == 1 {
 		return fmt.Sprintf("type %s", kinds[0])
 	}
 	return fmt.Sprintf("one of types %s", kinds)
 }
 
-func Defined(node parser.Node) diagnostic.Option {
+func Defined(node ast.Node) diagnostic.Option {
 	return node.Spanf(diagnostic.Secondary, "defined here")
 }
 
-func Imported(node parser.Node) diagnostic.Option {
+func Imported(node ast.Node) diagnostic.Option {
 	return node.Spanf(diagnostic.Secondary, "imported here")
 }
 
-func DefinedMaybeImported(scope *parser.Scope, ie *parser.IdentExpr, decl parser.Node) []diagnostic.Option {
+func DefinedMaybeImported(scope *ast.Scope, ie *ast.IdentExpr, decl ast.Node) []diagnostic.Option {
 	opts := []diagnostic.Option{Defined(decl)}
 	if ie.Reference != nil {
 		obj := scope.Lookup(ie.Ident.Text)
diff --git a/hlb.go b/hlb.go
index 5e5aee1..5a219aa 100644
--- a/hlb.go
+++ b/hlb.go
@@ -12,11 +12,11 @@ import (
 	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/linter"
 	"github.com/openllb/hlb/module"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/solver"
 )
 
-func Compile(ctx context.Context, cln *client.Client, mod *parser.Module, targets []codegen.Target, opts ...codegen.CodeGenOption) (solver.Request, error) {
+func Compile(ctx context.Context, cln *client.Client, mod *ast.Module, targets []codegen.Target, opts ...codegen.CodeGenOption) (solver.Request, error) {
 	err := checker.SemanticPass(mod)
 	if err != nil {
 		return nil, err
diff --git a/langserver/server.go b/langserver/server.go
index 3d14983..df06a09 100644
--- a/langserver/server.go
+++ b/langserver/server.go
@@ -21,6 +21,7 @@ import (
 	"github.com/openllb/hlb/linter"
 	"github.com/openllb/hlb/module"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	lsp "github.com/sourcegraph/go-lsp"
 )
 
@@ -191,12 +192,12 @@ func (ls *LangServer) publishSemanticHighlighting(ctx context.Context, td TextDo
 	return ls.server.Notify(ctx, "textDocument/semanticHighlighting", params)
 }
 
-func highlightModule(lines map[int]lsp.SemanticHighlightingTokens, mod *parser.Module) {
-	parser.Match(mod, parser.MatchOpts{},
-		func(comment *parser.Comment) {
+func highlightModule(lines map[int]lsp.SemanticHighlightingTokens, mod *ast.Module) {
+	ast.Match(mod, ast.MatchOpts{},
+		func(comment *ast.Comment) {
 			highlightNode(lines, comment, Comment)
 		},
-		func(id *parser.ImportDecl) {
+		func(id *ast.ImportDecl) {
 			if id.Import != nil {
 				highlightNode(lines, id.Import, Keyword)
 			}
@@ -221,7 +222,7 @@ func highlightModule(lines map[int]lsp.SemanticHighlightingTokens, mod *parser.M
 				highlightExpr(lines, id.Expr)
 			}
 		},
-		func(ed *parser.ExportDecl) {
+		func(ed *ast.ExportDecl) {
 			if ed.Export != nil {
 				highlightNode(lines, ed.Export, Keyword)
 			}
@@ -229,7 +230,7 @@ func highlightModule(lines map[int]lsp.SemanticHighlightingTokens, mod *parser.M
 				highlightNode(lines, ed.Name, Variable)
 			}
 		},
-		func(fun *parser.FuncDecl) {
+		func(fun *ast.FuncDecl) {
 			if fun.Type != nil {
 				highlightNode(lines, fun.Type, Type)
 			}
@@ -271,9 +272,9 @@ func highlightModule(lines map[int]lsp.SemanticHighlightingTokens, mod *parser.M
 	)
 }
 
-func highlightBlock(lines map[int]lsp.SemanticHighlightingTokens, block *parser.BlockStmt) {
-	parser.Match(block, parser.MatchOpts{},
-		func(call *parser.CallStmt) {
+func highlightBlock(lines map[int]lsp.SemanticHighlightingTokens, block *ast.BlockStmt) {
+	ast.Match(block, ast.MatchOpts{},
+		func(call *ast.CallStmt) {
 			if call.Name != nil {
 				highlightIdentExpr(lines, call.Name)
 			}
@@ -308,7 +309,7 @@ func highlightBlock(lines map[int]lsp.SemanticHighlightingTokens, block *parser.
 			}
 
 		},
-		func(expr *parser.ExprStmt) {
+		func(expr *ast.ExprStmt) {
 			if expr.Expr != nil {
 				highlightExpr(lines, expr.Expr)
 			}
@@ -316,7 +317,7 @@ func highlightBlock(lines map[int]lsp.SemanticHighlightingTokens, block *parser.
 	)
 }
 
-func highlightExpr(lines map[int]lsp.SemanticHighlightingTokens, expr *parser.Expr) {
+func highlightExpr(lines map[int]lsp.SemanticHighlightingTokens, expr *ast.Expr) {
 	switch {
 	case expr.FuncLit != nil:
 		if expr.FuncLit.Type != nil {
@@ -366,7 +367,7 @@ func highlightExpr(lines map[int]lsp.SemanticHighlightingTokens, expr *parser.Ex
 	}
 }
 
-func highlightStringFragment(lines map[int]lsp.SemanticHighlightingTokens, f *parser.StringFragment) {
+func highlightStringFragment(lines map[int]lsp.SemanticHighlightingTokens, f *ast.StringFragment) {
 	switch {
 	case f.Escaped != nil:
 		highlightNode(lines, f, Comment)
@@ -377,7 +378,7 @@ func highlightStringFragment(lines map[int]lsp.SemanticHighlightingTokens, f *pa
 	}
 }
 
-func highlightHeredocFragment(lines map[int]lsp.SemanticHighlightingTokens, f *parser.HeredocFragment) {
+func highlightHeredocFragment(lines map[int]lsp.SemanticHighlightingTokens, f *ast.HeredocFragment) {
 	switch {
 	case f.Escaped != nil:
 		highlightNode(lines, f, Comment)
@@ -386,7 +387,7 @@ func highlightHeredocFragment(lines map[int]lsp.SemanticHighlightingTokens, f *p
 	}
 }
 
-func highlightIdentExpr(lines map[int]lsp.SemanticHighlightingTokens, ie *parser.IdentExpr) {
+func highlightIdentExpr(lines map[int]lsp.SemanticHighlightingTokens, ie *ast.IdentExpr) {
 	if ie.Reference != nil {
 		if ie.Ident != nil {
 			highlightNode(lines, ie.Ident, Module)
@@ -397,7 +398,7 @@ func highlightIdentExpr(lines map[int]lsp.SemanticHighlightingTokens, ie *parser
 	}
 }
 
-func highlightNode(lines map[int]lsp.SemanticHighlightingTokens, node parser.Node, s Scope) {
+func highlightNode(lines map[int]lsp.SemanticHighlightingTokens, node ast.Node, s Scope) {
 	line := node.Position().Line - 1
 	lines[line] = append(lines[line], lsp.SemanticHighlightingToken{
 		Character: uint32(node.Position().Column - 1),
@@ -525,16 +526,16 @@ func (ls *LangServer) textDocumentDefinitionHandler(ctx context.Context, params
 		pos = params.Position
 	)
 
-	parser.Match(td.Module,
-		parser.MatchOpts{
-			Filter: func(node parser.Node) bool {
+	ast.Match(td.Module,
+		ast.MatchOpts{
+			Filter: func(node ast.Node) bool {
 				return isPositionWithinNode(pos, node)
 			},
 		},
-		func(_ *parser.ExportDecl, ident *parser.Ident) {
+		func(_ *ast.ExportDecl, ident *ast.Ident) {
 			loc = newLocationFromIdent(td.Module.Scope, uri, ident.Text)
 		},
-		func(block *parser.BlockStmt, ie *parser.IdentExpr) {
+		func(block *ast.BlockStmt, ie *ast.IdentExpr) {
 			if isPositionWithinNode(pos, ie.Ident) {
 				loc = newLocationFromIdent(block.Scope, uri, ie.Ident.Text)
 				return
@@ -547,7 +548,7 @@ func (ls *LangServer) textDocumentDefinitionHandler(ctx context.Context, params
 				return
 			}
 
-			id, ok := obj.Node.(*parser.ImportDecl)
+			id, ok := obj.Node.(*ast.ImportDecl)
 			if !ok {
 				return
 			}
@@ -600,7 +601,7 @@ func (ls *LangServer) textDocumentDefinitionHandler(ctx context.Context, params
 	return locs, nil
 }
 
-func newLocationFromIdent(scope *parser.Scope, uri lsp.DocumentURI, name string) *lsp.Location {
+func newLocationFromIdent(scope *ast.Scope, uri lsp.DocumentURI, name string) *lsp.Location {
 	obj := scope.Lookup(name)
 	if obj == nil {
 		return nil
@@ -608,9 +609,9 @@ func newLocationFromIdent(scope *parser.Scope, uri lsp.DocumentURI, name string)
 
 	var loc *lsp.Location
 	switch n := obj.Node.(type) {
-	case *parser.FuncDecl:
+	case *ast.FuncDecl:
 		loc = newLocationFromNode(uri, n.Name)
-	case *parser.BindClause:
+	case *ast.BindClause:
 		if n.Ident != nil {
 			loc = newLocationFromNode(uri, n.Ident)
 		}
@@ -625,9 +626,9 @@ func newLocationFromIdent(scope *parser.Scope, uri lsp.DocumentURI, name string)
 				loc = newLocationFromNode(uri, n)
 			}
 		}
-	case *parser.ImportDecl:
+	case *ast.ImportDecl:
 		loc = newLocationFromNode(uri, n.Name)
-	case *parser.Field:
+	case *ast.Field:
 		loc = newLocationFromNode(uri, n.Name)
 	default:
 		log.Printf("%s unknown decl kind", parser.FormatPos(n.Position()))
@@ -650,14 +651,14 @@ func (ls *LangServer) textDocumentHoverHandler(ctx context.Context, params lsp.T
 
 	var h lsp.Hover
 
-	parser.Match(td.Module,
-		parser.MatchOpts{
+	ast.Match(td.Module,
+		ast.MatchOpts{
 			AllowDuplicates: true,
-			Filter: func(node parser.Node) bool {
+			Filter: func(node ast.Node) bool {
 				return isPositionWithinNode(pos, node)
 			},
 		},
-		func(block *parser.BlockStmt, ident *parser.Ident) {
+		func(block *ast.BlockStmt, ident *ast.Ident) {
 			lookupByKind, ok := builtin.Lookup.ByKind[block.Kind()]
 			if !ok {
 				return
@@ -705,7 +706,7 @@ func (ls *LangServer) textDocumentCompletionHandler(ctx context.Context, params
 	return nil, nil
 }
 
-func isPositionWithinNode(pos lsp.Position, node parser.Node) bool {
+func isPositionWithinNode(pos lsp.Position, node ast.Node) bool {
 	if (pos.Line < node.Position().Line-1 || pos.Line > node.End().Line-1) ||
 		(pos.Line == node.Position().Line-1 && pos.Character < node.Position().Column-1) ||
 		(pos.Line == node.End().Line-1 && pos.Character >= node.End().Column-1) {
@@ -715,14 +716,14 @@ func isPositionWithinNode(pos lsp.Position, node parser.Node) bool {
 	return true
 }
 
-func newLocationFromNode(uri lsp.DocumentURI, node parser.Node) *lsp.Location {
+func newLocationFromNode(uri lsp.DocumentURI, node ast.Node) *lsp.Location {
 	return &lsp.Location{
 		URI:   uri,
 		Range: newRangeFromNode(node),
 	}
 }
 
-func newRangeFromNode(node parser.Node) lsp.Range {
+func newRangeFromNode(node ast.Node) lsp.Range {
 	return lsp.Range{
 		Start: lsp.Position{Line: node.Position().Line - 1, Character: node.Position().Column - 1},
 		End:   lsp.Position{Line: node.End().Line - 1, Character: node.End().Column - 1},
@@ -731,11 +732,11 @@ func newRangeFromNode(node parser.Node) lsp.Range {
 
 type TextDocument struct {
 	Identifier lsp.VersionedTextDocumentIdentifier
-	Module     *parser.Module
+	Module     *ast.Module
 	Err        error
 }
 
-func NewTextDocument(ctx context.Context, uri lsp.DocumentURI, r io.Reader, dir parser.Directory) TextDocument {
+func NewTextDocument(ctx context.Context, uri lsp.DocumentURI, r io.Reader, dir ast.Directory) TextDocument {
 	td := TextDocument{
 		Identifier: lsp.VersionedTextDocumentIdentifier{
 			TextDocumentIdentifier: lsp.TextDocumentIdentifier{
diff --git a/linter/linter.go b/linter/linter.go
index 05d8c90..74f375d 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/errdefs"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 )
 
 type Linter struct {
@@ -14,7 +14,7 @@ type Linter struct {
 
 type LintOption func(*Linter)
 
-func Lint(ctx context.Context, mod *parser.Module, opts ...LintOption) error {
+func Lint(ctx context.Context, mod *ast.Module, opts ...LintOption) error {
 	l := Linter{}
 	for _, opt := range opts {
 		opt(&l)
@@ -26,32 +26,32 @@ func Lint(ctx context.Context, mod *parser.Module, opts ...LintOption) error {
 	return nil
 }
 
-func (l *Linter) Lint(ctx context.Context, mod *parser.Module) {
-	parser.Match(mod, parser.MatchOpts{},
-		func(id *parser.ImportDecl) {
+func (l *Linter) Lint(ctx context.Context, mod *ast.Module) {
+	ast.Match(mod, ast.MatchOpts{},
+		func(id *ast.ImportDecl) {
 			if id.DeprecatedPath != nil {
 				l.errs = append(l.errs, errdefs.WithDeprecated(
 					mod, id.DeprecatedPath,
 					`import path without keyword "from" is deprecated`,
 				))
-				id.From = &parser.From{Text: "from"}
-				id.Expr = &parser.Expr{
-					BasicLit: &parser.BasicLit{
+				id.From = &ast.From{Text: "from"}
+				id.Expr = &ast.Expr{
+					BasicLit: &ast.BasicLit{
 						Str: id.DeprecatedPath,
 					},
 				}
 			}
 		},
-		func(t *parser.Type) {
+		func(t *ast.Type) {
 			if string(t.Kind) == "group" {
 				l.errs = append(l.errs, errdefs.WithDeprecated(
 					mod, t,
 					"type `group` is deprecated, use `pipeline` instead",
 				))
-				t.Kind = parser.Pipeline
+				t.Kind = ast.Pipeline
 			}
 		},
-		func(call *parser.CallStmt) {
+		func(call *ast.CallStmt) {
 			if call.Name != nil && call.Name.Ident.Text == "parallel" {
 				l.errs = append(l.errs, errdefs.WithDeprecated(
 					mod, call.Name,
diff --git a/module/resolve.go b/module/resolve.go
index c69216d..d326733 100644
--- a/module/resolve.go
+++ b/module/resolve.go
@@ -16,6 +16,7 @@ import (
 	"github.com/openllb/hlb/checker"
 	"github.com/openllb/hlb/codegen"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/pkg/llbutil"
 	"github.com/openllb/hlb/solver"
 	"golang.org/x/sync/errgroup"
@@ -70,7 +71,7 @@ type vendorResolver struct {
 	modulePath string
 }
 
-func (r *vendorResolver) Resolve(ctx context.Context, id *parser.ImportDecl, fs codegen.Filesystem) (parser.Directory, error) {
+func (r *vendorResolver) Resolve(ctx context.Context, id *ast.ImportDecl, fs codegen.Filesystem) (ast.Directory, error) {
 	dir, err := resolveLocal(ctx, r.modulePath, fs)
 	if err != nil {
 		return dir, err
@@ -87,7 +88,7 @@ func (r *vendorResolver) Resolve(ctx context.Context, id *parser.ImportDecl, fs
 	return dir, fmt.Errorf("missing module %q from vendor, run `hlb mod vendor --target %s %s` to vendor module", id.Name, id.Name, id.Pos.Filename)
 }
 
-func resolveLocal(ctx context.Context, modulePath string, fs codegen.Filesystem) (parser.Directory, error) {
+func resolveLocal(ctx context.Context, modulePath string, fs codegen.Filesystem) (ast.Directory, error) {
 	dgst, err := fs.Digest(ctx)
 	if err != nil {
 		return nil, err
@@ -102,7 +103,7 @@ type remoteResolver struct {
 	modulePath string
 }
 
-func (r *remoteResolver) Resolve(ctx context.Context, id *parser.ImportDecl, fs codegen.Filesystem) (parser.Directory, error) {
+func (r *remoteResolver) Resolve(ctx context.Context, id *ast.ImportDecl, fs codegen.Filesystem) (ast.Directory, error) {
 	dgst, err := fs.Digest(ctx)
 	if err != nil {
 		return nil, err
@@ -227,7 +228,7 @@ type tidyResolver struct {
 	remote *remoteResolver
 }
 
-func (r *tidyResolver) Resolve(ctx context.Context, id *parser.ImportDecl, fs codegen.Filesystem) (parser.Directory, error) {
+func (r *tidyResolver) Resolve(ctx context.Context, id *ast.ImportDecl, fs codegen.Filesystem) (ast.Directory, error) {
 	dir, err := resolveLocal(ctx, r.remote.modulePath, fs)
 	if err != nil {
 		return dir, err
@@ -252,7 +253,7 @@ type targetResolver struct {
 	remote   *remoteResolver
 }
 
-func (r *targetResolver) Resolve(ctx context.Context, id *parser.ImportDecl, fs codegen.Filesystem) (parser.Directory, error) {
+func (r *targetResolver) Resolve(ctx context.Context, id *ast.ImportDecl, fs codegen.Filesystem) (ast.Directory, error) {
 	if id.Pos.Filename == r.filename {
 		matchTarget := true
 		if len(r.targets) > 0 {
@@ -298,9 +299,9 @@ func VendorPath(root string, dgst digest.Digest) string {
 type Visitor func(info VisitInfo) error
 
 type VisitInfo struct {
-	Parent     *parser.Module
-	Import     *parser.Module
-	ImportDecl *parser.ImportDecl
+	Parent     *ast.Module
+	Import     *ast.Module
+	ImportDecl *ast.ImportDecl
 	Filename   string
 	Digest     digest.Digest
 }
@@ -311,7 +312,7 @@ type resolveGraphInfo struct {
 }
 
 // ResolveGraph traverses the import graph of a given module.
-func ResolveGraph(ctx context.Context, cln *client.Client, resolver codegen.Resolver, mod *parser.Module, visitor Visitor) error {
+func ResolveGraph(ctx context.Context, cln *client.Client, resolver codegen.Resolver, mod *ast.Module, visitor Visitor) error {
 	cg, err := codegen.New(cln, resolver)
 	if err != nil {
 		return err
@@ -324,11 +325,11 @@ func ResolveGraph(ctx context.Context, cln *client.Client, resolver codegen.Reso
 	return resolveGraph(ctx, info, mod)
 }
 
-func resolveGraph(ctx context.Context, info *resolveGraphInfo, mod *parser.Module) error {
+func resolveGraph(ctx context.Context, info *resolveGraphInfo, mod *ast.Module) error {
 	g, ctx := errgroup.WithContext(ctx)
 
-	parser.Match(mod, parser.MatchOpts{},
-		func(id *parser.ImportDecl) {
+	ast.Match(mod, ast.MatchOpts{},
+		func(id *ast.ImportDecl) {
 			obj := mod.Scope.Lookup(id.Name.Text)
 			if obj == nil {
 				return
diff --git a/module/tree.go b/module/tree.go
index 2ed5373..d05e429 100644
--- a/module/tree.go
+++ b/module/tree.go
@@ -7,14 +7,14 @@ import (
 	"sync"
 
 	"github.com/moby/buildkit/client"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/xlab/treeprint"
 )
 
 // NewTree resolves the import graph and returns a treeprint.Tree that can be
 // printed to display a visualization of the imports. Imports that transitively
 // import the same module will be duplicated in the tree.
-func NewTree(ctx context.Context, cln *client.Client, mod *parser.Module, long bool) (treeprint.Tree, error) {
+func NewTree(ctx context.Context, cln *client.Client, mod *ast.Module, long bool) (treeprint.Tree, error) {
 	resolver, err := NewResolver(cln)
 	if err != nil {
 		return nil, err
@@ -22,7 +22,7 @@ func NewTree(ctx context.Context, cln *client.Client, mod *parser.Module, long b
 
 	var (
 		tree         = treeprint.New()
-		nodeByModule = make(map[*parser.Module]treeprint.Tree)
+		nodeByModule = make(map[*ast.Module]treeprint.Tree)
 		mu           sync.Mutex
 	)
 
diff --git a/module/vendor.go b/module/vendor.go
index eea3315..baed3bb 100644
--- a/module/vendor.go
+++ b/module/vendor.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/moby/buildkit/client"
 	"github.com/openllb/hlb/codegen"
-	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
 	"golang.org/x/sync/errgroup"
 )
 
@@ -17,7 +17,7 @@ import (
 //
 // If tidy mode is enabled, vertices with digests that already exist in the
 // modules directory are skipped, and unused modules are pruned.
-func Vendor(ctx context.Context, cln *client.Client, mod *parser.Module, targets []string, tidy bool) error {
+func Vendor(ctx context.Context, cln *client.Client, mod *ast.Module, targets []string, tidy bool) error {
 	root := ModulesPath
 
 	var mu sync.Mutex
diff --git a/parser/cst.go b/parser/ast/ast.go
similarity index 99%
rename from parser/cst.go
rename to parser/ast/ast.go
index d99575d..1979c9d 100644
--- a/parser/cst.go
+++ b/parser/ast/ast.go
@@ -1,4 +1,4 @@
-package parser
+package ast
 
 import (
 	"fmt"
diff --git a/parser/deprecated.go b/parser/ast/deprecated.go
similarity index 98%
rename from parser/deprecated.go
rename to parser/ast/deprecated.go
index a15d240..f833093 100644
--- a/parser/deprecated.go
+++ b/parser/ast/deprecated.go
@@ -1,4 +1,4 @@
-package parser
+package ast
 
 import (
 	"github.com/alecthomas/participle/lexer"
diff --git a/parser/kind_set.go b/parser/ast/kind_set.go
similarity index 97%
rename from parser/kind_set.go
rename to parser/ast/kind_set.go
index 296b9da..0e784f5 100644
--- a/parser/kind_set.go
+++ b/parser/ast/kind_set.go
@@ -1,4 +1,4 @@
-package parser
+package ast
 
 import (
 	"sort"
diff --git a/parser/scope.go b/parser/ast/scope.go
similarity index 99%
rename from parser/scope.go
rename to parser/ast/scope.go
index 30141e1..f14386c 100644
--- a/parser/scope.go
+++ b/parser/ast/scope.go
@@ -1,4 +1,4 @@
-package parser
+package ast
 
 import (
 	"sort"
diff --git a/parser/unparse.go b/parser/ast/unparse.go
similarity index 99%
rename from parser/unparse.go
rename to parser/ast/unparse.go
index f96af31..e4aabc2 100644
--- a/parser/unparse.go
+++ b/parser/ast/unparse.go
@@ -1,4 +1,4 @@
-package parser
+package ast
 
 import (
 	"fmt"
diff --git a/parser/walk.go b/parser/ast/walk.go
similarity index 99%
rename from parser/walk.go
rename to parser/ast/walk.go
index c366689..f31c316 100644
--- a/parser/walk.go
+++ b/parser/ast/walk.go
@@ -1,4 +1,4 @@
-package parser
+package ast
 
 import (
 	"fmt"
diff --git a/parser/directory.go b/parser/directory.go
new file mode 100644
index 0000000..bcd25b0
--- /dev/null
+++ b/parser/directory.go
@@ -0,0 +1,36 @@
+package parser
+
+import (
+	"io"
+	"os"
+	"path/filepath"
+
+	digest "github.com/opencontainers/go-digest"
+	"github.com/openllb/hlb/parser/ast"
+)
+
+type localDirectory struct {
+	root string
+	dgst digest.Digest
+}
+
+func NewLocalDirectory(root string, dgst digest.Digest) ast.Directory {
+	return &localDirectory{root, dgst}
+}
+
+func (r *localDirectory) Path() string {
+	return r.root
+}
+
+func (r *localDirectory) Digest() digest.Digest {
+	return r.dgst
+}
+
+func (r *localDirectory) Open(filename string) (io.ReadCloser, error) {
+	if filepath.IsAbs(filename) {
+		return os.Open(filename)
+	}
+	return os.Open(filepath.Join(r.root, filename))
+}
+
+func (r *localDirectory) Close() error { return nil }
diff --git a/parser/docstrings.go b/parser/docstrings.go
index 6ede13c..012a2d0 100644
--- a/parser/docstrings.go
+++ b/parser/docstrings.go
@@ -1,29 +1,31 @@
 package parser
 
+import "github.com/openllb/hlb/parser/ast"
+
 // AssignDocStrings assigns the comment group immediately before a function
 // declaration as the function's doc string.
-func AssignDocStrings(mod *Module) {
+func AssignDocStrings(mod *ast.Module) {
 	var (
-		lastCG *CommentGroup
+		lastCG *ast.CommentGroup
 	)
 
-	Match(mod, MatchOpts{},
-		func(decl *Decl) {
+	ast.Match(mod, ast.MatchOpts{},
+		func(decl *ast.Decl) {
 			if decl.Comments != nil {
 				lastCG = decl.Comments
 			}
 		},
-		func(fun *FuncDecl) {
+		func(fun *ast.FuncDecl) {
 			if lastCG != nil && lastCG.End().Line == fun.Pos.Line-1 {
 				fun.Doc = lastCG
 			}
 
 			if fun.Body != nil {
-				Match(fun.Body, MatchOpts{},
-					func(cg *CommentGroup) {
+				ast.Match(fun.Body, ast.MatchOpts{},
+					func(cg *ast.CommentGroup) {
 						lastCG = cg
 					},
-					func(call *CallStmt) {
+					func(call *ast.CallStmt) {
 						if lastCG != nil && lastCG.End().Line == call.Pos.Line-1 {
 							call.Doc = lastCG
 						}
diff --git a/parser/parse.go b/parser/parse.go
index 99fbd6c..0fc6223 100644
--- a/parser/parse.go
+++ b/parser/parse.go
@@ -4,38 +4,36 @@ import (
 	"context"
 	"errors"
 	"io"
-	"os"
 	"path/filepath"
 
 	"github.com/alecthomas/participle/lexer"
-	digest "github.com/opencontainers/go-digest"
-	"github.com/openllb/hlb/diagnostic"
+	"github.com/openllb/hlb/parser/ast"
 	"github.com/openllb/hlb/pkg/filebuffer"
 	"golang.org/x/sync/errgroup"
 )
 
-func Parse(ctx context.Context, r io.Reader) (*Module, error) {
+func Parse(ctx context.Context, r io.Reader) (*ast.Module, error) {
 	name := lexer.NameOfReader(r)
 	if name == "" {
 		name = "<stdin>"
 	}
 	r = &NewlinedReader{Reader: r}
 
-	mod := &Module{}
+	mod := &ast.Module{}
 	defer AssignDocStrings(mod)
 
-	sources := diagnostic.Sources(ctx)
-	if sources != nil {
+	files := filebuffer.Buffers(ctx)
+	if files != nil {
 		fb := filebuffer.New(name)
 		r = io.TeeReader(r, fb)
 		defer func() {
 			if mod.Pos.Filename != "" {
-				sources.Set(mod.Pos.Filename, fb)
+				files.Set(mod.Pos.Filename, fb)
 			}
 		}()
 	}
 
-	err := Parser.Parse(name, r, mod)
+	err := ast.Parser.Parse(name, r, mod)
 	if err != nil {
 		return nil, err
 	}
@@ -43,8 +41,8 @@ func Parse(ctx context.Context, r io.Reader) (*Module, error) {
 	return mod, nil
 }
 
-func ParseMultiple(ctx context.Context, rs []io.Reader) ([]*Module, error) {
-	mods := make([]*Module, len(rs))
+func ParseMultiple(ctx context.Context, rs []io.Reader) ([]*ast.Module, error) {
+	mods := make([]*ast.Module, len(rs))
 
 	var g errgroup.Group
 	for i, r := range rs {
@@ -102,29 +100,3 @@ func (nr *NewlinedReader) Read(p []byte) (n int, err error) {
 	}
 	return n, nil
 }
-
-type localDirectory struct {
-	root string
-	dgst digest.Digest
-}
-
-func NewLocalDirectory(root string, dgst digest.Digest) Directory {
-	return &localDirectory{root, dgst}
-}
-
-func (r *localDirectory) Path() string {
-	return r.root
-}
-
-func (r *localDirectory) Digest() digest.Digest {
-	return r.dgst
-}
-
-func (r *localDirectory) Open(filename string) (io.ReadCloser, error) {
-	if filepath.IsAbs(filename) {
-		return os.Open(filename)
-	}
-	return os.Open(filepath.Join(r.root, filename))
-}
-
-func (r *localDirectory) Close() error { return nil }
diff --git a/pkg/filebuffer/filebuffer.go b/pkg/filebuffer/filebuffer.go
index a40f38f..5286a3c 100644
--- a/pkg/filebuffer/filebuffer.go
+++ b/pkg/filebuffer/filebuffer.go
@@ -2,6 +2,7 @@ package filebuffer
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io"
 	"sort"
@@ -12,27 +13,41 @@ import (
 	"github.com/moby/buildkit/solver/pb"
 )
 
-type Sources struct {
+type buffersKey struct{}
+
+func WithBuffers(ctx context.Context, buffers *BufferLookup) context.Context {
+	return context.WithValue(ctx, buffersKey{}, buffers)
+}
+
+func Buffers(ctx context.Context) *BufferLookup {
+	buffers, ok := ctx.Value(buffersKey{}).(*BufferLookup)
+	if !ok {
+		return NewBuffers()
+	}
+	return buffers
+}
+
+type BufferLookup struct {
 	fbs map[string]*FileBuffer
 	mu  sync.Mutex
 }
 
-func NewSources() *Sources {
-	return &Sources{
+func NewBuffers() *BufferLookup {
+	return &BufferLookup{
 		fbs: make(map[string]*FileBuffer),
 	}
 }
 
-func (s *Sources) Get(filename string) *FileBuffer {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	return s.fbs[filename]
+func (b *BufferLookup) Get(filename string) *FileBuffer {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+	return b.fbs[filename]
 }
 
-func (s *Sources) Set(filename string, fb *FileBuffer) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	s.fbs[filename] = fb
+func (b *BufferLookup) Set(filename string, fb *FileBuffer) {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+	b.fbs[filename] = fb
 }
 
 type FileBuffer struct {


Test patch:
diff --git a/checker/checker_test.go b/checker/checker_test.go
index 5d56936..ed047a0 100644
--- a/checker/checker_test.go
+++ b/checker/checker_test.go
@@ -10,13 +10,15 @@ import (
 	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	"github.com/stretchr/testify/require"
 )
 
 type testCase struct {
 	name  string
 	input string
-	fn    func(*parser.Module) error
+	fn    func(*ast.Module) error
 }
 
 func TestChecker_Check(t *testing.T) {
@@ -167,10 +169,10 @@ func TestChecker_Check(t *testing.T) {
 			image
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithNumArgs(
-				parser.Find(mod, "image"), 1, 0,
-				errdefs.Defined(parser.Find(builtin.Module, "image")),
+				ast.Find(mod, "image"), 1, 0,
+				errdefs.Defined(ast.Find(builtin.Module, "image")),
 			)
 		},
 	}, {
@@ -181,10 +183,10 @@ func TestChecker_Check(t *testing.T) {
 			image ref
 		}
 		`,
-		func(mod *parser.Module) error {
-			return errdefs.WithDuplicates([]parser.Node{
-				parser.Find(mod, "duplicate"),
-				parser.Find(mod, "duplicate", parser.WithSkip(1)),
+		func(mod *ast.Module) error {
+			return errdefs.WithDuplicates([]ast.Node{
+				ast.Find(mod, "duplicate"),
+				ast.Find(mod, "duplicate", ast.WithSkip(1)),
 			})
 		},
 	}, {
@@ -197,10 +199,10 @@ func TestChecker_Check(t *testing.T) {
 			}
 		}
 		`,
-		func(mod *parser.Module) error {
-			return errdefs.WithDuplicates([]parser.Node{
-				parser.Find(mod, "duplicate"),
-				parser.Find(mod, "duplicate", parser.WithSkip(1)),
+		func(mod *ast.Module) error {
+			return errdefs.WithDuplicates([]ast.Node{
+				ast.Find(mod, "duplicate"),
+				ast.Find(mod, "duplicate", ast.WithSkip(1)),
 			})
 		},
 	}, {
@@ -210,10 +212,10 @@ func TestChecker_Check(t *testing.T) {
 			run "echo Hello"
 		}
 		`,
-		func(mod *parser.Module) error {
-			return errdefs.WithDuplicates([]parser.Node{
-				parser.Find(builtin.Module, "image"),
-				parser.Find(mod, "image"),
+		func(mod *ast.Module) error {
+			return errdefs.WithDuplicates([]ast.Node{
+				ast.Find(builtin.Module, "image"),
+				ast.Find(mod, "image"),
 			})
 		},
 	}, {
@@ -225,10 +227,10 @@ func TestChecker_Check(t *testing.T) {
 			}
 		}
 		`,
-		func(mod *parser.Module) error {
-			return errdefs.WithDuplicates([]parser.Node{
-				parser.Find(builtin.Module, "image"),
-				parser.Find(mod, "image"),
+		func(mod *ast.Module) error {
+			return errdefs.WithDuplicates([]ast.Node{
+				ast.Find(builtin.Module, "image"),
+				ast.Find(mod, "image"),
 			})
 		},
 	}, {
@@ -243,10 +245,10 @@ func TestChecker_Check(t *testing.T) {
 			}
 		}
 		`,
-		func(mod *parser.Module) error {
-			return errdefs.WithDuplicates([]parser.Node{
-				parser.Find(mod, "duplicate"),
-				parser.Find(mod, "duplicate", parser.WithSkip(1)),
+		func(mod *ast.Module) error {
+			return errdefs.WithDuplicates([]ast.Node{
+				ast.Find(mod, "duplicate"),
+				ast.Find(mod, "duplicate", ast.WithSkip(1)),
 			})
 		},
 	}, {
@@ -258,10 +260,10 @@ func TestChecker_Check(t *testing.T) {
 			foo
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithCallImport(
-				parser.Find(mod, "foo", parser.WithSkip(1)),
-				parser.Find(mod, "foo"),
+				ast.Find(mod, "foo", ast.WithSkip(1)),
+				ast.Find(mod, "foo"),
 			)
 		},
 	}, {
@@ -289,9 +291,9 @@ func TestChecker_Check(t *testing.T) {
 		`
 		export foo
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithUndefinedIdent(
-				parser.Find(mod, "foo"),
+				ast.Find(mod, "foo"),
 				nil,
 			)
 		},
@@ -303,10 +305,10 @@ func TestChecker_Check(t *testing.T) {
 			myFunction.build
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithNotImport(
-				parser.Find(mod, "myFunction.build").(*parser.IdentExpr),
-				parser.Find(mod, "myFunction"),
+				ast.Find(mod, "myFunction.build").(*ast.IdentExpr),
+				ast.Find(mod, "myFunction"),
 			)
 		},
 	}, {
@@ -331,12 +333,12 @@ func TestChecker_Check(t *testing.T) {
 			image "alpine"
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithWrongType(
-				parser.Find(mod, "image"),
-				[]parser.Kind{parser.Pipeline},
-				parser.Filesystem,
-				errdefs.Defined(parser.Find(builtin.Module, "image")),
+				ast.Find(mod, "image"),
+				[]ast.Kind{ast.Pipeline},
+				ast.Filesystem,
+				errdefs.Defined(ast.Find(builtin.Module, "image")),
 			)
 		},
 	}, {
@@ -350,8 +352,8 @@ func TestChecker_Check(t *testing.T) {
 			dockerPush "some/ref" as
 		}
 		`,
-		func(mod *parser.Module) error {
-			return errdefs.WithNoBindTarget(parser.Find(mod, "as"))
+		func(mod *ast.Module) error {
+			return errdefs.WithNoBindTarget(ast.Find(mod, "as"))
 		},
 	}, {
 		"no error when bind list is empty",
@@ -369,12 +371,12 @@ func TestChecker_Check(t *testing.T) {
 			imageID
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithWrongType(
-				parser.Find(mod, "imageID", parser.WithSkip(1)),
-				[]parser.Kind{parser.Filesystem},
-				parser.String,
-				errdefs.Defined(parser.Find(mod, "imageID")),
+				ast.Find(mod, "imageID", ast.WithSkip(1)),
+				[]ast.Kind{ast.Filesystem},
+				ast.String,
+				errdefs.Defined(ast.Find(mod, "imageID")),
 			)
 		},
 	}, {
@@ -385,12 +387,12 @@ func TestChecker_Check(t *testing.T) {
 			imageID
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithWrongType(
-				parser.Find(mod, "imageID", parser.WithSkip(1)),
-				[]parser.Kind{parser.Filesystem},
-				parser.String,
-				errdefs.Defined(parser.Find(mod, "imageID")),
+				ast.Find(mod, "imageID", ast.WithSkip(1)),
+				[]ast.Kind{ast.Filesystem},
+				ast.String,
+				errdefs.Defined(ast.Find(mod, "imageID")),
 			)
 		},
 	}, {
@@ -400,12 +402,12 @@ func TestChecker_Check(t *testing.T) {
 			run "cmd" as nothing
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithNoBindEffects(
-				parser.Find(mod, "run"),
-				parser.Find(mod, "as"),
+				ast.Find(mod, "run"),
+				ast.Find(mod, "as"),
 				errdefs.Defined(
-					parser.Find(builtin.Module, "run"),
+					ast.Find(builtin.Module, "run"),
 				),
 			)
 		},
@@ -416,10 +418,10 @@ func TestChecker_Check(t *testing.T) {
 			dockerPush "some/ref:latest" as (undefined foo)
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithUndefinedBindTarget(
-				parser.Find(mod, "dockerPush"),
-				parser.Find(mod, "undefined"),
+				ast.Find(mod, "dockerPush"),
+				ast.Find(mod, "undefined"),
 			)
 		},
 	}, {
@@ -429,10 +431,10 @@ func TestChecker_Check(t *testing.T) {
 			mount scratch "/out" as default
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithNoBindClosure(
-				parser.Find(mod, "as"),
-				parser.Find(mod, "option::run"),
+				ast.Find(mod, "as"),
+				ast.Find(mod, "option::run"),
 			)
 		},
 	}, {
@@ -448,10 +450,10 @@ func TestChecker_Check(t *testing.T) {
 			run with opts
 		}
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithNoBindClosure(
-				parser.Find(mod, "as"),
-				parser.Find(mod, "option::run"),
+				ast.Find(mod, "as"),
+				ast.Find(mod, "option::run"),
 			)
 		},
 	}, {
@@ -473,7 +475,7 @@ func TestChecker_Check(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			in := strings.NewReader(dedent.Dedent(tc.input))
 
-			ctx := diagnostic.WithSources(context.Background(), builtin.Sources())
+			ctx := filebuffer.WithBuffers(context.Background(), builtin.Buffers())
 			mod, err := parser.Parse(ctx, in)
 			require.NoError(t, err)
 
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 763fda6..2927c4c 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -22,6 +22,8 @@ import (
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/local"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	"github.com/openllb/hlb/pkg/llbutil"
 	"github.com/openllb/hlb/solver"
 	"github.com/stretchr/testify/require"
@@ -937,7 +939,7 @@ func TestCodeGen(t *testing.T) {
 	}} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			ctx := diagnostic.WithSources(context.Background(), builtin.Sources())
+			ctx := filebuffer.WithBuffers(context.Background(), builtin.Buffers())
 			mod, err := parser.Parse(ctx, strings.NewReader(dedent.Dedent(tc.hlb)))
 			require.NoError(t, err, tc.name)
 
@@ -1005,7 +1007,7 @@ func TestCodegenError(t *testing.T) {
 		name    string
 		targets []string
 		input   string
-		fn      func(*parser.Module) error
+		fn      func(*ast.Module) error
 	}
 
 	for _, tc := range []testCase{
@@ -1017,10 +1019,10 @@ func TestCodegenError(t *testing.T) {
 				downloadDockerTarball "image.tar" "#"
 			}
 			`,
-			func(mod *parser.Module) error {
+			func(mod *ast.Module) error {
 				return errdefs.WithInvalidImageRef(
 					errors.New("invalid reference format"),
-					parser.Find(mod, `"#"`),
+					ast.Find(mod, `"#"`),
 					"#",
 				)
 			},
@@ -1028,7 +1030,7 @@ func TestCodegenError(t *testing.T) {
 	} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			ctx := diagnostic.WithSources(context.Background(), builtin.Sources())
+			ctx := filebuffer.WithBuffers(context.Background(), builtin.Buffers())
 			mod, err := parser.Parse(ctx, strings.NewReader(tc.input))
 			require.NoError(t, err, "unexpected parse error")
 
@@ -1069,7 +1071,7 @@ func TestCodeGenImport(t *testing.T) {
 		name    string
 		hlb     string
 		imports []testImport
-		fn      func(*parser.Module) error
+		fn      func(*ast.Module) error
 	}
 
 	for _, tc := range []testCase{{
@@ -1105,11 +1107,11 @@ func TestCodeGenImport(t *testing.T) {
 			fs foo()
 			`,
 		}},
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithUndefinedIdent(
-				parser.Find(mod, "bar"),
+				ast.Find(mod, "bar"),
 				nil,
-				errdefs.Imported(parser.Find(mod, "other")),
+				errdefs.Imported(ast.Find(mod, "other")),
 			)
 		},
 	}, {
@@ -1127,10 +1129,10 @@ func TestCodeGenImport(t *testing.T) {
 			fs foo()
 			`,
 		}},
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithCallUnexported(
-				parser.Find(mod, "foo"),
-				errdefs.Imported(parser.Find(mod, "other")),
+				ast.Find(mod, "foo"),
+				errdefs.Imported(ast.Find(mod, "other")),
 			)
 		},
 	}, {
@@ -1234,7 +1236,7 @@ func TestCodeGenImport(t *testing.T) {
 	}} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			ctx := diagnostic.WithSources(context.Background(), builtin.Sources())
+			ctx := filebuffer.WithBuffers(context.Background(), builtin.Buffers())
 			r := &parser.NamedReader{
 				Reader: strings.NewReader(dedent.Dedent(tc.hlb)),
 				Value:  "build.hlb",
diff --git a/linter/linter_test.go b/linter/linter_test.go
index eb4d95a..9562c4e 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -11,13 +11,15 @@ import (
 	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	"github.com/stretchr/testify/require"
 )
 
 type testCase struct {
 	name  string
 	input string
-	fn    func(*parser.Module) error
+	fn    func(*ast.Module) error
 }
 
 func TestLinter_Lint(t *testing.T) {
@@ -28,9 +30,9 @@ func TestLinter_Lint(t *testing.T) {
 		`
 		import foo "./foo.hlb"
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return errdefs.WithDeprecated(
-				mod, parser.Find(mod, `"./foo.hlb"`).(*parser.StringLit),
+				mod, ast.Find(mod, `"./foo.hlb"`).(*ast.StringLit),
 				`import path without keyword "from" is deprecated`,
 			)
 		},
@@ -44,15 +46,15 @@ func TestLinter_Lint(t *testing.T) {
 		fs foo()
 		fs bar()
 		`,
-		func(mod *parser.Module) error {
+		func(mod *ast.Module) error {
 			return &diagnostic.Error{
 				Diagnostics: []error{
 					errdefs.WithDeprecated(
-						mod, parser.Find(mod, "group"),
+						mod, ast.Find(mod, "group"),
 						"type `group` is deprecated, use `pipeline` instead",
 					),
 					errdefs.WithDeprecated(
-						mod, parser.Find(mod, "parallel"),
+						mod, ast.Find(mod, "parallel"),
 						"function `parallel` is deprecated, use `stage` instead",
 					),
 				},
@@ -63,7 +65,7 @@ func TestLinter_Lint(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			in := strings.NewReader(dedent.Dedent(tc.input))
 
-			ctx := diagnostic.WithSources(context.Background(), builtin.Sources())
+			ctx := filebuffer.WithBuffers(context.Background(), builtin.Buffers())
 			mod, err := parser.Parse(ctx, in)
 			require.NoError(t, err)
 
diff --git a/module/resolve_test.go b/module/resolve_test.go
index ca558ff..1ed7062 100644
--- a/module/resolve_test.go
+++ b/module/resolve_test.go
@@ -15,13 +15,15 @@ import (
 	"github.com/openllb/hlb/diagnostic"
 	"github.com/openllb/hlb/errdefs"
 	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/parser/ast"
+	"github.com/openllb/hlb/pkg/filebuffer"
 	"github.com/stretchr/testify/require"
 )
 
 type testCase struct {
 	name  string
 	input string
-	fn    func(mod *parser.Module, imods map[string]*parser.Module) error
+	fn    func(mod *ast.Module, imods map[string]*ast.Module) error
 }
 
 type testDirectory struct {
@@ -90,10 +92,10 @@ func TestResolveGraph(t *testing.T) {
 		`
 		import unknown from "unknown.hlb"
 		`,
-		func(mod *parser.Module, imods map[string]*parser.Module) error {
+		func(mod *ast.Module, imods map[string]*ast.Module) error {
 			return errdefs.WithImportPathNotExist(
 				os.ErrNotExist,
-				parser.Find(mod, `"unknown.hlb"`),
+				ast.Find(mod, `"unknown.hlb"`),
 				"unknown.hlb",
 			)
 		},
@@ -102,10 +104,10 @@ func TestResolveGraph(t *testing.T) {
 		`
 		import transitive from "transitive-deprecated-unknown.hlb"
 		`,
-		func(mod *parser.Module, imods map[string]*parser.Module) error {
+		func(mod *ast.Module, imods map[string]*ast.Module) error {
 			return errdefs.WithImportPathNotExist(
 				os.ErrNotExist,
-				parser.Find(imods["transitive"], `"unknown.hlb"`),
+				ast.Find(imods["transitive"], `"unknown.hlb"`),
 				"unknown.hlb",
 			)
 		},
@@ -114,7 +116,7 @@ func TestResolveGraph(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			in := strings.NewReader(dedent.Dedent(tc.input))
 
-			ctx := diagnostic.WithSources(context.Background(), builtin.Sources())
+			ctx := filebuffer.WithBuffers(context.Background(), builtin.Buffers())
 			mod, err := parser.Parse(ctx, in)
 			require.NoError(t, err)
 			mod.Directory = dir
@@ -125,7 +127,7 @@ func TestResolveGraph(t *testing.T) {
 			err = checker.Check(mod)
 			require.NoError(t, err)
 
-			imods := make(map[string]*parser.Module)
+			imods := make(map[string]*ast.Module)
 			err = ResolveGraph(ctx, nil, nil, mod, func(info VisitInfo) error {
 				imods[info.ImportDecl.Name.Text] = info.Import
 				return nil
diff --git a/parser/unparse_test.go b/parser/ast/unparse_test.go
similarity index 97%
rename from parser/unparse_test.go
rename to parser/ast/unparse_test.go
index 249bcdb..0d2b794 100644
--- a/parser/unparse_test.go
+++ b/parser/ast/unparse_test.go
@@ -1,7 +1,6 @@
-package parser
+package ast
 
 import (
-	"context"
 	"fmt"
 	"strings"
 	"testing"
@@ -659,9 +658,12 @@ func TestUnparse(t *testing.T) {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
-			file, err := Parse(context.Background(), strings.NewReader(cleanup(tc.input)))
+
+			mod := &Module{}
+			r := strings.NewReader(cleanup(tc.input))
+			err := Parser.Parse("", r, mod)
 			require.NoError(t, err)
-			require.Equal(t, cleanup(tc.expected), file.String())
+			require.Equal(t, cleanup(tc.expected), mod.String())
 		})
 	}
 }