Repository: openllb/hlb
Pull request #129 (2020-04-30 03:43:51)

fix(doc): update target flag for lets-begin


Patch:
diff --git a/LANGSERVER.md b/LANGSERVER.md
deleted file mode 100644
index 5d6d4a6..0000000
--- a/LANGSERVER.md
+++ /dev/null
@@ -1,54 +0,0 @@
-hlb langserver
-==============
-
-Language server for [hlb](https://github.com/openllb/hlb) speaking [LSP](https://github.com/Microsoft/language-server-protocol).
-
-Capabilities
-------------
-
-| Capability            | Support |
-|-----------------------|---------|
-| Hover                 |    ✔    |
-| Jump to definition    |    ✔    |
-| Find references       |         |
-| Completion            |         |
-| Workspace symbols     |         |
-| Semantic highlighting |    ✔    |
-
-Installation
-------------
-
-To build and install the `hlb langserver` run:
-
-```sh
-go get -u github.com/openllb/hlb/cmd/hlb
-```
-
-Usage
------
-
-Kakoune ([kak-lsp](https://github.com/ul/kak-lsp/))
-```toml
-[language.hlb]
-filetypes = ["hlb"]
-roots = [".git", ".hg"]
-command = "hlb-langserver"
-offset_encoding = "utf-8"
-
-[semantic_scopes]
-# Map textmate scopes to kakoune faces for semantic highlighting
-# the underscores are translated to dots, and indicate nesting.
-# That is, if variable_other_field is omitted, it will try the face for
-# variable_other and then variable
-#
-# To see a list of available scopes in the debug buffer, run lsp-semantic-available-scopes
-string="string"
-constant="value"
-variable="variable"
-keyword="keyword"
-storage_modifier="type"
-storage_type="type"
-entity_name_function="function"
-entity_name_namespace="module"
-comment="comment"
-```
diff --git a/build.hlb b/build.hlb
index d0fb004..d5da7e0 100644
--- a/build.hlb
+++ b/build.hlb
@@ -1,6 +1,4 @@
-# TODO replace with openllb/go.hlb
 import go "./go.hlb"
-import mkdocs "./mkdocs.hlb"
 
 fs default() {
 	crossHLB
@@ -20,20 +18,3 @@ fs src() {
 		excludePatterns "build"
 	}
 }
-
-fs gen() {
-	mkdocs.generatedBuiltin
-	download "./builtin/."
-
-	mkdocs.generatedMarkdown
-	download "./docs/."
-}
-
-fs docs() {
-	mkdocs.build
-	download "./site/."
-}
-
-fs publishDocs() {
-	mkdocs.publish
-}
diff --git a/builtin/builtin.go b/builtin/builtin.go
index fd60852..4a571ce 100644
--- a/builtin/builtin.go
+++ b/builtin/builtin.go
@@ -1,4 +1,4 @@
-// Code generated by builtingen /language/builtin.hlb /out/builtin.go; DO NOT EDIT.
+// Code generated by builtingen ../language/builtin.hlb ../builtin/builtin.go; DO NOT EDIT.
 
 package builtin
 
@@ -248,22 +248,6 @@ var (
 					},
 				},
 			},
-			"option::localRun": LookupByType{
-				Func: map[string]FuncLookup{
-					"ignoreError": FuncLookup{
-						Params: []*parser.Field{},
-					},
-					"includeStderr": FuncLookup{
-						Params: []*parser.Field{},
-					},
-					"onlyStderr": FuncLookup{
-						Params: []*parser.Field{},
-					},
-					"shlex": FuncLookup{
-						Params: []*parser.Field{},
-					},
-				},
-			},
 			"option::mkdir": LookupByType{
 				Func: map[string]FuncLookup{
 					"createParents": FuncLookup{
@@ -360,9 +344,6 @@ var (
 							parser.NewField(parser.Str, "securitymode", false),
 						},
 					},
-					"shlex": FuncLookup{
-						Params: []*parser.Field{},
-					},
 					"host": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "hostname", false),
@@ -482,12 +463,6 @@ var (
 					"localOs": FuncLookup{
 						Params: []*parser.Field{},
 					},
-					"localRun": FuncLookup{
-						Params: []*parser.Field{
-							parser.NewField(parser.Str, "command", false),
-							parser.NewField(parser.Str, "args", true),
-						},
-					},
 					"template": FuncLookup{
 						Params: []*parser.Field{
 							parser.NewField(parser.Str, "text", false),
diff --git a/checker/checker.go b/checker/checker.go
index 57cd9da..a9c2dd9 100644
--- a/checker/checker.go
+++ b/checker/checker.go
@@ -40,13 +40,19 @@ func (c *checker) Check(mod *parser.Module) error {
 			}
 		case *parser.ImportDecl:
 			if n.Ident != nil {
-				c.registerDecl(mod.Scope, n.Ident, n)
+				skip := c.registerDecl(mod.Scope, n.Ident, n)
+				if skip {
+					return false
+				}
 			}
 		case *parser.FuncDecl:
 			fun := n
 
 			if fun.Name != nil {
-				c.registerDecl(mod.Scope, fun.Name, fun)
+				skip := c.registerDecl(mod.Scope, fun.Name, fun)
+				if skip {
+					return false
+				}
 			}
 
 			// Create a function-level scope.
@@ -230,7 +236,7 @@ func (c *checker) CheckSelectors(mod *parser.Module) error {
 	return nil
 }
 
-func (c *checker) registerDecl(scope *parser.Scope, ident *parser.Ident, node parser.Node) {
+func (c *checker) registerDecl(scope *parser.Scope, ident *parser.Ident, node parser.Node) bool {
 	// Ensure that this identifier is not already defined in the module scope.
 	obj := scope.Lookup(ident.Name)
 	if obj != nil {
@@ -238,7 +244,7 @@ func (c *checker) registerDecl(scope *parser.Scope, ident *parser.Ident, node pa
 			c.duplicateDecls = append(c.duplicateDecls, obj.Ident)
 		}
 		c.duplicateDecls = append(c.duplicateDecls, ident)
-		return
+		return true
 	}
 
 	scope.Insert(&parser.Object{
@@ -246,6 +252,7 @@ func (c *checker) registerDecl(scope *parser.Scope, ident *parser.Ident, node pa
 		Ident: ident,
 		Node:  node,
 	})
+	return false
 }
 
 func (c *checker) checkFieldList(fields []*parser.Field) error {
diff --git a/codegen/chain.go b/codegen/chain.go
index f47193c..d084456 100644
--- a/codegen/chain.go
+++ b/codegen/chain.go
@@ -5,7 +5,6 @@ import (
 	"context"
 	"fmt"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strings"
 	"text/template"
@@ -37,11 +36,7 @@ func (cg *CodeGen) EmitChainStmt(ctx context.Context, scope *parser.Scope, typ p
 			return nil, err
 		}
 		return func(v interface{}) (interface{}, error) {
-			st, ok := v.(llb.State)
-			if !ok {
-				return st, errors.WithStack(ErrCodeGen{call, ErrBadCast})
-			}
-			return chain(st)
+			return chain(v.(llb.State))
 		}, nil
 	case parser.Str:
 		chain, err := cg.EmitStringChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, chainStart)
@@ -49,11 +44,7 @@ func (cg *CodeGen) EmitChainStmt(ctx context.Context, scope *parser.Scope, typ p
 			return nil, err
 		}
 		return func(v interface{}) (interface{}, error) {
-			str, ok := v.(string)
-			if !ok {
-				return str, errors.WithStack(ErrCodeGen{call, ErrBadCast})
-			}
-			return chain(str)
+			return chain(v.(string))
 		}, nil
 	case parser.Group:
 		chain, err := cg.EmitGroupChainStmt(ctx, scope, call.Func, call.Args, call.WithOpt, ac, chainStart)
@@ -61,11 +52,7 @@ func (cg *CodeGen) EmitChainStmt(ctx context.Context, scope *parser.Scope, typ p
 			return nil, err
 		}
 		return func(v interface{}) (interface{}, error) {
-			requests, ok := v.([]solver.Request)
-			if !ok {
-				return requests, errors.WithStack(ErrCodeGen{call, ErrBadCast})
-			}
-			return chain(requests)
+			return chain(v.([]solver.Request))
 		}, nil
 	default:
 		return nil, errors.WithStack(ErrCodeGen{call, errors.Errorf("unknown chain stmt")})
@@ -82,7 +69,7 @@ func (cg *CodeGen) EmitFilesystemChainStmt(ctx context.Context, scope *parser.Sc
 		// Must be a named reference.
 		obj := scope.Lookup(expr.Name())
 		if obj == nil {
-			return fc, errors.WithStack(ErrCodeGen{expr.IdentNode(), ErrUndefinedReference})
+			return fc, errors.WithStack(ErrCodeGen{expr, errors.Errorf("could not find reference")})
 		}
 
 		var v interface{}
@@ -96,7 +83,7 @@ func (cg *CodeGen) EmitFilesystemChainStmt(ctx context.Context, scope *parser.Sc
 			importScope := obj.Data.(*parser.Scope)
 			importObj := importScope.Lookup(expr.Selector.Select.Name)
 			if importObj == nil {
-				return nil, errors.WithStack(ErrCodeGen{expr.Selector, ErrUndefinedReference})
+				return nil, errors.WithStack(ErrCodeGen{expr, errors.Errorf("could not find reference")})
 			}
 
 			switch m := importObj.Node.(type) {
@@ -116,49 +103,12 @@ func (cg *CodeGen) EmitFilesystemChainStmt(ctx context.Context, scope *parser.Sc
 			return fc, err
 		}
 		fc = func(_ llb.State) (llb.State, error) {
-			st, ok := v.(llb.State)
-			if !ok {
-				return st, errors.WithStack(ErrCodeGen{expr, errors.Errorf("bad cast")})
-			}
-			return st, nil
+			return v.(llb.State), nil
 		}
 	}
 
 	return fc, nil
 }
-
-func (cg *CodeGen) EmitShellCommand(ctx context.Context, scope *parser.Scope, args []*parser.Expr, wantShlex bool) ([]string, error) {
-	if len(args) == 0 {
-		return nil, nil
-	}
-
-	if len(args) == 1 {
-		commandStr, err := cg.EmitStringExpr(ctx, scope, args[0])
-		if err != nil {
-			return nil, err
-		}
-
-		if wantShlex {
-			parts, err := shellquote.Split(commandStr)
-			if err != nil {
-				return nil, err
-			}
-			return parts, nil
-		}
-
-		return []string{"/bin/sh", "-c", commandStr}, nil
-	}
-	var runArgs []string
-	for _, arg := range args {
-		runArg, err := cg.EmitStringExpr(ctx, scope, arg)
-		if err != nil {
-			return nil, err
-		}
-		runArgs = append(runArgs, runArg)
-	}
-	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) {
 	var iopts []interface{}
 	if with != nil {
@@ -321,20 +271,39 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 			}), nil
 		}
 	case "run":
-		wantShlex := false
-		var opts []llb.RunOption
-		for _, iopt := range iopts {
-			switch opt := iopt.(type) {
-			case llb.RunOption:
-				opts = append(opts, opt)
-			case *shlexOption:
-				wantShlex = true
+		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...)
 		}
 
-		cmd, err := cg.EmitShellCommand(ctx, scope, args, wantShlex)
-		if err != nil {
-			return fc, err
+		var opts []llb.RunOption
+		for _, iopt := range iopts {
+			opt := iopt.(llb.RunOption)
+			opts = append(opts, opt)
 		}
 
 		var targets []string
@@ -366,8 +335,8 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa
 			}
 		}
 
-		customName := strings.ReplaceAll(shellquote.Join(cmd...), "\n", "")
-		opts = append(opts, llb.Args(cmd), llb.WithCustomName(customName))
+		customName := strings.ReplaceAll(shlex, "\n", "")
+		opts = append(opts, llb.Shlex(shlex), llb.WithCustomName(customName))
 
 		err = fixReadonlyMounts(opts)
 		if err != nil {
@@ -844,46 +813,6 @@ func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope,
 		return func(_ string) (string, error) {
 			return local.Os(ctx), nil
 		}, nil
-	case "localRun":
-		wantShlex := false
-		execOpts := &LocalRunOptions{}
-		for _, iopt := range iopts {
-			switch opt := iopt.(type) {
-			case func(*LocalRunOptions):
-				opt(execOpts)
-			case *shlexOption:
-				wantShlex = true
-			}
-		}
-
-		cmd, err := cg.EmitShellCommand(ctx, scope, args, wantShlex)
-		if err != nil {
-			return nil, err
-		}
-
-		c := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
-		c.Env = local.Environ(ctx)
-		c.Dir, err = local.Cwd(ctx)
-		if err != nil {
-			return nil, err
-		}
-
-		var buf strings.Builder
-		if execOpts.OnlyStderr {
-			c.Stderr = &buf
-		} else {
-			c.Stdout = &buf
-		}
-		if execOpts.IncludeStderr {
-			c.Stderr = &buf
-		}
-		err = c.Run()
-		if err != nil && !execOpts.IgnoreError {
-			return nil, err
-		}
-		return func(_ string) (string, error) {
-			return buf.String(), nil
-		}, nil
 	case "template":
 		text, err := cg.EmitStringExpr(ctx, scope, args[0])
 		if err != nil {
@@ -910,7 +839,7 @@ func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope,
 		// Must be a named reference.
 		obj := scope.Lookup(expr.Name())
 		if obj == nil {
-			return nil, errors.WithStack(ErrCodeGen{expr.IdentNode(), ErrUndefinedReference})
+			return nil, errors.WithStack(ErrCodeGen{expr, errors.Errorf("could not find reference")})
 		}
 
 		var v interface{}
@@ -923,10 +852,6 @@ func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope,
 		case *parser.ImportDecl:
 			importScope := obj.Data.(*parser.Scope)
 			importObj := importScope.Lookup(expr.Selector.Select.Name)
-			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)
@@ -944,11 +869,7 @@ 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})
-			}
-			return str, nil
+			return v.(string), nil
 		}, nil
 	}
 }
@@ -978,7 +899,7 @@ func (cg *CodeGen) EmitGroupChainStmt(ctx context.Context, scope *parser.Scope,
 		// Must be a named reference.
 		obj := scope.Lookup(expr.Name())
 		if obj == nil {
-			return gc, errors.WithStack(ErrCodeGen{expr.IdentNode(), ErrUndefinedReference})
+			return gc, errors.WithStack(ErrCodeGen{expr, errors.Errorf("could not find reference")})
 		}
 
 		var v interface{}
@@ -990,10 +911,6 @@ func (cg *CodeGen) EmitGroupChainStmt(ctx context.Context, scope *parser.Scope,
 		case *parser.ImportDecl:
 			importScope := obj.Data.(*parser.Scope)
 			importObj := importScope.Lookup(expr.Selector.Select.Name)
-			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)
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 8a27860..ea665d2 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -184,17 +184,9 @@ func (cg *CodeGen) Generate(ctx context.Context, mod *parser.Module, targets []T
 
 		switch typ.Primary() {
 		case parser.Group:
-			var ok bool
-			request, ok = v.(solver.Request)
-			if !ok {
-				return nil, errors.WithStack(ErrCodeGen{obj.Node, ErrBadCast})
-			}
+			request = v.(solver.Request)
 		case parser.Filesystem:
-			st, ok := v.(llb.State)
-			if !ok {
-				return nil, errors.WithStack(ErrCodeGen{obj.Node, ErrBadCast})
-			}
-
+			st := v.(llb.State)
 			request, err = cg.outputRequest(ctx, st, Output{})
 			if err != nil {
 				return nil, err
@@ -285,7 +277,7 @@ 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.NonEmptyStmts(), nil, nil)
 }
 
 type aliasCallback func(*parser.CallStmt, interface{}) bool
@@ -361,42 +353,26 @@ func (cg *CodeGen) EmitBlock(ctx context.Context, scope *parser.Scope, typ parse
 	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})
-	}
-	return
+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
 }
 
-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)
-	if err != nil {
-		return
-	}
-
-	str, ok := v.(string)
-	if !ok {
-		return str, errors.WithStack(ErrCodeGen{body, ErrBadCast})
+func (cg *CodeGen) EmitStringBlock(ctx context.Context, scope *parser.Scope, stmts []*parser.Stmt, chainStart interface{}) (string, error) {
+	v, err := cg.EmitBlock(ctx, scope, parser.Str, stmts, noopAliasCallback, chainStart)
+	if v == nil {
+		return "", err
 	}
-	return
+	return v.(string), err
 }
 
-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, stmts []*parser.Stmt, ac aliasCallback, chainStart interface{}) (solver.Request, error) {
+	v, err := cg.EmitBlock(ctx, scope, parser.Group, stmts, ac, chainStart)
 	if err != nil {
 		return nil, err
 	}
 
-	requests, ok := v.([]solver.Request)
-	if !ok {
-		return nil, errors.WithStack(ErrCodeGen{body, ErrBadCast})
-	}
+	requests := v.([]solver.Request)
 	if len(requests) == 1 {
 		return requests[0], nil
 	}
@@ -408,20 +384,19 @@ func (cg *CodeGen) EmitFuncLit(ctx context.Context, scope *parser.Scope, lit *pa
 	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.NonEmptyStmts(), ac, nil)
 	case parser.Str:
-		return cg.EmitStringBlock(ctx, scope, lit.Body, nil)
+		return cg.EmitStringBlock(ctx, scope, lit.Body.NonEmptyStmts(), nil)
 	case parser.Option:
-		return cg.EmitOptionBlock(ctx, scope, op, lit.Body, ac)
+		return cg.EmitOptions(ctx, scope, op, lit.Body.NonEmptyStmts(), ac)
 	case parser.Group:
-		return cg.EmitGroupBlock(ctx, scope, lit.Body, ac, nil)
+		return cg.EmitGroupBlock(ctx, scope, lit.Body.NonEmptyStmts(), ac, 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) {
-	stmts := body.NonEmptyStmts()
+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":
 		return cg.EmitImageOptions(ctx, scope, op, stmts)
@@ -451,8 +426,6 @@ func (cg *CodeGen) EmitOptionBlock(ctx context.Context, scope *parser.Scope, op
 		return cg.EmitCopyOptions(ctx, scope, op, stmts)
 	case "template":
 		return cg.EmitTemplateOptions(ctx, scope, op, stmts)
-	case "localRun":
-		return cg.EmitLocalExecOptions(ctx, scope, op, stmts)
 	default:
 		return opts, errors.Errorf("call stmt does not support options: %s", op)
 	}
@@ -485,10 +458,6 @@ func (cg *CodeGen) EmitImageOptions(ctx context.Context, scope *parser.Scope, op
 
 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())
-	if obj == nil {
-		return opts, errors.WithStack(ErrCodeGen{expr.IdentNode(), ErrUndefinedReference})
-	}
-
 	switch obj.Kind {
 	case parser.DeclKind:
 		switch n := obj.Node.(type) {
@@ -497,10 +466,6 @@ func (cg *CodeGen) EmitOptionLookup(ctx context.Context, scope *parser.Scope, ex
 		case *parser.ImportDecl:
 			importScope := obj.Data.(*parser.Scope)
 			importObj := importScope.Lookup(expr.Selector.Select.Name)
-			if importObj == nil {
-				return opts, errors.WithStack(ErrCodeGen{expr.Selector, ErrUndefinedReference})
-			}
-
 			switch m := importObj.Node.(type) {
 			case *parser.FuncDecl:
 				return cg.EmitOptionFuncDecl(ctx, scope, m, args)
@@ -951,45 +916,6 @@ func (cg *CodeGen) EmitTemplateOptions(ctx context.Context, scope *parser.Scope,
 	return opts, nil
 }
 
-type LocalRunOptions struct {
-	IgnoreError   bool
-	OnlyStderr    bool
-	IncludeStderr bool
-}
-
-func (cg *CodeGen) EmitLocalExecOptions(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.Name() {
-			case "ignoreError":
-				opts = append(opts, func(o *LocalRunOptions) {
-					o.IgnoreError = true
-				})
-			case "onlyStderr":
-				opts = append(opts, func(o *LocalRunOptions) {
-					o.OnlyStderr = true
-				})
-			case "includeStderr":
-				opts = append(opts, func(o *LocalRunOptions) {
-					o.IncludeStderr = true
-				})
-			case "shlex":
-				opts = append(opts, &shlexOption{})
-			default:
-				iopts, err := cg.EmitOptionLookup(ctx, scope, stmt.Call.Func, args, op)
-				if err != nil {
-					return opts, err
-				}
-				opts = append(opts, iopts...)
-			}
-		}
-	}
-	return opts, nil
-}
-
-type shlexOption struct{}
-
 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 {
@@ -1081,8 +1007,6 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op
 				}
 
 				opts = append(opts, llb.Security(securityMode))
-			case "shlex":
-				opts = append(opts, &shlexOption{})
 			case "host":
 				host, err := cg.EmitStringExpr(ctx, scope, args[0])
 				if err != nil {
diff --git a/codegen/debug.go b/codegen/debug.go
index 0aa92c1..5e06ead 100644
--- a/codegen/debug.go
+++ b/codegen/debug.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -20,11 +21,10 @@ import (
 	"github.com/openllb/hlb/checker"
 	"github.com/openllb/hlb/parser"
 	"github.com/openllb/hlb/report"
-	"github.com/pkg/errors"
 )
 
 var (
-	ErrDebugExit = errors.Errorf("exiting debugger")
+	ErrDebugExit = errors.New("exiting debugger")
 )
 
 type Debugger func(ctx context.Context, scope *parser.Scope, node parser.Node, value interface{}) error
@@ -288,12 +288,8 @@ func NewDebugger(c *client.Client, w io.Writer, r *bufio.Reader, ibs map[string]
 					if fun != nil {
 						args := fun.Params.List
 						for _, arg := range args {
-							obj := s.scope.Lookup(arg.Name.Name)
-							if obj == nil {
-								fmt.Fprintf(w, "err: %s\n", errors.WithStack(ErrCodeGen{arg, ErrUndefinedReference}))
-								continue
-							}
-							fmt.Fprintf(w, "%s %s = %#v\n", arg.Type, arg.Name, obj.Data)
+							data := s.scope.Lookup(arg.Name.Name).Data
+							fmt.Fprintf(w, "%s %s = %#v\n", arg.Type, arg.Name, data)
 						}
 					}
 				case "next", "n":
diff --git a/codegen/decl.go b/codegen/decl.go
index 15c4ca4..f32004c 100644
--- a/codegen/decl.go
+++ b/codegen/decl.go
@@ -34,55 +34,37 @@ 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.NonEmptyStmts(), ac, chainStart)
 	case parser.Option:
-		return cg.EmitOptionBlock(ctx, fun.Scope, string(fun.Type.Secondary()), fun.Body, ac)
+		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, chainStart)
+		return cg.EmitStringBlock(ctx, fun.Scope, fun.Body.NonEmptyStmts(), chainStart)
 	case parser.Group:
-		return cg.EmitGroupBlock(ctx, fun.Scope, fun.Body, ac, chainStart)
+		return cg.EmitGroupBlock(ctx, fun.Scope, fun.Body.NonEmptyStmts(), ac, 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) {
+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)
-	if err != nil {
-		return
-	}
-
-	st, ok := v.(llb.State)
-	if !ok {
-		return st, errors.WithStack(ErrCodeGen{fun, ErrBadCast})
-	}
-	return
+	return v.(llb.State), err
 }
 
-func (cg *CodeGen) EmitOptionFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr) (opts []interface{}, err error) {
+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 err != nil {
-		return
-	}
-
-	opts, ok := v.([]interface{})
-	if !ok {
-		return opts, errors.WithStack(ErrCodeGen{fun, ErrBadCast})
+	if v == nil {
+		return nil, err
 	}
-	return
+	return v.([]interface{}), err
 }
 
-func (cg *CodeGen) EmitStringFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (str string, err error) {
+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 err != nil {
-		return
-	}
-
-	str, ok := v.(string)
-	if !ok {
-		return str, errors.WithStack(ErrCodeGen{fun, ErrBadCast})
+	if v == nil {
+		return "", err
 	}
-	return
+	return v.(string), err
 }
 
 func (cg *CodeGen) EmitGroupFuncDecl(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback, chainStart interface{}) (solver.Request, error) {
@@ -128,43 +110,28 @@ 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, args []*parser.Expr, chainStart interface{}) (st llb.State, err error) {
+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 err != nil {
-		return
-	}
-
-	st, ok := v.(llb.State)
-	if !ok {
-		return st, errors.WithStack(ErrCodeGen{alias, ErrBadCast})
+	if v == nil {
+		return llb.Scratch(), err
 	}
-	return
+	return v.(llb.State), err
 }
 
-func (cg *CodeGen) EmitStringAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (str string, err error) {
+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 err != nil {
-		return
-	}
-
-	str, ok := v.(string)
-	if !ok {
-		return str, errors.WithStack(ErrCodeGen{alias, ErrBadCast})
+	if v == nil {
+		return "", err
 	}
-	return
+	return v.(string), err
 }
 
-func (cg *CodeGen) EmitGroupAliasDecl(ctx context.Context, scope *parser.Scope, alias *parser.AliasDecl, args []*parser.Expr, chainStart interface{}) (request solver.Request, err error) {
+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 err != nil {
-		return
-	}
-
-	request, ok := v.(solver.Request)
-	if !ok {
-		return request, errors.WithStack(ErrCodeGen{alias, ErrBadCast})
+	if v == nil {
+		return nil, err
 	}
-	return
+	return v.(solver.Request), err
 }
 
 func (cg *CodeGen) ParameterizedScope(ctx context.Context, scope *parser.Scope, fun *parser.FuncDecl, args []*parser.Expr, ac aliasCallback) error {
diff --git a/codegen/errors.go b/codegen/errors.go
index fbb55fc..d78ac43 100644
--- a/codegen/errors.go
+++ b/codegen/errors.go
@@ -5,19 +5,6 @@ import (
 
 	"github.com/openllb/hlb/checker"
 	"github.com/openllb/hlb/parser"
-	"github.com/pkg/errors"
-)
-
-var (
-	// ErrBadCast is returned when codegen was unable to cast an interface to an
-	// expected type. Type checking should occur in the checker so that's often
-	// where the bug should be fixed, but it may be in the codegen.
-	ErrBadCast = errors.Errorf("bad cast")
-
-	// ErrUndefinedReference is returned when codegen was unable to lookup an
-	// object for a scope. Lexical scope checking should occur in the checker so
-	// that's often where the bug shuold be fixed, but it may be in the codegen.
-	ErrUndefinedReference = errors.Errorf("undefined reference")
 )
 
 type ErrCodeGen struct {
diff --git a/codegen/expr.go b/codegen/expr.go
index 121276b..1694f4e 100644
--- a/codegen/expr.go
+++ b/codegen/expr.go
@@ -24,7 +24,7 @@ func (cg *CodeGen) EmitStringExpr(ctx context.Context, scope *parser.Scope, expr
 		}
 		return expr.BasicLit.HereDoc.Value, nil
 	case expr.FuncLit != nil:
-		return cg.EmitStringBlock(ctx, scope, expr.FuncLit.Body, nil)
+		return cg.EmitStringBlock(ctx, scope, expr.FuncLit.Body.NonEmptyStmts(), nil)
 	default:
 		return "", errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown string expr")})
 	}
@@ -33,11 +33,7 @@ func (cg *CodeGen) EmitStringExpr(ctx context.Context, scope *parser.Scope, expr
 func (cg *CodeGen) EmitIntExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr) (int, error) {
 	switch {
 	case expr.Ident != nil:
-		obj := scope.Lookup(expr.Name())
-		if obj == nil {
-			return 0, errors.WithStack(ErrCodeGen{expr.IdentNode(), ErrUndefinedReference})
-		}
-
+		obj := scope.Lookup(expr.Ident.Name)
 		switch obj.Kind {
 		case parser.DeclKind:
 			panic("unimplemented")
@@ -65,11 +61,7 @@ func (cg *CodeGen) EmitIntExpr(ctx context.Context, scope *parser.Scope, expr *p
 func (cg *CodeGen) EmitBoolExpr(ctx context.Context, scope *parser.Scope, expr *parser.Expr) (bool, error) {
 	switch {
 	case expr.Ident != nil:
-		obj := scope.Lookup(expr.Name())
-		if obj == nil {
-			return false, errors.WithStack(ErrCodeGen{expr.IdentNode(), ErrUndefinedReference})
-		}
-
+		obj := scope.Lookup(expr.Ident.Name)
 		switch obj.Kind {
 		case parser.DeclKind:
 			panic("unimplemented")
@@ -111,7 +103,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.NonEmptyStmts(), ac, nil)
 	default:
 		return st, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown fs expr")})
 	}
@@ -120,16 +112,16 @@ func (cg *CodeGen) EmitFilesystemExpr(ctx context.Context, scope *parser.Scope,
 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, expr.Selector != nil:
-		return cg.EmitOptionBlock(ctx, scope, op, parser.NewBlockStmt(&parser.Stmt{
+		return cg.EmitOptions(ctx, scope, op, []*parser.Stmt{{
 			Call: &parser.CallStmt{
 				Func: expr,
 				Args: args,
 			},
-		}), noopAliasCallback)
+		}}, 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.EmitOptions(ctx, scope, op, expr.FuncLit.Body.NonEmptyStmts(), noopAliasCallback)
 	default:
 		return opts, errors.WithStack(ErrCodeGen{expr, errors.Errorf("unknown option expr")})
 	}
@@ -158,9 +150,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.NonEmptyStmts(), ac, nil)
 		case parser.Filesystem:
-			st, err := cg.EmitFilesystemBlock(ctx, scope, expr.FuncLit.Body, ac, nil)
+			st, err := cg.EmitFilesystemBlock(ctx, scope, expr.FuncLit.Body.NonEmptyStmts(), ac, nil)
 			if err != nil {
 				return nil, err
 			}
diff --git a/docs/intro/quickstart.md b/docs/intro/quickstart.md
index eb3cdfe..c8ea3d6 100644
--- a/docs/intro/quickstart.md
+++ b/docs/intro/quickstart.md
@@ -21,7 +21,7 @@ Now that you have installed `hlb`, we can run our first build. Typically, we wil
 
 ```sh
 export BUILDKIT_HOST=docker-container://buildkitd
-echo 'fs default() { scratch; mkfile "/output" 0o644 "hello world"; }' | hlb run --target default,download=.
+echo 'fs default() { scratch; mkfile "/out" 0o644 "hello world"; }' | hlb run --download .
 ```
 
 Once the build has finished, you should end up with a file `output` in your working directory.
diff --git a/docs/reference.md b/docs/reference.md
index 2ffdb2b..05e6b1f 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -1,225 +1,21 @@
-## <span class='hlb-type'>fs</span> functions
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>copy</span>(<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>src</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>dst</span>)
+## Functions
+### <span class='hlb-type'>fs</span> generate(<span class='hlb-type'>fs</span> <span class='hlb-variable'>frontend</span>)
 
-!!! info "<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>"
-	the filesystem to copy from.
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>src</span>"
-	the path from the input filesystem.
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>dst</span>"
-	the path in the current filesystem.
-
-Copies a file from an input filesystem into the current filesystem.
-
-	#!hlb
-	fs default() {
-		copy scratch "src" "dst" with option {
-			allowEmptyWildcard
-			allowWildcard
-			chmod 0
-			chown "owner"
-			contentsOnly
-			createDestPath
-			createdTime "created"
-			followSymlinks
-			unpack
-		}
-	}
-
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>allowEmptyWildcard</span>()
-
-
-Allows wildcards to match no files in the path to copy.
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>allowWildcard</span>()
-
-
-Allows wildcards in the path to copy.
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>chmod</span>(<span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>)
-
-!!! info "<span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>"
-	the new permissions of the file.
-
-Modifies the permissions of the copied files.
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>chown</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>owner</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>owner</span>"
-	the user:group owner of the copy path.
-
-Change the owner of the copy path.
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>contentsOnly</span>()
-
-
-If the &#x60;src&#x60; path is a directory, only the contents of the directory is
-copied to the destination.
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>createDestPath</span>()
-
-
-Create the parent directories of the destination if they don&#x27;t already exist.
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>createdTime</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>created</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>created</span>"
-	the created time in the RFC3339 format.
-
-Sets the created time of the copy path.
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>followSymlinks</span>()
-
-
-Follow symlinks in the input filesystem and copy the symlink targets too.
-
-#### <span class='hlb-type'>option::copy</span> <span class='hlb-name'>unpack</span>()
-
-
-If the &#x60;src&#x60; path is an archive, attempt to unpack its contents into the
-destination.
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>dir</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
-	the new working directory.
-
-Sets the working directory for all subsequent calls in this filesystem block.
-
-	#!hlb
-	fs default() {
-		dir "path"
-	}
-
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>dockerLoad</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>"
-	the name of the Docker image.
-
-Loads the filesystem as a Docker image to the docker client found in your
-environment.
-
-	#!hlb
-	fs default() {
-		dockerLoad "ref"
-	}
-
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>dockerPush</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>"
-	a distribution reference. if not fully qualified, it will be expanded the same as the docker CLI.
-
-Pushes the filesystem to a registry following the distribution
-spec: https://github.com/opencontainers/distribution-spec/
-
-	#!hlb
-	fs default() {
-		dockerPush "ref"
-	}
-
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>download</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>"
-	the destination filepath for the filesystem contents.
-
-Downloads the filesystem to a local path.
-
-	#!hlb
-	fs default() {
-		download "localPath"
-	}
-
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>downloadDockerTarball</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>"
-	the destination filepath for the tarball.
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>"
-	the name of the Docker image.
-
-Downloads the filesystem as a Docker image tarball to a local path.
-The tarball is able to be loaded into a docker engine via &#x60;docker load&#x60;.
-See: https://docs.docker.com/engine/reference/commandline/save/
-and https://docs.docker.com/engine/reference/commandline/load/
-
-	#!hlb
-	fs default() {
-		downloadDockerTarball "localPath" "ref"
-	}
-
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>downloadOCITarball</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>"
-	the destination filepath for the tarball.
-
-Downloads the filesystem as a OCI filesystem bundle to a local path.
-See: https://github.com/opencontainers/runtime-spec/blob/master/bundle.md
-
-	#!hlb
-	fs default() {
-		downloadOCITarball "localPath"
-	}
-
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>downloadTarball</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>"
-	the destination filepath for the tarball.
-
-Downloads the filesystem as a tarball to a local path.
-
-	#!hlb
-	fs default() {
-		downloadTarball "localPath"
-	}
-
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>env</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>"
-	the environment key.
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>"
-	the environment value.
-
-Sets an environment key pair for all subsequent calls in this filesystem
-block.
-
-	#!hlb
-	fs default() {
-		env "key" "value"
-	}
-
-
-
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>frontend</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>source</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>source</span>"
-	
+!!! info "<span class='hlb-type'>fs</span> <span class='hlb-variable'>frontend</span>"
+	a filesystem with an executable that runs a BuildKit gateway GRPC client over stdio.
 
 Generates a filesystem using an external frontend.
 
 	#!hlb
 	fs default() {
-		frontend "source" with option {
-			input "key" scratch
-			opt "key" "value"
+		generate fs { scratch; } with option {
+			frontendInput "key" fs { scratch; }
+			frontendOpt "key" "value"
 		}
 	}
 
 
-#### <span class='hlb-type'>option::frontend</span> <span class='hlb-name'>input</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>fs</span> <span class='hlb-variable'>value</span>)
+#### <span class='hlb-type'>option::generate</span> frontendInput(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>fs</span> <span class='hlb-variable'>value</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>"
 	an unique key for the input.
@@ -229,7 +25,7 @@ Generates a filesystem using an external frontend.
 Provide an input filesystem to the external frontend. Read the documentation
 for the frontend to see what it will accept.
 
-#### <span class='hlb-type'>option::frontend</span> <span class='hlb-name'>opt</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
+#### <span class='hlb-type'>option::generate</span> frontendOpt(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>"
 	an unique key for the option.
@@ -240,7 +36,7 @@ Provide a key value pair to the external frontend. Read the documentation
 for the frontend to see what it will accept.
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>git</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>remote</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>)
+### <span class='hlb-type'>fs</span> git(<span class='hlb-type'>string</span> <span class='hlb-variable'>remote</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>remote</span>"
 	the fully qualified git remote.
@@ -258,13 +54,13 @@ a git reference. Note that by default, the &#x60;.git&#x60; directory is not inc
 	}
 
 
-#### <span class='hlb-type'>option::git</span> <span class='hlb-name'>keepGitDir</span>()
+#### <span class='hlb-type'>option::git</span> keepGitDir()
 
 
 Keeps the &#x60;.git&#x60; directory of the git repository.
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>http</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>url</span>)
+### <span class='hlb-type'>fs</span> http(<span class='hlb-type'>string</span> <span class='hlb-variable'>url</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>url</span>"
 	a fully-qualified URL to send a HTTP GET request.
@@ -275,27 +71,27 @@ A filesystem with a file retrieved from a HTTP URL.
 	fs default() {
 		http "url" with option {
 			checksum "digest"
-			chmod 0
+			chmod 0644
 			filename "name"
 		}
 	}
 
 
-#### <span class='hlb-type'>option::http</span> <span class='hlb-name'>checksum</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>digest</span>)
+#### <span class='hlb-type'>option::http</span> checksum(<span class='hlb-type'>string</span> <span class='hlb-variable'>digest</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>digest</span>"
 	a checksum in the form of an OCI digest. https://github.com/opencontainers/image-spec/blob/master/descriptor.md#digests
 
 Verifies the checksum of the retrieved file against a digest.
 
-#### <span class='hlb-type'>option::http</span> <span class='hlb-name'>chmod</span>(<span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>)
+#### <span class='hlb-type'>option::http</span> chmod(<span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>)
 
-!!! info "<span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>"
-	the new permissions of the file.
+!!! info "<span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>"
+	the new permissions of the file in octal.
 
 Modifies the permissions of the retrieved file.
 
-#### <span class='hlb-type'>option::http</span> <span class='hlb-name'>filename</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>)
+#### <span class='hlb-type'>option::http</span> filename(<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>"
 	the name of the file.
@@ -303,7 +99,7 @@ Modifies the permissions of the retrieved file.
 Writes the retrieved file with a specified name.
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>image</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>)
+### <span class='hlb-type'>fs</span> image(<span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>ref</span>"
 	a docker registry reference. if not fully qualified, it will be expanded the same as the docker CLI.
@@ -318,14 +114,14 @@ An OCI image&#x27;s filesystem.
 	}
 
 
-#### <span class='hlb-type'>option::image</span> <span class='hlb-name'>resolve</span>()
+#### <span class='hlb-type'>option::image</span> resolve()
 
 
 Resolves the OCI Image Config and inherit its environment, working directory,
 and entrypoint.
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>local</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
+### <span class='hlb-type'>fs</span> local(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
 	the local path to the directory to sync up.
@@ -343,21 +139,21 @@ local system.
 	}
 
 
-#### <span class='hlb-type'>option::local</span> <span class='hlb-name'>excludePatterns</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>pattern</span>)
+#### <span class='hlb-type'>option::local</span> excludePatterns(<span class='hlb-type'>string</span> <span class='hlb-variable'>pattern</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>pattern</span>"
 	a list of patterns for files that should not be synced.
 
 Sync only files that do not match any of the excluded patterns.
 
-#### <span class='hlb-type'>option::local</span> <span class='hlb-name'>followPaths</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
+#### <span class='hlb-type'>option::local</span> followPaths(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
 	a list of paths to files that may be symlinks.
 
 Sync the targets of symlinks if path is to a symlink.
 
-#### <span class='hlb-type'>option::local</span> <span class='hlb-name'>includePatterns</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>pattern</span>)
+#### <span class='hlb-type'>option::local</span> includePatterns(<span class='hlb-type'>string</span> <span class='hlb-variable'>pattern</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>pattern</span>"
 	a list of patterns for files that should be synced.
@@ -365,38 +161,140 @@ Sync the targets of symlinks if path is to a symlink.
 Sync only files that match any of the included patterns.
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>mkdir</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>, <span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>)
+### <span class='hlb-type'>fs</span> scratch()
+
+
+An empty filesystem.
+
+	#!hlb
+	fs default() {
+		scratch
+	}
+
+
+
+
+## Methods
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) copy(<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>src</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>dst</span>)
+
+!!! info "<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>"
+	the filesystem to copy from.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>src</span>"
+	the path from the input filesystem.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>dst</span>"
+	the path in the current filesystem.
+
+Copies a file from an input filesystem into the current filesystem.
+
+	#!hlb
+	fs default() {
+		scratch
+		copy fs { scratch; } "src" "dst" with option {
+			contentsOnly
+			createDestPath
+			followSymlinks
+			unpack
+		}
+	}
+
+#### <span class='hlb-type'>option::copy</span> contentsOnly()
+
+
+If the &#x60;src&#x60; path is a directory, only the contents of the directory is
+copied to the destination.
+
+
+
+#### <span class='hlb-type'>option::copy</span> createDestPath()
+
+
+Create the parent directories of the destination if they don&#x27;t already exist.
+
+
+
+#### <span class='hlb-type'>option::copy</span> followSymlinks()
+
+
+Follow symlinks in the input filesystem and copy the symlink targets too.
+
+
+
+#### <span class='hlb-type'>option::copy</span> unpack()
+
+
+If the &#x60;src&#x60; path is an archive, attempt to unpack its contents into the
+destination.
+
+
+
+
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) dir(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
+
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
+	the new working directory.
+
+Sets the working directory for all subsequent calls in this filesystem block.
+
+	#!hlb
+	fs default() {
+		scratch
+		dir "path"
+	}
+
+
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) env(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
+
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>"
+	the environment key.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>"
+	the environment value.
+
+Sets an environment key pair for all subsequent calls in this filesystem
+block.
+
+	#!hlb
+	fs default() {
+		scratch
+		env "key" "value"
+	}
+
+
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) mkdir(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>, <span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
 	the path of the directory.
-!!! info "<span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>"
+!!! info "<span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>"
 	the permissions of the directory.
 
 Creates a directory in the current filesystem.
 
 	#!hlb
 	fs default() {
-		mkdir "path" 0 with option {
+		scratch
+		mkdir "path" 0644 with option {
 			chown "owner"
 			createParents
 			createdTime "created"
 		}
 	}
 
-
-#### <span class='hlb-type'>option::mkdir</span> <span class='hlb-name'>chown</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>owner</span>)
+#### <span class='hlb-type'>option::mkdir</span> chown(<span class='hlb-type'>string</span> <span class='hlb-variable'>owner</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>owner</span>"
 	the user:group owner of the directory.
 
 Change the owner of the directory.
 
-#### <span class='hlb-type'>option::mkdir</span> <span class='hlb-name'>createParents</span>()
+
+
+#### <span class='hlb-type'>option::mkdir</span> createParents()
 
 
 Create the parent directories if they don&#x27;t exist already.
 
-#### <span class='hlb-type'>option::mkdir</span> <span class='hlb-name'>createdTime</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>created</span>)
+
+
+#### <span class='hlb-type'>option::mkdir</span> createdTime(<span class='hlb-type'>string</span> <span class='hlb-variable'>created</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>created</span>"
 	the created time in the RFC3339 format.
@@ -404,11 +302,13 @@ Create the parent directories if they don&#x27;t exist already.
 Sets the created time of the directory.
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>mkfile</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>, <span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>content</span>)
+
+
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) mkfile(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>, <span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>content</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
 	the path of the file.
-!!! info "<span class='hlb-type'>int</span> <span class='hlb-variable'>filemode</span>"
+!!! info "<span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>"
 	the permissions of the file.
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>content</span>"
 	the contents of the file.
@@ -417,21 +317,23 @@ Creates a file in the current filesystem.
 
 	#!hlb
 	fs default() {
-		mkfile "path" 0 "content" with option {
+		scratch
+		mkfile "path" 0644 "content" with option {
 			chown "owner"
 			createdTime "created"
 		}
 	}
 
-
-#### <span class='hlb-type'>option::mkfile</span> <span class='hlb-name'>chown</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>owner</span>)
+#### <span class='hlb-type'>option::mkfile</span> chown(<span class='hlb-type'>string</span> <span class='hlb-variable'>owner</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>owner</span>"
 	the user:group owner of the file.
 
 Change the owner of the file.
 
-#### <span class='hlb-type'>option::mkfile</span> <span class='hlb-name'>createdTime</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>created</span>)
+
+
+#### <span class='hlb-type'>option::mkfile</span> createdTime(<span class='hlb-type'>string</span> <span class='hlb-variable'>created</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>created</span>"
 	the created time in the RFC3339 format.
@@ -439,7 +341,9 @@ Change the owner of the file.
 Sets the created time of the file.
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>rm</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
+
+
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) rm(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
 	the path of the file to remove.
@@ -448,63 +352,83 @@ Removes a file from the current filesystem.
 
 	#!hlb
 	fs default() {
+		scratch
 		rm "path" with option {
 			allowNotFound
-			allowWildcard
+			allowWildcards
 		}
 	}
 
-
-#### <span class='hlb-type'>option::rm</span> <span class='hlb-name'>allowNotFound</span>()
+#### <span class='hlb-type'>option::rm</span> allowNotFound()
 
 
 Allows the file to not be found.
 
-#### <span class='hlb-type'>option::rm</span> <span class='hlb-name'>allowWildcard</span>()
+
+
+#### <span class='hlb-type'>option::rm</span> allowWildcards()
 
 
 Allows wildcards in the path to remove.
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>run</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>arg</span>)
+
+
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) run(<span class='hlb-type'>string</span> <span class='hlb-variable'>arg</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>arg</span>"
-	are optional arguments to execute.
+	a command to execute.
 
 Executes an command in the current filesystem.
 If no arguments are given, it will execute the current args set on the
 filesystem.
-If exactly one arg is given it will be wrapped with /bin/sh -c &#x27;arg&#x27;.
+If exactly one arg is given, it will attempt to parse as a command with
+shell-style quoting. If it remains a single element, it is executed directly,
+otherwise it is run with the current shell.
 If more than one arg is given, it will be executed directly, without a shell.
 
 	#!hlb
 	fs default() {
+		scratch
 		run "arg" with option {
 			dir "path"
 			env "key" "value"
-			forward "src" "dest"
 			host "hostname" "address"
-			ignoreCache
-			mount scratch "mountPoint"
+			mount fs { scratch; } "mountPoint" with option {
+				readonly
+				tmpfs
+				sourcePath "path"
+				cache "cacheid" "sharingmode"
+			}
 			network "networkmode"
 			readonlyRootfs
-			secret "localPath" "mountPoint"
+			secret "localPath" "mountPoint" with option {
+				uid 0
+				gid 0
+				mode 0644
+			}
 			security "securitymode"
-			shlex
-			ssh
+			ssh with option {
+				target "mountPoint"
+				localPath "path"
+				uid 0
+				gid 0
+				mode 0644
+			}
 			user "name"
 		}
 	}
 
-
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>dir</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
+#### <span class='hlb-type'>option::run</span> dir(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
 	the new working directory.
 
 Sets the working directory for the duration of the run command.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>env</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
+
+
+#### <span class='hlb-type'>option::run</span> env(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>"
 	the environment key.
@@ -513,18 +437,9 @@ Sets the working directory for the duration of the run command.
 
 Sets an environment key pair for the duration of the run command.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>forward</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>src</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>dest</span>)
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>src</span>"
-	a fully qualified URI to forward traffic to/from.
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>dest</span>"
-	a mountpoint for a unix domain socket that is forwarded to/from.
-
-Forwards traffic to/from a local source to a unix domain socket mounted for
-the duration of the run command. The source must be a fully qualified URI
-where the scheme must be either &#x60;unix://&#x60; or &#x60;tcp://&#x60;.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>host</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>hostname</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>address</span>)
+#### <span class='hlb-type'>option::run</span> host(<span class='hlb-type'>string</span> <span class='hlb-variable'>hostname</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>address</span>)
 
 !!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>hostname</span>"
 	the host name of the entry, may include spaces to delimit multiple host names.
@@ -533,13 +448,9 @@ where the scheme must be either &#x60;unix://&#x60; or &#x60;tcp://&#x60;.
 
 Adds a host entry to /etc/hosts for the duration of the run command.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>ignoreCache</span>()
-
 
-Ignore any previously cached results for the run command.
-@ return an option to ignore existing cache for the run command.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>mount</span>(<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>mountPoint</span>)
+#### <span class='hlb-type'>option::run</span> mount(<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>mountPoint</span>)
 
 !!! info "<span class='hlb-type'>fs</span> <span class='hlb-variable'>input</span>"
 	the additional filesystem to mount. the input&#x27;s root filesystem becomes available from the mountPoint directory.
@@ -548,241 +459,195 @@ Ignore any previously cached results for the run command.
 
 Attaches an additional filesystem for the duration of the run command.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>network</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>networkmode</span>)
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>networkmode</span>"
-	the network mode of the container, must be one of the following: - unset: use the default network provider. - host: use the host&#x27;s network namespace. - none: disable networking.
+#### <span class='hlb-type'>option::mount</span> readonly()
 
-Sets the networking mode for the duration of the run command. By default, the
-value is &#x60;unset&#x60; (using BuildKit&#x27;s CNI provider, otherwise its host
-namespace).
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>readonlyRootfs</span>()
+Sets the mount to be attached as a read-only filesystem.
 
+#### <span class='hlb-type'>option::mount</span> tmpfs()
 
-Sets the rootfs as read-only for the duration of the run command.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>secret</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>mountPoint</span>)
+Sets the mount to be attached as a tmpfs filesystem.
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>"
-	the filepath for a secure file or directory.
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>mountPoint</span>"
-	the directory where the secret is attached.
+#### <span class='hlb-type'>option::mount</span> sourcePath(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
-Mounts a secure file for the duration of the run command. Secrets are
-attached via a tmpfs mount, so all the data stays in volatile memory.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
+	the path in the input filesystem.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>security</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>securitymode</span>)
+Mount a path from the input filesystem. By default, the root of the input
+filesystem is mounted.
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>securitymode</span>"
-	the security mode of the container, must be one of the following: - sandbox: use the default containerd seccomp profile. - insecure: enables all capabilities.
+#### <span class='hlb-type'>option::mount</span> cache(<span class='hlb-type'>string</span> <span class='hlb-variable'>cacheid</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>sharingmode</span>)
 
-Sets the security mode for the duration of the run command. By default, the
-value is &#x60;sandbox&#x60;.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>cacheid</span>"
+	the unique ID to identify the cache.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>sharingmode</span>"
+	the sharing mode of the cache, must be one of the following: - shared: can be used concurrently by multiple writers. - private: creates a new mount if there are multiple writers. - locked: pauses additional writers until the first one releases the mount.
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>shlex</span>()
+Cache a snapshot of the mount after the run command has executed. A cacheid
+must be provided to uniquely identify the cache mount.
+Compilers and package managers commonly have an option to specify cache
+directories. Depending on their implementation, it may be safe to share the
+cache with concurrent processes. This is adjusted via the &#x60;sharingmode&#x60;
+argument.
+The cache is modified every time the parent run command is executed. A cache
+could also be managed by not using the &#x60;cache&#x60; option. Instead, the mount can
+be aliased, and then pushed as an image, so that there it can be a stable
+snapshot, or updated externally.
 
 
-Attempt to lex the single-argument shell command provided to &#x60;run&#x60;
-to determine if a &#x60;/bin/sh -c &#x27;...&#x27;&#x60; wrapper needs to be added.
-
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>ssh</span>()
+#### <span class='hlb-type'>option::run</span> network(<span class='hlb-type'>string</span> <span class='hlb-variable'>networkmode</span>)
 
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>networkmode</span>"
+	the network mode of the container, must be one of the following: - unset: use the default network provider. - host: use the host&#x27;s network namespace. - none: disable networking.
 
-Mounts a SSH socket for the duration of the run command. By default, it will
-try to use the SSH socket found from $SSH_AUTH_SOCK. Otherwise, an option
-&#x60;localPath&#x60; can be provided to specify a filepath to a SSH auth socket or
-*.pem file.
+Sets the networking mode for the duration of the run command. By default, the
+value is &quot;unset&quot; (using BuildKit&#x27;s CNI provider, otherwise its host
+namespace).
 
-#### <span class='hlb-type'>option::run</span> <span class='hlb-name'>user</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>)
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>"
-	the name of the user.
 
-Sets the current user for the duration of the run command.
+#### <span class='hlb-type'>option::run</span> readonlyRootfs()
 
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>scratch</span>()
+Sets the rootfs as read-only for the duration of the run command.
 
 
-An empty filesystem.
 
-	#!hlb
-	fs default() {
-		scratch
-	}
+#### <span class='hlb-type'>option::run</span> secret(<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>mountPoint</span>)
 
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>localPath</span>"
+	the filepath for a secure file or directory.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>mountPoint</span>"
+	the directory where the secret is attached.
 
+Mounts a secure file for the duration of the run command. Secrets are
+attached via a tmpfs mount, so all the data stays in volatile memory.
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>shell</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>arg</span>)
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>arg</span>"
-	the list of args used to prefix &#x60;run&#x60; statements.
+#### <span class='hlb-type'>option::secret</span> uid(<span class='hlb-type'>int</span> <span class='hlb-variable'>id</span>)
 
