cue: implement unary comparators and remove ranges

This introduces a new unification type that accumulates
overallapping values that cannot be represented as a single
value. Before ranges would simplify as they were unified.
This unification type is a generalization of this mechanism.

As a result of this change, interval arithmetic is gone
(was not allowed according to the spec.)

Fixes cuelang/cue#11.

Change-Id: I1a3d21d95a0c02cf0bea6ce3422047e062fa5c6e
diff --git a/cmd/cue/cmd/testdata/trim/trim.cue b/cmd/cue/cmd/testdata/trim/trim.cue
index 362e3c5..7a4b264 100644
--- a/cmd/cue/cmd/testdata/trim/trim.cue
+++ b/cmd/cue/cmd/testdata/trim/trim.cue
@@ -19,7 +19,7 @@
 	rcList: [{a: "a", c: b}]
 
 	t <Name>: {
-		x: 0..5
+		x: >=0 & <=5
 	}
 }
 
@@ -52,7 +52,7 @@
 	t <Name>: {
 		// Combined with the other template, we know the value must be 5 and
 		// thus the entry below can be eliminated.
-		x: 5..8
+		x: >=5 & <=8
 	}
 
 	t u: {
diff --git a/cmd/cue/cmd/testdata/trim/trim.out b/cmd/cue/cmd/testdata/trim/trim.out
index 862b8a4..63b5cc2 100644
--- a/cmd/cue/cmd/testdata/trim/trim.out
+++ b/cmd/cue/cmd/testdata/trim/trim.out
@@ -19,7 +19,7 @@
 	rcList: [{a: "a", c: b}]
 
 	t <Name>: {
-		x: 0..5
+		x: >=0 & <=5
 	}
 }
 
