Skip to content

Commit b3e8da8

Browse files
committed
internal/filetypes/internal/genfunc: CUE to Go translation
This package implements some extremely limited CUE to Go translation, targeted strictly to the logic required by the subsidiary tags logic in `filetypes/types.cue`. For #3280. Signed-off-by: Roger Peppe <[email protected]> Change-Id: I23a01c43a2f391128de2f9443f4dfc753430e5ab Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1214222 TryBot-Result: CUEcueckoo <[email protected]> Reviewed-by: Daniel Martí <[email protected]> Unity-Result: CUE porcuepine <[email protected]>
1 parent 9c267d8 commit b3e8da8

File tree

4 files changed

+545
-0
lines changed

4 files changed

+545
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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+
"bytes"
19+
_ "embed"
20+
"fmt"
21+
"iter"
22+
"strings"
23+
24+
"cuelang.org/go/cue"
25+
"cuelang.org/go/cue/ast"
26+
"cuelang.org/go/cue/format"
27+
"cuelang.org/go/cue/token"
28+
)
29+
30+
// GenerateGoTypeForFields writes to buf a definition for a Go struct type named
31+
// structName with the given field names, all of which are of the same member type.
32+
// It also generates unmarshalFromMap and marshalToMap methods to convert to
33+
// and from map[string]memberType values.
34+
func GenerateGoTypeForFields(buf *bytes.Buffer, structName string, fields []string, memberType string) {
35+
emitf(buf, "type %s struct {", structName)
36+
for _, name := range fields {
37+
emitf(buf, "\t%s opt.Opt[%s]", name, memberType)
38+
}
39+
emitf(buf, "}")
40+
41+
emitf(buf, "func (t *%s) unmarshalFromMap(m map[string] %s) error {", structName, memberType)
42+
for _, name := range fields {
43+
emitf(buf, "\tif x, ok := m[%q]; ok {", name)
44+
emitf(buf, "\t\tt.%s = opt.Some(x)", name)
45+
emitf(buf, "\t}")
46+
}
47+
// TODO error when a key isn't allowed
48+
emitf(buf, "\treturn nil")
49+
emitf(buf, "}")
50+
emitf(buf, "func (t %s) marshalToMap() map[string] %s {", structName, memberType)
51+
emitf(buf, "\tm := make(map[string] %s)", memberType)
52+
for _, name := range fields {
53+
emitf(buf, "\tif t.%s.IsPresent() {", name)
54+
emitf(buf, "\t\tm[%[1]q] = t.%[1]s.Value()", name)
55+
emitf(buf, "\t}")
56+
}
57+
emitf(buf, "\treturn m")
58+
emitf(buf, "}")
59+
}
60+
61+
// GenerateGoFuncForCUEStruct writes to buf a function definition that will unify a Go struct
62+
// as defined by [GenerateGoTypeForFields] with the given CUE value, of the form:
63+
//
64+
// func funcName(t typeName) (typeName, error)
65+
//
66+
// It only understands an extremely limited subset of CUE, as driven by the usage
67+
// inside internal/filetypes/types.cue:
68+
// - no cyclic dependencies
69+
// - only a single scalar type for all fields in the struct
70+
// - all fields are known ahead of time
71+
//
72+
// and many more restrictions. It should fail when the CUE falls outside those restrictions.
73+
func GenerateGoFuncForCUEStruct(buf *bytes.Buffer, funcName, structName string, e cue.Value, keys []string, typeName string) {
74+
g := &funcGenerator{
75+
buf: buf,
76+
structName: structName,
77+
generated: make(map[string]bool),
78+
generating: make(map[string]bool),
79+
scope: make(map[string]ast.Expr),
80+
typeName: typeName,
81+
}
82+
for name, v := range structFields(e) {
83+
//log.Printf("syntax for %s: %s", name, dump(v.Syntax(cue.Raw())))
84+
g.scope[name] = simplify(v.Syntax(cue.Raw()).(ast.Expr))
85+
}
86+
g.emitf("// %s unifies %s values according to the following CUE logic:", funcName, structName)
87+
g.emitf("// %s", strings.ReplaceAll(dump(e.Syntax(cue.Raw())), "\n", "\n// "))
88+
g.emitf("func %[1]s(t %[2]s) (%[2]s, error) {", funcName, g.structName)
89+
g.emitf("\tvar r %s", g.structName)
90+
for _, name := range keys {
91+
g.generateField(name)
92+
}
93+
g.emitf("\treturn r, nil")
94+
g.emitf("}")
95+
}
96+
97+
type funcGenerator struct {
98+
structName string
99+
scope map[string]ast.Expr
100+
buf *bytes.Buffer
101+
generated map[string]bool
102+
generating map[string]bool
103+
typeName string
104+
}
105+
106+
func (g *funcGenerator) generateField(fieldName string) {
107+
// TODO fail when there's a recursive dependency.
108+
if g.generated[fieldName] {
109+
return
110+
}
111+
g.generated[fieldName] = true
112+
if g.generating[fieldName] {
113+
// Recursive reference.
114+
g.emitf("error: recursive reference to field %v", fieldName)
115+
return
116+
}
117+
g.generating[fieldName] = true
118+
defer func() {
119+
delete(g.generating, fieldName)
120+
}()
121+
x := g.scope[fieldName]
122+
if x == nil {
123+
g.emitf("if t.%s.IsPresent() {", fieldName)
124+
g.emitf("\treturn %s{}, fmt.Errorf(\"field %%q not allowed\", %q)", g.structName, fieldName)
125+
g.emitf("}")
126+
return
127+
}
128+
var binExpr *ast.BinaryExpr
129+
var unaryExpr *ast.UnaryExpr
130+
var ident *ast.Ident
131+
132+
switch {
133+
case isLiteral(x):
134+
g.emitf("r.%s = opt.Some(%s)", fieldName, dump(x))
135+
g.emitf("if t.%[1]s.IsPresent() && t.%[1]s.Value() != r.%[1]s.Value() {", fieldName)
136+
g.emitf("\treturn %[1]s{}, fmt.Errorf(\"conflict on %s; %%#v provided but need %%#v\", t.%[2]s.Value(), r.%[2]s.Value())", g.structName, fieldName)
137+
g.emitf("}")
138+
case match(x, &binExpr) && binExpr.Op == token.OR &&
139+
match(binExpr.X, &unaryExpr) && unaryExpr.Op == token.MUL &&
140+
match(binExpr.Y, &ident) && ident.Name == g.typeName:
141+
// *reference | bool
142+
143+
g.emitf("r.%s = %s", fieldName, g.exprFor(unaryExpr.X))
144+
g.emitf("if t.%s.IsPresent() {", fieldName)
145+
g.emitf("\tr.%s = t.%s", fieldName, fieldName)
146+
g.emitf("}")
147+
default:
148+
data, err := format.Node(x)
149+
if err != nil {
150+
panic(err)
151+
}
152+
g.emitf("error: cannot cope with field %s = %s", fieldName, data)
153+
}
154+
}
155+
156+
func (g *funcGenerator) exprFor(e ast.Expr) string {
157+
var binExpr *ast.BinaryExpr
158+
var ident *ast.Ident
159+
switch {
160+
case match(e, &ident) && g.scope[ident.Name] != nil:
161+
// Ensure that we've evaluated the field we're referring
162+
// to before we use it.
163+
g.generateField(ident.Name)
164+
return fmt.Sprintf("r.%s", ident.Name)
165+
case isLiteral(e):
166+
return fmt.Sprintf("opt.Some(%s)", dump(e))
167+
case match(e, &binExpr) && binExpr.Op == token.AND:
168+
switch {
169+
case g.isTypeName(binExpr.Y):
170+
// foo & bool
171+
return g.exprFor(binExpr.X)
172+
case g.isTypeName(binExpr.X):
173+
// bool & foo
174+
return g.exprFor(binExpr.Y)
175+
}
176+
}
177+
return fmt.Sprintf("error: cannot build expr for %s", dump(e))
178+
}
179+
180+
func (g *funcGenerator) isTypeName(x ast.Expr) bool {
181+
var ident *ast.Ident
182+
return match(x, &ident) && ident.Name == g.typeName
183+
}
184+
185+
func emitf(buf *bytes.Buffer, f string, a ...any) {
186+
fmt.Fprintf(buf, f, a...)
187+
if !strings.HasSuffix(f, "\n") {
188+
buf.WriteByte('\n')
189+
}
190+
}
191+
192+
func (g *funcGenerator) emitf(f string, a ...any) {
193+
emitf(g.buf, f, a...)
194+
}
195+
196+
// structFields returns an iterator over the names of all the fields
197+
// in v and their values.
198+
func structFields(v cue.Value, opts ...cue.Option) iter.Seq2[string, cue.Value] {
199+
return func(yield func(string, cue.Value) bool) {
200+
if !v.Exists() {
201+
return
202+
}
203+
iter, err := v.Fields(opts...)
204+
if err != nil {
205+
return
206+
}
207+
for iter.Next() {
208+
if !yield(iter.Selector().Unquoted(), iter.Value()) {
209+
break
210+
}
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)