-Sets the current shell command to use when executing subsequent &#x60;run&#x60;
-methods. By default, this is [&quot;sh&quot;, &quot;-c&quot;].
+!!! info "<span class='hlb-type'>int</span> <span class='hlb-variable'>id</span>"
+	the user id.
 
-	#!hlb
-	fs default() {
-		shell "arg"
-	}
+Sets the user ID for the secure file. By default, the UID is 0.
 
+#### <span class='hlb-type'>option::secret</span> gid(<span class='hlb-type'>int</span> <span class='hlb-variable'>id</span>)
 
+!!! info "<span class='hlb-type'>int</span> <span class='hlb-variable'>id</span>"
+	the group id.
 
-### <span class='hlb-type'>fs</span> <span class='hlb-name'>user</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>)
+Sets the group ID for the secure file. By default, the GID is 0.
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>"
-	the name of the user.
+#### <span class='hlb-type'>option::secret</span> mode(<span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>)
 
-Sets the current user for all subsequent calls in this filesystem block.
+!!! info "<span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>"
+	the new permissions of the secure file in octal.
 
-	#!hlb
-	fs default() {
-		user "name"
-	}
+Sets the permissions for the secure file. By default, the file mode is 0600.
 
 
+#### <span class='hlb-type'>option::run</span> security(<span class='hlb-type'>string</span> <span class='hlb-variable'>securitymode</span>)
 
