Repository: openllb/hlb
Pull request #164 (2020-06-12 16:59:58)

Side effects and Binds
This PR introduces Side Effects on Function Declarations, and exposes a BindClause that consumes those side effects. 

Still a Work in Progress, feedback appreciated!


Patch:
diff --git a/build.hlb b/build.hlb
index d0fb004..501f855 100644
--- a/build.hlb
+++ b/build.hlb
@@ -21,12 +21,15 @@ fs src() {
 	}
 }
 
-fs gen() {
-	mkdocs.generatedBuiltin
-	download "./builtin/."
-
-	mkdocs.generatedMarkdown
-	download "./docs/."
+group gen() {
+	parallel fs {
+        	mkdocs.generatedBuiltin
+        	download "./builtin/."
+	}
+	parallel fs {
+        	mkdocs.generatedMarkdown
+        	download "./docs/."
+	}
 }
 
 fs docs() {
diff --git a/builtin/builtin.go b/builtin/builtin.go
index dbed4a1..7cee57e 100644
--- a/builtin/builtin.go
+++ b/builtin/builtin.go
@@ -13,7 +13,8 @@ type LookupByType struct {
 }
 
 type FuncLookup struct {
-	Params []*parser.Field
+	Params  []*parser.Field
+	Effects []*parser.Field
 }
 
 var (
@@ -22,65 +23,83 @@ var (
 			parser.Filesystem: LookupByType{
 				Func: map[string]FuncLookup{
 					"scratch": FuncLookup{
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
+					},
+					"state": FuncLookup{
 						Params: []*parser.Field{},
+						Effects: []*parser.Field{
+							parser.NewField(parser.Filesystem, "current", false),
+						},
 					},
 					"image": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "ref", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"http": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "url", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"git": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "remote", false),
 							parser.NewField(parser.Str, "ref", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"local": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "path", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"frontend": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "source", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"shell": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "arg", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"run": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "arg", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"env": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "key", false),
 							parser.NewField(parser.Str, "value", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"dir": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "path", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"user": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "name", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"mkdir": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "path", false),
 							parser.NewField(parser.Int, "filemode", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"mkfile": FuncLookup{
 						Params: []*parser.Field{
@@ -88,11 +107,13 @@ var (
 							parser.NewField(parser.Int, "filemode", false),
 							parser.NewField(parser.Str, "content", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"rm": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "path", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"copy": FuncLookup{
 						Params: []*parser.Field{
@@ -100,68 +121,83 @@ var (
 							parser.NewField(parser.Str, "src", false),
 							parser.NewField(parser.Str, "dst", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"dockerPush": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "ref", false),
 						},
+						Effects: []*parser.Field{
+							parser.NewField(parser.Str, "digest", false),
+						},
 					},
 					"dockerLoad": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "ref", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"download": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "localPath", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"downloadTarball": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "localPath", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"downloadOCITarball": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "localPath", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"downloadDockerTarball": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "localPath", false),
 							parser.NewField(parser.Str, "ref", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"entrypoint": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "args", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"cmd": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "args", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"label": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "key", false),
 							parser.NewField(parser.Str, "value", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"expose": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "ports", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"volumes": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "mountpoints", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"stopSignal": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "signal", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
@@ -171,43 +207,53 @@ var (
 						Params: []*parser.Field{
 							parser.NewField("group", "groups", true),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			"option::copy": LookupByType{
 				Func: map[string]FuncLookup{
 					"followSymlinks": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"contentsOnly": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"unpack": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"createDestPath": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"allowWildcard": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"allowEmptyWildcard": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"chown": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "owner", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"chmod": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Int, "filemode", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"createdTime": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "created", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
@@ -218,19 +264,22 @@ var (
 							parser.NewField(parser.Str, "key", false),
 							parser.NewField(parser.Filesystem, "value", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"opt": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "key", false),
 							parser.NewField(parser.Str, "value", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			"option::git": LookupByType{
 				Func: map[string]FuncLookup{
 					"keepGitDir": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
@@ -240,23 +289,27 @@ var (
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "digest", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"chmod": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Int, "filemode", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"filename": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "name", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			"option::image": LookupByType{
 				Func: map[string]FuncLookup{
 					"resolve": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
@@ -266,49 +319,59 @@ var (
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "pattern", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"excludePatterns": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "pattern", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"followPaths": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "path", true),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			"option::localRun": LookupByType{
 				Func: map[string]FuncLookup{
 					"ignoreError": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"includeStderr": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"onlyStderr": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"shlex": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			"option::mkdir": LookupByType{
 				Func: map[string]FuncLookup{
 					"createParents": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"chown": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "owner", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"createdTime": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "created", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
@@ -318,108 +381,131 @@ var (
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "owner", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"createdTime": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "created", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			"option::mount": LookupByType{
 				Func: map[string]FuncLookup{
 					"readonly": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"tmpfs": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"sourcePath": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "path", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"cache": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "cacheid", false),
 							parser.NewField(parser.Str, "sharingmode", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			"option::rm": LookupByType{
 				Func: map[string]FuncLookup{
 					"allowNotFound": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"allowWildcard": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			"option::run": LookupByType{
 				Func: map[string]FuncLookup{
 					"readonlyRootfs": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"env": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "key", false),
 							parser.NewField(parser.Str, "value", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"dir": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "path", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"user": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "name", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"ignoreCache": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"network": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "networkmode", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"security": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "securitymode", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"shlex": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"host": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "hostname", false),
 							parser.NewField(parser.Str, "address", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"ssh": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"forward": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "src", false),
 							parser.NewField(parser.Str, "dest", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"secret": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "localPath", false),
 							parser.NewField(parser.Str, "mountPoint", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"mount": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Filesystem, "input", false),
 							parser.NewField(parser.Str, "mountPoint", false),
 						},
+						Effects: []*parser.Field{
+							parser.NewField(parser.Filesystem, "target", false),
+						},
 					},
 				},
 			},
@@ -429,26 +515,31 @@ var (
 						Params: []*parser.Field{
 							parser.NewField(parser.Int, "id", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"gid": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Int, "id", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"mode": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Int, "filemode", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"includePatterns": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "pattern", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"excludePatterns": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "pattern", true),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
@@ -458,26 +549,31 @@ var (
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "mountPoint", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"localPaths": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "path", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"uid": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Int, "id", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"gid": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Int, "id", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"mode": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Int, "filemode", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
@@ -488,41 +584,55 @@ var (
 							parser.NewField(parser.Str, "name", false),
 							parser.NewField(parser.Str, "value", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
 			parser.Str: LookupByType{
 				Func: map[string]FuncLookup{
+					"state": FuncLookup{
+						Params: []*parser.Field{},
+						Effects: []*parser.Field{
+							parser.NewField(parser.Str, "current", false),
+						},
+					},
 					"format": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "formatString", false),
 							parser.NewField(parser.Str, "values", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"localArch": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"localCwd": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"localEnv": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "key", false),
 						},
+						Effects: []*parser.Field{},
 					},
 					"localOs": FuncLookup{
-						Params: []*parser.Field{},
+						Params:  []*parser.Field{},
+						Effects: []*parser.Field{},
 					},
 					"localRun": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "command", false),
 							parser.NewField(parser.Str, "args", true),
 						},
+						Effects: []*parser.Field{},
 					},
 					"template": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "text", false),
 						},
+						Effects: []*parser.Field{},
 					},
 				},
 			},
diff --git a/checker/builtin.go b/checker/builtin.go
new file mode 100644
index 0000000..288a917
--- /dev/null
+++ b/checker/builtin.go
@@ -0,0 +1,50 @@
+package checker
+
+import (
+	"github.com/openllb/hlb/builtin"
+	"github.com/openllb/hlb/parser"
+)
+
+// Builtin is a Scope containing references to all builtins.
+var Builtin = NewBuiltinScope(builtin.Lookup)
+
+// BuiltinDecl is a synthetic declaration representing a builtin name. Special type checking rules
+// apply to builtins.
+type BuiltinDecl struct {
+	*parser.Ident
+	Func map[parser.ObjType]*parser.FuncDecl
+}
+
+// NewBuiltinScope returns a new Scope containing synthetic FuncDecl Objects for
+// builtins.
+func NewBuiltinScope(builtins builtin.BuiltinLookup) *parser.Scope {
+	scope := parser.NewScope(nil, nil)
+	for typ, entries := range builtins.ByType {
+		for name, fn := range entries.Func {
+			obj := scope.Lookup(name)
+			if obj == nil {
+				ident := parser.NewIdent(name)
+				ident.Pos.Filename = "<builtin>"
+				obj = &parser.Object{
+					Kind:  parser.DeclKind,
+					Ident: ident,
+					Node: &BuiltinDecl{
+						Ident: ident,
+						Func:  make(map[parser.ObjType]*parser.FuncDecl),
+					},
+				}
+			}
+			decl, ok := obj.Node.(*BuiltinDecl)
+			if !ok {
+				panic("implementation error")
+			}
+
+			fun := parser.NewFuncDecl(typ, name, fn.Params, fn.Effects).Func
+			fun.Pos.Filename = "<builtin>"      // for errors attached to func
+			fun.Name.Pos.Filename = "<builtin>" // for errors attached to Name
+			decl.Func[typ] = fun
+			scope.Insert(obj)
+		}
+	}
+	return scope
+}
diff --git a/checker/checker.go b/checker/checker.go
index edd10a0..060645f 100644
--- a/checker/checker.go
+++ b/checker/checker.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/openllb/hlb/builtin"
 	"github.com/openllb/hlb/parser"
 )
 
@@ -26,7 +25,7 @@ func (c *checker) Check(mod *parser.Module) error {
 	//
 	// HLB is mod-scoped, so HLBs in the same directory will have separate
 	// scopes and must be imported to be used.
-	mod.Scope = parser.NewScope(mod, nil)
+	mod.Scope = parser.NewScope(mod, Builtin)
 
 	// We have to make a pass first to construct scopes because later checks
 	// depend on having scopes available. While constructing scopes, we can also
@@ -65,36 +64,54 @@ func (c *checker) Check(mod *parser.Module) error {
 				}
 			}
 
-			// Aliases may be declared inside the body of a function, and alias
+			// Create scope entries for the functions side effects
+			if fun.SideEffects != nil && fun.SideEffects.Effects != nil {
+				for _, effect := range fun.SideEffects.Effects.List {
+					fun.Scope.Insert(&parser.Object{
+						Kind:  parser.FieldKind,
+						Ident: effect.Name,
+						Node:  effect,
+					})
+				}
+			}
+
+			// Binds may be declared inside the body of a function, and their target
 			// identifiers will also collide with any other declarations, so we must
 			// check if there are any duplicate declarations there.
 			parser.Inspect(fun, func(node parser.Node) bool {
-				if n, ok := node.(*parser.CallStmt); ok {
-					alias := n.Alias
-					if alias == nil {
-						return true
+				n, ok := node.(parser.Block)
+				if !ok {
+					return true
+				}
+
+				for _, stmt := range n.List() {
+					call := stmt.Call
+					binds := call.Binds
+					if binds == nil {
+						continue
+					}
+
+					// mount scratch "/" as default
+					if binds.Ident != nil {
+						c.registerDecl(mod.Scope, binds.Ident, binds)
 					}
 
-					alias.Func = fun
-					alias.Call = n
-
-					if alias.Ident != nil {
-						obj := mod.Scope.Lookup(alias.Ident.Name)
-						if obj != nil {
-							if len(c.duplicateDecls) == 0 {
-								c.duplicateDecls = append(c.duplicateDecls, obj.Ident)
-							}
-							c.duplicateDecls = append(c.duplicateDecls, alias.Ident)
-							return true
+					// mount scratch "/" as (target default)
+					if binds.List != nil {
+						for _, b := range binds.List.List {
+							c.registerDecl(mod.Scope, b.Target, binds)
 						}
+					}
 
-						mod.Scope.Insert(&parser.Object{
-							Kind:  parser.DeclKind,
-							Ident: alias.Ident,
-							Node:  alias,
-						})
+					// Link to its lexical scope.
+					binds.Lexical = fun
+
+					err := c.bindEffects(mod.Scope, n.ObjType(), call)
+					if err != nil {
+						c.errs = append(c.errs, err)
 					}
 				}
+
 				return true
 			})
 
@@ -108,7 +125,7 @@ func (c *checker) Check(mod *parser.Module) error {
 		c.errs = append(c.errs, ErrDuplicateDecls{c.duplicateDecls})
 	}
 
-	// Now we have scopes constructed, we walk again to check everything else.
+	// Now its safe to check everything else.
 	parser.Inspect(mod, func(node parser.Node) bool {
 		switch n := node.(type) {
 		case *parser.ImportDecl:
@@ -145,6 +162,14 @@ func (c *checker) Check(mod *parser.Module) error {
 				}
 			}
 
+			if fun.SideEffects != nil && fun.SideEffects.Effects != nil {
+				err := c.checkFieldList(fun.SideEffects.Effects.List)
+				if err != nil {
+					c.errs = append(c.errs, err)
+					return false
+				}
+			}
+
 			if fun.Type != nil && fun.Body != nil {
 				err := c.checkBlockStmt(fun.Scope, fun.Type.ObjType, fun.Body)
 				if err != nil {
@@ -235,7 +260,9 @@ func (c *checker) registerDecl(scope *parser.Scope, ident *parser.Ident, node pa
 	obj := scope.Lookup(ident.Name)
 	if obj != nil {
 		if len(c.duplicateDecls) == 0 {
-			c.duplicateDecls = append(c.duplicateDecls, obj.Ident)
+			if _, ok := obj.Node.(*BuiltinDecl); !ok {
+				c.duplicateDecls = append(c.duplicateDecls, obj.Ident)
+			}
 		}
 		c.duplicateDecls = append(c.duplicateDecls, ident)
 		return
@@ -328,43 +355,48 @@ func (c *checker) checkBlockStmt(scope *parser.Scope, typ parser.ObjType, block
 			panic("implementation error")
 		}
 
-		// If the function is not a builtin, retrieve it from the scope and then
-		// type check it.
-		_, ok := builtin.Lookup.ByType[typ].Func[name]
-		if !ok {
-			obj := scope.Lookup(name)
-			if obj == nil {
-				return ErrIdentNotDefined{Ident: call.Func.IdentNode()}
-			}
+		// Retrieve the function from the scope and then type check it.
+		obj := scope.Lookup(name)
+		if obj == nil {
+			return ErrIdentNotDefined{Ident: call.Func.IdentNode()}
+		}
 
-			// The retrieved object may be either a function declaration or a field
-			// in the current scope's function parameters.
-			var callType *parser.Type
-			switch obj.Kind {
-			case parser.DeclKind:
-				switch n := obj.Node.(type) {
-				case *parser.FuncDecl:
-					callType = n.Type
-				case *parser.AliasDecl:
-					callType = n.Func.Type
-				case *parser.ImportDecl:
-					c.errs = append(c.errs, ErrUseModuleWithoutSelector{Ident: call.Func.IdentNode()})
+		// The retrieved object may be either a function declaration, a field
+		// in the current scope's function parameters, a bound side effect, or
+		// a builtin.
+		var callType *parser.Type
+		switch obj.Kind {
+		case parser.DeclKind:
+			switch n := obj.Node.(type) {
+			case *parser.FuncDecl:
+				callType = n.Type
+			case *parser.BindClause:
+				b := n.TargetBinding(name)
+				callType = b.Field.Type
+			case *parser.ImportDecl:
+				c.errs = append(c.errs, ErrUseModuleWithoutSelector{Ident: call.Func.IdentNode()})
+				continue
+			case *BuiltinDecl:
+				fun, err := c.checkBuiltinCall(call, typ, n)
+				if err != nil {
+					c.errs = append(c.errs, err)
 					continue
 				}
-			case parser.FieldKind:
-				field, ok := obj.Node.(*parser.Field)
-				if ok {
-					callType = field.Type
-				}
+				callType = fun.Type
 			}
-
-			err := c.checkType(call, typ, callType)
-			if err != nil {
-				return err
+		case parser.FieldKind:
+			field, ok := obj.Node.(*parser.Field)
+			if ok {
+				callType = field.Type
 			}
 		}
 
-		err := c.checkCallStmt(scope, typ, call)
+		err := c.checkType(call, typ, callType)
+		if err != nil {
+			return err
+		}
+
+		err = c.checkCallStmt(scope, typ, call)
 		if err != nil {
 			return err
 		}
@@ -373,6 +405,70 @@ func (c *checker) checkBlockStmt(scope *parser.Scope, typ parser.ObjType, block
 	return nil
 }
 
+func (c *checker) bindEffects(scope *parser.Scope, typ parser.ObjType, call *parser.CallStmt) error {
+	binds := call.Binds
+	if binds == nil {
+		return nil
+	}
+
+	if binds.Ident == nil && binds.List == nil {
+		return ErrBindNoTarget{binds.As.Pos}
+	}
+
+	ident := call.Func.IdentNode()
+	obj := scope.Lookup(ident.String())
+	if obj == nil {
+		return ErrIdentNotDefined{ident}
+	}
+
+	decl, ok := obj.Node.(*BuiltinDecl)
+	if !ok {
+		return ErrBindBadSource{call}
+	}
+
+	fun, ok := decl.Func[typ]
+	if !ok {
+		return ErrWrongBuiltinType{call.Pos, typ, decl}
+	}
+
+	if fun.SideEffects == nil ||
+		fun.SideEffects.Effects == nil ||
+		fun.SideEffects.Effects.NumFields() == 0 {
+		return ErrBindBadSource{call}
+	}
+
+	// Link its side effects.
+	binds.Effects = fun.SideEffects.Effects
+
+	// Match each Bind to a Field on call's EffectsClause.
+	if binds.List != nil {
+		for _, b := range binds.List.List {
+			var field *parser.Field
+			for _, f := range binds.Effects.List {
+				if f.Name.String() == b.Source.String() {
+					field = f
+					break
+				}
+			}
+			if field == nil {
+				return ErrBindBadTarget{call, b}
+			}
+			b.Field = field
+		}
+	}
+
+	return nil
+}
+
+func (c *checker) checkBuiltinCall(call *parser.CallStmt, typ parser.ObjType, decl *BuiltinDecl) (*parser.FuncDecl, error) {
+	fun, ok := decl.Func[typ]
+	if !ok {
+		return nil, ErrWrongBuiltinType{call.Pos, typ, decl}
+	}
+
+	return fun, nil
+}
+
 func (c *checker) checkCallStmt(scope *parser.Scope, typ parser.ObjType, call *parser.CallStmt) error {
 	if call.Func.Selector != nil {
 		return nil
@@ -383,51 +479,53 @@ func (c *checker) checkCallStmt(scope *parser.Scope, typ parser.ObjType, call *p
 		return err
 	}
 
-	return c.checkCallArgs(scope, call.Func, call.Args, call.WithOpt, params)
+	err = c.checkCallArgs(scope, call.Func, call.Args, call.WithOpt, params)
+	if err != nil {
+		return err
+	}
+	return nil
 }
 
 func (c *checker) checkCallSignature(scope *parser.Scope, typ parser.ObjType, expr *parser.Expr, args []*parser.Expr) ([]*parser.Field, error) {
 	var signature []*parser.Field
-	fun, ok := builtin.Lookup.ByType[typ].Func[expr.Name()]
-	if !ok && typ == parser.Group {
-		fun, ok = builtin.Lookup.ByType[parser.Filesystem].Func[expr.Name()]
+
+	obj := scope.Lookup(expr.Name())
+	if obj == nil {
+		return nil, ErrIdentUndefined{expr.IdentNode()}
 	}
 
-	if ok {
-		signature = fun.Params
-	} else {
-		obj := scope.Lookup(expr.Name())
-		if obj == nil {
-			return nil, ErrIdentUndefined{expr.IdentNode()}
-		}
+	if obj.Kind == parser.DeclKind {
+		switch n := obj.Node.(type) {
+		case *parser.FuncDecl:
+			signature = n.Params.List
+		case *parser.BindClause:
+			signature = n.Lexical.Params.List
+		case *BuiltinDecl:
+			fun, ok := n.Func[typ]
+			if !ok {
+				return nil, ErrWrongBuiltinType{expr.Pos, typ, n}
+			}
+			signature = fun.Params.List
+		case *parser.ImportDecl:
+			importScope := obj.Data.(*parser.Scope)
+			importObj := importScope.Lookup(expr.Selector.Select.Name)
+			if importObj == nil {
+				return nil, ErrIdentUndefined{expr.Selector.Select}
+			}
+			if !importObj.Exported {
+				return nil, ErrCallUnexported{expr.Selector}
+			}
 
-		if obj.Kind == parser.DeclKind {
-			switch n := obj.Node.(type) {
+			switch m := importObj.Node.(type) {
 			case *parser.FuncDecl:
-				signature = n.Params.List
-			case *parser.AliasDecl:
-				signature = n.Func.Params.List
-			case *parser.ImportDecl:
-				importScope := obj.Data.(*parser.Scope)
-				importObj := importScope.Lookup(expr.Selector.Select.Name)
-				if importObj == nil {
-					return nil, ErrIdentUndefined{expr.Selector.Select}
-				}
-				if !importObj.Exported {
-					return nil, ErrCallUnexported{expr.Selector}
-				}
-
-				switch m := importObj.Node.(type) {
-				case *parser.FuncDecl:
-					signature = m.Params.List
-				case *parser.AliasDecl:
-					signature = m.Func.Params.List
-				default:
-					panic("implementation error")
-				}
+				signature = m.Params.List
+			case *parser.BindClause:
+				signature = m.Lexical.Params.List
 			default:
 				panic("implementation error")
 			}
+		default:
+			panic("implementation error")
 		}
 	}
 
@@ -492,14 +590,6 @@ func (c *checker) checkExpr(scope *parser.Scope, typ parser.ObjType, expr *parse
 }
 
 func (c *checker) checkIdentArg(scope *parser.Scope, typ parser.ObjType, ident *parser.Ident) error {
-	fun, ok := builtin.Lookup.ByType[typ].Func[ident.Name]
-	if ok {
-		if len(fun.Params) > 0 {
-			return ErrFuncArg{ident}
-		}
-		return nil
-	}
-
 	obj := scope.Lookup(ident.Name)
 	if obj == nil {
 		return ErrIdentNotDefined{ident}
@@ -512,8 +602,16 @@ func (c *checker) checkIdentArg(scope *parser.Scope, typ parser.ObjType, ident *
 			if n.Params.NumFields() > 0 {
 				return ErrFuncArg{ident}
 			}
-		case *parser.AliasDecl:
-			if n.Func.Params.NumFields() > 0 {
+		case *parser.BindClause:
+			if n.Lexical.Params.NumFields() > 0 {
+				return ErrFuncArg{ident}
+			}
+		case *BuiltinDecl:
+			fun, ok := n.Func[typ]
+			if !ok {
+				return ErrWrongBuiltinType{ident.Pos, typ, n}
+			}
+			if fun.Params.NumFields() > 0 {
 				return ErrFuncArg{ident}
 			}
 		default:
@@ -576,6 +674,19 @@ func (c *checker) checkOptionBlockStmt(scope *parser.Scope, typ parser.ObjType,
 			continue
 		}
 
+		// check builtin options
+		name := call.Func.Name()
+		obj := scope.Lookup(name)
+		if obj != nil {
+			decl, ok := obj.Node.(*BuiltinDecl)
+			if ok {
+				_, err := c.checkBuiltinCall(call, typ, decl)
+				if err != nil {
+					return err
+				}
+			}
+		}
+
 		err := c.checkCallStmt(scope, typ, call)
 		if err != nil {
 			return err
diff --git a/checker/errors.go b/checker/errors.go
index 5bca4e7..d160269 100644
--- a/checker/errors.go
+++ b/checker/errors.go
@@ -49,6 +49,33 @@ func (e ErrInvalidFunc) Error() string {
 	return fmt.Sprintf("%s invalid func %s", FormatPos(e.CallStmt.Pos), e.CallStmt.Func)
 }
 
+type ErrBindNoTarget struct {
+	Pos lexer.Position
+}
+
+func (e ErrBindNoTarget) Error() string {
+	return fmt.Sprintf("%s cannot bind: has no target", FormatPos(e.Pos))
+}
+
+type ErrBindBadSource struct {
+	CallStmt *parser.CallStmt
+}
+
+func (e ErrBindBadSource) Error() string {
+	return fmt.Sprintf("%s cannot bind: %s has no side effects",
+		FormatPos(e.CallStmt.Pos), e.CallStmt.Func)
+}
+
+type ErrBindBadTarget struct {
+	CallStmt *parser.CallStmt
+	Bind     *parser.Bind
+}
+
+func (e ErrBindBadTarget) Error() string {
+	return fmt.Sprintf("%s cannot bind: %s is not a side effect of %s",
+		FormatPos(e.Bind.Pos), e.Bind.Source, e.CallStmt.Func)
+}
+
 type ErrNumArgs struct {
 	Node     parser.Node
 	Expected int
@@ -85,6 +112,17 @@ func (e ErrWrongArgType) Error() string {
 	return fmt.Sprintf("%s expected arg to be type %s, found %s", FormatPos(e.Pos), e.Expected, e.Found)
 }
 
+type ErrWrongBuiltinType struct {
+	Pos      lexer.Position
+	Expected parser.ObjType
+	Builtin  *BuiltinDecl
+}
+
+func (e ErrWrongBuiltinType) Error() string {
+	return fmt.Sprintf("%s builtin %s does not provide type %s",
+		FormatPos(e.Pos), e.Builtin.Name, e.Expected)
+}
+
 type ErrInvalidTarget struct {
 	Node   parser.Node
 	Target string
diff --git a/codegen/bind.go b/codegen/bind.go
new file mode 100644
index 0000000..b40c4c4
--- /dev/null
+++ b/codegen/bind.go
@@ -0,0 +1,69 @@
+package codegen
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/openllb/hlb/parser"
+)
+
+type ErrBindingCycle struct {
+	Binding parser.Binding
+}
+
+func (e ErrBindingCycle) Error() string {
+	return fmt.Sprintf("%s cyclic binding", e.Binding.Bind.Pos)
+}
+
+func (cg *CodeGen) setBindingValue(b parser.Binding, value interface{}) error {
+	oldValue := cg.values[b]
+	if oldValue == nil {
+		cg.values[b] = value
+		return nil
+	}
+
+	switch val := oldValue.(type) {
+	// A sentinel value to detect cycles. Store the new value, but signal
+	// the caller to break the cycle.
+	case ErrBindingCycle:
+		cg.values[b] = value
+		return val
+	// Any other error value is just an error and should be returned up the stack.
+	case error:
+		return val
+	default:
+		panic("implementation error")
+	}
+}
+
+func (cg *CodeGen) EmitBinding(ctx context.Context, scope *parser.Scope, b parser.Binding, args []*parser.Expr, chainStart interface{}) (interface{}, error) {
+	v, ok := cg.values[b]
+
+	// When the binding value is not yet set, emit the containing func to compute a value.
+	if !ok {
+		// Store an error value for this Binding, which we'll use to prevent cycles.
+		cycle := ErrBindingCycle{b}
+		cg.values[b] = cycle
+
+		// Codegen can short-circuit control flow by returning ErrBindingCycle up the
+		// stack. If it matches b then we've finished early and the error is ignored.
+		var err error
+		v, err = cg.EmitFuncDecl(ctx, scope, b.Bind.Lexical, args, chainStart)
+		if errors.As(err, &cycle) && cycle.Binding == b {
+			err = nil
+			v = cg.values[b]
+		} else {
+			cg.values[b] = v
+		}
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// An error value usually means a cycle.
+	if val, ok := v.(error); ok {
+		return nil, val
+	}
+	return v, nil
+}
diff --git a/codegen/chain.go b/codegen/chain.go
index 412ea0d..b1b9c6f 100644
--- a/codegen/chain.go
+++ b/codegen/chain.go
@@ -11,16 +11,18 @@ import (
 	"text/template"
 
 	shellquote "github.com/kballard/go-shellquote"
+	"github.com/moby/buildkit/client"
 	"github.com/moby/buildkit/client/llb"
 	gateway "github.com/moby/buildkit/frontend/gateway/client"
 	"github.com/moby/buildkit/session/filesync"
 	"github.com/moby/buildkit/solver/pb"
-	"github.com/openllb/hlb/local"
-	"github.com/openllb/hlb/parser"
-	"github.com/openllb/hlb/solver"
 	"github.com/pkg/errors"
 	fstypes "github.com/tonistiigi/fsutil/types"
 	"golang.org/x/sync/errgroup"
+
+	"github.com/openllb/hlb/local"
+	"github.com/openllb/hlb/parser"
+	"github.com/openllb/hlb/solver"
 )
 
 type FilesystemChain func(llb.State) (llb.State, error)
@@ -29,10 +31,10 @@ type GroupChain func([]solver.Request) ([]solver.Request, error)
 
 type StringChain func(string) (string, error)
 
-func (cg *CodeGen) EmitChainStmt(ctx context.Context, scope *parser.Scope, typ parser.ObjType, call *parser.CallStmt, ac aliasCallback, chainStart interface{}) (func(v interface{}) (interface{}, error), error) {
+func (cg *CodeGen) EmitChainStmt(ctx context.Context, scope *parser.Scope, typ parser.ObjType, call *parser.CallStmt, chainStart interface{}) (func(v interface{}) (interface{}, error), error) {
 	switch typ {
 	case parser.Filesystem:
-		chain, err := cg.EmitFilesystemChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, ac, chainStart)
+		chain, err := cg.EmitFilesystemChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, call.Binds, chainStart)
 		if err != nil {
 			return nil, err
 		}
@@ -44,7 +46,7 @@ func (cg *CodeGen) EmitChainStmt(ctx context.Context, scope *parser.Scope, typ p
 			return chain(st)
 		}, nil
 	case parser.Str:
-		chain, err := cg.EmitStringChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, chainStart)
+		chain, err := cg.EmitStringChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, call.Binds, chainStart)
 		if err != nil {
 			return nil, err
 		}
@@ -56,7 +58,7 @@ func (cg *CodeGen) EmitChainStmt(ctx context.Context, scope *parser.Scope, typ p
 			return chain(str)
 		}, nil
 	case parser.Group:
-		chain, err := cg.EmitGroupChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, ac, chainStart)
+		chain, err := cg.EmitGroupChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, chainStart)
 		if err != nil {
 			return nil, err
 		}
@@ -72,8 +74,8 @@ func (cg *CodeGen) EmitChainStmt(ctx context.Context, scope *parser.Scope, typ p
 	}
 }
 
-func (cg *CodeGen) EmitFilesystemChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, with *parser.WithOpt, ac aliasCallback, chainStart interface{}) (fc FilesystemChain, err error) {
-	fc, err = cg.EmitFilesystemBuiltinChainStmt(ctx, scope, expr, args, with, ac, chainStart)
+func (cg *CodeGen) EmitFilesystemChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, with *parser.WithOpt, binds *parser.BindClause, chainStart interface{}) (fc FilesystemChain, err error) {
+	fc, err = cg.EmitFilesystemBuiltinChainStmt(ctx, scope, expr, args, with, binds, chainStart)
 	if err != nil {
 		return fc, err
 	}
@@ -89,21 +91,24 @@ func (cg *CodeGen) EmitFilesystemChainStmt(ctx context.Context, scope *parser.Sc
 		var err error
 		switch n := obj.Node.(type) {
 		case *parser.FuncDecl:
-			v, err = cg.EmitFuncDecl(ctx, scope, n, args, ac, chainStart)
-		case *parser.AliasDecl:
-			v, err = cg.EmitAliasDecl(ctx, scope, n, args, chainStart)
+			v, err = cg.EmitFuncDecl(ctx, scope, n, args, chainStart)
+		case *parser.BindClause:
+			b := n.TargetBinding(expr.Name())
+			v, err = cg.EmitBinding(ctx, scope, b, args, chainStart)
 		case *parser.ImportDecl:
 			importScope := obj.Data.(*parser.Scope)
-			importObj := importScope.Lookup(expr.Selector.Select.Name)
+			importName := expr.Selector.Select.Name
+			importObj := importScope.Lookup(importName)
 			if importObj == nil {
 				return nil, errors.WithStack(ErrCodeGen{expr.Selector, ErrUndefinedReference})
 			}
 
 			switch m := importObj.Node.(type) {
 			case *parser.FuncDecl:
-				v, err = cg.EmitFuncDecl(ctx, scope, m, args, ac, chainStart)
-			case *parser.AliasDecl:
-				v, err = cg.EmitAliasDecl(ctx, scope, m, args, chainStart)
+				v, err = cg.EmitFuncDecl(ctx, scope, m, args, chainStart)
+			case *parser.BindClause:
+				b := m.TargetBinding(importName)
+				v, err = cg.EmitBinding(ctx, scope, b, args, chainStart)
 			default:
 				return fc, errors.WithStack(ErrCodeGen{m, errors.Errorf("unknown obj type")})
 			}
@@ -159,7 +164,7 @@ func (cg *CodeGen) EmitShellCommand(ctx context.Context, scope *parser.Scope, ar
 	return runArgs, nil
 }
 
-func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, with *parser.WithOpt, ac aliasCallback, chainStart interface{}) (fc FilesystemChain, err error) {
+func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, with *parser.WithOpt, binds *parser.BindClause, chainStart interface{}) (fc FilesystemChain, err error) {
 	var iopts []interface{}
 	if with != nil {
 		iopts, err = cg.EmitOptionExpr(ctx, scope, with.Expr, nil, expr.Name())
@@ -351,8 +356,7 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 			return fc, err
 		}
 
-		var targets []string
-		calls := make(map[string]*parser.CallStmt)
+		mounts := make(map[string]*parser.CallStmt)
 
 		if with != nil {
 			switch {
@@ -363,17 +367,15 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 				// to be in the context of a specific function run is in.
 			case with.Expr.FuncLit != nil:
 				for _, stmt := range with.Expr.FuncLit.Body.NonEmptyStmts() {
-					if stmt.Call.Func.Name() != "mount" || stmt.Call.Alias == nil {
+					if stmt.Call.Func.Name() != "mount" || stmt.Call.Binds == nil {
 						continue
 					}
 
-					target, err := cg.EmitStringExpr(ctx, scope, stmt.Call.Args[1])
+					path, err := cg.EmitStringExpr(ctx, scope, stmt.Call.Args[1])
 					if err != nil {
 						return fc, err
 					}
-
-					calls[target] = stmt.Call
-					targets = append(targets, target)
+					mounts[path] = stmt.Call
 				}
 			default:
 				return nil, errors.WithStack(ErrCodeGen{with, errors.Errorf("unknown with option")})
@@ -389,20 +391,23 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 		}
 
 		fc = func(st llb.State) (llb.State, error) {
-			exec := st.Run(opts...)
-
-			if len(targets) > 0 {
-				for _, target := range targets {
-					// Mounts are unique by its mountpoint, and its vertex representing the
-					// mount after execing can be aliased.
-					cont := ac(calls[target], exec.GetMount(target))
-					if !cont {
-						return exec.Root(), ErrAliasReached
+			run := st.Run(opts...)
+
+			if len(mounts) > 0 {
+				for path, mount := range mounts {
+					mnt := run.GetMount(path)
+					b := mount.Binds.SourceBinding("target")
+					// This will return an error if the Binding is the current CodeGen target.
+					// Execution is short-circuited here, and the actual mnt value is returned to
+					// the caller.
+					err := cg.setBindingValue(b, mnt)
+					if err != nil {
+						return run.Root(), err
 					}
 				}
 			}
 
-			return exec.Root(), nil
+			return run.Root(), nil
 		}
 	case "env":
 		key, err := cg.EmitStringExpr(ctx, scope, args[0])
@@ -503,7 +508,7 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 			), nil
 		}
 	case "copy":
-		input, err := cg.EmitFilesystemExpr(ctx, scope, args[0], ac)
+		input, err := cg.EmitFilesystemExpr(ctx, scope, args[0])
 		if err != nil {
 			return fc, err
 		}
@@ -637,12 +642,39 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 		}
 
 		fc = func(st llb.State) (llb.State, error) {
-			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDockerPush, Ref: ref})
+			var dgst string
+			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDockerPush, Ref: ref},
+				solver.WithCallback(func(resp *client.SolveResponse) error {
+					dgst = resp.ExporterResponse[keyContainerImageDigest]
+					return nil
+				}),
+			)
 			if err != nil {
 				return st, err
 			}
-			cg.requests = append(cg.requests, request)
-			return st, nil
+
+			// If there are no binds, its safe to return immediately and add the output
+			// request to the queue.
+			// Otherwise, we need to solve the request before the binding value will be
+			// available.
+			if binds == nil {
+				cg.requests = append(cg.requests, request)
+				return st, nil
+			}
+
+			g, ctx := errgroup.WithContext(ctx)
+
+			g.Go(func() error {
+				return request.Solve(ctx, cg.cln, cg.mw)
+			})
+
+			err = cg.setBindingValue(
+				binds.SourceBinding("digest"),
+				func() (string, error) {
+					return dgst, g.Wait()
+				},
+			)
+			return st, err
 		}
 	case "dockerLoad":
 		ref, err := cg.EmitStringExpr(ctx, scope, args[0])
@@ -904,7 +936,7 @@ func fixReadonlyMounts(opts []llb.RunOption) error {
 	return nil
 }
 
-func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, with *parser.WithOpt, chainStart interface{}) (StringChain, error) {
+func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, with *parser.WithOpt, binds *parser.BindClause, chainStart interface{}) (StringChain, error) {
 	var iopts []interface{}
 	var err error
 	if with != nil {
@@ -1032,21 +1064,24 @@ func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope,
 		var err error
 		switch n := obj.Node.(type) {
 		case *parser.FuncDecl:
-			v, err = cg.EmitFuncDecl(ctx, scope, n, args, noopAliasCallback, chainStart)
-		case *parser.AliasDecl:
-			v, err = cg.EmitAliasDecl(ctx, scope, n, args, chainStart)
+			v, err = cg.EmitFuncDecl(ctx, scope, n, args, chainStart)
+		case *parser.BindClause:
+			b := n.TargetBinding(expr.Name())
+			v, err = cg.EmitBinding(ctx, scope, b, args, chainStart)
 		case *parser.ImportDecl:
 			importScope := obj.Data.(*parser.Scope)
-			importObj := importScope.Lookup(expr.Selector.Select.Name)
+			importName := expr.Selector.Select.Name
+			importObj := importScope.Lookup(importName)
 			if importObj == nil {
 				return nil, errors.WithStack(ErrCodeGen{expr.Selector, ErrUndefinedReference})
 			}
 
 			switch m := importObj.Node.(type) {
 			case *parser.FuncDecl:
-				v, err = cg.EmitFuncDecl(ctx, scope, m, args, noopAliasCallback, chainStart)
-			case *parser.AliasDecl:
-				v, err = cg.EmitAliasDecl(ctx, scope, m, args, chainStart)
+				v, err = cg.EmitFuncDecl(ctx, scope, m, args, chainStart)
+			case *parser.BindClause:
+				b := m.TargetBinding(importName)
+				v, err = cg.EmitBinding(ctx, scope, b, args, chainStart)
 			default:
 				return nil, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
 			}
@@ -1059,21 +1094,23 @@ func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope,
 			return nil, err
 		}
 		return func(_ string) (string, error) {
-			str, ok := v.(string)
-			if !ok {
-				return str, errors.WithStack(ErrCodeGen{obj.Node, ErrBadCast})
+			switch s := v.(type) {
+			case string:
+				return s, nil
+			case func() (string, error):
+				return s()
 			}
-			return str, nil
+			return "", errors.WithStack(ErrCodeGen{obj.Node, ErrBadCast})
 		}, nil
 	}
 }
 
-func (cg *CodeGen) EmitGroupChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, with *parser.WithOpt, ac aliasCallback, chainStart interface{}) (gc GroupChain, err error) {
+func (cg *CodeGen) EmitGroupChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, with *parser.WithOpt, chainStart interface{}) (gc GroupChain, err error) {
 	switch expr.Name() {
 	case "parallel":
 		var peerRequests []solver.Request
 		for _, arg := range args {
-			request, err := cg.EmitGroupExpr(ctx, scope, arg, ac)
+			request, err := cg.EmitGroupExpr(ctx, scope, arg)
 			if err != nil {
 				return gc, err
 			}
@@ -1099,21 +1136,24 @@ func (cg *CodeGen) EmitGroupChainStmt(ctx context.Context, scope *parser.Scope,
 		var v interface{}
 		switch n := obj.Node.(type) {
 		case *parser.FuncDecl:
-			v, err = cg.EmitFuncDecl(ctx, scope, n, args, ac, chainStart)
-		case *parser.AliasDecl:
-			v, err = cg.EmitAliasDecl(ctx, scope, n, args, chainStart)
+			v, err = cg.EmitFuncDecl(ctx, scope, n, args, chainStart)
+		case *parser.BindClause:
+			b := n.TargetBinding(expr.Name())
+			v, err = cg.EmitBinding(ctx, scope, b, args, chainStart)
 		case *parser.ImportDecl:
 			importScope := obj.Data.(*parser.Scope)
-			importObj := importScope.Lookup(expr.Selector.Select.Name)
+			importName := expr.Selector.Select.Name
+			importObj := importScope.Lookup(importName)
 			if importObj == nil {
 				return gc, errors.WithStack(ErrCodeGen{expr.Selector, ErrUndefinedReference})
 			}
 
 			switch m := importObj.Node.(type) {
 			case *parser.FuncDecl:
-				v, err = cg.EmitFuncDecl(ctx, scope, m, args, ac, chainStart)
-			case *parser.AliasDecl:
-				v, err = cg.EmitAliasDecl(ctx, scope, m, args, chainStart)
+				v, err = cg.EmitFuncDecl(ctx, scope, m, args, chainStart)
+			case *parser.BindClause:
+				b := m.TargetBinding(importName)
+				v, err = cg.EmitBinding(ctx, scope, b, args, chainStart)
 			default:
 				return gc, errors.WithStack(ErrCodeGen{m, errors.Errorf("unknown obj type")})
 			}
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 9e1f175..3fed6c0 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -38,15 +38,13 @@ import (
 	"golang.org/x/sync/errgroup"
 )
 
-var (
-	ErrAliasReached = errors.New("alias reached")
-)
-
 type CodeGen struct {
 	Debug     Debugger
 	cln       *client.Client
 	sessionID string
 
+	values map[parser.Binding]interface{}
+
 	requests        []solver.Request
 	syncedDirByID   map[string]filesync.SyncedDir
 	fileSourceByID  map[string]secretsprovider.FileSource
@@ -135,17 +133,18 @@ func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []T
 					return nil, checker.ErrInvalidTarget{Node: n, Target: target.Name}
 				}
 
-				v, err = cg.EmitFuncDecl(ctx, mod.Scope, n, nil, noopAliasCallback, nil)
+				v, err = cg.EmitFuncDecl(ctx, mod.Scope, n, nil, nil)
 				if err != nil {
 					return nil, err
 				}
-			case *parser.AliasDecl:
-				typ = n.Func.Type
+			case *parser.BindClause:
+				b := n.TargetBinding(target.Name)
+				typ = b.Field.Type
 				if typ.Primary() != parser.Group && typ.Primary() != parser.Filesystem {
 					return nil, checker.ErrInvalidTarget{Node: n, Target: target.Name}
 				}
 
-				v, err = cg.EmitAliasDecl(ctx, mod.Scope, n, nil, nil)
+				v, err = cg.EmitBinding(ctx, mod.Scope, b, nil, nil)
 				if err != nil {
 					return nil, err
 				}
@@ -206,6 +205,7 @@ func (cg *CodeGen) reset() {
 	cg.fileSourceByID = map[string]secretsprovider.FileSource{}
 	cg.agentConfigByID = map[string]sockprovider.AgentConfig{}
 	cg.image = &specs.Image{}
+	cg.values = make(map[parser.Binding]interface{})
 }
 
 func (cg *CodeGen) newSession(ctx context.Context) (*session.Session, error) {
@@ -260,18 +260,14 @@ func (cg *CodeGen) newSession(ctx context.Context) (*session.Session, error) {
 }
 
 func (cg *CodeGen) GenerateImport(ctx context.Context, scope *parser.Scope, lit *parser.FuncLit) (llb.State, error) {
-	return cg.EmitFilesystemBlock(ctx, scope, lit.Body, nil, nil)
+	return cg.EmitFilesystemBlock(ctx, scope, lit.Body, nil)
 }
 
-type aliasCallback func(*parser.CallStmt, interface{}) bool
-
-func noopAliasCallback(_ *parser.CallStmt, _ interface{}) bool { return true }
-
 func isBreakpoint(call *parser.CallStmt) bool {
 	return call.Func.Name() == "breakpoint"
 }
 
-func (cg *CodeGen) EmitBlock(ctx context.Context, scope *parser.Scope, typ parser.ObjType, stmts []*parser.Stmt, ac aliasCallback, chainStart interface{}) (interface{}, error) {
+func (cg *CodeGen) EmitBlock(ctx context.Context, scope *parser.Scope, typ parser.ObjType, stmts []*parser.Stmt, chainStart interface{}) (interface{}, error) {
 	var v = chainStart
 	switch typ {
 	case parser.Filesystem:
@@ -305,14 +301,14 @@ func (cg *CodeGen) EmitBlock(ctx context.Context, scope *parser.Scope, typ parse
 			return v, err
 		}
 
-		chain, err := cg.EmitChainStmt(ctx, scope, typ, call, ac, v)
+		chain, err := cg.EmitChainStmt(ctx, scope, typ, call, v)
 		if err != nil {
 			return v, err
 		}
 
 		var cerr error
 		v, cerr = chain(v)
-		if cerr == nil || cerr == ErrAliasReached {
+		if cerr == nil || errors.As(cerr, &ErrBindingCycle{}) {
 			if st, ok := v.(llb.State); ok && st.Output() != nil {
 				err = st.Validate(ctx)
 				if err != nil {
@@ -323,34 +319,25 @@ func (cg *CodeGen) EmitBlock(ctx context.Context, scope *parser.Scope, typ parse
 		if cerr != nil {
 			return v, cerr
 		}
-
-		if call.Alias != nil {
-			// Chain statements may be aliased.
-			cont := ac(call, v)
-			if !cont {
-				return v, ErrAliasReached
-			}
-		}
 	}
 
 	return v, nil
 }
 
-func (cg *CodeGen) EmitFilesystemBlock(ctx context.Context, scope *parser.Scope, body *parser.BlockStmt, ac aliasCallback, chainStart interface{}) (st llb.State, err error) {
-	v, err := cg.EmitBlock(ctx, scope, parser.Filesystem, body.NonEmptyStmts(), ac, chainStart)
-	if err != nil {
-		return
-	}
-
-	st, ok := v.(llb.State)
-	if !ok {
-		return st, errors.WithStack(ErrCodeGen{body, ErrBadCast})
+func (cg *CodeGen) EmitFilesystemBlock(ctx context.Context, scope *parser.Scope, body *parser.BlockStmt, chainStart interface{}) (llb.State, error) {
+	v, err := cg.EmitBlock(ctx, scope, parser.Filesystem, body.NonEmptyStmts(), chainStart)
+	if errors.As(err, &ErrBindingCycle{}) || err == nil {
+		st, ok := v.(llb.State)
+		if !ok {
+			return st, errors.WithStack(ErrCodeGen{body, ErrBadCast})
+		}
+		return st, err
 	}
-	return
+	return llb.State{}, err
 }
 
 func (cg *CodeGen) EmitStringBlock(ctx context.Context, scope *parser.Scope, body *parser.BlockStmt, chainStart interface{}) (str string, err error) {
-	v, err := cg.EmitBlock(ctx, scope, parser.Str, body.NonEmptyStmts(), noopAliasCallback, chainStart)
+	v, err := cg.EmitBlock(ctx, scope, parser.Str, body.NonEmptyStmts(), chainStart)
 	if err != nil {
 		return
 	}
@@ -362,8 +349,8 @@ func (cg *CodeGen) EmitStringBlock(ctx context.Context, scope *parser.Scope, bod
 	return
 }
 
-func (cg *CodeGen) EmitGroupBlock(ctx context.Context, scope *parser.Scope, body *parser.BlockStmt, ac aliasCallback, chainStart interface{}) (solver.Request, error) {
-	v, err := cg.EmitBlock(ctx, scope, parser.Group, body.NonEmptyStmts(), ac, chainStart)
+func (cg *CodeGen) EmitGroupBlock(ctx context.Context, scope *parser.Scope, body *parser.BlockStmt, chainStart interface{}) (solver.Request, error) {
+	v, err := cg.EmitBlock(ctx, scope, parser.Group, body.NonEmptyStmts(), chainStart)
 	if err != nil {
 		return nil, err
 	}
@@ -378,24 +365,24 @@ func (cg *CodeGen) EmitGroupBlock(ctx context.Context, scope *parser.Scope, body
 	return solver.Sequential(requests...), nil
 }
 
-func (cg *CodeGen) EmitFuncLit(ctx context.Context, scope *parser.Scope, lit *parser.FuncLit, op string, ac aliasCallback) (interface{}, error) {
+func (cg *CodeGen) EmitFuncLit(ctx context.Context, scope *parser.Scope, lit *parser.FuncLit, op string) (interface{}, error) {
 	switch lit.Type.Primary() {
 	case parser.Int, parser.Bool:
 		panic("unimplemented")
 	case parser.Filesystem:
-		return cg.EmitFilesystemBlock(ctx, scope, lit.Body, ac, nil)
+		return cg.EmitFilesystemBlock(ctx, scope, lit.Body, nil)
 	case parser.Str:
 		return cg.EmitStringBlock(ctx, scope, lit.Body, nil)
 	case parser.Option:
-		return cg.EmitOptionBlock(ctx, scope, op, lit.Body, ac)
+		return cg.EmitOptionBlock(ctx, scope, op, lit.Body)
 	case parser.Group:
-		return cg.EmitGroupBlock(ctx, scope, lit.Body, ac, nil)
+		return cg.EmitGroupBlock(ctx, scope, lit.Body, nil)
 	default:
 		return nil, errors.WithStack(ErrCodeGen{lit, errors.Errorf("unknown func lit")})
 	}
 }
 
-func (cg *CodeGen) EmitOptionBlock(ctx context.Context, scope *parser.Scope, op string, body *parser.BlockStmt, ac aliasCallback) (opts []interface{}, err error) {
+func (cg *CodeGen) EmitOptionBlock(ctx context.Context, scope *parser.Scope, op string, body *parser.BlockStmt) (opts []interface{}, err error) {
 	stmts := body.NonEmptyStmts()
 	switch op {
 	case "image":
@@ -407,9 +394,9 @@ func (cg *CodeGen) EmitOptionBlock(ctx context.Context, scope *parser.Scope, op
 	case "local":
 		return cg.EmitLocalOptions(ctx, scope, op, stmts)
 	case "frontend":
-		return cg.EmitFrontendOptions(ctx, scope, op, stmts, ac)
+		return cg.EmitFrontendOptions(ctx, scope, op, stmts)
 	case "run":
-		return cg.EmitExecOptions(ctx, scope, op, stmts, ac)
+		return cg.EmitExecOptions(ctx, scope, op, stmts)
 	case "ssh":
 		return cg.EmitSSHOptions(ctx, scope, op, stmts)
 	case "secret":
@@ -617,7 +604,7 @@ func withFrontendOpt(key, value string) gatewayOption {
 	}
 }
 
-func (cg *CodeGen) EmitFrontendOptions(ctx context.Context, scope *parser.Scope, op string, stmts []*parser.Stmt, ac aliasCallback) (opts []interface{}, err error) {
+func (cg *CodeGen) EmitFrontendOptions(ctx context.Context, scope *parser.Scope, op string, stmts []*parser.Stmt) (opts []interface{}, err error) {
 	for _, stmt := range stmts {
 		if stmt.Call != nil {
 			args := stmt.Call.Args
@@ -627,7 +614,7 @@ func (cg *CodeGen) EmitFrontendOptions(ctx context.Context, scope *parser.Scope,
 				if err != nil {
 					return opts, err
 				}
-				st, err := cg.EmitFilesystemExpr(ctx, scope, args[1], ac)
+				st, err := cg.EmitFilesystemExpr(ctx, scope, args[1])
 				if err != nil {
 					return opts, err
 				}
@@ -969,7 +956,7 @@ func (cg *CodeGen) EmitLocalExecOptions(ctx context.Context, scope *parser.Scope
 
 type shlexOption struct{}
 
-func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op string, stmts []*parser.Stmt, ac aliasCallback) (opts []interface{}, err error) {
+func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op string, stmts []*parser.Stmt) (opts []interface{}, err error) {
 	for _, stmt := range stmts {
 		if stmt.Call != nil {
 			var (
@@ -1149,7 +1136,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 
 					g, _ := errgroup.WithContext(ctx)
 
-					cg.SolveOpts = append(cg.SolveOpts, solver.WithCallback(func() error {
+					cg.SolveOpts = append(cg.SolveOpts, solver.WithCallback(func(*client.SolveResponse) error {
 						defer os.RemoveAll(dir)
 
 						err := l.Close()
@@ -1299,7 +1286,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 					))
 				}
 			case "mount":
-				input, err := cg.EmitFilesystemExpr(ctx, scope, args[0], ac)
+				input, err := cg.EmitFilesystemExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
diff --git a/codegen/debug.go b/codegen/debug.go
index 0aa92c1..1f981ab 100644
--- a/codegen/debug.go
+++ b/codegen/debug.go
@@ -246,8 +246,8 @@ func NewDebugger(c *client.Client, w io.Writer, r *bufio.Reader, ibs map[string]
 						switch n := obj.Node.(type) {
 						case *parser.FuncDecl:
 							fmt.Fprintf(w, "%s\n", n.Name)
-						case *parser.AliasDecl:
-							fmt.Fprintf(w, "%s\n", n.Ident)
+						case *parser.BindClause:
+							fmt.Fprintf(w, "%s\n", obj.Ident)
 						}
 					}
 				case "help":
diff --git a/codegen/decl.go b/codegen/decl.go
index 15c4ca4..9dd885e 100644
--- a/codegen/decl.go
+++ b/codegen/decl.go
@@ -10,7 +10,7 @@ import (
 	"github.com/pkg/errors"
 )
 
-func (cg *CodeGen) EmitFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (interface{}, error) {
+func (cg *CodeGen) EmitFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, chainStart interface{}) (interface{}, error) {
 	nonVariadicArgs := 0
 	for _, field := range fun.Params.List {
 		if field.Variadic == nil {
@@ -21,7 +21,7 @@ func (cg *CodeGen) EmitFuncDecl(ctx context.Context, scope *parser.Scope, fun *p
 		return nil, errors.WithStack(errors.Errorf("%s expected args %s, found %s", fun.Name, fun.Params.List, args))
 	}
 
-	err := cg.ParameterizedScope(ctx, scope, fun, args, ac)
+	err := cg.ParameterizedScope(ctx, scope, fun, args)
 	if err != nil {
 		return nil, err
 	}
@@ -34,20 +34,20 @@ func (cg *CodeGen) EmitFuncDecl(ctx context.Context, scope *parser.Scope, fun *p
 
 	switch fun.Type.Primary() {
 	case parser.Filesystem:
-		return cg.EmitFilesystemBlock(ctx, fun.Scope, fun.Body, ac, chainStart)
+		return cg.EmitFilesystemBlock(ctx, fun.Scope, fun.Body, chainStart)
 	case parser.Option:
-		return cg.EmitOptionBlock(ctx, fun.Scope, string(fun.Type.Secondary()), fun.Body, ac)
+		return cg.EmitOptionBlock(ctx, fun.Scope, string(fun.Type.Secondary()), fun.Body)
 	case parser.Str:
 		return cg.EmitStringBlock(ctx, fun.Scope, fun.Body, chainStart)
 	case parser.Group:
-		return cg.EmitGroupBlock(ctx, fun.Scope, fun.Body, ac, chainStart)
+		return cg.EmitGroupBlock(ctx, fun.Scope, fun.Body, chainStart)
 	default:
 		return chainStart, checker.ErrInvalidTarget{Node: fun}
 	}
 }
 
-func (cg *CodeGen) EmitFilesystemFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (st llb.State, err error) {
-	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, ac, chainStart)
+func (cg *CodeGen) EmitFilesystemFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, chainStart interface{}) (st llb.State, err error) {
+	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, chainStart)
 	if err != nil {
 		return
 	}
@@ -60,7 +60,7 @@ func (cg *CodeGen) EmitFilesystemFuncDecl(ctx context.Context, scope *parser.Sco
 }
 
 func (cg *CodeGen) EmitOptionFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr) (opts []interface{}, err error) {
-	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, noopAliasCallback, nil)
+	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, nil)
 	if err != nil {
 		return
 	}
@@ -72,8 +72,8 @@ func (cg *CodeGen) EmitOptionFuncDecl(ctx context.Context, scope *parser.Scope,
 	return
 }
 
-func (cg *CodeGen) EmitStringFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (str string, err error) {
-	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, ac, chainStart)
+func (cg *CodeGen) EmitStringFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, chainStart interface{}) (str string, err error) {
+	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, chainStart)
 	if err != nil {
 		return
 	}
@@ -85,8 +85,8 @@ func (cg *CodeGen) EmitStringFuncDecl(ctx context.Context, scope *parser.Scope,
 	return
 }
 
-func (cg *CodeGen) EmitGroupFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (solver.Request, error) {
-	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, ac, chainStart)
+func (cg *CodeGen) EmitGroupFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, chainStart interface{}) (solver.Request, error) {
+	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, chainStart)
 	if v == nil {
 		return nil, err
 	}
@@ -113,61 +113,7 @@ func (cg *CodeGen) EmitGroupFuncDecl(ctx context.Context, scope *parser.Scope, f
 	return request, err
 }
 
-func (cg *CodeGen) EmitAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (interface{}, error) {
-	var v interface{}
-	_, err := cg.EmitFuncDecl(ctx, scope, alias.Func, args, func(aliasCall *parser.CallStmt, aliasValue interface{}) bool {
-		if alias.Call == aliasCall {
-			v = aliasValue
-			return false
-		}
-		return true
-	}, chainStart)
-	if err == ErrAliasReached {
-		err = nil
-	}
-	return v, err
-}
-
-func (cg *CodeGen) EmitFilesystemAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (st llb.State, err error) {
-	v, err := cg.EmitAliasDecl(ctx, scope, alias, args, chainStart)
-	if err != nil {
-		return
-	}
-
-	st, ok := v.(llb.State)
-	if !ok {
-		return st, errors.WithStack(ErrCodeGen{alias, ErrBadCast})
-	}
-	return
-}
-
-func (cg *CodeGen) EmitStringAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (str string, err error) {
-	v, err := cg.EmitAliasDecl(ctx, scope, alias, args, chainStart)
-	if err != nil {
-		return
-	}
-
-	str, ok := v.(string)
-	if !ok {
-		return str, errors.WithStack(ErrCodeGen{alias, ErrBadCast})
-	}
-	return
-}
-
-func (cg *CodeGen) EmitGroupAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (request solver.Request, err error) {
-	v, err := cg.EmitAliasDecl(ctx, scope, alias, args, chainStart)
-	if err != nil {
-		return
-	}
-
-	request, ok := v.(solver.Request)
-	if !ok {
-		return request, errors.WithStack(ErrCodeGen{alias, ErrBadCast})
-	}
-	return
-}
-
-func (cg *CodeGen) ParameterizedScope(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback) error {
+func (cg *CodeGen) ParameterizedScope(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr) error {
 	for i, field := range fun.Params.List {
 		var (
 			data interface{}
@@ -190,7 +136,7 @@ func (cg *CodeGen) ParameterizedScope(ctx context.Context, scope *parser.Scope,
 			data = v
 		case parser.Filesystem:
 			var v llb.State
-			v, err = cg.EmitFilesystemExpr(ctx, scope, args[i], ac)
+			v, err = cg.EmitFilesystemExpr(ctx, scope, args[i])
 			data = v
 		case parser.Option:
 			var v []interface{}
@@ -209,7 +155,7 @@ func (cg *CodeGen) ParameterizedScope(ctx context.Context, scope *parser.Scope,
 			data = v
 		case parser.Group:
 			var v solver.Request
-			v, err = cg.EmitGroupExpr(ctx, scope, args[i], ac)
+			v, err = cg.EmitGroupExpr(ctx, scope, args[i])
 			data = v
 		}
 		if err != nil {
diff --git a/codegen/expr.go b/codegen/expr.go
index 121276b..ff93108 100644
--- a/codegen/expr.go
+++ b/codegen/expr.go
@@ -12,7 +12,7 @@ import (
 func (cg *CodeGen) EmitStringExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr) (string, error) {
 	switch {
 	case expr.Ident != nil, expr.Selector != nil:
-		sc, err := cg.EmitStringChainStmt(ctx, scope, expr, nil, nil, nil)
+		sc, err := cg.EmitStringChainStmt(ctx, scope, expr, nil, nil, nil, nil)
 		if err != nil {
 			return "", err
 		}
@@ -99,10 +99,10 @@ func (cg *CodeGen) MaybeEmitBoolExpr(ctx context.Context, scope *parser.Scope, a
 	return v, nil
 }
 
-func (cg *CodeGen) EmitFilesystemExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr, ac aliasCallback) (st llb.State, err error) {
+func (cg *CodeGen) EmitFilesystemExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr) (st llb.State, err error) {
 	switch {
 	case expr.Ident != nil, expr.Selector != nil:
-		so, err := cg.EmitFilesystemChainStmt(ctx, scope, expr, nil, nil, ac, nil)
+		so, err := cg.EmitFilesystemChainStmt(ctx, scope, expr, nil, nil, nil, nil)
 		if err != nil {
 			return st, err
 		}
@@ -111,7 +111,7 @@ func (cg *CodeGen) EmitFilesystemExpr(ctx context.Context, scope *parser.Scope,
 	case expr.BasicLit != nil:
 		return llb.Scratch(), errors.WithStack(ErrCodeGen{expr, errors.Errorf("fs expr cannot be basic lit")})
 	case expr.FuncLit != nil:
-		return cg.EmitFilesystemBlock(ctx, scope, expr.FuncLit.Body, ac, nil)
+		return cg.EmitFilesystemBlock(ctx, scope, expr.FuncLit.Body, nil)
 	default:
 		return st, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown fs expr")})
 	}
@@ -125,20 +125,20 @@ func (cg *CodeGen) EmitOptionExpr(ctx context.Context, scope *parser.Scope, expr
 				Func: expr,
 				Args: args,
 			},
-		}), noopAliasCallback)
+		}))
 	case expr.BasicLit != nil:
 		return nil, errors.WithStack(ErrCodeGen{expr, errors.Errorf("option expr cannot be basic lit")})
 	case expr.FuncLit != nil:
-		return cg.EmitOptionBlock(ctx, scope, op, expr.FuncLit.Body, noopAliasCallback)
+		return cg.EmitOptionBlock(ctx, scope, op, expr.FuncLit.Body)
 	default:
 		return opts, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown option expr")})
 	}
 }
 
-func (cg *CodeGen) EmitGroupExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr, ac aliasCallback) (solver.Request, error) {
+func (cg *CodeGen) EmitGroupExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr) (solver.Request, error) {
 	switch {
 	case expr.Ident != nil, expr.Selector != nil:
-		gc, err := cg.EmitGroupChainStmt(ctx, scope, expr, nil, nil, ac, nil)
+		gc, err := cg.EmitGroupChainStmt(ctx, scope, expr, nil, nil, nil)
 		if err != nil {
 			return nil, err
 		}
@@ -158,9 +158,9 @@ func (cg *CodeGen) EmitGroupExpr(ctx context.Context, scope *parser.Scope, expr
 	case expr.FuncLit != nil:
 		switch expr.FuncLit.Type.Primary() {
 		case parser.Group:
-			return cg.EmitGroupBlock(ctx, scope, expr.FuncLit.Body, ac, nil)
+			return cg.EmitGroupBlock(ctx, scope, expr.FuncLit.Body, nil)
 		case parser.Filesystem:
-			st, err := cg.EmitFilesystemBlock(ctx, scope, expr.FuncLit.Body, ac, nil)
+			st, err := cg.EmitFilesystemBlock(ctx, scope, expr.FuncLit.Body, nil)
 			if err != nil {
 				return nil, err
 			}
diff --git a/codegen/output.go b/codegen/output.go
index feea9a5..db5fddc 100644
--- a/codegen/output.go
+++ b/codegen/output.go
@@ -10,11 +10,13 @@ import (
 	"github.com/docker/buildx/util/progress"
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/cli/cli/flags"
+	"github.com/moby/buildkit/client"
 	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/session/filesync"
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
-	"github.com/openllb/hlb/solver"
 	"github.com/pkg/errors"
+
+	"github.com/openllb/hlb/solver"
 )
 
 type Target struct {
@@ -40,6 +42,10 @@ const (
 	OutputDownloadDockerTarball
 )
 
+const (
+	keyContainerImageDigest = "containerimage.digest"
+)
+
 func (cg *CodeGen) imageSpec(ctx context.Context, st llb.State) (*specs.Image, error) {
 	var err error
 	cg.image.Config.Env, err = st.Env(ctx)
@@ -55,8 +61,8 @@ func (cg *CodeGen) imageSpec(ctx context.Context, st llb.State) (*specs.Image, e
 	return cg.image, nil
 }
 
-func (cg *CodeGen) outputRequest(ctx context.Context, st llb.State, output Output) (solver.Request, error) {
-	opts := cg.SolveOpts
+func (cg *CodeGen) outputRequest(ctx context.Context, st llb.State, output Output, solveOpts ...solver.SolveOption) (solver.Request, error) {
+	opts := append(cg.SolveOpts, solveOpts...)
 
 	// Only add image spec when exporting as a Docker image.
 	switch output.Type {
@@ -98,7 +104,7 @@ func (cg *CodeGen) outputRequest(ctx context.Context, st llb.State, output Outpu
 		s.Allow(filesync.NewFSSyncTarget(outputFromWriter(w)))
 
 		done := make(chan struct{})
-		opts = append(opts, solver.WithCallback(func() error {
+		opts = append(opts, solver.WithCallback(func(*client.SolveResponse) error {
 			<-done
 			return nil
 		}))
diff --git a/docs/reference.md b/docs/reference.md
index 3b892ba..1e9cb08 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -691,6 +691,18 @@ methods. By default, this is [&quot;sh&quot;, &quot;-c&quot;].
 
 
 
+### <span class='hlb-type'>fs</span> <span class='hlb-name'>state</span>()
+
+
+A reference to the current filesystem state.
+
+	#!hlb
+	fs default() {
+		state
+	}
+
+
+
 ### <span class='hlb-type'>fs</span> <span class='hlb-name'>stopSignal</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>signal</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>signal</span>"
@@ -851,6 +863,18 @@ Attempt to lex the single-argument shell command provided to &#x60;localRun&#x60
 to determine if a &#x60;/bin/sh -c &#x27;...&#x27;&#x60; wrapper needs to be added.
 
 
+### <span class='hlb-type'>string</span> <span class='hlb-name'>state</span>()
+
+
+A reference to the current string state.
+
+	#!hlb
+	string myString() {
+		state
+	}
+
+
+
 ### <span class='hlb-type'>string</span> <span class='hlb-name'>template</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>text</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>text</span>"
diff --git a/gen/builtins.go b/gen/builtins.go
index 7d113ec..efe9f5a 100644
--- a/gen/builtins.go
+++ b/gen/builtins.go
@@ -22,8 +22,9 @@ type BuiltinData struct {
 }
 
 type ParsedFunc struct {
-	Name   string
-	Params []*parser.Field
+	Name    string
+	Params  []*parser.Field
+	Effects []*parser.Field
 }
 
 func GenerateBuiltins(r io.Reader) ([]byte, error) {
@@ -39,10 +40,16 @@ func GenerateBuiltins(r io.Reader) ([]byte, error) {
 			continue
 		}
 
+		var effects []*parser.Field
+		if fun.SideEffects != nil && fun.SideEffects.Effects != nil {
+			effects = fun.SideEffects.Effects.List
+		}
+
 		typ := fun.Type.ObjType
 		funcsByType[typ] = append(funcsByType[typ], ParsedFunc{
-			Name:   fun.Name.Name,
-			Params: fun.Params.List,
+			Name:    fun.Name.Name,
+			Params:  fun.Params.List,
+			Effects: effects,
 		})
 	}
 
@@ -101,6 +108,7 @@ type LookupByType struct {
 
 type FuncLookup struct {
 	Params []*parser.Field
+	Effects []*parser.Field
 }
 
 var (
@@ -109,10 +117,14 @@ var (
 			{{range $typ, $funcs := .FuncsByType}}{{objType $typ}}: LookupByType{
 				Func: map[string]FuncLookup{
 					{{range $i, $func := $funcs}}"{{$func.Name}}": FuncLookup{
-						Params: []*parser.Field{
+						Params:  []*parser.Field{
 							{{range $i, $param := $func.Params}}parser.NewField({{objType $param.Type.ObjType}}, "{{$param.Name}}", {{if $param.Variadic}}true{{else}}false{{end}}),
 							{{end}}
 						},
+						Effects: []*parser.Field{
+							{{range $i, $effect := $func.Effects}}parser.NewField({{objType $effect.Type.ObjType}}, "{{$effect.Name}}", {{if $effect.Variadic}}true{{else}}false{{end}}),
+							{{end}}
+						},
 					},
 					{{end}}
 				},
diff --git a/go.mod b/go.mod
index 071a51f..e1df759 100644
--- a/go.mod
+++ b/go.mod
@@ -46,4 +46,4 @@ replace k8s.io/client-go => k8s.io/client-go v0.0.0-20180806134042-1f13a808da65
 // necessary for langserver with vscode
 replace github.com/sourcegraph/go-lsp => github.com/radeksimko/go-lsp v0.0.0-20200223162147-9f2c54f29c9f
 
-replace github.com/tonistiigi/fsutil => github.com/coryb/fsutil v0.0.0-20200508061958-7d16a3dcbd1d
+replace github.com/tonistiigi/fsutil => github.com/slushie/fsutil v0.0.0-20200508061958-7d16a3dcbd1d
diff --git a/go.sum b/go.sum
index 7a33459..ea6d8b4 100644
--- a/go.sum
+++ b/go.sum
@@ -77,8 +77,10 @@ github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kw
 github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=
 github.com/containerd/typeurl v0.0.0-20200205145503-b45ef1f1f737/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
 github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
@@ -327,6 +329,8 @@ github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvH
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/slushie/fsutil v0.0.0-20200508061958-7d16a3dcbd1d h1:+rqYXBNuULpf/QXGB3UemY6z+lITC6SVaonrkX6qqsQ=
+github.com/slushie/fsutil v0.0.0-20200508061958-7d16a3dcbd1d/go.mod h1:0G1sLZ/0ttFf09xvh7GR4AEECnjifHRNJN/sYbLianU=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -354,6 +358,9 @@ github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG
 github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
 github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
+github.com/tonistiigi/fsutil v0.0.0-20200225063759-013a9fe6aee2/go.mod h1:0G1sLZ/0ttFf09xvh7GR4AEECnjifHRNJN/sYbLianU=
+github.com/tonistiigi/fsutil v0.0.0-20200326231323-c2c7d7b0e144 h1:6RY1EKxCnPQShPM46xFDHta2JSOd+YKCgHyyBHtKuo8=
+github.com/tonistiigi/fsutil v0.0.0-20200326231323-c2c7d7b0e144/go.mod h1:0G1sLZ/0ttFf09xvh7GR4AEECnjifHRNJN/sYbLianU=
 github.com/tonistiigi/go-immutable-radix v0.0.0-20170803185627-826af9ccf0fe/go.mod h1:/+MCh11CJf2oz0BXmlmqyopK/ad1rKkcOXPoYuPCJYU=
 github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
 github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
@@ -361,6 +368,7 @@ github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305 h1:y/1cL5AL2oRcfz
 github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305/go.mod h1:gXOLibKqQTRAVuVZ9gX7G9Ykky8ll8yb4slxsEMoY0c=
 github.com/uber/jaeger-client-go v0.0.0-20180103221425-e02c85f9069e/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
 github.com/uber/jaeger-lib v1.2.1/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
diff --git a/langserver/server.go b/langserver/server.go
index 805bdd3..a683248 100644
--- a/langserver/server.go
+++ b/langserver/server.go
@@ -240,6 +240,19 @@ func highlightModule(lines map[int]lsp.SemanticHighlightingTokens, mod *parser.M
 					}
 				}
 			}
+			if n.SideEffects != nil {
+				if n.SideEffects.As != nil {
+					highlightNode(lines, n.SideEffects.As, Keyword)
+				}
+				for _, field := range n.SideEffects.Effects.List {
+					if field.Type != nil {
+						highlightNode(lines, field.Type, Type)
+					}
+					if field.Name != nil {
+						highlightNode(lines, field.Name, Parameter)
+					}
+				}
+			}
 			if n.Type != nil && n.Body != nil {
 				highlightBlock(lines, n.Type.ObjType, n.Body)
 			}
@@ -335,12 +348,18 @@ func highlightBlock(lines map[int]lsp.SemanticHighlightingTokens, typ parser.Obj
 				}
 			}
 
-			if n.Alias != nil {
-				if n.Alias.As != nil {
-					highlightNode(lines, n.Alias.As, Keyword)
+			if n.Binds != nil {
+				if n.Binds.As != nil {
+					highlightNode(lines, n.Binds.As, Keyword)
 				}
-				if n.Alias.Ident != nil {
-					highlightNode(lines, n.Alias.Ident, Function)
+				if n.Binds.Ident != nil {
+					highlightNode(lines, n.Binds.Ident, Function)
+				}
+				if n.Binds.List != nil {
+					for _, b := range n.Binds.List.List {
+						highlightNode(lines, b.Source, Parameter)
+						highlightNode(lines, b.Target, Function)
+					}
 				}
 			}
 
@@ -598,8 +617,21 @@ func newLocationFromIdent(scope *parser.Scope, uri lsp.DocumentURI, name string)
 		switch n := obj.Node.(type) {
 		case *parser.FuncDecl:
 			loc = newLocationFromNode(uri, n.Name)
-		case *parser.AliasDecl:
-			loc = newLocationFromNode(uri, n.Ident)
+		case *parser.BindClause:
+			if n.Ident != nil {
+				loc = newLocationFromNode(uri, n.Ident)
+			}
+			if n.List != nil {
+				for _, b := range n.List.List {
+					if b.Target.String() == name {
+						loc = newLocationFromNode(uri, b.Target)
+						break
+					}
+				}
+				if loc == nil {
+					loc = newLocationFromNode(uri, n)
+				}
+			}
 		case *parser.ImportDecl:
 			loc = newLocationFromNode(uri, n.Ident)
 		default:
diff --git a/language/builtin.hlb b/language/builtin.hlb
index 7584f0e..97538d5 100644
--- a/language/builtin.hlb
+++ b/language/builtin.hlb
@@ -3,6 +3,11 @@
 # @return a scratch filesystem.
 fs scratch()
 
+# A reference to the current filesystem state.
+#
+# @return the current fs state unchanged
+fs state() as (fs current)
+
 # An OCI image's filesystem.
 #
 # @param ref a docker registry reference. if not fully qualified, it will be 
@@ -215,7 +220,7 @@ option::run secret(string localPath, string mountPoint)
 # becomes available from the mountPoint directory.
 # @param mountPoint the directory where the mount is attached.
 # @param an option to mount an additional filesystem.
-option::run mount(fs input, string mountPoint)
+option::run mount(fs input, string mountPoint) as (fs target)
 
 # Sets the target directory to mount the SSH agent socket. By default, it is
 # mounted to `/run/buildkit/ssh_agent.${N}`, where N is the index of the 
@@ -469,7 +474,7 @@ option::copy createdTime(string created)
 # @param ref a distribution reference. if not fully qualified, it will be
 # expanded the same as the docker CLI.
 # @return an option to push the filesystem to a registry.
-fs dockerPush(string ref)
+fs dockerPush(string ref) as (string digest)
 
 # Loads the filesystem as a Docker image to the docker client found in your
 # environment.
@@ -560,6 +565,11 @@ fs volumes(variadic string mountpoints)
 # @return the filesystem with the stop signal set.
 fs stopSignal(string signal)
 
+# A reference to the current string state.
+#
+# @return the current string state unchanged
+string state() as (string current)
+
 # A format specifier that is interpolated with values.
 #
 # @param formatString the format specifier.
diff --git a/mkdocs.hlb b/mkdocs.hlb
index 8d33bba..2c1d19b 100644
--- a/mkdocs.hlb
+++ b/mkdocs.hlb
@@ -27,7 +27,7 @@ fs _runMkdocsBuild() {
 			local "." with option {
 				includePatterns "mkdocs.yml" "docs/" ".git"
 			}
-		} "/mkdocs" as readonly
+		} "/mkdocs" with readonly
 		mount generatedMarkdown "/mkdocs/docs/reference.md" with option {
 			sourcePath "reference.md"
 			readonly
diff --git a/parser/cst.go b/parser/cst.go
index 5791c83..7e9f16e 100644
--- a/parser/cst.go
+++ b/parser/cst.go
@@ -250,21 +250,23 @@ func (e *Export) End() lexer.Position      { return shiftPosition(e.Pos, len(e.K
 
 // FuncDecl represents a function declaration.
 type FuncDecl struct {
-	Pos    lexer.Position
-	Scope  *Scope
-	Doc    *CommentGroup
-	Type   *Type      `parser:"@@"`
-	Name   *Ident     `parser:"@@"`
-	Params *FieldList `parser:"@@"`
-	Body   *BlockStmt `parser:"( @@ )?"`
+	Pos         lexer.Position
+	Scope       *Scope
+	Doc         *CommentGroup
+	Type        *Type          `parser:"@@"`
+	Name        *Ident         `parser:"@@"`
+	Params      *FieldList     `parser:"@@"`
+	SideEffects *EffectsClause `parser:"( @@ )?"`
+	Body        *BlockStmt     `parser:"( @@ )?"`
 }
 
-func NewFuncDecl(typ ObjType, name string, params []*Field, stmts ...*Stmt) *Decl {
+func NewFuncDecl(typ ObjType, name string, params []*Field, effects []*Field, stmts ...*Stmt) *Decl {
 	fun := &FuncDecl{
-		Type:   NewType(typ),
-		Name:   NewIdent(name),
-		Params: NewFieldList(params...),
-		Body:   NewBlockStmt(stmts...),
+		Type:        NewType(typ),
+		Name:        NewIdent(name),
+		Params:      NewFieldList(params...),
+		Body:        NewBlockStmt(stmts...),
+		SideEffects: NewEffectsClause(effects...),
 	}
 
 	return &Decl{Func: fun}
@@ -273,6 +275,37 @@ func NewFuncDecl(typ ObjType, name string, params []*Field, stmts ...*Stmt) *Dec
 func (d *FuncDecl) Position() lexer.Position { return d.Pos }
 func (d *FuncDecl) End() lexer.Position      { return d.Body.CloseBrace.End() }
 
+func (d *FuncDecl) ObjType() ObjType {
+	return d.Type.ObjType
+}
+
+func (d *FuncDecl) List() []*Stmt {
+	return d.Body.NonEmptyStmts()
+}
+
+// Block represents a group of statements of a specific type.
+type Block interface {
+	ObjType() ObjType
+	List() []*Stmt
+}
+
+// EffectsClause represents the side effect "as ..." clause for a function.
+type EffectsClause struct {
+	Pos     lexer.Position
+	As      *As        `parser:"@@"`
+	Effects *FieldList `parser:"@@"`
+}
+
+func NewEffectsClause(effect ...*Field) *EffectsClause {
+	return &EffectsClause{
+		As:      &As{Keyword: "as"},
+		Effects: NewFieldList(effect...),
+	}
+}
+
+func (e *EffectsClause) Position() lexer.Position { return e.Pos }
+func (e *EffectsClause) End() lexer.Position      { return e.Effects.End() }
+
 // FieldList represents a list of Fields, enclosed by parentheses.
 type FieldList struct {
 	Pos        lexer.Position
@@ -741,20 +774,21 @@ func (s *Stmt) End() lexer.Position {
 type CallStmt struct {
 	Pos     lexer.Position
 	Doc     *CommentGroup
-	Func    *Expr      `parser:"@@"`
-	Args    []*Expr    `parser:"( @@ )*"`
-	WithOpt *WithOpt   `parser:"( @@ )?"`
-	Alias   *AliasDecl `parser:"( @@ )?"`
-	StmtEnd *StmtEnd   `parser:"@@"`
+	Func    *Expr       `parser:"@@"`
+	Args    []*Expr     `parser:"( @@ )*"`
+	WithOpt *WithOpt    `parser:"( @@ )?"`
+	Binds   *BindClause `parser:"( @@ )?"`
+	StmtEnd *StmtEnd    `parser:"@@"`
+	Callee  *FuncDecl
 }
 
-func NewCallStmt(name string, args []*Expr, withOpt *WithOpt, alias *AliasDecl) *Stmt {
+func NewCallStmt(name string, args []*Expr, withOpt *WithOpt, binds *BindClause) *Stmt {
 	return &Stmt{
 		Call: &CallStmt{
 			Func:    NewIdentExpr(name),
 			Args:    args,
 			WithOpt: withOpt,
-			Alias:   alias,
+			Binds:   binds,
 		},
 	}
 }
@@ -762,6 +796,21 @@ func NewCallStmt(name string, args []*Expr, withOpt *WithOpt, alias *AliasDecl)
 func (s *CallStmt) Position() lexer.Position { return s.Pos }
 func (s *CallStmt) End() lexer.Position      { return s.StmtEnd.End() }
 
+func (s *CallStmt) ObjType() ObjType {
+	if s.WithOpt == nil || s.WithOpt.Expr == nil || s.WithOpt.Expr.FuncLit == nil {
+		return None
+	}
+
+	return ObjType(fmt.Sprintf("%s::%s", Option, s.Func.Name()))
+}
+
+func (s *CallStmt) List() []*Stmt {
+	if s.WithOpt == nil || s.WithOpt.Expr == nil || s.WithOpt.Expr.FuncLit == nil {
+		return nil
+	}
+	return s.WithOpt.Expr.FuncLit.Body.NonEmptyStmts()
+}
+
 // WithOpt represents optional arguments for a CallStmt.
 type WithOpt struct {
 	Pos  lexer.Position
@@ -795,17 +844,88 @@ type With struct {
 func (w *With) Position() lexer.Position { return w.Pos }
 func (w *With) End() lexer.Position      { return shiftPosition(w.Pos, len(w.Keyword), 0) }
 
-// AliasDecl represents a declaration of an alias for a CallStmt.
-type AliasDecl struct {
-	Pos   lexer.Position
-	As    *As    `parser:"@@"`
-	Ident *Ident `parser:"@@"`
-	Func  *FuncDecl
-	Call  *CallStmt
+// Binding is a value type that represents the call site where a single side effect is bound.
+type Binding struct {
+	Bind  *BindClause
+	Field *Field
+}
+
+// BindClause represents the entire "as ..." clause on a CallStmt, with either a
+// default side effect or a list of Binds.
+type BindClause struct {
+	Pos     lexer.Position
+	As      *As       `parser:"@@"`
+	Ident   *Ident    `parser:"( @@"`
+	List    *BindList `parser:"| @@ )?"`
+	Lexical *FuncDecl
+	Effects *FieldList
+}
+
+func (b *BindClause) Bindings() []Binding {
+	var bs []Binding
+	for _, bb := range b.Effects.List {
+		bs = append(bs, Binding{b, bb})
+	}
+	return bs
+}
+
+func (b *BindClause) SourceBinding(source string) Binding {
+	for _, bb := range b.Effects.List {
+		if bb.Name.String() == source {
+			return Binding{b, bb}
+		}
+	}
+	panic("no such source")
+}
+
+func (b *BindClause) TargetBinding(target string) Binding {
+	if b.Ident != nil || target == "" {
+		// The default bind is the first.
+		return Binding{b, b.Effects.List[0]}
+	}
+	if b.List != nil {
+		for _, bb := range b.List.List {
+			if bb.Target.Name == target {
+				return Binding{b, bb.Field}
+			}
+		}
+	}
+	panic("no such target")
+}
+
+func (b *BindClause) Position() lexer.Position { return b.Pos }
+func (b *BindClause) End() lexer.Position {
+	switch {
+	case b.Ident != nil:
+		return b.Ident.End()
+	case b.List != nil:
+		return b.List.End()
+	default:
+		panic("missing ident or list")
+	}
+}
+
+// BindList is a parenthetical list of Binds.
+type BindList struct {
+	Pos        lexer.Position
+	OpenParen  *OpenParen  `parser:"@@"`
+	List       []*Bind     `parser:"( ( Newline )? @@ ( \",\" ( Newline )?  @@ )* ( \",\" Newline )? )?"`
+	CloseParen *CloseParen `parser:"@@"`
+}
+
+func (b *BindList) Position() lexer.Position { return b.Pos }
+func (b *BindList) End() lexer.Position      { return b.CloseParen.End() }
+
+// Bind represents the binding of a CallStmt's side effect Source to the identifier Target.
+type Bind struct {
+	Pos    lexer.Position
+	Source *Ident `parser:"@@"`
+	Target *Ident `parser:"@@"`
+	Field  *Field
 }
 
-func (d *AliasDecl) Position() lexer.Position { return d.Pos }
-func (d *AliasDecl) End() lexer.Position      { return d.Ident.End() }
+func (b *Bind) Position() lexer.Position { return b.Pos }
+func (b *Bind) End() lexer.Position      { return b.Target.End() }
 
 // As represents the keyword "as".
 type As struct {
diff --git a/parser/unparse.go b/parser/unparse.go
index af34ed5..7b5558c 100644
--- a/parser/unparse.go
+++ b/parser/unparse.go
@@ -126,12 +126,21 @@ func (d *FuncDecl) String() string {
 		params = d.Params.String()
 	}
 
+	effects := ""
+	if d.SideEffects != nil {
+		effects = fmt.Sprintf(" %s", d.SideEffects)
+	}
+
 	body := ""
 	if d.Body != nil {
 		body = fmt.Sprintf(" %s", d.Body)
 	}
 
-	return fmt.Sprintf("%s %s%s%s", d.Type, d.Name, params, body)
+	return fmt.Sprintf("%s %s%s%s%s", d.Type, d.Name, params, effects, body)
+}
+
+func (e *EffectsClause) String() string {
+	return fmt.Sprintf("%s %s", e.As, e.Effects)
 }
 
 func (f *FieldList) String() string {
@@ -258,9 +267,9 @@ func (s *CallStmt) String() string {
 		withOpt = fmt.Sprintf(" %s", s.WithOpt)
 	}
 
-	alias := ""
-	if s.Alias != nil {
-		alias = fmt.Sprintf(" %s", s.Alias)
+	binds := ""
+	if s.Binds != nil {
+		binds = fmt.Sprintf(" %s", s.Binds)
 	}
 
 	end := ""
@@ -272,11 +281,30 @@ func (s *CallStmt) String() string {
 		}
 	}
 
-	return fmt.Sprintf("%s%s%s%s%s", s.Func, args, withOpt, alias, end)
+	return fmt.Sprintf("%s%s%s%s%s", s.Func, args, withOpt, binds, end)
+}
+
+func (b *BindClause) String() string {
+	switch {
+	case b.Ident != nil:
+		return fmt.Sprintf("%s %s", b.As, b.Ident)
+	case b.List != nil:
+		return fmt.Sprintf("%s %s", b.As, b.List)
+	default:
+		return fmt.Sprintf("%s _", b.As)
+	}
+}
+
+func (b *BindList) String() string {
+	var binds []string
+	for _, bind := range b.List {
+		binds = append(binds, bind.String())
+	}
+	return fmt.Sprintf("(%s)", strings.Join(binds, ", "))
 }
 
-func (d *AliasDecl) String() string {
-	return fmt.Sprintf("%s %s", d.As, d.Ident)
+func (b *Bind) String() string {
+	return fmt.Sprintf("%s %s", b.Source, b.Target)
 }
 
 func (a *As) String() string {
diff --git a/parser/walk.go b/parser/walk.go
index 3f4a871..cb066a0 100644
--- a/parser/walk.go
+++ b/parser/walk.go
@@ -63,16 +63,12 @@ func Walk(node Node, v Visitor) {
 		if n.Params != nil {
 			Walk(n.Params, v)
 		}
+		if n.SideEffects != nil {
+			Walk(n.SideEffects, v)
+		}
 		if n.Body != nil {
 			Walk(n.Body, v)
 		}
-	case *AliasDecl:
-		if n.As != nil {
-			Walk(n.As, v)
-		}
-		if n.Ident != nil {
-			Walk(n.Ident, v)
-		}
 	case *FieldList:
 		walkFieldList(n.List, v)
 	case *Field:
@@ -127,8 +123,8 @@ func Walk(node Node, v Visitor) {
 			Walk(n.Func, v)
 		}
 		walkExprList(n.Args, v)
-		if n.Alias != nil {
-			Walk(n.Alias, v)
+		if n.Binds != nil {
+			Walk(n.Binds, v)
 		}
 		if n.WithOpt != nil {
 			Walk(n.WithOpt, v)
@@ -138,6 +134,17 @@ func Walk(node Node, v Visitor) {
 				Walk(n.StmtEnd.Comment, v)
 			}
 		}
+	case *BindClause:
+		if n.As != nil {
+			Walk(n.As, v)
+		}
+		if n.Ident != nil {
+			Walk(n.Ident, v)
+		}
+		if n.List != nil {
+			Walk(n.List, v)
+		}
+
 	case *WithOpt:
 		if n.With != nil {
 			Walk(n.With, v)
diff --git a/solver/solve.go b/solver/solve.go
index 9b3b85f..00abb67 100644
--- a/solver/solve.go
+++ b/solver/solve.go
@@ -17,13 +17,15 @@ import (
 
 type SolveOption func(*SolveInfo) error
 
+type SolveCallback func(resp *client.SolveResponse) error
+
 type SolveInfo struct {
 	OutputDockerRef       string
 	OutputPushImage       string
 	OutputLocal           string
 	OutputLocalTarball    bool
 	OutputLocalOCITarball bool
-	Callbacks             []func() error `json:"-"`
+	Callbacks             []SolveCallback `json:"-"`
 	ImageSpec             *specs.Image
 	Entitlements          []entitlements.Entitlement
 }
@@ -63,7 +65,7 @@ func WithDownloadOCITarball() SolveOption {
 	}
 }
 
-func WithCallback(fn func() error) SolveOption {
+func WithCallback(fn SolveCallback) SolveOption {
 	return func(info *SolveInfo) error {
 		info.Callbacks = append(info.Callbacks, fn)
 		return nil
@@ -168,14 +170,17 @@ func Build(ctx context.Context, c *client.Client, s *session.Session, pw progres
 
 	g, ctx := errgroup.WithContext(ctx)
 
-	var statusCh chan *client.SolveStatus
+	var (
+		statusCh chan *client.SolveStatus
+		resp     *client.SolveResponse
+	)
 	if pw != nil {
 		pw = progress.ResetTime(pw)
 		statusCh = pw.Status()
 	}
 
-	g.Go(func() error {
-		_, err := c.Build(ctx, solveOpt, "", f, statusCh)
+	g.Go(func() (err error) {
+		resp, err = c.Build(ctx, solveOpt, "", f, statusCh)
 		return err
 	})
 
@@ -188,7 +193,9 @@ func Build(ctx context.Context, c *client.Client, s *session.Session, pw progres
 
 	for _, fn := range info.Callbacks {
 		fn := fn
-		g.Go(fn)
+		g.Go(func() error {
+			return fn(resp)
+		})
 	}
 
 	return g.Wait()


Test patch:
diff --git a/checker/checker_test.go b/checker/checker_test.go
index a6c6862..568f9f8 100644
--- a/checker/checker_test.go
+++ b/checker/checker_test.go
@@ -119,7 +119,9 @@ func TestChecker_Check(t *testing.T) {
 		`
 		fs default() {
 			scratch
-			mkfile "/foo" 0o644 "foo" as this
+			run "cmd" with option {
+				mount scratch "/" as this
+			}
 			copy this "/foo" "/bar"
 		}
 		`,
@@ -133,21 +135,6 @@ func TestChecker_Check(t *testing.T) {
 		}
 		`,
 		nil,
-	}, {
-		"compose fs",
-		`
-		fs default() {
-			image "alpine" as this
-			myfunc this
-			image "busybox"
-		}
-		fs myfunc(fs base) {
-			base
-			mkfile "/foo" 0o644 "contents"
-			run "echo hi"
-		}
-		`,
-		nil,
 	}, {
 		"empty variadic options",
 		`
@@ -224,6 +211,42 @@ func TestChecker_Check(t *testing.T) {
 				Name: "duplicateName",
 			}},
 		},
+	}, {
+		"errors with function and builtin name collisions",
+		`
+		fs image() {
+			run "echo Hello"
+		}
+		`,
+		ErrDuplicateDecls{
+			Idents: []*parser.Ident{{
+				Pos: lexer.Position{
+					Filename: "<stdin>",
+					Line:     1,
+					Column:   4,
+				},
+				Name: "image",
+			}},
+		},
+	}, {
+		"errors with alias and builtin name collisions",
+		`
+		fs default() {
+			run "echo Hello" with option {
+				mount scratch "/" as image
+			}
+		}
+		`,
+		ErrDuplicateDecls{
+			Idents: []*parser.Ident{{
+				Pos: lexer.Position{
+					Filename: "<stdin>",
+					Line:     3,
+					Column:   23,
+				},
+				Name: "image",
+			}},
+		},
 	}, {
 		"errors with duplicate alias names",
 		`
@@ -342,21 +365,22 @@ func TestChecker_Check(t *testing.T) {
 			image "alpine"
 		}
 		`,
-		ErrIdentNotDefined{
-			Ident: &parser.Ident{
-				Pos: lexer.Position{
-					Filename: "<stdin>",
-					Line:     2,
-					Column:   1,
-				},
-				Name: "image",
+		ErrWrongBuiltinType{
+			Pos: lexer.Position{
+				Filename: "<stdin>",
+				Line:     2,
+				Column:   1,
+			},
+			Expected: "group",
+			Builtin: &BuiltinDecl{
+				Ident: parser.NewIdent("image"),
 			},
 		},
 	}, {
 		"errors when non-zero arg builtin is used as arg",
 		`
 		fs default() {
-			env localEnv "TEST"
+			run localEnv
 		}
 		`,
 		ErrFuncArg{
@@ -390,6 +414,100 @@ func TestChecker_Check(t *testing.T) {
 			},
 			Lexeme: "0644",
 		},
+	}, {
+		"errors without bind target",
+		`
+		fs default() {
+			dockerPush "some/ref" as
+		}
+		`,
+		ErrBindNoTarget{
+			Pos: lexer.Position{
+				Filename: "<stdin>",
+				Line:     2,
+				Column:   23,
+			},
+		},
+	}, {
+		"no error when bind list is empty",
+		`
+		fs default() {
+			dockerPush "some/ref" as ()
+		}
+		`,
+		nil,
+	}, {
+		"errors with wrong type for default bind",
+		`
+		fs default() {
+			dockerPush "some/ref:latest" as imageID
+			imageID
+		}
+		`,
+		ErrWrongArgType{
+			Pos: lexer.Position{
+				Filename: "<stdin>",
+				Line:     3,
+				Column:   1,
+			},
+			Expected: "fs",
+			Found:    "string",
+		},
+	}, {
+		"errors with wrong type for named bind",
+		`
+		fs default() {
+			dockerPush "some/ref:latest" as (digest imageID)
+			imageID
+		}
+		`,
+		ErrWrongArgType{
+			Pos: lexer.Position{
+				Filename: "<stdin>",
+				Line:     3,
+				Column:   1,
+			},
+			Expected: "fs",
+			Found:    "string",
+		},
+	}, {
+		"errors when binding without side effects",
+		`
+		fs default() {
+			run "cmd" as nothing
+		}
+		`,
+		ErrBindBadSource{
+			CallStmt: &parser.CallStmt{
+				Pos: lexer.Position{
+					Filename: "<stdin>",
+					Line:     2,
+					Column:   1,
+				},
+				Func: parser.NewIdentExpr("run"),
+			},
+		},
+	}, {
+		"errors when binding unknown side effects",
+		`
+		fs default() {
+			dockerPush "some/ref:latest" as (badSource nothing)
+		}
+		`,
+		ErrBindBadTarget{
+			CallStmt: &parser.CallStmt{
+				Func: parser.NewIdentExpr("dockerPush"),
+			},
+			Bind: &parser.Bind{
+				Pos: lexer.Position{
+					Filename: "<stdin>",
+					Line:     2,
+					Column:   34,
+				},
+				Source: parser.NewIdent("badSource"),
+				Target: parser.NewIdent("nothing"),
+			},
+		},
 	}} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
@@ -399,7 +517,7 @@ func TestChecker_Check(t *testing.T) {
 			require.NoError(t, err)
 
 			err = Check(mod)
-			validateError(t, tc.errType, err)
+			validateError(t, tc.errType, err, tc.name)
 		})
 	}
 }
@@ -513,15 +631,18 @@ func TestChecker_CheckSelectors(t *testing.T) {
 			obj.Data = importedModule.Scope
 
 			err = CheckSelectors(module)
-			validateError(t, tc.errType, err)
+			validateError(t, tc.errType, err, tc.name)
 		})
 	}
 }
 
-func validateError(t *testing.T, expected error, actual error) {
-	if expected == nil {
-		require.NoError(t, actual)
-	} else {
-		require.Equal(t, expected.Error(), actual.Error())
+func validateError(t *testing.T, expected error, actual error, name string) {
+	switch {
+	case expected == nil:
+		require.NoError(t, actual, name)
+	case actual == nil:
+		require.NotNil(t, actual, name)
+	default:
+		require.Equal(t, expected.Error(), actual.Error(), name)
 	}
 }
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 5e27d5a..40c699a 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -278,8 +278,8 @@ func TestCodeGen(t *testing.T) {
 		[]string{"default"},
 		`
 		fs default() {
-			scratch as this
-			copy this "testSource" "testDest"
+			scratch
+			copy scratch "testSource" "testDest"
 		}
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
@@ -291,8 +291,8 @@ func TestCodeGen(t *testing.T) {
 		[]string{"default"},
 		`
 		fs default() {
-			scratch as this
-			copy this "testSource" "testDest" with option {
+			scratch
+			copy scratch "testSource" "testDest" with option {
 				followSymlinks
 				contentsOnly
 				unpack
@@ -579,7 +579,7 @@ func TestCodeGen(t *testing.T) {
 		"templates",
 		[]string{"default"},
 		`
-		string cmd() {
+		string command() {
 			template <<-EOM
 				echo hi {{.user}}
 			EOM with option {
@@ -591,7 +591,7 @@ func TestCodeGen(t *testing.T) {
 
 		fs default() {
 			image "busybox"
-			run cmd with shlex
+			run command with shlex
 		}
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
@@ -791,13 +791,13 @@ func TestCodeGen(t *testing.T) {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			cg, err := New()
-			require.NoError(t, err)
+			require.NoError(t, err, tc.name)
 
 			mod, err := parser.Parse(strings.NewReader(cleanup(tc.input)))
-			require.NoError(t, err)
+			require.NoError(t, err, tc.name)
 
 			err = checker.Check(mod)
-			require.NoError(t, err)
+			require.NoError(t, err, tc.name)
 
 			var targets []Target
 			for _, target := range tc.targets {
@@ -805,20 +805,22 @@ func TestCodeGen(t *testing.T) {
 			}
 
 			request, err := cg.Generate(context.Background(), mod, targets)
-			require.NoError(t, err)
+			require.NoError(t, err, tc.name)
 
 			testRequest := tc.fn(t, cg)
 
 			expected := treeprint.New()
-			testRequest.Tree(expected)
+			err = testRequest.Tree(expected)
+			require.NoError(t, err, tc.name)
 			t.Logf("expected: %s", expected)
 
 			actual := treeprint.New()
-			request.Tree(actual)
+			err = request.Tree(actual)
+			require.NoError(t, err, tc.name)
 			t.Logf("actual: %s", actual)
 
 			// Compare trees.
-			require.Equal(t, expected.String(), actual.String())
+			require.Equal(t, expected.String(), actual.String(), tc.name)
 		})
 	}
 }