Skip to content

Commit 9c267d8

Browse files
committed
internal/filetypes/internal/genfunc: CUE expression simplification
The new `genfunc` package will provide very limited support for generating Go code from CUE, targeted to exactly the domain required by the `types.cue` logic for now. As part of that, we want the ability to simplify CUE expressions so that we can pattern-match on a relatively small number of possible syntax patterns. This functionality should really exist inside the CUE evaluator itself, but doesn't currently, and it seems easiest to implement a small amount of it here rather than mess with the core evaluator for now. For #3280. Signed-off-by: Roger Peppe <[email protected]> Change-Id: I133e180e98c4900e1b5daa4e3d61a34b7e8a1eca Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1214221 Reviewed-by: Daniel Martí <[email protected]> TryBot-Result: CUEcueckoo <[email protected]> Unity-Result: CUE porcuepine <[email protected]>
1 parent c65ecb4 commit 9c267d8

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright 2025 CUE Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package genfunc
16+
17+
import (
18+
_ "embed"
19+
"reflect"
20+
21+
"cuelang.org/go/cue/ast"
22+
"cuelang.org/go/cue/format"
23+
"cuelang.org/go/cue/token"
24+
)
25+
26+
// simplify applies some simplifications to the given expression to reduce the range
27+
// of syntax we need to handle.
28+
// TODO this should really be something that the core CUE evaluator can do.
29+
func simplify(e ast.Expr) (_r ast.Expr) {
30+
// Continue simplifying until nothing more happens.
31+
// TODO this could probably be done without looping.
32+
for {
33+
old := e
34+
e = simplify0(e)
35+
if reflect.DeepEqual(e, old) {
36+
return e
37+
}
38+
}
39+
}
40+
41+
func simplify0(e ast.Expr) (_r ast.Expr) {
42+
var structLit *ast.StructLit
43+
var embed *ast.EmbedDecl
44+
var binExpr *ast.BinaryExpr
45+
var unaryExpr *ast.UnaryExpr
46+
switch {
47+
case match(e, &structLit) && len(structLit.Elts) == 1 && match(structLit.Elts[0], &embed):
48+
// { x } -> x
49+
return simplify0(embed.Expr)
50+
case match(e, &binExpr) && binExpr.Op == token.AND &&
51+
isLiteral(binExpr.X) && isLiteral(binExpr.Y):
52+
if isBottom(binExpr.X) || isBottom(binExpr.Y) {
53+
// _|_ & lit => _|_
54+
return &ast.BottomLit{}
55+
}
56+
if !equal(binExpr.X, binExpr.Y) {
57+
// true & false => _|_
58+
return &ast.BottomLit{}
59+
}
60+
// lit & lit => lit
61+
return binExpr.X
62+
case match(e, &binExpr) && binExpr.Op == token.AND:
63+
x, y := simplify0(binExpr.X), simplify0(binExpr.Y)
64+
if equal(x, y) {
65+
// x & x => x
66+
return x
67+
}
68+
unifyDisjunct := func(x, y ast.Expr) ast.Expr {
69+
hasDefault := false
70+
if match(x, &unaryExpr) && unaryExpr.Op == token.MUL {
71+
hasDefault = true
72+
x = unaryExpr.X
73+
}
74+
if match(y, &unaryExpr) && unaryExpr.Op == token.MUL {
75+
hasDefault = true
76+
y = unaryExpr.X
77+
}
78+
e := ast.Expr(&ast.BinaryExpr{Op: token.AND, X: x, Y: y})
79+
if hasDefault {
80+
e = &ast.UnaryExpr{Op: token.MUL, X: e}
81+
}
82+
return e
83+
}
84+
85+
switch {
86+
case match(y, &binExpr) && binExpr.Op == token.OR:
87+
// x & (a | b) => (x & a) | (x & b)
88+
a, b := binExpr.X, binExpr.Y
89+
return &ast.BinaryExpr{
90+
Op: token.OR,
91+
X: simplify0(unifyDisjunct(x, a)),
92+
Y: simplify0(unifyDisjunct(x, b)),
93+
}
94+
case match(x, &binExpr) && binExpr.Op == token.OR:
95+
// (a | b) & y => (a & y) | (b & y)
96+
a, b := binExpr.X, binExpr.Y
97+
return &ast.BinaryExpr{Op: token.OR,
98+
X: simplify0(unifyDisjunct(a, y)),
99+
Y: simplify0(unifyDisjunct(b, y)),
100+
}
101+
case isLiteral(x) && simpleType(y) == literalType(x):
102+
// "foo" & string => "foo"
103+
return x
104+
case isLiteral(y) && simpleType(x) == literalType(y):
105+
// string & "foo" => "foo"
106+
return y
107+
}
108+
return &ast.BinaryExpr{Op: token.AND, X: x, Y: y}
109+
case match(e, &unaryExpr):
110+
return &ast.UnaryExpr{Op: unaryExpr.Op, X: simplify0(unaryExpr.X)}
111+
case match(e, &binExpr) && binExpr.Op == token.OR:
112+
switch {
113+
case isBottom(binExpr.X):
114+
// _|_ | x => x
115+
return withoutDefaultMarker(binExpr.Y)
116+
case isBottom(binExpr.Y):
117+
// x | _|_ => x
118+
return withoutDefaultMarker(binExpr.X)
119+
}
120+
return &ast.BinaryExpr{
121+
Op: token.OR,
122+
X: simplify0(binExpr.X),
123+
Y: simplify0(binExpr.Y),
124+
}
125+
}
126+
return e
127+
}
128+
129+
func withoutDefaultMarker(e ast.Expr) ast.Expr {
130+
var unaryExpr *ast.UnaryExpr
131+
if match(e, &unaryExpr) && unaryExpr.Op == token.MUL {
132+
return unaryExpr.X
133+
}
134+
return e
135+
}
136+
137+
func isBottom(e ast.Expr) bool {
138+
_, ok := withoutDefaultMarker(e).(*ast.BottomLit)
139+
return ok
140+
}
141+
142+
func simpleType(x ast.Expr) string {
143+
switch x := x.(type) {
144+
case *ast.Ident:
145+
switch x.Name {
146+
case "string", "int", "number", "bool", "null":
147+
return x.Name
148+
}
149+
}
150+
return ""
151+
}
152+
153+
func literalType(x ast.Expr) string {
154+
switch x := x.(type) {
155+
case *ast.Ident:
156+
switch x.Name {
157+
case "true", "false":
158+
return "bool"
159+
case "null":
160+
return "null"
161+
}
162+
case *ast.BasicLit:
163+
switch x.Kind {
164+
case token.INT,
165+
token.FLOAT:
166+
return "number"
167+
case token.STRING:
168+
return "string"
169+
}
170+
case *ast.BottomLit:
171+
return "_|_"
172+
}
173+
return ""
174+
}
175+
176+
func isLiteral(x ast.Expr) bool {
177+
switch x := x.(type) {
178+
case *ast.Ident:
179+
return x.Name == "true" || x.Name == "false" || x.Name == "null"
180+
case *ast.BasicLit:
181+
return true
182+
case *ast.BottomLit:
183+
return true
184+
}
185+
return false
186+
}
187+
188+
func match[T any](x any, xp *T) (ok bool) {
189+
*xp, ok = x.(T)
190+
return ok
191+
}
192+
193+
func dump(n ast.Node) string {
194+
if n == nil {
195+
return "<nil ast.Node>"
196+
}
197+
data, err := format.Node(n)
198+
if err != nil {
199+
panic(err)
200+
}
201+
return string(data)
202+
}
203+
204+
func equal(x, y ast.Node) bool {
205+
if reflect.TypeOf(x) != reflect.TypeOf(y) {
206+
return false
207+
}
208+
switch x := x.(type) {
209+
case *ast.BinaryExpr:
210+
y := y.(*ast.BinaryExpr)
211+
return equal(x.X, y.X) && equal(x.Y, y.Y)
212+
case *ast.UnaryExpr:
213+
y := y.(*ast.UnaryExpr)
214+
return equal(x.X, y.X)
215+
case *ast.StructLit:
216+
y := y.(*ast.StructLit)
217+
if len(x.Elts) != len(y.Elts) {
218+
return false
219+
}
220+
for i := range x.Elts {
221+
if !equal(x.Elts[i], y.Elts[i]) {
222+
return false
223+
}
224+
}
225+
return true
226+
case *ast.Ident:
227+
y := y.(*ast.Ident)
228+
return x.Name == y.Name
229+
}
230+
// TODO more nodes.
231+
return false
232+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2025 CUE Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package genfunc
16+
17+
import (
18+
"testing"
19+
20+
"github.com/go-quicktest/qt"
21+
22+
"cuelang.org/go/cue"
23+
"cuelang.org/go/cue/ast"
24+
"cuelang.org/go/cue/cuecontext"
25+
)
26+
27+
var simplifyTests = []struct {
28+
testName string
29+
cue string
30+
want string
31+
}{{
32+
testName: "SimpleScalar",
33+
cue: "true",
34+
want: "true",
35+
}, {
36+
testName: "UnifyIdentical",
37+
cue: "true & true",
38+
want: "true",
39+
}, {
40+
testName: "UnifyIdenticalLeft",
41+
cue: "true & (true & true)",
42+
want: "true",
43+
}, {
44+
testName: "UnifyIdenticalRight",
45+
cue: "(true & true) & true",
46+
want: "true",
47+
}, {
48+
// TODO
49+
testName: "DisjointIdentical",
50+
cue: "true | true",
51+
want: "true | true",
52+
}, {
53+
testName: "SimpleEmbed",
54+
cue: "{true}",
55+
want: "true",
56+
}, {
57+
testName: "ConjunctionDistributesOverDisjunction",
58+
cue: "(1 | 2) & int",
59+
want: "int & 1 | int & 2",
60+
}, {
61+
testName: "ConjunctionDistributesOverDisjunctionWithDefault",
62+
cue: "(*1 | 2) & int",
63+
want: "*(int & 1) | int & 2",
64+
}, {
65+
testName: "DisjunctElimination",
66+
cue: `*("" & "go") | string & "go"`,
67+
want: `"go"`,
68+
}}
69+
70+
func TestSimplify(t *testing.T) {
71+
ctx := cuecontext.New()
72+
for _, test := range simplifyTests {
73+
t.Run(test.testName, func(t *testing.T) {
74+
v := ctx.CompileString(test.cue)
75+
qt.Assert(t, qt.IsNil(v.Err()))
76+
before := v.Syntax(cue.Raw()).(ast.Expr)
77+
t.Logf("before: %s", dump(before))
78+
after := simplify(before)
79+
qt.Assert(t, qt.Equals(dump(after), test.want))
80+
})
81+
}
82+
}

0 commit comments

Comments
 (0)