-## <span class='hlb-type'>string</span> functions
-### <span class='hlb-type'>string</span> <span class='hlb-name'>format</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>formatString</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>values</span>)
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>securitymode</span>"
+	the security mode of the container, must be one of the following: - sandbox: use the default containerd seccomp profile. - insecure: enables all capabilities.
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>formatString</span>"
-	the format specifier.
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>values</span>"
-	the list of values to be interpolated into the format specifier.
+Sets the security mode for the duration of the run command. By default, the
+value is &quot;sandbox&quot;.
 
-A format specifier that is interpolated with values.
 
-	#!hlb
-	string myString() {
-		format "formatString" "values"
-	}
 
+#### <span class='hlb-type'>option::run</span> ssh()
 
 
-### <span class='hlb-type'>string</span> <span class='hlb-name'>localArch</span>()
+Mounts a SSH socket for the duration of the run command. By default, it will
+try to use the SSH socket found from $SSH_AUTH_SOCK. Otherwise, an option
+&#x60;localPath&#x60; can be provided to specify a filepath to a SSH auth socket or
+*.pem file.
 
 
-The architecture for the clients local environment.
+#### <span class='hlb-type'>option::ssh</span> target(<span class='hlb-type'>string</span> <span class='hlb-variable'>mountPoint</span>)
 
