1// Copyright 2018 The 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//go:build ignore
16
17// gen.go generates the pkg.go files inside the packages under the pkg directory.
18//
19// It takes the list of packages from the packages.txt.
20//
21// Be sure to also update an entry in pkg/pkg.go, if so desired.
22package main
23
24import (
25 "bytes"
26 "cmp"
27 _ "embed"
28 "flag"
29 "fmt"
30 "go/constant"
31 "go/format"
32 "go/token"
33 "go/types"
34 "log"
35 "math/big"
36 "os"
37 "path/filepath"
38 "slices"
39 "strings"
40 "text/template"
41
42 "golang.org/x/tools/go/packages"
43
44 "cuelang.org/go/cue/ast"
45 cueformat "cuelang.org/go/cue/format"
46 "cuelang.org/go/cue/parser"
47 "cuelang.org/go/internal"
48)
49
50type headerParams struct {
51 GoPkg string
52 CUEPkg string
53
54 PackageDoc string
55 PackageDefs string
56}
57
58var header = template.Must(template.New("").Parse(
59 `// Code generated by cuelang.org/go/pkg/gen. DO NOT EDIT.
60
61{{if .PackageDoc}}
62{{.PackageDoc -}}
63// {{.PackageDefs}}
64{{end -}}
65package {{.GoPkg}}
66
67{{if .CUEPkg -}}
68import (
69 "cuelang.org/go/internal/core/adt"
70 "cuelang.org/go/internal/pkg"
71)
72
73func init() {
74 pkg.Register({{printf "%q" .CUEPkg}}, p)
75}
76
77var _ = adt.TopKind // in case the adt package isn't used
78{{end}}
79`))
80
81const pkgParent = "cuelang.org/go/pkg"
82
83func main() {
84 flag.Parse()
85 log.SetFlags(log.Lshortfile)
86 log.SetOutput(os.Stdout)
87
88 cfg := &packages.Config{Mode: packages.NeedName | packages.NeedFiles | packages.NeedTypes}
89 pkgs, err := packages.Load(cfg, "./...")
90 if err != nil {
91 fmt.Fprintf(os.Stderr, "load: %v\n", err)
92 os.Exit(1)
93 }
94 if packages.PrintErrors(pkgs) > 0 {
95 os.Exit(1)
96 }
97 // Sort the Go packages by import path; otherwise adding a new builtin package
98 // puts it at the very end of the list the first time it is getting added to register.go,
99 // as it's not imported by the root package yet. Sorting ensures consistent output.
100 slices.SortFunc(pkgs, func(a, b *packages.Package) int {
101 return cmp.Compare(a.PkgPath, b.PkgPath)
102 })
103
104 regBuf := new(bytes.Buffer)
105 fmt.Fprintf(regBuf, "// Code generated by cuelang.org/go/pkg/gen. DO NOT EDIT.\n\n")
106 fmt.Fprintf(regBuf, "package pkg\n\n")
107 fmt.Fprintf(regBuf, "import (\n")
108 for _, pkg := range pkgs {
109 switch {
110 case pkg.PkgPath == pkgParent:
111 // The pkg package itself should not be generated.
112 case strings.Contains(pkg.PkgPath, "/internal"):
113 // Internal packages are not for public use.
114 default:
115 fmt.Fprintf(regBuf, "\t_ %q\n", pkg.PkgPath)
116 if pkg.PkgPath == "cuelang.org/go/pkg/path" {
117 // TODO remove this special case. Currently the path
118 // pkg.go file cannot be generated automatically but that
119 // will be possible when we can attach arbitrary signatures
120 // to builtin functions.
121 break
122 }
123 if err := generate(pkg); err != nil {
124 log.Fatalf("%s: %v", pkg, err)
125 }
126 }
127 }
128 fmt.Fprintf(regBuf, ")\n")
129 if err := os.WriteFile("register.go", regBuf.Bytes(), 0o666); err != nil {
130 log.Fatal(err)
131 }
132}
133
134type generator struct {
135 dir string
136 w *bytes.Buffer
137 cuePkgPath string
138 first bool
139 nonConcrete bool
140}
141
142func generate(pkg *packages.Package) error {
143 // go/packages supports multiple build systems, including some which don't keep
144 // a Go package entirely within a single directory.
145 // However, we know for certain that CUE uses modules, so it is the case here.
146 // We can figure out the directory from the first Go file.
147 pkgDir := filepath.Dir(pkg.GoFiles[0])
148 cuePkg := strings.TrimPrefix(pkg.PkgPath, pkgParent+"/")
149 g := generator{
150 dir: pkgDir,
151 cuePkgPath: cuePkg,
152 w: &bytes.Buffer{},
153 }
154
155 params := headerParams{
156 GoPkg: pkg.Name,
157 CUEPkg: cuePkg,
158 }
159 // As a special case, the "tool" package cannot be imported from CUE.
160 skipRegister := params.CUEPkg == "tool"
161 if skipRegister {
162 params.CUEPkg = ""
163 }
164
165 if doc, err := os.ReadFile(filepath.Join(pkgDir, "doc.txt")); err == nil {
166 defs, err := os.ReadFile(filepath.Join(pkgDir, pkg.Name+".cue"))
167 if err != nil {
168 return err
169 }
170 i := bytes.Index(defs, []byte("package "+pkg.Name))
171 defs = defs[i+len("package "+pkg.Name)+1:]
172 defs = bytes.TrimRight(defs, "\n")
173 defs = bytes.ReplaceAll(defs, []byte("\n"), []byte("\n//\t"))
174 params.PackageDoc = string(doc)
175 params.PackageDefs = string(defs)
176 }
177
178 if err := header.Execute(g.w, params); err != nil {
179 return err
180 }
181
182 if !skipRegister {
183 fmt.Fprintf(g.w, "var p = &pkg.Package{\nNative: []*pkg.Builtin{")
184 g.first = true
185 if err := g.processGo(pkg); err != nil {
186 return err
187 }
188 fmt.Fprintf(g.w, "},\n")
189 if err := g.processCUE(); err != nil {
190 return err
191 }
192 fmt.Fprintf(g.w, "}\n")
193 }
194
195 b, err := format.Source(g.w.Bytes())
196 if err != nil {
197 fmt.Printf("go/format error on %s: %v\n", pkg.PkgPath, err)
198 b = g.w.Bytes() // write the unformatted source
199 }
200
201 filename := filepath.Join(pkgDir, "pkg.go")
202
203 if err := os.WriteFile(filename, b, 0666); err != nil {
204 return err
205 }
206 return nil
207}
208
209func (g *generator) sep() {
210 if g.first {
211 g.first = false
212 return
213 }
214 fmt.Fprint(g.w, ", ")
215}
216
217// processCUE mixes in CUE definitions defined in the package directory.
218func (g *generator) processCUE() error {
219 // Note: we avoid using the cue/load and the cuecontext packages
220 // because they depend on the standard library which is what this
221 // command is generating - cyclic dependencies are undesirable in general.
222 // We only need to load the declarations from one CUE file if it exists.
223 expr, err := loadCUEDecls(g.dir)
224 if err != nil {
225 return fmt.Errorf("error processing %s: %v", g.cuePkgPath, err)
226 }
227 if expr == nil { // No syntax to add.
228 return nil
229 }
230 b, err := cueformat.Node(expr)
231 if err != nil {
232 return err
233 }
234
235 // Compact the CUE by removing empty lines. This requires re-formatting to align fields.
236 // TODO(mvdan): provide a "compact" option in cue/format for this purpose?
237 b = bytes.ReplaceAll(b, []byte("\n\n"), []byte("\n"))
238 b, err = cueformat.Source(b)
239 if err != nil {
240 return err
241 }
242 b = bytes.TrimSpace(b) // no trailing newline
243
244 // Try to use a Go string with backquotes, for readability.
245 // If not possible due to cueSrc itself having backquotes,
246 // use a single-line double-quoted string, removing tabs for brevity.
247 // We don't use strconv.CanBackquote as it is for quoting as a single line.
248 if cueSrc := string(b); !strings.Contains(cueSrc, "`") {
249 fmt.Fprintf(g.w, "CUE: `%s`,\n", cueSrc)
250 } else {
251 cueSrc = strings.ReplaceAll(cueSrc, "\t", "")
252 fmt.Fprintf(g.w, "CUE: %q,\n", cueSrc)
253 }
254 return nil
255}
256
257func (g *generator) processGo(pkg *packages.Package) error {
258 // We sort the objects by their original source code position.
259 // Otherwise, go/types defaults to sorting by name strings.
260 // We could remove this code if we were fine with sorting by name.
261 scope := pkg.Types.Scope()
262 type objWithPos struct {
263 obj types.Object
264 pos token.Position
265 }
266 var objs []objWithPos
267 for _, name := range scope.Names() {
268 obj := scope.Lookup(name)
269 objs = append(objs, objWithPos{obj, pkg.Fset.Position(obj.Pos())})
270 }
271 slices.SortFunc(objs, func(a, b objWithPos) int {
272 if c := cmp.Compare(a.pos.Filename, b.pos.Filename); c != 0 {
273 return c
274 }
275 return cmp.Compare(a.pos.Line, b.pos.Line)
276 })
277
278 for _, obj := range objs {
279 obj := obj.obj // no longer need the token.Position
280 if !obj.Exported() {
281 continue
282 }
283 // TODO: support type declarations.
284 switch obj := obj.(type) {
285 case *types.Const:
286 var value string
287 switch v := obj.Val(); v.Kind() {
288 case constant.Bool, constant.Int, constant.String:
289 // TODO: convert octal numbers
290 value = v.ExactString()
291 case constant.Float:
292 var rat big.Rat
293 rat.SetString(v.ExactString())
294 var float big.Float
295 float.SetRat(&rat)
296 value = float.Text('g', -1)
297 default:
298 fmt.Printf("Dropped entry %s.%s (%T: %v)\n", g.cuePkgPath, obj.Name(), v.Kind(), v.ExactString())
299 continue
300 }
301 g.sep()
302 fmt.Fprintf(g.w, "{\nName: %q,\n Const: %q,\n}", obj.Name(), value)
303 case *types.Func:
304 g.genFunc(obj)
305 }
306 }
307 return nil
308}
309
310var (
311 typeError = types.Universe.Lookup("error").Type()
312 typeByte = types.Universe.Lookup("byte").Type()
313)
314
315func (g *generator) genFunc(fn *types.Func) {
316 g.nonConcrete = false
317 sign := fn.Signature()
318 if sign.Recv() != nil {
319 return
320 }
321 params := sign.Params()
322 results := sign.Results()
323 if results == nil || (results.Len() != 1 && results.At(1).Type() != typeError) {
324 fmt.Printf("Dropped func %s.%s: must have one return value or a value and an error %v\n", g.cuePkgPath, fn.Name(), sign)
325 return
326 }
327
328 g.sep()
329 fmt.Fprintf(g.w, "{\n")
330 defer fmt.Fprintf(g.w, "}")
331
332 fmt.Fprintf(g.w, "Name: %q,\n", fn.Name())
333
334 needCallContext := false
335 args := []string{}
336 vals := []string{}
337 kind := []string{}
338 for param := range params.Variables() {
339 typ := param.Type()
340 if typ.String() == "cuelang.org/go/internal/pkg.Schema" {
341 needCallContext = true
342 }
343 methodName := g.callCtxtGetter(typ)
344 argKind := g.adtKind(param.Type())
345 vals = append(vals, fmt.Sprintf("c.%s(%d)", methodName, len(args)))
346 args = append(args, param.Name())
347 kind = append(kind, argKind)
348 }
349
350 fmt.Fprintf(g.w, "Params: []pkg.Param{\n")
351 for _, k := range kind {
352 fmt.Fprintf(g.w, "{Kind: %s},\n", k)
353 }
354 fmt.Fprintf(g.w, "\n},\n")
355
356 fmt.Fprintf(g.w, "Result: %s,\n", g.adtKind(results.At(0).Type()))
357 if g.nonConcrete {
358 fmt.Fprintf(g.w, "NonConcrete: true,\n")
359 }
360
361 argList := strings.Join(args, ", ")
362 valList := strings.Join(vals, ", ")
363 init := ""
364 if len(args) > 0 {
365 init = fmt.Sprintf("%s := %s", argList, valList)
366 }
367
368 name := fn.Name()
369 if needCallContext {
370 argList = "c.OpContext(), " + argList
371
372 // Main function is used for Godoc documentation. Once we have proper
373 // CUE function signatures, we can remove these stubs.
374 // NOTE: this will not work for scripts that are not cased. But this
375 // is intended to be a temporary situation anyway.
376 name = strings.ToLower(name[:1]) + name[1:]
377 }
378
379 fmt.Fprintf(g.w, "Func: func(c *pkg.CallCtxt) {")
380 defer fmt.Fprintln(g.w, "},")
381 fmt.Fprintln(g.w)
382 if init != "" {
383 fmt.Fprintln(g.w, init)
384 }
385 fmt.Fprintln(g.w, "if c.Do() {")
386 defer fmt.Fprintln(g.w, "}")
387 if results.Len() == 1 {
388 fmt.Fprintf(g.w, "c.Ret = %s(%s)", name, argList)
389 } else {
390 fmt.Fprintf(g.w, "c.Ret, c.Err = %s(%s)", name, argList)
391 }
392}
393
394// callCtxtGetter returns the name of the [cuelang.org/go/internal/pkg.CallCtxt] method
395// which can be used to fetch a parameter of the given type.
396func (g *generator) callCtxtGetter(typ types.Type) string {
397 switch typ := typ.(type) {
398 case *types.Basic:
399 return strings.Title(typ.String()) // "int" turns into "Int"
400 case *types.Slice:
401 switch typ.Elem().String() {
402 case "byte":
403 return "Bytes"
404 case "string":
405 return "StringList"
406 case "*cuelang.org/go/internal.Decimal":
407 return "DecimalList"
408 }
409 return "List"
410 }
411 switch typ.String() {
412 case "*math/big.Int":
413 return "BigInt"
414 case "*math/big.Float":
415 return "BigFloat"
416 case "*cuelang.org/go/internal.Decimal":
417 return "Decimal"
418 case "cuelang.org/go/internal/pkg.List":
419 return "CueList"
420 case "cuelang.org/go/internal/pkg.Struct":
421 return "Struct"
422 case "cuelang.org/go/cue.Value",
423 "cuelang.org/go/cue/ast.Expr":
424 return "Value"
425 case "cuelang.org/go/internal/pkg.Schema":
426 g.nonConcrete = true
427 return "Schema"
428 case "io.Reader":
429 return "Reader"
430 }
431 log.Fatal("callCtxtGetter: unhandled Go type ", typ.String())
432 return ""
433}
434
435// adtKind provides a Go expression string which describes
436// a [cuelang.org/go/internal/core/adt.Kind] value for the given type.
437func (g *generator) adtKind(typ types.Type) string {
438 // TODO: detect list and structs types for return values.
439 switch typ := typ.(type) {
440 case *types.Slice:
441 if typ.Elem() == typeByte {
442 return "adt.BytesKind | adt.StringKind"
443 }
444 return "adt.ListKind"
445 case *types.Map:
446 return "adt.StructKind"
447 case *types.Basic:
448 if typ.Info()&types.IsInteger != 0 {
449 return "adt.IntKind"
450 }
451 if typ.Kind() == types.Float64 {
452 return "adt.NumberKind"
453 }
454 return "adt." + strings.Title(typ.String()) + "Kind" // "bool" turns into "adt.BoolKind"
455 }
456 switch typ.String() {
457 case "error":
458 return "adt.BottomKind"
459 case "io.Reader":
460 return "adt.BytesKind | adt.StringKind"
461 case "cuelang.org/go/internal/pkg.Struct":
462 return "adt.StructKind"
463 case "cuelang.org/go/internal/pkg.List":
464 return "adt.ListKind"
465 case "*math/big.Int":
466 return "adt.IntKind"
467 case "*cuelang.org/go/internal.Decimal", "*math/big.Float":
468 return "adt.NumberKind"
469 case "cuelang.org/go/cue.Value", "cuelang.org/go/cue/ast.Expr", "cuelang.org/go/internal/pkg.Schema":
470 return "adt.TopKind" // TODO: can be more precise
471
472 // Some builtin functions return custom types, like [cuelang.org/go/pkg/time.Split].
473 // TODO: we can simplify this once the CUE API declarations in ./pkg/...
474 // use CUE function signatures to validate their parameters and results.
475 case "*cuelang.org/go/pkg/time.Parts":
476 return "adt.StructKind"
477 case "*cuelang.org/go/pkg/net.ParsedCIDR":
478 return "adt.StructKind"
479 }
480 log.Fatal("adtKind: unhandled Go type ", typ.String())
481 return ""
482}
483
484// loadCUEDecls parses a single CUE file from a directory and returns its contents
485// as an expression, typically a struct holding all of a file's declarations.
486// If there are no CUE files, it returns (nil, nil).
487func loadCUEDecls(dir string) (ast.Expr, error) {
488 cuefiles, err := filepath.Glob(filepath.Join(dir, "*.cue"))
489 if err != nil || len(cuefiles) == 0 {
490 return nil, err
491 }
492 if len(cuefiles) == 0 {
493 return nil, nil
494 }
495 if len(cuefiles) > 1 {
496 // Supporting multiple CUE files would require merging declarations.
497 return nil, fmt.Errorf("multiple CUE files not supported in this generator")
498 }
499 src, err := os.ReadFile(cuefiles[0])
500 if err != nil {
501 return nil, err
502 }
503 file, err := parser.ParseFile(cuefiles[0], src)
504 if err != nil {
505 return nil, err
506 }
507 return internal.ToExpr(file), nil
508}