internal/core/adt: support interpolation of bytes and bool

Also fixes bytes interpolation, which was still WIP.

For bool: use JSON representation

For bytes: assume bytes are UTF-8 and replace illegal characters according to the recommendations of the Unicode consortium and W3C requirement for character encodings. (So not the Go standard for replacement.)

Details clarified in spec.

Fixes #475.

Change-Id: I94c068f8a73a3948194b179a33e556c37692c05f
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6951
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
diff --git a/cue/testdata/interpolation/041_interpolation.txtar b/cue/testdata/interpolation/041_interpolation.txtar
index f23b618..8f2dd9b 100644
--- a/cue/testdata/interpolation/041_interpolation.txtar
+++ b/cue/testdata/interpolation/041_interpolation.txtar
@@ -33,7 +33,7 @@
 }
 -- out/eval --
 Errors:
-e: invalid interpolation: cannot use [] (type list) as type (string|number):
+e: invalid interpolation: cannot use [] (type list) as type (bool|string|bytes|number):
     ./in.cue:7:4
 
 Result:
@@ -52,7 +52,7 @@
   }
   r: (_){ _ }
   e: (_|_){
-    // [eval] e: invalid interpolation: cannot use [] (type list) as type (string|number):
+    // [eval] e: invalid interpolation: cannot use [] (type list) as type (bool|string|bytes|number):
     //     ./in.cue:7:4
   }
 }
diff --git a/cue/testdata/interpolation/scalars.txtar b/cue/testdata/interpolation/scalars.txtar
new file mode 100644
index 0000000..23cce03
--- /dev/null
+++ b/cue/testdata/interpolation/scalars.txtar
@@ -0,0 +1,48 @@
+-- in.cue --
+bool1: "1+1=2:  \(true)"
+bool1: "1+1=2:  \(true)"
+bool2: "1+1=1:  \(false)"
+
+// one replacement character
+b1: 'a\xED\x95a'
+bytes1s: "\(b1)"
+bytes1b: '\(b1)'
+
+// two replacement characters
+b2: 'a\x80\x95a'
+bytes2s: "\(b2)"
+bytes2b: '\(b2)'
+
+// preserve precision
+n1: "\(1) \(2.00)"
+
+// but normalize representation
+n2: "\(1e2)"
+-- out/eval --
+(struct){
+  bool1: (string){ "1+1=2:  true" }
+  bool2: (string){ "1+1=1:  false" }
+  b1: (bytes){ 'a\xed\x95a' }
+  bytes1s: (string){ "a�a" }
+  bytes1b: (bytes){ 'a\xed\x95a' }
+  b2: (bytes){ 'a\x80\x95a' }
+  bytes2s: (string){ "a��a" }
+  bytes2b: (bytes){ 'a\x80\x95a' }
+  n1: (string){ "1 2.00" }
+  n2: (string){ "1E+2" }
+}
+-- out/compile --
+--- in.cue
+{
+  bool1: "1+1=2:  \(true)"
+  bool1: "1+1=2:  \(true)"
+  bool2: "1+1=1:  \(false)"
+  b1: 'a\xed\x95a'
+  bytes1s: "\(〈0;b1〉)"
+  bytes1b: '\(〈0;b1〉)'
+  b2: 'a\x80\x95a'
+  bytes2s: "\(〈0;b2〉)"
+  bytes2b: '\(〈0;b2〉)'
+  n1: "\(1) \(2.00)"
+  n2: "\(1E+2)"
+}
diff --git a/doc/ref/spec.md b/doc/ref/spec.md
index 11712e2..f7f0326 100644
--- a/doc/ref/spec.md
+++ b/doc/ref/spec.md
@@ -2609,8 +2609,21 @@
 String interpolation may be used in single- and double-quoted strings, as well
 as their multiline equivalent.
 
