cuelang.org/go@v0.13.0/internal/internal.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  // Package internal exposes some cue internals to other packages.
    16  //
    17  // A better name for this package would be technicaldebt.
    18  package internal
    19  
    20  // TODO: refactor packages as to make this package unnecessary.
    21  
    22  import (
    23  	"bufio"
    24  	"fmt"
    25  	"path/filepath"
    26  	"slices"
    27  	"strings"
    28  
    29  	"github.com/cockroachdb/apd/v3"
    30  
    31  	"cuelang.org/go/cue/ast"
    32  	"cuelang.org/go/cue/ast/astutil"
    33  	"cuelang.org/go/cue/errors"
    34  	"cuelang.org/go/cue/token"
    35  )
    36  
    37  // A Decimal is an arbitrary-precision binary-coded decimal number.
    38  //
    39  // Right now Decimal is aliased to apd.Decimal. This may change in the future.
    40  type Decimal = apd.Decimal
    41  
    42  // Context wraps apd.Context for CUE's custom logic.
    43  //
    44  // Note that it avoids pointers to make it easier to make copies.
    45  type Context struct {
    46  	apd.Context
    47  }
    48  
    49  // WithPrecision mirrors upstream, but returning our type without a pointer.
    50  func (c Context) WithPrecision(p uint32) Context {
    51  	c.Context = *c.Context.WithPrecision(p)
    52  	return c
    53  }
    54  
    55  // apd/v2 used to call Reduce on the result of Quo and Rem,
    56  // so that the operations always trimmed all but one trailing zeros.
    57  // apd/v3 does not do that at all.
    58  // For now, get the old behavior back by calling Reduce ourselves.
    59  // Note that v3's Reduce also removes all trailing zeros,
    60  // whereas v2's Reduce would leave ".0" behind.
    61  // Get that detail back as well, to consistently show floats with decimal points.
    62  //
    63  // TODO: Rather than reducing all trailing zeros,
    64  // we should keep a number of zeros that makes sense given the operation.
    65  
    66  func reduceKeepingFloats(d *apd.Decimal) {
    67  	oldExponent := d.Exponent
    68  	d.Reduce(d)
    69  	// If the decimal had decimal places, like "3.000" and "5.000E+5",
    70  	// Reduce gives us "3" and "5E+5", but we want "3.0" and "5.0E+5".
    71  	if oldExponent < 0 && d.Exponent >= 0 {
    72  		d.Exponent--
    73  		// TODO: we can likely make the NewBigInt(10) a static global to reduce allocs
    74  		d.Coeff.Mul(&d.Coeff, apd.NewBigInt(10))
    75  	}
    76  }
    77  
    78  func (c Context) Quo(d, x, y *apd.Decimal) (apd.Condition, error) {
    79  	res, err := c.Context.Quo(d, x, y)
    80  	reduceKeepingFloats(d)
    81  	return res, err
    82  }
    83  
    84  func (c Context) Sqrt(d, x *apd.Decimal) (apd.Condition, error) {
    85  	res, err := c.Context.Sqrt(d, x)
    86  	reduceKeepingFloats(d)
    87  	return res, err
    88  }
    89  
    90  // ErrIncomplete can be used by builtins to signal the evaluation was
    91  // incomplete.
    92  var ErrIncomplete = errors.New("incomplete value")
    93  
    94  // BaseContext is used as CUE's default context for arbitrary-precision decimals.
    95  var BaseContext = Context{*apd.BaseContext.WithPrecision(34)}
    96  
    97  // APIVersionSupported is the back version until which deprecated features
    98  // are still supported.
    99  var APIVersionSupported = Version(MinorSupported, PatchSupported)
   100  
   101  const (
   102  	MinorCurrent   = 5
   103  	MinorSupported = 4
   104  	PatchSupported = 0
   105  )
   106  
   107  func Version(minor, patch int) int {
   108  	return -1000 + 100*minor + patch
   109  }
   110  
   111  // EvaluatorVersion is declared here so it can be used everywhere without import cycles,
   112  // but the canonical documentation lives at [cuelang.org/go/cue/cuecontext.EvalVersion].
   113  //
   114  // TODO(mvdan): rename to EvalVersion for consistency with cuecontext.
   115  type EvaluatorVersion int
   116  
   117  const (
   118  	// EvalVersionUnset is the zero value, which signals that no evaluator version is provided.
   119  	EvalVersionUnset EvaluatorVersion = 0
   120  
   121  	// DefaultVersion is a special value as it selects a version depending on the current
   122  	// value of CUE_EXPERIMENT. It exists separately to [EvalVersionUnset], even though both
   123  	// implement the same version selection logic, so that we can distinguish between
   124  	// a user explicitly asking for the default version versus an entirely unset version.
   125  	DefaultVersion EvaluatorVersion = -1 // TODO(mvdan): rename to EvalDefault for consistency with cuecontext
   126  
   127  	// The values below are documented under [cuelang.org/go/cue/cuecontext.EvalVersion].
   128  	// We should never change or delete the values below, as they describe all known past versions
   129  	// which is useful for understanding old debug output.
   130  
   131  	EvalV2 EvaluatorVersion = 2
   132  	EvalV3 EvaluatorVersion = 3
   133  
   134  	// The current default, stable, and experimental versions.
   135  
   136  	StableVersion = EvalV3 // TODO(mvdan): rename to EvalStable for consistency with cuecontext
   137  	DevVersion    = EvalV3 // TODO(mvdan): rename to EvalExperiment for consistency with cuecontext
   138  )
   139  
   140  // ListEllipsis reports the list type and remaining elements of a list. If we
   141  // ever relax the usage of ellipsis, this function will likely change. Using
   142  // this function will ensure keeping correct behavior or causing a compiler
   143  // failure.
   144  func ListEllipsis(n *ast.ListLit) (elts []ast.Expr, e *ast.Ellipsis) {
   145  	elts = n.Elts
   146  	if n := len(elts); n > 0 {
   147  		var ok bool
   148  		if e, ok = elts[n-1].(*ast.Ellipsis); ok {
   149  			elts = elts[:n-1]
   150  		}
   151  	}
   152  	return elts, e
   153  }
   154  
   155  // Package finds the package declaration from the preamble of a file.
   156  func Package(f *ast.File) *ast.Package {
   157  	for _, d := range f.Decls {
   158  		switch d := d.(type) {
   159  		case *ast.CommentGroup:
   160  		case *ast.Attribute:
   161  		case *ast.Package:
   162  			if d.Name == nil { // malformed package declaration
   163  				return nil
   164  			}
   165  			return d
   166  		default:
   167  			return nil
   168  		}
   169  	}
   170  	return nil
   171  }
   172  
   173  func SetPackage(f *ast.File, name string, overwrite bool) {
   174  	if pkg := Package(f); pkg != nil {
   175  		if !overwrite || pkg.Name.Name == name {
   176  			return
   177  		}
   178  		ident := ast.NewIdent(name)
   179  		astutil.CopyMeta(ident, pkg.Name)
   180  		return
   181  	}
   182  
   183  	decls := make([]ast.Decl, len(f.Decls)+1)
   184  	k := 0
   185  	for _, d := range f.Decls {
   186  		if _, ok := d.(*ast.CommentGroup); ok {
   187  			decls[k] = d
   188  			k++
   189  			continue
   190  		}
   191  		break
   192  	}
   193  	decls[k] = &ast.Package{Name: ast.NewIdent(name)}
   194  	copy(decls[k+1:], f.Decls[k:])
   195  	f.Decls = decls
   196  }
   197  
   198  // NewComment creates a new CommentGroup from the given text.
   199  // Each line is prefixed with "//" and the last newline is removed.
   200  // Useful for ASTs generated by code other than the CUE parser.
   201  func NewComment(isDoc bool, s string) *ast.CommentGroup {
   202  	if s == "" {
   203  		return nil
   204  	}
   205  	cg := &ast.CommentGroup{Doc: isDoc}
   206  	if !isDoc {
   207  		cg.Line = true
   208  		cg.Position = 10
   209  	}
   210  	scanner := bufio.NewScanner(strings.NewReader(s))
   211  	for scanner.Scan() {
   212  		scanner := bufio.NewScanner(strings.NewReader(scanner.Text()))
   213  		scanner.Split(bufio.ScanWords)
   214  		const maxRunesPerLine = 66
   215  		count := 2
   216  		buf := strings.Builder{}
   217  		buf.WriteString("//")
   218  		for scanner.Scan() {
   219  			s := scanner.Text()
   220  			n := len([]rune(s)) + 1
   221  			if count+n > maxRunesPerLine && count > 3 {
   222  				cg.List = append(cg.List, &ast.Comment{Text: buf.String()})
   223  				count = 3
   224  				buf.Reset()
   225  				buf.WriteString("//")
   226  			}
   227  			buf.WriteString(" ")
   228  			buf.WriteString(s)
   229  			count += n
   230  		}
   231  		cg.List = append(cg.List, &ast.Comment{Text: buf.String()})
   232  	}
   233  	if last := len(cg.List) - 1; cg.List[last].Text == "//" {
   234  		cg.List = cg.List[:last]
   235  	}
   236  	return cg
   237  }
   238  
   239  func FileComments(f *ast.File) (docs, rest []*ast.CommentGroup) {
   240  	hasPkg := false
   241  	if pkg := Package(f); pkg != nil {
   242  		hasPkg = true
   243  		docs = pkg.Comments()
   244  	}
   245  
   246  	for _, c := range f.Comments() {
   247  		if c.Doc {
   248  			docs = append(docs, c)
   249  		} else {
   250  			rest = append(rest, c)
   251  		}
   252  	}
   253  
   254  	if !hasPkg && len(docs) == 0 && len(rest) > 0 {
   255  		// use the first file comment group as as doc comment.
   256  		docs, rest = rest[:1], rest[1:]
   257  		docs[0].Doc = true
   258  	}
   259  
   260  	return
   261  }
   262  
   263  // MergeDocs merges multiple doc comments into one single doc comment.
   264  func MergeDocs(comments []*ast.CommentGroup) []*ast.CommentGroup {
   265  	if len(comments) <= 1 || !hasDocComment(comments) {
   266  		return comments
   267  	}
   268  
   269  	comments1 := make([]*ast.CommentGroup, 0, len(comments))
   270  	comments1 = append(comments1, nil)
   271  	var docComment *ast.CommentGroup
   272  	for _, c := range comments {
   273  		switch {
   274  		case !c.Doc:
   275  			comments1 = append(comments1, c)
   276  		case docComment == nil:
   277  			docComment = c
   278  		default:
   279  			docComment.List = append(slices.Clip(docComment.List), &ast.Comment{Text: "//"})
   280  			docComment.List = append(docComment.List, c.List...)
   281  		}
   282  	}
   283  	comments1[0] = docComment
   284  	return comments1
   285  }
   286  
   287  func hasDocComment(comments []*ast.CommentGroup) bool {
   288  	for _, c := range comments {
   289  		if c.Doc {
   290  			return true
   291  		}
   292  	}
   293  	return false
   294  }
   295  
   296  func NewAttr(name, str string) *ast.Attribute {
   297  	buf := &strings.Builder{}
   298  	buf.WriteByte('@')
   299  	buf.WriteString(name)
   300  	buf.WriteByte('(')
   301  	buf.WriteString(str)
   302  	buf.WriteByte(')')
   303  	return &ast.Attribute{Text: buf.String()}
   304  }
   305  
   306  // ToExpr converts a node to an expression. If it is a file, it will return
   307  // it as a struct. If is an expression, it will return it as is. Otherwise
   308  // it panics.
   309  func ToExpr(n ast.Node) ast.Expr {
   310  	switch x := n.(type) {
   311  	case nil:
   312  		return nil
   313  
   314  	case ast.Expr:
   315  		return x
   316  
   317  	case *ast.File:
   318  		start := 0
   319  	outer:
   320  		for i, d := range x.Decls {
   321  			switch d.(type) {
   322  			case *ast.Package, *ast.ImportDecl:
   323  				start = i + 1
   324  			case *ast.CommentGroup, *ast.Attribute:
   325  			default:
   326  				break outer
   327  			}
   328  		}
   329  		decls := x.Decls[start:]
   330  		if len(decls) == 1 {
   331  			if e, ok := decls[0].(*ast.EmbedDecl); ok {
   332  				return e.Expr
   333  			}
   334  		}
   335  		return &ast.StructLit{Elts: decls}
   336  
   337  	default:
   338  		panic(fmt.Sprintf("Unsupported node type %T", x))
   339  	}
   340  }
   341  
   342  // ToFile converts an expression to a file.
   343  //
   344  // Adjusts the spacing of x when needed.
   345  func ToFile(n ast.Node) *ast.File {
   346  	if n == nil {
   347  		return nil
   348  	}
   349  	switch n := n.(type) {
   350  	case *ast.StructLit:
   351  		f := &ast.File{Decls: n.Elts}
   352  		// Ensure that the comments attached to the struct literal are not lost.
   353  		ast.SetComments(f, ast.Comments(n))
   354  		return f
   355  	case ast.Expr:
   356  		ast.SetRelPos(n, token.NoSpace)
   357  		return &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: n}}}
   358  	case *ast.File:
   359  		return n
   360  	default:
   361  		panic(fmt.Sprintf("Unsupported node type %T", n))
   362  	}
   363  }
   364  
   365  func IsDef(s string) bool {
   366  	return strings.HasPrefix(s, "#") || strings.HasPrefix(s, "_#")
   367  }
   368  
   369  func IsHidden(s string) bool {
   370  	return strings.HasPrefix(s, "_")
   371  }
   372  
   373  func IsDefOrHidden(s string) bool {
   374  	return strings.HasPrefix(s, "#") || strings.HasPrefix(s, "_")
   375  }
   376  
   377  func IsDefinition(label ast.Label) bool {
   378  	switch x := label.(type) {
   379  	case *ast.Alias:
   380  		if ident, ok := x.Expr.(*ast.Ident); ok {
   381  			return IsDef(ident.Name)
   382  		}
   383  	case *ast.Ident:
   384  		return IsDef(x.Name)
   385  	}
   386  	return false
   387  }
   388  
   389  func IsRegularField(f *ast.Field) bool {
   390  	var ident *ast.Ident
   391  	switch x := f.Label.(type) {
   392  	case *ast.Alias:
   393  		ident, _ = x.Expr.(*ast.Ident)
   394  	case *ast.Ident:
   395  		ident = x
   396  	}
   397  	if ident == nil {
   398  		return true
   399  	}
   400  	if strings.HasPrefix(ident.Name, "#") || strings.HasPrefix(ident.Name, "_") {
   401  		return false
   402  	}
   403  	return true
   404  }
   405  
   406  // ConstraintToken reports which constraint token (? or !) is associated
   407  // with a field (if any), taking into account compatibility of deprecated
   408  // fields.
   409  func ConstraintToken(f *ast.Field) (t token.Token, ok bool) {
   410  	if f.Constraint != token.ILLEGAL {
   411  		return f.Constraint, true
   412  	}
   413  	if f.Optional != token.NoPos {
   414  		return token.OPTION, true
   415  	}
   416  	return f.Constraint, false
   417  }
   418  
   419  // SetConstraints sets both the main and deprecated fields of f according to the
   420  // given constraint token.
   421  func SetConstraint(f *ast.Field, t token.Token) {
   422  	f.Constraint = t
   423  	if t == token.ILLEGAL {
   424  		f.Optional = token.NoPos
   425  	} else {
   426  		f.Optional = token.Blank.Pos()
   427  	}
   428  }
   429  
   430  func EmbedStruct(s *ast.StructLit) *ast.EmbedDecl {
   431  	e := &ast.EmbedDecl{Expr: s}
   432  	if len(s.Elts) == 1 {
   433  		d := s.Elts[0]
   434  		astutil.CopyPosition(e, d)
   435  		ast.SetRelPos(d, token.NoSpace)
   436  		astutil.CopyComments(e, d)
   437  		ast.SetComments(d, nil)
   438  		if f, ok := d.(*ast.Field); ok {
   439  			ast.SetRelPos(f.Label, token.NoSpace)
   440  		}
   441  	}
   442  	s.Lbrace = token.Newline.Pos()
   443  	s.Rbrace = token.NoSpace.Pos()
   444  	return e
   445  }
   446  
   447  // IsEllipsis reports whether the declaration can be represented as an ellipsis.
   448  func IsEllipsis(x ast.Decl) bool {
   449  	// ...
   450  	if _, ok := x.(*ast.Ellipsis); ok {
   451  		return true
   452  	}
   453  
   454  	// [string]: _ or [_]: _
   455  	f, ok := x.(*ast.Field)
   456  	if !ok {
   457  		return false
   458  	}
   459  	v, ok := f.Value.(*ast.Ident)
   460  	if !ok || v.Name != "_" {
   461  		return false
   462  	}
   463  	l, ok := f.Label.(*ast.ListLit)
   464  	if !ok || len(l.Elts) != 1 {
   465  		return false
   466  	}
   467  	i, ok := l.Elts[0].(*ast.Ident)
   468  	if !ok {
   469  		return false
   470  	}
   471  	return i.Name == "string" || i.Name == "_"
   472  }
   473  
   474  // GenPath reports the directory in which to store generated files.
   475  func GenPath(root string) string {
   476  	return filepath.Join(root, "cue.mod", "gen")
   477  }
   478  
   479  var ErrInexact = errors.New("inexact subsumption")