cuelang.org/go@v0.13.0/pkg/gen.go (about)

     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.
    22  package main
    23  
    24  import (
    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  
    50  type headerParams struct {
    51  	GoPkg  string
    52  	CUEPkg string
    53  
    54  	PackageDoc  string
    55  	PackageDefs string
    56  }
    57  
    58  var 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 -}}
    65  package {{.GoPkg}}
    66  
    67  {{if .CUEPkg -}}
    68  import (
    69  	"cuelang.org/go/internal/core/adt"
    70  	"cuelang.org/go/internal/pkg"
    71  )
    72  
    73  func init() {
    74  	pkg.Register({{printf "%q" .CUEPkg}}, p)
    75  }
    76  
    77  var _ = adt.TopKind // in case the adt package isn't used
    78  {{end}}
    79  `))
    80  
    81  const pkgParent = "cuelang.org/go/pkg"
    82  
    83  func 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  
   134  type generator struct {
   135  	dir         string
   136  	w           *bytes.Buffer
   137  	cuePkgPath  string
   138  	first       bool
   139  	nonConcrete bool
   140  }
   141  
   142  func 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  
   209  func (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.
   218  func (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  
   257  func (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  
   310  var (
   311  	typeError = types.Universe.Lookup("error").Type()
   312  	typeByte  = types.Universe.Lookup("byte").Type()
   313  )
   314  
   315  func (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 i := 0; i < params.Len(); i++ {
   339  		param := params.At(i)
   340  		typ := param.Type()
   341  		if typ.String() == "cuelang.org/go/internal/pkg.Schema" {
   342  			needCallContext = true
   343  		}
   344  		methodName := g.callCtxtGetter(typ)
   345  		argKind := g.adtKind(param.Type())
   346  		vals = append(vals, fmt.Sprintf("c.%s(%d)", methodName, len(args)))
   347  		args = append(args, param.Name())
   348  		kind = append(kind, argKind)
   349  	}
   350  
   351  	fmt.Fprintf(g.w, "Params: []pkg.Param{\n")
   352  	for _, k := range kind {
   353  		fmt.Fprintf(g.w, "{Kind: %s},\n", k)
   354  	}
   355  	fmt.Fprintf(g.w, "\n},\n")
   356  
   357  	fmt.Fprintf(g.w, "Result: %s,\n", g.adtKind(results.At(0).Type()))
   358  	if g.nonConcrete {
   359  		fmt.Fprintf(g.w, "NonConcrete: true,\n")
   360  	}
   361  
   362  	argList := strings.Join(args, ", ")
   363  	valList := strings.Join(vals, ", ")
   364  	init := ""
   365  	if len(args) > 0 {
   366  		init = fmt.Sprintf("%s := %s", argList, valList)
   367  	}
   368  
   369  	name := fn.Name()
   370  	if needCallContext {
   371  		argList = "c.OpContext(), " + argList
   372  
   373  		// Main function is used for Godoc documentation. Once we have proper
   374  		// CUE function signatures, we can remove these stubs.
   375  		// NOTE: this will not work for scripts that are not cased. But this
   376  		// is intended to be a temporary situation anyway.
   377  		name = strings.ToLower(name[:1]) + name[1:]
   378  	}
   379  
   380  	fmt.Fprintf(g.w, "Func: func(c *pkg.CallCtxt) {")
   381  	defer fmt.Fprintln(g.w, "},")
   382  	fmt.Fprintln(g.w)
   383  	if init != "" {
   384  		fmt.Fprintln(g.w, init)
   385  	}
   386  	fmt.Fprintln(g.w, "if c.Do() {")
   387  	defer fmt.Fprintln(g.w, "}")
   388  	if results.Len() == 1 {
   389  		fmt.Fprintf(g.w, "c.Ret = %s(%s)", name, argList)
   390  	} else {
   391  		fmt.Fprintf(g.w, "c.Ret, c.Err = %s(%s)", name, argList)
   392  	}
   393  }
   394  
   395  // callCtxtGetter returns the name of the [cuelang.org/go/internal/pkg.CallCtxt] method
   396  // which can be used to fetch a parameter of the given type.
   397  func (g *generator) callCtxtGetter(typ types.Type) string {
   398  	switch typ := typ.(type) {
   399  	case *types.Basic:
   400  		return strings.Title(typ.String()) // "int" turns into "Int"
   401  	case *types.Slice:
   402  		switch typ.Elem().String() {
   403  		case "byte":
   404  			return "Bytes"
   405  		case "string":
   406  			return "StringList"
   407  		case "*cuelang.org/go/internal.Decimal":
   408  			return "DecimalList"
   409  		}
   410  		return "List"
   411  	}
   412  	switch typ.String() {
   413  	case "*math/big.Int":
   414  		return "BigInt"
   415  	case "*math/big.Float":
   416  		return "BigFloat"
   417  	case "*cuelang.org/go/internal.Decimal":
   418  		return "Decimal"
   419  	case "cuelang.org/go/internal/pkg.List":
   420  		return "CueList"
   421  	case "cuelang.org/go/internal/pkg.Struct":
   422  		return "Struct"
   423  	case "cuelang.org/go/cue.Value",
   424  		"cuelang.org/go/cue/ast.Expr":
   425  		return "Value"
   426  	case "cuelang.org/go/internal/pkg.Schema":
   427  		g.nonConcrete = true
   428  		return "Schema"
   429  	case "io.Reader":
   430  		return "Reader"
   431  	}
   432  	log.Fatal("callCtxtGetter: unhandled Go type ", typ.String())
   433  	return ""
   434  }
   435  
   436  // adtKind provides a Go expression string which describes
   437  // a [cuelang.org/go/internal/core/adt.Kind] value for the given type.
   438  func (g *generator) adtKind(typ types.Type) string {
   439  	// TODO: detect list and structs types for return values.
   440  	switch typ := typ.(type) {
   441  	case *types.Slice:
   442  		if typ.Elem() == typeByte {
   443  			return "adt.BytesKind | adt.StringKind"
   444  		}
   445  		return "adt.ListKind"
   446  	case *types.Map:
   447  		return "adt.StructKind"
   448  	case *types.Basic:
   449  		if typ.Info()&types.IsInteger != 0 {
   450  			return "adt.IntKind"
   451  		}
   452  		if typ.Kind() == types.Float64 {
   453  			return "adt.NumberKind"
   454  		}
   455  		return "adt." + strings.Title(typ.String()) + "Kind" // "bool" turns into "adt.BoolKind"
   456  	}
   457  	switch typ.String() {
   458  	case "error":
   459  		return "adt.BottomKind"
   460  	case "io.Reader":
   461  		return "adt.BytesKind | adt.StringKind"
   462  	case "cuelang.org/go/internal/pkg.Struct":
   463  		return "adt.StructKind"
   464  	case "cuelang.org/go/internal/pkg.List":
   465  		return "adt.ListKind"
   466  	case "*math/big.Int":
   467  		return "adt.IntKind"
   468  	case "*cuelang.org/go/internal.Decimal", "*math/big.Float":
   469  		return "adt.NumberKind"
   470  	case "cuelang.org/go/cue.Value", "cuelang.org/go/cue/ast.Expr", "cuelang.org/go/internal/pkg.Schema":
   471  		return "adt.TopKind" // TODO: can be more precise
   472  
   473  	// Some builtin functions return custom types, like [cuelang.org/go/pkg/time.Split].
   474  	// TODO: we can simplify this once the CUE API declarations in ./pkg/...
   475  	// use CUE function signatures to validate their parameters and results.
   476  	case "*cuelang.org/go/pkg/time.Parts":
   477  		return "adt.StructKind"
   478  	}
   479  	log.Fatal("adtKind: unhandled Go type ", typ.String())
   480  	return ""
   481  }
   482  
   483  // loadCUEDecls parses a single CUE file from a directory and returns its contents
   484  // as an expression, typically a struct holding all of a file's declarations.
   485  // If there are no CUE files, it returns (nil, nil).
   486  func loadCUEDecls(dir string) (ast.Expr, error) {
   487  	cuefiles, err := filepath.Glob(filepath.Join(dir, "*.cue"))
   488  	if err != nil || len(cuefiles) == 0 {
   489  		return nil, err
   490  	}
   491  	if len(cuefiles) == 0 {
   492  		return nil, nil
   493  	}
   494  	if len(cuefiles) > 1 {
   495  		// Supporting multiple CUE files would require merging declarations.
   496  		return nil, fmt.Errorf("multiple CUE files not supported in this generator")
   497  	}
   498  	src, err := os.ReadFile(cuefiles[0])
   499  	if err != nil {
   500  		return nil, err
   501  	}
   502  	file, err := parser.ParseFile(cuefiles[0], src)
   503  	if err != nil {
   504  		return nil, err
   505  	}
   506  	return internal.ToExpr(file), nil
   507  }