Skip to content

Commit 30ca062

Browse files
committed
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 <[email protected]> Reviewed-by: CUE cueckoo <[email protected]>
1 parent dd0fa88 commit 30ca062

File tree

8 files changed

+134
-42
lines changed

8 files changed

+134
-42
lines changed

cue/testdata/interpolation/041_interpolation.txtar

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ e: _|_ // expression in interpolation must evaluate to a number kind or string (
3333
}
3434
-- out/eval --
3535
Errors:
36-
e: invalid interpolation: cannot use [] (type list) as type (string|number):
36+
e: invalid interpolation: cannot use [] (type list) as type (bool|string|bytes|number):
3737
./in.cue:7:4
3838

3939
Result:
@@ -52,7 +52,7 @@ Result:
5252
}
5353
r: (_){ _ }
5454
e: (_|_){
55-
// [eval] e: invalid interpolation: cannot use [] (type list) as type (string|number):
55+
// [eval] e: invalid interpolation: cannot use [] (type list) as type (bool|string|bytes|number):
5656
// ./in.cue:7:4
5757
}
5858
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
-- in.cue --
2+
bool1: "1+1=2: \(true)"
3+
bool1: "1+1=2: \(true)"
4+
bool2: "1+1=1: \(false)"
5+
6+
// one replacement character
7+
b1: 'a\xED\x95a'
8+
bytes1s: "\(b1)"
9+
bytes1b: '\(b1)'
10+
11+
// two replacement characters
12+
b2: 'a\x80\x95a'
13+
bytes2s: "\(b2)"
14+
bytes2b: '\(b2)'
15+
16+
// preserve precision
17+
n1: "\(1) \(2.00)"
18+
19+
// but normalize representation
20+
n2: "\(1e2)"
21+
-- out/eval --
22+
(struct){
23+
bool1: (string){ "1+1=2: true" }
24+
bool2: (string){ "1+1=1: false" }
25+
b1: (bytes){ 'a\xed\x95a' }
26+
bytes1s: (string){ "a�a" }
27+
bytes1b: (bytes){ 'a\xed\x95a' }
28+
b2: (bytes){ 'a\x80\x95a' }
29+
bytes2s: (string){ "a��a" }
30+
bytes2b: (bytes){ 'a\x80\x95a' }
31+
n1: (string){ "1 2.00" }
32+
n2: (string){ "1E+2" }
33+
}
34+
-- out/compile --
35+
--- in.cue
36+
{
37+
bool1: "1+1=2: \(true)"
38+
bool1: "1+1=2: \(true)"
39+
bool2: "1+1=1: \(false)"
40+
b1: 'a\xed\x95a'
41+
bytes1s: "\(〈0;b1〉)"
42+
bytes1b: '\(〈0;b1〉)'
43+
b2: 'a\x80\x95a'
44+
bytes2s: "\(〈0;b2〉)"
45+
bytes2b: '\(〈0;b2〉)'
46+
n1: "\(1) \(2.00)"
47+
n2: "\(1E+2)"
48+
}

doc/ref/spec.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,8 +2609,21 @@ expressions with their string representation.
26092609
String interpolation may be used in single- and double-quoted strings, as well
26102610
as their multiline equivalent.
26112611

2612-
A placeholder consists of "\(" followed by an expression and a ")". The
2613-
expression is evaluated within the scope within which the string is defined.
2612+
A placeholder consists of "\(" followed by an expression and a ")".
2613+
The expression is evaluated in the scope within which the string is defined.
2614+
2615+
The result of the expression is substituted as follows:
2616+
- string: as is
2617+
- bool: the JSON representation of the bool
2618+
- number: a JSON representation of the number that preserves the
2619+
precision of the underlying binary coded decimal
2620+
- bytes: as if substituted within single quotes or
2621+
converted to valid UTF-8 replacing the
2622+
maximal subpart of ill-formed subsequences with a single
2623+
replacement character (W3C encoding standard) otherwise
2624+
- list: illegal
2625+
- struct: illegal
2626+
26142627