@@ -42,7 +42,7 @@
 	t <Name>: {
 		// Combined with the other template, we know the value must be 5 and
 		// thus the entry below can be eliminated.
-		x: 5..8
+		x: >=5 & <=8
 	}
 
 	t u: {
diff --git a/cmd/cue/cmd/trim.go b/cmd/cue/cmd/trim.go
index 8265ede..92e0f3d 100644
--- a/cmd/cue/cmd/trim.go
+++ b/cmd/cue/cmd/trim.go
@@ -61,8 +61,8 @@
 	$ cat <<EOF > foo.cue
 	light <Name>: {
 		room:          string
-		brightnessOff: 0.0 | 0..100.0
-		brightnessOn:  100.0 | 0..100.0
+		brightnessOff: 0.0 | >=0 & <=100.0
+		brightnessOn:  100.0 | >=0 & <=100.0
 	}
 
 	light ceiling50: {
@@ -76,8 +76,8 @@
 	$ cat foo.cue
 	light <Name>: {
 		room:          string
-		brightnessOff: 0.0 | 0..100.0
-		brightnessOn:  100.0 | 0..100.0
+		brightnessOff: 0.0 | >=0 & <=100.0
+		brightnessOn:  100.0 | >=0 & <=100.0
 	}
 
 	light ceiling50: {
diff --git a/cue/ast.go b/cue/ast.go
index 013b119..7b067bd 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -417,7 +417,7 @@
 		}
 		list.initLit()
 		if n.Ellipsis != token.NoPos || n.Type != nil {
-			list.len = &rangeLit{list.baseValue, list.len, &top{list.baseValue}}
+			list.len = &bound{list.baseValue, opGeq, list.len}
 			if n.Type != nil {
 				list.typ = v.walk(n.Type)
 			}
@@ -457,13 +457,24 @@
 		value = call
 
 	case *ast.UnaryExpr:
-		if n.Op == token.MUL {
+		switch n.Op {
+		case token.NOT, token.ADD, token.SUB:
+			value = &unaryExpr{
+				newExpr(n),
+				tokenMap[n.Op],
+				v.walk(n.X),
+			}
+		case token.GEQ, token.GTR, token.LSS, token.LEQ, token.NEQ:
+			value = &bound{
+				newExpr(n),
+				tokenMap[n.Op],
+				v.walk(n.X),
+			}
+
+		case token.MUL:
 			return v.error(n, "preference mark not allowed at this position")
-		}
-		value = &unaryExpr{
-			newExpr(n),
-			tokenMap[n.Op],
-			v.walk(n.X),
+		default:
+			return v.error(n, "unsupported unary operator %q", n.Op)
 		}
 
 	case *ast.BinaryExpr:
@@ -473,12 +484,7 @@
 			v.addDisjunctionElem(d, n.X, false)
 			v.addDisjunctionElem(d, n.Y, false)
 			value = d
-		case token.RANGE:
-			value = &rangeLit{
-				newExpr(n),
-				v.walk(n.X), // from
-				v.walk(n.Y), // to
-			}
+
 		default:
 			value = &binaryExpr{
 				newExpr(n),
diff --git a/cue/ast_test.go b/cue/ast_test.go
index 8ae1aff..91ec603 100644
--- a/cue/ast_test.go
+++ b/cue/ast_test.go
@@ -54,6 +54,14 @@
 		`,
 		out: "<0>{a: null, b: true, c: false}",
 	}, {
+		in: `
+		a: <1
+		b: >= 0 & <= 10
+		c: != null
+		d: >100
+		`,
+		out: `<0>{a: <1, b: (>=0 & <=10), c: !=null, d: >100}`,
+	}, {
 		in: "" +
 			`a: "\(4)",
 			b: "one \(a) two \(  a + c  )",
@@ -117,23 +125,23 @@
 		in: `
 			l0: 3*[int]
 			l0: [1, 2, 3]
-			l1: (0..5)*[string]
+			l1: <=5*[string]
 			l1: ["a", "b"]
-			l2: (0..5)*[{ a: int }]
+			l2: (<=5)*[{ a: int }]
 			l2: [{a: 1}, {a: 2, b: 3}]
-			l3: (0..10)*[int]
+			l3: (<=10)*[int]
 			l3: [1, 2, 3, ...]
 			l4: [1, 2, ...]
 			l4: [...int]
 			l5: [1, ...int]
 
-			s1: ((0..6)*[int])[2:3]
+			s1: ((<=6)*[int])[2:3]
 			s2: [0,2,3][1:2]
 
-			e0: (2..5)*[{}]
+			e0: (>=2 & <=5)*[{}]
 			e0: [{}]
 			`,
-		out: `<0>{l0: ((3 * [int]) & [1,2,3]), l1: (((0..5) * [string]) & ["a","b"]), l2: (((0..5) * [<1>{a: int}]) & [<2>{a: 1},<3>{a: 2, b: 3}]), l3: (((0..10) * [int]) & [1,2,3, ...]), l4: ([1,2, ...] & [, ...int]), l5: [1, ...int], s1: ((0..6) * [int])[2:3], s2: [0,2,3][1:2], e0: (((2..5) * [<4>{}]) & [<5>{}])}`,
+		out: `<0>{l0: ((3 * [int]) & [1,2,3]), l1: ((<=5 * [string]) & ["a","b"]), l2: ((<=5 * [<1>{a: int}]) & [<2>{a: 1},<3>{a: 2, b: 3}]), l3: ((<=10 * [int]) & [1,2,3, ...]), l4: ([1,2, ...] & [, ...int]), l5: [1, ...int], s1: (<=6 * [int])[2:3], s2: [0,2,3][1:2], e0: (((>=2 & <=5) * [<4>{}]) & [<5>{}])}`,
 	}, {
 		in: `
 		a: 5 | "a" | true
@@ -227,12 +235,12 @@
 		out: `<0>{a: [ <1>for _, v in <0>.b yield (*nil*): <1>.v ], b: <2>{a: 1, b: 2, c: 3}}`,
 	}, {
 		in: `
-			a: 1..2
-			b: 1..2..3
-			c: "a".."b"
-			d: (2+3)..(4+5)
+			a: >=1 & <=2
+			b: >=1 & >=2 & <=3
+			c: >="a" & <"b"
+			d: >(2+3) & <(4+5)
 			`,
-		out: `<0>{a: (1..2), b: ((1..2)..3), c: ("a".."b"), d: ((2 + 3)..(4 + 5))}`,
+		out: `<0>{a: (>=1 & <=2), b: ((>=1 & >=2) & <=3), c: (>="a" & <"b"), d: (>(2 + 3) & <(4 + 5))}`,
 	}, {
 		in: `
 			a: *1,
diff --git a/cue/binop.go b/cue/binop.go
index 409d4a0..62fe166 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -64,6 +64,9 @@
 		left, right = right, left
 	}
 	if op != opUnify {
+		if !kind.isGround() {
+			return ctx.mkErr(src, codeIncomplete, "incomplete error")
+		}
 		ctx.exprDepth++
 		v := left.binOp(ctx, src, op, right) // may return incomplete
 		ctx.exprDepth--
@@ -89,6 +92,10 @@
 		return distribute(ctx, src, dr, left)
 	}
 
+	if _, ok := right.(*unification); ok {
+		return right.binOp(ctx, src, opUnify, left)
+	}
+
 	// TODO: value may be incomplete if there is a cycle. Instead of an error
 	// schedule an assert and return the atomic value, if applicable.
 	v := left.binOp(ctx, src, op, right)
@@ -142,6 +149,58 @@
 	panic("unreachable: special-cased")
 }
 
+func (x *unification) add(ctx *context, src source, v evaluated) evaluated {
+	for progress := true; progress; {
+		progress = false
+		k := 0
+
+		for i, vx := range x.values {
+			a := binOp(ctx, src, opUnify, vx, v)
+			switch _, isUnify := a.(*unification); {
+			case isBottom(a):
+				if !isIncomplete(a) {
+					return a
+				}
+				fallthrough
+			case isUnify:
+				x.values[k] = x.values[i]
+				k++
+				continue
+			}
+			// k will not be raised in this iteration. So the outer loop
+			// will ultimately terminate as k reaches 0.
+			// In practice it is seems unlikely that there will be more than
+			// two iterations for any addition.
+			// progress = true
+			v = a
+		}
+		if k == 0 {
+			return v
+		}
+		x.values = x.values[:k]
+	}
+	x.values = append(x.values, v)
+	return nil
+}
+
+func (x *unification) binOp(ctx *context, src source, op op, other evaluated) evaluated {
+	if op == opUnify {
+		u := &unification{baseValue: baseValue{src}}
+		u.values = append(u.values, x.values...)
+		if y, ok := other.(*unification); ok {
+			for _, vy := range y.values {
+				if v := u.add(ctx, src, vy); v != nil {
+					return v
+				}
+			}
+		} else if v := u.add(ctx, src, other); v != nil {
+			return v
+		}
+		return u
+	}
+	return ctx.mkIncompatible(src, op, x, other)
+}
+
 func (x *top) binOp(ctx *context, src source, op op, other evaluated) evaluated {
 	switch op {
 	case opUnify:
@@ -162,9 +221,11 @@
 				return &basicType{binSrc(src.Pos(), op, x, other), k & typeKinds}
 			}
 		}
-	case *rangeLit:
+
+	case *bound:
 		src = mkBin(ctx, src.Pos(), op, x, other)
 		return ctx.mkErr(src, codeIncomplete, "%s with incomplete values", op)
+
 	case *numLit:
 		if op == opUnify {
 			if k == y.k {
@@ -176,6 +237,7 @@
 		}
 		src = mkBin(ctx, src.Pos(), op, x, other)
 		return ctx.mkErr(src, codeIncomplete, "%s with incomplete values", op)
+
 	default:
 		if k&typeKinds != bottomKind {
 			return other
@@ -184,238 +246,203 @@
 	return ctx.mkIncompatible(src, op, x, other)
 }
 
-// unifyFrom determines the maximum value of a and b.
-func unifyFrom(ctx *context, src source, a, b evaluated) evaluated {
-	if a.kind().isGround() && b.kind().isGround() {
-		if leq(ctx, src, a, b) {
-			return b
-		}
-		return a
+func checkBounds(ctx *context, src source, r *bound, op op, a, b evaluated) evaluated {
+	v := binOp(ctx, src, op, a, b)
+	if isBottom(v) || !v.(*boolLit).b {
+		return errOutOfBounds(ctx, src.Pos(), r, a)
 	}
-	if isTop(a) {
-		return b
-	}
-	if isTop(b) {
-		return a
-	}
-	if x, ok := a.(*rangeLit); ok {
-		return unifyFrom(ctx, src, x.from.(evaluated), b)
-	}
-	if x, ok := b.(*rangeLit); ok {
-		return unifyFrom(ctx, src, a, x.from.(evaluated))
-	}
-	src = mkBin(ctx, src.Pos(), opUnify, a, b)
-	return ctx.mkErr(src, "incompatible types %v and %v", a.kind(), b.kind())
+	return nil
 }
 
-// unifyTo determines the minimum value of a and b.
-func unifyTo(ctx *context, src source, a, b evaluated) evaluated {
-	if a.kind().isGround() && b.kind().isGround() {
-		if leq(ctx, src, a, b) {
-			return a
-		}
-		return b
-	}
-	if isTop(a) {
-		return b
-	}
-	if isTop(b) {
-		return a
-	}
-	if x, ok := a.(*rangeLit); ok {
-		return unifyTo(ctx, src, x.to.(evaluated), b)
-	}
-	if x, ok := b.(*rangeLit); ok {
-		return unifyTo(ctx, src, a, x.to.(evaluated))
-	}
-	src = mkBin(ctx, src.Pos(), opUnify, a, b)
-	return ctx.mkErr(src, "incompatible types %v and %v", a.kind(), b.kind())
-}
-
-func errInRange(ctx *context, pos token.Pos, r *rangeLit, v evaluated) *bottom {
+func errOutOfBounds(ctx *context, pos token.Pos, r *bound, v evaluated) *bottom {
 	if pos == token.NoPos {
 		pos = r.Pos()
 	}
-	const msgInRange = "value %v not in range %v"
 	e := mkBin(ctx, pos, opUnify, r, v)
-	return ctx.mkErr(e, msgInRange, v.strValue(), debugStr(ctx, r))
+	if r.op == opNeq {
+		const msgInRange = "%v excluded by %v"
+		return ctx.mkErr(e, msgInRange, debugStr(ctx, v), debugStr(ctx, r))
+	}
+	const msgInRange = "%v not within bound %v"
+	return ctx.mkErr(e, msgInRange, debugStr(ctx, v), debugStr(ctx, r))
 }
 
-func (x *rangeLit) binOp(ctx *context, src source, op op, other evaluated) evaluated {
-	combine := func(x, y evaluated) evaluated {
-		if _, ok := x.(*numLit); !ok {
-			return x
-		}
-		if _, ok := y.(*numLit); !ok {
-			return y
-		}
-		return binOp(ctx, src, op, x, y)
+func opInfo(op op) (cmp op, norm int) {
+	switch op {
+	case opGtr:
+		return opGeq, 1
+	case opGeq:
+		return opGtr, 1
+	case opLss:
+		return opLeq, -1
+	case opLeq:
+		return opLss, -1
+	case opNeq:
+		return opNeq, 0
 	}
-	from := x.from.(evaluated)
-	to := x.to.(evaluated)
-	newSrc := mkBin(ctx, src.Pos(), op, x, other)
+	panic("cue: unreachable")
+}
+
+func (x *bound) binOp(ctx *context, src source, op op, other evaluated) evaluated {
+	xv := x.value.(evaluated)
+
+	newSrc := binSrc(src.Pos(), op, x, other)
 	switch op {
 	case opUnify:
-		k := unifyType(x.kind(), other.kind())
-		if k&comparableKind != bottomKind {
-			switch y := other.(type) {
-			case *basicType:
-				from := unify(ctx, src, x.from.(evaluated), y)
-				to := unify(ctx, src, x.to.(evaluated), y)
-				if from == x.from && to == x.to {
-					return x
-				}
-				return &rangeLit{newSrc.base(), from, to}
-			case *rangeLit:
-				from := unifyFrom(ctx, src, x.from.(evaluated), y.from.(evaluated))
-				to := unifyTo(ctx, src, x.to.(evaluated), y.to.(evaluated))
-				if from.kind().isGround() && to.kind().isGround() && !leq(ctx, src, from, to) {
-					r1 := debugStr(ctx, x)
-					r2 := debugStr(ctx, y)
-					return ctx.mkErr(newSrc, "non-overlapping ranges %s and %s", r1, r2)
-				}
-				return ctx.manifest(&rangeLit{newSrc.base(), from, to})
-
-			case *numLit:
-				if !leq(ctx, src, x.from.(evaluated), y) || !leq(ctx, src, y, x.to.(evaluated)) {
-					return errInRange(ctx, src.Pos(), x, y)
-				}
-				if y.k != k {
-					n := *y
-					n.k = k
-					return &n
-				}
-				return other
-
-			case *durationLit, *stringLit:
-				if !leq(ctx, src, x.from.(evaluated), y) || !leq(ctx, src, y, x.to.(evaluated)) {
-					return errInRange(ctx, src.Pos(), x, y)
-				}
-				return other
-			}
+		k, _ := matchBinOpKind(opUnify, x.kind(), other.kind())
+		if k == bottomKind {
+			break
 		}
-	// See https://en.wikipedia.org/wiki/Interval_arithmetic.
-	case opAdd:
-		switch x.kind() & typeKinds {
-		case stringKind:
-			if !x.from.kind().isGround() || !x.to.kind().isGround() {
-				// TODO: return regexp
+		switch y := other.(type) {
+		case *basicType:
+			v := unify(ctx, src, xv, y)
+			if v == xv {
+				return x
+			}
+			return &bound{newSrc.base(), x.op, v}
+
+		case *bound:
+			yv := y.value.(evaluated)
+			if !xv.kind().isGround() || !yv.kind().isGround() {
 				return ctx.mkErr(newSrc, codeIncomplete, "cannot add incomplete values")
 			}
-			combine := func(x, y evaluated) evaluated {
-				if _, ok := x.(*basicType); ok {
-					return ctx.mkErr(newSrc, "adding string to non-concrete type")
+
+			cmp, xCat := opInfo(x.op)
+			_, yCat := opInfo(y.op)
+
+			switch {
+			case xCat == yCat:
+				if x.op == opNeq {
+					if test(ctx, x, opEql, xv, yv) {
+						return x
+					}
+					break
 				}
-				if _, ok := y.(*basicType); ok {
+
+				// xCat == yCat && x.op != opNeq
+				// > a & >= b
+				//    > a   if a >= b
+				//    >= b  if a <  b
+				// > a & > b
+				//    > a   if a >= b
+				//    > b   if a <  b
+				// >= a & > b
+				//    >= a   if a > b
+				//    > b    if a <= b
+				// >= a & >= b
+				//    >= a   if a > b
+				//    >= b   if a <= b
+				// inverse is true as well.
+
+				// Tighten bound.
+				if test(ctx, x, cmp, xv, yv) {
 					return x
 				}
-				return binOp(ctx, src, opAdd, x, y)
-			}
-			return &rangeLit{
-				baseValue: binSrc(src.Pos(), op, x, other),
-				from:      combine(minNum(from), minNum(other)),
-				to:        combine(maxNum(to), maxNum(other)),
-			}
+				return y
 
-		case intKind, numKind, floatKind:
-			return &rangeLit{
-				baseValue: binSrc(src.Pos(), op, x, other),
-				from:      combine(minNum(from), minNum(other)),
-				to:        combine(maxNum(to), maxNum(other)),
-			}
+			case xCat == -yCat:
+				if xCat == -1 {
+					x, y = y, x
+				}
+				a, aOK := x.value.(evaluated).(*numLit)
+				b, bOK := y.value.(evaluated).(*numLit)
 
-		default:
-			return ctx.mkErrUnify(src, x, other)
-		}
+				if !aOK || !bOK {
+					break
+				}
 
-	case opSub:
-		return &rangeLit{
-			baseValue: binSrc(src.Pos(), op, x, other),
-			from:      combine(minNum(from), maxNum(other)),
-			to:        combine(maxNum(to), minNum(other)),
-		}
+				var d apd.Decimal
+				cond, err := apd.BaseContext.Sub(&d, &b.v, &a.v)
+				if cond.Inexact() || err != nil {
+					break
+				}
 
-	case opQuo:
-		// See https://en.wikipedia.org/wiki/Interval_arithmetic.
-		// TODO: all this is strictly not correct. To do it right we need to
-		// have non-inclusive ranges at the least. So for now we just do this.
-		var from, to evaluated
-		if max := maxNum(other); !max.kind().isGround() {
-			from = newNum(other, max.kind()) // 1/infinity is 0
-		} else if num, ok := max.(*numLit); ok && num.v.IsZero() {
-			from = &basicType{num.baseValue, num.kind()} // div by 0
-		} else {
-			one := newNum(other, max.kind())
-			one.v.SetInt64(1)
-			from = combine(one, max)
-		}
+				// attempt simplification
+				// numbers
+				// >=a & <=b
+				//     a   if a == b
+				//     _|_ if a < b
+				// >=a & <b
+				//     _|_ if b <= a
+				// >a  & <=b
+				//     _|_ if b <= a
+				// >a  & <b
+				//     _|_ if b <= a
 
-		if _, ok := other.(*rangeLit); !ok {
-			other = from
-		} else {
-			if min := minNum(other); !min.kind().isGround() {
-				to = newNum(other, min.kind()) // 1/infinity is 0
-			} else if num, ok := min.(*numLit); ok && num.v.IsZero() {
-				to = &basicType{num.baseValue, num.kind()} // div by 0
-			} else {
-				one := newNum(other, min.kind())
-				one.v.SetInt64(1)
-				to = combine(one, min)
-			}
+				// integers
+				// >=a & <=b
+				//     a   if b-a == 0
+				//     _|_ if a < b
+				// >=a & <b
+				//     a   if b-a == 1
+				//     _|_ if b <= a
+				// >a  & <=b
+				//     b   if b-a == 1
+				//     _|_ if b <= a
+				// >a  & <b
+				//     a+1 if b-a == 2
+				//     _|_ if b <= a
 
-			if !from.kind().isGround() && !to.kind().isGround() {
-				other = from
-			} else if leq(ctx, src, from, to) && leq(ctx, src, to, from) {
-				other = from
-			} else {
-				other = &rangeLit{newSrc.base(), from, to}
-			}
-		}
-		fallthrough
+				switch diff, err := d.Int64(); {
+				case err != nil:
 
-	case opMul:
-		xMin, xMax := minNum(from), maxNum(to)
-		yMin, yMax := minNum(other), maxNum(other)
+				case diff == 1:
+					if k&floatKind == 0 {
+						if x.op == opGeq && y.op == opLss {
+							return a
+						}
+						if x.op == opGtr && y.op == opLeq {
+							return b
+						}
+					}
 
-		var from, to evaluated
-		negMax := func(from, to *evaluated, val, sign evaluated) {
-			if !val.kind().isGround() {
-				*from = val
-				if num, ok := sign.(*numLit); ok && num.v.Negative {
-					*to = val
+				case diff == 2:
+					if k&floatKind == 0 && x.op == opGtr && y.op == opLss {
+						apd.BaseContext.Add(&d, d.SetInt64(1), &a.v)
+						n := *a
+						n.k = k
+						n.v = d
+						return &n
+					}
+
+				case diff == 0:
+					if x.op == opGeq && y.op == opLeq {
+						return a
+					}
+					fallthrough
+
+				case d.Negative:
+					return ctx.mkErr(newSrc, "incompatible bounds %v and %v",
+						debugStr(ctx, x), debugStr(ctx, y))
+				}
+
+			case y.op == opNeq:
+				if !test(ctx, x, x.op, yv, xv) {
+					return x
 				}
 			}
-		}
-		negMax(&from, &to, yMin, xMax)
-		negMax(&to, &from, yMax, xMin)
-		negMax(&from, &to, xMin, yMax)
-		negMax(&to, &from, xMax, yMin)
-		if from != nil && to != nil {
-			return binOp(ctx, src, opUnify, from, to)
-		}
+			return &unification{newSrc, []evaluated{x, y}}
 
-		values := []evaluated{}
-		add := func(a, b evaluated) {
-			if a.kind().isGround() && b.kind().isGround() {
-				values = append(values, combine(a, b))
+		case *numLit:
+			if err := checkBounds(ctx, src, x, x.op, y, xv); err != nil {
+				return err
 			}
-		}
-		add(xMin, yMin)
-		add(xMax, yMin)
-		add(xMin, yMax)
-		add(xMax, yMax)
-		sort.Slice(values, func(i, j int) bool {
-			return !leq(ctx, src, values[j], values[i])
-		})
+			// Narrow down number type.
+			if y.k != k {
+				n := *y
+				n.k = k
+				return &n
+			}
+			return other
 
-		r := &rangeLit{baseValue: binSrc(src.Pos(), op, x, other), from: from, to: to}
-		if from == nil {
-			r.from = values[0]
+		case *nullLit, *boolLit, *durationLit, *list, *structLit, *stringLit, *bytesLit:
+			// All remaining concrete types. This includes non-comparable types
+			// for comparison to null.
+			if err := checkBounds(ctx, src, x, x.op, y, xv); err != nil {
+				return err
+			}
+			return y
 		}
-		if to == nil {
-			r.to = values[len(values)-1]
-		}
-		return r
 	}
 	return ctx.mkIncompatible(src, op, x, other)
 }
@@ -521,6 +548,13 @@
 			return x
 		}
 
+	case *bound:
+		// Not strictly necessary, but handling this results in better error
+		// messages.
+		if op == opUnify {
+			return other.binOp(ctx, src, opUnify, x)
+		}
+
 	default:
 		switch op {
 		case opEql:
@@ -610,6 +644,14 @@
 	return ctx.mkIncompatible(src, op, x, other)
 }
 
+func test(ctx *context, src source, op op, a, b evaluated) bool {
+	v := binOp(ctx, src, op, a, b)
+	if isBottom(v) {
+		return false
+	}
+	return v.(*boolLit).b
+}
+
 func leq(ctx *context, src source, a, b evaluated) bool {
 	if isTop(a) || isTop(b) {
 		return true
@@ -621,42 +663,35 @@
 	return v.(*boolLit).b
 }
 
-func maxNum(v evaluated) evaluated {
+// TODO: should these go?
+func maxNum(v value) value {
 	switch x := v.(type) {
 	case *numLit:
 		return x
-	case *rangeLit:
-		return maxNum(x.to.(evaluated))
+	case *bound:
+		switch x.op {
+		case opLeq:
+			return x.value
+		case opLss:
+			return &binaryExpr{x.baseValue, opSub, x.value, one}
+		}
+		return &basicType{x.baseValue, intKind}
 	}
 	return v
 }
 
-func minNum(v evaluated) evaluated {
+func minNum(v value) value {
 	switch x := v.(type) {
 	case *numLit:
 		return x
-	case *rangeLit:
-		return minNum(x.from.(evaluated))
-	}
-	return v
-}
-
-func maxNumRaw(v value) value {
-	switch x := v.(type) {
-	case *numLit:
-		return x
-	case *rangeLit:
-		return maxNumRaw(x.to)
-	}
-	return v
-}
-
-func minNumRaw(v value) value {
-	switch x := v.(type) {
-	case *numLit:
-		return x
-	case *rangeLit:
-		return minNumRaw(x.from)
+	case *bound:
+		switch x.op {
+		case opGeq:
+			return x.value
+		case opGtr:
+			return &binaryExpr{x.baseValue, opAdd, x.value, one}
+		}
+		return &basicType{x.baseValue, intKind}
 	}
 	return v
 }
@@ -690,13 +725,6 @@
 		if op == opUnify {
 			return y.binOp(ctx, src, op, x)
 		}
-		// infinity math
-		// 4 * int = int
-	case *rangeLit:
-		if op == opUnify {
-			return y.binOp(ctx, src, op, x)
-		}
-		// 5..7 - 8 = -3..4
 	case *numLit:
 		k := unifyType(x.kind(), y.kind())
 		n := newNumBin(k, x, y)
@@ -843,6 +871,7 @@
 		if !ok {
 			break
 		}
+
 		n := unify(ctx, src, x.len.(evaluated), y.len.(evaluated))
 		if isBottom(n) {
 			src = mkBin(ctx, src.Pos(), op, x, other)
diff --git a/cue/debug.go b/cue/debug.go
index c51cc1d..d2d1807 100644
--- a/cue/debug.go
+++ b/cue/debug.go
@@ -204,6 +204,15 @@
 		writef(" %v ", x.op)
 		p.debugStr(x.right)
 		write(")")
+	case *unification:
+		write("(")
+		for i, v := range x.values {
+			if i != 0 {
+				writef(" & ")
+			}
+			p.debugStr(v)
+		}
+		write(")")
 	case *disjunction:
 		write("(")
 		for i, v := range x.values {
@@ -312,12 +321,9 @@
 		}
 	case *durationLit:
 		write(x.d.String())
-	case *rangeLit:
-		write("(")
-		p.debugStr(x.from)
-		write("..")
-		p.debugStr(x.to)
-		write(")")
+	case *bound:
+		p.writef("%v", x.op)
+		p.debugStr(x.value)
 	case *interpolation:
 		for i, e := range x.parts {
 			if i != 0 {
@@ -327,13 +333,13 @@
 		}
 	case *list:
 		// TODO: do not evaluate
-		max := maxNum(x.len.evalPartial(p.ctx))
+		max := maxNum(x.len).evalPartial(p.ctx)
 		inCast := false
 		ellipsis := false
 		n, ok := max.(*numLit)
 		if !ok {
 			// TODO: do not evaluate
-			min := minNum(x.len.evalPartial(p.ctx))
+			min := minNum(x.len).evalPartial(p.ctx)
 			n, _ = min.(*numLit)
 		}
 		ln := 0
@@ -341,8 +347,13 @@
 			x, _ := n.v.Int64()
 			ln = int(x)
 		}
+		open := false
+		switch max.(type) {
+		case *top, *basicType:
+			open = true
+		}
 		if !ok || ln > len(x.a) {
-			if !isTop(max) && !isTop(x.typ) {
+			if !open && !isTop(x.typ) {
 				p.debugStr(x.len)
 				write("*[")
 				p.debugStr(x.typ)
diff --git a/cue/eval.go b/cue/eval.go
index 93efbd3..8832ffc 100644
--- a/cue/eval.go
+++ b/cue/eval.go
@@ -168,44 +168,19 @@
 	return e.err(err)
 }
 
-func (x *rangeLit) evalPartial(ctx *context) (result evaluated) {
+func (x *bound) evalPartial(ctx *context) (result evaluated) {
 	if ctx.trace {
-		defer uni(indent(ctx, "rangeLit", x))
+		defer uni(indent(ctx, "bound", x))
 		defer func() { ctx.debugPrint("result:", result) }()
 	}
-	rngFrom := x.from.evalPartial(ctx)
-	rngTo := x.to.evalPartial(ctx)
-	// rngFrom := ctx.manifest(x.from)
-	// rngTo := ctx.manifest(x.to)
-	// kind := unifyType(rngFrom.kind(), rngTo.kind())
-	// TODO: sufficient to do just this?
-	kind, _ := matchBinOpKind(opLeq, rngFrom.kind(), rngTo.kind())
-	if kind&comparableKind == bottomKind {
-		return ctx.mkErr(x, "invalid range: must be defined for strings or numbers")
+	v := x.value.evalPartial(ctx)
+	if isBottom(v) {
+		return ctx.mkErr(x, v, "error evaluating bound")
 	}
-	// Collapse evaluated nested ranges
-	if from, ok := rngFrom.(*rangeLit); ok {
-		rngFrom = from.from.(evaluated)
+	if v == x.value {
+		return x
 	}
-	if to, ok := rngTo.(*rangeLit); ok {
-		rngTo = to.to.(evaluated)
-	}
-	rng := &rangeLit{x.baseValue, rngFrom, rngTo}
-	if !rngFrom.kind().isGround() || !rngTo.kind().isGround() {
-		return rng
-	}
-	// validate range
-	comp := binOp(ctx, x, opLeq, rngFrom, rngTo)
-	if isBottom(comp) {
-		return ctx.mkErr(comp, "invalid range")
-	}
-	if !comp.(*boolLit).b {
-		return ctx.mkErr(x, "for ranges from <= to, found %v > %v", rngFrom, rngTo)
-	}
-	if binOp(ctx, x, opEql, rngFrom, rngTo).(*boolLit).b {
-		return rngFrom
-	}
-	return rng
+	return &bound{x.baseValue, x.op, v}
 }
 
 func (x *interpolation) evalPartial(ctx *context) (result evaluated) {
@@ -302,6 +277,11 @@
 	return x
 }
 
+func (x *unification) evalPartial(ctx *context) (result evaluated) {
+	// By definition, all of the values in this type are already evaluated.
+	return x
+}
+
 func (x *disjunction) evalPartial(ctx *context) (result evaluated) {
 	if ctx.trace {
 		defer uni(indent(ctx, "disjunction", x))
@@ -460,10 +440,6 @@
 			return &basicType{v.baseValue, numeric | nonGround}
 		case *basicType:
 			return &basicType{v.baseValue, (v.k & numeric) | nonGround}
-		case *rangeLit:
-			from := evalUnary(ctx, src, op, v.from)
-			to := evalUnary(ctx, src, op, v.to)
-			return &rangeLit{src.base(), from, to}
 		}
 
 	case opNot:
diff --git a/cue/export.go b/cue/export.go
index 15c0b04..3366d9b 100644
--- a/cue/export.go
+++ b/cue/export.go
@@ -99,6 +99,18 @@
 			X:  p.expr(x.left),
 			Op: opMap[x.op], Y: p.expr(x.right),
 		}
+	case *bound:
+		return &ast.UnaryExpr{Op: opMap[x.op], X: p.expr(x.value)}
+	case *unification:
+		if len(x.values) == 1 {
+			return p.expr(x.values[0])
+		}
+		bin := p.expr(x.values[0])
+		for _, v := range x.values[1:] {
+			bin = &ast.BinaryExpr{X: bin, Op: token.UNIFY, Y: p.expr(v)}
+		}
+		return bin
+
 	case *disjunction:
 		if len(x.values) == 1 {
 			return p.expr(x.values[0].val)
@@ -110,12 +122,8 @@
 			}
 			return e
 		}
-		bin := &ast.BinaryExpr{
-			X:  expr(x.values[0]),
-			Op: token.DISJUNCTION,
-			Y:  expr(x.values[1]),
-		}
-		for _, v := range x.values[2:] {
+		bin := expr(x.values[0])
+		for _, v := range x.values[1:] {
 			bin = &ast.BinaryExpr{X: bin, Op: token.DISJUNCTION, Y: expr(v)}
 		}
 		return bin
@@ -203,13 +211,6 @@
 	case *durationLit:
 		panic("unimplemented")
 
-	case *rangeLit:
-		return &ast.BinaryExpr{
-			X:  p.expr(x.from),
-			Op: token.RANGE,
-			Y:  p.expr(x.to),
-		}
-
 	case *interpolation:
 		t := &ast.Interpolation{}
 		multiline := false
@@ -257,10 +258,10 @@
 		for _, e := range x.a {
 			list.Elts = append(list.Elts, p.expr(e))
 		}
-		max := maxNumRaw(x.len)
+		max := maxNum(x.len)
 		num, ok := max.(*numLit)
 		if !ok {
-			min := minNumRaw(x.len)
+			min := minNum(x.len)
 			num, _ = min.(*numLit)
 		}
 		ln := 0
@@ -268,9 +269,14 @@
 			x, _ := num.v.Int64()
 			ln = int(x)
 		}
+		open := false
+		switch max.(type) {
+		case *top, *basicType:
+			open = true
+		}
 		if !ok || ln > len(x.a) {
 			list.Type = p.expr(x.typ)
-			if !isTop(max) && !isTop(x.typ) {
+			if !open && !isTop(x.typ) {
 				expr = &ast.BinaryExpr{
 					X: &ast.BinaryExpr{
 						X:  p.expr(x.len),
diff --git a/cue/export_test.go b/cue/export_test.go
index f5dd274..2673305 100644
--- a/cue/export_test.go
+++ b/cue/export_test.go
@@ -74,11 +74,11 @@
 		in: `{
 			a: 5*[int]
 			a: [1, 2, ...]
-			b: 0..5*[int]
+			b: <=5*[int]
 			b: [1, 2, ...]
-			c: 3..5*[int]
+			c: (>=3 & <=5)*[int]
 			c: [1, 2, ...]
-			d: 2.._*[int]
+			d: >=2*[int]
 			d: [1, 2, ...]
 			e: [...int]
 			e: [1, 2, ...]
@@ -87,16 +87,16 @@
 		out: unindent(`
 			{
 				a: 5*[int] & [1, 2, ...int]
-				b: 2..5*[int] & [1, 2, ...int]
-				c: 3..5*[int] & [1, 2, ...int]
+				b: (>=2 & <=5)*[int] & [1, 2, ...int]
+				c: (<=5 & >=3)*[int] & [1, 2, ...int]
 				d: [1, 2, ...int]
 				e: [1, 2, ...int]
 				f: [1, 2, ...]
 			}`),
 	}, {
 		in: `{
-			a: (0.._)*[int]
-			a: (0.._)*[...int]
+			a: >=0*[int]
+			a: [...int]
 		}`,
 		out: unindent(`
 			{
@@ -120,6 +120,23 @@
 				b: a[2:3]
 			}`),
 	}, {
+		in: `{
+			a: >=0 & <=10 & !=1
+		}`,
+		out: unindent(`
+			{
+				a: >=0 & <=10 & !=1
+			}`),
+	}, {
+		raw: true,
+		in: `{
+				a: >=0 & <=10 & !=1
+			}`,
+		out: unindent(`
+			{
+				a: >=0 & <=10 & !=1
+			}`),
+	}, {
 		raw: true,
 		in:  `{ a: [1, 2], b: { "\(k)": v for k, v in a if a > 1 } }`,
 		out: unindent(`
@@ -139,10 +156,10 @@
 			}`),
 	}, {
 		raw: true,
-		in:  `{ a: 0..10, b: "Count: \(a) times" }`,
+		in:  `{ a: >=0 & <=10, b: "Count: \(a) times" }`,
 		out: unindent(`
 			{
-				a: 0..10
+				a: >=0 & <=10
 				b: "Count: \(a) times"
 			}`),
 	}, {
diff --git a/cue/format/node.go b/cue/format/node.go
index f9cef31..85035a5 100644
--- a/cue/format/node.go
+++ b/cue/format/node.go
@@ -310,9 +310,6 @@
 			f.internalError("depth < 1:", depth)
 			depth = 1
 		}
-		if prec1 == 8 { // ..
-			prec1 = 9 // always parentheses for values of ranges
-		}
 		f.binaryExpr(x, prec1, cutoff(x, depth), depth)
 
 	case *ast.UnaryExpr:
@@ -556,7 +553,6 @@
 // (Algorithm suggestion by Russ Cox.)
 //
 // The precedences are:
-//  8             ..
 //	7             *  /  % quo rem div mod
 //	6             +  -
 //	5             ==  !=  <  <=  >  >=
diff --git a/cue/format/testdata/expressions.golden b/cue/format/testdata/expressions.golden
index ed5f3bf..15c6377 100644
--- a/cue/format/testdata/expressions.golden
+++ b/cue/format/testdata/expressions.golden
@@ -29,10 +29,10 @@
 
 	e:  1 + 2*3
 	e:  1 * 2 * 3 // error
-	e:  2..3
-	e:  2..(3 + 4)
-	ex: 2..3 + 4*5
-	e:  (2..3)..4
+	e:  >=2 & <=3
+	e:  >2 & <=(3 + 4)
+	ex: >2 & <=(3 + 4*5)
+	e:  >2 & <=3 & <=4
 	e:  1 + 2 + 3 // error
 
 	e: s[1+2]
diff --git a/cue/format/testdata/expressions.input b/cue/format/testdata/expressions.input
index 60f513a..6b324a3 100644
--- a/cue/format/testdata/expressions.input
+++ b/cue/format/testdata/expressions.input
@@ -29,10 +29,10 @@
 
     e: 1+2*3
     e: 1*2*3 // error
-    e: 2..3
-    e: 2..(3 + 4)
-    ex: 2..3+4*5
-    e: 2..3..4
+    e: >=2 & <=3
+    e: >2 & <=(3 + 4)
+    ex: >2 & <=(3 + 4*5)
+    e: >2 & <=3 & <=4
     e: 1 + 2 + 3 // error
 
     e: s[1+2]
diff --git a/cue/kind.go b/cue/kind.go
index 86a16d0..a35e886 100644
--- a/cue/kind.go
+++ b/cue/kind.go
@@ -232,7 +232,7 @@
 	case a&nonGround == 0 && b&nonGround == 0:
 		// both ground values: nothing to do
 
-	case op != opUnify && op != opLand && op != opLor:
+	case op != opUnify && op != opLand && op != opLor && op != opNeq:
 
 	default:
 		invert = aGround && !bGround
diff --git a/cue/op.go b/cue/op.go
index 9a2a809..9d9c0a8 100644
--- a/cue/op.go
+++ b/cue/op.go
@@ -67,8 +67,6 @@
 	opIMod
 	opIQuo
 	opIRem
-
-	opRange // Used in computedSource
 )
 
 var opStrings = []string{
@@ -99,8 +97,6 @@
 	opIMod: "mod",
 	opIQuo: "quo",
 	opIRem: "rem",
-
-	opRange: "..",
 }
 
 func (op op) String() string { return opStrings[op] }
@@ -131,8 +127,6 @@
 	token.NEQ: opNeq, // !=
 	token.LEQ: opLeq, // <=
 	token.GEQ: opGeq, // >=
-
-	token.RANGE: opRange, // ..
 }
 
 var opMap = map[op]token.Token{}
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index 13870ec..b46fa25 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -425,12 +425,6 @@
 	return true // "insert" comma and continue
 }
 
-func assert(cond bool, msg string) {
-	if !cond {
-		panic("lacelang/parser internal error: " + msg)
-	}
-}
-
 // syncExpr advances to the next field in a field list.
 // Used for synchronization after an error.
 func syncExpr(p *parser) {
@@ -1109,7 +1103,8 @@
 	}
 
 	switch p.tok {
-	case token.ADD, token.SUB, token.NOT, token.MUL:
+	case token.ADD, token.SUB, token.NOT, token.MUL,
+		token.NEQ, token.LSS, token.LEQ, token.GEQ, token.GTR:
 		pos, op := p.pos, p.tok
 		c := p.openComments()
 		p.next()
@@ -1228,18 +1223,6 @@
 	return x
 }
 
-func (p *parser) parseCallExpr(callType string) *ast.CallExpr {
-	x := p.parseRHS() // could be a conversion: (some type)(x)
-	if call, isCall := x.(*ast.CallExpr); isCall {
-		return call
-	}
-	if _, isBad := x.(*ast.BadExpr); !isBad {
-		// only report error if it's a new one
-		p.error(p.safePos(x.End()), fmt.Sprintf("function must be invoked in %s statement", callType))
-	}
-	return nil
-}
-
 // ----------------------------------------------------------------------------
 // Declarations
 
diff --git a/cue/parser/parser_test.go b/cue/parser/parser_test.go
index e0c5a69..fdae715 100644
--- a/cue/parser/parser_test.go
+++ b/cue/parser/parser_test.go
@@ -143,14 +143,14 @@
 		"a: (2 div 3) mod 5, b: (2 quo 3) rem 4, c: 2 div 3 div 4",
 	}, {
 		"ranges",
-		`	a: 1..2
-			b: 2.0 .. 40.0
-			c: "a".."b"
-			v: (1..2)..(5..10)
-			w: 1..2..3
-			d: 3T..5M
+		`	a: >=1 & <=2
+			b: >2.0  & <= 40.0
+			c: >"a" & <="b"
+			v: (>=1 & <=2) & <=(>=5 & <=10)
+			w: >1 & <=2 & <=3
+			d: >=3T & <=5M
 		`,
-		"a: 1..2, b: 2.0..40.0, c: \"a\"..\"b\", v: (1..2)..(5..10), w: 1..2..3, d: 3T..5M",
+		"a: >=1&<=2, b: >2.0&<=40.0, c: >\"a\"&<=\"b\", v: (>=1&<=2)&<=(>=5&<=10), w: >1&<=2&<=3, d: >=3T&<=5M",
 	}, {
 		"indices",
 		`{
@@ -183,12 +183,12 @@
 		"list types",
 		`{
 			a: 4*[int]
-			b: 0..5*[ {a: 5} ]
+			b: <=5*[ {a: 5} ]
 			c1: [...int]
 			c2: [...]
 			c3: [1, 2, ...int,]
 		}`,
-		`{a: 4*[int], b: 0..5*[{a: 5}], c1: [...int], c2: [...], c3: [1, 2, ...int]}`,
+		`{a: 4*[int], b: <=5*[{a: 5}], c1: [...int], c2: [...], c3: [1, 2, ...int]}`,
 	}, {
 		"list comprehensions",
 		`{
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index d9bef3a..bfac4ee 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -231,10 +231,10 @@
 			e: [] & 4
 			e2: [3]["d"]
 			e3: [3][-1]
-			e4: [1, 2, ...4..5] & [1, 2, 4, 8]
-			e5: [1, 2, 4, 8] & [1, 2, ...4..5]
+			e4: [1, 2, ...>=4 & <=5] & [1, 2, 4, 8]
+			e5: [1, 2, 4, 8] & [1, 2, ...>=4 & <=5]
 			`,
-		out: `<0>{list: [1,2,3], index: 2, unify: [1,2,3], e: _|_(([] & 4):unsupported op &(list, number)), e2: _|_("d":invalid list index "d" (type string)), e3: _|_(-1:invalid list index -1 (index must be non-negative)), e4: _|_(((4..5) & 8):value 8 not in range (4..5)), e5: _|_(((4..5) & 8):value 8 not in range (4..5))}`,
+		out: `<0>{list: [1,2,3], index: 2, unify: [1,2,3], e: _|_(([] & 4):unsupported op &(list, number)), e2: _|_("d":invalid list index "d" (type string)), e3: _|_(-1:invalid list index -1 (index must be non-negative)), e4: _|_((<=5 & 8):8 not within bound <=5), e5: _|_((<=5 & 8):8 not within bound <=5)}`,
 	}, {
 		desc: "selecting",
 		in: `
@@ -274,7 +274,7 @@
 			o9: (2 | 3) & (1 | 2 | 3)
 			o10: (3 | 2) & (1 | *2 | 3)
 
-			m1: (*1 | (*2 | 3)) & 2..3
+			m1: (*1 | (*2 | 3)) & (>=2 & <=3)
 			m2: (*1 | (*2 | 3)) & (2 | 3)
 			m3: (*1 | *(*2 | 3)) & (2 | 3)
 			m4: (2 | 3) & (*2 | 3)
@@ -462,6 +462,92 @@
 		`,
 		out: `<0>{a: true, b: true, c: false, d: true, e: false, f: true}`,
 	}, {
+		desc: "bounds",
+		in: `
+			i1: >1 & 5
+			i2: (>=0 & <=10) & 5
+			i3: !=null & []
+			i4: !=2 & !=4
+
+
+			s1: >=0 & <=10 & !=1        // no simplification
+			s2: >=0 & <=10 & !=11       // >=0 & <=10
+			s3: >5 & !=5                // >5
+			s4: <10 & !=10              // <10
+			s5: !=2 & !=2
+
+			s10: >=0 & <=10 & <12 & >1   // >1  & <=10
+			s11: >0 & >=0 & <=12 & <12   // >0  & <12
+
+			s20: >=10 & <=10             // 10
+
+			s22:  >5 & <=6                // no simplification
+			s22a: >5 & (<=6 & int)       // 6
+			s22b: (int & >5) & <=6       // 6
+			s22c: >=5 & (<6 & int)       // 5
+			s22d: (int & >=5) & <6       // 5
+			s22e: (>=5 & <6) & int       // 5
+			s22f: int & (>=5 & <6)       // 5
+
+			s23: >0 & <2                 // no simplification
+			s23a: (>0 & <2) & int        // int & 1
+			s23b: int & (>0 & <2)        // int & 1
+			s23c: (int & >0) & <2        // int & 1
+			s23d: >0 & (int & <2)        // int & 1
+			s23e: >0.0 & <2.0            // no simplification
+
+			s30: >0 & int
+
+			e1: null & !=null
+			e2: !=null & null
+			e3: >1 & 1
+			e4: <0 & 0
+			e5: >1 & <0
+			e6: >11 & <11
+			e7: >=11 & <11
+			e8: >11 & <=11
+			e9: >"a" & <1
+		`,
+		out: `<0>{i1: 5, i2: 5, i3: [], i4: (!=2 & !=4), ` +
+
+			`s1: (>=0 & <=10 & !=1), ` +
+			`s2: (>=0 & <=10), ` +
+			`s3: >5, ` +
+			`s4: <10, ` +
+			`s5: !=2, ` +
+
+			`s10: (<=10 & >1), ` +
+			`s11: (>0 & <12), ` +
+
+			`s20: 10, ` +
+
+			`s22: (>5 & <=6), ` +
+			`s22a: 6, ` +
+			`s22b: 6, ` +
+			`s22c: 5, ` +
+			`s22d: 5, ` +
+			`s22e: 5, ` +
+			`s22f: 5, ` +
+
+			`s23: (>0 & <2), ` +
+			`s23a: 1, ` +
+			`s23b: 1, ` +
+			`s23c: 1, ` +
+			`s23d: 1, ` +
+			`s23e: (>0.0 & <2.0), ` +
+
+			`s30: >0, ` +
+
+			`e1: _|_((!=null & null):null excluded by !=null), ` +
+			`e2: _|_((!=null & null):null excluded by !=null), ` +
+			`e3: _|_((>1 & 1):1 not within bound >1), ` +
+			`e4: _|_((<0 & 0):0 not within bound <0), ` +
+			`e5: _|_(incompatible bounds >1 and <0), ` +
+			`e6: _|_(incompatible bounds >11 and <11), ` +
+			`e7: _|_(incompatible bounds >=11 and <11), ` +
+			`e8: _|_(incompatible bounds >11 and <=11), ` +
+			`e9: _|_((>"a" & <1):unsupported op &((string)*, (number)*))}`,
+	}, {
 		desc: "null coalescing",
 		in: `
 				a: null
@@ -553,40 +639,39 @@
 		in: `
 			l0: 3*[int]
 			l0: [1, 2, 3]
-			l1:(0..5)*[string]
+			l1: <=5*[string]
 			l1: ["a", "b"]
-			l2: (0..5)*[{ a: int }]
+			l2: <=5*[{ a: int }]
 			l2: [{a: 1}, {a: 2, b: 3}]
-			l3: (0..10)*[int]
-			l3: [1, 2, 3, ...]
 
-			s1: ((0..6)*[int])[2:3] // TODO: simplify 1*[int] to [int]
+			// TODO: work out a decent way to specify length ranges of lists.
+			// l3: <=10*[int]
+			// l3: [1, 2, 3, ...]
+
+			s1: (<=6*[int])[2:3] // TODO: simplify 1*[int] to [int]
 			s2: [0,2,3][1:2]
 
-			i1: ((0..6)*[int])[2]
+			i1: (<=6*[int])[2]
 			i2: [0,2,3][2]
 
 			t0: [...{a: 8}]
 			t0: [{}]
 
-			e0: (2..5)*[{}]
+			e0: >=2*[{}]
 			e0: [{}]
-
-			e1: 0.._*[...int]
 			`,
-		out: `<0>{l0: [1,2,3], l1: ["a","b"], l2: [<1>{a: 1},<2>{a: 2, b: 3}], l3: (3..10)*[int]([1,2,3, ...int]), s1: 1*[int], s2: [2], i1: int, i2: 3, t0: [<3>{a: 8}], e0: _|_(((2..5)*[<4>{}] & [<5>{}]):incompatible list lengths: value 1 not in range (2..5)), e1: [, ...int]}`,
+		out: `<0>{l0: [1,2,3], l1: ["a","b"], l2: [<1>{a: 1},<2>{a: 2, b: 3}], s1: 1*[int], s2: [2], i1: int, i2: 3, t0: [<3>{a: 8}], e0: _|_(([, ...<4>{}] & [<5>{}]):incompatible list lengths: 1 not within bound >=2)}`,
 	}, {
 		desc: "list arithmetic",
 		in: `
 			l0: 3*[1, 2, 3]
 			l1: 0*[1, 2, 3]
 			l2: 10*[]
-			l3: (0..2)*[]
-			l4: (0..2)*[int]
-			l5: (0..2)*(int*[int])
-			l6: 3*((3..4)*[int])
+			l3: <=2*[]
+			l4: <=2*[int]
+			l5: <=2*(int*[int])
 		`,
-		out: `<0>{l0: [1,2,3,1,2,3,1,2,3], l1: [], l2: [], l3: [], l4: (0..2)*[int], l5: (0..2)*[int], l6: (9..12)*[int]}`,
+		out: `<0>{l0: [1,2,3,1,2,3,1,2,3], l1: [], l2: [], l3: [], l4: <=2*[int], l5: <=2*[int]}`,
 	}, {
 		desc: "correct error messages",
 		// Tests that it is okay to partially evaluate structs.
@@ -694,98 +779,93 @@
 			`,
 		out: `<0>{a: <1>{<>: <2>(name: string)->int, k: 1}, b: <3>{<>: <4>(x: string)->(<5>{x: 0, y: (1 | int)} & <6>{}), v: <7>{x: 0, y: (1 | int)}, w: <8>{x: 0, y: (1 | int)}}, c: <9>{<>: <10>(Name: string)-><11>{name: <10>.Name, y: 1}, foo: <12>{name: "foo", y: 1}, bar: <13>{name: "bar", y: 1}}}`,
 	}, {
-		desc: "simple ranges",
-		in: `
-			a: 1..2
-			c: "a".."b"
-			d: (2+3)..(4+5)  // 5..9
-
-			s1: 1..1       // 1
-			s2: 1..2..3    // simplify (1..2)..3 to 1..3
-			s3: (1..10)..5 // This is okay!
-			s4: 5..(1..10) // This is okay!
-			s5: (0..(5..6))..(1..10)
-			`,
-		out: `<0>{a: (1..2), c: ("a".."b"), d: (5..9), s1: 1, s2: (1..3), s3: (1..5), s4: (5..10), s5: (0..10)}`,
-	}, {
 		desc: "range unification",
 		in: `
 			// with concrete values
-			a1: 1..5 & 3
-			a2: 1..5 & 1
-			a3: 1..5 & 5
-			a4: 1..5 & 6
-			a5: 1..5 & 0
+			a1: >=1 & <=5 & 3
+			a2: >=1 & <=5 & 1
+			a3: >=1 & <=5 & 5
+			a4: >=1 & <=5 & 6
+			a5: >=1 & <=5 & 0
 
-			a6: 3 & 1..5
-			a7: 1 & 1..5
-			a8: 5 & 1..5
-			a9: 6 & 1..5
-			a10: 0 & 1..5
+			a6: 3 & >=1 & <=5
+			a7: 1 & >=1 & <=5
+			a8: 5 & >=1 & <=5
+			a9: 6 & >=1 & <=5
+			a10: 0 & >=1 & <=5
 
 			// with ranges
-			b1: 1..5 & 1..5
-			b2: 1..5 & 1..1
-			b3: 1..5 & 5..5
-			b4: 1..5 & 2..3
-			b5: 1..5 & 3..9
-			b6: 1..5 & 5..9
-			b7: 1..5 & 6..9
+			b1: >=1 & <=5 & >=1 & <=5
+			b2: >=1 & <=5 & >=1 & <=1
+			b3: >=1 & <=5 & >=5 & <=5
+			b4: >=1 & <=5 & >=2 & <=3
+			b5: >=1 & <=5 & >=3 & <=9
+			b6: >=1 & <=5 & >=5 & <=9
+			b7: >=1 & <=5 & >=6 & <=9
 
-			b8: 1..5 & 1..5
-			b9: 1..1 & 1..5
-			b10: 5..5 & 1..5
-			b11: 2..3 & 1..5
-			b12: 3..9 & 1..5
-			b13: 5..9 & 1..5
-			b14: 6..9 & 1..5
+			b8: >=1 & <=5 & >=1 & <=5
+			b9: >=1 & <=1 & >=1 & <=5
+			b10: >=5 & <=5 & >=1 & <=5
+			b11: >=2 & <=3 & >=1 & <=5
+			b12: >=3 & <=9 & >=1 & <=5
+			b13: >=5 & <=9 & >=1 & <=5
+			b14: >=6 & <=9 & >=1 & <=5
 
 			// ranges with more general types
-			c1: int & 1..5
-			c2: 1..5 & int
-			c3: string & 1..5
-			c4: 1..5 & string
+			c1: int & >=1 & <=5
+			c2: >=1 & <=5 & int
+			c3: string & >=1 & <=5
+			c4: >=1 & <=5 & string
 
 			// other types
-			s1: "d" .. "z" & "e"
-			s2: "d" .. "z" & "ee"
+			s1: >="d" & <="z" & "e"
+			s2: >="d" & <="z" & "ee"
 
-			n1: number & 1..2
-			n2: int & 1.1 .. 1.3
-			n3: 1.0..3.0 & 2
-			n4: 0.0..0.1 & 0.09999
-			n5: 1..5 & 2.5
+			n1: number & >=1 & <=2
+			n2: int & >=1.1 & <=1.3
+			n3: >=1.0 & <=3.0 & 2
+			n4: >=0.0 & <=0.1 & 0.09999
+			n5: >=1 & <=5 & 2.5
 			`,
-		out: `<0>{a1: 3, a2: 1, a3: 5, a4: _|_(((1..5) & 6):value 6 not in range (1..5)), a5: _|_(((1..5) & 0):value 0 not in range (1..5)), a6: 3, a7: 1, a8: 5, a9: _|_(((1..5) & 6):value 6 not in range (1..5)), a10: _|_(((1..5) & 0):value 0 not in range (1..5)), b1: (1..5), b2: 1, b3: 5, b4: (2..3), b5: (3..5), b6: 5, b7: _|_(((1..5) & (6..9)):non-overlapping ranges (1..5) and (6..9)), b8: (1..5), b9: 1, b10: 5, b11: (2..3), b12: (3..5), b13: 5, b14: _|_(((6..9) & (1..5)):non-overlapping ranges (6..9) and (1..5)), c1: (1..5), c2: (1..5), c3: _|_((string & (1..5)):unsupported op &((string)*, (number)*)), c4: _|_(((1..5) & string):unsupported op &((number)*, (string)*)), s1: "e", s2: "ee", n1: (1..2), n2: _|_((int & (1.1..1.3)):unsupported op &((int)*, (float)*)), n3: 2, n4: 0.09999, n5: 2.5}`,
-	}, {
-		desc: "range arithmetic",
-		in: `
-			r0: (1..2) * (4..5)
-			r1: (1..2) * (-1..2)
-			r2: (1.0..2.0) * (-0.5..1.0)
-			r3: (1..2) + (4..5)
+		out: `<0>{` +
+			`a1: 3, ` +
+			`a2: 1, ` +
+			`a3: 5, ` +
+			`a4: _|_((<=5 & 6):6 not within bound <=5), ` +
+			`a5: _|_((>=1 & 0):0 not within bound >=1), ` +
+			`a6: 3, ` +
+			`a7: 1, ` +
+			`a8: 5, ` +
 
-			i0: (1..2) * 2
-			i1: (2..3) * -2
-			i2: (1..2) * 2
-			i3: (2..3) * -2
+			// TODO: improve error
+			`a9: _|_((6 & <=5):unsupported op &(number, (number)*)), ` +
+			`a10: _|_((0 & >=1):unsupported op &(number, (number)*)), ` +
 
-			t0: int * (1..2) // TODO: should be int
-			t1: (1..2) * int
-			t2: (1..2) * (0..int)
-			t3: (1..int) * (0..2)
-			t4: (1..int) * (-1..2)
-			t5: _ * (1..2)  // TODO: should be int
-
-			s0: (1..2) - (3..5)
-			s1: (1..2) - 1
-
-			str0: ("ab".."cd") + "ef"
-			str1: ("ab".."cd") + ("ef".."gh")
-			str2: ("ab".."cd") + string
-
-		`,
-		out: `<0>{r0: (4..10), r1: (-2..4), r2: (-1.00..2.00), r3: (5..7), i0: (2..4), i1: (-6..-4), i2: (2..4), i3: (-6..-4), t0: (int * (1..2)), t1: int, t2: (0..int), t3: (0..int), t4: int, t5: _|_((_ * (1..2)):binary operation on non-ground top value), s0: (-4..-1), s1: (0..1), str0: ("abef".."cdef"), str1: ("abef".."cdgh"), str2: ("ab".."cd")}`,
+			`b1: (>=1 & <=5), ` +
+			`b2: 1, ` +
+			`b3: 5, ` +
+			`b4: (>=2 & <=3), ` +
+			`b5: (>=3 & <=5), ` +
+			`b6: 5, ` +
+			`b7: _|_(incompatible bounds >=6 and <=5), ` +
+			`b8: (>=1 & <=5), ` +
+			`b9: 1, ` +
+			`b10: 5, ` +
+			`b11: (>=2 & <=3), ` +
+			`b12: (>=3 & <=5), ` +
+			`b13: 5, ` +
+			`b14: _|_(incompatible bounds >=6 and <=5), ` +
+			`c1: (>=1 & <=5), ` +
+			`c2: (<=5 & >=1), ` +
+			`c3: _|_((string & >=1):unsupported op &((string)*, (number)*)), ` +
+			`c4: _|_(((>=1 & <=5) & string):unsupported op &((number)*, (string)*)), ` +
+			`s1: "e", ` +
+			`s2: "ee", ` +
+			`n1: (>=1 & <=2), ` +
+			`n2: _|_((int & >=1.1):unsupported op &((int)*, (float)*)), ` +
+			`n3: 2, ` +
+			`n4: 0.09999, ` +
+			`n5: 2.5}`,
 	}, {
 		desc: "predefined ranges",
 		in: `
@@ -799,7 +879,7 @@
 			e1: 100_000
 		`,
 		out: `<0>{k1: 44, k2: -8000000000, ` +
-			`e1: _|_(((-32768..32767) & 100000):value 100000 not in range (-32768..32767))}`,
+			`e1: _|_((<=32767 & 100000):100000 not within bound <=32767)}`,
 	}, {
 		desc: "field comprehensions",
 		in: `
@@ -1070,18 +1150,18 @@
 	}, {
 		desc: "ips",
 		in: `
-		IP: 4*[ 0..255 ]
+		IP: 4*[ uint8 ]
 
 		Private:
-			*[ 192, 168, 0..255, 0..255 ] |
-			[ 10, 0..255, 0..255, 0..255] |
-			[ 172, 16..32, 0..255, 0..255 ]
+			*[ 192, 168, uint8, uint8 ] |
+			[ 10, uint8, uint8, uint8] |
+			[ 172, >=16 & <=32, uint8, uint8 ]
 
 		Inst: Private & [ _, 10, ... ]
 
 		MyIP: Inst & [_, _, 10, 10 ]
 		`,
-		out: `<0>{IP: 4*[(0..255)], Private: [192,168,(0..255),(0..255)], Inst: [10,10,(0..255),(0..255)], MyIP: [10,10,10,10]}`,
+		out: `<0>{IP: 4*[(>=0 & <=255)], Private: [192,168,(>=0 & <=255),(>=0 & <=255)], Inst: [10,10,(>=0 & <=255),(>=0 & <=255)], MyIP: [10,10,10,10]}`,
 	}, {
 		desc: "complex interaction of groundness",
 		in: `
diff --git a/cue/rewrite.go b/cue/rewrite.go
index 6e28522..479605c 100644
--- a/cue/rewrite.go
+++ b/cue/rewrite.go
@@ -83,13 +83,12 @@
 func (x *numLit) rewrite(ctx *context, fn rewriteFunc) value      { return x }
 func (x *durationLit) rewrite(ctx *context, fn rewriteFunc) value { return x }
 
-func (x *rangeLit) rewrite(ctx *context, fn rewriteFunc) value {
-	from := rewrite(ctx, x.from, fn)
-	to := rewrite(ctx, x.to, fn)
-	if from == x.from && to == x.to {
+func (x *bound) rewrite(ctx *context, fn rewriteFunc) value {
+	v := rewrite(ctx, x.value, fn)
+	if v == x.value {
 		return x
 	}
-	return &rangeLit{x.baseValue, from, to}
+	return &bound{x.baseValue, x.op, v}
 }
 
 func (x *interpolation) rewrite(ctx *context, fn rewriteFunc) value {
@@ -177,6 +176,11 @@
 	return &binaryExpr{x.baseValue, x.op, left, right}
 }
 
+func (x *unification) rewrite(ctx *context, fn rewriteFunc) value {
+	// Can a unification ever be rewritten as it is a post-evaluation type?
+	panic("cue: unification only used post-evaluation")
+}
+
 func (x *disjunction) rewrite(ctx *context, fn rewriteFunc) value {
 	values := make([]dValue, len(x.values))
 	changed := false
diff --git a/cue/scanner/scanner.go b/cue/scanner/scanner.go
index f5f0881..5b8fc65 100644
--- a/cue/scanner/scanner.go
+++ b/cue/scanner/scanner.go
@@ -730,7 +730,7 @@
 					s.next()
 					tok = token.ELLIPSIS
 				} else {
-					tok = token.RANGE
+					s.error(s.file.Offset(pos), "illegal token '..'; expected '.'")
 				}
 			} else {
 				tok = token.PERIOD
diff --git a/cue/scanner/scanner_test.go b/cue/scanner/scanner_test.go
index 45e73a5..d32b057 100644
--- a/cue/scanner/scanner_test.go
+++ b/cue/scanner/scanner_test.go
@@ -134,7 +134,6 @@
 	{token.NEQ, "!=", operator},
 	{token.LEQ, "<=", operator},
 	{token.GEQ, ">=", operator},
-	{token.RANGE, "..", operator},
 	{token.ELLIPSIS, "...", operator},
 
 	{token.LPAREN, "(", operator},
diff --git a/cue/subsume.go b/cue/subsume.go
index fccc07c..f4d9a18 100644
--- a/cue/subsume.go
+++ b/cue/subsume.go
@@ -62,6 +62,28 @@
 		// 	// no resolution if references are in play.
 		// 	return false, false
 	}
+	switch lt := lt.(type) {
+	case *unification:
+		if _, ok := gt.(*unification); !ok {
+			for _, x := range lt.values {
+				if subsumes(ctx, gt, x, mode) {
+					return true
+				}
+			}
+			return false
+		}
+
+	case *disjunction:
+		if _, ok := gt.(*disjunction); !ok {
+			for _, x := range lt.values {
+				if !subsumes(ctx, gt, x.val, mode) {
+					return false
+				}
+			}
+			return true
+		}
+	}
+
 	return gt.subsumesImpl(ctx, lt, mode)
 }
 
@@ -97,38 +119,80 @@
 	return true
 }
 
-func (x *rangeLit) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
-	k := unifyType(x.from.kind(), x.to.kind())
-	if k.isDone() && k.isGround() {
-		switch y := v.(type) {
-		case *rangeLit:
-			// structural equivalence
-			return subsumes(ctx, x.from, y.from, 0) && subsumes(ctx, x.to, y.to, 0)
-		case *numLit, *stringLit, *durationLit:
-			kv := v.kind()
-			k, _ := matchBinOpKind(opAdd, k, kv)
-			if k != kv {
+func (x *bound) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
+	if isBottom(v) {
+		return true
+	}
+	kx := x.value.kind()
+	if !kx.isDone() || !kx.isGround() {
+		return false
+	}
+
+	switch y := v.(type) {
+	case *bound:
+		if ky := y.value.kind(); ky.isDone() && ky.isGround() {
+			if (kx&ky)&^kx != 0 {
 				return false
 			}
-			v := v.(evaluated)
-			if x.from != nil {
-				from := minNumRaw(x.from)
-				if !from.kind().isGround() {
-					return subsumes(ctx, from, v, 0) // false negative is okay
+			// x subsumes y if
+			// x: >= a, y: >= b ==> a <= b
+			// x: >= a, y: >  b ==> a <= b
+			// x: >  a, y: >  b ==> a <= b
+			// x: >  a, y: >= b ==> a < b
+			//
+			// x: <= a, y: <= b ==> a >= b
+			//
+			// x: != a, y: != b ==> a != b
+			//
+			// false if types or op direction doesn't match
+
+			xv := x.value.(evaluated)
+			yv := y.value.(evaluated)
+			switch x.op {
+			case opGtr:
+				if y.op == opGeq {
+					return test(ctx, x, opLss, xv, yv)
 				}
-				return leq(ctx, x, x.from.(evaluated), v)
-			}
-			if x.to != nil {
-				to := minNumRaw(x.to)
-				if !to.kind().isGround() {
-					return subsumes(ctx, to, v, 0) // false negative is okay
+				fallthrough
+			case opGeq:
+				if y.op == opGtr || y.op == opGeq {
+					return test(ctx, x, opLeq, xv, yv)
 				}
-				return leq(ctx, x, v, x.to.(evaluated))
+			case opLss:
+				if y.op == opLeq {
+					return test(ctx, x, opGtr, xv, yv)
+				}
+				fallthrough
+			case opLeq:
+				if y.op == opLss || y.op == opLeq {
+					return test(ctx, x, opGeq, xv, yv)
+				}
+			case opNeq:
+				switch y.op {
+				case opNeq:
+					return test(ctx, x, opEql, xv, yv)
+				case opGeq:
+					return test(ctx, x, opLss, xv, yv)
+				case opGtr:
+					return test(ctx, x, opLeq, xv, yv)
+				case opLss:
+					return test(ctx, x, opGeq, xv, yv)
+				case opLeq:
+					return test(ctx, x, opGtr, xv, yv)
+				}
+
+			default:
+				// opNeq already handled above.
+				panic("cue: undefined bound mode")
 			}
-			return true
 		}
+		// structural equivalence
+		return false
+
+	case *numLit, *stringLit, *durationLit, *boolLit:
+		return test(ctx, x, x.op, y.(evaluated), x.value.(evaluated))
 	}
-	return isBottom(v)
+	return false
 }
 
 func (x *nullLit) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
@@ -207,12 +271,38 @@
 	return isBottom(v)
 }
 
+func (x *unification) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
+	if y, ok := v.(*unification); ok {
+		// A unification subsumes another unification if for all values a in x
+		// there is a value b in y such that a subsumes b.
+		//
+		// This assumes overlapping ranges in disjunctions are merged.If this is
+		// not the case, subsumes will return a false negative, which is
+		// allowed.
+	outer:
+		for _, vx := range x.values {
+			for _, vy := range y.values {
+				if subsumes(ctx, vx, vy, mode) {
+					continue outer
+				}
+			}
+			return false
+		}
+		return true
+	}
+	subsumed := true
+	for _, vx := range x.values {
+		subsumed = subsumed && subsumes(ctx, vx, v, mode)
+	}
+	return subsumed
+}
+
 // subsumes for disjunction is logically precise. However, just like with
 // structural subsumption, it should not have to be called after evaluation.
 func (x *disjunction) subsumesImpl(ctx *context, v value, mode subsumeMode) bool {
 	// A disjunction subsumes another disjunction if all values of v are
-	// subsumed by the values of x, and default values in v are subsumed by the
-	// default values of x.
+	// subsumed by any of the values of x, and default values in v are subsumed
+	// by the default values of x.
 	//
 	// This assumes that overlapping ranges in x are merged. If this is not the
 	// case, subsumes will return a false negative, which is allowed.
diff --git a/cue/subsume_test.go b/cue/subsume_test.go
index b48f8ef..021fd54 100644
--- a/cue/subsume_test.go
+++ b/cue/subsume_test.go
@@ -167,7 +167,7 @@
 		97: {subsumes: true, in: `a: number + number, b: int + int`},
 		// TODO: allow subsumption of unevaluated values?
 		// TODO: may be false if we allow arithmetic on incomplete values.
-		98: {subsumes: true, in: `a: int + int, b: int * int`},
+		98: {subsumes: false, in: `a: int + int, b: int * int`},
 
 		99:  {subsumes: true, in: `a: !int, b: !int`},
 		100: {subsumes: true, in: `a: !number, b: !int`},
@@ -175,8 +175,8 @@
 		// true because both evaluate to bottom
 		101: {subsumes: true, in: `a: !int, b: !number`},
 		// TODO: allow subsumption of unevaluated values?
-		// true because both evaluate to bottom
-		102: {subsumes: true, in: `a: int + int, b: !number`},
+		// May be true because both evaluate to bottom. false is always allowed.
+		102: {subsumes: false, in: `a: int + int, b: !number`},
 		// TODO: allow subsumption of unevaluated values?
 		// true because both evaluate to bool
 		103: {subsumes: true, in: `a: !bool, b: bool`},
@@ -256,7 +256,52 @@
 		150: {subsumes: false, in: `a: number | *1, b: number | *2`},
 		151: {subsumes: true, in: `a: number | *2, b: number | *2`},
 		152: {subsumes: true, in: `a: int | *float, b: int | *2.0`},
-		154: {subsumes: true, in: `a: number, b: number | *2`},
+		153: {subsumes: true, in: `a: int | *2, b: int | *2.0`},
+		154: {subsumes: true, in: `a: number | *2 | *3, b: number | *2`},
+		155: {subsumes: true, in: `a: number, b: number | *2`},
+
+		// Bounds
+		170: {subsumes: true, in: `a: >=2, b: >=2`},
+		171: {subsumes: true, in: `a: >=1, b: >=2`},
+		172: {subsumes: true, in: `a: >0, b: >=2`},
+		173: {subsumes: true, in: `a: >1, b: >1`},
+		174: {subsumes: true, in: `a: >=1, b: >1`},
+		175: {subsumes: false, in: `a: >1, b: >=1`},
+		176: {subsumes: true, in: `a: >=1, b: >=1`},
+		177: {subsumes: true, in: `a: <1, b: <1`},
+		178: {subsumes: true, in: `a: <=1, b: <1`},
+		179: {subsumes: false, in: `a: <1, b: <=1`},
+		180: {subsumes: true, in: `a: <=1, b: <=1`},
+
+		181: {subsumes: true, in: `a: !=1, b: !=1`},
+		182: {subsumes: false, in: `a: !=1, b: !=2`},
+
+		183: {subsumes: false, in: `a: !=1, b: <=1`},
+		184: {subsumes: true, in: `a: !=1, b: <1`},
+		185: {subsumes: false, in: `a: !=1, b: >=1`},
+		186: {subsumes: true, in: `a: !=1, b: <1`},
+
+		187: {subsumes: true, in: `a: !=1, b: <=0`},
+		188: {subsumes: true, in: `a: !=1, b: >=2`},
+		189: {subsumes: true, in: `a: !=1, b: >1`},
+
+		195: {subsumes: false, in: `a: >=2, b: !=2`},
+		196: {subsumes: false, in: `a: >2, b: !=2`},
+		197: {subsumes: false, in: `a: <2, b: !=2`},
+		198: {subsumes: false, in: `a: <=2, b: !=2`},
+
+		// Conjunctions
+		200: {subsumes: true, in: `a: >0, b: >=2 & <=100`},
+		201: {subsumes: false, in: `a: >0, b: >=0 & <=100`},
+
+		210: {subsumes: true, in: `a: >=0 & <=100, b: 10`},
+		211: {subsumes: true, in: `a: >=0 & <=100, b: >=0 & <=100`},
+		212: {subsumes: false, in: `a: !=2 & !=4, b: >3`},
+		213: {subsumes: true, in: `a: !=2 & !=4, b: >5`},
+
+		// Disjunctions
+		230: {subsumes: true, in: `a: >5, b: >10 | 8`},
+		231: {subsumes: false, in: `a: >8, b: >10 | 8`},
 	}
 
 	re := regexp.MustCompile(`a: (.*).*b: ([^\n]*)`)
diff --git a/cue/token/token.go b/cue/token/token.go
index edc9050..5238bc9 100644
--- a/cue/token/token.go
+++ b/cue/token/token.go
@@ -77,7 +77,6 @@
 	LBRACE   // {
 	COMMA    // ,
 	PERIOD   // .
-	RANGE    // ..
 	ELLIPSIS // ...
 
 	RPAREN    // )
@@ -148,7 +147,6 @@
 	LBRACE:   "{",
 	COMMA:    ",",
 	PERIOD:   ".",
-	RANGE:    "..",
 	ELLIPSIS: "...",
 
 	RPAREN:    ")",
@@ -222,8 +220,6 @@
 		return 6
 	case MUL, QUO, REM, IDIV, IMOD, IQUO, IREM:
 		return 7
-	case RANGE:
-		return 8
 	}
 	return lowestPrec
 }
diff --git a/cue/types.go b/cue/types.go
index 8f669e3..7cecd8c 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -494,8 +494,8 @@
 }
 
 // Kind returns the kind of value. It returns BottomKind for atomic values that
-// are not concrete. For instance, it will return BottomKind for the range
-// 0..5.
+// are not concrete. For instance, it will return BottomKind for the bounds
+// >=0.
 func (v Value) Kind() Kind {
 	k := v.eval(v.ctx()).kind()
 	if k.isGround() {
diff --git a/cue/types_test.go b/cue/types_test.go
index fca29ae..4f43796 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -107,7 +107,7 @@
 		kind:           NumberKind,
 		incompleteKind: NumberKind,
 	}, {
-		value:          `0..5`,
+		value:          `>=0 & <5`,
 		kind:           BottomKind,
 		incompleteKind: NumberKind,
 	}, {
@@ -513,7 +513,7 @@
 		value: `[1,2,3]`,
 		res:   "[1,2,3,]",
 	}, {
-		value: `(0..5)*[1,2,3, ...int]`,
+		value: `>=5*[1,2,3, ...int]`,
 		res:   "[1,2,3,]",
 	}, {
 		value: `[x for x in y if x > 1]
@@ -998,7 +998,7 @@
 		value: `[int]`,
 		err:   `cannot convert incomplete value`,
 	}, {
-		value: `((0..3) * [1, 2])`,
+		value: `(>=3 * [1, 2])`,
 		json:  `[1,2]`,
 	}, {
 		value: `{}`,
@@ -1071,7 +1071,7 @@
 		value: `[int]`,
 		out:   `[int]`,
 	}, {
-		value: `((0..3) * [1, 2])`,
+		value: `(>=3 * [1, 2])`,
 		out:   `[1,2]`,
 	}, {
 		value: `{}`,
diff --git a/cue/value.go b/cue/value.go
index 945fe30..897af40 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -354,6 +354,8 @@
 
 var ten = big.NewInt(10)
 
+var one = parseInt(intKind, "1")
+
 func (x *numLit) kind() kind       { return x.k }
 func (x *numLit) strValue() string { return x.v.String() }
 
@@ -404,26 +406,30 @@
 func (x *durationLit) kind() kind       { return durationKind }
 func (x *durationLit) strValue() string { return x.d.String() }
 
-type rangeLit struct {
+type bound struct {
 	baseValue
-	from, to value
+	op    op // opNeq, opLss, opLeq, opGeq, or opGtr
+	value value
 }
 
-func (x *rangeLit) kind() kind {
-	return unifyType(x.from.kind(), x.to.kind()) | nonGround
+func (x *bound) kind() kind {
+	k := x.value.kind()
+	if x.op == opNeq && k&atomKind == nullKind {
+		k = typeKinds &^ nullKind
+	}
+	return k | nonGround
 }
 
-func mkIntRange(a, b string) *rangeLit {
-	from := parseInt(intKind, a)
-	to := parseInt(intKind, b)
-	return &rangeLit{
-		binSrc(token.NoPos, opRange, from, to),
-		from,
-		to,
+func mkIntRange(a, b string) evaluated {
+	from := &bound{op: opGeq, value: parseInt(intKind, a)}
+	to := &bound{op: opLeq, value: parseInt(intKind, b)}
+	return &unification{
+		binSrc(token.NoPos, opUnify, from, to),
+		[]evaluated{from, to},
 	}
 }
 
-var predefinedRanges = map[string]*rangeLit{
+var predefinedRanges = map[string]evaluated{
 	"rune":  mkIntRange("0", strconv.Itoa(0x10FFFF)),
 	"int8":  mkIntRange("-128", "127"),
 	"int16": mkIntRange("-32768", "32767"),
@@ -435,6 +441,7 @@
 
 	// Do not include an alias for "byte", as it would be too easily confused
 	// with the builtin "bytes".
+	"uint":    &bound{op: opGeq, value: parseInt(intKind, "0")},
 	"uint8":   mkIntRange("0", "255"),
 	"uint16":  mkIntRange("0", "65535"),
 	"uint32":  mkIntRange("0", "4294967295"),
@@ -514,7 +521,7 @@
 // lo and hi must be nil or a ground integer.
 func (x *list) slice(ctx *context, lo, hi *numLit) evaluated {
 	a := x.a
-	max := maxNum(x.len.(evaluated))
+	max := maxNum(x.len).evalPartial(ctx)
 	if hi != nil {
 		n := hi.intValue(ctx)
 		if n < 0 {
@@ -956,8 +963,22 @@
 	return kind | nonGround
 }
 
-// TODO: make disjunction a binOp, but translate disjunctions into
-// arrays, or at least linked lists.
+// unification collects evaluated values that are not mutually exclusive
+// but cannot be represented as a single value. It allows doing the bookkeeping
+// on accumulating conjunctions, simplifying them along the way, until they do
+// resolve into a single value.
+type unification struct {
+	baseValue
+	values []evaluated
+}
+
+func (x *unification) kind() kind {
+	k := topKind
+	for _, v := range x.values {
+		k &= v.kind()
+	}
+	return k | nonGround
+}
 
 type disjunction struct {
 	baseValue