-	#!hlb
-	string myString() {
-		localArch
-	}
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>mountPoint</span>"
+	the directory where the SSH agent socket is attached.
 
+Sets the target directory to mount the SSH agent socket. By default, it is
+mounted to &#x60;/run/buildkit/ssh_agent.${N}&#x60;, where N is the index of the SSH
+socket. If $SSH_AUTH_SOCK is not set, it will set SSH_AUTH_SOCK to the
+mountPoint.
 
+#### <span class='hlb-type'>option::ssh</span> localPath(<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>)
 
-### <span class='hlb-type'>string</span> <span class='hlb-name'>localCwd</span>()
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>path</span>"
+	the path to a local SSH agent socket or PEM key.
 
+Sets the path to a local SSH agent socket or PEM key, with support for
+passphrases. By default, the SSH agent defined by $SSH_AUTH_SOCK will be
+mounted into the container.
 
-The current working directory from the clients local environment.
+#### <span class='hlb-type'>option::ssh</span> uid(<span class='hlb-type'>int</span> <span class='hlb-variable'>id</span>)
 
-	#!hlb
-	string myString() {
-		localCwd
-	}
+!!! info "<span class='hlb-type'>int</span> <span class='hlb-variable'>id</span>"
+	the user ID.
 
+Sets the user ID for the SSH agent socket. By default, the UID is 0.
 