26152628
```
26162629
a: "World"

internal/core/adt/context.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"regexp"
2121

2222
"github.com/cockroachdb/apd/v2"
23-
"golang.org/x/text/runes"
23+
"golang.org/x/text/encoding/unicode"
2424

2525
"cuelang.org/go/cue/ast"
2626
"cuelang.org/go/cue/errors"
@@ -738,9 +738,17 @@ func (c *OpContext) StringValue(v Value) string {
738738
return c.stringValue(v, nil)
739739
}
740740

741-
// ToString returns the string value of a numeric or string value.
741+
// ToBytes returns the bytes value of a scalar value.
742+
func (c *OpContext) ToBytes(v Value) []byte {
743+
if x, ok := v.(*Bytes); ok {
744+
return x.B
745+
}
746+
return []byte(c.ToString(v))
747+
}
748+
749+
// ToString returns the string value of a scalar value.
742750
func (c *OpContext) ToString(v Value) string {
743-
return c.toStringValue(v, StringKind|NumKind, nil)
751+
return c.toStringValue(v, StringKind|NumKind|BytesKind|BoolKind, nil)
744752

745753
}
746754

@@ -766,18 +774,29 @@ func (c *OpContext) toStringValue(v Value, k Kind, as interface{}) string {
766774
return x.Str
767775

768776
case *Bytes:
769-
return string(runes.ReplaceIllFormed().Bytes(x.B))
777+
return bytesToString(x.B)
770778

771779
case *Num:
772780
return x.X.String()
773781

782+
case *Bool:
783+
if x.B {
784+
return "true"
785+
}
786+
return "false"
787+
774788
default:
775789
c.addErrf(IncompleteError, c.pos(),
776790
"non-concrete value %s (type %s)", c.Str(v), v.Kind())
777791
}
778792
return ""
779793
}
780794

795+
func bytesToString(b []byte) string {
796+
b, _ = unicode.UTF8.NewDecoder().Bytes(b)
797+
return string(b)
798+
}
799+
781800
func (c *OpContext) bytesValue(v Value, as interface{}) []byte {
782801
v = Unwrap(v)
783802
if isError(v) {

internal/core/adt/expr.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -717,8 +717,11 @@ func (x *Interpolation) evaluate(c *OpContext) Value {
717717
buf := bytes.Buffer{}
718718
for _, e := range x.Parts {
719719
v := c.value(e)
720-
s := c.ToString(v)
721-
buf.WriteString(s)
720+
if x.K == BytesKind {
721+
buf.Write(c.ToBytes(v))
722+
} else {
723+
buf.WriteString(c.ToString(v))
724+
}
722725
}
723726
if err := c.Err(); err != nil {
724727
err = &Bottom{
@@ -729,9 +732,9 @@ func (x *Interpolation) evaluate(c *OpContext) Value {
729732
// return nil
730733
return err
731734
}
732-
// if k == bytesKind {
733-
// return &BytesLit{x.source, buf.String(), nil}
734-
// }
735+
if x.K == BytesKind {
736+
return &Bytes{x.Src, buf.Bytes(), nil}
737+
}
735738
return &String{x.Src, buf.String(), nil}
736739
}
737740

internal/core/compile/compile.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,11 +799,16 @@ func (c *compiler) expr(expr ast.Expr) adt.Expr {
799799
if len(n.Elts) == 1 {
800800
return c.expr(n.Elts[0])
801801
}
802-
lit := &adt.Interpolation{Src: n, K: adt.StringKind}
802+
lit := &adt.Interpolation{Src: n}
803803
info, prefixLen, _, err := literal.ParseQuotes(first.Value, last.Value)
804804
if err != nil {
805805
return c.errf(n, "invalid interpolation: %v", err)
806806
}
807+
if info.IsDouble() {
808+
lit.K = adt.StringKind
809+
} else {
810+
lit.K = adt.BytesKind
811+
}
807812
prefix := ""
808813
for i := 0; i < len(n.Elts); i += 2 {
809814
l, ok := n.Elts[i].(*ast.BasicLit)

internal/core/debug/compact.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -226,20 +226,7 @@ func (w *compactPrinter) node(n adt.Node) {
226226
w.string("]")
227227

228228
case *adt.Interpolation:
229-
w.string(`"`)
230-
for i := 0; i < len(x.Parts); i += 2 {
231-
if s, ok := x.Parts[i].(*adt.String); ok {
232-
w.string(s.Str)
233-
} else {
234-
w.string("<bad string>")
235-
}
236-
if i+1 < len(x.Parts) {
237-
w.string(`\(`)
238-
w.node(x.Parts[i+1])
239-
w.string(`)`)
240-
}
241-
}
242-
w.string(`"`)
229+
w.interpolation(x)
243230

244231
case *adt.UnaryExpr:
245232
fmt.Fprint(w, x.Op)

internal/core/debug/debug.go

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,36 @@ func (w *printer) shortError(errs errors.Error) {
106106
}
107107
}
108108

109+
func (w *printer) interpolation(x *adt.Interpolation) {
110+
quote := `"`
111+
if x.K == adt.BytesKind {
112+
quote = `'`
113+
}
114+
w.string(quote)
115+
for i := 0; i < len(x.Parts); i += 2 {
116+
switch x.K {
117+
case adt.StringKind:
118+
if s, ok := x.Parts[i].(*adt.String); ok {
119+
w.string(s.Str)
120+
} else {
121+
w.string("<bad string>")
122+
}
123+
case adt.BytesKind:
124+
if s, ok := x.Parts[i].(*adt.Bytes); ok {
125+
_, _ = w.Write(s.B)
126+
} else {
127+
w.string("<bad bytes>")
128+
}
129+
}
130+
if i+1 < len(x.Parts) {
131+
w.string(`\(`)
132+
w.node(x.Parts[i+1])
133+
w.string(`)`)
134+
}
135+
}
136+
w.string(quote)
137+
}
138+
109139
func (w *printer) node(n adt.Node) {
110140
switch x := n.(type) {
111141
case *adt.Vertex:
@@ -375,20 +405,7 @@ func (w *printer) node(n adt.Node) {
375405
w.string("]")
376406

377407
case *adt.Interpolation:
378-
w.string(`"`)
379-
for i := 0; i < len(x.Parts); i += 2 {
380-
if s, ok := x.Parts[i].(*adt.String); ok {
381-
w.string(s.Str)
382-
} else {
383-
w.string("<bad string>")
384-
}
385-
if i+1 < len(x.Parts) {
386-
w.string(`\(`)
387-
w.node(x.Parts[i+1])
388-
w.string(`)`)
389-
}
390-
}
391-
w.string(`"`)
408+
w.interpolation(x)
392409

393410
case *adt.UnaryExpr:
394411
fmt.Fprint(w, x.Op)

0 commit comments

Comments
 (0)