Repository: openllb/hlb
Pull request #102 (2020-04-17 01:28:54)

Refactor codegen to allow calling functions without arguments as an expression
- Refactored `EmitChainXYZ` functions to `codegen/chain.go`
- Refactored `EmitChainXYZ` to depend on `expr *parser.Expr, args []*parser.Expr` instead of `call *parser.CallStmt` (So that `EmitXYZExpr` functions can call `EmitChainXYZ`. Reducing code duplication)
- Removed `call *parser.CallStmt` from `EmitStringExpr`, turns out it was unused.
- Made chain functions consistent (before: `StateOption`, `GroupChain`, `func(string) (string, error)`) (after: `FilesystemChain`, `GroupChain`, `StringChain`)

Now you can call builtin functions that have zero args directly:

```hlb
fs default() {
    image "alpine"
    run "touch /out/foo" with option {
        mount scratch "/out" as fooOut
    }
}
```


Patch:
diff --git a/checker/checker.go b/checker/checker.go
index c2132cd..1cce89c 100644
--- a/checker/checker.go
+++ b/checker/checker.go
@@ -445,15 +445,7 @@ func (c *checker) checkCallArgs(scope *parser.Scope, call *parser.CallStmt, para
 		// Inherit the secondary type from the calling function name.
 		optionType := parser.ObjType(fmt.Sprintf("%s::%s", parser.Option, name))
 
-		var err error
-		switch {
-		case call.WithOpt.Ident != nil:
-			err = c.checkIdentArg(scope, optionType, call.WithOpt.Ident)
-		case call.WithOpt.FuncLit != nil:
-			err = c.checkFuncLitArg(scope, optionType, call.WithOpt.FuncLit)
-		default:
-			panic("unknown with opt type")
-		}
+		err := c.checkExpr(scope, optionType, call.WithOpt.Expr)
 		if err != nil {
 			return err
 		}
@@ -482,6 +474,11 @@ 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 {
+	_, ok := builtin.Lookup.ByType[typ].Func[ident.Name]
+	if ok {
+		return nil
+	}
+
 	obj := scope.Lookup(ident.Name)
 	if obj == nil {
 		return ErrIdentNotDefined{ident}
diff --git a/cmd/hlb/command/run.go b/cmd/hlb/command/run.go
index c80ad38..286fe25 100644
--- a/cmd/hlb/command/run.go
+++ b/cmd/hlb/command/run.go
@@ -157,7 +157,7 @@ func Run(ctx context.Context, cln *client.Client, rc io.ReadCloser, opts RunOpti
 		return err
 	}
 
-	if opts.Debug || opts.Tree {
+	if solveReq == nil || opts.Debug || opts.Tree {
 		p.Release()
 		err = p.Wait()
 		if err != nil {
diff --git a/codegen/chain.go b/codegen/chain.go
new file mode 100644
index 0000000..694daa7
--- /dev/null
+++ b/codegen/chain.go
@@ -0,0 +1,735 @@
+package codegen
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	shellquote "github.com/kballard/go-shellquote"
+	"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/parser"
+	"github.com/openllb/hlb/solver"
+	"github.com/pkg/errors"
+	fstypes "github.com/tonistiigi/fsutil/types"
+	"golang.org/x/sync/errgroup"
+)
+
+type FilesystemChain func(llb.State) (llb.State, error)
+
+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) {
+	switch typ {
+	case parser.Filesystem:
+		chain, err := cg.EmitFilesystemChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, ac, chainStart)
+		if err != nil {
+			return nil, err
+		}
+		return func(v interface{}) (interface{}, error) {
+			return chain(v.(llb.State))
+		}, nil
+	case parser.Str:
+		chain, err := cg.EmitStringChainStmt(ctx, scope, call.Func, call.Args, chainStart)
+		if err != nil {
+			return nil, err
+		}
+		return func(v interface{}) (interface{}, error) {
+			return chain(v.(string))
+		}, nil
+	case parser.Group:
+		chain, err := cg.EmitGroupChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, ac, chainStart)
+		if err != nil {
+			return nil, err
+		}
+		return func(v interface{}) (interface{}, error) {
+			return chain(v.([]solver.Request))
+		}, nil
+	default:
+		return nil, errors.WithStack(ErrCodeGen{call, errors.Errorf("unknown chain stmt")})
+	}
+}
+
+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)
+	if err != nil {
+		return fc, err
+	}
+
+	if fc == nil {
+		// Must be a named reference.
+		obj := scope.Lookup(expr.Name())
+		if obj == nil {
+			return fc, errors.WithStack(ErrCodeGen{expr, errors.Errorf("could not find reference")})
+		}
+
+		var v interface{}
+		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)
+		case *parser.ImportDecl:
+			importScope := obj.Data.(*parser.Scope)
+			importObj := importScope.Lookup(expr.Selector.Select.Name)
+			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)
+			default:
+				return fc, errors.WithStack(ErrCodeGen{m, errors.Errorf("unknown obj type")})
+			}
+		case *parser.Field:
+			v = obj.Data
+		default:
+			return fc, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
+		}
+		if err != nil {
+			return fc, err
+		}
+		fc = func(_ llb.State) (llb.State, error) {
+			return v.(llb.State), nil
+		}
+	}
+
+	return fc, 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) {
+	var iopts []interface{}
+	if with != nil {
+		iopts, err = cg.EmitOptionExpr(ctx, scope, with.Expr, nil, expr.Name())
+		if err != nil {
+			return fc, err
+		}
+	}
+
+	switch expr.Name() {
+	case "scratch":
+		fc = func(_ llb.State) (llb.State, error) {
+			return llb.Scratch(), nil
+		}
+	case "image":
+		ref, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []llb.ImageOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.ImageOption)
+			opts = append(opts, opt)
+		}
+
+		fc = func(_ llb.State) (llb.State, error) {
+			return llb.Image(ref, opts...), nil
+		}
+	case "http":
+		url, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []llb.HTTPOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.HTTPOption)
+			opts = append(opts, opt)
+		}
+
+		fc = func(_ llb.State) (llb.State, error) {
+			return llb.HTTP(url, opts...), nil
+		}
+	case "git":
+		remote, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+		ref, err := cg.EmitStringExpr(ctx, scope, args[1])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []llb.GitOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.GitOption)
+			opts = append(opts, opt)
+		}
+		fc = func(_ llb.State) (llb.State, error) {
+			return llb.Git(remote, ref, opts...), nil
+		}
+	case "local":
+		path, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []llb.LocalOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.LocalOption)
+			opts = append(opts, opt)
+		}
+
+		id, err := cg.LocalID(path, opts...)
+		if err != nil {
+			return fc, err
+		}
+		opts = append(opts, llb.SessionID(cg.sessionID), llb.WithDescription(map[string]string{
+			solver.LocalPathDescriptionKey: fmt.Sprintf("local://%s", path),
+		}))
+
+		// Register paths as syncable directories for the session.
+		cg.syncedDirByID[id] = filesync.SyncedDir{
+			Name: id,
+			Dir:  path,
+			Map: func(_ string, st *fstypes.Stat) bool {
+				st.Uid = 0
+				st.Gid = 0
+				return true
+			},
+		}
+
+		fc = func(_ llb.State) (llb.State, error) {
+			return llb.Local(id, opts...), nil
+		}
+	case "frontend":
+		fcurce, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []gatewayOption
+		for _, iopt := range iopts {
+			opt := iopt.(gatewayOption)
+			opts = append(opts, opt)
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st.Async(func(ctx context.Context, _ llb.State) (llb.State, error) {
+				pw := cg.mw.WithPrefix("", false)
+
+				var st llb.State
+				s, err := cg.newSession(ctx)
+				if err != nil {
+					return st, err
+				}
+
+				g, ctx := errgroup.WithContext(ctx)
+
+				g.Go(func() error {
+					return s.Run(ctx, cg.cln.Dialer())
+				})
+
+				g.Go(func() error {
+					return solver.Build(ctx, cg.cln, s, pw, func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
+						req := gateway.SolveRequest{
+							Frontend: "gateway.v0",
+							FrontendOpt: map[string]string{
+								"fcurce": fcurce,
+							},
+							FrontendInputs: make(map[string]*pb.Definition),
+						}
+
+						for _, opt := range opts {
+							opt(&req)
+						}
+
+						res, err := c.Solve(ctx, req)
+						if err != nil {
+							return res, err
+						}
+
+						ref, err := res.SingleRef()
+						if err != nil {
+							return res, err
+						}
+
+						st, err = ref.ToState()
+						return res, err
+					})
+				})
+
+				return st, g.Wait()
+			}), nil
+		}
+	case "run":
+		var shlex string
+		if len(args) == 1 {
+			commandStr, err := cg.EmitStringExpr(ctx, scope, args[0])
+			if err != nil {
+				return fc, err
+			}
+
+			parts, err := shellquote.Split(commandStr)
+			if err != nil {
+				return fc, err
+			}
+
+			if len(parts) == 1 {
+				shlex = commandStr
+			} else {
+				shlex = shellquote.Join("/bin/sh", "-c", commandStr)
+			}
+		} else {
+			var runArgs []string
+			for _, arg := range args {
+				runArg, err := cg.EmitStringExpr(ctx, scope, arg)
+				if err != nil {
+					return fc, err
+				}
+				runArgs = append(runArgs, runArg)
+			}
+			shlex = shellquote.Join(runArgs...)
+		}
+
+		var opts []llb.RunOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.RunOption)
+			opts = append(opts, opt)
+		}
+
+		var targets []string
+		calls := make(map[string]*parser.CallStmt)
+
+		if with != nil {
+			switch {
+			case with.Expr.Ident != nil:
+				// Do nothing.
+				//
+				// Mounts inside option functions cannot be aliased because they need
+				// 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.Ident.Name != "mount" || stmt.Call.Alias == nil {
+						continue
+					}
+
+					target, err := cg.EmitStringExpr(ctx, scope, stmt.Call.Args[1])
+					if err != nil {
+						return fc, err
+					}
+
+					calls[target] = stmt.Call
+					targets = append(targets, target)
+				}
+			default:
+				return nil, errors.WithStack(ErrCodeGen{with, errors.Errorf("unknown with option")})
+			}
+		}
+
+		opts = append(opts, llb.Shlex(shlex))
+		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
+					}
+				}
+			}
+
+			return exec.Root(), nil
+		}
+	case "env":
+		key, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		value, err := cg.EmitStringExpr(ctx, scope, args[1])
+		if err != nil {
+			return fc, err
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st.AddEnv(key, value), nil
+		}
+	case "dir":
+		path, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st.Dir(path), nil
+		}
+	case "user":
+		name, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st.User(name), nil
+		}
+	case "mkdir":
+		path, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		mode, err := cg.EmitIntExpr(ctx, scope, args[1])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []llb.MkdirOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.MkdirOption)
+			opts = append(opts, opt)
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st.File(
+				llb.Mkdir(path, os.FileMode(mode), opts...),
+			), nil
+		}
+	case "mkfile":
+		path, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		mode, err := cg.EmitIntExpr(ctx, scope, args[1])
+		if err != nil {
+			return fc, err
+		}
+
+		content, err := cg.EmitStringExpr(ctx, scope, args[2])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []llb.MkfileOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.MkfileOption)
+			opts = append(opts, opt)
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st.File(
+				llb.Mkfile(path, os.FileMode(mode), []byte(content), opts...),
+			), nil
+		}
+	case "rm":
+		path, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []llb.RmOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.RmOption)
+			opts = append(opts, opt)
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st.File(
+				llb.Rm(path, opts...),
+			), nil
+		}
+	case "copy":
+		input, err := cg.EmitFilesystemExpr(ctx, scope, args[0], ac)
+		if err != nil {
+			return fc, err
+		}
+
+		src, err := cg.EmitStringExpr(ctx, scope, args[1])
+		if err != nil {
+			return fc, err
+		}
+
+		dest, err := cg.EmitStringExpr(ctx, scope, args[2])
+		if err != nil {
+			return fc, err
+		}
+
+		var opts []llb.CopyOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.CopyOption)
+			opts = append(opts, opt)
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			return st.File(
+				llb.Copy(input, src, dest, opts...),
+			), nil
+		}
+	case "dockerPush":
+		ref, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDockerPush, Ref: ref})
+			if err != nil {
+				return st, err
+			}
+			cg.requests = append(cg.requests, request)
+			return st, nil
+		}
+	case "dockerLoad":
+		ref, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+		fc = func(st llb.State) (llb.State, error) {
+			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDockerLoad, Ref: ref})
+			if err != nil {
+				return st, err
+			}
+			cg.requests = append(cg.requests, request)
+			return st, nil
+		}
+	case "download":
+		localPath, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDownload, LocalPath: localPath})
+			if err != nil {
+				return st, err
+			}
+			cg.requests = append(cg.requests, request)
+			return st, nil
+		}
+	case "downloadTarball":
+		localPath, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDownloadTarball, LocalPath: localPath})
+			if err != nil {
+				return st, err
+			}
+			cg.requests = append(cg.requests, request)
+			return st, nil
+		}
+	case "downloadOCITarball":
+		localPath, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDownloadOCITarball, LocalPath: localPath})
+			if err != nil {
+				return st, err
+			}
+			cg.requests = append(cg.requests, request)
+			return st, nil
+		}
+	case "downloadDockerTarball":
+		localPath, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return fc, err
+		}
+
+		ref, err := cg.EmitStringExpr(ctx, scope, args[1])
+		if err != nil {
+			return fc, err
+		}
+
+		fc = func(st llb.State) (llb.State, error) {
+			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDownloadDockerTarball, LocalPath: localPath, Ref: ref})
+			if err != nil {
+				return st, err
+			}
+			cg.requests = append(cg.requests, request)
+			return st, nil
+		}
+	}
+
+	return fc, nil
+}
+
+func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, chainStart interface{}) (StringChain, error) {
+	switch expr.Name() {
+	case "value":
+		val, err := cg.EmitStringExpr(ctx, scope, args[0])
+		return func(_ string) (string, error) {
+			return val, nil
+		}, err
+	case "format":
+		formatStr, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return nil, err
+		}
+
+		var as []interface{}
+		for _, arg := range args[1:] {
+			a, err := cg.EmitStringExpr(ctx, scope, arg)
+			if err != nil {
+				return nil, err
+			}
+			as = append(as, a)
+		}
+
+		return func(_ string) (string, error) {
+			return fmt.Sprintf(formatStr, as...), nil
+		}, nil
+	case "localEnv":
+		key, err := cg.EmitStringExpr(ctx, scope, args[0])
+		if err != nil {
+			return nil, err
+		}
+
+		return func(_ string) (string, error) {
+			return os.Getenv(key), nil
+		}, nil
+	default:
+		// Must be a named reference.
+		obj := scope.Lookup(expr.Name())
+		if obj == nil {
+			return nil, errors.WithStack(ErrCodeGen{expr, errors.Errorf("could not find reference")})
+		}
+
+		var v interface{}
+		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)
+		case *parser.ImportDecl:
+			importScope := obj.Data.(*parser.Scope)
+			importObj := importScope.Lookup(expr.Selector.Select.Name)
+			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)
+			default:
+				return nil, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
+			}
+		case *parser.Field:
+			v = obj.Data
+		default:
+			return nil, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
+		}
+		if err != nil {
+			return nil, err
+		}
+		return func(_ string) (string, error) {
+			return v.(string), nil
+		}, 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) {
+	switch expr.Name() {
+	case "parallel":
+		var peerRequests []solver.Request
+		for _, arg := range args {
+			request, err := cg.EmitGroupExpr(ctx, scope, arg, ac, nil)
+			if err != nil {
+				return gc, err
+			}
+
+			peerRequests = append(peerRequests, request)
+		}
+
+		gc = func(requests []solver.Request) ([]solver.Request, error) {
+			requests = append(requests, solver.Parallel(peerRequests...))
+			return requests, nil
+		}
+	default:
+		so, err := cg.EmitFilesystemBuiltinChainStmt(ctx, scope, expr, args, with, ac, nil)
+		if err != nil {
+			return gc, err
+		}
+
+		if so != nil {
+			return func(requests []solver.Request) ([]solver.Request, error) {
+				st, err := so(llb.Scratch())
+				if err != nil {
+					return requests, err
+				}
+
+				request, err := cg.outputRequest(ctx, st, Output{})
+				if err != nil {
+					return requests, err
+				}
+
+				if len(cg.requests) > 0 {
+					request = solver.Parallel(append([]solver.Request{request}, cg.requests...)...)
+				}
+
+				cg.reset()
+
+				requests = append(requests, request)
+				return requests, nil
+			}, nil
+		}
+
+		// Must be a named reference.
+		obj := scope.Lookup(expr.Name())
+		if obj == nil {
+			return gc, errors.WithStack(ErrCodeGen{expr, errors.Errorf("could not find reference")})
+		}
+
+		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)
+		case *parser.ImportDecl:
+			importScope := obj.Data.(*parser.Scope)
+			importObj := importScope.Lookup(expr.Selector.Select.Name)
+			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)
+			default:
+				return gc, errors.WithStack(ErrCodeGen{m, errors.Errorf("unknown obj type")})
+			}
+		case *parser.Field:
+			v = obj.Data
+		default:
+			return gc, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
+		}
+		if err != nil {
+			return gc, err
+		}
+		gc = func(requests []solver.Request) ([]solver.Request, error) {
+			var request solver.Request
+			switch t := v.(type) {
+			case solver.Request:
+				request = t
+			case llb.State:
+				request, err = cg.outputRequest(ctx, t, Output{})
+				if err != nil {
+					return requests, err
+				}
+
+				if len(cg.requests) > 0 {
+					request = solver.Parallel(append([]solver.Request{request}, cg.requests...)...)
+				}
+
+				cg.reset()
+			default:
+				return requests, errors.WithStack(ErrCodeGen{obj.Node, errors.Errorf("unknown group chain statement")})
+			}
+
+			requests = append(requests, request)
+			return requests, nil
+		}
+	}
+
+	return
+}
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 0d95ad3..c9bf974 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -15,7 +15,6 @@ import (
 
 	"github.com/docker/buildx/util/progress"
 	"github.com/docker/cli/cli/command"
-	shellquote "github.com/kballard/go-shellquote"
 	"github.com/moby/buildkit/client"
 	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/client/llb/imagemetaresolver"
@@ -33,7 +32,6 @@ import (
 	"github.com/openllb/hlb/sockprovider"
 	"github.com/openllb/hlb/solver"
 	"github.com/pkg/errors"
-	fstypes "github.com/tonistiigi/fsutil/types"
 	"golang.org/x/sync/errgroup"
 )
 
@@ -41,8 +39,6 @@ var (
 	ErrAliasReached = errors.New("alias reached")
 )
 
-type StateOption func(llb.State) (llb.State, error)
-
 type CodeGen struct {
 	Debug     Debugger
 	cln       *client.Client
@@ -149,8 +145,6 @@ func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []T
 			return nil, err
 		}
 
-		call := parser.NewCallStmt(target.Name, nil, nil, nil).Call
-
 		var (
 			v   interface{}
 			typ *parser.Type
@@ -164,7 +158,7 @@ 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, call, noopAliasCallback, nil)
+				v, err = cg.EmitFuncDecl(ctx, mod.Scope, n, nil, noopAliasCallback, nil)
 				if err != nil {
 					return nil, err
 				}
@@ -174,7 +168,7 @@ func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []T
 					return nil, checker.ErrInvalidTarget{Node: n, Target: target.Name}
 				}
 
