this repo has no description
at master 508 lines 14 kB view raw
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}