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.
18package internal
19
20// TODO: refactor packages as to make this package unnecessary.
21
22import (
23 "bufio"
24 "fmt"
25 "path/filepath"
26 "strings"
27 "unicode/utf8"
28
29 "github.com/cockroachdb/apd/v3"
30
31 "cuelang.org/go/cue/ast"
32 "cuelang.org/go/cue/token"
33)
34
35// A Decimal is an arbitrary-precision binary-coded decimal number.
36//
37// Right now Decimal is aliased to apd.Decimal. This may change in the future.
38type Decimal = apd.Decimal
39
40// Context wraps apd.Context for CUE's custom logic.
41//
42// Note that it avoids pointers to make it easier to make copies.
43type Context struct {
44 apd.Context
45}
46
47// WithPrecision mirrors upstream, but returning our type without a pointer.
48func (c Context) WithPrecision(p uint32) Context {
49 c.Context = *c.Context.WithPrecision(p)
50 return c
51}
52
53// apd/v2 used to call Reduce on the result of Quo and Rem,
54// so that the operations always trimmed all but one trailing zeros.
55// apd/v3 does not do that at all.
56// For now, get the old behavior back by calling Reduce ourselves.
57// Note that v3's Reduce also removes all trailing zeros,
58// whereas v2's Reduce would leave ".0" behind.
59// Get that detail back as well, to consistently show floats with decimal points.
60//
61// TODO: Rather than reducing all trailing zeros,
62// we should keep a number of zeros that makes sense given the operation.
63
64func reduceKeepingFloats(d *apd.Decimal) {
65 oldExponent := d.Exponent
66 d.Reduce(d)
67 // If the decimal had decimal places, like "3.000" and "5.000E+5",
68 // Reduce gives us "3" and "5E+5", but we want "3.0" and "5.0E+5".
69 if oldExponent < 0 && d.Exponent >= 0 {
70 d.Exponent--
71 // TODO: we can likely make the NewBigInt(10) a static global to reduce allocs
72 d.Coeff.Mul(&d.Coeff, apd.NewBigInt(10))
73 }
74}
75
76func (c Context) Quo(d, x, y *apd.Decimal) (apd.Condition, error) {
77 res, err := c.Context.Quo(d, x, y)
78 reduceKeepingFloats(d)
79 return res, err
80}
81
82func (c Context) Sqrt(d, x *apd.Decimal) (apd.Condition, error) {
83 res, err := c.Context.Sqrt(d, x)
84 reduceKeepingFloats(d)
85 return res, err
86}
87
88// BaseContext is used as CUE's default context for arbitrary-precision decimals.
89var BaseContext = Context{*apd.BaseContext.WithPrecision(34)}
90
91// EvaluatorVersion is declared here so it can be used everywhere without import cycles,
92// but the canonical documentation lives at [cuelang.org/go/cue/cuecontext.EvalVersion].
93//
94// TODO(mvdan): rename to EvalVersion for consistency with cuecontext.
95type EvaluatorVersion int
96
97const (
98 // EvalVersionUnset is the zero value, which signals that no evaluator version is provided.
99 EvalVersionUnset EvaluatorVersion = 0
100
101 // DefaultVersion is a special value as it selects a version depending on the current
102 // value of CUE_EXPERIMENT. It exists separately to [EvalVersionUnset], even though both
103 // implement the same version selection logic, so that we can distinguish between
104 // a user explicitly asking for the default version versus an entirely unset version.
105 DefaultVersion EvaluatorVersion = -1 // TODO(mvdan): rename to EvalDefault for consistency with cuecontext
106
107 // The values below are documented under [cuelang.org/go/cue/cuecontext.EvalVersion].
108 // We should never change or delete the values below, as they describe all known past versions
109 // which is useful for understanding old debug output.
110
111 EvalV2 EvaluatorVersion = 2
112 EvalV3 EvaluatorVersion = 3
113
114 // The current default, stable, and experimental versions.
115
116 StableVersion = EvalV3 // TODO(mvdan): rename to EvalStable for consistency with cuecontext
117 DevVersion = EvalV3 // TODO(mvdan): rename to EvalExperiment for consistency with cuecontext
118)
119
120// Package finds the package declaration from the preamble of a file,
121// returning it, and its index within the file's Decls.
122func Package(f *ast.File) (*ast.Package, int) {
123 for i, d := range f.Decls {
124 switch d := d.(type) {
125 case *ast.CommentGroup:
126 case *ast.Attribute:
127 case *ast.Package:
128 if d.Name == nil { // malformed package declaration
129 return nil, -1
130 }
131 return d, i
132 default:
133 return nil, -1
134 }
135 }
136 return nil, -1
137}
138
139// NewComment creates a new CommentGroup from the given text.
140// Each line is prefixed with "//" and the last newline is removed.
141// Useful for ASTs generated by code other than the CUE parser.
142func NewComment(isDoc bool, s string) *ast.CommentGroup {
143 if s == "" {
144 return nil
145 }
146 cg := &ast.CommentGroup{Doc: isDoc}
147 if !isDoc {
148 cg.Line = true
149 cg.Position = 10
150 }
151 scanner := bufio.NewScanner(strings.NewReader(s))
152 for scanner.Scan() {
153 scanner := bufio.NewScanner(strings.NewReader(scanner.Text()))
154 scanner.Split(bufio.ScanWords)
155 const maxRunesPerLine = 66
156 count := 2
157 buf := strings.Builder{}
158 buf.WriteString("//")
159 for scanner.Scan() {
160 s := scanner.Text()
161 n := utf8.RuneCountInString(s) + 1
162 if count+n > maxRunesPerLine && count > 3 {
163 cg.List = append(cg.List, &ast.Comment{Text: buf.String()})
164 count = 3
165 buf.Reset()
166 buf.WriteString("//")
167 }
168 buf.WriteString(" ")
169 buf.WriteString(s)
170 count += n
171 }
172 cg.List = append(cg.List, &ast.Comment{Text: buf.String()})
173 }
174 if last := len(cg.List) - 1; cg.List[last].Text == "//" {
175 cg.List = cg.List[:last]
176 }
177 return cg
178}
179
180func FileComments(f *ast.File) (docs, rest []*ast.CommentGroup) {
181 hasPkg := false
182 if pkg, _ := Package(f); pkg != nil {
183 hasPkg = true
184 docs = ast.Comments(pkg)
185 }
186
187 for _, c := range ast.Comments(f) {
188 if c.Doc {
189 docs = append(docs, c)
190 } else {
191 rest = append(rest, c)
192 }
193 }
194
195 if !hasPkg && len(docs) == 0 && len(rest) > 0 {
196 // use the first file comment group as as doc comment.
197 docs, rest = rest[:1], rest[1:]
198 docs[0].Doc = true
199 }
200
201 return
202}
203
204// ToExpr converts a node to an expression. If it is a file, it will return
205// it as a struct. If is an expression, it will return it as is. Otherwise
206// it panics.
207func ToExpr(n ast.Node) ast.Expr {
208 switch x := n.(type) {
209 case nil:
210 return nil
211
212 case ast.Expr:
213 return x
214
215 case *ast.File:
216 start := 0
217 outer:
218 for i, d := range x.Decls {
219 switch d.(type) {
220 case *ast.Package, *ast.ImportDecl:
221 start = i + 1
222 case *ast.CommentGroup, *ast.Attribute:
223 default:
224 break outer
225 }
226 }
227 decls := x.Decls[start:]
228 if len(decls) == 1 {
229 if e, ok := decls[0].(*ast.EmbedDecl); ok {
230 return e.Expr
231 }
232 }
233 return &ast.StructLit{Elts: decls}
234
235 default:
236 panic(fmt.Sprintf("Unsupported node type %T", x))
237 }
238}
239
240// ToFile converts an expression to a file.
241//
242// Adjusts the spacing of x when needed.
243func ToFile(n ast.Node) *ast.File {
244 if n == nil {
245 return nil
246 }
247 switch n := n.(type) {
248 case *ast.StructLit:
249 f := &ast.File{Decls: n.Elts}
250 // Ensure that the comments attached to the struct literal are not lost.
251 ast.SetComments(f, ast.Comments(n))
252 return f
253 case ast.Expr:
254 ast.SetRelPos(n, token.NoSpace)
255 return &ast.File{Decls: []ast.Decl{&ast.EmbedDecl{Expr: n}}}
256 case *ast.File:
257 return n
258 default:
259 panic(fmt.Sprintf("Unsupported node type %T", n))
260 }
261}
262
263func IsDef(s string) bool {
264 return strings.HasPrefix(s, "#") || strings.HasPrefix(s, "_#")
265}
266
267func IsHidden(s string) bool {
268 return strings.HasPrefix(s, "_")
269}
270
271func IsDefOrHidden(s string) bool {
272 return strings.HasPrefix(s, "#") || strings.HasPrefix(s, "_")
273}
274
275func IsDefinition(label ast.Label) bool {
276 switch x := label.(type) {
277 case *ast.Alias:
278 if ident, ok := x.Expr.(*ast.Ident); ok {
279 return IsDef(ident.Name)
280 }
281 case *ast.Ident:
282 return IsDef(x.Name)
283 }
284 return false
285}
286
287func IsRegularField(f *ast.Field) bool {
288 var ident *ast.Ident
289 switch x := f.Label.(type) {
290 case *ast.Alias:
291 ident, _ = x.Expr.(*ast.Ident)
292 case *ast.Ident:
293 ident = x
294 }
295 if ident == nil {
296 return true
297 }
298 if strings.HasPrefix(ident.Name, "#") || strings.HasPrefix(ident.Name, "_") {
299 return false
300 }
301 return true
302}
303
304// GenPath reports the directory in which to store generated files.
305func GenPath(root string) string {
306 return filepath.Join(root, "cue.mod", "gen")
307}