-				v, err = cg.EmitAliasDecl(ctx, mod.Scope, n, call, nil)
+				v, err = cg.EmitAliasDecl(ctx, mod.Scope, n, nil, nil)
 				if err != nil {
 					return nil, err
 				}
@@ -356,112 +350,6 @@ func (cg *CodeGen) EmitBlock(ctx context.Context, scope *parser.Scope, typ parse
 	return v, nil
 }
 
-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) {
-	switch typ {
-	case parser.Filesystem:
-		chain, err := cg.EmitFilesystemChainStmt(ctx, scope, call, ac, chainStart)
-		if err != nil {
-			return nil, err
-		}
-		return func(v interface{}) (interface{}, error) {
-			return chain(v.(llb.State))
-		}, nil
-	case parser.Str:
-		chain, err := cg.EmitStringChainStmt(ctx, scope, call, chainStart)
-		if err != nil {
-			return nil, err
-		}
-		return func(v interface{}) (interface{}, error) {
-			return chain(v.(string))
-		}, nil
-	case parser.Group:
-		chain, err := cg.EmitGroupChainStmt(ctx, scope, call, ac, chainStart)
-		if err != nil {
-			return nil, err
-		}
-		return func(v interface{}) (interface{}, error) {
-			return chain(v.([]solver.Request))
-		}, nil
-	default:
-		return nil, errors.WithStack(ErrCodeGen{call, errors.Errorf("unknown chain stmt")})
-	}
-}
-
-func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, chainStart interface{}) (func(string) (string, error), error) {
-	args := call.Args
-	name := call.Func.Ident.Name
-	switch name {
-	case "value":
-		val, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		return func(_ string) (string, error) {
-			return val, nil
-		}, err
-	case "format":
-		formatStr, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return nil, err
-		}
-
-		var as []interface{}
-		for _, arg := range args[1:] {
-			a, err := cg.EmitStringExpr(ctx, scope, call, arg)
-			if err != nil {
-				return nil, err
-			}
-			as = append(as, a)
-		}
-
-		return func(_ string) (string, error) {
-			return fmt.Sprintf(formatStr, as...), nil
-		}, nil
-	case "localEnv":
-		key, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return nil, err
-		}
-
-		return func(_ string) (string, error) {
-			return os.Getenv(key), nil
-		}, nil
-	default:
-		// Must be a named reference.
-		obj := scope.Lookup(name)
-		if obj == nil {
-			return nil, errors.WithStack(ErrCodeGen{call, errors.Errorf("could not find reference")})
-		}
-
-		var v interface{}
-		var err error
-		switch n := obj.Node.(type) {
-		case *parser.FuncDecl:
-			v, err = cg.EmitFuncDecl(ctx, scope, n, call, noopAliasCallback, chainStart)
-		case *parser.AliasDecl:
-			v, err = cg.EmitAliasDecl(ctx, scope, n, call, chainStart)
-		case *parser.ImportDecl:
-			importScope := obj.Data.(*parser.Scope)
-			importObj := importScope.Lookup(call.Func.Selector.Select.Name)
-			switch m := importObj.Node.(type) {
-			case *parser.FuncDecl:
-				v, err = cg.EmitFuncDecl(ctx, scope, m, call, noopAliasCallback, chainStart)
-			case *parser.AliasDecl:
-				v, err = cg.EmitAliasDecl(ctx, scope, m, call, chainStart)
-			default:
-				return nil, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
-			}
-		case *parser.Field:
-			v = obj.Data
-		default:
-			return nil, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
-		}
-		if err != nil {
-			return nil, err
-		}
-		return func(_ string) (string, error) {
-			return v.(string), nil
-		}, nil
-	}
-}
-
 func (cg *CodeGen) EmitFilesystemBlock(ctx context.Context, scope *parser.Scope, stmts []*parser.Stmt, ac aliasCallback, chainStart interface{}) (llb.State, error) {
 	v, err := cg.EmitBlock(ctx, scope, parser.Filesystem, stmts, ac, chainStart)
 	return v.(llb.State), err
@@ -505,670 +393,6 @@ func (cg *CodeGen) EmitFuncLit(ctx context.Context, scope *parser.Scope, lit *pa
 	}
 }
 
