cue: implement regexp support

add support for both unary and binary operators

Change-Id: I6d470b93b38fc30f1b682fcee34229acd7945ea5
diff --git a/cue/ast.go b/cue/ast.go
index 7b067bd..e1303ea 100644
--- a/cue/ast.go
+++ b/cue/ast.go
@@ -464,7 +464,8 @@
 				tokenMap[n.Op],
 				v.walk(n.X),
 			}
-		case token.GEQ, token.GTR, token.LSS, token.LEQ, token.NEQ:
+		case token.GEQ, token.GTR, token.LSS, token.LEQ,
+			token.NEQ, token.MAT, token.NMAT:
 			value = &bound{
 				newExpr(n),
 				tokenMap[n.Op],
diff --git a/cue/binop.go b/cue/binop.go
index 62fe166..c534fc9 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -17,6 +17,7 @@
 import (
 	"bytes"
 	"math/big"
+	"regexp"
 	"sort"
 	"strings"
 	"time"
@@ -259,12 +260,14 @@
 		pos = r.Pos()
 	}
 	e := mkBin(ctx, pos, opUnify, r, v)
-	if r.op == opNeq {
-		const msgInRange = "%v excluded by %v"
-		return ctx.mkErr(e, msgInRange, debugStr(ctx, v), debugStr(ctx, r))
+	msg := "%v not within bound %v"
+	switch r.op {
+	case opNeq, opNMat:
+		msg = "%v excluded by %v"
+	case opMat:
+		msg = "%v does not match %v"
 	}
-	const msgInRange = "%v not within bound %v"
-	return ctx.mkErr(e, msgInRange, debugStr(ctx, v), debugStr(ctx, r))
+	return ctx.mkErr(e, msg, debugStr(ctx, v), debugStr(ctx, r))
 }
 
 func opInfo(op op) (cmp op, norm int) {
@@ -279,6 +282,10 @@
 		return opLss, -1
 	case opNeq:
 		return opNeq, 0
+	case opMat:
+		return opMat, 2
+	case opNMat:
+		return opNMat, 3
 	}
 	panic("cue: unreachable")
 }
@@ -312,11 +319,11 @@
 
 			switch {
 			case xCat == yCat:
-				if x.op == opNeq {
+				if x.op == opNeq || x.op == opMat || x.op == opNMat {
 					if test(ctx, x, opEql, xv, yv) {
 						return x
 					}
-					break
+					break // unify the two bounds
 				}
 
 				// xCat == yCat && x.op != opNeq
@@ -416,6 +423,11 @@
 						debugStr(ctx, x), debugStr(ctx, y))
 				}
 
+			case x.op == opNeq:
+				if !test(ctx, x, y.op, xv, yv) {
+					return y
+				}
+
 			case y.op == opNeq:
 				if !test(ctx, x, x.op, yv, xv) {
 					return x
@@ -610,9 +622,21 @@
 			}
 			return x
 		case opLss, opLeq, opEql, opNeq, opGeq, opGtr:
-			return cmpTonode(src, op, strings.Compare(string(x.str), string(str)))
+			return cmpTonode(src, op, strings.Compare(x.str, str))
 		case opAdd:
 			return &stringLit{binSrc(src.Pos(), op, x, other), x.str + str}
+		case opMat:
+			b, err := regexp.MatchString(str, x.str)
+			if err != nil {
+				return ctx.mkErr(src, "error parsing regexp: %v", err)
+			}
+			return boolTonode(src, b)
+		case opNMat:
+			b, err := regexp.MatchString(str, x.str)
+			if err != nil {
+				return ctx.mkErr(src, "error parsing regexp: %v", err)
+			}
+			return boolTonode(src, !b)
 		}
 	}
 	return ctx.mkIncompatible(src, op, x, other)
diff --git a/cue/kind.go b/cue/kind.go
index a35e886..57b9928 100644
--- a/cue/kind.go
+++ b/cue/kind.go
@@ -247,7 +247,7 @@
 		if u.isAnyOf(boolKind) {
 			return boolKind | catBits, invert
 		}
-	case opEql, opNeq:
+	case opEql, opNeq, opMat, opNMat:
 		if u.isAnyOf(fixedKinds) {
 			return boolKind | catBits, false
 		}
diff --git a/cue/op.go b/cue/op.go
index 9d9c0a8..258343a 100644
--- a/cue/op.go
+++ b/cue/op.go
@@ -51,6 +51,8 @@
 
 	opEql
 	opNeq
+	opMat
+	opNMat
 
 	opLss
 	opGtr
@@ -79,8 +81,10 @@
 	opLor:  "||",
 	opNot:  "!",
 
-	opEql: "==",
-	opNeq: "!=",
+	opEql:  "==",
+	opNeq:  "!=",
+	opMat:  "=~",
+	opNMat: "!~",
 
 	opLss: "<",
 	opGtr: ">",
@@ -124,9 +128,11 @@
 	token.GTR: opGtr, // >
 	token.NOT: opNot, // !
 
-	token.NEQ: opNeq, // !=
-	token.LEQ: opLeq, // <=
-	token.GEQ: opGeq, // >=
+	token.NEQ:  opNeq,  // !=
+	token.LEQ:  opLeq,  // <=
+	token.GEQ:  opGeq,  // >=
+	token.MAT:  opMat,  // =~
+	token.NMAT: opNMat, // !~
 }
 
 var opMap = map[op]token.Token{}