+#### <span class='hlb-type'>option::ssh</span> gid(<span class='hlb-type'>int</span> <span class='hlb-variable'>id</span>)
 
-### <span class='hlb-type'>string</span> <span class='hlb-name'>localEnv</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>)
+!!! info "<span class='hlb-type'>int</span> <span class='hlb-variable'>id</span>"
+	the group ID.
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>key</span>"
-	the environment variable&#x27;s key.
+Sets the group ID for the SSH agent socket. By default, the GID is 0.
 
-An environment variable from the client&#x27;s local environment.
+#### <span class='hlb-type'>option::ssh</span> mode(<span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>)
 
-	#!hlb
-	string myString() {
-		localEnv "key"
-	}
+!!! info "<span class='hlb-type'>octal</span> <span class='hlb-variable'>filemode</span>"
+	the new permissions of the SSH agent socket in octal.
 
+Sets the permissions for the SSH agent socket. By default, the file mode is
+0600.
 
 
-### <span class='hlb-type'>string</span> <span class='hlb-name'>localOs</span>()
+#### <span class='hlb-type'>option::run</span> user(<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>)
 
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>"
+	the name of the user.
 
-The OS from the clients local environment.
+Sets the current user for the duration of the run command.
 
-	#!hlb
-	string myString() {
-		localOs
-	}
 
 
 
-### <span class='hlb-type'>string</span> <span class='hlb-name'>localRun</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>command</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>args</span>)
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) shell(<span class='hlb-type'>string</span> <span class='hlb-variable'>arg</span>)
 
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>command</span>"
-	a command to execute.
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>args</span>"
-	optional arguments to the command.
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>arg</span>"
+	the list of args used to prefix &#x60;run&#x60; statements.
 
-Executes an command in the local environment.
-If exactly one arg is given it will be wrapped with /bin/sh -c &#x27;arg&#x27;.
-If more than one arg is given, it will be executed directly, without a shell.
+Sets the current shell command to use when executing subsequent &#x60;run&#x60;
+methods. By default, this is [&quot;sh&quot;, &quot;-c&quot;].
 
 	#!hlb