-func (cg *CodeGen) EmitWithOption(ctx context.Context, scope *parser.Scope, parent *parser.CallStmt, with *parser.WithOpt, ac aliasCallback) (opts []interface{}, err error) {
-	if with == nil {
-		return
-	}
-
-	switch {
-	case with.Ident != nil:
-		obj := scope.Lookup(with.Ident.Name)
-		switch obj.Kind {
-		case parser.ExprKind:
-			return obj.Data.([]interface{}), nil
-		case parser.DeclKind:
-			if n, ok := obj.Node.(*parser.FuncDecl); ok {
-				return cg.EmitOptions(ctx, scope, parent.Func.Ident.Name, n.Body.NonEmptyStmts(), ac)
-			} else {
-				return opts, errors.WithStack(ErrCodeGen{obj.Node, errors.Errorf("unknown decl type")})
-			}
-		default:
-			return opts, errors.WithStack(ErrCodeGen{obj.Node, errors.Errorf("unknown with option kind")})
-		}
-	case with.FuncLit != nil:
-		return cg.EmitOptions(ctx, scope, parent.Func.Ident.Name, with.FuncLit.Body.NonEmptyStmts(), ac)
-	default:
-		return opts, errors.WithStack(ErrCodeGen{with, errors.Errorf("unknown with option")})
-	}
-}
-
-type GroupChain func([]solver.Request) ([]solver.Request, error)
-
-func (cg *CodeGen) EmitGroupChainStmt(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, ac aliasCallback, chainStart interface{}) (gc GroupChain, err error) {
-	var name string
-	switch {
-	case call.Func.Ident != nil:
-		name = call.Func.Ident.Name
-	case call.Func.Selector != nil:
-		name = call.Func.Selector.Ident.Name
-	}
-
-	switch name {
-	case "parallel":
-		var peerRequests []solver.Request
-		for _, arg := range call.Args {
-			request, err := cg.EmitGroupExpr(ctx, scope, call, arg, ac, nil)
-			if err != nil {
-				return gc, err
-			}
-
-			peerRequests = append(peerRequests, request)
-		}
-
-		gc = func(requests []solver.Request) ([]solver.Request, error) {
-			requests = append(requests, solver.Parallel(peerRequests...))
-			return requests, nil
-		}
-	default:
-		so, err := cg.EmitFilesystemBuiltinChainStmt(ctx, scope, call, ac, nil)
-		if err != nil {
-			return gc, err
-		}
-
-		if so != nil {
-			return func(requests []solver.Request) ([]solver.Request, error) {
-				st, err := so(llb.Scratch())
-				if err != nil {
-					return requests, err
-				}
-
-				request, err := cg.outputRequest(ctx, st, Output{})
-				if err != nil {
-					return requests, err
-				}
-
-				if len(cg.requests) > 0 {
-					request = solver.Parallel(append([]solver.Request{request}, cg.requests...)...)
-				}
-
-				cg.reset()
-
-				requests = append(requests, request)
-				return requests, nil
-			}, nil
-		}
-
-		// Must be a named reference.
-		obj := scope.Lookup(name)
-		if obj == nil {
-			return gc, errors.WithStack(ErrCodeGen{call, errors.Errorf("could not find reference")})
-		}
-
-		var v interface{}
-		switch n := obj.Node.(type) {
-		case *parser.FuncDecl:
-			v, err = cg.EmitFuncDecl(ctx, scope, n, call, ac, chainStart)
-		case *parser.AliasDecl:
-			v, err = cg.EmitAliasDecl(ctx, scope, n, call, chainStart)
-		case *parser.ImportDecl:
-			importScope := obj.Data.(*parser.Scope)
-			importObj := importScope.Lookup(call.Func.Selector.Select.Name)
-			switch m := importObj.Node.(type) {
-			case *parser.FuncDecl:
-				v, err = cg.EmitFuncDecl(ctx, scope, m, call, ac, chainStart)
-			case *parser.AliasDecl:
-				v, err = cg.EmitAliasDecl(ctx, scope, m, call, chainStart)
-			default:
-				return gc, errors.WithStack(ErrCodeGen{m, errors.Errorf("unknown obj type")})
-			}
-		case *parser.Field:
-			v = obj.Data
-		default:
-			return gc, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
-		}
-		if err != nil {
-			return gc, err
-		}
-		gc = func(requests []solver.Request) ([]solver.Request, error) {
-			var request solver.Request
-			switch t := v.(type) {
-			case solver.Request:
-				request = t
-			case llb.State:
-				request, err = cg.outputRequest(ctx, t, Output{})
-				if err != nil {
-					return requests, err
-				}
-
-				if len(cg.requests) > 0 {
-					request = solver.Parallel(append([]solver.Request{request}, cg.requests...)...)
-				}
-
-				cg.reset()
-			default:
-				return requests, errors.WithStack(ErrCodeGen{obj.Node, errors.Errorf("unknown group chain statement")})
-			}
-
-			requests = append(requests, request)
-			return requests, nil
-		}
-	}
-
-	return
-}
-
-func (cg *CodeGen) EmitFilesystemChainStmt(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, ac aliasCallback, chainStart interface{}) (so StateOption, err error) {
-	so, err = cg.EmitFilesystemBuiltinChainStmt(ctx, scope, call, ac, chainStart)
-	if err != nil {
-		return so, err
-	}
-
-	var name string
-	switch {
-	case call.Func.Ident != nil:
-		name = call.Func.Ident.Name
-	case call.Func.Selector != nil:
-		name = call.Func.Selector.Ident.Name
-	}
-
-	if so == nil {
-		// Must be a named reference.
-		obj := scope.Lookup(name)
-		if obj == nil {
-			return so, errors.WithStack(ErrCodeGen{call, errors.Errorf("could not find reference")})
-		}
-
-		var v interface{}
-		var err error
-		switch n := obj.Node.(type) {
-		case *parser.FuncDecl:
-			v, err = cg.EmitFuncDecl(ctx, scope, n, call, ac, chainStart)
-		case *parser.AliasDecl:
-			v, err = cg.EmitAliasDecl(ctx, scope, n, call, chainStart)
-		case *parser.ImportDecl:
-			importScope := obj.Data.(*parser.Scope)
-			importObj := importScope.Lookup(call.Func.Selector.Select.Name)
-			switch m := importObj.Node.(type) {
-			case *parser.FuncDecl:
-				v, err = cg.EmitFuncDecl(ctx, scope, m, call, ac, chainStart)
-			case *parser.AliasDecl:
-				v, err = cg.EmitAliasDecl(ctx, scope, m, call, chainStart)
-			default:
-				return so, errors.WithStack(ErrCodeGen{m, errors.Errorf("unknown obj type")})
-			}
-		case *parser.Field:
-			v = obj.Data
-		default:
-			return so, errors.WithStack(ErrCodeGen{n, errors.Errorf("unknown obj type")})
-		}
-		if err != nil {
-			return so, err
-		}
-		so = func(_ llb.State) (llb.State, error) {
-			return v.(llb.State), nil
-		}
-	}
-
-	return so, nil
-}
-func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, ac aliasCallback, chainStart interface{}) (so StateOption, err error) {
-	args := call.Args
-	iopts, err := cg.EmitWithOption(ctx, scope, call, call.WithOpt, ac)
-	if err != nil {
-		return so, err
-	}
-
-	var name string
-	switch {
-	case call.Func.Ident != nil:
-		name = call.Func.Ident.Name
-	case call.Func.Selector != nil:
-		name = call.Func.Selector.Ident.Name
-	}
-
-	switch name {
-	case "scratch":
-		so = func(_ llb.State) (llb.State, error) {
-			return llb.Scratch(), nil
-		}
-	case "image":
-		ref, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		var opts []llb.ImageOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.ImageOption)
-			opts = append(opts, opt)
-		}
-
-		so = func(_ llb.State) (llb.State, error) {
-			return llb.Image(ref, opts...), nil
-		}
-	case "http":
-		url, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		var opts []llb.HTTPOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.HTTPOption)
-			opts = append(opts, opt)
-		}
-
-		so = func(_ llb.State) (llb.State, error) {
-			return llb.HTTP(url, opts...), nil
-		}
-	case "git":
-		remote, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-		ref, err := cg.EmitStringExpr(ctx, scope, call, args[1])
-		if err != nil {
-			return so, err
-		}
-
-		var opts []llb.GitOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.GitOption)
-			opts = append(opts, opt)
-		}
-		so = func(_ llb.State) (llb.State, error) {
-			return llb.Git(remote, ref, opts...), nil
-		}
-	case "local":
-		path, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		var opts []llb.LocalOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.LocalOption)
-			opts = append(opts, opt)
-		}
-
-		id, err := cg.LocalID(path, opts...)
-		if err != nil {
-			return so, err
-		}
-		opts = append(opts, llb.SessionID(cg.sessionID), llb.WithDescription(map[string]string{
-			solver.LocalPathDescriptionKey: fmt.Sprintf("local://%s", path),
-		}))
-
-		// Register paths as syncable directories for the session.
-		cg.syncedDirByID[id] = filesync.SyncedDir{
-			Name: id,
-			Dir:  path,
-			Map: func(_ string, st *fstypes.Stat) bool {
-				st.Uid = 0
-				st.Gid = 0
-				return true
-			},
-		}
-
-		so = func(_ llb.State) (llb.State, error) {
-			return llb.Local(id, opts...), nil
-		}
-	case "frontend":
-		source, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		var opts []gatewayOption
-		for _, iopt := range iopts {
-			opt := iopt.(gatewayOption)
-			opts = append(opts, opt)
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			return st.Async(func(ctx context.Context, _ llb.State) (llb.State, error) {
-				pw := cg.mw.WithPrefix("", false)
-
-				var st llb.State
-				s, err := cg.newSession(ctx)
-				if err != nil {
-					return st, err
-				}
-
-				g, ctx := errgroup.WithContext(ctx)
-
-				g.Go(func() error {
-					return s.Run(ctx, cg.cln.Dialer())
-				})
-
-				g.Go(func() error {
-					return solver.Build(ctx, cg.cln, s, pw, func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
-						req := gateway.SolveRequest{
-							Frontend: "gateway.v0",
-							FrontendOpt: map[string]string{
-								"source": source,
-							},
-							FrontendInputs: make(map[string]*pb.Definition),
-						}
-
-						for _, opt := range opts {
-							opt(&req)
-						}
-
-						res, err := c.Solve(ctx, req)
-						if err != nil {
-							return res, err
-						}
-
-						ref, err := res.SingleRef()
-						if err != nil {
-							return res, err
-						}
-
-						st, err = ref.ToState()
-						return res, err
-					})
-				})
-
-				return st, g.Wait()
-			}), nil
-		}
-	case "run":
-		var shlex string
-		if len(args) == 1 {
-			commandStr, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-			if err != nil {
-				return so, err
-			}
-
-			parts, err := shellquote.Split(commandStr)
-			if err != nil {
-				return so, err
-			}
-
-			if len(parts) == 1 {
-				shlex = commandStr
-			} else {
-				shlex = shellquote.Join("/bin/sh", "-c", commandStr)
-			}
-		} else {
-			var runArgs []string
-			for _, arg := range args {
-				runArg, err := cg.EmitStringExpr(ctx, scope, call, arg)
-				if err != nil {
-					return so, err
-				}
-				runArgs = append(runArgs, runArg)
-			}
-			shlex = shellquote.Join(runArgs...)
-		}
-
-		var opts []llb.RunOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.RunOption)
-			opts = append(opts, opt)
-		}
-
-		var targets []string
-		calls := make(map[string]*parser.CallStmt)
-
-		with := call.WithOpt
-		if with != nil {
-			switch {
-			case with.Ident != nil:
-				// Do nothing.
-				//
-				// Mounts inside option functions cannot be aliased because they need
-				// to be in the context of a specific function run is in.
-			case with.FuncLit != nil:
-				for _, stmt := range with.FuncLit.Body.NonEmptyStmts() {
-					if stmt.Call.Func.Ident.Name != "mount" || stmt.Call.Alias == nil {
-						continue
-					}
-
-					target, err := cg.EmitStringExpr(ctx, scope, call, stmt.Call.Args[1])
-					if err != nil {
-						return so, err
-					}
-
-					calls[target] = stmt.Call
-					targets = append(targets, target)
-				}
-			default:
-				return nil, errors.WithStack(ErrCodeGen{with, errors.Errorf("unknown with option")})
-			}
-		}
-
-		opts = append(opts, llb.Shlex(shlex))
-		so = 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
-					}
-				}
-			}
-
-			return exec.Root(), nil
-		}
-	case "env":
-		key, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		value, err := cg.EmitStringExpr(ctx, scope, call, args[1])
-		if err != nil {
-			return so, err
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			return st.AddEnv(key, value), nil
-		}
-	case "dir":
-		path, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			return st.Dir(path), nil
-		}
-	case "user":
-		name, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			return st.User(name), nil
-		}
-	case "mkdir":
-		path, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		mode, err := cg.EmitIntExpr(ctx, scope, args[1])
-		if err != nil {
-			return so, err
-		}
-
-		iopts, err := cg.EmitWithOption(ctx, scope, call, call.WithOpt, ac)
-		if err != nil {
-			return so, err
-		}
-
-		var opts []llb.MkdirOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.MkdirOption)
-			opts = append(opts, opt)
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			return st.File(
-				llb.Mkdir(path, os.FileMode(mode), opts...),
-			), nil
-		}
-	case "mkfile":
-		path, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		mode, err := cg.EmitIntExpr(ctx, scope, args[1])
-		if err != nil {
-			return so, err
-		}
-
-		content, err := cg.EmitStringExpr(ctx, scope, call, args[2])
-		if err != nil {
-			return so, err
-		}
-
-		var opts []llb.MkfileOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.MkfileOption)
-			opts = append(opts, opt)
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			return st.File(
-				llb.Mkfile(path, os.FileMode(mode), []byte(content), opts...),
-			), nil
-		}
-	case "rm":
-		path, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		var opts []llb.RmOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.RmOption)
-			opts = append(opts, opt)
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			return st.File(
-				llb.Rm(path, opts...),
-			), nil
-		}
-	case "copy":
-		input, err := cg.EmitFilesystemExpr(ctx, scope, call, args[0], ac, nil)
-		if err != nil {
-			return so, err
-		}
-
-		src, err := cg.EmitStringExpr(ctx, scope, call, args[1])
-		if err != nil {
-			return so, err
-		}
-
-		dest, err := cg.EmitStringExpr(ctx, scope, call, args[2])
-		if err != nil {
-			return so, err
-		}
-
-		var opts []llb.CopyOption
-		for _, iopt := range iopts {
-			opt := iopt.(llb.CopyOption)
-			opts = append(opts, opt)
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			return st.File(
-				llb.Copy(input, src, dest, opts...),
-			), nil
-		}
-	case "dockerPush":
-		ref, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDockerPush, Ref: ref})
-			if err != nil {
-				return st, err
-			}
-			cg.requests = append(cg.requests, request)
-			return st, nil
-		}
-	case "dockerLoad":
-		ref, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-		so = func(st llb.State) (llb.State, error) {
-			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDockerLoad, Ref: ref})
-			if err != nil {
-				return st, err
-			}
-			cg.requests = append(cg.requests, request)
-			return st, nil
-		}
-	case "download":
-		localPath, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDownload, LocalPath: localPath})
-			if err != nil {
-				return st, err
-			}
-			cg.requests = append(cg.requests, request)
-			return st, nil
-		}
-	case "downloadTarball":
-		localPath, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDownloadTarball, LocalPath: localPath})
-			if err != nil {
-				return st, err
-			}
-			cg.requests = append(cg.requests, request)
-			return st, nil
-		}
-	case "downloadOCITarball":
-		localPath, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDownloadOCITarball, LocalPath: localPath})
-			if err != nil {
-				return st, err
-			}
-			cg.requests = append(cg.requests, request)
-			return st, nil
-		}
-	case "downloadDockerTarball":
-		localPath, err := cg.EmitStringExpr(ctx, scope, call, args[0])
-		if err != nil {
-			return so, err
-		}
-
-		ref, err := cg.EmitStringExpr(ctx, scope, call, args[1])
-		if err != nil {
-			return so, err
-		}
-
-		so = func(st llb.State) (llb.State, error) {
-			request, err := cg.outputRequest(ctx, st, Output{Type: OutputDownloadDockerTarball, LocalPath: localPath, Ref: ref})
-			if err != nil {
-				return st, err
-			}
-			cg.requests = append(cg.requests, request)
-			return st, nil
-		}
-	}
-
-	return so, nil
-}
-
 func (cg *CodeGen) EmitOptions(ctx context.Context, scope *parser.Scope, op string, stmts []*parser.Stmt, ac aliasCallback) (opts []interface{}, err error) {
 	switch op {
 	case "image":
@@ -1216,7 +440,7 @@ func (cg *CodeGen) EmitImageOptions(ctx context.Context, scope *parser.Scope, op
 					opts = append(opts, imagemetaresolver.WithDefault)
 				}
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1227,13 +451,33 @@ func (cg *CodeGen) EmitImageOptions(ctx context.Context, scope *parser.Scope, op
 	return
 }
 
+func (cg *CodeGen) EmitOptionLookup(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, op string) (opts []interface{}, err error) {
+	obj := scope.Lookup(expr.Name())
+	switch obj.Kind {
+	case parser.DeclKind:
+		switch n := obj.Node.(type) {
+		case *parser.FuncDecl:
+			return cg.EmitOptionFuncDecl(ctx, scope, n, args)
+		default:
+			return opts, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown option decl kind")})
+		}
+	case parser.FieldKind:
+		// we will get here with a variadic argument that is used with zero values
+		return nil, nil
+	case parser.ExprKind:
+		return obj.Data.([]interface{}), nil
+	default:
+		return opts, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown obj type")})
+	}
+}
+
 func (cg *CodeGen) EmitHTTPOptions(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
 			switch stmt.Call.Func.Ident.Name {
 			case "checksum":
-				dgst, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				dgst, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1245,13 +489,13 @@ func (cg *CodeGen) EmitHTTPOptions(ctx context.Context, scope *parser.Scope, op
 				}
 				opts = append(opts, llb.Chmod(os.FileMode(mode)))
 			case "filename":
-				filename, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				filename, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 				opts = append(opts, llb.Filename(filename))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1276,7 +520,7 @@ func (cg *CodeGen) EmitGitOptions(ctx context.Context, scope *parser.Scope, op s
 					opts = append(opts, llb.KeepGitDir())
 				}
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1295,7 +539,7 @@ func (cg *CodeGen) EmitLocalOptions(ctx context.Context, scope *parser.Scope, op
 			case "includePatterns":
 				patterns := make([]string, len(args))
 				for i, arg := range args {
-					patterns[i], err = cg.EmitStringExpr(ctx, scope, stmt.Call, arg)
+					patterns[i], err = cg.EmitStringExpr(ctx, scope, arg)
 					if err != nil {
 						return opts, err
 					}
@@ -1304,7 +548,7 @@ func (cg *CodeGen) EmitLocalOptions(ctx context.Context, scope *parser.Scope, op
 			case "excludePatterns":
 				patterns := make([]string, len(args))
 				for i, arg := range args {
-					patterns[i], err = cg.EmitStringExpr(ctx, scope, stmt.Call, arg)
+					patterns[i], err = cg.EmitStringExpr(ctx, scope, arg)
 					if err != nil {
 						return opts, err
 					}
@@ -1313,14 +557,14 @@ func (cg *CodeGen) EmitLocalOptions(ctx context.Context, scope *parser.Scope, op
 			case "followPaths":
 				paths := make([]string, len(args))
 				for i, arg := range args {
-					paths[i], err = cg.EmitStringExpr(ctx, scope, stmt.Call, arg)
+					paths[i], err = cg.EmitStringExpr(ctx, scope, arg)
 					if err != nil {
 						return opts, err
 					}
 				}
 				opts = append(opts, llb.FollowPaths(paths))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1351,11 +595,11 @@ func (cg *CodeGen) EmitFrontendOptions(ctx context.Context, scope *parser.Scope,
 			args := stmt.Call.Args
 			switch stmt.Call.Func.Ident.Name {
 			case "input":
-				key, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				key, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
-				st, err := cg.EmitFilesystemExpr(ctx, scope, stmt.Call, args[1], ac, nil)
+				st, err := cg.EmitFilesystemExpr(ctx, scope, args[1], ac)
 				if err != nil {
 					return opts, err
 				}
@@ -1365,17 +609,17 @@ func (cg *CodeGen) EmitFrontendOptions(ctx context.Context, scope *parser.Scope,
 				}
 				opts = append(opts, withFrontendInput(key, def))
 			case "opt":
-				key, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				key, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
-				value, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[1])
+				value, err := cg.EmitStringExpr(ctx, scope, args[1])
 				if err != nil {
 					return opts, err
 				}
 				opts = append(opts, withFrontendOpt(key, value))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1398,13 +642,13 @@ func (cg *CodeGen) EmitMkdirOptions(ctx context.Context, scope *parser.Scope, op
 				}
 				opts = append(opts, llb.WithParents(v))
 			case "chown":
-				owner, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				owner, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 				opts = append(opts, llb.WithUser(owner))
 			case "createdTime":
-				v, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				v, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1416,7 +660,7 @@ func (cg *CodeGen) EmitMkdirOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.WithCreatedTime(t))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1433,13 +677,13 @@ func (cg *CodeGen) EmitMkfileOptions(ctx context.Context, scope *parser.Scope, o
 			args := stmt.Call.Args
 			switch stmt.Call.Func.Ident.Name {
 			case "chown":
-				owner, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				owner, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 				opts = append(opts, llb.WithUser(owner))
 			case "createdTime":
-				v, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				v, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1451,7 +695,7 @@ func (cg *CodeGen) EmitMkfileOptions(ctx context.Context, scope *parser.Scope, o
 
 				opts = append(opts, llb.WithCreatedTime(t))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1480,7 +724,7 @@ func (cg *CodeGen) EmitRmOptions(ctx context.Context, scope *parser.Scope, op st
 				}
 				opts = append(opts, llb.WithAllowWildcard(v))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1535,13 +779,13 @@ func (cg *CodeGen) EmitCopyOptions(ctx context.Context, scope *parser.Scope, op
 				}
 				cp.AllowEmptyWildcard = v
 			case "chown":
-				owner, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				owner, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 				opts = append(opts, llb.WithUser(owner))
 			case "createdTime":
-				v, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				v, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1553,7 +797,7 @@ func (cg *CodeGen) EmitCopyOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.WithCreatedTime(t))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1569,13 +813,20 @@ func (cg *CodeGen) EmitCopyOptions(ctx context.Context, scope *parser.Scope, op
 func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op string, stmts []*parser.Stmt, ac aliasCallback) (opts []interface{}, err error) {
 	for _, stmt := range stmts {
 		if stmt.Call != nil {
-			args := stmt.Call.Args
-			iopts, err := cg.EmitWithOption(ctx, scope, stmt.Call, stmt.Call.WithOpt, ac)
-			if err != nil {
-				return opts, err
+			var (
+				expr = stmt.Call.Func
+				args = stmt.Call.Args
+				with = stmt.Call.WithOpt
+			)
+			var iopts []interface{}
+			if with != nil {
+				iopts, err = cg.EmitOptionExpr(ctx, scope, with.Expr, nil, expr.Name())
+				if err != nil {
+					return opts, err
+				}
 			}
 
-			switch stmt.Call.Func.Ident.Name {
+			switch expr.Name() {
 			case "readonlyRootfs":
 				v, err := cg.MaybeEmitBoolExpr(ctx, scope, args)
 				if err != nil {
@@ -1585,26 +836,26 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 					opts = append(opts, llb.ReadonlyRootFS())
 				}
 			case "env":
-				key, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				key, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 
-				value, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[1])
+				value, err := cg.EmitStringExpr(ctx, scope, args[1])
 				if err != nil {
 					return opts, err
 				}
 
 				opts = append(opts, llb.AddEnv(key, value))
 			case "dir":
-				path, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				path, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 
 				opts = append(opts, llb.Dir(path))
 			case "user":
-				name, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				name, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1613,7 +864,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 			case "ignoreCache":
 				opts = append(opts, llb.IgnoreCache)
 			case "network":
-				mode, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				mode, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1632,7 +883,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.Network(netMode))
 			case "security":
-				mode, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				mode, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1649,12 +900,12 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.Security(securityMode))
 			case "host":
-				host, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				host, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 
-				address, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[1])
+				address, err := cg.EmitStringExpr(ctx, scope, args[1])
 				if err != nil {
 					return opts, err
 				}
@@ -1674,7 +925,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 				}
 
 				sort.Strings(localPaths)
-				id := digest.FromString(strings.Join(localPaths, "")).String()
+				id := SSHID(localPaths...)
 				sshOpts = append(sshOpts, llb.SSHID(id))
 
 				// Register paths as forwardable SSH agent sockets or PEM keys for the
@@ -1687,12 +938,12 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.AddSSHSocket(sshOpts...))
 			case "forward":
-				src, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				src, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 
-				dest, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[1])
+				dest, err := cg.EmitStringExpr(ctx, scope, args[1])
 				if err != nil {
 					return opts, err
 				}
@@ -1765,12 +1016,12 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.AddSSHSocket(sshOpts...))
 			case "secret":
-				localPath, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				localPath, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 
-				mountPoint, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[1])
+				mountPoint, err := cg.EmitStringExpr(ctx, scope, args[1])
 				if err != nil {
 					return opts, err
 				}
@@ -1793,12 +1044,12 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.AddSecret(mountPoint, secretOpts...))
 			case "mount":
-				input, err := cg.EmitFilesystemExpr(ctx, scope, stmt.Call, args[0], ac, nil)
+				input, err := cg.EmitFilesystemExpr(ctx, scope, args[0], ac)
 				if err != nil {
 					return opts, err
 				}
 
-				target, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[1])
+				target, err := cg.EmitStringExpr(ctx, scope, args[1])
 				if err != nil {
 					return opts, err
 				}
@@ -1811,7 +1062,7 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.AddMount(target, input, mountOpts...))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, expr, args, op)
 				if err != nil {
 					return opts, ErrCodeGen{Node: stmt, Err: err}
 				}
@@ -1838,7 +1089,7 @@ func (cg *CodeGen) EmitSSHOptions(ctx context.Context, scope *parser.Scope, op s
 			args := stmt.Call.Args
 			switch stmt.Call.Func.Ident.Name {
 			case "target":
-				target, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				target, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1875,14 +1126,14 @@ func (cg *CodeGen) EmitSSHOptions(ctx context.Context, scope *parser.Scope, op s
 				sopt.mode = os.FileMode(mode)
 			case "localPaths":
 				for _, arg := range args {
-					localPath, err := cg.EmitStringExpr(ctx, scope, stmt.Call, arg)
+					localPath, err := cg.EmitStringExpr(ctx, scope, arg)
 					if err != nil {
 						return opts, err
 					}
 					opts = append(opts, localPath)
 				}
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1916,7 +1167,7 @@ func (cg *CodeGen) EmitSecretOptions(ctx context.Context, scope *parser.Scope, o
 			args := stmt.Call.Args
 			switch stmt.Call.Func.Ident.Name {
 			case "id":
-				id, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				id, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
@@ -1949,7 +1200,7 @@ func (cg *CodeGen) EmitSecretOptions(ctx context.Context, scope *parser.Scope, o
 				}
 				sopt.mode = os.FileMode(mode)
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -1991,18 +1242,18 @@ func (cg *CodeGen) EmitMountOptions(ctx context.Context, scope *parser.Scope, op
 					opts = append(opts, llb.Tmpfs())
 				}
 			case "sourcePath":
-				path, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				path, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 				opts = append(opts, llb.SourcePath(path))
 			case "cache":
-				id, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[0])
+				id, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
 					return opts, err
 				}
 
-				mode, err := cg.EmitStringExpr(ctx, scope, stmt.Call, args[1])
+				mode, err := cg.EmitStringExpr(ctx, scope, args[1])
 				if err != nil {
 					return opts, err
 				}
@@ -2021,7 +1272,7 @@ func (cg *CodeGen) EmitMountOptions(ctx context.Context, scope *parser.Scope, op
 
 				opts = append(opts, llb.AsPersistentCacheDir(id, sharing))
 			default:
-				iopts, err := cg.EmitOptionExpr(ctx, scope, stmt.Call, op, parser.NewIdentExpr(stmt.Call.Func.Ident.Name))
+				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
 				if err != nil {
 					return opts, err
 				}
@@ -2054,6 +1305,10 @@ func SecretID(path string) string {
 	return digest.FromString(path).String()
 }
 
+func SSHID(paths ...string) string {
+	return digest.FromString(strings.Join(paths, "")).String()
+}
+
 func outputFromWriter(w io.WriteCloser) func(map[string]string) (io.WriteCloser, error) {
 	return func(map[string]string) (io.WriteCloser, error) {
 		return w, nil
diff --git a/codegen/decl.go b/codegen/decl.go
index 471cf71..9561f05 100644
--- a/codegen/decl.go
+++ b/codegen/decl.go
@@ -10,12 +10,7 @@ import (
 	"github.com/pkg/errors"
 )
 
-func (cg *CodeGen) EmitFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, call *parser.CallStmt, ac aliasCallback, chainStart interface{}) (interface{}, error) {
-	var args []*parser.Expr
-	if call != nil {
-		args = call.Args
-	}
-
+func (cg *CodeGen) EmitFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (interface{}, error) {
 	nonVariadicArgs := 0
 	for _, field := range fun.Params.List {
 		if field.Variadic == nil {
@@ -26,66 +21,54 @@ 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, call, fun, args, ac, chainStart)
+	err := cg.ParameterizedScope(ctx, scope, fun, args, ac)
 	if err != nil {
 		return nil, err
 	}
 
-	v := chainStart
-	switch fun.Type.Primary() {
-	case parser.Filesystem, parser.Option:
-		if _, ok := v.(llb.State); v == nil || !ok {
-			v = llb.Scratch()
-		}
-	case parser.Str:
-		if _, ok := v.(string); v == nil || !ok {
-			v = ""
-		}
-	}
-
 	// Before executing a function.
-	err = cg.Debug(ctx, fun.Scope, fun, v)
+	err = cg.Debug(ctx, fun.Scope, fun, chainStart)
 	if err != nil {
-		return v, err
+		return chainStart, err
 	}
 
 	switch fun.Type.Primary() {
 	case parser.Filesystem:
-		return cg.EmitFilesystemBlock(ctx, fun.Scope, fun.Body.NonEmptyStmts(), ac, v)
+		return cg.EmitFilesystemBlock(ctx, fun.Scope, fun.Body.NonEmptyStmts(), ac, chainStart)
 	case parser.Option:
 		return cg.EmitOptions(ctx, fun.Scope, string(fun.Type.Secondary()), fun.Body.NonEmptyStmts(), ac)
 	case parser.Str:
-		return cg.EmitStringBlock(ctx, fun.Scope, fun.Body.NonEmptyStmts(), v)
+		return cg.EmitStringBlock(ctx, fun.Scope, fun.Body.NonEmptyStmts(), chainStart)
 	case parser.Group:
-		return cg.EmitGroupBlock(ctx, fun.Scope, fun.Body.NonEmptyStmts(), ac, v)
+		return cg.EmitGroupBlock(ctx, fun.Scope, fun.Body.NonEmptyStmts(), ac, chainStart)
 	default:
-		return v, checker.ErrInvalidTarget{Node: fun}
+		return chainStart, checker.ErrInvalidTarget{Node: fun}
 	}
 }
 
-func (cg *CodeGen) EmitFilesystemFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, call *parser.CallStmt, ac aliasCallback, chainStart interface{}) (llb.State, error) {
-	v, err := cg.EmitFuncDecl(ctx, scope, fun, call, ac, chainStart)
+func (cg *CodeGen) EmitFilesystemFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (llb.State, error) {
+	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, ac, chainStart)
 	return v.(llb.State), err
 }
 
-func (cg *CodeGen) EmitOptionFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, call *parser.CallStmt) ([]interface{}, error) {
-	v, err := cg.EmitFuncDecl(ctx, scope, fun, call, noopAliasCallback, nil)
+func (cg *CodeGen) EmitOptionFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr) ([]interface{}, error) {
+	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, noopAliasCallback, nil)
 	if v == nil {
 		return nil, err
 	}
 	return v.([]interface{}), err
 }
 
-func (cg *CodeGen) EmitStringFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, call *parser.CallStmt, ac aliasCallback, chainStart interface{}) (string, error) {
-	v, err := cg.EmitFuncDecl(ctx, scope, fun, call, ac, chainStart)
+func (cg *CodeGen) EmitStringFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (string, error) {
+	v, err := cg.EmitFuncDecl(ctx, scope, fun, args, ac, chainStart)
 	if v == nil {
 		return "", err
 	}
 	return v.(string), err
 }
 
-func (cg *CodeGen) EmitGroupFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, call *parser.CallStmt, ac aliasCallback, chainStart interface{}) (solver.Request, error) {
-	v, err := cg.EmitFuncDecl(ctx, scope, fun, call, ac, chainStart)
+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)
 	if v == nil {
 		return nil, err
 	}
@@ -112,9 +95,9 @@ 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, call *parser.CallStmt, chainStart interface{}) (interface{}, error) {
+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, call, func(aliasCall *parser.CallStmt, aliasValue interface{}) bool {
+	_, err := cg.EmitFuncDecl(ctx, scope, alias.Func, args, func(aliasCall *parser.CallStmt, aliasValue interface{}) bool {
 		if alias.Call == aliasCall {
 			v = aliasValue
 			return false
@@ -127,31 +110,31 @@ func (cg *CodeGen) EmitAliasDecl(ctx context.Context, scope *parser.Scope, alias
 	return v, err
 }
 
-func (cg *CodeGen) EmitFilesystemAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, call *parser.CallStmt, chainStart interface{}) (llb.State, error) {
-	v, err := cg.EmitAliasDecl(ctx, scope, alias, call, chainStart)
+func (cg *CodeGen) EmitFilesystemAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (llb.State, error) {
+	v, err := cg.EmitAliasDecl(ctx, scope, alias, args, chainStart)
 	if v == nil {
 		return llb.Scratch(), err
 	}
 	return v.(llb.State), err
 }
 
-func (cg *CodeGen) EmitStringAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, call *parser.CallStmt, chainStart interface{}) (string, error) {
-	v, err := cg.EmitAliasDecl(ctx, scope, alias, call, chainStart)
+func (cg *CodeGen) EmitStringAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (string, error) {
+	v, err := cg.EmitAliasDecl(ctx, scope, alias, args, chainStart)
 	if v == nil {
 		return "", err
 	}
 	return v.(string), err
 }
 
-func (cg *CodeGen) EmitGroupAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, call *parser.CallStmt, chainStart interface{}) (solver.Request, error) {
-	v, err := cg.EmitAliasDecl(ctx, scope, alias, call, chainStart)
+func (cg *CodeGen) EmitGroupAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (solver.Request, error) {
+	v, err := cg.EmitAliasDecl(ctx, scope, alias, args, chainStart)
 	if v == nil {
 		return nil, err
 	}
 	return v.(solver.Request), err
 }
 
-func (cg *CodeGen) ParameterizedScope(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) error {
+func (cg *CodeGen) ParameterizedScope(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback) error {
 	for i, field := range fun.Params.List {
 		var (
 			data interface{}
@@ -162,7 +145,7 @@ func (cg *CodeGen) ParameterizedScope(ctx context.Context, scope *parser.Scope,
 		switch typ {
 		case parser.Str:
 			var v string
-			v, err = cg.EmitStringExpr(ctx, scope, call, args[i])
+			v, err = cg.EmitStringExpr(ctx, scope, args[i])
 			data = v
 		case parser.Int:
 			var v int
@@ -174,30 +157,30 @@ 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, nil, args[i], ac, chainStart)
+			v, err = cg.EmitFilesystemExpr(ctx, scope, args[i], ac)
 			data = v
 		case parser.Option:
 			var v []interface{}
 			if field.Variadic != nil {
 				for j := i; j < len(args); j++ {
 					var vv []interface{}
-					vv, err = cg.EmitOptionExpr(ctx, scope, nil, string(field.Type.Secondary()), args[j])
+					vv, err = cg.EmitOptionExpr(ctx, scope, args[i], nil, string(field.Type.Secondary()))
 					if err != nil {
 						break
 					}
 					v = append(v, vv...)
 				}
 			} else {
-				v, err = cg.EmitOptionExpr(ctx, scope, nil, string(field.Type.Secondary()), args[i])
+				v, err = cg.EmitOptionExpr(ctx, scope, args[i], nil, string(field.Type.Secondary()))
 			}
 			data = v
 		case parser.Group:
 			var v solver.Request
-			v, err = cg.EmitGroupExpr(ctx, scope, nil, args[i], ac, chainStart)
+			v, err = cg.EmitGroupExpr(ctx, scope, args[i], ac, nil)
 			data = v
 		}
 		if err != nil {
-			return ErrCodeGen{Node: call, Err: err}
+			return ErrCodeGen{Node: args[i], Err: err}
 		}
 
 		fun.Scope.Insert(&parser.Object{
diff --git a/codegen/expr.go b/codegen/expr.go
index 142f2ae..c9fe17d 100644
--- a/codegen/expr.go
+++ b/codegen/expr.go
@@ -9,17 +9,18 @@ import (
 	"github.com/pkg/errors"
 )
 
-func (cg *CodeGen) EmitStringExpr(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, expr *parser.Expr) (string, error) {
+func (cg *CodeGen) EmitStringExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr) (string, error) {
 	switch {
-	case expr.Ident != nil:
+	case expr.Ident != nil, expr.Selector != nil:
+
 		obj := scope.Lookup(expr.Ident.Name)
 		switch obj.Kind {
 		case parser.DeclKind:
 			switch n := obj.Node.(type) {
 			case *parser.FuncDecl:
-				return cg.EmitStringFuncDecl(ctx, scope, n, call, noopAliasCallback, nil)
+				return cg.EmitStringFuncDecl(ctx, scope, n, nil, noopAliasCallback, nil)
 			case *parser.AliasDecl:
-				return cg.EmitStringAliasDecl(ctx, scope, n, call, nil)
+				return cg.EmitStringAliasDecl(ctx, scope, n, nil, nil)
 			default:
 				return "", errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown decl object")})
 			}
@@ -98,54 +99,34 @@ func (cg *CodeGen) MaybeEmitBoolExpr(ctx context.Context, scope *parser.Scope, a
 	return v, nil
 }
 
-func (cg *CodeGen) EmitFilesystemExpr(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, expr *parser.Expr, ac aliasCallback, chainStart interface{}) (st llb.State, err error) {
+func (cg *CodeGen) EmitFilesystemExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr, ac aliasCallback) (st llb.State, err error) {
 	switch {
-	case expr.Ident != nil:
-		obj := scope.Lookup(expr.Ident.Name)
-		switch obj.Kind {
-		case parser.DeclKind:
-			switch n := obj.Node.(type) {
-			case *parser.FuncDecl:
-				return cg.EmitFilesystemFuncDecl(ctx, scope, n, nil, noopAliasCallback, chainStart)
-			case *parser.AliasDecl:
-				return cg.EmitFilesystemAliasDecl(ctx, scope, n, nil, chainStart)
-			default:
-				return llb.Scratch(), errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown decl object")})
-			}
-		case parser.ExprKind:
-			return obj.Data.(llb.State), nil
-		default:
-			return llb.Scratch(), errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown obj type")})
+	case expr.Ident != nil, expr.Selector != nil:
+		so, err := cg.EmitFilesystemChainStmt(ctx, scope, expr, nil, nil, ac, nil)
+		if err != nil {
+			return st, err
 		}
+
+		st, err = so(st)
+		return st, err
 	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.NonEmptyStmts(), ac, chainStart)
+		return cg.EmitFilesystemBlock(ctx, scope, expr.FuncLit.Body.NonEmptyStmts(), ac, nil)
 	default:
 		return st, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown fs expr")})
 	}
 }
 
-func (cg *CodeGen) EmitOptionExpr(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, op string, expr *parser.Expr) (opts []interface{}, err error) {
+func (cg *CodeGen) EmitOptionExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr, args []*parser.Expr, op string) (opts []interface{}, err error) {
 	switch {
-	case expr.Ident != nil:
-		obj := scope.Lookup(expr.Ident.Name)
-		switch obj.Kind {
-		case parser.DeclKind:
-			switch n := obj.Node.(type) {
-			case *parser.FuncDecl:
-				return cg.EmitOptionFuncDecl(ctx, scope, n, call)
-			default:
-				return opts, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown option decl kind")})
-			}
-		case parser.FieldKind:
-			// we will get here with a variadic argument that is used with zero values
-			return nil, nil
-		case parser.ExprKind:
-			return obj.Data.([]interface{}), nil
-		default:
-			return opts, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown obj type")})
-		}
+	case expr.Ident != nil, expr.Selector != nil:
+		return cg.EmitOptions(ctx, scope, op, []*parser.Stmt{{
+			Call: &parser.CallStmt{
+				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:
@@ -155,25 +136,24 @@ func (cg *CodeGen) EmitOptionExpr(ctx context.Context, scope *parser.Scope, call
 	}
 }
 
-func (cg *CodeGen) EmitGroupExpr(ctx context.Context, scope *parser.Scope, call *parser.CallStmt, expr *parser.Expr, ac aliasCallback, chainStart interface{}) (solver.Request, error) {
+func (cg *CodeGen) EmitGroupExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr, ac aliasCallback, chainStart interface{}) (solver.Request, error) {
 	switch {
-	case expr.Ident != nil:
-		obj := scope.Lookup(expr.Ident.Name)
-		switch obj.Kind {
-		case parser.DeclKind:
-			switch n := obj.Node.(type) {
-			case *parser.FuncDecl:
-				return cg.EmitGroupFuncDecl(ctx, scope, n, nil, noopAliasCallback, chainStart)
-			case *parser.AliasDecl:
-				return cg.EmitGroupAliasDecl(ctx, scope, n, nil, chainStart)
-			default:
-				return nil, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown decl object")})
-			}
-		case parser.ExprKind:
-			return obj.Data.(solver.Request), nil
-		default:
-			return nil, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown obj type")})
+	case expr.Ident != nil, expr.Selector != nil:
+		gc, err := cg.EmitGroupChainStmt(ctx, scope, expr, nil, nil, ac, chainStart)
+		if err != nil {
+			return nil, err
+		}
+
+		var requests []solver.Request
+		requests, err = gc(requests)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(requests) == 1 {
+			return requests[0], nil
 		}
+		return solver.Sequential(requests...), nil
 	case expr.BasicLit != nil:
 		return nil, errors.WithStack(ErrCodeGen{expr, errors.Errorf("group expr cannot be basic lit")})
 	case expr.FuncLit != nil:
diff --git a/go.hlb b/go.hlb
index c100217..540264a 100644
--- a/go.hlb
+++ b/go.hlb
@@ -14,10 +14,10 @@ fs build(fs src, string package, string verPackage) {
 	env "GO111MODULE" "on"
 	dir "/go/src/hlb"
 	run string {
-		format "v=$(%s) && /usr/local/go/bin/go build -o /out/binary -ldflags \"-linkmode external -extldflags -static -X %s.Version=$v\" -a %s" string { versionCmd; }  package verPackage
+		format "v=$(%s) && /usr/local/go/bin/go build -o /out/binary -ldflags \"-linkmode external -extldflags -static -X %s.Version=$v\" -a %s" versionCmd package verPackage
 	} with option {
 		cacheMounts src
-		mount fs { scratch; } "/out" as binary
+		mount scratch "/out" as binary
 	}
 }
 
@@ -27,14 +27,14 @@ fs crossBuild(fs src, string package, string verPackage) {
 	env "GO111MODULE" "on"
 	dir "/go/src/hlb"
 	run string {
-		format "v=$(%s) && LDFLAGS=\"-X %s.Version=$v\" /cross/build %q" string { versionCmd; } verPackage package 
+		format "v=$(%s) && LDFLAGS=\"-X %s.Version=$v\" /cross/build %q" versionCmd verPackage package 
 	} with option {
 		cacheMounts src
 		mount fs { git "https://github.com/hinshun/go-cross.git" ""; } "/cross" with option {
 			sourcePath "/scripts"
 			readonly
 		}
-		mount fs { scratch; } "/root/go/bin" as crossBinaries
+		mount scratch "/root/go/bin" as crossBinaries
 	}
 }
 
@@ -61,10 +61,10 @@ option::run cacheMounts(fs src) {
 	mount src "/go/src/hlb" with option {
 		readonly
 	}
-	mount fs { scratch; } "/root/.cache/go-build" with option {
+	mount scratch "/root/.cache/go-build" with option {
 		cache "hlb/go-build" "private"
 	}
-	mount fs { scratch; } "/go/pkg/mod" with option {
+	mount scratch "/go/pkg/mod" with option {
 		cache "hlb/go-mod" "private"
 	}
 }
diff --git a/parser/cst.go b/parser/cst.go
index 414d130..86789c1 100644
--- a/parser/cst.go
+++ b/parser/cst.go
@@ -299,6 +299,17 @@ func (e *Expr) End() lexer.Position {
 	}
 }
 
+func (e *Expr) Name() string {
+	switch {
+	case e.Selector != nil:
+		return e.Selector.Ident.Name
+	case e.Ident != nil:
+		return e.Ident.Name
+	default:
+		return ""
+	}
+}
+
 // Type represents an object type.
 type Type struct {
 	Pos     lexer.Position
@@ -580,37 +591,27 @@ func (s *CallStmt) End() lexer.Position      { return s.StmtEnd.End() }
 
 // WithOpt represents optional arguments for a CallStmt.
 type WithOpt struct {
-	Pos     lexer.Position
-	With    *With    `parser:"@@"`
-	Ident   *Ident   `parser:"( @@"`
-	FuncLit *FuncLit `parser:"| @@ )"`
+	Pos  lexer.Position
+	With *With `parser:"@@"`
+	Expr *Expr `parser:"@@"`
 }
 
 func NewWithIdent(name string) *WithOpt {
 	return &WithOpt{
-		With:  &With{Keyword: "with"},
-		Ident: NewIdent(name),
+		With: &With{Keyword: "with"},
+		Expr: NewIdentExpr(name),
 	}
 }
 
 func NewWithFuncLit(stmts ...*Stmt) *WithOpt {
 	return &WithOpt{
-		With:    &With{Keyword: "with"},
-		FuncLit: NewFuncLit(Option, stmts...),
+		With: &With{Keyword: "with"},
+		Expr: NewFuncLitExpr(Option, stmts...),
 	}
 }
 
 func (w *WithOpt) Position() lexer.Position { return w.Pos }
-func (w *WithOpt) End() lexer.Position {
-	switch {
-	case w.Ident != nil:
-		return w.Ident.End()
-	case w.FuncLit != nil:
-		return w.FuncLit.End()
-	default:
-		return shiftPosition(w.Pos, 1, 0)
-	}
-}
+func (w *WithOpt) End() lexer.Position      { return w.Expr.End() }
 
 // With represents the keyword "with".
 type With struct {
diff --git a/parser/unparse.go b/parser/unparse.go
index bcc558a..7eff881 100644
--- a/parser/unparse.go
+++ b/parser/unparse.go
@@ -256,7 +256,7 @@ func (s *CallStmt) String() string {
 	}
 
 	withOpt := ""
-	if s.WithOpt != nil && (s.WithOpt.Ident != nil || (s.WithOpt.Ident == nil && s.WithOpt.FuncLit.NumStmts() > 0)) {
+	if s.WithOpt != nil && (s.WithOpt.Expr.FuncLit == nil || (s.WithOpt.Expr.FuncLit != nil && s.WithOpt.Expr.FuncLit.NumStmts() > 0)) {
 		withOpt = fmt.Sprintf(" %s", s.WithOpt)
 	}
 
@@ -286,13 +286,7 @@ func (a *As) String() string {
 }
 
 func (w *WithOpt) String() string {
-	switch {
-	case w.Ident != nil:
-		return fmt.Sprintf("%s %s", w.With, w.Ident)
-	case w.FuncLit != nil:
-		return fmt.Sprintf("%s %s", w.With, w.FuncLit)
-	}
-	panic("unknown with opt")
+	return fmt.Sprintf("with %s", w.Expr)
 }
 
 func (w *With) String() string {
diff --git a/parser/walk.go b/parser/walk.go
index e922bc0..3f4a871 100644
--- a/parser/walk.go
+++ b/parser/walk.go
@@ -142,11 +142,8 @@ func Walk(node Node, v Visitor) {
 		if n.With != nil {
 			Walk(n.With, v)
 		}
-		switch {
-		case n.Ident != nil:
-			Walk(n.Ident, v)
-		case n.FuncLit != nil:
-			Walk(n.FuncLit, v)
+		if n.Expr != nil {
+			Walk(n.Expr, v)
 		}
 	case *BlockStmt:
 		walkStmtList(n.List, v)
diff --git a/solver/request.go b/solver/request.go
index fa29fb8..a0492a3 100644
--- a/solver/request.go
+++ b/solver/request.go
@@ -151,9 +151,11 @@ func (o op) Tree(tree treeprint.Tree) error {
 		for _, mnt := range v.Exec.Mounts {
 			source := "scratch"
 			if mnt.Input >= 0 {
-				source = sources[mnt.Input].Source.Identifier
-				if mnt.Selector != "" {
-					source += mnt.Selector
+				if int(mnt.Input) < len(sources) {
+					source = sources[mnt.Input].Source.Identifier
+					if mnt.Selector != "" {
+						source += mnt.Selector
+					}
 				}
 				if strings.HasPrefix(source, "local://") {
 					if localPath, ok := sourceMeta[mnt.Input].Description[LocalPathDescriptionKey]; ok {


Test patch:
diff --git a/checker/checker_test.go b/checker/checker_test.go
index 9c57b23..a0696e9 100644
--- a/checker/checker_test.go
+++ b/checker/checker_test.go
@@ -50,6 +50,27 @@ func TestCompile(t *testing.T) {
 		}
 		`,
 		nil,
+	}, {
+		"second source from function without func lit",
+		`
+		fs default() {
+			scratch
+			nothing scratch
+		}
+		fs nothing(fs repo) {
+			scratch
+		}
+		`,
+		nil,
+	}, {
+		"single builtin option",
+		`
+		fs default() {
+			image "busybox:latest"
+			run "ssh root@foobar" with ssh
+		}
+		`,
+		nil,
 	}, {
 		"single named option",
 		`
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 8e4f576..74fc65c 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -94,6 +94,37 @@ func TestCodeGen(t *testing.T) {
 		func(t *testing.T, cg *CodeGen) solver.Request {
 			return Expect(t, llb.Scratch().File(llb.Mkfile("home", 0644, []byte(os.Getenv("HOME")))))
 		},
+	}, {
+		"scratch mounts without func lit",
+		[]string{"default"},
+		`
+		fs echo() {
+			image "alpine"
+			run "touch /out/foo" with option {
+				mount scratch "/out" as default
+			}
+		}
+		`,
+		func(t *testing.T, cg *CodeGen) solver.Request {
+			return Expect(t, llb.Image("alpine").Run(
+				llb.Shlex("touch /out/foo"),
+			).AddMount("/out", llb.Scratch()))
+		},
+	}, {
+		"option builtin without func lit",
+		[]string{"default"},
+		`
+		fs default() {
+			image "alpine"
+			run "echo unchanged" with readonlyRootfs
+		}
+		`,
+		func(t *testing.T, cg *CodeGen) solver.Request {
+			return Expect(t, llb.Image("alpine").Run(
+				llb.Shlex("echo unchanged"),
+				llb.ReadonlyRootFS(),
+			).Root())
+		},
 	}, {
 		"empty group",
 		[]string{"default"},