diff --git a/cue/parser/parser.go b/cue/parser/parser.go
index b46fa25..9d0d308 100644
--- a/cue/parser/parser.go
+++ b/cue/parser/parser.go
@@ -1104,7 +1104,8 @@
 
 	switch p.tok {
 	case token.ADD, token.SUB, token.NOT, token.MUL,
-		token.NEQ, token.LSS, token.LEQ, token.GEQ, token.GTR:
+		token.LSS, token.LEQ, token.GEQ, token.GTR,
+		token.NEQ, token.MAT, token.NMAT:
 		pos, op := p.pos, p.tok
 		c := p.openComments()
 		p.next()
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index bfac4ee..b6fbaa6 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -101,6 +101,46 @@
 			`,
 		out: `<0>{a: _|_(from source), b: _|_(from source), c: true, d: false, e: true}`,
 	}, {
+		desc: "regexp",
+		in: `
+			c1: "a" =~ "a"
+			c2: "foo" =~ "[a-z]{3}"
+			c3: "foo" =~ "[a-z]{4}"
+			c4: "foo" !~ "[a-z]{4}"
+
+			b1: =~ "a"
+			b1: "a"
+			b2: =~ "[a-z]{3}"
+			b2: "foo"
+			b3: =~ "[a-z]{4}"
+			b3: "foo"
+			b4: !~ "[a-z]{4}"
+			b4: "foo"
+
+			s1: != "b" & =~"c"      // =~"c"
+			s2: != "b" & =~"[a-z]"  // != "b" & =~"[a-z]"
+
+			e1: "foo" =~ 1
+			e2: "foo" !~ true
+			e3: != "a" & <5
+		`,
+		out: `<0>{c1: true, ` +
+			`c2: true, ` +
+			`c3: false, ` +
+			`c4: true, ` +
+
+			`b1: "a", ` +
+			`b2: "foo", ` +
+			`b3: _|_((=~"[a-z]{4}" & "foo"):"foo" does not match =~"[a-z]{4}"), ` +
+			`b4: "foo", ` +
+
+			`s1: =~"c", ` +
+			`s2: (!="b" & =~"[a-z]"), ` +
+
+			`e1: _|_(("foo" =~ 1):unsupported op =~(string, number)), ` +
+			`e2: _|_(("foo" !~ true):unsupported op !~(string, bool)), ` +
+			`e3: _|_((!="a" & <5):unsupported op &((string)*, (number)*))}`,
+	}, {
 		desc: "arithmetic",
 		in: `
 			sum: -1 + +2        // 1
@@ -476,6 +516,12 @@
 			s4: <10 & !=10              // <10
 			s5: !=2 & !=2
 
+			// TODO: could change inequality
+			s6: !=2 & >=2
+			s7: >=2 & !=2
+
+			s8: !=5 & >5
+
 			s10: >=0 & <=10 & <12 & >1   // >1  & <=10
 			s11: >0 & >=0 & <=12 & <12   // >0  & <12
 
@@ -516,6 +562,11 @@
 			`s4: <10, ` +
 			`s5: !=2, ` +
 
+			`s6: (!=2 & >=2), ` +
+			`s7: (>=2 & !=2), ` +
+
+			`s8: >5, ` +
+
 			`s10: (<=10 & >1), ` +
 			`s11: (>0 & <12), ` +
 
diff --git a/cue/scanner/scanner.go b/cue/scanner/scanner.go
index 5b8fc65..aab40d5 100644
--- a/cue/scanner/scanner.go
+++ b/cue/scanner/scanner.go
@@ -793,9 +793,19 @@
 		case '>':
 			tok = s.switch2(token.GTR, token.GEQ)
 		case '=':
-			tok = s.switch2(token.BIND, token.EQL)
+			if s.ch == '~' {
+				s.next()
+				tok = token.MAT
+			} else {
+				tok = s.switch2(token.BIND, token.EQL)
+			}
 		case '!':
-			tok = s.switch2(token.NOT, token.NEQ)
+			if s.ch == '~' {
+				s.next()
+				tok = token.NMAT
+			} else {
+				tok = s.switch2(token.NOT, token.NEQ)
+			}
 		case '&':
 			switch s.ch {
 			case '&':
diff --git a/cue/scanner/scanner_test.go b/cue/scanner/scanner_test.go
index d32b057..53d1bb0 100644
--- a/cue/scanner/scanner_test.go
+++ b/cue/scanner/scanner_test.go
@@ -136,6 +136,9 @@
 	{token.GEQ, ">=", operator},
 	{token.ELLIPSIS, "...", operator},
 
+	{token.MAT, "=~", operator},
+	{token.NMAT, "!~", operator},
+
 	{token.LPAREN, "(", operator},
 	{token.LBRACK, "[", operator},
 	{token.LBRACE, "{", operator},
diff --git a/cue/token/token.go b/cue/token/token.go
index 5238bc9..7db3329 100644
--- a/cue/token/token.go
+++ b/cue/token/token.go
@@ -72,6 +72,9 @@
 	LEQ // <=
 	GEQ // >=
 
+	MAT  // =~
+	NMAT // !~
+
 	LPAREN   // (
 	LBRACK   // [
 	LBRACE   // {
@@ -142,6 +145,9 @@
 	LEQ: "<=",
 	GEQ: ">=",
 
+	MAT:  "=~",
+	NMAT: "!~",
+
 	LPAREN:   "(",
 	LBRACK:   "[",
 	LBRACE:   "{",
@@ -214,7 +220,7 @@
 		return 3
 	case LAND:
 		return 4
-	case EQL, NEQ, LSS, LEQ, GTR, GEQ:
+	case EQL, NEQ, LSS, LEQ, GTR, GEQ, MAT, NMAT:
 		return 5
 	case ADD, SUB:
 		return 6