pkg/net: added package

Support:
- various IP types
- FQDNs

Also supports:
- use of single-param builtins as types
  (consistent with curried builtins).
- fixed bug in interpretation of top
- allow unexported functions to be used by builtins

Change-Id: I3568f2383510a904e00d4cf941c753efd03679ce
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2664
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/binop.go b/cue/binop.go
index 3f081a4..1ac8c1b 100644
--- a/cue/binop.go
+++ b/cue/binop.go
@@ -53,6 +53,9 @@
 		return right
 	}
 
+	left = convertBuiltin(left)
+	right = convertBuiltin(right)
+
 	leftKind := left.kind()
 	rightKind := right.kind()
 	kind, invert, msg := matchBinOpKind(op, leftKind, rightKind)
@@ -125,7 +128,7 @@
 		v := right.binOp(ctx, src, op, left)
 		// Return the original failure if both fail, as this will result in
 		// better error messages.
-		if !isBottom(v) {
+		if !isBottom(v) || isCustom(v) {
 			return v
 		}
 	}
diff --git a/cue/builtin.go b/cue/builtin.go
index 939c3d1..89b1028 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -195,6 +195,14 @@
 	return fmt.Sprintf("%s.%s", ctx.labelStr(x.pkg), x.Name)
 }
 
+func convertBuiltin(v evaluated) evaluated {
+	x, ok := v.(*builtin)
+	if ok && len(x.Params) == 1 && x.Result == boolKind {
+		return &customValidator{v.base(), []evaluated{}, x}
+	}
+	return v
+}
+
 func (x *builtin) call(ctx *context, src source, args ...evaluated) (ret value) {
 	if x.Func == nil {
 		return ctx.mkErr(x, "builtin %s is not a function", x.name(ctx))
diff --git a/cue/builtin_test.go b/cue/builtin_test.go
index fd2a48d..b2a487f 100644
--- a/cue/builtin_test.go
+++ b/cue/builtin_test.go
@@ -186,6 +186,57 @@
 		test("encoding/yaml", `yaml.MarshalStream([{a: 1}, {b: 2}])`),
 		`"a: 1\n---\nb: 2\n"`,
 	}, {
+		test("net", `net.FQDN & "foo.bar."`),
+		`"foo.bar."`,
+	}, {
+		test("net", `net.FQDN("foo.bararararararararararararararararararararararararararararararararara")`),
+		`false`,
+	}, {
+		test("net", `net.SplitHostPort("[::%lo0]:80")`),
+		`["::%lo0","80"]`,
+	}, {
+		test("net", `net.JoinHostPort("example.com", "80")`),
+		`"example.com:80"`,
+	}, {
+		test("net", `net.JoinHostPort("2001:db8::1", 80)`),
+		`"[2001:db8::1]:80"`,
+	}, {
+		test("net", `net.JoinHostPort([192,30,4,2], 80)`),
+		`"192.30.4.2:80"`,
+	}, {
+		test("net", `net.JoinHostPort([192,30,4], 80)`),
+		`_|_(error in call to net.JoinHostPort: invalid host [192,30,4])`,
+	}, {
+		test("net", `net.IP("23.23.23.23")`),
+		`true`,
+	}, {
+		test("net", `net.IPv4 & "23.23.23.2333"`),
+		`_|_(invalid value "23.23.23.2333" (does not satisfy net.IPv4()))`,
+	}, {
+		test("net", `net.IP("23.23.23.23")`),
+		`true`,
+	}, {
+		test("net", `net.IP("2001:db8::1")`),
+		`true`,
+	}, {
+		test("net", `net.IPv4("2001:db8::1")`),
+		`false`,
+	}, {
+		test("net", `net.IPv4() & "ff02::1:3"`),
+		`_|_(invalid value "ff02::1:3" (does not satisfy net.IPv4()))`,
+	}, {
+		test("net", `net.LoopbackIP([127, 0, 0, 1])`),
+		`true`,
+	}, {
+		test("net", `net.LoopbackIP("127.0.0.1")`),
+		`true`,
+	}, {
+		test("net", `net.ToIP4("127.0.0.1")`),
+		`[127,0,0,1]`,
+	}, {
+		test("net", `net.ToIP16("127.0.0.1")`),
+		`[0,0,0,0,0,0,0,0,0,0,255,255,127,0,0,1]`,
+	}, {
 		test("strings", `strings.ToCamel("AlphaBeta")`),
 		`"alphaBeta"`,
 	}, {
diff --git a/cue/builtins.go b/cue/builtins.go
index b7917a9..48c2c2d 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -18,6 +18,7 @@
 	"math"
 	"math/big"
 	"math/bits"
+	"net"
 	"path"
 	"regexp"
 	"sort"
@@ -26,12 +27,14 @@
 	"text/tabwriter"
 	"text/template"
 	"unicode"
+	"unicode/utf8"
 
 	"cuelang.org/go/cue/literal"
 	"cuelang.org/go/cue/parser"
 	"cuelang.org/go/internal/third_party/yaml"
 	"github.com/cockroachdb/apd/v2"
 	goyaml "github.com/ghodss/yaml"
+	"golang.org/x/net/idna"
 )
 
 func init() {
@@ -42,6 +45,67 @@
 
 var mulContext = apd.BaseContext.WithPrecision(1)
 
+var idnaProfile = idna.New(
+	idna.ValidateLabels(true),
+	idna.VerifyDNSLength(true),
+	idna.StrictDomainName(true),
+)
+
+func netGetIP(ip Value) (goip net.IP) {
+	switch ip.Kind() {
+	case StringKind:
+		s, err := ip.String()
+		if err != nil {
+			return nil
+		}
+		goip := net.ParseIP(s)
+		if goip == nil {
+			return nil
+		}
+		return goip
+
+	case BytesKind:
+		b, err := ip.Bytes()
+		if err != nil {
+			return nil
+		}
+		goip := net.ParseIP(string(b))
+		if goip == nil {
+			return nil
+		}
+		return goip
+
+	case ListKind:
+		iter, err := ip.List()
+		if err != nil {
+			return nil
+		}
+		for iter.Next() {
+			v, err := iter.Value().Int64()
+			if err != nil {
+				return nil
+			}
+			if v < 0 || 255 < v {
+				return nil
+			}
+			goip = append(goip, byte(v))
+		}
+		return goip
+
+	default:
+
+		return nil
+	}
+}
+
+func netToList(ip net.IP) []uint {
+	a := make([]uint, len(ip))
+	for i, p := range ip {
+		a[i] = uint(p)
+	}
+	return a
+}
+
 var split = path.Split
 
 var pathClean = path.Clean
@@ -1324,6 +1388,244 @@
 			},
 		}},
 	},
