Repository: brimsec/zq
Pull request #1582 (2020-11-09 21:12:48)

zar import: Allow multiple paths
Now that zar import can handle an unsorted stream we can do this. Much easier, and because we don't necessarily to spill all records to disk, should be faster than `zq "sort -r ts" *.log`.


Patch:
diff --git a/driver/compile.go b/driver/compile.go
index 9840e4653..caa4a5c43 100644
--- a/driver/compile.go
+++ b/driver/compile.go
@@ -53,7 +53,7 @@ func programPrep(program ast.Proc, sortKey field.Static, sortReversed bool) (ast
 	filterExpr, program := liftFilter(program)
 	if filterExpr != nil {
 		var err error
-		if filt, err = filter.Compile(filterExpr); err != nil {
+		if filt, err = filter.Compile(resolver.NewContext(), filterExpr); err != nil {
 			return nil, nil, nil, err
 		}
 	}
diff --git a/driver/parallel.go b/driver/parallel.go
index 50ce60582..57a025478 100644
--- a/driver/parallel.go
+++ b/driver/parallel.go
@@ -8,13 +8,10 @@ import (
 	"github.com/brimsec/zq/api"
 	"github.com/brimsec/zq/api/client"
 	"github.com/brimsec/zq/ast"
-	"github.com/brimsec/zq/expr"
-	"github.com/brimsec/zq/field"
 	"github.com/brimsec/zq/filter"
 	"github.com/brimsec/zq/proc"
 	"github.com/brimsec/zq/scanner"
 	"github.com/brimsec/zq/zbuf"
-	"github.com/brimsec/zq/zng"
 )
 
 type parallelHead struct {
@@ -212,25 +209,6 @@ func (pg *parallelGroup) run() {
 	close(pg.sourceChan)
 }
 
-func newCompareFn(fieldName string, reversed bool) (zbuf.RecordCmpFn, error) {
-	if fieldName == "ts" {
-		if reversed {
-			return zbuf.CmpTimeReverse, nil
-		} else {
-			return zbuf.CmpTimeForward, nil
-		}
-	}
-	fieldRead := ast.NewDotExpr(field.New(fieldName))
-	res, err := expr.CompileExpr(fieldRead)
-	if err != nil {
-		return nil, err
-	}
-	rcmp := expr.NewCompareFn(true, res)
-	return func(a, b *zng.Record) bool {
-		return rcmp(a, b) < 0
-	}, nil
-}
-
 func createParallelGroup(pctx *proc.Context, filt filter.Filter, filterExpr ast.BooleanExpr, msrc MultiSource, mcfg MultiConfig, workerURLs []string) ([]proc.Interface, *parallelGroup, error) {
 	pg := &parallelGroup{
 		pctx: pctx,
diff --git a/expr/coerce.go b/expr/coerce/coerce.go
similarity index 80%
rename from expr/coerce.go
rename to expr/coerce/coerce.go
index 365f8a5bd..b150a0ac6 100644
--- a/expr/coerce.go
+++ b/expr/coerce/coerce.go
@@ -1,43 +1,45 @@
-package expr
+package coerce
 
 import (
 	"bytes"
 	"errors"
 	"math"
 
+	"github.com/brimsec/zq/expr/result"
 	"github.com/brimsec/zq/pkg/nano"
 	"github.com/brimsec/zq/zcode"
 	"github.com/brimsec/zq/zng"
 )
 
 var ErrOverflow = errors.New("integer overflow: uint64 value too large for int64")
+var ErrIncompatibleTypes = errors.New("incompatible types")
 
 // XXX aliases should probably be preserved according to the rank
 // of the underlying number type.
 
-// Coercion provides a buffer to decode values into while doing comparisons
+// Pair provides a buffer to decode values into while doing comparisons
 // so the same buffers can be reused on each call without zcode.Bytes buffers
 // escaping to GC.  This method uses the zng.AppendInt(), zng.AppendUint(),
 // etc to encode zcode.Bytes as an in-place slice instead of allocating
 // new slice buffers for every value created.
-type Coercion struct {
+type Pair struct {
 	// a and b point to inputs that can't change
-	a zcode.Bytes
-	b zcode.Bytes
-	// result is a scratch buffer that stays around between calls and is the
+	A zcode.Bytes
+	B zcode.Bytes
+	// Buffer is a scratch buffer that stays around between calls and is the
 	// landing place for either the a or b value if one of them needs to
 	// be coerced (you never need to coerce both).  Then we point a or b
 	// at buf and let go of the other input pointer.
-	result
+	result.Buffer
 }
 
-func (c *Coercion) equal() bool {
-	return bytes.Compare(c.a, c.b) == 0
+func (c *Pair) Equal() bool {
+	return bytes.Equal(c.A, c.B)
 }
 
-func (c *Coercion) coerce(a, b zng.Value) (int, error) {
-	c.a = a.Bytes
-	c.b = b.Bytes
+func (c *Pair) Coerce(a, b zng.Value) (int, error) {
+	c.A = a.Bytes
+	c.B = b.Bytes
 	aid := a.Type.ID()
 	bid := b.Type.ID()
 	if aid == bid {
@@ -60,11 +62,11 @@ func (c *Coercion) coerce(a, b zng.Value) (int, error) {
 	return 0, ErrIncompatibleTypes
 }
 
-func (c *Coercion) compare(lhs, rhs zng.Value) (bool, error) {
-	if _, err := c.coerce(lhs, rhs); err != nil {
+func (c *Pair) compare(lhs, rhs zng.Value) (bool, error) {
+	if _, err := c.Coerce(lhs, rhs); err != nil {
 		return false, err
 	}
-	return c.equal(), nil
+	return c.Equal(), nil
 }
 
 func intToFloat(id int, b zcode.Bytes) float64 {
@@ -76,7 +78,7 @@ func intToFloat(id int, b zcode.Bytes) float64 {
 	return float64(v)
 }
 
-func (c *Coercion) promoteToSigned(in zcode.Bytes) (zcode.Bytes, error) {
+func (c *Pair) promoteToSigned(in zcode.Bytes) (zcode.Bytes, error) {
 	v, _ := zng.DecodeUint(in)
 	if v > math.MaxInt64 {
 		return nil, ErrOverflow
@@ -84,7 +86,7 @@ func (c *Coercion) promoteToSigned(in zcode.Bytes) (zcode.Bytes, error) {
 	return c.Int(int64(v)), nil
 }
 
-func (c *Coercion) promoteToUnsigned(in zcode.Bytes) (zcode.Bytes, error) {
+func (c *Pair) promoteToUnsigned(in zcode.Bytes) (zcode.Bytes, error) {
 	v, _ := zng.DecodeInt(in)
 	if v < 0 {
 		return nil, ErrOverflow
@@ -92,13 +94,13 @@ func (c *Coercion) promoteToUnsigned(in zcode.Bytes) (zcode.Bytes, error) {
 	return c.Uint(uint64(v)), nil
 }
 
-func (c *Coercion) coerceNumbers(aid, bid int) (int, error) {
+func (c *Pair) coerceNumbers(aid, bid int) (int, error) {
 	if zng.IsFloat(aid) {
-		c.b = c.Float64(intToFloat(bid, c.b))
+		c.B = c.Float64(intToFloat(bid, c.B))
 		return aid, nil
 	}
 	if zng.IsFloat(bid) {
-		c.a = c.Float64(intToFloat(aid, c.a))
+		c.A = c.Float64(intToFloat(aid, c.A))
 		return bid, nil
 	}
 	aIsSigned := zng.IsSigned(aid)
@@ -120,24 +122,24 @@ func (c *Coercion) coerceNumbers(aid, bid int) (int, error) {
 	// case, we report an overflow error.
 	var err error
 	if aIsSigned {
-		c.b, err = c.promoteToSigned(c.b)
+		c.B, err = c.promoteToSigned(c.B)
 	} else {
-		c.a, err = c.promoteToSigned(c.a)
+		c.A, err = c.promoteToSigned(c.A)
 	}
 	if err == ErrOverflow {
 		// We got overflow trying to turn the unsigned to signed,
 		// so try turning the signed into unsigned.
 		if aIsSigned {
-			c.a, err = c.promoteToUnsigned(c.a)
+			c.A, err = c.promoteToUnsigned(c.A)
 		} else {
-			c.b, err = c.promoteToUnsigned(c.b)
+			c.B, err = c.promoteToUnsigned(c.B)
 		}
 		id = zng.IdUint64
 	}
 	return id, err
 }
 
-func CoerceToFloat(zv zng.Value) (float64, bool) {
+func ToFloat(zv zng.Value) (float64, bool) {
 	id := zv.Type.ID()
 	if zng.IsFloat(id) {
 		f, _ := zng.DecodeFloat64(zv.Bytes)
@@ -159,7 +161,7 @@ func CoerceToFloat(zv zng.Value) (float64, bool) {
 	return 0, false
 }
 
-func CoerceToUint(zv zng.Value) (uint64, bool) {
+func ToUint(zv zng.Value) (uint64, bool) {
 	id := zv.Type.ID()
 	if zng.IsFloat(id) {
 		f, _ := zng.DecodeFloat64(zv.Bytes)
@@ -184,7 +186,7 @@ func CoerceToUint(zv zng.Value) (uint64, bool) {
 	return 0, false
 }
 
-func CoerceToInt(zv zng.Value) (int64, bool) {
+func ToInt(zv zng.Value) (int64, bool) {
 	id := zv.Type.ID()
 	if zng.IsFloat(id) {
 		f, _ := zng.DecodeFloat64(zv.Bytes)
@@ -207,7 +209,7 @@ func CoerceToInt(zv zng.Value) (int64, bool) {
 	return 0, false
 }
 
-func CoerceToTime(zv zng.Value) (nano.Ts, bool) {
+func ToTime(zv zng.Value) (nano.Ts, bool) {
 	id := zv.Type.ID()
 	if id == zng.IdTime {
 		ts, _ := zng.DecodeTime(zv.Bytes)
@@ -232,11 +234,11 @@ func CoerceToTime(zv zng.Value) (nano.Ts, bool) {
 	return 0, false
 }
 
-// CoerceToDuration attempts to convert a value to a duration.  Int
+// ToDuration attempts to convert a value to a duration.  Int
 // and Double are converted as seconds. The resulting coerced value is
 // written to out, and true is returned. If the value cannot be
 // coerced, then false is returned.
-func CoerceToDuration(in zng.Value) (int64, bool) {
+func ToDuration(in zng.Value) (int64, bool) {
 	var out int64
 	var err error
 	switch in.Type.ID() {
diff --git a/expr/cut.go b/expr/cut.go
new file mode 100644
index 000000000..405d80992
--- /dev/null
+++ b/expr/cut.go
@@ -0,0 +1,43 @@
+package expr
+
+import (
+	"github.com/brimsec/zq/ast"
+	"github.com/brimsec/zq/field"
+	"github.com/brimsec/zq/zng"
+	"github.com/brimsec/zq/zng/resolver"
+)
+
+type cutFunc struct {
+	*Cutter
+}
+
+func compileCut(zctx *resolver.Context, node ast.FunctionCall) (Evaluator, error) {
+	var lhs []field.Static
+	var rhs []Evaluator
+	for _, expr := range node.Args {
+		// This is a bit of a hack and could be cleaed up by re-factoring
+		// CompileAssigment, but for now, we create an assigment expression
+		// where the LHS and RHS are the same, so that cut(id.orig_h,_path)
+		// gives a value of type record[id:record[orig_h:ip],_path:string]
+		// with field names that are the same as the cut names.
+		assignment := &ast.Assignment{LHS: expr, RHS: expr}
+		field, expression, err := CompileAssignment(zctx, assignment)
+		if err != nil {
+			return nil, err
+		}
+		lhs = append(lhs, field)
+		rhs = append(rhs, expression)
+	}
+	return &cutFunc{NewCutter(zctx, false, lhs, rhs)}, nil
+}
+
+func (c *cutFunc) Eval(rec *zng.Record) (zng.Value, error) {
+	out, err := c.Cut(rec)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	if out == nil {
+		return zng.Value{}, ErrNoSuchField
+	}
+	return zng.Value{out.Type, out.Raw}, nil
+}
diff --git a/expr/expr.go b/expr/expr.go
index bdff7a69d..4cb1e5115 100644
--- a/expr/expr.go
+++ b/expr/expr.go
@@ -9,11 +9,14 @@ import (
 	"regexp"
 
 	"github.com/brimsec/zq/ast"
+	"github.com/brimsec/zq/expr/coerce"
+	"github.com/brimsec/zq/expr/function"
 	"github.com/brimsec/zq/field"
 	"github.com/brimsec/zq/pkg/nano"
 	"github.com/brimsec/zq/reglob"
 	"github.com/brimsec/zq/zcode"
 	"github.com/brimsec/zq/zng"
+	"github.com/brimsec/zq/zng/resolver"
 )
 
 type Evaluator interface {
@@ -21,9 +24,8 @@ type Evaluator interface {
 }
 
 var ErrNoSuchField = errors.New("field is not present")
-var ErrIncompatibleTypes = errors.New("incompatible types")
+var ErrIncompatibleTypes = coerce.ErrIncompatibleTypes
 var ErrIndexOutOfBounds = errors.New("array index out of bounds")
-var ErrNoSuchFunction = errors.New("no such function")
 var ErrNotContainer = errors.New("cannot apply in to a non-container")
 var ErrBadCast = errors.New("bad cast")
 
@@ -57,11 +59,7 @@ var ErrBadCast = errors.New("bad cast")
 // TBD: string values and net.IP address do not need to be copied because they
 // are allocated by go libraries and temporary buffers are not used.  This will
 // change down the road when we implement no-allocation string and IP conversion.
-func CompileExpr(node ast.Expression) (Evaluator, error) {
-	return compileExpr(node)
-}
-
-func compileExpr(node ast.Expression) (Evaluator, error) {
+func CompileExpr(zctx *resolver.Context, node ast.Expression) (Evaluator, error) {
 	switch n := node.(type) {
 	case *ast.Literal:
 		return NewLiteral(*n)
@@ -70,17 +68,17 @@ func compileExpr(node ast.Expression) (Evaluator, error) {
 	case *ast.RootRecord:
 		return &RootRecord{}, nil
 	case *ast.UnaryExpression:
-		return compileUnary(*n)
+		return compileUnary(zctx, *n)
 
 	case *ast.BinaryExpression:
 		if n.Operator == "." {
-			return compileDotExpr(n.LHS, n.RHS)
+			return compileDotExpr(zctx, n.LHS, n.RHS)
 		}
-		lhs, err := compileExpr(n.LHS)
+		lhs, err := CompileExpr(zctx, n.LHS)
 		if err != nil {
 			return nil, err
 		}
-		rhs, err := compileExpr(n.RHS)
+		rhs, err := CompileExpr(zctx, n.RHS)
 		if err != nil {
 			return nil, err
 		}
@@ -90,7 +88,7 @@ func compileExpr(node ast.Expression) (Evaluator, error) {
 		case "in":
 			return compileIn(lhs, rhs)
 		case "=", "!=":
-			return compileCompareEquality(lhs, rhs, n.Operator)
+			return compileCompareEquality(zctx, lhs, rhs, n.Operator)
 		case "=~", "!~":
 			return compilePatternMatch(lhs, rhs, n.Operator)
 		case "<", "<=", ">", ">=":
@@ -98,29 +96,29 @@ func compileExpr(node ast.Expression) (Evaluator, error) {
 		case "+", "-", "*", "/":
 			return compileArithmetic(lhs, rhs, n.Operator)
 		case "[":
-			return compileIndexExpr(lhs, rhs)
+			return compileIndexExpr(zctx, lhs, rhs)
 		default:
 			return nil, fmt.Errorf("invalid binary operator %s", n.Operator)
 		}
 
 	case *ast.ConditionalExpression:
-		return compileConditional(*n)
+		return compileConditional(zctx, *n)
 
 	case *ast.FunctionCall:
-		return compileCall(*n)
+		return compileCall(zctx, *n)
 
 	case *ast.CastExpression:
-		return compileCast(*n)
+		return compileCast(zctx, *n)
 
 	default:
 		return nil, fmt.Errorf("invalid expression type %T", node)
 	}
 }
 
-func CompileExprs(nodes []ast.Expression) ([]Evaluator, error) {
+func CompileExprs(zctx *resolver.Context, nodes []ast.Expression) ([]Evaluator, error) {
 	var exprs []Evaluator
 	for k := range nodes {
-		e, err := compileExpr(nodes[k])
+		e, err := CompileExpr(zctx, nodes[k])
 		if err != nil {
 			return nil, err
 		}
@@ -133,11 +131,11 @@ type Not struct {
 	expr Evaluator
 }
 
-func compileUnary(node ast.UnaryExpression) (Evaluator, error) {
+func compileUnary(zctx *resolver.Context, node ast.UnaryExpression) (Evaluator, error) {
 	if node.Operator != "!" {
 		return nil, fmt.Errorf("unknown unary operator %s\n", node.Operator)
 	}
-	e, err := compileExpr(node.Operand)
+	e, err := CompileExpr(zctx, node.Operand)
 	if err != nil {
 		return nil, err
 	}
@@ -226,7 +224,7 @@ func (o *Or) Eval(rec *zng.Record) (zng.Value, error) {
 type In struct {
 	elem      Evaluator
 	container Evaluator
-	vals      Coercion
+	vals      coerce.Pair
 }
 
 func compileIn(elem, container Evaluator) (Evaluator, error) {
@@ -258,11 +256,11 @@ func (i *In) Eval(rec *zng.Record) (zng.Value, error) {
 		if err != nil {
 			return zng.Value{}, err
 		}
-		_, err = i.vals.coerce(elem, zng.Value{typ, zv})
+		_, err = i.vals.Coerce(elem, zng.Value{typ, zv})
 		if err != nil {
 			return zng.Value{}, err
 		}
-		if i.vals.equal() {
+		if i.vals.Equal() {
 			return zng.True, nil
 		}
 	}
@@ -273,7 +271,7 @@ type Equal struct {
 	equality bool
 }
 
-func compileCompareEquality(lhs, rhs Evaluator, operator string) (Evaluator, error) {
+func compileCompareEquality(zctx *resolver.Context, lhs, rhs Evaluator, operator string) (Evaluator, error) {
 	e := &Equal{numeric: newNumeric(lhs, rhs)}
 	switch operator {
 	case "=":
@@ -288,7 +286,7 @@ func compileCompareEquality(lhs, rhs Evaluator, operator string) (Evaluator, err
 func (e *Equal) Eval(rec *zng.Record) (zng.Value, error) {
 	_, err := e.numeric.eval(rec)
 	if err != nil {
-		if err == ErrOverflow {
+		if err == coerce.ErrOverflow {
 			// If there was overflow converting one to the other,
 			// we know they can't be equal.
 			if e.equality {
@@ -298,7 +296,7 @@ func (e *Equal) Eval(rec *zng.Record) (zng.Value, error) {
 		}
 		return zng.Value{}, err
 	}
-	result := e.vals.equal()
+	result := e.vals.Equal()
 	if !e.equality {
 		result = !result
 	}
@@ -364,9 +362,10 @@ func (m *Match) Eval(rec *zng.Record) (zng.Value, error) {
 }
 
 type numeric struct {
+	zctx *resolver.Context
 	lhs  Evaluator
 	rhs  Evaluator
-	vals Coercion
+	vals coerce.Pair
 }
 
 func newNumeric(lhs, rhs Evaluator) numeric {
@@ -409,7 +408,7 @@ func (n *numeric) eval(rec *zng.Record) (int, error) {
 	if err != nil {
 		return 0, err
 	}
-	return n.vals.coerce(lhs, rhs)
+	return n.vals.Coerce(lhs, rhs)
 }
 
 type Compare struct {
@@ -450,7 +449,7 @@ func (c *Compare) Eval(rec *zng.Record) (zng.Value, error) {
 	if err != nil {
 		return zng.Value{}, err
 	}
-	id, err := c.vals.coerce(lhs, rhs)
+	id, err := c.vals.Coerce(lhs, rhs)
 	if err != nil {
 		// If coercion fails due to overflow, then we know there is a
 		// mixed signed and unsigned situation and either the unsigned
@@ -458,7 +457,7 @@ func (c *Compare) Eval(rec *zng.Record) (zng.Value, error) {
 		// or the signed value couldn't be converted to a uint64 because
 		// it was negative.  In either case, the unsigned value is bigger
 		// than the signed value.
-		if err == ErrOverflow {
+		if err == coerce.ErrOverflow {
 			result := 1
 			if zng.IsSigned(lhs.Type.ID()) {
 				result = -1
@@ -468,35 +467,35 @@ func (c *Compare) Eval(rec *zng.Record) (zng.Value, error) {
 		return zng.False, err
 	}
 	var result int
-	if !c.vals.equal() {
+	if !c.vals.Equal() {
 		switch {
 		case zng.IsFloat(id):
-			v1, _ := zng.DecodeFloat64(c.vals.a)
-			v2, _ := zng.DecodeFloat64(c.vals.b)
+			v1, _ := zng.DecodeFloat64(c.vals.A)
+			v2, _ := zng.DecodeFloat64(c.vals.B)
 			if v1 < v2 {
 				result = -1
 			} else {
 				result = 1
 			}
 		case zng.IsSigned(id):
-			v1, _ := zng.DecodeInt(c.vals.a)
-			v2, _ := zng.DecodeInt(c.vals.b)
+			v1, _ := zng.DecodeInt(c.vals.A)
+			v2, _ := zng.DecodeInt(c.vals.B)
 			if v1 < v2 {
 				result = -1
 			} else {
 				result = 1
 			}
 		case zng.IsNumber(id):
-			v1, _ := zng.DecodeUint(c.vals.a)
-			v2, _ := zng.DecodeUint(c.vals.b)
+			v1, _ := zng.DecodeUint(c.vals.A)
+			v2, _ := zng.DecodeUint(c.vals.B)
 			if v1 < v2 {
 				result = -1
 			} else {
 				result = 1
 			}
 		case zng.IsStringy(id):
-			v1, _ := zng.DecodeString(c.vals.a)
-			v2, _ := zng.DecodeString(c.vals.b)
+			v1, _ := zng.DecodeString(c.vals.A)
+			v2, _ := zng.DecodeString(c.vals.B)
 			if v1 < v2 {
 				result = -1
 			} else {
@@ -553,20 +552,20 @@ func (a *Add) Eval(rec *zng.Record) (zng.Value, error) {
 	typ := zng.LookupPrimitiveById(id)
 	switch {
 	case zng.IsFloat(id):
-		v1, _ := zng.DecodeFloat64(a.vals.a)
-		v2, _ := zng.DecodeFloat64(a.vals.b)
+		v1, _ := zng.DecodeFloat64(a.vals.A)
+		v2, _ := zng.DecodeFloat64(a.vals.B)
 		return zng.Value{typ, a.vals.Float64(v1 + v2)}, nil
 	case zng.IsSigned(id):
-		v1, _ := zng.DecodeInt(a.vals.a)
-		v2, _ := zng.DecodeInt(a.vals.b)
+		v1, _ := zng.DecodeInt(a.vals.A)
+		v2, _ := zng.DecodeInt(a.vals.B)
 		return zng.Value{typ, a.vals.Int(v1 + v2)}, nil
 	case zng.IsNumber(id):
-		v1, _ := zng.DecodeUint(a.vals.a)
-		v2, _ := zng.DecodeUint(a.vals.b)
+		v1, _ := zng.DecodeUint(a.vals.A)
+		v2, _ := zng.DecodeUint(a.vals.B)
 		return zng.Value{typ, a.vals.Uint(v1 + v2)}, nil
 	case zng.IsStringy(id):
-		v1, _ := zng.DecodeString(a.vals.a)
-		v2, _ := zng.DecodeString(a.vals.b)
+		v1, _ := zng.DecodeString(a.vals.A)
+		v2, _ := zng.DecodeString(a.vals.B)
 		// XXX GC
 		return zng.Value{typ, zng.EncodeString(v1 + v2)}, nil
 	}
@@ -581,16 +580,16 @@ func (s *Subtract) Eval(rec *zng.Record) (zng.Value, error) {
 	typ := zng.LookupPrimitiveById(id)
 	switch {
 	case zng.IsFloat(id):
-		v1, _ := zng.DecodeFloat64(s.vals.a)
-		v2, _ := zng.DecodeFloat64(s.vals.b)
+		v1, _ := zng.DecodeFloat64(s.vals.A)
+		v2, _ := zng.DecodeFloat64(s.vals.B)
 		return zng.Value{typ, s.vals.Float64(v1 - v2)}, nil
 	case zng.IsSigned(id):
-		v1, _ := zng.DecodeInt(s.vals.a)
-		v2, _ := zng.DecodeInt(s.vals.b)
+		v1, _ := zng.DecodeInt(s.vals.A)
+		v2, _ := zng.DecodeInt(s.vals.B)
 		return zng.Value{typ, s.vals.Int(v1 - v2)}, nil
 	case zng.IsNumber(id):
-		v1, _ := zng.DecodeUint(s.vals.a)
-		v2, _ := zng.DecodeUint(s.vals.b)
+		v1, _ := zng.DecodeUint(s.vals.A)
+		v2, _ := zng.DecodeUint(s.vals.B)
 		return zng.Value{typ, s.vals.Uint(v1 - v2)}, nil
 	}
 	return zng.Value{}, ErrIncompatibleTypes
@@ -604,16 +603,16 @@ func (m *Multiply) Eval(rec *zng.Record) (zng.Value, error) {
 	typ := zng.LookupPrimitiveById(id)
 	switch {
 	case zng.IsFloat(id):
-		v1, _ := zng.DecodeFloat64(m.vals.a)
-		v2, _ := zng.DecodeFloat64(m.vals.b)
+		v1, _ := zng.DecodeFloat64(m.vals.A)
+		v2, _ := zng.DecodeFloat64(m.vals.B)
 		return zng.Value{typ, m.vals.Float64(v1 * v2)}, nil
 	case zng.IsSigned(id):
-		v1, _ := zng.DecodeInt(m.vals.a)
-		v2, _ := zng.DecodeInt(m.vals.b)
+		v1, _ := zng.DecodeInt(m.vals.A)
+		v2, _ := zng.DecodeInt(m.vals.B)
 		return zng.Value{typ, m.vals.Int(v1 * v2)}, nil
 	case zng.IsNumber(id):
-		v1, _ := zng.DecodeUint(m.vals.a)
-		v2, _ := zng.DecodeUint(m.vals.b)
+		v1, _ := zng.DecodeUint(m.vals.A)
+		v2, _ := zng.DecodeUint(m.vals.B)
 		return zng.Value{typ, m.vals.Uint(v1 * v2)}, nil
 	}
 	return zng.Value{}, ErrIncompatibleTypes
@@ -627,24 +626,24 @@ func (d *Divide) Eval(rec *zng.Record) (zng.Value, error) {
 	typ := zng.LookupPrimitiveById(id)
 	switch {
 	case zng.IsFloat(id):
-		v1, _ := zng.DecodeFloat64(d.vals.a)
-		v2, _ := zng.DecodeFloat64(d.vals.b)
+		v1, _ := zng.DecodeFloat64(d.vals.A)
+		v2, _ := zng.DecodeFloat64(d.vals.B)
 		if v2 == 0 {
-			return zng.Value{zng.TypeError, zng.EncodeString("floating point divide by 0")}, nil
+			return zng.NewErrorf("floating point divide by 0"), nil
 		}
 		return zng.Value{typ, d.vals.Float64(v1 / v2)}, nil
 	case zng.IsSigned(id):
-		v1, _ := zng.DecodeInt(d.vals.a)
-		v2, _ := zng.DecodeInt(d.vals.b)
+		v1, _ := zng.DecodeInt(d.vals.A)
+		v2, _ := zng.DecodeInt(d.vals.B)
 		if v2 == 0 {
-			return zng.Value{zng.TypeError, zng.EncodeString("signed integer divide by 0")}, nil
+			return zng.NewErrorf("signed integer divide by 0"), nil
 		}
 		return zng.Value{typ, d.vals.Int(v1 / v2)}, nil
 	case zng.IsNumber(id):
-		v1, _ := zng.DecodeUint(d.vals.a)
-		v2, _ := zng.DecodeUint(d.vals.b)
+		v1, _ := zng.DecodeUint(d.vals.A)
+		v2, _ := zng.DecodeUint(d.vals.B)
 		if v2 == 0 {
-			return zng.Value{zng.TypeError, zng.EncodeString("unsigned integer divide by 0")}, nil
+			return zng.NewErrorf("unsigned integer divide by 0"), nil
 		}
 		return zng.Value{typ, d.vals.Uint(v1 / v2)}, nil
 	}
@@ -687,12 +686,13 @@ func lookupKey(mapBytes, target zcode.Bytes) (zcode.Bytes, bool) {
 // Index represents an index operator "container[index]" where container is
 // either an array (with index type integer) or a record (with index type string).
 type Index struct {
+	zctx      *resolver.Context
 	container Evaluator
 	index     Evaluator
 }
 
-func compileIndexExpr(container, index Evaluator) (Evaluator, error) {
-	return &Index{container, index}, nil
+func compileIndexExpr(zctx *resolver.Context, container, index Evaluator) (Evaluator, error) {
+	return &Index{zctx, container, index}, nil
 }
 
 func (i *Index) Eval(rec *zng.Record) (zng.Value, error) {
@@ -719,13 +719,13 @@ func (i *Index) Eval(rec *zng.Record) (zng.Value, error) {
 func indexArray(typ *zng.TypeArray, array zcode.Bytes, index zng.Value) (zng.Value, error) {
 	id := index.Type.ID()
 	if !zng.IsInteger(id) {
-		return zng.NewError(errors.New("array index is not an integer")), nil
+		return zng.NewErrorf("array index is not an integer"), nil
 	}
 	var idx uint
 	if zng.IsSigned(id) {
 		v, _ := zng.DecodeInt(index.Bytes)
 		if idx < 0 {
-			return zng.NewError(errors.New("array index out of bounds")), nil
+			return zng.NewErrorf("array index out of bounds"), nil
 		}
 		idx = uint(v)
 	} else {
@@ -742,7 +742,7 @@ func indexArray(typ *zng.TypeArray, array zcode.Bytes, index zng.Value) (zng.Val
 func indexRecord(typ *zng.TypeRecord, record zcode.Bytes, index zng.Value) (zng.Value, error) {
 	id := index.Type.ID()
 	if !zng.IsStringy(id) {
-		return zng.NewError(errors.New("record index is not a string")), nil
+		return zng.NewErrorf("record index is not a string"), nil
 	}
 	field, _ := zng.DecodeString(index.Bytes)
 	result, err := zng.NewRecord(typ, record).ValueByField(string(field))
@@ -755,12 +755,12 @@ func indexRecord(typ *zng.TypeRecord, record zcode.Bytes, index zng.Value) (zng.
 func indexMap(typ *zng.TypeMap, mapBytes zcode.Bytes, key zng.Value) (zng.Value, error) {
 	if key.Type != typ.KeyType {
 		//XXX should try coercing?
-		return zng.NewError(errors.New("map key type does not match index type")), nil
+		return zng.NewErrorf("map key type does not match index type"), nil
 	}
 	if valBytes, ok := lookupKey(mapBytes, key.Bytes); ok {
 		return zng.Value{typ.ValType, valBytes}, nil
 	}
-	return zng.NewError(fmt.Errorf("key not found in map: %s", key)), nil
+	return zng.NewErrorf("key not found in map: %s", key), nil
 }
 
 type Conditional struct {
@@ -769,17 +769,17 @@ type Conditional struct {
 	elseExpr  Evaluator
 }
 
-func compileConditional(node ast.ConditionalExpression) (Evaluator, error) {
+func compileConditional(zctx *resolver.Context, node ast.ConditionalExpression) (Evaluator, error) {
 	var err error
-	predicate, err := compileExpr(node.Condition)
+	predicate, err := CompileExpr(zctx, node.Condition)
 	if err != nil {
 		return nil, err
 	}
-	thenExpr, err := compileExpr(node.Then)
+	thenExpr, err := CompileExpr(zctx, node.Then)
 	if err != nil {
 		return nil, err
 	}
-	elseExpr, err := compileExpr(node.Else)
+	elseExpr, err := CompileExpr(zctx, node.Else)
 	if err != nil {
 		return nil, err
 	}
@@ -804,12 +804,12 @@ func (c *Conditional) Eval(rec *zng.Record) (zng.Value, error) {
 	return c.elseExpr.Eval(rec)
 }
 
-func compileDotExpr(lhs, rhs ast.Expression) (*DotExpr, error) {
+func compileDotExpr(zctx *resolver.Context, lhs, rhs ast.Expression) (*DotExpr, error) {
 	id, ok := rhs.(*ast.Identifier)
 	if !ok {
 		return nil, errors.New("rhs of dot expression is not an identifier")
 	}
-	record, err := compileExpr(lhs)
+	record, err := CompileExpr(zctx, lhs)
 	if err != nil {
 		return nil, err
 	}
@@ -817,27 +817,29 @@ func compileDotExpr(lhs, rhs ast.Expression) (*DotExpr, error) {
 }
 
 type Call struct {
-	name     string
-	function Function
-	exprs    []Evaluator
-	args     *Args
+	zctx  *resolver.Context
+	name  string
+	fn    function.Interface
+	exprs []Evaluator
+	args  []zng.Value
 }
 
-func compileCall(node ast.FunctionCall) (Evaluator, error) {
-	fn, ok := allFns[node.Function]
-	if !ok {
-		return nil, fmt.Errorf("%s: %w", node.Function, ErrNoSuchFunction)
+func compileCall(zctx *resolver.Context, node ast.FunctionCall) (Evaluator, error) {
+	// For now, we special case cut here.  As we add more stateful functions
+	// this will make more sense.  We could also compile any reducer as a
+	// stateful function here, e.g., count(), would produce a new count() for
+	// each record encountered analogous to juttle stream functions.
+	if node.Function == "cut" {
+		return compileCut(zctx, node)
 	}
 	nargs := len(node.Args)
-	if fn.minArgs >= 0 && nargs < fn.minArgs {
-		return nil, fmt.Errorf("%s: %w", node.Function, ErrTooFewArgs)
-	}
-	if fn.maxArgs >= 0 && nargs > fn.maxArgs {
-		return nil, fmt.Errorf("%s: %w", node.Function, ErrTooManyArgs)
+	fn, err := function.New(node.Function, nargs)
+	if err != nil {
+		return nil, fmt.Errorf("%s: %w", node.Function, err)
 	}
 	exprs := make([]Evaluator, 0, nargs)
 	for _, expr := range node.Args {
-		e, err := compileExpr(expr)
+		e, err := CompileExpr(zctx, expr)
 		if err != nil {
 			return nil, err
 		}
@@ -845,10 +847,11 @@ func compileCall(node ast.FunctionCall) (Evaluator, error) {
 	}
 
 	return &Call{
-		name:     node.Function,
-		function: fn.impl,
-		exprs:    exprs,
-		args:     NewArgs(nargs),
+		zctx:  zctx,
+		name:  node.Function,
+		fn:    fn,
+		exprs: exprs,
+		args:  make([]zng.Value, nargs),
 	}, nil
 }
 
@@ -858,13 +861,13 @@ func (c *Call) Eval(rec *zng.Record) (zng.Value, error) {
 		if err != nil {
 			return zng.Value{}, err
 		}
-		c.args.vals[k] = val
+		c.args[k] = val
 	}
-	return c.function(c.args)
+	return c.fn.Call(c.args)
 }
 
-func compileCast(node ast.CastExpression) (Evaluator, error) {
-	expr, err := compileExpr(node.Expr)
+func compileCast(zctx *resolver.Context, node ast.CastExpression) (Evaluator, error) {
+	expr, err := CompileExpr(zctx, node.Expr)
 	if err != nil {
 		return nil, err
 	}
@@ -901,6 +904,7 @@ func compileCast(node ast.CastExpression) (Evaluator, error) {
 	case "bytes":
 		return &BytesCast{expr}, nil
 	default:
+		// XXX See issue #1572.   To implement aliascast here.
 		return nil, fmt.Errorf("cast to %s not implemeneted", node.Type)
 	}
 }
@@ -917,7 +921,7 @@ func (i *IntCast) Eval(rec *zng.Record) (zng.Value, error) {
 	if err != nil {
 		return zng.Value{}, err
 	}
-	v, ok := CoerceToInt(zv)
+	v, ok := coerce.ToInt(zv)
 	// XXX better error message
 	if !ok || (i.min != 0 && (v < i.min || v > i.max)) {
 		return zng.Value{}, ErrBadCast
@@ -937,7 +941,7 @@ func (u *UintCast) Eval(rec *zng.Record) (zng.Value, error) {
 	if err != nil {
 		return zng.Value{}, err
 	}
-	v, ok := CoerceToUint(zv)
+	v, ok := coerce.ToUint(zv)
 	// XXX better error message
 	if !ok || (u.max != 0 && v > u.max) {
 		return zng.Value{}, ErrBadCast
@@ -955,7 +959,7 @@ func (i *Float64Cast) Eval(rec *zng.Record) (zng.Value, error) {
 	if err != nil {
 		return zng.Value{}, err
 	}
-	f, ok := CoerceToFloat(zv)
+	f, ok := coerce.ToFloat(zv)
 	if !ok {
 		return zng.Value{}, ErrBadCast
 	}
@@ -971,7 +975,7 @@ func (i *IPCast) Eval(rec *zng.Record) (zng.Value, error) {
 	if err != nil {
 		return zng.Value{}, err
 	}
-	if !isStringy(zv) {
+	if !zv.IsStringy() {
 		return zng.Value{}, ErrBadCast
 	}
 	ip := net.ParseIP(string(zv.Bytes))
@@ -997,7 +1001,7 @@ func (t *TimeCast) Eval(rec *zng.Record) (zng.Value, error) {
 		// XXX GC
 		return zng.Value{zng.TypeTime, zng.EncodeTime(ts)}, nil
 	}
-	ns, ok := CoerceToInt(zv)
+	ns, ok := coerce.ToInt(zv)
 	if !ok {
 		return zng.Value{}, ErrBadCast
 	}
@@ -1069,8 +1073,8 @@ func NewRootField(name string) Evaluator {
 
 var ErrInference = errors.New("assigment name could not be inferred from rhs expressioin")
 
-func CompileAssignment(node *ast.Assignment) (field.Static, Evaluator, error) {
-	rhs, err := CompileExpr(node.RHS)
+func CompileAssignment(zctx *resolver.Context, node *ast.Assignment) (field.Static, Evaluator, error) {
+	rhs, err := CompileExpr(zctx, node.RHS)
 	if err != nil {
 		return nil, nil, fmt.Errorf("rhs of assigment expression: %w", err)
 	}
diff --git a/expr/function/bytes.go b/expr/function/bytes.go
new file mode 100644
index 000000000..16ca64d11
--- /dev/null
+++ b/expr/function/bytes.go
@@ -0,0 +1,33 @@
+package function
+
+import (
+	"encoding/base64"
+
+	"github.com/brimsec/zq/zng"
+)
+
+type fromBase64 struct{}
+
+func (*fromBase64) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("from_base64")
+	}
+	s, _ := zng.DecodeString(zv.Bytes)
+	b, err := base64.StdEncoding.DecodeString(s)
+	if err != nil {
+		return zng.NewError(err), nil
+	}
+	return zng.Value{zng.TypeBytes, zng.EncodeBytes(b)}, nil
+}
+
+type toBase64 struct{}
+
+func (*toBase64) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("from_base64")
+	}
+	s := base64.StdEncoding.EncodeToString(zv.Bytes)
+	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
+}
diff --git a/expr/function/function.go b/expr/function/function.go
new file mode 100644
index 000000000..0c93b7466
--- /dev/null
+++ b/expr/function/function.go
@@ -0,0 +1,159 @@
+package function
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/brimsec/zq/anymath"
+	"github.com/brimsec/zq/expr/result"
+	"github.com/brimsec/zq/zng"
+)
+
+var (
+	ErrBadArgument    = errors.New("bad argument")
+	ErrNoSuchFunction = errors.New("no such function")
+	ErrTooFewArgs     = errors.New("too few arguments")
+	ErrTooManyArgs    = errors.New("too many arguments")
+)
+
+type Interface interface {
+	Call([]zng.Value) (zng.Value, error)
+}
+
+func New(name string, narg int) (Interface, error) {
+	argmin := 1
+	argmax := 1
+	var f Interface
+	switch name {
+	default:
+		return nil, ErrNoSuchFunction
+	case "len":
+		f = &lenFn{}
+	case "Math.abs", "abs":
+		f = &abs{}
+	case "Math.ceil", "ceil":
+		f = &ceil{}
+	case "Math.floor", "floor":
+		f = &floor{}
+	case "Math.log", "log":
+		f = &log{}
+	case "Math.max", "max":
+		argmax = -1
+		f = &reducer{fn: anymath.Max}
+	case "Math.min", "min":
+		argmax = -1
+		f = &reducer{fn: anymath.Min}
+	case "Math.mod", "mod":
+		argmin = 2
+		argmax = 2
+		f = &mod{}
+	case "Math.round", "round":
+		f = &round{}
+	case "Math.pow", "pow":
+		argmin = 2
+		argmax = 2
+		f = &pow{}
+	case "Math.sqrt", "sqrt":
+		f = &sqrt{}
+	case "String.byteLen":
+		f = &bytelen{}
+	case "String.formatFloat":
+		// deprecated by <float-val>:string
+		f = &stringFormatFloat{}
+	case "String.formatInt":
+		// deprecated by <int-val>:string
+		f = &stringFormatInt{}
+	case "String.formatIp":
+		// deprecated by <ip-val>:string
+		f = &stringFormatIp{}
+	case "String.parseFloat":
+		// deprecated by <string-val>:float
+		f = &stringParseFloat{}
+	case "String.parseInt":
+		// deprecated by <string-val>:int<n>
+		f = &stringParseInt{}
+	case "String.parseIp":
+		// deprecated by <string-val>:ip
+		f = &stringParseIp{}
+	case "String.replace", "replace":
+		argmin = 3
+		argmax = 3
+		f = &replace{}
+	case "String.runeLen", "rune_len":
+		f = &runeLen{}
+	case "String.toLower", "to_lower":
+		f = &toLower{}
+	case "String.toUpper", "to_upper":
+		f = &toUpper{}
+	case "String.trim", "trim":
+		f = &trim{}
+	case "Time.fromISO", "iso":
+		f = &iso{}
+	case "Time.fromMilliseconds", "ms":
+		f = &ms{}
+	case "Time.fromMicroseconds", "us":
+		f = &us{}
+	case "Time.fromNanoseconds", "ns":
+		f = &ns{}
+	case "Time.trunc", "trunc":
+		argmin = 2
+		argmax = 2
+		f = &trunc{}
+	case "typeof":
+		f = &typeOf{}
+	case "iserr":
+		f = &isErr{}
+	case "toBase64", "to_base64":
+		f = &toBase64{}
+	case "fromBase64", "from_base64":
+		f = &fromBase64{}
+	}
+	if argmin != -1 && narg < argmin {
+		return nil, ErrTooFewArgs
+	}
+	if argmax != -1 && narg > argmax {
+		return nil, ErrTooManyArgs
+	}
+	return f, nil
+}
+
+func zverr(msg string, err error) (zng.Value, error) {
+	return zng.Value{}, fmt.Errorf("%s: %w", msg, err)
+}
+
+func badarg(msg string) (zng.Value, error) {
+	return zverr(msg, ErrBadArgument)
+}
+
+type lenFn struct {
+	result.Buffer
+}
+
+func (l *lenFn) Call(args []zng.Value) (zng.Value, error) {
+	switch zng.AliasedType(args[0].Type).(type) {
+	case *zng.TypeArray, *zng.TypeSet:
+		v := args[0]
+		len, err := v.ContainerLength()
+		if err != nil {
+			return zng.Value{}, err
+		}
+		return zng.Value{zng.TypeInt64, l.Int(int64(len))}, nil
+	default:
+		return badarg("len")
+	}
+}
+
+type typeOf struct{}
+
+func (t *typeOf) Call(args []zng.Value) (zng.Value, error) {
+	return zng.Value{zng.TypeType, zng.EncodeType(args[0].Type.String())}, nil
+}
+
+type isErr struct{}
+
+func (*isErr) Call(args []zng.Value) (zng.Value, error) {
+	if args[0].IsError() {
+		return zng.True, nil
+	}
+	return zng.False, nil
+}
diff --git a/expr/function/math.go b/expr/function/math.go
new file mode 100644
index 000000000..64a2a312c
--- /dev/null
+++ b/expr/function/math.go
@@ -0,0 +1,219 @@
+package function
+
+import (
+	"math"
+
+	"github.com/brimsec/zq/anymath"
+	"github.com/brimsec/zq/expr/coerce"
+	"github.com/brimsec/zq/expr/result"
+	"github.com/brimsec/zq/zng"
+)
+
+type abs struct {
+	result.Buffer
+}
+
+func (a *abs) Call(args []zng.Value) (zng.Value, error) {
+	v := args[0]
+	id := v.Type.ID()
+	if zng.IsFloat(id) {
+		f, _ := zng.DecodeFloat64(v.Bytes)
+		f = math.Abs(f)
+		return zng.Value{zng.TypeFloat64, a.Float64(f)}, nil
+	}
+	if !zng.IsInteger(id) {
+		return badarg("abs")
+	}
+	if !zng.IsSigned(id) {
+		return v, nil
+	}
+	x, _ := zng.DecodeInt(v.Bytes)
+	if x < 0 {
+		x = -x
+	}
+	return zng.Value{v.Type, a.Int(x)}, nil
+}
+
+type ceil struct {
+	result.Buffer
+}
+
+func (c *ceil) Call(args []zng.Value) (zng.Value, error) {
+	v := args[0]
+	id := v.Type.ID()
+	if zng.IsFloat(id) {
+		f, _ := zng.DecodeFloat64(v.Bytes)
+		f = math.Ceil(f)
+		return zng.Value{zng.TypeFloat64, c.Float64(f)}, nil
+	}
+	if zng.IsInteger(id) {
+		return v, nil
+	}
+	return badarg("ceil")
+}
+
+type floor struct {
+	result.Buffer
+}
+
+func (f *floor) Call(args []zng.Value) (zng.Value, error) {
+	v := args[0]
+	id := v.Type.ID()
+	if zng.IsFloat(id) {
+		v, _ := zng.DecodeFloat64(v.Bytes)
+		v = math.Floor(v)
+		return zng.Value{zng.TypeFloat64, f.Float64(v)}, nil
+	}
+	if zng.IsInteger(id) {
+		return v, nil
+	}
+	return badarg("floor")
+}
+
+type log struct {
+	result.Buffer
+}
+
+func (l *log) Call(args []zng.Value) (zng.Value, error) {
+	x, ok := coerce.ToFloat(args[0])
+	// XXX should have better error messages
+	if !ok {
+		return badarg("log")
+	}
+	if x <= 0 {
+		return badarg("log")
+	}
+	return zng.Value{zng.TypeFloat64, l.Float64(math.Log(x))}, nil
+}
+
+type reducer struct {
+	result.Buffer
+	fn *anymath.Function
+}
+
+func (r *reducer) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	typ := zv.Type
+	id := typ.ID()
+	if zng.IsFloat(id) {
+		result, _ := zng.DecodeFloat64(zv.Bytes)
+		for _, zv := range args[1:] {
+			v, ok := coerce.ToFloat(zv)
+			if !ok {
+				return zng.Value{}, ErrBadArgument
+			}
+			result = r.fn.Float64(result, v)
+		}
+		return zng.Value{typ, r.Float64(result)}, nil
+	}
+	if !zng.IsNumber(id) {
+		// XXX better message
+		return zng.Value{}, ErrBadArgument
+	}
+	if zng.IsSigned(id) {
+		result, _ := zng.DecodeInt(zv.Bytes)
+		for _, zv := range args[1:] {
+			v, ok := coerce.ToInt(zv)
+			if !ok {
+				// XXX better message
+				return zng.Value{}, ErrBadArgument
+			}
+			result = r.fn.Int64(result, v)
+		}
+		return zng.Value{typ, r.Int(result)}, nil
+	}
+	result, _ := zng.DecodeUint(zv.Bytes)
+	for _, zv := range args[1:] {
+		v, ok := coerce.ToUint(zv)
+		if !ok {
+			// XXX better message
+			return zng.Value{}, ErrBadArgument
+		}
+		result = r.fn.Uint64(result, v)
+	}
+	return zng.Value{typ, r.Uint(result)}, nil
+}
+
+type mod struct {
+	result.Buffer
+}
+
+//XXX currently integer mod, but this could also do fmod
+// also why doesn't zql have x%y instead of Math.mod(x,y)?
+func (m *mod) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	id := zv.Type.ID()
+	if zng.IsFloat(id) {
+		return badarg("mod")
+	}
+	y, ok := coerce.ToUint(args[1])
+	if !ok {
+		return badarg("mod")
+	}
+	if !zng.IsNumber(id) {
+		return badarg("mod")
+	}
+	if zng.IsSigned(id) {
+		x, _ := zng.DecodeInt(zv.Bytes)
+		return zng.Value{zv.Type, m.Int(x % int64(y))}, nil
+	}
+	x, _ := zng.DecodeUint(zv.Bytes)
+	return zng.Value{zv.Type, m.Uint(x % y)}, nil
+}
+
+type round struct {
+	result.Buffer
+}
+
+func (r *round) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	id := zv.Type.ID()
+	if zng.IsFloat(id) {
+		f, _ := zng.DecodeFloat64(zv.Bytes)
+		return zng.Value{zv.Type, r.Float64(math.Round(f))}, nil
+
+	}
+	if !zng.IsNumber(id) {
+		return badarg("round")
+	}
+	return zv, nil
+}
+
+type pow struct {
+	result.Buffer
+}
+
+func (p *pow) Call(args []zng.Value) (zng.Value, error) {
+	x, ok := coerce.ToFloat(args[0])
+	if !ok {
+		return badarg("pow")
+	}
+	y, ok := coerce.ToFloat(args[1])
+	if !ok {
+		return badarg("pow")
+	}
+	r := math.Pow(x, y)
+	if math.IsNaN(r) {
+		return badarg("pow")
+	}
+	return zng.Value{zng.TypeFloat64, p.Float64(r)}, nil
+}
+
+type sqrt struct {
+	result.Buffer
+}
+
+func (s *sqrt) Call(args []zng.Value) (zng.Value, error) {
+	x, ok := coerce.ToFloat(args[0])
+	if !ok {
+		return badarg("sqrt")
+	}
+	x = math.Sqrt(x)
+	if math.IsNaN(x) {
+		// For now we can't represent non-numeric values in a float64,
+		// we will revisit this but it has implications for file
+		// formats, zql, etc.
+		return badarg("sqrt")
+	}
+	return zng.Value{zng.TypeFloat64, s.Float64(x)}, nil
+}
diff --git a/expr/function/string.go b/expr/function/string.go
new file mode 100644
index 000000000..c37a5194a
--- /dev/null
+++ b/expr/function/string.go
@@ -0,0 +1,226 @@
+package function
+
+import (
+	"net"
+	"strconv"
+	"strings"
+	"unicode/utf8"
+
+	"github.com/brimsec/zq/expr/result"
+	"github.com/brimsec/zq/zng"
+)
+
+type bytelen struct {
+	result.Buffer
+}
+
+// XXX we should just have a len function that applies to different types
+// and a way to get unicode char len, charlen()?
+func (b *bytelen) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("Strings.byteLen")
+	}
+	v := len(string(zv.Bytes))
+	return zng.Value{zng.TypeInt64, b.Int(int64(v))}, nil
+}
+
+// XXX these string format functions should be handlded by :string cast
+
+type stringFormatFloat struct{}
+
+func (s *stringFormatFloat) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if zv.Type.ID() != zng.IdFloat64 {
+		return badarg("string.floatToString")
+	}
+	f, _ := zng.DecodeFloat64(zv.Bytes)
+	// XXX GC
+	v := strconv.FormatFloat(f, 'g', -1, 64)
+	return zng.Value{zng.TypeString, zng.EncodeString(v)}, nil
+}
+
+type stringFormatInt struct{}
+
+func (s *stringFormatInt) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	id := zv.Type.ID()
+	var out string
+	if !zng.IsInteger(id) {
+		return badarg("string.intToString")
+	}
+	if zng.IsSigned(id) {
+		v, _ := zng.DecodeInt(zv.Bytes)
+		// XXX GC
+		out = strconv.FormatInt(v, 10)
+	} else {
+		v, _ := zng.DecodeUint(zv.Bytes)
+		// XXX GC
+		out = strconv.FormatUint(v, 10)
+	}
+	return zng.Value{zng.TypeString, zng.EncodeString(out)}, nil
+}
+
+type stringFormatIp struct{}
+
+func (s *stringFormatIp) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if zv.Type.ID() != zng.IdIP {
+		return badarg("string.ipToString")
+	}
+	// XXX GC
+	ip, _ := zng.DecodeIP(zv.Bytes)
+	return zng.Value{zng.TypeString, zng.EncodeString(ip.String())}, nil
+}
+
+type stringParseInt struct {
+	result.Buffer
+}
+
+func (s *stringParseInt) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("String.parseInt")
+	}
+	v, e := zng.DecodeString(zv.Bytes)
+	if e != nil {
+		return zng.Value{}, e
+	}
+	i, perr := strconv.ParseInt(v, 10, 64)
+	if perr != nil {
+		// Get rid of the strconv wrapping gunk to get the
+		// actual error message
+		e := perr.(*strconv.NumError)
+		return zverr("String.parseInt", e.Err)
+	}
+	return zng.Value{zng.TypeInt64, s.Int(i)}, nil
+}
+
+type stringParseFloat struct {
+	result.Buffer
+}
+
+func (s *stringParseFloat) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("String.parseFloat")
+	}
+	v, perr := zng.DecodeString(zv.Bytes)
+	if perr != nil {
+		return zng.Value{}, perr
+	}
+	f, perr := strconv.ParseFloat(v, 64)
+	if perr != nil {
+		// Get rid of the strconv wrapping gunk to get the
+		// actual error message
+		e := perr.(*strconv.NumError)
+		return zverr("String.parseFloat", e.Err)
+	}
+	return zng.Value{zng.TypeFloat64, s.Float64(f)}, nil
+}
+
+type stringParseIp struct{}
+
+func (s *stringParseIp) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("String.parseIp")
+	}
+	v, err := zng.DecodeString(zv.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	// XXX GC
+	a := net.ParseIP(v)
+	if a == nil {
+		return badarg("String.parseIp")
+	}
+	// XXX GC
+	return zng.Value{zng.TypeIP, zng.EncodeIP(a)}, nil
+}
+
+type replace struct{}
+
+func (*replace) Call(args []zng.Value) (zng.Value, error) {
+	zvs := args[0]
+	zvold := args[1]
+	zvnew := args[2]
+	if !zvs.IsStringy() || !zvold.IsStringy() || !zvnew.IsStringy() {
+		return badarg("replace")
+	}
+	s, err := zng.DecodeString(zvs.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	old, err := zng.DecodeString(zvold.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	new, err := zng.DecodeString(zvnew.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	result := strings.ReplaceAll(s, old, new)
+	return zng.Value{zng.TypeString, zng.EncodeString(result)}, nil
+}
+
+type runeLen struct {
+	result.Buffer
+}
+
+func (s *runeLen) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("Strings.byteLen")
+	}
+	in, err := zng.DecodeString(zv.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	v := utf8.RuneCountInString(in)
+	return zng.Value{zng.TypeInt64, s.Int(int64(v))}, nil
+}
+
+type toLower struct{}
+
+func (*toLower) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("to_lower")
+	}
+	s, err := zng.DecodeString(zv.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	// XXX GC
+	s = strings.ToLower(s)
+	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
+}
+
+type toUpper struct{}
+
+func (*toUpper) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("to_upper")
+	}
+	s, err := zng.DecodeString(zv.Bytes)
+	if err != nil {
+		return zng.Value{}, err
+	}
+	// XXX GC
+	s = strings.ToUpper(s)
+	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
+}
+
+type trim struct{}
+
+func (*trim) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("trim")
+	}
+	// XXX GC
+	s := strings.TrimSpace(string(zv.Bytes))
+	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
+}
diff --git a/expr/function/time.go b/expr/function/time.go
new file mode 100644
index 000000000..5f2acbf44
--- /dev/null
+++ b/expr/function/time.go
@@ -0,0 +1,83 @@
+package function
+
+import (
+	"time"
+
+	"github.com/brimsec/zq/expr/coerce"
+	"github.com/brimsec/zq/expr/result"
+	"github.com/brimsec/zq/pkg/nano"
+	"github.com/brimsec/zq/zng"
+)
+
+type iso struct {
+	result.Buffer
+}
+
+func (i *iso) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	if !zv.IsStringy() {
+		return badarg("iso")
+	}
+	ts, e := time.Parse(time.RFC3339Nano, string(zv.Bytes))
+	if e != nil {
+		return badarg("iso")
+	}
+	return zng.Value{zng.TypeTime, i.Time(nano.Ts(ts.UnixNano()))}, nil
+}
+
+type ms struct {
+	result.Buffer
+}
+
+func (m *ms) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	ms, ok := coerce.ToInt(zv)
+	if !ok {
+		return badarg("ms")
+	}
+	return zng.Value{zng.TypeTime, m.Time(nano.Ts(ms * 1_000_000))}, nil
+}
+
+type us struct {
+	result.Buffer
+}
+
+func (u *us) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	us, ok := coerce.ToInt(zv)
+	if !ok {
+		return badarg("us")
+	}
+	return zng.Value{zng.TypeTime, u.Time(nano.Ts(us * 1000))}, nil
+}
+
+type ns struct {
+	result.Buffer
+}
+
+func (n *ns) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	ns, ok := coerce.ToInt(zv)
+	if !ok {
+		return badarg("ns")
+	}
+	return zng.Value{zng.TypeTime, n.Time(nano.Ts(ns))}, nil
+}
+
+type trunc struct {
+	result.Buffer
+}
+
+func (t *trunc) Call(args []zng.Value) (zng.Value, error) {
+	zv := args[0]
+	ts, ok := coerce.ToTime(zv)
+	if !ok {
+		return badarg("trunc")
+	}
+	dur, ok := coerce.ToInt(args[1])
+	if !ok {
+		return badarg("trunc")
+	}
+	dur *= 1_000_000_000
+	return zng.Value{zng.TypeTime, t.Time(nano.Ts(ts.Trunc(dur)))}, nil
+}
diff --git a/expr/functions.go b/expr/functions.go
deleted file mode 100644
index f9874c8f3..000000000
--- a/expr/functions.go
+++ /dev/null
@@ -1,550 +0,0 @@
-package expr
-
-import (
-	"encoding/base64"
-	"errors"
-	"fmt"
-	"math"
-	"net"
-	"strconv"
-	"strings"
-	"time"
-	"unicode/utf8"
-
-	"github.com/brimsec/zq/anymath"
-	"github.com/brimsec/zq/pkg/nano"
-	"github.com/brimsec/zq/zng"
-)
-
-type Args struct {
-	vals []zng.Value
-	result
-}
-
-func NewArgs(n int) *Args {
-	return &Args{
-		vals: make([]zng.Value, n),
-	}
-}
-
-type Function func(*Args) (zng.Value, error)
-
-var ErrTooFewArgs = errors.New("too few arguments")
-var ErrTooManyArgs = errors.New("too many arguments")
-var ErrBadArgument = errors.New("bad argument")
-
-var allFns = map[string]struct {
-	minArgs int
-	maxArgs int
-	impl    Function
-}{
-	"len": {1, 1, lenFn},
-
-	"Math.abs":   {1, 1, mathAbs},
-	"Math.ceil":  {1, 1, mathCeil},
-	"Math.floor": {1, 1, mathFloor},
-	"Math.log":   {1, 1, mathLog},
-	"Math.max":   {1, -1, mathMax},
-	"Math.min":   {1, -1, mathMin},
-	"Math.mod":   {2, 2, mathMod},
-	"Math.round": {1, 1, mathRound},
-	"Math.pow":   {2, 2, mathPow},
-	"Math.sqrt":  {1, 1, mathSqrt},
-
-	"String.byteLen":     {1, 1, stringByteLen},
-	"String.formatFloat": {1, 1, stringFormatFloat},
-	"String.formatInt":   {1, 1, stringFormatInt},
-	"String.formatIp":    {1, 1, stringFormatIp},
-	"String.parseFloat":  {1, 1, stringParseFloat},
-	"String.parseInt":    {1, 1, stringParseInt},
-	"String.parseIp":     {1, 1, stringParseIp},
-	"String.replace":     {3, 3, stringReplace},
-	"String.runeLen":     {1, 1, stringRuneLen},
-	"String.toLower":     {1, 1, stringToLower},
-	"String.toUpper":     {1, 1, stringToUpper},
-	"String.trim":        {1, 1, stringTrim},
-
-	"Time.fromISO":          {1, 1, timeFromISO},
-	"Time.fromMilliseconds": {1, 1, timeFromMsec},
-	"Time.fromMicroseconds": {1, 1, timeFromUsec},
-	"Time.fromNanoseconds":  {1, 1, timeFromNsec},
-	"Time.trunc":            {2, 2, timeTrunc},
-
-	"typeof":     {1, 1, typeOf},
-	"iserr":      {1, 1, isErr},
-	"toBase64":   {1, 1, toBase64},
-	"fromBase64": {1, 1, fromBase64},
-}
-
-//XXX this should be renamed so as not to clash with the conventional
-// id "err" used for the local variable
-func err(fn string, err error) (zng.Value, error) {
-	return zng.Value{}, fmt.Errorf("%s: %w", fn, err)
-}
-
-func lenFn(args *Args) (zng.Value, error) {
-	switch zng.AliasedType(args.vals[0].Type).(type) {
-	case *zng.TypeArray, *zng.TypeSet:
-		v := args.vals[0]
-		len, err := v.ContainerLength()
-		if err != nil {
-			return zng.Value{}, err
-		}
-		return zng.Value{zng.TypeInt64, args.Int(int64(len))}, nil
-	default:
-		return err("len", ErrBadArgument)
-	}
-}
-
-func mathAbs(args *Args) (zng.Value, error) {
-	v := args.vals[0]
-	id := v.Type.ID()
-	if zng.IsFloat(id) {
-		f, _ := zng.DecodeFloat64(v.Bytes)
-		f = math.Abs(f)
-		return zng.Value{zng.TypeFloat64, args.Float64(f)}, nil
-	}
-	if !zng.IsInteger(id) {
-		return err("Math.abs", ErrBadArgument)
-	}
-	if !zng.IsSigned(id) {
-		return v, nil
-	}
-	x, _ := zng.DecodeInt(v.Bytes)
-	if x < 0 {
-		x = -x
-	}
-	return zng.Value{v.Type, args.Int(x)}, nil
-}
-
-func mathCeil(args *Args) (zng.Value, error) {
-	v := args.vals[0]
-	id := v.Type.ID()
-	if zng.IsFloat(id) {
-		f, _ := zng.DecodeFloat64(v.Bytes)
-		f = math.Ceil(f)
-		return zng.Value{zng.TypeFloat64, args.Float64(f)}, nil
-	}
-	if zng.IsInteger(id) {
-		return v, nil
-	}
-	return err("Math.Ceil", ErrBadArgument)
-}
-
-func mathFloor(args *Args) (zng.Value, error) {
-	v := args.vals[0]
-	id := v.Type.ID()
-	if zng.IsFloat(id) {
-		f, _ := zng.DecodeFloat64(v.Bytes)
-		f = math.Floor(f)
-		return zng.Value{zng.TypeFloat64, args.Float64(f)}, nil
-	}
-	if zng.IsInteger(id) {
-		return v, nil
-	}
-	return err("Math.Floor", ErrBadArgument)
-}
-
-func mathLog(args *Args) (zng.Value, error) {
-	x, ok := CoerceToFloat(args.vals[0])
-	// XXX should have better error messages
-	if !ok {
-		return err("Math.log", ErrBadArgument)
-	}
-	if x <= 0 {
-		return err("Math.log", ErrBadArgument)
-	}
-	return zng.Value{zng.TypeFloat64, args.Float64(math.Log(x))}, nil
-}
-
-func mathMax(args *Args) (zng.Value, error) {
-	return reduce(args, anymath.Max)
-}
-
-func mathMin(args *Args) (zng.Value, error) {
-	return reduce(args, anymath.Min)
-}
-
-func reduce(args *Args, fn *anymath.Function) (zng.Value, error) {
-	zv := args.vals[0]
-	typ := zv.Type
-	id := typ.ID()
-	if zng.IsFloat(id) {
-		result, _ := zng.DecodeFloat64(zv.Bytes)
-		for _, zv := range args.vals[1:] {
-			v, ok := CoerceToFloat(zv)
-			if !ok {
-				return zng.Value{}, ErrBadArgument
-			}
-			result = fn.Float64(result, v)
-		}
-		return zng.Value{typ, args.Float64(result)}, nil
-	}
-	if !zng.IsNumber(id) {
-		// XXX better message
-		return zng.Value{}, ErrBadArgument
-	}
-	if zng.IsSigned(id) {
-		result, _ := zng.DecodeInt(zv.Bytes)
-		for _, zv := range args.vals[1:] {
-			v, ok := CoerceToInt(zv)
-			if !ok {
-				// XXX better message
-				return zng.Value{}, ErrBadArgument
-			}
-			result = fn.Int64(result, v)
-		}
-		return zng.Value{typ, args.Int(result)}, nil
-	}
-	result, _ := zng.DecodeUint(zv.Bytes)
-	for _, zv := range args.vals[1:] {
-		v, ok := CoerceToUint(zv)
-		if !ok {
-			// XXX better message
-			return zng.Value{}, ErrBadArgument
-		}
-		result = fn.Uint64(result, v)
-	}
-	return zng.Value{typ, args.Uint(result)}, nil
-}
-
-//XXX currently integer mod, but this could also do fmod
-// also why doesn't zql have x%y instead of Math.mod(x,y)?
-func mathMod(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	id := zv.Type.ID()
-	if zng.IsFloat(id) {
-		return err("Math.mod", ErrBadArgument)
-	}
-	y, ok := CoerceToUint(args.vals[1])
-	if !ok {
-		return err("Math.mod", ErrBadArgument)
-	}
-	if !zng.IsNumber(id) {
-		return err("Math.mod", ErrBadArgument)
-	}
-	if zng.IsSigned(id) {
-		x, _ := zng.DecodeInt(zv.Bytes)
-		return zng.Value{zv.Type, args.Int(x % int64(y))}, nil
-	}
-	x, _ := zng.DecodeUint(zv.Bytes)
-	return zng.Value{zv.Type, args.Uint(x % y)}, nil
-}
-
-func mathRound(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	id := zv.Type.ID()
-	if zng.IsFloat(id) {
-		f, _ := zng.DecodeFloat64(zv.Bytes)
-		return zng.Value{zv.Type, args.Float64(math.Round(f))}, nil
-
-	}
-	if !zng.IsNumber(id) {
-		return err("Math.round", ErrBadArgument)
-	}
-	return zv, nil
-}
-
-func mathPow(args *Args) (zng.Value, error) {
-	x, ok := CoerceToFloat(args.vals[0])
-	if !ok {
-		return err("Math.pow", ErrBadArgument)
-	}
-	y, ok := CoerceToFloat(args.vals[1])
-	if !ok {
-		return err("Math.pow", ErrBadArgument)
-	}
-	r := math.Pow(x, y)
-	if math.IsNaN(r) {
-		return err("Math.pow", ErrBadArgument)
-	}
-	return zng.Value{zng.TypeFloat64, args.Float64(r)}, nil
-}
-
-func mathSqrt(args *Args) (zng.Value, error) {
-	x, ok := CoerceToFloat(args.vals[0])
-	if !ok {
-		return err("Math.sqrt", ErrBadArgument)
-	}
-	x = math.Sqrt(x)
-	if math.IsNaN(x) {
-		// For now we can't represent non-numeric values in a float64,
-		// we will revisit this but it has implications for file
-		// formats, zql, etc.
-		return err("Math.sqrt", ErrBadArgument)
-	}
-	return zng.Value{zng.TypeFloat64, args.Float64(x)}, nil
-}
-
-// XXX we should just have a len function that applies to different types
-// and a way to get unicode char len, charlen()?
-func stringByteLen(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !zng.IsStringy(zv.Type.ID()) {
-		return err("Strings.byteLen", ErrBadArgument)
-	}
-	v := len(string(zv.Bytes))
-	return zng.Value{zng.TypeInt64, args.Int(int64(v))}, nil
-}
-
-func stringFormatFloat(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if zv.Type.ID() != zng.IdFloat64 {
-		return err("string.floatToString", ErrBadArgument)
-	}
-	f, _ := zng.DecodeFloat64(zv.Bytes)
-	s := strconv.FormatFloat(f, 'g', -1, 64)
-	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
-}
-
-func stringFormatInt(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	id := zv.Type.ID()
-	var s string
-	if !zng.IsInteger(id) {
-		return err("string.intToString", ErrBadArgument)
-	}
-	if zng.IsSigned(id) {
-		v, _ := zng.DecodeInt(zv.Bytes)
-		// XXX GC
-		s = strconv.FormatInt(v, 10)
-	} else {
-		v, _ := zng.DecodeUint(zv.Bytes)
-		// XXX GC
-		s = strconv.FormatUint(v, 10)
-	}
-	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
-}
-
-func stringFormatIp(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if zv.Type.ID() != zng.IdIP {
-		return err("string.ipToString", ErrBadArgument)
-	}
-	ip, _ := zng.DecodeIP(zv.Bytes)
-	return zng.Value{zng.TypeString, zng.EncodeString(ip.String())}, nil
-}
-
-func stringParseInt(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !zng.IsStringy(zv.Type.ID()) {
-		return err("String.parseInt", ErrBadArgument)
-	}
-	v, e := zng.DecodeString(zv.Bytes)
-	if e != nil {
-		return zng.Value{}, e
-	}
-	i, perr := strconv.ParseInt(v, 10, 64)
-	if perr != nil {
-		// Get rid of the strconv wrapping gunk to get the
-		// actual error message
-		e := perr.(*strconv.NumError)
-		return err("String.parseInt", e.Err)
-	}
-	return zng.Value{zng.TypeInt64, args.Int(i)}, nil
-}
-
-func stringParseFloat(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !zng.IsStringy(zv.Type.ID()) {
-		return err("String.parseFloat", ErrBadArgument)
-	}
-	v, perr := zng.DecodeString(zv.Bytes)
-	if perr != nil {
-		return zng.Value{}, perr
-	}
-	f, perr := strconv.ParseFloat(v, 64)
-	if perr != nil {
-		// Get rid of the strconv wrapping gunk to get the
-		// actual error message
-		e := perr.(*strconv.NumError)
-		return err("String.parseFloat", e.Err)
-	}
-	return zng.Value{zng.TypeFloat64, args.Float64(f)}, nil
-}
-
-func stringParseIp(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !zng.IsStringy(zv.Type.ID()) {
-		return err("String.parseIp", ErrBadArgument)
-	}
-	v, perr := zng.DecodeString(zv.Bytes)
-	if perr != nil {
-		return zng.Value{}, perr
-	}
-	// XXX GC
-	a := net.ParseIP(v)
-	if a == nil {
-		return err("String.parseIp", ErrBadArgument)
-	}
-	// XXX GC
-	return zng.Value{zng.TypeIP, zng.EncodeIP(a)}, nil
-}
-
-func isStringy(v zng.Value) bool {
-	return zng.IsStringy(v.Type.ID())
-}
-
-func stringReplace(args *Args) (zng.Value, error) {
-	zvs := args.vals[0]
-	zvold := args.vals[1]
-	zvnew := args.vals[2]
-	if !isStringy(zvs) || !isStringy(zvold) || !isStringy(zvnew) {
-		return err("String.replace", ErrBadArgument)
-	}
-	s, err := zng.DecodeString(zvs.Bytes)
-	if err != nil {
-		return zng.Value{}, err
-	}
-	old, err := zng.DecodeString(zvold.Bytes)
-	if err != nil {
-		return zng.Value{}, err
-	}
-	new, err := zng.DecodeString(zvnew.Bytes)
-	if err != nil {
-		return zng.Value{}, err
-	}
-	result := strings.ReplaceAll(s, old, new)
-	return zng.Value{zng.TypeString, zng.EncodeString(result)}, nil
-}
-
-func stringRuneLen(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !isStringy(zv) {
-		return err("Strings.byteLen", ErrBadArgument)
-	}
-	s, err := zng.DecodeString(zv.Bytes)
-	if err != nil {
-		return zng.Value{}, err
-	}
-	v := utf8.RuneCountInString(s)
-	return zng.Value{zng.TypeInt64, args.Int(int64(v))}, nil
-}
-
-func stringToLower(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !isStringy(zv) {
-		return err("String.toLower", ErrBadArgument)
-	}
-	s, err := zng.DecodeString(zv.Bytes)
-	if err != nil {
-		return zng.Value{}, err
-	}
-	// XXX GC
-	s = strings.ToLower(s)
-	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
-}
-
-func stringToUpper(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !isStringy(zv) {
-		return err("String.toUpper", ErrBadArgument)
-	}
-	s, err := zng.DecodeString(zv.Bytes)
-	if err != nil {
-		return zng.Value{}, err
-	}
-	// XXX GC
-	s = strings.ToUpper(s)
-	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
-}
-
-func stringTrim(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !isStringy(zv) {
-		return err("String.trim", ErrBadArgument)
-	}
-	// XXX GC
-	s := strings.TrimSpace(string(zv.Bytes))
-	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
-}
-
-func timeFromISO(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !isStringy(zv) {
-		return err("Time.fromISO", ErrBadArgument)
-	}
-	ts, e := time.Parse(time.RFC3339Nano, string(zv.Bytes))
-	if e != nil {
-		return err("Time.fromISO", ErrBadArgument)
-	}
-	return zng.Value{zng.TypeTime, args.Time(nano.Ts(ts.UnixNano()))}, nil
-}
-
-func timeFromMsec(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	ms, ok := CoerceToInt(zv)
-	if !ok {
-		return err("Time.fromMilliseconds", ErrBadArgument)
-	}
-	return zng.Value{zng.TypeTime, args.Time(nano.Ts(ms * 1_000_000))}, nil
-}
-
-func timeFromUsec(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	us, ok := CoerceToInt(zv)
-	if !ok {
-		return err("Time.fromMicroseconds", ErrBadArgument)
-	}
-	return zng.Value{zng.TypeTime, args.Time(nano.Ts(us * 1000))}, nil
-}
-
-func timeFromNsec(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	ns, ok := CoerceToInt(zv)
-	if !ok {
-		return err("Time.fromNanoseconds", ErrBadArgument)
-	}
-	return zng.Value{zng.TypeTime, args.Time(nano.Ts(ns))}, nil
-}
-
-func timeTrunc(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	ts, ok := CoerceToTime(zv)
-	if !ok {
-		return err("Time.trunc", ErrBadArgument)
-	}
-	dur, ok := CoerceToInt(args.vals[1])
-	if !ok {
-		return err("Time.trunc", ErrBadArgument)
-	}
-	dur *= 1_000_000_000
-	return zng.Value{zng.TypeTime, args.Time(nano.Ts(ts.Trunc(dur)))}, nil
-}
-
-func typeOf(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	return zng.Value{zng.TypeType, zng.EncodeType(zv.Type.String())}, nil
-}
-
-func isErr(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if zv.Type == zng.TypeError {
-		return zng.True, nil
-	}
-	return zng.False, nil
-}
-
-func fromBase64(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !isStringy(zv) {
-		return err("fromBase64", ErrBadArgument)
-
-	}
-	s, _ := zng.DecodeString(zv.Bytes)
-	b, err := base64.StdEncoding.DecodeString(s)
-	if err != nil {
-		return zng.Value{zng.TypeError, zng.EncodeString(err.Error())}, nil
-	}
-	return zng.Value{zng.TypeBytes, zng.EncodeBytes(b)}, nil
-}
-
-func toBase64(args *Args) (zng.Value, error) {
-	zv := args.vals[0]
-	if !isStringy(zv) {
-		return err("fromBase64", ErrBadArgument)
-
-	}
-	s := base64.StdEncoding.EncodeToString(zv.Bytes)
-	return zng.Value{zng.TypeString, zng.EncodeString(s)}, nil
-}
diff --git a/expr/result.go b/expr/result.go
deleted file mode 100644
index 15a07c161..000000000
--- a/expr/result.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package expr
-
-import (
-	"github.com/brimsec/zq/pkg/nano"
-	"github.com/brimsec/zq/zcode"
-	"github.com/brimsec/zq/zng"
-)
-
-type result struct {
-	buf zcode.Bytes
-}
-
-func (r *result) Int(v int64) zcode.Bytes {
-	r.buf = zng.AppendInt(r.buf[:0], v)
-	return r.buf
-}
-
-func (r *result) Uint(v uint64) zcode.Bytes {
-	r.buf = zng.AppendUint(r.buf[:0], v)
-	return r.buf
-}
-
-func (r *result) Float64(v float64) zcode.Bytes {
-	r.buf = zng.AppendFloat64(r.buf[:0], v)
-	return r.buf
-}
-
-func (r *result) Time(v nano.Ts) zcode.Bytes {
-	r.buf = zng.AppendTime(r.buf[:0], v)
-	return r.buf
-}
diff --git a/expr/result/buffer.go b/expr/result/buffer.go
new file mode 100644
index 000000000..89728f5cc
--- /dev/null
+++ b/expr/result/buffer.go
@@ -0,0 +1,29 @@
+package result
+
+import (
+	"github.com/brimsec/zq/pkg/nano"
+	"github.com/brimsec/zq/zcode"
+	"github.com/brimsec/zq/zng"
+)
+
+type Buffer zcode.Bytes
+
+func (b *Buffer) Int(v int64) zcode.Bytes {
+	*b = Buffer(zng.AppendInt(zcode.Bytes((*b)[:0]), v))
+	return zcode.Bytes(*b)
+}
+
+func (b *Buffer) Uint(v uint64) zcode.Bytes {
+	*b = Buffer(zng.AppendUint(zcode.Bytes((*b)[:0]), v))
+	return zcode.Bytes(*b)
+}
+
+func (b *Buffer) Float64(v float64) zcode.Bytes {
+	*b = Buffer(zng.AppendFloat64(zcode.Bytes((*b)[:0]), v))
+	return zcode.Bytes(*b)
+}
+
+func (b *Buffer) Time(v nano.Ts) zcode.Bytes {
+	*b = Buffer(zng.AppendTime(zcode.Bytes((*b)[:0]), v))
+	return zcode.Bytes(*b)
+}
diff --git a/filter/filter.go b/filter/filter.go
index ff4fd37fd..a340e65e8 100644
--- a/filter/filter.go
+++ b/filter/filter.go
@@ -10,6 +10,7 @@ import (
 	"github.com/brimsec/zq/pkg/byteconv"
 	"github.com/brimsec/zq/zcode"
 	"github.com/brimsec/zq/zng"
+	"github.com/brimsec/zq/zng/resolver"
 )
 
 type Filter func(*zng.Record) bool
@@ -37,7 +38,7 @@ func combine(res expr.Evaluator, pred Predicate) Filter {
 	}
 }
 
-func CompileFieldCompare(node *ast.CompareField) (Filter, error) {
+func CompileFieldCompare(zctx *resolver.Context, node *ast.CompareField) (Filter, error) {
 	literal := node.Value
 	// Treat len(field) specially since we're looking at a computed
 	// value rather than a field from a record.
@@ -51,7 +52,7 @@ func CompileFieldCompare(node *ast.CompareField) (Filter, error) {
 	if err != nil {
 		return nil, err
 	}
-	resolver, err := expr.CompileExpr(node.Field)
+	resolver, err := expr.CompileExpr(zctx, node.Field)
 	if err != nil {
 		return nil, err
 	}
@@ -203,32 +204,32 @@ func searchRecordString(term string) Filter {
 	}
 }
 
-func Compile(node ast.BooleanExpr) (Filter, error) {
+func Compile(zctx *resolver.Context, node ast.BooleanExpr) (Filter, error) {
 	switch v := node.(type) {
 	case *ast.LogicalNot:
-		expr, err := Compile(v.Expr)
+		expr, err := Compile(zctx, v.Expr)
 		if err != nil {
 			return nil, err
 		}
 		return LogicalNot(expr), nil
 
 	case *ast.LogicalAnd:
-		left, err := Compile(v.Left)
+		left, err := Compile(zctx, v.Left)
 		if err != nil {
 			return nil, err
 		}
-		right, err := Compile(v.Right)
+		right, err := Compile(zctx, v.Right)
 		if err != nil {
 			return nil, err
 		}
 		return LogicalAnd(left, right), nil
 
 	case *ast.LogicalOr:
-		left, err := Compile(v.Left)
+		left, err := Compile(zctx, v.Left)
 		if err != nil {
 			return nil, err
 		}
-		right, err := Compile(v.Right)
+		right, err := Compile(zctx, v.Right)
 		if err != nil {
 			return nil, err
 		}
@@ -242,7 +243,7 @@ func Compile(node ast.BooleanExpr) (Filter, error) {
 
 	case *ast.CompareField:
 		if v.Comparator == "in" {
-			resolver, err := expr.CompileExpr(v.Field)
+			resolver, err := expr.CompileExpr(zctx, v.Field)
 			if err != nil {
 				return nil, err
 			}
@@ -251,10 +252,10 @@ func Compile(node ast.BooleanExpr) (Filter, error) {
 			return combine(resolver, comparison), nil
 		}
 
-		return CompileFieldCompare(v)
+		return CompileFieldCompare(zctx, v)
 
 	case *ast.BinaryExpression:
-		predicate, err := expr.CompileExpr(v)
+		predicate, err := expr.CompileExpr(zctx, v)
 		if err != nil {
 			return nil, err
 		}
diff --git a/ppl/cmd/zar/import/command.go b/ppl/cmd/zar/import/command.go
index b84717a1f..8887268c2 100644
--- a/ppl/cmd/zar/import/command.go
+++ b/ppl/cmd/zar/import/command.go
@@ -12,14 +12,14 @@ import (
 	"github.com/brimsec/zq/pkg/units"
 	"github.com/brimsec/zq/ppl/archive"
 	"github.com/brimsec/zq/ppl/cmd/zar/root"
-	"github.com/brimsec/zq/zio/detector"
+	"github.com/brimsec/zq/zbuf"
 	"github.com/brimsec/zq/zng/resolver"
 	"github.com/mccanne/charm"
 )
 
 var Import = &charm.Spec{
 	Name:  "import",
-	Usage: "import [-R root] [options] [file|S3-object|-]",
+	Usage: "import [-R root] [options] [file|S3-object|- ...]",
 	Short: "import log files into pieces",
 	Long: `
 The import command provides a way to create a new zar archive with ZNG data
@@ -73,8 +73,8 @@ func (c *Command) Run(args []string) error {
 	}
 	if c.empty && len(args) > 0 {
 		return errors.New("zar import: empty flag specified with input files")
-	} else if !c.empty && len(args) != 1 {
-		return errors.New("zar import: exactly one input file must be specified (- for stdin)")
+	} else if !c.empty && len(args) == 0 {
+		return errors.New("zar import: at least one input file must be specified (- for stdin)")
 	}
 	archive.ImportBufSize = int64(c.importBufSize)
 	archive.ImportStreamRecordsMax = c.importStreamRecordMax
@@ -99,15 +99,18 @@ func (c *Command) Run(args []string) error {
 		return nil
 	}
 
-	path := args[0]
-	if path == "-" {
-		path = detector.StdinPath
-	}
+	paths := args
 	zctx := resolver.NewContext()
-	reader, err := detector.OpenFile(zctx, path, c.inputFlags.Options())
+	readers, err := c.inputFlags.Open(zctx, paths, false)
 	if err != nil {
 		return err
 	}
+	var reader zbuf.ReadCloser
+	if ark.DataOrder == zbuf.OrderAsc {
+		reader = zbuf.NewCombiner(readers, zbuf.CmpTimeForward)
+	} else {
+		reader = zbuf.NewCombiner(readers, zbuf.CmpTimeReverse)
+	}
 	defer reader.Close()
 
 	ctx, cancel := signalctx.New(os.Interrupt)
diff --git a/ppl/cmd/zqd/listen/command.go b/ppl/cmd/zqd/listen/command.go
index 7fbb88210..96644a20d 100644
--- a/ppl/cmd/zqd/listen/command.go
+++ b/ppl/cmd/zqd/listen/command.go
@@ -156,10 +156,23 @@ func (c *Command) init() error {
 	if err := c.initWorkers(); err != nil {
 		return err
 	}
-	if err := c.initZeek(); err != nil {
+	var err error
+	c.conf.Suricata, err = getLauncher(c.suricataRunnerPath, "suricatarunner")
+	if err != nil {
 		return err
 	}
-	return c.initSuricata()
+	c.conf.Zeek, err = getLauncher(c.zeekRunnerPath, "zeekrunner")
+	return err
+}
+
+func getLauncher(path, defaultFile string) (pcapanalyzer.Launcher, error) {
+	if path == "" {
+		var err error
+		if path, err = exec.LookPath(defaultFile); err != nil {
+			return nil, nil
+		}
+	}
+	return pcapanalyzer.LauncherFromPath(path)
 }
 
 func (c *Command) watchBrimFd(ctx context.Context) (context.Context, error) {
@@ -235,36 +248,6 @@ func (c *Command) loadConfigFile() error {
 	return err
 }
 
-func (c *Command) initZeek() error {
-	if c.zeekRunnerPath == "" {
-		var err error
-		if c.zeekRunnerPath, err = exec.LookPath("zeekrunner"); err != nil {
-			return nil
-		}
-	}
-	ln, err := pcapanalyzer.LauncherFromPath(c.zeekRunnerPath)
-	if err != nil {
-		return err
-	}
-	c.conf.Zeek = ln
-	return nil
-}
-
-func (c *Command) initSuricata() error {
-	if c.suricataRunnerPath == "" {
-		var err error
-		if c.suricataRunnerPath, err = exec.LookPath("suricatarunner"); err != nil {
-			return nil
-		}
-	}
-	ln, err := pcapanalyzer.LauncherFromPath(c.suricataRunnerPath)
-	if err != nil {
-		return err
-	}
-	c.conf.Suricata = ln
-	return nil
-}
-
 func (c *Command) initWorkers() error {
 	// This is for local testing only, at this point.
 	// Workers will be available through a K8s service in a prod deployment.
diff --git a/proc/compiler/compiler.go b/proc/compiler/compiler.go
index c3176f585..244dc7336 100644
--- a/proc/compiler/compiler.go
+++ b/proc/compiler/compiler.go
@@ -86,14 +86,14 @@ func compileProc(custom Hook, node ast.Proc, pctx *proc.Context, parent proc.Int
 		return pass.New(parent), nil
 
 	case *ast.FilterProc:
-		f, err := filter.Compile(v.Filter)
+		f, err := filter.Compile(pctx.TypeContext, v.Filter)
 		if err != nil {
 			return nil, fmt.Errorf("compiling filter: %w", err)
 		}
 		return filterproc.New(parent, f), nil
 
 	case *ast.TopProc:
-		fields, err := expr.CompileExprs(v.Fields)
+		fields, err := expr.CompileExprs(pctx.TypeContext, v.Fields)
 		if err != nil {
 			return nil, fmt.Errorf("compiling top: %w", err)
 		}
diff --git a/proc/cut/cut.go b/proc/cut/cut.go
index 5d0c9c382..45e5ea3ba 100644
--- a/proc/cut/cut.go
+++ b/proc/cut/cut.go
@@ -25,7 +25,7 @@ func New(pctx *proc.Context, parent proc.Interface, node *ast.CutProc) (*Proc, e
 	var lhs []field.Static
 	var rhs []expr.Evaluator
 	for _, f := range node.Fields {
-		field, expression, err := expr.CompileAssignment(&f)
+		field, expression, err := expr.CompileAssignment(pctx.TypeContext, &f)
 		if err != nil {
 			return nil, err
 		}
diff --git a/proc/groupby/compile.go b/proc/groupby/compile.go
index ba6e7347d..a60af2e3d 100644
--- a/proc/groupby/compile.go
+++ b/proc/groupby/compile.go
@@ -16,7 +16,7 @@ func compileParams(node *ast.GroupByProc, zctx *resolver.Context) (*Params, erro
 	keys := []Key{}
 	var targets []field.Static
 	for k, key := range node.Keys {
-		name, rhs, err := expr.CompileAssignment(&key)
+		name, rhs, err := expr.CompileAssignment(zctx, &key)
 		if err != nil {
 			return nil, err
 		}
@@ -29,7 +29,7 @@ func compileParams(node *ast.GroupByProc, zctx *resolver.Context) (*Params, erro
 	}
 	reducerMakers := make([]reducerMaker, 0, len(node.Reducers))
 	for _, assignment := range node.Reducers {
-		name, f, err := CompileReducer(assignment)
+		name, f, err := CompileReducer(zctx, assignment)
 		if err != nil {
 			return nil, err
 		}
@@ -53,7 +53,7 @@ func compileParams(node *ast.GroupByProc, zctx *resolver.Context) (*Params, erro
 	}, nil
 }
 
-func CompileReducer(assignment ast.Assignment) (field.Static, reducer.Maker, error) {
+func CompileReducer(zctx *resolver.Context, assignment ast.Assignment) (field.Static, reducer.Maker, error) {
 	reducerAST, ok := assignment.RHS.(*ast.Reducer)
 	if !ok {
 		return nil, nil, errors.New("reducer is not a reducer expression")
@@ -62,7 +62,7 @@ func CompileReducer(assignment ast.Assignment) (field.Static, reducer.Maker, err
 	var err error
 	var arg expr.Evaluator
 	if reducerAST.Expr != nil {
-		arg, err = expr.CompileExpr(reducerAST.Expr)
+		arg, err = expr.CompileExpr(zctx, reducerAST.Expr)
 		if err != nil {
 			return nil, nil, err
 		}
@@ -81,7 +81,7 @@ func CompileReducer(assignment ast.Assignment) (field.Static, reducer.Maker, err
 	}
 	var where expr.Evaluator
 	if reducerAST.Where != nil {
-		where, err = expr.CompileExpr(reducerAST.Where)
+		where, err = expr.CompileExpr(zctx, reducerAST.Where)
 		if err != nil {
 			return nil, nil, err
 		}
diff --git a/proc/groupby/groupby.go b/proc/groupby/groupby.go
index 74a833b60..491888130 100644
--- a/proc/groupby/groupby.go
+++ b/proc/groupby/groupby.go
@@ -183,8 +183,9 @@ func decomposable(rs []reducerMaker) bool {
 }
 
 func IsDecomposable(assignments []ast.Assignment) bool {
+	zctx := resolver.NewContext()
 	for _, assignment := range assignments {
-		_, create, err := CompileReducer(assignment)
+		_, create, err := CompileReducer(zctx, assignment)
 		if err != nil {
 			return false
 		}
diff --git a/proc/put/put.go b/proc/put/put.go
index 3f6ac827c..350673d69 100644
--- a/proc/put/put.go
+++ b/proc/put/put.go
@@ -62,7 +62,7 @@ func New(pctx *proc.Context, parent proc.Interface, node *ast.PutProc) (proc.Int
 	clauses := make([]clause, len(node.Clauses))
 	for k, cl := range node.Clauses {
 		var err error
-		clauses[k].field, clauses[k].eval, err = expr.CompileAssignment(&cl)
+		clauses[k].field, clauses[k].eval, err = expr.CompileAssignment(pctx.TypeContext, &cl)
 		if err != nil {
 			return nil, err
 		}
diff --git a/proc/sort/sort.go b/proc/sort/sort.go
index e643a731e..4e889402c 100644
--- a/proc/sort/sort.go
+++ b/proc/sort/sort.go
@@ -32,7 +32,7 @@ type Proc struct {
 }
 
 func New(pctx *proc.Context, parent proc.Interface, node *ast.SortProc) (*Proc, error) {
-	fields, err := expr.CompileExprs(node.Fields)
+	fields, err := expr.CompileExprs(pctx.TypeContext, node.Fields)
 	if err != nil {
 		return nil, err
 	}
diff --git a/reducer/avg.go b/reducer/avg.go
index 3ff611755..8085f134e 100644
--- a/reducer/avg.go
+++ b/reducer/avg.go
@@ -2,6 +2,7 @@ package reducer
 
 import (
 	"github.com/brimsec/zq/expr"
+	"github.com/brimsec/zq/expr/coerce"
 	"github.com/brimsec/zq/zcode"
 	"github.com/brimsec/zq/zng"
 	"github.com/brimsec/zq/zng/resolver"
@@ -26,7 +27,7 @@ func (a *Avg) Consume(r *zng.Record) {
 	if v.Bytes == nil {
 		return
 	}
-	d, ok := expr.CoerceToFloat(v)
+	d, ok := coerce.ToFloat(v)
 	if !ok {
 		a.TypeMismatch++
 		return
diff --git a/reducer/math.go b/reducer/math.go
index 37581e396..22414d078 100644
--- a/reducer/math.go
+++ b/reducer/math.go
@@ -3,6 +3,7 @@ package reducer
 import (
 	"github.com/brimsec/zq/anymath"
 	"github.com/brimsec/zq/expr"
+	"github.com/brimsec/zq/expr/coerce"
 	"github.com/brimsec/zq/pkg/nano"
 	"github.com/brimsec/zq/zng"
 	"github.com/brimsec/zq/zng/resolver"
@@ -109,7 +110,7 @@ func (f *Float64) result() zng.Value {
 }
 
 func (f *Float64) consume(v zng.Value) error {
-	if v, ok := expr.CoerceToFloat(v); ok {
+	if v, ok := coerce.ToFloat(v); ok {
 		f.state = f.function(f.state, v)
 		return nil
 	}
@@ -133,7 +134,7 @@ func (i *Int64) result() zng.Value {
 }
 
 func (i *Int64) consume(v zng.Value) error {
-	if v, ok := expr.CoerceToInt(v); ok {
+	if v, ok := coerce.ToInt(v); ok {
 		i.state = i.function(i.state, v)
 		return nil
 	}
@@ -157,7 +158,7 @@ func (u *Uint64) result() zng.Value {
 }
 
 func (u *Uint64) consume(v zng.Value) error {
-	if v, ok := expr.CoerceToUint(v); ok {
+	if v, ok := coerce.ToUint(v); ok {
 		u.state = u.function(u.state, v)
 		return nil
 	}
@@ -181,7 +182,7 @@ func (d *Duration) result() zng.Value {
 }
 
 func (d *Duration) consume(v zng.Value) error {
-	if v, ok := expr.CoerceToDuration(v); ok {
+	if v, ok := coerce.ToDuration(v); ok {
 		d.state = d.function(d.state, v)
 		return nil
 	}
@@ -205,7 +206,7 @@ func (t *Time) result() zng.Value {
 }
 
 func (t *Time) consume(v zng.Value) error {
-	if v, ok := expr.CoerceToTime(v); ok {
+	if v, ok := coerce.ToTime(v); ok {
 		t.state = nano.Ts(t.function(int64(t.state), int64(v)))
 		return nil
 	}
diff --git a/zng/error.go b/zng/error.go
index 9162015e6..625c2a00f 100644
--- a/zng/error.go
+++ b/zng/error.go
@@ -2,12 +2,18 @@ package zng
 
 import (
 	"errors"
+	"fmt"
 
 	"github.com/brimsec/zq/zcode"
 )
 
 type TypeOfError struct{}
 
+func NewErrorf(format string, args ...interface{}) Value {
+	msg := fmt.Sprintf(format, args...)
+	return Value{TypeError, zcode.Bytes(msg)}
+}
+
 func NewError(err error) Value {
 	return Value{TypeError, zcode.Bytes(err.Error())}
 }
diff --git a/zng/value.go b/zng/value.go
index 961798afd..f962d5729 100644
--- a/zng/value.go
+++ b/zng/value.go
@@ -181,3 +181,11 @@ func (v Value) Copy() Value {
 	}
 	return Value{v.Type, b}
 }
+
+func (v Value) IsStringy() bool {
+	return IsStringy(v.Type.ID())
+}
+
+func (v Value) IsError() bool {
+	return v.Type == TypeError
+}


Test patch:
diff --git a/expr/expr_test.go b/expr/expr_test.go
index 21a84e5be..c9b62295f 100644
--- a/expr/expr_test.go
+++ b/expr/expr_test.go
@@ -53,7 +53,7 @@ func compileExpr(s string) (expr.Evaluator, error) {
 		return nil, errors.New("expected Expression")
 	}
 
-	return expr.CompileExpr(node)
+	return expr.CompileExpr(resolver.NewContext(), node)
 }
 
 // Compile and evaluate a zql expression against a provided Record.
diff --git a/expr/functions_test.go b/expr/functions_test.go
index 50a7be9a0..d295a32a8 100644
--- a/expr/functions_test.go
+++ b/expr/functions_test.go
@@ -6,7 +6,7 @@ import (
 	"strconv"
 	"testing"
 
-	"github.com/brimsec/zq/expr"
+	"github.com/brimsec/zq/expr/function"
 	"github.com/brimsec/zq/pkg/nano"
 	"github.com/brimsec/zq/zng"
 	"github.com/stretchr/testify/require"
@@ -18,7 +18,7 @@ func zaddr(addr string) zng.Value {
 }
 
 func TestBadFunction(t *testing.T) {
-	testError(t, "notafunction()", nil, expr.ErrNoSuchFunction, "calling nonexistent function")
+	testError(t, "notafunction()", nil, function.ErrNoSuchFunction, "calling nonexistent function")
 }
 
 func TestAbs(t *testing.T) {
@@ -33,9 +33,9 @@ func TestAbs(t *testing.T) {
 	testSuccessful(t, "Math.abs(3.2)", record, zfloat64(3.2))
 	testSuccessful(t, "Math.abs(u)", record, zuint64(50))
 
-	testError(t, "Math.abs()", record, expr.ErrTooFewArgs, "abs with no args")
-	testError(t, "Math.abs(1, 2)", record, expr.ErrTooManyArgs, "abs with too many args")
-	testError(t, `Math.abs("hello")`, record, expr.ErrBadArgument, "abs with non-number")
+	testError(t, "Math.abs()", record, function.ErrTooFewArgs, "abs with no args")
+	testError(t, "Math.abs(1, 2)", record, function.ErrTooManyArgs, "abs with too many args")
+	testError(t, `Math.abs("hello")`, record, function.ErrBadArgument, "abs with non-number")
 }
 
 func TestSqrt(t *testing.T) {
@@ -48,9 +48,9 @@ func TestSqrt(t *testing.T) {
 	testSuccessful(t, "Math.sqrt(f)", record, zfloat64(2.5))
 	testSuccessful(t, "Math.sqrt(i)", record, zfloat64(3.0))
 
-	testError(t, "Math.sqrt()", record, expr.ErrTooFewArgs, "sqrt with no args")
-	testError(t, "Math.sqrt(1, 2)", record, expr.ErrTooManyArgs, "sqrt with too many args")
-	testError(t, "Math.sqrt(-1)", record, expr.ErrBadArgument, "sqrt of negative")
+	testError(t, "Math.sqrt()", record, function.ErrTooFewArgs, "sqrt with no args")
+	testError(t, "Math.sqrt(1, 2)", record, function.ErrTooManyArgs, "sqrt with too many args")
+	testError(t, "Math.sqrt(-1)", record, function.ErrBadArgument, "sqrt of negative")
 }
 
 func TestMinMax(t *testing.T) {
@@ -68,8 +68,8 @@ func TestMinMax(t *testing.T) {
 	testSuccessful(t, "Math.max(3, 2, 1)", record, zint64(3))
 
 	// Fails with no arguments
-	testError(t, "Math.min()", record, expr.ErrTooFewArgs, "min with no args")
-	testError(t, "Math.max()", record, expr.ErrTooFewArgs, "max with no args")
+	testError(t, "Math.min()", record, function.ErrTooFewArgs, "min with no args")
+	testError(t, "Math.max()", record, function.ErrTooFewArgs, "max with no args")
 
 	// Mixed types work
 	testSuccessful(t, "Math.min(i, 2, 3)", record, zuint64(1))
@@ -82,10 +82,10 @@ func TestMinMax(t *testing.T) {
 	testSuccessful(t, "Math.max(2.0, -1)", record, zfloat64(2))
 
 	// Fails on invalid types
-	testError(t, `Math.min("hello", 2)`, record, expr.ErrBadArgument, "min() on string")
-	testError(t, `Math.max("hello", 2)`, record, expr.ErrBadArgument, "max() on string")
-	testError(t, `Math.min(1.2.3.4, 2)`, record, expr.ErrBadArgument, "min() on ip")
-	testError(t, `Math.max(1.2.3.4, 2)`, record, expr.ErrBadArgument, "max() on ip")
+	testError(t, `Math.min("hello", 2)`, record, function.ErrBadArgument, "min() on string")
+	testError(t, `Math.max("hello", 2)`, record, function.ErrBadArgument, "max() on string")
+	testError(t, `Math.min(1.2.3.4, 2)`, record, function.ErrBadArgument, "min() on ip")
+	testError(t, `Math.max(1.2.3.4, 2)`, record, function.ErrBadArgument, "max() on ip")
 
 }
 
@@ -98,12 +98,12 @@ func TestCeilFloorRound(t *testing.T) {
 	testSuccessful(t, "Math.floor(5)", nil, zint64(5))
 	testSuccessful(t, "Math.round(5)", nil, zint64(5))
 
-	testError(t, "Math.ceil()", nil, expr.ErrTooFewArgs, "ceil() with no args")
-	testError(t, "Math.ceil(1, 2)", nil, expr.ErrTooManyArgs, "ceil() with too many args")
-	testError(t, "Math.floor()", nil, expr.ErrTooFewArgs, "floor() with no args")
-	testError(t, "Math.floor(1, 2)", nil, expr.ErrTooManyArgs, "floor() with too many args")
-	testError(t, "Math.round()", nil, expr.ErrTooFewArgs, "round() with no args")
-	testError(t, "Math.round(1, 2)", nil, expr.ErrTooManyArgs, "round() with too many args")
+	testError(t, "Math.ceil()", nil, function.ErrTooFewArgs, "ceil() with no args")
+	testError(t, "Math.ceil(1, 2)", nil, function.ErrTooManyArgs, "ceil() with too many args")
+	testError(t, "Math.floor()", nil, function.ErrTooFewArgs, "floor() with no args")
+	testError(t, "Math.floor(1, 2)", nil, function.ErrTooManyArgs, "floor() with too many args")
+	testError(t, "Math.round()", nil, function.ErrTooFewArgs, "round() with no args")
+	testError(t, "Math.round(1, 2)", nil, function.ErrTooManyArgs, "round() with too many args")
 }
 
 func TestLogPow(t *testing.T) {
@@ -116,14 +116,14 @@ func TestLogPow(t *testing.T) {
 	testSuccessful(t, "Math.pow(10, 2)", nil, zfloat64(100))
 	testSuccessful(t, "Math.pow(4.0, 1.5)", nil, zfloat64(8))
 
-	testError(t, "Math.log()", nil, expr.ErrTooFewArgs, "log() with no args")
-	testError(t, "Math.log(2, 3)", nil, expr.ErrTooManyArgs, "log() with too many args")
-	testError(t, "Math.log(0)", nil, expr.ErrBadArgument, "log() of 0")
-	testError(t, "Math.log(-1)", nil, expr.ErrBadArgument, "log() of negative number")
+	testError(t, "Math.log()", nil, function.ErrTooFewArgs, "log() with no args")
+	testError(t, "Math.log(2, 3)", nil, function.ErrTooManyArgs, "log() with too many args")
+	testError(t, "Math.log(0)", nil, function.ErrBadArgument, "log() of 0")
+	testError(t, "Math.log(-1)", nil, function.ErrBadArgument, "log() of negative number")
 
-	testError(t, "Math.pow()", nil, expr.ErrTooFewArgs, "pow() with no args")
-	testError(t, "Math.pow(2, 3, r)", nil, expr.ErrTooManyArgs, "pow() with too many args")
-	testError(t, "Math.pow(-1, 0.5)", nil, expr.ErrBadArgument, "pow() with invalid arguments")
+	testError(t, "Math.pow()", nil, function.ErrTooFewArgs, "pow() with no args")
+	testError(t, "Math.pow(2, 3, r)", nil, function.ErrTooManyArgs, "pow() with too many args")
+	testError(t, "Math.pow(-1, 0.5)", nil, function.ErrBadArgument, "pow() with invalid arguments")
 }
 
 func TestMod(t *testing.T) {
@@ -137,63 +137,63 @@ func TestMod(t *testing.T) {
 	testSuccessful(t, "Math.mod(8, 5)", record, zint64(3))
 	testSuccessful(t, "Math.mod(8, u)", record, zint64(3))
 
-	testError(t, "Math.mod()", record, expr.ErrTooFewArgs, "mod() with no args")
-	testError(t, "Math.mod(1, 2, 3)", record, expr.ErrTooManyArgs, "mod() with too many args")
-	testError(t, "Math.mod(3.2, 2)", record, expr.ErrBadArgument, "mod() with float64 arg")
+	testError(t, "Math.mod()", record, function.ErrTooFewArgs, "mod() with no args")
+	testError(t, "Math.mod(1, 2, 3)", record, function.ErrTooManyArgs, "mod() with too many args")
+	testError(t, "Math.mod(3.2, 2)", record, function.ErrBadArgument, "mod() with float64 arg")
 }
 
 func TestStrFormat(t *testing.T) {
 	testSuccessful(t, "String.formatFloat(1.2)", nil, zstring("1.2"))
-	testError(t, "String.formatFloat()", nil, expr.ErrTooFewArgs, "formatFloat() with no args")
-	testError(t, "String.formatFloat(1.2, 3.4)", nil, expr.ErrTooManyArgs, "formatFloat() with too many args")
-	testError(t, "String.formatFloat(1)", nil, expr.ErrBadArgument, "formatFloat() with non-float arg")
+	testError(t, "String.formatFloat()", nil, function.ErrTooFewArgs, "formatFloat() with no args")
+	testError(t, "String.formatFloat(1.2, 3.4)", nil, function.ErrTooManyArgs, "formatFloat() with too many args")
+	testError(t, "String.formatFloat(1)", nil, function.ErrBadArgument, "formatFloat() with non-float arg")
 
 	testSuccessful(t, "String.formatInt(5)", nil, zstring("5"))
-	testError(t, "String.formatInt()", nil, expr.ErrTooFewArgs, "formatInt() with no args")
-	testError(t, "String.formatInt(3, 4)", nil, expr.ErrTooManyArgs, "formatInt() with too many args")
-	testError(t, "String.formatInt(1.5)", nil, expr.ErrBadArgument, "formatInt() with non-int arg")
+	testError(t, "String.formatInt()", nil, function.ErrTooFewArgs, "formatInt() with no args")
+	testError(t, "String.formatInt(3, 4)", nil, function.ErrTooManyArgs, "formatInt() with too many args")
+	testError(t, "String.formatInt(1.5)", nil, function.ErrBadArgument, "formatInt() with non-int arg")
 
 	testSuccessful(t, "String.formatIp(1.2.3.4)", nil, zstring("1.2.3.4"))
-	testError(t, "String.formatIp()", nil, expr.ErrTooFewArgs, "formatIp() with no args")
-	testError(t, "String.formatIp(1.2, 3.4)", nil, expr.ErrTooManyArgs, "formatIp() with too many args")
-	testError(t, "String.formatIp(1)", nil, expr.ErrBadArgument, "formatIp() with non-ip arg")
+	testError(t, "String.formatIp()", nil, function.ErrTooFewArgs, "formatIp() with no args")
+	testError(t, "String.formatIp(1.2, 3.4)", nil, function.ErrTooManyArgs, "formatIp() with too many args")
+	testError(t, "String.formatIp(1)", nil, function.ErrBadArgument, "formatIp() with non-ip arg")
 }
 
 func TestStrParse(t *testing.T) {
 	testSuccessful(t, `String.parseInt("1")`, nil, zint64(1))
 	testSuccessful(t, `String.parseInt("-1")`, nil, zint64(-1))
-	testError(t, `String.parseInt()`, nil, expr.ErrTooFewArgs, "parseInt() with no args")
-	testError(t, `String.parseInt("a", "b")`, nil, expr.ErrTooManyArgs, "parseInt() with too many args")
+	testError(t, `String.parseInt()`, nil, function.ErrTooFewArgs, "parseInt() with no args")
+	testError(t, `String.parseInt("a", "b")`, nil, function.ErrTooManyArgs, "parseInt() with too many args")
 	testError(t, `String.parseInt("abc")`, nil, strconv.ErrSyntax, "parseInt() with non-parseable string")
 
 	testSuccessful(t, `String.parseFloat("5.5")`, nil, zfloat64(5.5))
-	testError(t, `String.parseFloat()`, nil, expr.ErrTooFewArgs, "parseFloat() with no args")
-	testError(t, `String.parseFloat("a", "b")`, nil, expr.ErrTooManyArgs, "parseFloat() with too many args")
+	testError(t, `String.parseFloat()`, nil, function.ErrTooFewArgs, "parseFloat() with no args")
+	testError(t, `String.parseFloat("a", "b")`, nil, function.ErrTooManyArgs, "parseFloat() with too many args")
 	testError(t, `String.parseFloat("abc")`, nil, strconv.ErrSyntax, "parseFloat() with non-parseable string")
 
 	testSuccessful(t, `String.parseIp("1.2.3.4")`, nil, zaddr("1.2.3.4"))
-	testError(t, `String.parseIp()`, nil, expr.ErrTooFewArgs, "parseIp() with no args")
-	testError(t, `String.parseIp("a", "b")`, nil, expr.ErrTooManyArgs, "parseIp() with too many args")
-	testError(t, `String.parseIp("abc")`, nil, expr.ErrBadArgument, "parseIp() with non-parseable string")
+	testError(t, `String.parseIp()`, nil, function.ErrTooFewArgs, "parseIp() with no args")
+	testError(t, `String.parseIp("a", "b")`, nil, function.ErrTooManyArgs, "parseIp() with too many args")
+	testError(t, `String.parseIp("abc")`, nil, function.ErrBadArgument, "parseIp() with non-parseable string")
 }
 
 func TestOtherStrFuncs(t *testing.T) {
 	testSuccessful(t, `String.replace("bann", "n", "na")`, nil, zstring("banana"))
-	testError(t, `String.replace("foo", "bar")`, nil, expr.ErrTooFewArgs, "replace() with too few args")
-	testError(t, `String.replace("foo", "bar", "baz", "blort")`, nil, expr.ErrTooManyArgs, "replace() with too many args")
-	testError(t, `String.replace("foo", "o", 5)`, nil, expr.ErrBadArgument, "replace() with non-string arg")
+	testError(t, `String.replace("foo", "bar")`, nil, function.ErrTooFewArgs, "replace() with too few args")
+	testError(t, `String.replace("foo", "bar", "baz", "blort")`, nil, function.ErrTooManyArgs, "replace() with too many args")
+	testError(t, `String.replace("foo", "o", 5)`, nil, function.ErrBadArgument, "replace() with non-string arg")
 
 	testSuccessful(t, `String.toLower("BOO")`, nil, zstring("boo"))
-	testError(t, `String.toLower()`, nil, expr.ErrTooFewArgs, "toLower() with no args")
-	testError(t, `String.toLower("BOO", "HOO")`, nil, expr.ErrTooManyArgs, "toLower() with too many args")
+	testError(t, `String.toLower()`, nil, function.ErrTooFewArgs, "toLower() with no args")
+	testError(t, `String.toLower("BOO", "HOO")`, nil, function.ErrTooManyArgs, "toLower() with too many args")
 
 	testSuccessful(t, `String.toUpper("boo")`, nil, zstring("BOO"))
-	testError(t, `String.toUpper()`, nil, expr.ErrTooFewArgs, "toUpper() with no args")
-	testError(t, `String.toUpper("boo", "hoo")`, nil, expr.ErrTooManyArgs, "toUpper() with too many args")
+	testError(t, `String.toUpper()`, nil, function.ErrTooFewArgs, "toUpper() with no args")
+	testError(t, `String.toUpper("boo", "hoo")`, nil, function.ErrTooManyArgs, "toUpper() with too many args")
 
 	testSuccessful(t, `String.trim("  hi  there   ")`, nil, zstring("hi  there"))
-	testError(t, `String.trim()`, nil, expr.ErrTooFewArgs, "trim() with no args")
-	testError(t, `String.trim("  hi  ", "  there  ")`, nil, expr.ErrTooManyArgs, "trim() with too many args")
+	testError(t, `String.trim()`, nil, function.ErrTooFewArgs, "trim() with no args")
+	testError(t, `String.trim("  hi  ", "  there  ")`, nil, function.ErrTooManyArgs, "trim() with too many args")
 }
 
 func TestLen(t *testing.T) {
@@ -205,10 +205,10 @@ func TestLen(t *testing.T) {
 	testSuccessful(t, "len(s)", record, zint64(3))
 	testSuccessful(t, "len(a)", record, zint64(3))
 
-	testError(t, "len()", record, expr.ErrTooFewArgs, "len() with no args")
-	testError(t, `len("foo", "bar")`, record, expr.ErrTooManyArgs, "len() with too many args")
-	testError(t, `len("foo")`, record, expr.ErrBadArgument, "len() with string arg")
-	testError(t, "len(5)", record, expr.ErrBadArgument, "len() with non-container arg")
+	testError(t, "len()", record, function.ErrTooFewArgs, "len() with no args")
+	testError(t, `len("foo", "bar")`, record, function.ErrTooManyArgs, "len() with too many args")
+	testError(t, `len("foo")`, record, function.ErrBadArgument, "len() with string arg")
+	testError(t, "len(5)", record, function.ErrBadArgument, "len() with non-container arg")
 
 	record, err = parseOneRecord(`
 #0:record[s:string,bs:bstring,bs2:bstring]
@@ -247,19 +247,19 @@ func TestTime(t *testing.T) {
 	testSuccessful(t, exp, nil, zval)
 	testSuccessful(t, "Time.trunc(1590506867.967, 1)", nil, zng.Value{zng.TypeTime, zng.EncodeTime(nano.Ts(1590506867 * 1_000_000_000))})
 
-	testError(t, "Time.fromISO()", nil, expr.ErrTooFewArgs, "Time.fromISO() with no args")
-	testError(t, `Time.fromISO("abc", "def")`, nil, expr.ErrTooManyArgs, "Time.fromISO() with too many args")
-	testError(t, "Time.fromISO(1234)", nil, expr.ErrBadArgument, "Time.fromISO() with wrong argument type")
+	testError(t, "Time.fromISO()", nil, function.ErrTooFewArgs, "Time.fromISO() with no args")
+	testError(t, `Time.fromISO("abc", "def")`, nil, function.ErrTooManyArgs, "Time.fromISO() with too many args")
+	testError(t, "Time.fromISO(1234)", nil, function.ErrBadArgument, "Time.fromISO() with wrong argument type")
 
-	testError(t, "Time.fromMilliseconds()", nil, expr.ErrTooFewArgs, "Time.fromMilliseconds() with no args")
-	testError(t, "Time.fromMilliseconds(123, 456)", nil, expr.ErrTooManyArgs, "Time.fromMilliseconds() with too many args")
-	testError(t, `Time.fromMilliseconds("1234")`, nil, expr.ErrBadArgument, "Time.fromMilliseconds() with wrong argument type")
+	testError(t, "Time.fromMilliseconds()", nil, function.ErrTooFewArgs, "Time.fromMilliseconds() with no args")
+	testError(t, "Time.fromMilliseconds(123, 456)", nil, function.ErrTooManyArgs, "Time.fromMilliseconds() with too many args")
+	testError(t, `Time.fromMilliseconds("1234")`, nil, function.ErrBadArgument, "Time.fromMilliseconds() with wrong argument type")
 
-	testError(t, "Time.fromMicroseconds()", nil, expr.ErrTooFewArgs, "Time.fromMicroseconds() with no args")
-	testError(t, "Time.fromMicroseconds(123, 456)", nil, expr.ErrTooManyArgs, "Time.fromMicroseconds() with too many args")
-	testError(t, `Time.fromMicroseconds("1234")`, nil, expr.ErrBadArgument, "Time.fromMicroseconds() with wrong argument type")
+	testError(t, "Time.fromMicroseconds()", nil, function.ErrTooFewArgs, "Time.fromMicroseconds() with no args")
+	testError(t, "Time.fromMicroseconds(123, 456)", nil, function.ErrTooManyArgs, "Time.fromMicroseconds() with too many args")
+	testError(t, `Time.fromMicroseconds("1234")`, nil, function.ErrBadArgument, "Time.fromMicroseconds() with wrong argument type")
 
-	testError(t, "Time.fromNanoseconds()", nil, expr.ErrTooFewArgs, "Time.fromNanoseconds() with no args")
-	testError(t, "Time.fromNanoseconds(123, 456)", nil, expr.ErrTooManyArgs, "Time.fromNanoseconds() with too many args")
-	testError(t, `Time.fromNanoseconds("1234")`, nil, expr.ErrBadArgument, "Time.fromNanoseconds() with wrong argument type")
+	testError(t, "Time.fromNanoseconds()", nil, function.ErrTooFewArgs, "Time.fromNanoseconds() with no args")
+	testError(t, "Time.fromNanoseconds(123, 456)", nil, function.ErrTooManyArgs, "Time.fromNanoseconds() with too many args")
+	testError(t, `Time.fromNanoseconds("1234")`, nil, function.ErrBadArgument, "Time.fromNanoseconds() with wrong argument type")
 }
diff --git a/expr/ztests/cut-union.yaml b/expr/ztests/cut-union.yaml
new file mode 100644
index 000000000..8035ebfe4
--- /dev/null
+++ b/expr/ztests/cut-union.yaml
@@ -0,0 +1,17 @@
+zql: a=union(cut(x)),b=union(cut(x,s))
+
+input: |
+  #0:record[x:int32,s:string]
+  0:[1;a;]
+  0:[2;b;]
+  #1:record[s:string]
+  1:[x;]
+  1:[b;]
+  #2:record[none:string]
+  2:[bad;]
+  0:[1;a;]
+  0:[3;e;]
+
+output: |
+  #0:record[a:set[record[x:int32]],b:set[record[x:int32,s:string]]]
+  0:[[[1;][2;][3;]][[1;a;][2;b;][3;e;]]]
diff --git a/expr/ztests/cut.yaml b/expr/ztests/cut.yaml
new file mode 100644
index 000000000..c69dda532
--- /dev/null
+++ b/expr/ztests/cut.yaml
@@ -0,0 +1,28 @@
+zql: put v=cut(s,x)
+
+warnings: |
+  field is not present
+
+input: |
+  #0:record[x:int32,s:string]
+  0:[1;a;]
+  0:[2;b;]
+  #1:record[s:string]
+  1:[x;]
+  1:[b;]
+  #2:record[none:string]
+  2:[bad;]
+  0:[1;a;]
+  0:[3;e;]
+
+output: |
+  #0:record[x:int32,s:string,v:record[s:string,x:int32]]
+  0:[1;a;[a;1;]]
+  0:[2;b;[b;2;]]
+  #1:record[s:string,v:record[s:string]]
+  1:[x;[x;]]
+  1:[b;[b;]]
+  #2:record[none:string]
+  2:[bad;]
+  0:[1;a;[a;1;]]
+  0:[3;e;[e;3;]]
diff --git a/filter/filter_test.go b/filter/filter_test.go
index aa44fdd06..08369a294 100644
--- a/filter/filter_test.go
+++ b/filter/filter_test.go
@@ -48,7 +48,7 @@ func runCasesHelper(t *testing.T, tzng string, cases []testcase, expectBufferFil
 			require.NoError(t, err, "filter: %q", c.filter)
 			filterExpr := proc.(*ast.FilterProc).Filter
 
-			f, err := filter.Compile(filterExpr)
+			f, err := filter.Compile(zctx, filterExpr)
 			assert.NoError(t, err, "filter: %q", c.filter)
 			if f != nil {
 				assert.Equal(t, c.expected, f(rec),
@@ -504,7 +504,7 @@ func TestBadFilter(t *testing.T) {
 	t.Parallel()
 	proc, err := zql.ParseProc(`s =~ \xa8*`)
 	require.NoError(t, err)
-	_, err = filter.Compile(proc.(*ast.FilterProc).Filter)
+	_, err = filter.Compile(resolver.NewContext(), proc.(*ast.FilterProc).Filter)
 	assert.Error(t, err, "Received error for bad glob")
 	assert.Contains(t, err.Error(), "invalid UTF-8", "Received good error message for invalid UTF-8 in a regexp")
 }
diff --git a/ztests/suite/put/put-error.yaml b/ztests/suite/put/put-error.yaml
index c827a315a..b839deb81 100644
--- a/ztests/suite/put/put-error.yaml
+++ b/ztests/suite/put/put-error.yaml
@@ -13,4 +13,4 @@ output: |
   0:[-2;]
 
 warnings: |
-  Math.sqrt: bad argument
+  sqrt: bad argument