-A placeholder consists of "\(" followed by an expression and a ")". The
-expression is evaluated within the scope within which the string is defined.
+A placeholder consists of "\(" followed by an expression and a ")".
+The expression is evaluated in the scope within which the string is defined.
+
+The result of the expression is substituted as follows:
+- string: as is
+- bool: the JSON representation of the bool
+- number: a JSON representation of the number that preserves the
+precision of the underlying binary coded decimal
+- bytes: as if substituted within single quotes or
+converted to valid UTF-8 replacing the
+maximal subpart of ill-formed subsequences with a single
+replacement character (W3C encoding standard) otherwise
+- list: illegal
+- struct: illegal
+
 
 ```
 a: "World"
diff --git a/internal/core/adt/context.go b/internal/core/adt/context.go
index db385b8..9a36cc1 100644
--- a/internal/core/adt/context.go
+++ b/internal/core/adt/context.go
@@ -20,7 +20,7 @@
 	"regexp"
 
 	"github.com/cockroachdb/apd/v2"
-	"golang.org/x/text/runes"
+	"golang.org/x/text/encoding/unicode"
 
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
@@ -738,9 +738,17 @@
 	return c.stringValue(v, nil)
 }
 
-// ToString returns the string value of a numeric or string value.
+// ToBytes returns the bytes value of a scalar value.
+func (c *OpContext) ToBytes(v Value) []byte {
+	if x, ok := v.(*Bytes); ok {
+		return x.B
+	}
+	return []byte(c.ToString(v))
+}
+
+// ToString returns the string value of a scalar value.
 func (c *OpContext) ToString(v Value) string {
-	return c.toStringValue(v, StringKind|NumKind, nil)
+	return c.toStringValue(v, StringKind|NumKind|BytesKind|BoolKind, nil)
 
 }
 
@@ -766,11 +774,17 @@
 		return x.Str
 
 	case *Bytes:
-		return string(runes.ReplaceIllFormed().Bytes(x.B))
+		return bytesToString(x.B)
 
 	case *Num:
 		return x.X.String()
 
+	case *Bool:
+		if x.B {
+			return "true"
+		}
+		return "false"
+
 	default:
 		c.addErrf(IncompleteError, c.pos(),
 			"non-concrete value %s (type %s)", c.Str(v), v.Kind())
@@ -778,6 +792,11 @@
 	return ""
 }
 
+func bytesToString(b []byte) string {
+	b, _ = unicode.UTF8.NewDecoder().Bytes(b)
+	return string(b)
+}
+
 func (c *OpContext) bytesValue(v Value, as interface{}) []byte {
 	v = Unwrap(v)
 	if isError(v) {
diff --git a/internal/core/adt/expr.go b/internal/core/adt/expr.go
index 82df806..8fbdcec 100644
--- a/internal/core/adt/expr.go
+++ b/internal/core/adt/expr.go
@@ -717,8 +717,11 @@
 	buf := bytes.Buffer{}
 	for _, e := range x.Parts {
 		v := c.value(e)
-		s := c.ToString(v)
-		buf.WriteString(s)
+		if x.K == BytesKind {
+			buf.Write(c.ToBytes(v))
+		} else {
+			buf.WriteString(c.ToString(v))
+		}
 	}
 	if err := c.Err(); err != nil {
 		err = &Bottom{
@@ -729,9 +732,9 @@
 		// return nil
 		return err
 	}
-	// if k == bytesKind {
-	// 	return &BytesLit{x.source, buf.String(), nil}
-	// }
+	if x.K == BytesKind {
+		return &Bytes{x.Src, buf.Bytes(), nil}
+	}
 	return &String{x.Src, buf.String(), nil}
 }
 
diff --git a/internal/core/compile/compile.go b/internal/core/compile/compile.go
index 853a6a6..dbb556a 100644
--- a/internal/core/compile/compile.go
+++ b/internal/core/compile/compile.go
@@ -799,11 +799,16 @@
 		if len(n.Elts) == 1 {
 			return c.expr(n.Elts[0])
 		}
-		lit := &adt.Interpolation{Src: n, K: adt.StringKind}
+		lit := &adt.Interpolation{Src: n}
 		info, prefixLen, _, err := literal.ParseQuotes(first.Value, last.Value)
 		if err != nil {
 			return c.errf(n, "invalid interpolation: %v", err)
 		}
+		if info.IsDouble() {
+			lit.K = adt.StringKind
+		} else {
+			lit.K = adt.BytesKind
+		}
 		prefix := ""
 		for i := 0; i < len(n.Elts); i += 2 {
 			l, ok := n.Elts[i].(*ast.BasicLit)
diff --git a/internal/core/debug/compact.go b/internal/core/debug/compact.go
index b40ada5..cbd2dfb 100644
--- a/internal/core/debug/compact.go
+++ b/internal/core/debug/compact.go
@@ -226,20 +226,7 @@
 		w.string("]")
 
 	case *adt.Interpolation:
-		w.string(`"`)
-		for i := 0; i < len(x.Parts); i += 2 {
-			if s, ok := x.Parts[i].(*adt.String); ok {
-				w.string(s.Str)
-			} else {
-				w.string("<bad string>")
-			}
-			if i+1 < len(x.Parts) {
-				w.string(`\(`)
-				w.node(x.Parts[i+1])
-				w.string(`)`)
-			}
-		}
-		w.string(`"`)
+		w.interpolation(x)
 
 	case *adt.UnaryExpr:
 		fmt.Fprint(w, x.Op)
diff --git a/internal/core/debug/debug.go b/internal/core/debug/debug.go
index b61d58f..af3f413 100644
--- a/internal/core/debug/debug.go
+++ b/internal/core/debug/debug.go
@@ -106,6 +106,36 @@
 	}
 }
 
+func (w *printer) interpolation(x *adt.Interpolation) {
+	quote := `"`
+	if x.K == adt.BytesKind {
+		quote = `'`
+	}
+	w.string(quote)
+	for i := 0; i < len(x.Parts); i += 2 {
+		switch x.K {
+		case adt.StringKind:
+			if s, ok := x.Parts[i].(*adt.String); ok {
+				w.string(s.Str)
+			} else {
+				w.string("<bad string>")
+			}
+		case adt.BytesKind:
+			if s, ok := x.Parts[i].(*adt.Bytes); ok {
+				_, _ = w.Write(s.B)
+			} else {
+				w.string("<bad bytes>")
+			}
+		}
+		if i+1 < len(x.Parts) {
+			w.string(`\(`)
+			w.node(x.Parts[i+1])
+			w.string(`)`)
+		}
+	}
+	w.string(quote)
+}
+
 func (w *printer) node(n adt.Node) {
 	switch x := n.(type) {
 	case *adt.Vertex:
@@ -375,20 +405,7 @@
 		w.string("]")
 
 	case *adt.Interpolation:
-		w.string(`"`)
-		for i := 0; i < len(x.Parts); i += 2 {
-			if s, ok := x.Parts[i].(*adt.String); ok {
-				w.string(s.Str)
-			} else {
-				w.string("<bad string>")
-			}
-			if i+1 < len(x.Parts) {
-				w.string(`\(`)
-				w.node(x.Parts[i+1])
-				w.string(`)`)
-			}
-		}
-		w.string(`"`)
+		w.interpolation(x)
 
 	case *adt.UnaryExpr:
 		fmt.Fprint(w, x.Op)