+	"net": &builtinPkg{
+		native: []*builtin{{
+			Name:   "SplitHostPort",
+			Params: []kind{stringKind},
+			Result: listKind,
+			Func: func(c *callCtxt) {
+				s := c.string(0)
+				c.ret, c.err = func() (interface{}, error) {
+					host, port, err := net.SplitHostPort(s)
+					if err != nil {
+						return nil, err
+					}
+					return []string{host, port}, nil
+				}()
+			},
+		}, {
+			Name:   "JoinHostPort",
+			Params: []kind{topKind, topKind},
+			Result: stringKind,
+			Func: func(c *callCtxt) {
+				host, port := c.value(0), c.value(1)
+				c.ret, c.err = func() (interface{}, error) {
+					var err error
+					hostStr := ""
+					switch host.Kind() {
+					case ListKind:
+						ipdata := netGetIP(host)
+						if len(ipdata) != 4 && len(ipdata) != 16 {
+							err = fmt.Errorf("invalid host %q", host)
+						}
+						hostStr = ipdata.String()
+					case BytesKind:
+						var b []byte
+						b, err = host.Bytes()
+						hostStr = string(b)
+					default:
+						hostStr, err = host.String()
+					}
+					if err != nil {
+						return "", err
+					}
+
+					portStr := ""
+					switch port.Kind() {
+					case StringKind:
+						portStr, err = port.String()
+					case BytesKind:
+						var b []byte
+						b, err = port.Bytes()
+						portStr = string(b)
+					default:
+						var i int64
+						i, err = port.Int64()
+						portStr = strconv.Itoa(int(i))
+					}
+					if err != nil {
+						return "", err
+					}
+
+					return net.JoinHostPort(hostStr, portStr), nil
+				}()
+			},
+		}, {
+			Name:   "FQDN",
+			Params: []kind{stringKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				s := c.string(0)
+				c.ret = func() interface{} {
+					for i := 0; i < len(s); i++ {
+						if s[i] >= utf8.RuneSelf {
+							return false
+						}
+					}
+					_, err := idnaProfile.ToASCII(s)
+					return err == nil
+				}()
+			},
+		}, {
+			Name:  "IPv4len",
+			Const: "4",
+		}, {
+			Name:  "IPv6len",
+			Const: "16",
+		}, {
+			Name:   "ParseIP",
+			Params: []kind{stringKind},
+			Result: listKind,
+			Func: func(c *callCtxt) {
+				s := c.string(0)
+				c.ret, c.err = func() (interface{}, error) {
+					goip := net.ParseIP(s)
+					if goip == nil {
+						return nil, fmt.Errorf("invalid IP address %q", s)
+					}
+					return netToList(goip), nil
+				}()
+			},
+		}, {
+			Name:   "IPv4",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+
+					return netGetIP(ip).To4() != nil
+				}()
+			},
+		}, {
+			Name:   "IP",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+
+					return netGetIP(ip) != nil
+				}()
+			},
+		}, {
+			Name:   "LoopbackIP",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+					return netGetIP(ip).IsLoopback()
+				}()
+			},
+		}, {
+			Name:   "MulticastIP",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+					return netGetIP(ip).IsMulticast()
+				}()
+			},
+		}, {
+			Name:   "InterfaceLocalMulticastIP",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+					return netGetIP(ip).IsInterfaceLocalMulticast()
+				}()
+			},
+		}, {
+			Name:   "LinkLocalMulticastIP",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+					return netGetIP(ip).IsLinkLocalMulticast()
+				}()
+			},
+		}, {
+			Name:   "LinkLocalUnicastIP",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+					return netGetIP(ip).IsLinkLocalUnicast()
+				}()
+			},
+		}, {
+			Name:   "GlobalUnicastIP",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+					return netGetIP(ip).IsGlobalUnicast()
+				}()
+			},
+		}, {
+			Name:   "UnspecifiedIP",
+			Params: []kind{topKind},
+			Result: boolKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret = func() interface{} {
+					return netGetIP(ip).IsUnspecified()
+				}()
+			},
+		}, {
+			Name:   "ToIP4",
+			Params: []kind{topKind},
+			Result: listKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret, c.err = func() (interface{}, error) {
+					ipdata := netGetIP(ip)
+					if ipdata == nil {
+						return nil, fmt.Errorf("invalid IP %q", ip)
+					}
+					ipv4 := ipdata.To4()
+					if ipv4 == nil {
+						return nil, fmt.Errorf("cannot convert %q to IPv4", ipdata)
+					}
+					return netToList(ipv4), nil
+				}()
+			},
+		}, {
+			Name:   "ToIP16",
+			Params: []kind{topKind},
+			Result: listKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret, c.err = func() (interface{}, error) {
+					ipdata := netGetIP(ip)
+					if ipdata == nil {
+						return nil, fmt.Errorf("invalid IP %q", ip)
+					}
+					return netToList(ipdata), nil
+				}()
+			},
+		}, {
+			Name:   "IPString",
+			Params: []kind{topKind},
+			Result: stringKind,
+			Func: func(c *callCtxt) {
+				ip := c.value(0)
+				c.ret, c.err = func() (interface{}, error) {
+					ipdata := netGetIP(ip)
+					if ipdata == nil {
+						return "", fmt.Errorf("invalid IP %q", ip)
+					}
+					return ipdata.String(), nil
+				}()
+			},
+		}},
+	},
 	"path": &builtinPkg{
 		native: []*builtin{{
 			Name:   "Split",
diff --git a/cue/gen.go b/cue/gen.go
index 3d5a1c8..4e866a5 100644
--- a/cue/gen.go
+++ b/cue/gen.go
@@ -134,6 +134,7 @@
 type generator struct {
 	w          *bytes.Buffer
 	decls      *bytes.Buffer
+	name       string
 	fset       *token.FileSet
 	defaultPkg string
 	first      bool
@@ -220,6 +221,7 @@
 		log.Fatal(err)
 	}
 	g.defaultPkg = ""
+	g.name = f.Name.Name
 
 	for _, d := range f.Decls {
 		switch x := d.(type) {
@@ -323,6 +325,10 @@
 	}
 
 	if !ast.IsExported(x.Name.Name) || x.Recv != nil {
+		if strings.HasPrefix(x.Name.Name, g.name) {
+			printer.Fprint(g.decls, g.fset, x)
+			fmt.Fprint(g.decls, "\n\n")
+		}
 		return
 	}
 
diff --git a/cue/kind.go b/cue/kind.go
index 95e5277..425df87 100644
--- a/cue/kind.go
+++ b/cue/kind.go
@@ -80,6 +80,11 @@
 	return ok
 }
 
+func isCustom(v value) bool {
+	_, ok := v.(*customValidator)
+	return ok
+}
+
 // isDone means that the value will not evaluate further.
 func (k kind) isDone() bool        { return k&referenceKind == bottomKind }
 func (k kind) hasReferences() bool { return k&referenceKind != bottomKind }
diff --git a/doc/tutorial/kubernetes/testdata/manual.out b/doc/tutorial/kubernetes/testdata/manual.out
index 5213692..ba2e1a8 100644
--- a/doc/tutorial/kubernetes/testdata/manual.out
+++ b/doc/tutorial/kubernetes/testdata/manual.out
@@ -1651,6 +1651,13 @@
                         containers: [{
                             name: "etcd"
                             env: [{
+                                name: "IP"
+                                valueFrom: {
+                                    fieldRef: {
+                                        fieldPath: "status.podIP"
+                                    }
+                                }
+                            }, {
                                 name:  "ETCDCTL_API"
                                 value: "3"
                             }, {
@@ -1663,13 +1670,6 @@
                                         fieldPath: "metadata.name"
                                     }
                                 }
-                            }, {
-                                name: "IP"
-                                valueFrom: {
-                                    fieldRef: {
-                                        fieldPath: "status.podIP"
-                                    }
-                                }
                             }]
                             image: "quay.io/coreos/etcd:v3.3.10"
                             args: []
@@ -1847,6 +1847,13 @@
         }
         args: []
         envSpec: {
+            IP: {
+                valueFrom: {
+                    fieldRef: {
+                        fieldPath: "status.podIP"
+                    }
+                }
+            }
             ETCDCTL_API: {
                 value: "3"
             }
@@ -1860,13 +1867,6 @@
                     }
                 }
             }
-            IP: {
-                valueFrom: {
-                    fieldRef: {
-                        fieldPath: "status.podIP"
-                    }
-                }
-            }
         }
         volume: {
         }
diff --git a/go.mod b/go.mod
index f8e8e3d..407c130 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@
 	github.com/retr0h/go-gilt v0.0.0-20190206215556-f73826b37af2
 	github.com/spf13/cobra v0.0.3
 	github.com/spf13/pflag v1.0.3
+	golang.org/x/net v0.0.0-20190311183353-d8887717615a
 	golang.org/x/sync v0.0.0-20190423024810-112230192c58
 	golang.org/x/text v0.3.2
 	golang.org/x/tools v0.0.0-20181210225255-6a3e9aa2ab77
diff --git a/go.sum b/go.sum
index 5eac4a2..4df0f8d 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,10 @@
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
@@ -19,13 +23,16 @@
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
+github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/logrusorgru/aurora v0.0.0-20180419164547-d694e6f975a9/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
 github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto=
 github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY=
 github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/retr0h/go-gilt v0.0.0-20190206215556-f73826b37af2 h1:vZ42M1tDiMLtirFA1K5k2QVFhWRqR4BjdSw0IMclzH4=
@@ -40,19 +47,28 @@
 github.com/xeipuuv/gojsonpointer v0.0.0-20170225233418-6fe8760cad35/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 github.com/xeipuuv/gojsonschema v0.0.0-20171230112544-511d08a359d1/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/sync  v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181018182439-def26773749b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181210225255-6a3e9aa2ab77 h1:s+6psEFi3o1QryeA/qyvUoVaHMCQkYVvZ0i2ZolwSJc=
 golang.org/x/tools v0.0.0-20181210225255-6a3e9aa2ab77/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373 h1:PPwnA7z1Pjf7XYaBP9GL1VAMZmcIWyFz7QCMSIIa3Bg=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.0.0/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/cmd/qgo/qgo.go b/internal/cmd/qgo/qgo.go
index 4d5e57d..98e89fa 100644
--- a/internal/cmd/qgo/qgo.go
+++ b/internal/cmd/qgo/qgo.go
@@ -232,8 +232,10 @@
 	if name := x.Name.Name; *stripstr && strings.HasSuffix(name, "String") {
 		newName := name[:len(name)-len("String")]
 		x.Name = ast.NewIdent(newName)
-		for _, c := range x.Doc.List {
-			c.Text = strings.Replace(c.Text, name, newName, -1)
+		if x.Doc != nil {
+			for _, c := range x.Doc.List {
+				c.Text = strings.Replace(c.Text, name, newName, -1)
+			}
 		}
 	}
 	types := []ast.Expr{}
diff --git a/pkg/net/doc.go b/pkg/net/doc.go
new file mode 100644
index 0000000..1e7a237
--- /dev/null
+++ b/pkg/net/doc.go
@@ -0,0 +1,27 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package net provides net-related type definitions.
+//
+// The IP-related defintions can be represented as either a string or a list of
+// byte values. To allow one format over an other these types can be further
+// constraint using string or [...]. For instance,
+//
+//    // multicast defines a multicast IP address in string form.
+//    multicast: net.MulticastIP & string
+//
+//    // unicast defines a global unicast IP address in list form.
+//    unicast: net.GlobalUnicastIP & [...]
+//
+package net
diff --git a/pkg/net/host.go b/pkg/net/host.go
new file mode 100644
index 0000000..a690fac
--- /dev/null
+++ b/pkg/net/host.go
@@ -0,0 +1,105 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package net
+
+import (
+	"fmt"
+	"net"
+	"strconv"
+	"unicode/utf8"
+
+	"cuelang.org/go/cue"
+	"golang.org/x/net/idna"
+)
+
+var idnaProfile = idna.New(
+	idna.ValidateLabels(true),
+	idna.VerifyDNSLength(true),
+	idna.StrictDomainName(true),
+)
+
+// SplitHostPort splits a network address of the form "host:port",
+// "host%zone:port", "[host]:port" or "[host%zone]:port" into host or host%zone
+// and port.
+//
+// A literal IPv6 address in hostport must be enclosed in square brackets, as in
+// "[::1]:80", "[::1%lo0]:80".
+func SplitHostPort(s string) (hostport []string, err error) {
+	host, port, err := net.SplitHostPort(s)
+	if err != nil {
+		return nil, err
+	}
+	return []string{host, port}, nil
+}
+
+// JoinHostPort combines host and port into a network address of the
+// form "host:port". If host contains a colon, as found in literal
+// IPv6 addresses, then JoinHostPort returns "[host]:port".
+//
+// See func Dial for a description of the host and port parameters.
+func JoinHostPort(host, port cue.Value) (string, error) {
+	var err error
+	hostStr := ""
+	switch host.Kind() {
+	case cue.ListKind:
+		ipdata := netGetIP(host)
+		if len(ipdata) != 4 && len(ipdata) != 16 {
+			err = fmt.Errorf("invalid host %q", host)
+		}
+		hostStr = ipdata.String()
+	case cue.BytesKind:
+		var b []byte
+		b, err = host.Bytes()
+		hostStr = string(b)
+	default:
+		hostStr, err = host.String()
+	}
+	if err != nil {
+		return "", err
+	}
+
+	portStr := ""
+	switch port.Kind() {
+	case cue.StringKind:
+		portStr, err = port.String()
+	case cue.BytesKind:
+		var b []byte
+		b, err = port.Bytes()
+		portStr = string(b)
+	default:
+		var i int64
+		i, err = port.Int64()
+		portStr = strconv.Itoa(int(i))
+	}
+	if err != nil {
+		return "", err
+	}
+
+	return net.JoinHostPort(hostStr, portStr), nil
+}
+
+// FQDN reports whether is is a valid fully qualified domain name.
+//
+// FQDN allows only ASCII characters as prescribed by RFC 1034 (A-Z, a-z, 0-9
+// and the hyphen).
+func FQDN(s string) bool {
+	for i := 0; i < len(s); i++ {
+		if s[i] >= utf8.RuneSelf {
+			return false
+		}
+	}
+	_, err := idnaProfile.ToASCII(s)
+	return err == nil
+}
diff --git a/pkg/net/ip.go b/pkg/net/ip.go
new file mode 100644
index 0000000..6b658e3
--- /dev/null
+++ b/pkg/net/ip.go
@@ -0,0 +1,193 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package net defines net-related types.
+package net
+
+import (
+	"fmt"
+	"net"
+
+	"cuelang.org/go/cue"
+)
+
+// IP address lengths (bytes).
+const (
+	IPv4len = 4
+	IPv6len = 16
+)
+
+func netGetIP(ip cue.Value) (goip net.IP) {
+	switch ip.Kind() {
+	case cue.StringKind:
+		s, err := ip.String()
+		if err != nil {
+			return nil
+		}
+		goip := net.ParseIP(s)
+		if goip == nil {
+			return nil
+		}
+		return goip
+
+	case cue.BytesKind:
+		b, err := ip.Bytes()
+		if err != nil {
+			return nil
+		}
+		goip := net.ParseIP(string(b))
+		if goip == nil {
+			return nil
+		}
+		return goip
+
+	case cue.ListKind:
+		iter, err := ip.List()
+		if err != nil {
+			return nil
+		}
+		for iter.Next() {
+			v, err := iter.Value().Int64()
+			if err != nil {
+				return nil
+			}
+			if v < 0 || 255 < v {
+				return nil
+			}
+			goip = append(goip, byte(v))
+		}
+		return goip
+
+	default:
+		// TODO: return canonical invalid type.
+		return nil
+	}
+}
+
+// ParseIP parses s as an IP address, returning the result.
+// The string s can be in dotted decimal ("192.0.2.1")
+// or IPv6 ("2001:db8::68") form.
+// If s is not a valid textual representation of an IP address,
+// ParseIP returns nil.
+func ParseIP(s string) ([]uint, error) {
+	goip := net.ParseIP(s)
+	if goip == nil {
+		return nil, fmt.Errorf("invalid IP address %q", s)
+	}
+	return netToList(goip), nil
+}
+
+func netToList(ip net.IP) []uint {
+	a := make([]uint, len(ip))
+	for i, p := range ip {
+		a[i] = uint(p)
+	}
+	return a
+}
+
+// IPv4 reports whether s is a valid IPv4 address.
+//
+// The address may be a string or list of bytes.
+func IPv4(ip cue.Value) bool {
+	// TODO: convert to native CUE.
+	return netGetIP(ip).To4() != nil
+}
+
+// IP reports whether s is a valid IPv4 or IPv6 address.
+//
+// The address may be a string or list of bytes.
+func IP(ip cue.Value) bool {
+	// TODO: convert to native CUE.
+	return netGetIP(ip) != nil
+}
+
+// LoopbackIP reports whether ip is a loopback address.
+func LoopbackIP(ip cue.Value) bool {
+	return netGetIP(ip).IsLoopback()
+}
+
+// MulticastIP reports whether ip is a multicast address.
+func MulticastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsMulticast()
+}
+
+// InterfaceLocalMulticastIP reports whether ip is an interface-local multicast
+// address.
+func InterfaceLocalMulticastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsInterfaceLocalMulticast()
+}
+
+// LinkLocalMulticast reports whether ip is a link-local multicast address.
+func LinkLocalMulticastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsLinkLocalMulticast()
+}
+
+// LinkLocalUnicastIP reports whether ip is a link-local unicast address.
+func LinkLocalUnicastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsLinkLocalUnicast()
+}
+
+// GlobalUnicastIP reports whether ip is a global unicast address.
+//
+// The identification of global unicast addresses uses address type
+// identification as defined in RFC 1122, RFC 4632 and RFC 4291 with the
+// exception of IPv4 directed broadcast addresses. It returns true even if ip is
+// in IPv4 private address space or local IPv6 unicast address space.
+func GlobalUnicastIP(ip cue.Value) bool {
+	return netGetIP(ip).IsGlobalUnicast()
+}
+
+// UnspecifiedIP reports whether ip is an unspecified address, either the IPv4
+// address "0.0.0.0" or the IPv6 address "::".
+func UnspecifiedIP(ip cue.Value) bool {
+	return netGetIP(ip).IsUnspecified()
+}
+
+// ToIP4 converts a given IP address, which may be a string or a list, to its
+// 4-byte representation.
+func ToIP4(ip cue.Value) ([]uint, error) {
+	ipdata := netGetIP(ip)
+	if ipdata == nil {
+		return nil, fmt.Errorf("invalid IP %q", ip)
+	}
+	ipv4 := ipdata.To4()
+	if ipv4 == nil {
+		return nil, fmt.Errorf("cannot convert %q to IPv4", ipdata)
+	}
+	return netToList(ipv4), nil
+}
+
+// ToIP16 converts a given IP address, which may be a string or a list, to its
+// 16-byte representation.
+func ToIP16(ip cue.Value) ([]uint, error) {
+	ipdata := netGetIP(ip)
+	if ipdata == nil {
+		return nil, fmt.Errorf("invalid IP %q", ip)
+	}
+	return netToList(ipdata), nil
+}
+
+// IPString returns the string form of the IP address ip. It returns one of 4 forms:
+//
+// - "<nil>", if ip has length 0
+// - dotted decimal ("192.0.2.1"), if ip is an IPv4 or IP4-mapped IPv6 address
+// - IPv6 ("2001:db8::1"), if ip is a valid IPv6 address
+// - the hexadecimal form of ip, without punctuation, if no other cases apply
+func IPString(ip cue.Value) (string, error) {
+	ipdata := netGetIP(ip)
+	if ipdata == nil {
+		return "", fmt.Errorf("invalid IP %q", ip)
+	}
+	return ipdata.String(), nil
+}