-	string myString() {
-		localRun "command" "args" with option {
-			ignoreError
-			includeStderr
-			onlyStderr
-			shlex
-		}
+	fs default() {
+		scratch
+		shell "arg"
 	}
 
 
-#### <span class='hlb-type'>option::localRun</span> <span class='hlb-name'>ignoreError</span>()
-
-
-If the command returns a non-zero status code ignore
-the failure and continue processing the hlb file.
-
-#### <span class='hlb-type'>option::localRun</span> <span class='hlb-name'>includeStderr</span>()
-
-
-Capture stderr intermixed with stdout on the command.
+### <span class='hlb-type'>fs</span> (<span class='hlb-type'>fs</span>) user(<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>)
 
-#### <span class='hlb-type'>option::localRun</span> <span class='hlb-name'>onlyStderr</span>()
-
-
-Only capture the stderr from the command, ignore stdout.
-
-#### <span class='hlb-type'>option::localRun</span> <span class='hlb-name'>shlex</span>()
-
-
-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'>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>"
-	the text of the template
+!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>"
+	the name of the user.
 
-Process text as a Go text template.
-For template syntax documentation see:
-https://golang.org/pkg/text/template/
+Sets the current user for all subsequent calls in this filesystem block.
 
 	#!hlb
-	string myString() {
-		template "text" with option {
-			stringField "name" "value"
-		}
+	fs default() {
+		scratch
+		user "name"
 	}
 
 
-#### <span class='hlb-type'>option::template</span> <span class='hlb-name'>stringField</span>(<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>, <span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>)
-
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>name</span>"
-	the name of the field inside the template
-!!! info "<span class='hlb-type'>string</span> <span class='hlb-variable'>value</span>"
-	the value of the field inside the template
-
-Add a string field with provided name to be available
-inside the template.
-
-
 
 <style>
 .hlb-type {
@@ -792,8 +657,4 @@ inside the template.
 .hlb-variable {
 	color: #0366d6
 }
-
-.hlb-name {
-	font-weight: bold;
-}
 </style>
diff --git a/docs/templates/Makefile b/docs/templates/Makefile
new file mode 100644
index 0000000..a510cb8
--- /dev/null
+++ b/docs/templates/Makefile
@@ -0,0 +1,2 @@
+compile:
+	hlb run --download ../docs/ make.hlb
diff --git a/docs/templates/make.hlb b/docs/templates/make.hlb
new file mode 100644
index 0000000..a5ba8d4
--- /dev/null
+++ b/docs/templates/make.hlb
@@ -0,0 +1,70 @@
+fs hlbGit() {
+	git "https://github.com/openllb/hlb.git" "41d11fccf427dea498b9c3f697471746d4da936b"
+}
+
+fs npmInstall(fs manifest) {
+	image "node:alpine"
+	run "npm install" with option {
+		dir "/src"
+		mount manifest "/src"
+		mount fs { scratch; } "/src/node_modules" as nodeModules
+	}
+}
+
+fs goBuild(fs context, string package, string binary) {
+	image "golang:1.12-alpine" with option { resolve; }
+	run "apk add -U git gcc libc-dev"
+	run string {
+		format "go build -o /out/%s -ldflags '-linkmode external -extldflags -static' -a %s" binary package
+	} with option {
+        	env "GO111MODULE" "on"
+		dir "/src"
+		mount context "/src"
+		mount fs { scratch; } "/out" as goBinary
+        	mount fs { scratch; } "/root/.cache/go-build" with option {
+			cache "@hlb-docs/go-build" "shared"
+        	}
+        	mount fs { scratch; } "/go/pkg/mod" with option {
+			cache "@hlb-docs/go-mod" "shared"
+		}
+	}
+}
+
+fs docgen() {
+	scratch
+	mkdir "/out" 0600
+	run "/docgen" "reference.hlb" "/out/reference.json" with option {
+		mount fs { goBinary hlbGit "./cmd/docgen" "docgen"; } "/"
+		mount hlbGit "/reference.hlb" with option {
+			sourcePath "/docs/reference.hlb"
+		}
+		mount fs { scratch; } "/out" as json
+	}
+}
+
+fs handleBars(fs context, fs manifest) {
+	image "node:alpine"
+	run "npm run compile" with option {
+		dir "src"
+		mount context "/src"
+		mount fs { nodeModules manifest; } "/src/node_modules"
+		mount json "/src/data"
+		mount fs { scratch; } "/src/dist" as markdown
+	}
+}
+
+fs context() {
+	local "." with option {
+		excludePatterns "package.json" "package-lock.json" "node_modules" "hlb"
+	}
+}
+
+fs manifest() {
+	local "." with option {
+		includePatterns "package.json" "package-lock.json"
+	}
+}
+
+fs default() {
+	markdown context manifest
+}
diff --git a/docs/templates/reference/reference.md b/docs/templates/reference/reference.md
index 681454c..5a87adc 100644
--- a/docs/templates/reference/reference.md
+++ b/docs/templates/reference/reference.md
@@ -1,7 +1,7 @@
 {{#each Builtins}}
-## <span class='hlb-type'>{{Funcs.0.Type}}</span> functions
+## Functions
 {{#each Funcs}}
-### <span class='hlb-type'>{{Type}}</span> <span class='hlb-name'>{{Name}}</span>({{#each Params}}{{#if @first}}{{else}}, {{/if}}<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>{{/each}})
+### <span class='hlb-type'>{{Type}}</span> {{Name}}({{#each Params}}{{#if @first}}{{else}}, {{/if}}<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>{{/each}})
 
 {{#if Params}}
 {{#each Params}}
@@ -13,14 +13,10 @@
 {{Doc}}
 
 	#!hlb
-	{{#if (eq Type "fs")}}
 	fs default() {
-	{{else}}
-	string myString() {
-	{{/if}}
-		{{Name}}{{#if Params}}{{#each Params}} {{#if (eq Type "string")}}"{{Name}}"{{else if (eq Type "int")}}0{{else if (eq Type "octal")}}0o644{{else if (eq Type "bool")}}false{{else if (eq Type "fs")}}scratch{{else}}{{/if}}{{/each}}{{/if}}{{#if Options}} with option {
+		{{Name}}{{#if Params}}{{#each Params}} {{#if (eq Type "string")}}"{{Name}}"{{else if (eq Type "int")}}0{{else if (eq Type "octal")}}0644{{else if (eq Type "bool")}}false{{else if (eq Type "fs")}}fs { scratch; }{{else}}{{/if}}{{/each}}{{/if}}{{#if Options}} with option {
 			{{#each Options}}
-			{{Name}}{{#if Params}}{{#each Params}} {{#if (eq Type "string")}}"{{Name}}"{{else if (eq Type "int")}}0{{else if (eq Type "octal")}}0o644{{else if (eq Type "bool")}}false{{else if (eq Type "fs")}}scratch{{else}}{{/if}}{{/each}}{{/if}}
+			{{Name}}{{#if Params}}{{#each Params}} {{#if (eq Type "string")}}"{{Name}}"{{else if (eq Type "int")}}0{{else if (eq Type "octal")}}0644{{else if (eq Type "bool")}}false{{else if (eq Type "fs")}}fs { scratch; }{{else}}{{/if}}{{/each}}{{/if}}
 			{{/each}}
 		}{{/if}}
 	}
@@ -28,7 +24,7 @@
 
 {{#if Options}}
 {{#each Options}}
-#### <span class='hlb-type'>{{Type}}</span> <span class='hlb-name'>{{Name}}</span>({{#each Params}}{{#if @first}}{{else}}, {{/if}}<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>{{/each}})
+#### <span class='hlb-type'>{{Type}}</span> {{Name}}({{#each Params}}{{#if @first}}{{else}}, {{/if}}<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>{{/each}})
 
 {{#if Params}}
 {{#each Params}}
@@ -42,6 +38,68 @@
 {{/each}}
 {{/if}}
 
+{{/each}}
+
+## Methods
+{{#each Methods}}
+### <span class='hlb-type'>{{Type}}</span> (<span class='hlb-type'>{{Type}}</span>) {{Name}}({{#each Params}}{{#if @first}}{{else}}, {{/if}}<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>{{/each}})
+
+{{#if Params}}
+{{#each Params}}
+!!! info "<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>"
+	{{ Doc }}
+{{/each}}
+{{/if}}
+
+{{Doc}}
+
+	#!hlb
+	fs default() {
+		scratch
+		{{Name}}{{#if Params}}{{#each Params}} {{#if (eq Type "string")}}"{{Name}}"{{else if (eq Type "int")}}0{{else if (eq Type "octal")}}0644{{else if (eq Type "bool")}}false{{else if (eq Type "fs")}}fs { scratch; }{{else}}{{/if}}{{/each}}{{/if}}{{#if Options}} with option {
+			{{#each Options}}
+			{{Name}}{{#if Params}}{{#each Params}} {{#if (eq Type "string")}}"{{Name}}"{{else if (eq Type "int")}}0{{else if (eq Type "octal")}}0644{{else if (eq Type "bool")}}false{{else if (eq Type "fs")}}fs { scratch; }{{else}}{{/if}}{{/each}}{{/if}}{{#if Options}} with option {
+			{{#each Options}}
+				{{Name}}{{#if Params}}{{#each Params}} {{#if (eq Type "string")}}"{{Name}}"{{else if (eq Type "int")}}0{{else if (eq Type "octal")}}0644{{else if (eq Type "bool")}}false{{else if (eq Type "fs")}}fs { scratch; }{{else}}{{/if}}{{/each}}{{/if}}
+			{{/each}}
+			}{{/if}}
+			{{/each}}
+		}{{/if}}
+	}
+
+{{#if Options}}
+{{#each Options}}
+#### <span class='hlb-type'>{{Type}}</span> {{Name}}({{#each Params}}{{#if @first}}{{else}}, {{/if}}<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>{{/each}})
+
+{{#if Params}}
+{{#each Params}}
+!!! info "<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>"
+	{{ Doc }}
+{{/each}}
+{{/if}}
+
+{{Doc}}
+
+
+{{#if Options}}
+{{#each Options}}
+#### <span class='hlb-type'>{{Type}}</span> {{Name}}({{#each Params}}{{#if @first}}{{else}}, {{/if}}<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>{{/each}})
+
+{{#if Params}}
+{{#each Params}}
+!!! info "<span class='hlb-type'>{{Type}}</span> <span class='hlb-variable'>{{Name}}</span>"
+	{{ Doc }}
+{{/each}}
+{{/if}}
+
+{{Doc}}
+
+{{/each}}
+{{/if}}
+
+{{/each}}
+{{/if}}
+
 {{/each}}
 {{/each}}
 
@@ -53,8 +111,4 @@
 .hlb-variable {
 	color: #0366d6
 }
-
-.hlb-name {
-	font-weight: bold;
-}
 </style>
diff --git a/docs/tutorial/lets-begin.md b/docs/tutorial/lets-begin.md
index 6d01301..3e8b372 100644
--- a/docs/tutorial/lets-begin.md
+++ b/docs/tutorial/lets-begin.md
@@ -55,10 +55,10 @@ As we learned earlier, we can define functions which we can later target when ru
 
 Since `hlb` is a functional language, variables and functions cannot be modified dynamically. When we copy from `npmInstall`, it is always referring to a snapshot of its filesystem after all its instructions have been executed. If we want to modify `npmInstall`, we will have to write a new function that starts from `npmInstall` but it will have to be defined with a new identifier.
 
-Now that we have isolated the directory, we can download the filesystem (containing only the `node_modules`) by specifying `--download <dest-dir>`:
+Now that we have isolated the directory, we can download the filesystem (containing only the `node_modules`) by specifying `--target <target>,download=<dest-dir>`:
 
 ```sh
-hlb run --target nodeModules --download . node.hlb
+hlb run --target nodeModules,download=. node.hlb
 ```
 
 After the build have finished, you should see the `node_modules` in your working directory.
diff --git a/gen/documentation.go b/gen/documentation.go
index 22fbbc0..43cf2f4 100644
--- a/gen/documentation.go
+++ b/gen/documentation.go
@@ -145,7 +145,7 @@ func GenerateDocumentation(r io.Reader) (*Documentation, error) {
 
 	var doc Documentation
 
-	for _, typ := range []string{"fs", "string"} {
+	for _, typ := range []string{"fs"} {
 		funcs := funcsByType[typ]
 		for _, fun := range funcs {
 			fun := fun
diff --git a/language/builtin.hlb b/language/builtin.hlb
index e05b66a..580b001 100644
--- a/language/builtin.hlb
+++ b/language/builtin.hlb
@@ -113,10 +113,12 @@ fs shell(variadic string arg)
 #
 # If no arguments are given, it will execute the current args set on the
 # filesystem.
-# If exactly one arg is given it will be wrapped with /bin/sh -c 'arg'.
+# If exactly one arg is given, it will attempt to parse as a command with
+# shell-style quoting. If it remains a single element, it is executed directly,
+# otherwise it is run with the current shell.
 # If more than one arg is given, it will be executed directly, without a shell.
 #
-# @param arg are optional arguments to execute.
+# @param arg a command to execute.
 # @return the filesystem after the command has executed.
 fs run(variadic string arg)
 
@@ -169,12 +171,6 @@ option::run network(string networkmode)
 # - insecure: enables all capabilities.
 option::run security(string securitymode)
 
-# Attempt to lex the single-argument shell command provided to `run`
-# to determine if a `/bin/sh -c '...'` wrapper needs to be added.
-#
-# @return an option to attempt to optimize the command execution remoiving the /bin/sh -c "..." wrapper when possible.
-option::run shlex()
-
 # Adds a host entry to /etc/hosts for the duration of the run command.
 #
 # @param hostname the host name of the entry, may include spaces to delimit
@@ -537,38 +533,6 @@ string localEnv(string key)
 # @return the OS
 string localOs()
 
-# Executes an command in the local environment.
-#
-# If exactly one arg is given it will be wrapped with /bin/sh -c 'arg'.
-# If more than one arg is given, it will be executed directly, without a shell.
-#
-# @param command a command to execute.
-# @param args optional arguments to the command.
-# @return the string output from the command
-string localRun(string command, variadic string args)
-
-# If the command returns a non-zero status code ignore
-# the failure and continue processing the hlb file.
-#
-# @return an option to ignore errors on the command
-option::localRun ignoreError()
-
-# Capture stderr intermixed with stdout on the command.
-#
-# @return an option to capture stderr along with stdout on the command.
-option::localRun includeStderr()
-
-# Only capture the stderr from the command, ignore stdout.
-#
-# @return an option to ignore stdout on the command
-option::localRun onlyStderr()
-
-# Attempt to lex the single-argument shell command provided to `localRun`
-# to determine if a `/bin/sh -c '...'` wrapper needs to be added.
-#
-# @return an option to attempt to optimize the command execution remoiving the /bin/sh -c "..." wrapper when possible.
-option::localRun shlex()
-
 # Process text as a Go text template.
 # For template syntax documentation see:
 #   https://golang.org/pkg/text/template/
diff --git a/local/environment.go b/local/environment.go
index 148cf5a..76cccac 100644
--- a/local/environment.go
+++ b/local/environment.go
@@ -65,13 +65,6 @@ func Env(ctx context.Context, key string) string {
 	return os.Getenv(key)
 }
 
-func Environ(ctx context.Context) []string {
-	if environ, ok := ctx.Value(environContextKey).([]string); ok {
-		return environ
-	}
-	return os.Environ()
-}
-
 func Cwd(ctx context.Context) (string, error) {
 	if workdir, ok := ctx.Value(cwdContextKey).(string); ok {
 		return workdir, nil
diff --git a/mkdocs.hlb b/mkdocs.hlb
index ddc6e53..ee29384 100644
--- a/mkdocs.hlb
+++ b/mkdocs.hlb
@@ -1,167 +1,51 @@
-export generatedBuiltin
-export generatedMarkdown
-export build
-export publish
-
-import npm from fs {
-	image "openllb/npm.hlb"
+fs default() {
+	buildMkdocs src
 }
 
-import go from fs {
-	image "openllb/go.hlb"
+fs publish() {
+	publishMkdocs src
 }
 
-fs mkdocsMaterial() {
+fs testSSH() {
 	image "python:alpine"
-	run "apk add -U git git-fast-import openssh-client build-base"
-	sshKeyScan "github.com"
-	run "pip install --upgrade pip"
-	run "pip install mkdocs-material pymdown-extensions pygments"
-}
-
-fs _runMkdocsBuild() {
-	mkdocsMaterial
-	run "mkdocs build -d /site" with option {
-		dir "/mkdocs"
-		mount fs {
-			local "." with option {
-				includePatterns "mkdocs.yml" "docs/" ".git"
-			}
-		} "/mkdocs" as readonly
-		mount generatedMarkdown "/mkdocs/docs/reference.md" with option {
-			sourcePath "reference.md"
-			readonly
-		}
-		mount scratch "/site" as build
-	}
-}
-
-# Note this only publishes master on github, it does
-# not publish local files
-fs publish() {
-	mkdocsMaterial
-	run "mkdocs gh-deploy" with option {
-		dir "/src"
-		mount gitSource "/src"
+	run "apk add -U openssh-client"
+	mkdir "/root/.ssh" 0o700
+        run "ssh-keyscan github.com >> ~/.ssh/known_hosts"
+	run "ssh -q -T git@github.com" with option {
 		ssh
 	}
 }
 
-fs _runHandleBars() {
-	image "node:alpine"
-	run "node src/compile.js" with option {
-		dir "src"
-		mount fs {
-			local "docs/templates" with option {
-				includePatterns "src" "reference"
-			}
-		} "/src" with readonly
-		mount fs { 
-			npm.nodeModules fs {
-				local "docs/templates" with option {
-					includePatterns "package.json" "package-lock.json"
-				}
-			}
-		} "/src/node_modules" with readonly
-		mount referenceJson "/src/data" with readonly
-		mount scratch "/src/dist" as generatedMarkdown
-	}
-}
-
-fs _runDocGen() {
-	scratch
-	run "/docgen" "/language/builtin.hlb" "/out/reference.json" with option {
-		mount fs { 
-			staticGoBuild "./cmd/docgen" fs {
-				local "." with option {
-					includePatterns "./cmd/docgen" "parser" "gen" "go.mod" "go.sum"
-				}
-			}
-		 } "/" with readonly
-		mount fs {
-			local "language" with option {
-				 includePatterns "builtin.hlb"
-			 }
-		} "language" with readonly
-		mount scratch "/out" as referenceJson
-	}
-}
-
-fs _runBuiltinGen() {
-	scratch
-	run "/builtingen" "/language/builtin.hlb" "/out/builtin.go" with option {
-		mount fs { 
-			staticGoBuild "./cmd/builtingen" fs {
-				local "." with option {
-					includePatterns "./cmd/builtingen" "gen" "parser" "go.mod" "go.sum"
-				}
-			}
-		 } "/" with readonly
-		mount fs {
-			local "language" with option {
-				 includePatterns "builtin.hlb"
-			 }
-		} "language" with readonly
-		mount scratch "/out" as generatedBuiltin
-	}
+fs mkdocsMaterial() {
+	image "python:alpine"
+	run "apk add -U git git-fast-import openssh-client"
+	mkdir "/root/.ssh" 0o700
+        run "ssh-keyscan github.com >> ~/.ssh/known_hosts"
+	run "pip install --upgrade pip"
+	run "pip install mkdocs-material pymdown-extensions"
+	run "pip install -e git+https://github.com/hinshun/pygments.git@hlb-lexer#egg=pygments"
 }
 
-fs staticGoBuild(string package, fs src) {
-	go.build src package option::template {
-		stringField "goBuildFlags" <<~EOM
-			-ldflags "-extldflags -static"
-		EOM
-		stringField "platform" "linux"
-		stringField "arch" "amd64"
-		stringField "cgoEnabled" "0"
+fs src() {
+	local "." with option {
+		includePatterns "mkdocs.yml" "docs/" ".git"
 	}
 }
 
-# TODO add this to a generic util module?
-fs testSSH() {
-	image "alpine"
-	run "apk add -U openssh-client"
-	sshKeyScan "github.com"
-	run "ssh -q -T git@github.com" with option {
-		ssh
-	}
-}
-
-# TODO add this to a generic util moduile
-fs sshKeyScan(string host) {
-	mkdir "/root/.ssh" 0o700
-	run string {
-		format "ssh-keyscan %s >> ~/.ssh/known_hosts" host 
+fs runMkdocsBuild(fs src) {
+	mkdocsMaterial
+	run "mkdocs build -d /out" with option {
+		dir "/mkdocs"
+		mount src "/mkdocs"
+		mount fs { scratch; } "/out" as buildMkdocs
 	}
 }
 
-# TODO can we add this logic to a generic util module for publishing gh-pages?
-fs _fetchGhPagesBranch() {
-	image "alpine/git"
-	sshKeyScan "github.com"
-	run "git fetch origin gh-pages" with option {
-		dir "/src"
-		mount fs {
-			git "git://github.com/openllb/hlb.git" "master" with option {
-				keepGitDir
-			}
-			# we have to recreate the .git/config because the one that 
-			# comes from buildkit has invalid remote.origin.url and
-			# no branch.master properties
-			mkfile ".git/config" 0o644 <<-EOM
-				[core]
-					repositoryformatversion = 0
-					filemode = true
-					bare = false
-					logallrefupdates = true
-				[remote "origin"]
-					url = git@github.com:openllb/hlb.git
-					fetch = +refs/heads/*:refs/remotes/origin/*
-				[branch "master"]
-					remote = origin
-					merge = refs/heads/master
-			EOM
-		} "/src" as gitSource
+fs publishMkdocs(fs src) {
+	mkdocsMaterial
+	run "mkdocs gh-deploy" with option {
 		ssh
+		dir "/mkdocs"
+		mount src "/mkdocs"
 	}
 }
diff --git a/parser/cst.go b/parser/cst.go
index 2cd90ca..357bc31 100644
--- a/parser/cst.go
+++ b/parser/cst.go
@@ -20,7 +20,7 @@ var (
 		HereDoc = <<[-~]?
 		Keyword  = \b(with|as|import|export|from)\b
 		Modifier = \b(variadic)\b
-		Type     = \b(string|int|bool|fs|option|group)(::[a-z][a-zA-Z]*)?\b
+		Type     = \b(string|int|bool|fs|option|group)(::[a-z][a-z]*)?\b
 		Numeric  = \b(0(b|B|o|O|x|X)[a-fA-F0-9]+)\b
 		Decimal  = \b(0|[1-9][0-9]*)\b
 		String   = "(\\.|[^"])*"|'[^']*'
diff --git a/scripts/mkBuildkitdDroplet.sh b/scripts/mkBuildkitdDroplet.sh
deleted file mode 100755
index f7eb13c..0000000
--- a/scripts/mkBuildkitdDroplet.sh
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/bin/bash
-set -e
-
-# TODO add flags for --sshkey, --size, --region, --name, --image
-# TODO check for curl and jq binaries
-
-REPO="$1"
-SSH="$2"
-if [[ -z $REPO || -z $SSH ]]; then
-    echo "Usage: $0 <repo url> <ssh key fingerprint>" >&2
-    echo >&2
-    echo "For example to add buildkitd actions runner to openllb/hlb project you would run:" >&2
-    echo >&2
-    echo "	$0 https://github.com/openllb/hlb 3f:2a:4b:7d:d9:46:9f:43:99:3d:c3:48:bb:62:f3:a4" >&2
-    echo >&2
-    exit 1
-fi
-
-DIGITAL_OCEAN_TOKEN=$(cat ~/.digitalocean-token)
-
-if [[ -z $DIGITAL_OCEAN_TOKEN ]]; then
-    echo "Missing ~/.digitalocean-token file which must contain an API token for DigitalOcean" >&2
-    echo "To create a token go here: https://cloud.digitalocean.com/account/api/tokens and select" >&2
-    echo "\"Generate New Token\", then run:" >&2
-    echo >&2
-    echo "	echo \"put-token-here\" > $HOME/.digitalocean-token" >&2
-    echo >&2
-    exit 1
-fi
-
-GITHUB_TOKEN=$(cat ~/.github-repo-token)
-
-if [[ -z $GITHUB_TOKEN ]]; then
-    echo "Missing ~/.github-repo-token file which must contain an API token for Github" >&2
-    echo "To create a token go here: https://github.com/settings/tokens and select" >&2
-    echo "\"Generate new token\", then under \"Selected scopes\" check the \"repo\" box."  >&2
-    echo >&2
-    echo "	echo \"put-token-here\" > $HOME/.github-repo-token" >&2
-    echo >&2
-    exit 1
-fi
-
-# get short-lived token to allow actions runner to register with Github
-REGISTER_TOKEN=$(curl -qsfL -X POST -H 'Authorization: token '$GITHUB_TOKEN'' https://api.github.com/repos/openllb/hlb/actions/runners/registration-token | jq -r .token)
-
-# Use github api to find latest release tarball location
-ACTIONS_RUNNER_TGZ=$(curl -qsLf https://api.github.com/repos/actions/runner/releases/latest | jq -r '.assets[] | select(.name | contains("linux")) | select(.name | contains("x64")) | .browser_download_url')
-
-USERDATA=$(cat <<EOM
-#cloud-config
-runcmd:
-  - |
-    # setup firewall to only allow in ssh/22
-    ufw default deny incoming
-    ufw default allow outgoing
-    ufw allow ssh
-    ufw enable
-  - |
-    # install docker & latest git
-    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
-    add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu \$(lsb_release -cs) stable"
-    add-apt-repository ppa:git-core/ppa
-    apt-get update
-    apt-get install -y docker-ce docker-ce-cli containerd.io build-essential git
-  - |
-    # install actions-runner
-    useradd -g docker --create-home --shell /usr/sbin/nologin github
-    mkdir -p /github
-    chown github /github
-    sudo -u github mkdir -p /github/actions-runner /github/work
-    cd /github/actions-runner
-    curl -qsfL $ACTIONS_RUNNER_TGZ | sudo -u github tar xzf -
-    sudo -u github ./config.sh \
-        --unattended \
-        --replace \
-        --name buildkitd \
-        --labels buildkitd  \
-        --work ../work \
-        --url $REPO \
-        --token $REGISTER_TOKEN
-    sudo -H -u github nohup ./run.sh &
-  - docker run -d --name buildkitd --privileged moby/buildkit:latest
-EOM
-)
-
-# some valid sizes we might want to try:
-# "s-1vcpu-1gb" => $5/mo
-# "s-1vcpu-2gb" => $10/mo
-# "s-3vcpu-1gb" => $15/mo
-# "s-2vcpu-2gb" => $15/mo
-# "s-1vcpu-3gb" => $15/mo
-# "s-2vcpu-4gb" => $20/mo
-
-PAYLOAD=$(cat <<'EOM'
-{
-    name: "buildkitd",
-    region: "nyc3",
-    size: "s-2vcpu-2gb",
-    image: "ubuntu-18-04-x64",
-    ssh_keys: [$sshkey],
-    user_data: $userdata
-}
-EOM
-)
-
-jq -n --arg userdata "$USERDATA" --arg sshkey "$SSH" "$PAYLOAD" | \
-    curl -qsfL -X POST \
-        -H 'Content-Type: application/json' \
-        -H 'Authorization: Bearer '$DIGITAL_OCEAN_TOKEN'' \
-        "https://api.digitalocean.com/v2/droplets" \
-        -d@- | jq .
diff --git a/solver/request.go b/solver/request.go
index 2176d14..5dabed6 100644
--- a/solver/request.go
+++ b/solver/request.go
@@ -121,7 +121,12 @@ func (o op) Tree(tree treeprint.Tree) error {
 		branch = tree.AddMetaBranch("source", v.Source)
 	case *pb.Op_Exec:
 		meta := v.Exec.Meta
-		cmd := shellquote.Join(meta.Args...)
+		cmd := ""
+		if len(meta.Args) == 3 && meta.Args[0] == "/bin/sh" && meta.Args[1] == "-c" {
+			cmd = meta.Args[2]
+		} else {
+			cmd = shellquote.Join(meta.Args...)
+		}
 		if o.meta[o.dgst].IgnoreCache {
 			cmd += " [ignoreCache]"
 		}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5f27e04..0539b1f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,18 +8,16 @@ on:
 
 env:
   GO111MODULE: on
-  BUILDKIT_HOST: docker-container://buildkitd
 
 jobs:
   build:
     name: Build
-    # expression language does not support ternary, so hack it via indexing boolean keys of an json object
-    runs-on: ${{ fromJSON('{true:"buildkitd",false:"ubuntu-latest"}')[github.repository == 'openllb/hlb'] }}
+    runs-on: ubuntu-latest
     steps:
-    - name: Set up Go 1.13
-      uses: actions/setup-go@v2
+    - name: Set up Go 1.13.10
+      uses: actions/setup-go@v1
       with:
-        go-version: ^1.13
+        go-version: 1.13.10
       id: go
 
     - name: Check out code into the Go module directory
@@ -27,7 +25,6 @@ jobs:
 
     - name: Restore Cache
       uses: actions/cache@v1
-      if: github.repository != 'openllb/hlb'
       id: cache
       with:
         path: ~/go/pkg/mod
@@ -46,30 +43,16 @@ jobs:
       run: go test -v ./...
 
     - name: Start BuildkitD
-      if: github.repository != 'openllb/hlb'
       run: docker run --name buildkitd --rm -d --privileged openllb/buildkit:experimental
 
     - name: BuildkitD Wait
-      if: github.repository != 'openllb/hlb'
       # waiting for buildkitd to report 1 worker (2 lines, 1 for column titles, one for the worker details)
       run: while true; do lineCount=$(docker exec buildkitd buildctl debug workers | wc -l); if [ $lineCount -gt 1 ]; then break; fi; sleep 1; done
 
     - name: GoLint
-      run: ./build/hlb run -t lint
-
-    - name: Ensure generated files
-      run: |
-        ./build/hlb run -t gen
-        if ! git diff --quiet; then
-          echo "ERROR: Generated files need updating:" >&2
-          echo "" >&2
-          git ls-files -m | sed 's/^/	/g' >&2
-          echo "" >&2
-          echo "To resolve run:" >&2
-          echo "	./hlb run -t gen" >&2
-          echo "and add changes to the git branch" >&2
-          exit 1
-        fi
+      run: ./build/hlb --addr docker-container://buildkitd run --log-output plain -t lint ./build.hlb
 
     - name: Crosscompile
-      run: ./build/hlb run
+      run: ./build/hlb --addr docker-container://buildkitd run --log-output plain ./build.hlb
+
+
diff --git a/checker/checker_test.go b/checker/checker_test.go
index c84ff26..8771ef7 100644
--- a/checker/checker_test.go
+++ b/checker/checker_test.go
@@ -189,10 +189,8 @@ func TestChecker_Check(t *testing.T) {
 	}, {
 		"errors with duplicate function names",
 		`
-		fs duplicate(string ref) {}
-		fs duplicate(string ref) {
-			image ref
-		}
+		fs duplicateFunctionName() {}
+		fs duplicateFunctionName() {}
 		`,
 		ErrDuplicateDecls{
 			Idents: []*parser.Ident{{
@@ -201,7 +199,7 @@ func TestChecker_Check(t *testing.T) {
 					Line:     1,
 					Column:   4,
 				},
-				Name: "duplicate",
+				Name: "duplicateFunctionName",
 			}},
 		},
 	}, {
@@ -480,10 +478,17 @@ func TestChecker_CheckSelectors(t *testing.T) {
 	}
 }
 
-func validateError(t *testing.T, expected error, actual error) {
-	if expected == nil {
-		require.NoError(t, actual)
+func validateError(t *testing.T, expectedError error, actualError error) {
+	if expectedError == nil {
+		require.NoError(t, actualError)
 	} else {
-		require.Equal(t, expected.Error(), actual.Error())
+		// assume if we got a semantic error we really want
+		// to validate the underlying error
+		if semErr, ok := actualError.(ErrSemantic); ok {
+			require.IsType(t, expectedError, semErr.Errs[0])
+			require.Equal(t, expectedError.Error(), semErr.Errs[0].Error())
+		} else {
+			require.IsType(t, expectedError, actualError)
+		}
 	}
 }
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 06e8286..d71e228 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -2,13 +2,11 @@ package codegen
 
 import (
 	"context"
-	"fmt"
 	"os"
 	"strings"
 	"testing"
 	"time"
 
-	"github.com/lithammer/dedent"
 	"github.com/moby/buildkit/client/llb"
 	"github.com/moby/buildkit/solver/pb"
 	"github.com/moby/buildkit/util/entitlements"
@@ -37,7 +35,10 @@ type testCase struct {
 }
 
 func cleanup(value string) string {
-	return dedent.Dedent(value)
+	result := strings.TrimSpace(value)
+	result = strings.ReplaceAll(result, strings.Repeat("\t", 3), "")
+	result = strings.ReplaceAll(result, "|\n", "| \n")
+	return result
 }
 
 func TestCodeGen(t *testing.T) {
@@ -165,7 +166,7 @@ func TestCodeGen(t *testing.T) {
 		fs default() {
 			scratch
 			env "TEST_VAR" "test value"
-			run "echo Hello" with shlex
+			run "echo Hello"
 		}
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
@@ -178,7 +179,7 @@ func TestCodeGen(t *testing.T) {
 		fs default() {
 			scratch
 			dir "testDir"
-			run "echo Hello" with shlex
+			run "echo Hello"
 		}
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
@@ -191,7 +192,7 @@ func TestCodeGen(t *testing.T) {
 		fs default() {
 			scratch
 			user "testUser"
-			run "echo Hello" with shlex
+			run "echo Hello"
 		}
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
@@ -372,7 +373,6 @@ func TestCodeGen(t *testing.T) {
 			image "alpine"
 			run "touch /out/foo" with option {
 				mount scratch "/out" as default
-				shlex
 			}
 		}
 		`,
@@ -531,11 +531,11 @@ func TestCodeGen(t *testing.T) {
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
 			return Expect(t, llb.Image("busybox").Run(
-				llb.Args([]string{"/bin/sh", "-c", "echo hi"}),
+				llb.Shlex("echo hi"),
 			).Run(
-				llb.Args([]string{"/bin/sh", "-c", "echo hi"}),
+				llb.Shlex("echo hi"),
 			).Run(
-				llb.Args([]string{"/bin/sh", "-c", "\techo hi"}),
+				llb.Shlex("\techo hi"),
 			).Root())
 		},
 	}, {
@@ -554,7 +554,7 @@ func TestCodeGen(t *testing.T) {
 
 		fs default() {
 			image "busybox"
-			run cmd with shlex
+			run cmd 
 		}
 		`,
 		func(t *testing.T, cg *CodeGen) solver.Request {
@@ -577,7 +577,7 @@ func TestCodeGen(t *testing.T) {
 		func(t *testing.T, cg *CodeGen) solver.Request {
 			return Expect(t,
 				llb.Image("busybox").Run(
-					llb.Args([]string{"/bin/sh", "-c", "entitlements"}),
+					llb.Shlex("entitlements"),
 					llb.Network(pb.NetMode_HOST),
 					llb.Security(pb.SecurityMode_INSECURE),
 				).Root(),
@@ -592,7 +592,6 @@ func TestCodeGen(t *testing.T) {
 		fs default() {
 			image "busybox"
 			run "find ." with option {
-				shlex
 				dir "/foo"
 				mount fs {
 					local "."
@@ -662,48 +661,6 @@ func TestCodeGen(t *testing.T) {
 				}),
 			))
 		},
-	}, {
-		"localRun",
-		[]string{"default"},
-		`
-		fs default() {
-			mkfile "./just-stdout" 0o644 string {
-				localRun "echo stdout; echo stderr >&2"
-			}
-			mkfile "./just-stderr" 0o644 string {
-				localRun "echo stdout; echo stderr >&2" with onlyStderr
-			}
-			mkfile "./stdio" 0o644 string {
-				localRun "echo stdout; echo stderr >&2" with includeStderr
-			}
-			mkfile "./goterror" 0o644 string {
-				localRun "echo stdout; exit 1" with ignoreError
-			}
-			# this will write evaluate the $HOME env var because run as ["/bin/sh", "-c", "echo $HOME"]
-			mkfile "./noshlex" 0o644 string {
-				localRun "echo $HOME"
-			}
-			# this will write the literal string "$HOME" because run as ["/bin/echo", "$HOME"]
-			mkfile "./shlex" 0o644 string {
-				localRun "echo $HOME" with shlex
-			}
-		}
-		`,
-		func(t *testing.T, cg *CodeGen) solver.Request {
-			return Expect(t, llb.Scratch().File(
-				llb.Mkfile("just-stdout", os.FileMode(0644), []byte("stdout\n")),
-			).File(
-				llb.Mkfile("just-stderr", os.FileMode(0644), []byte("stderr\n")),
-			).File(
-				llb.Mkfile("stdio", os.FileMode(0644), []byte("stdout\nstderr\n")),
-			).File(
-				llb.Mkfile("goterror", os.FileMode(0644), []byte("stdout\n")),
-			).File(
-				llb.Mkfile("noshlex", os.FileMode(0644), []byte(fmt.Sprintf("%s\n", os.Getenv("HOME")))),
-			).File(
-				llb.Mkfile("shlex", os.FileMode(0644), []byte("$HOME\n")),
-			))
-		},
 	}} {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {