···11+// Copyright 2018 The CUE Authors
22+//
33+// Licensed under the Apache License, Version 2.0 (the "License");
44+// you may not use this file except in compliance with the License.
55+// You may obtain a copy of the License at
66+//
77+// http://www.apache.org/licenses/LICENSE-2.0
88+//
99+// Unless required by applicable law or agreed to in writing, software
1010+// distributed under the License is distributed on an "AS IS" BASIS,
1111+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212+// See the License for the specific language governing permissions and
1313+// limitations under the License.
1414+1515+package token
1616+1717+import (
1818+ "fmt"
1919+ "sort"
2020+ "sync"
2121+)
2222+2323+// -----------------------------------------------------------------------------
2424+// Positions
2525+2626+// Position describes an arbitrary source position
2727+// including the file, line, and column location.
2828+// A Position is valid if the line number is > 0.
2929+type Position struct {
3030+ Filename string // filename, if any
3131+ Offset int // offset, starting at 0
3232+ Line int // line number, starting at 1
3333+ Column int // column number, starting at 1 (byte count)
3434+ // RelPos Pos // relative position information
3535+}
3636+3737+// IsValid reports whether the position is valid.
3838+func (pos *Position) IsValid() bool { return pos.Line > 0 }
3939+4040+// String returns a string in one of several forms:
4141+//
4242+// file:line:column valid position with file name
4343+// line:column valid position without file name
4444+// file invalid position with file name
4545+// - invalid position without file name
4646+//
4747+func (pos Position) String() string {
4848+ s := pos.Filename
4949+ if pos.IsValid() {
5050+ if s != "" {
5151+ s += ":"
5252+ }
5353+ s += fmt.Sprintf("%d:%d", pos.Line, pos.Column)
5454+ }
5555+ if s == "" {
5656+ s = "-"
5757+ }
5858+ return s
5959+}
6060+6161+// Pos is a compact encoding of a source position within a file set, as well as
6262+// relative positioning information. It can be converted into a Position for a
6363+// more convenient, but much larger, representation.
6464+//
6565+// The Pos value for a given file is a number in the range [base, base+size],
6666+// where base and size are specified when adding the file to the file set via
6767+// AddFile.
6868+//
6969+// To create the Pos value for a specific source offset (measured in bytes),
7070+// first add the respective file to the current file set using FileSet.AddFile
7171+// and then call File.Pos(offset) for that file. Given a Pos value p for a
7272+// specific file set fset, the corresponding Position value is obtained by
7373+// calling fset.Position(p).
7474+//
7575+// Pos values can be compared directly with the usual comparison operators: If
7676+// two Pos values p and q are in the same file, comparing p and q is equivalent
7777+// to comparing the respective source file offsets. If p and q are in different
7878+// files, p < q is true if the file implied by p was added to the respective
7979+// file set before the file implied by cue.
8080+type Pos int
8181+8282+// NoPos is the zero value for Pos; there is no file and line information
8383+// associated with it, and NoPos().IsValid() is false. NoPos is always
8484+// smaller than any other Pos value. The corresponding Position value
8585+// for NoPos is the zero value for Position.
8686+const NoPos Pos = 0
8787+8888+// RelPos indicates the relative position of token to the previous token.
8989+type RelPos int
9090+9191+const (
9292+ // NoRelPos indicates no relative position is specified.
9393+ NoRelPos RelPos = iota
9494+9595+ // Elided indicates that the token for which this position is defined is
9696+ // not rendered at all.
9797+ Elided
9898+9999+ // NoSpace indicates there is no whitespace after this token.
100100+ NoSpace
101101+102102+ // Blank means there is horizontal space after this token.
103103+ Blank
104104+105105+ // Newline means there is a single newline after this token.
106106+ Newline
107107+108108+ // NewSection means there are two or more newlines after this token.
109109+ NewSection
110110+111111+ relMask Pos = 0xf
112112+ relShift = 4
113113+)
114114+115115+var relNames = []string{
116116+ "invalid", "elided", "nospace", "blank", "newline", "section",
117117+}
118118+119119+func (p RelPos) String() string { return relNames[p] }
120120+121121+// HasRelPos repors whether p has a relative position.
122122+func (p Pos) HasRelPos() bool {
123123+ return p&relMask != 0
124124+125125+}
126126+127127+func (p Pos) Add(n int) Pos {
128128+ return p + toPos(index(n))
129129+}
130130+131131+// IsValid reports whether the position is valid.
132132+func (p Pos) IsValid() bool {
133133+ return p != NoPos
134134+}
135135+136136+// IsNewline reports whether the relative information suggests this node should
137137+// be printed on a new lien.
138138+func (p Pos) IsNewline() bool {
139139+ return p.RelPos() >= Newline
140140+}
141141+142142+func (p Pos) WithRel(rel RelPos) Pos {
143143+ return p&^relMask | Pos(rel)
144144+}
145145+146146+func (p Pos) RelPos() RelPos {
147147+ return RelPos(p & relMask)
148148+}
149149+150150+func (p Pos) index() index {
151151+ return index(p) >> relShift
152152+}
153153+154154+func toPos(x index) Pos {
155155+ return (Pos(x) << relShift)
156156+}
157157+158158+// -----------------------------------------------------------------------------
159159+// File
160160+161161+type index int
162162+163163+// A File is a handle for a file belonging to a FileSet.
164164+// A File has a name, size, and line offset table.
165165+type File struct {
166166+ set *FileSet
167167+ name string // file name as provided to AddFile
168168+ base index // Pos index range for this file is [base...base+size]
169169+ size index // file size as provided to AddFile
170170+171171+ // lines and infos are protected by set.mutex
172172+ lines []index // lines contains the offset of the first character for each line (the first entry is always 0)
173173+ infos []lineInfo
174174+}
175175+176176+// Name returns the file name of file f as registered with AddFile.
177177+func (f *File) Name() string {
178178+ return f.name
179179+}
180180+181181+// Base returns the base offset of file f as registered with AddFile.
182182+func (f *File) Base() int {
183183+ return int(f.base)
184184+}
185185+186186+// Size returns the size of file f as registered with AddFile.
187187+func (f *File) Size() int {
188188+ return int(f.size)
189189+}
190190+191191+// LineCount returns the number of lines in file f.
192192+func (f *File) LineCount() int {
193193+ f.set.mutex.RLock()
194194+ n := len(f.lines)
195195+ f.set.mutex.RUnlock()
196196+ return n
197197+}
198198+199199+// AddLine adds the line offset for a new line.
200200+// The line offset must be larger than the offset for the previous line
201201+// and smaller than the file size; otherwise the line offset is ignored.
202202+//
203203+func (f *File) AddLine(offset int) {
204204+ x := index(offset)
205205+ f.set.mutex.Lock()
206206+ if i := len(f.lines); (i == 0 || f.lines[i-1] < x) && x < f.size {
207207+ f.lines = append(f.lines, x)
208208+ }
209209+ f.set.mutex.Unlock()
210210+}
211211+212212+// MergeLine merges a line with the following line. It is akin to replacing
213213+// the newline character at the end of the line with a space (to not change the
214214+// remaining offsets). To obtain the line number, consult e.g. Position.Line.
215215+// MergeLine will panic if given an invalid line number.
216216+//
217217+func (f *File) MergeLine(line int) {
218218+ if line <= 0 {
219219+ panic("illegal line number (line numbering starts at 1)")
220220+ }
221221+ f.set.mutex.Lock()
222222+ defer f.set.mutex.Unlock()
223223+ if line >= len(f.lines) {
224224+ panic("illegal line number")
225225+ }
226226+ // To merge the line numbered <line> with the line numbered <line+1>,
227227+ // we need to remove the entry in lines corresponding to the line
228228+ // numbered <line+1>. The entry in lines corresponding to the line
229229+ // numbered <line+1> is located at index <line>, since indices in lines
230230+ // are 0-based and line numbers are 1-based.
231231+ copy(f.lines[line:], f.lines[line+1:])
232232+ f.lines = f.lines[:len(f.lines)-1]
233233+}
234234+235235+// SetLines sets the line offsets for a file and reports whether it succeeded.
236236+// The line offsets are the offsets of the first character of each line;
237237+// for instance for the content "ab\nc\n" the line offsets are {0, 3}.
238238+// An empty file has an empty line offset table.
239239+// Each line offset must be larger than the offset for the previous line
240240+// and smaller than the file size; otherwise SetLines fails and returns
241241+// false.
242242+// Callers must not mutate the provided slice after SetLines returns.
243243+//
244244+func (f *File) SetLines(lines []int) bool {
245245+ // verify validity of lines table
246246+ size := f.size
247247+ for i, offset := range lines {
248248+ if i > 0 && offset <= lines[i-1] || size <= index(offset) {
249249+ return false
250250+ }
251251+ }
252252+253253+ // set lines table
254254+ f.set.mutex.Lock()
255255+ f.lines = f.lines[:0]
256256+ for _, l := range lines {
257257+ f.lines = append(f.lines, index(l))
258258+ }
259259+ f.set.mutex.Unlock()
260260+ return true
261261+}
262262+263263+// SetLinesForContent sets the line offsets for the given file content.
264264+// It ignores position-altering //line comments.
265265+func (f *File) SetLinesForContent(content []byte) {
266266+ var lines []index
267267+ line := index(0)
268268+ for offset, b := range content {
269269+ if line >= 0 {
270270+ lines = append(lines, line)
271271+ }
272272+ line = -1
273273+ if b == '\n' {
274274+ line = index(offset) + 1
275275+ }
276276+ }
277277+278278+ // set lines table
279279+ f.set.mutex.Lock()
280280+ f.lines = lines
281281+ f.set.mutex.Unlock()
282282+}
283283+284284+// A lineInfo object describes alternative file and line number
285285+// information (such as provided via a //line comment in a .go
286286+// file) for a given file offset.
287287+type lineInfo struct {
288288+ // fields are exported to make them accessible to gob
289289+ Offset int
290290+ Filename string
291291+ Line int
292292+}
293293+294294+// AddLineInfo adds alternative file and line number information for
295295+// a given file offset. The offset must be larger than the offset for
296296+// the previously added alternative line info and smaller than the
297297+// file size; otherwise the information is ignored.
298298+//
299299+// AddLineInfo is typically used to register alternative position
300300+// information for //line filename:line comments in source files.
301301+//
302302+func (f *File) AddLineInfo(offset int, filename string, line int) {
303303+ x := index(offset)
304304+ f.set.mutex.Lock()
305305+ if i := len(f.infos); i == 0 || index(f.infos[i-1].Offset) < x && x < f.size {
306306+ f.infos = append(f.infos, lineInfo{offset, filename, line})
307307+ }
308308+ f.set.mutex.Unlock()
309309+}
310310+311311+// Pos returns the Pos value for the given file offset;
312312+// the offset must be <= f.Size().
313313+// f.Pos(f.Offset(p)) == p.
314314+//
315315+func (f *File) Pos(offset int, rel RelPos) Pos {
316316+ if index(offset) > f.size {
317317+ panic("illegal file offset")
318318+ }
319319+ return toPos(f.base+index(offset)) + Pos(rel)
320320+}
321321+322322+// Offset returns the offset for the given file position p;
323323+// p must be a valid Pos value in that file.
324324+// f.Offset(f.Pos(offset)) == offset.
325325+//
326326+func (f *File) Offset(p Pos) int {
327327+ x := p.index()
328328+ if x < f.base || x > f.base+index(f.size) {
329329+ panic("illegal Pos value")
330330+ }
331331+ return int(x - f.base)
332332+}
333333+334334+// Line returns the line number for the given file position p;
335335+// p must be a Pos value in that file or NoPos.
336336+//
337337+func (f *File) Line(p Pos) int {
338338+ return f.Position(p).Line
339339+}
340340+341341+func searchLineInfos(a []lineInfo, x int) int {
342342+ return sort.Search(len(a), func(i int) bool { return a[i].Offset > x }) - 1
343343+}
344344+345345+// unpack returns the filename and line and column number for a file offset.
346346+// If adjusted is set, unpack will return the filename and line information
347347+// possibly adjusted by //line comments; otherwise those comments are ignored.
348348+//
349349+func (f *File) unpack(offset index, adjusted bool) (filename string, line, column int) {
350350+ filename = f.name
351351+ if i := searchInts(f.lines, offset); i >= 0 {
352352+ line, column = int(i+1), int(offset-f.lines[i]+1)
353353+ }
354354+ if adjusted && len(f.infos) > 0 {
355355+ // almost no files have extra line infos
356356+ if i := searchLineInfos(f.infos, int(offset)); i >= 0 {
357357+ alt := &f.infos[i]
358358+ filename = alt.Filename
359359+ if i := searchInts(f.lines, index(alt.Offset)); i >= 0 {
360360+ line += alt.Line - i - 1
361361+ }
362362+ }
363363+ }
364364+ return
365365+}
366366+367367+func (f *File) position(p Pos, adjusted bool) (pos Position) {
368368+ offset := p.index() - f.base
369369+ pos.Offset = int(offset)
370370+ pos.Filename, pos.Line, pos.Column = f.unpack(offset, adjusted)
371371+ return
372372+}
373373+374374+// PositionFor returns the Position value for the given file position p.
375375+// If adjusted is set, the position may be adjusted by position-altering
376376+// //line comments; otherwise those comments are ignored.
377377+// p must be a Pos value in f or NoPos.
378378+//
379379+func (f *File) PositionFor(p Pos, adjusted bool) (pos Position) {
380380+ x := p.index()
381381+ if p != NoPos {
382382+ if x < f.base || x > f.base+f.size {
383383+ panic("illegal Pos value")
384384+ }
385385+ pos = f.position(p, adjusted)
386386+ }
387387+ return
388388+}
389389+390390+// Position returns the Position value for the given file position p.
391391+// Calling f.Position(p) is equivalent to calling f.PositionFor(p, true).
392392+//
393393+func (f *File) Position(p Pos) (pos Position) {
394394+ return f.PositionFor(p, true)
395395+}
396396+397397+// A FileSet represents a set of source files.
398398+// Methods of file sets are synchronized; multiple goroutines
399399+// may invoke them concurrently.
400400+type FileSet struct {
401401+ mutex sync.RWMutex // protects the file set
402402+ base int // base offset for the next file
403403+ files []*File // list of files in the order added to the set
404404+ last *File // cache of last file looked up
405405+}
406406+407407+// NewFileSet creates a new file set.
408408+func NewFileSet() *FileSet {
409409+ return &FileSet{
410410+ base: 1, // 0 == NoPos
411411+ }
412412+}
413413+414414+// Base returns the minimum base offset that must be provided to
415415+// AddFile when adding the next file.
416416+func (s *FileSet) Base() int {
417417+ s.mutex.RLock()
418418+ b := s.base
419419+ s.mutex.RUnlock()
420420+ return b
421421+422422+}
423423+424424+// AddFile adds a new file with a given filename, base offset, and file size
425425+// to the file set s and returns the file. Multiple files may have the same
426426+// name. The base offset must not be smaller than the FileSet's Base(), and
427427+// size must not be negative. As a special case, if a negative base is provided,
428428+// the current value of the FileSet's Base() is used instead.
429429+//
430430+// Adding the file will set the file set's Base() value to base + size + 1
431431+// as the minimum base value for the next file. The following relationship
432432+// exists between a Pos value p for a given file offset offs:
433433+//
434434+// int(p) = base + offs
435435+//
436436+// with offs in the range [0, size] and thus p in the range [base, base+size].
437437+// For convenience, File.Pos may be used to create file-specific position
438438+// values from a file offset.
439439+func (s *FileSet) AddFile(filename string, base, size int) *File {
440440+ s.mutex.Lock()
441441+ defer s.mutex.Unlock()
442442+ if base < 0 {
443443+ base = s.base
444444+ }
445445+ if base < s.base || size < 0 {
446446+ panic("illegal base or size")
447447+ }
448448+ // base >= s.base && size >= 0
449449+ f := &File{s, filename, index(base), index(size), []index{0}, nil}
450450+ base += size + 1 // +1 because EOF also has a position
451451+ if base < 0 {
452452+ panic("token.Pos offset overflow (> 2G of source code in file set)")
453453+ }
454454+ // add the file to the file set
455455+ s.base = base
456456+ s.files = append(s.files, f)
457457+ s.last = f
458458+ return f
459459+}
460460+461461+// Iterate calls f for the files in the file set in the order they were added
462462+// until f returns false.
463463+//
464464+func (s *FileSet) Iterate(f func(*File) bool) {
465465+ for i := 0; ; i++ {
466466+ var file *File
467467+ s.mutex.RLock()
468468+ if i < len(s.files) {
469469+ file = s.files[i]
470470+ }
471471+ s.mutex.RUnlock()
472472+ if file == nil || !f(file) {
473473+ break
474474+ }
475475+ }
476476+}
477477+478478+func searchFiles(a []*File, x index) int {
479479+ return sort.Search(len(a), func(i int) bool { return a[i].base > x }) - 1
480480+}
481481+482482+func (s *FileSet) file(p Pos) *File {
483483+ x := p.index()
484484+ s.mutex.RLock()
485485+ // common case: p is in last file
486486+ if f := s.last; f != nil && f.base <= x && x <= f.base+f.size {
487487+ s.mutex.RUnlock()
488488+ return f
489489+ }
490490+ // p is not in last file - search all files
491491+ if i := searchFiles(s.files, x); i >= 0 {
492492+ f := s.files[i]
493493+ // f.base <= int(p) by definition of searchFiles
494494+ if x <= f.base+f.size {
495495+ s.mutex.RUnlock()
496496+ s.mutex.Lock()
497497+ s.last = f // race is ok - s.last is only a cache
498498+ s.mutex.Unlock()
499499+ return f
500500+ }
501501+ }
502502+ s.mutex.RUnlock()
503503+ return nil
504504+}
505505+506506+// File returns the file that contains the position p.
507507+// If no such file is found (for instance for p == NoPos),
508508+// the result is nil.
509509+//
510510+func (s *FileSet) File(p Pos) (f *File) {
511511+ if p.index() != 0 {
512512+ f = s.file(p)
513513+ }
514514+ return
515515+}
516516+517517+// PositionFor converts a Pos p in the fileset into a Position value.
518518+// If adjusted is set, the position may be adjusted by position-altering
519519+// //line comments; otherwise those comments are ignored.
520520+// p must be a Pos value in s or NoPos.
521521+//
522522+func (s *FileSet) PositionFor(p Pos, adjusted bool) (pos Position) {
523523+ if p.index() != 0 {
524524+ if f := s.file(p); f != nil {
525525+ s.mutex.RLock()
526526+ pos = f.position(p, adjusted)
527527+ s.mutex.RUnlock()
528528+ }
529529+ }
530530+ return
531531+}
532532+533533+// Position converts a Pos p in the fileset into a Position value.
534534+// Calling s.Position(p) is equivalent to calling s.PositionFor(p, true).
535535+//
536536+func (s *FileSet) Position(p Pos) (pos Position) {
537537+ return s.PositionFor(p, true)
538538+}
539539+540540+// -----------------------------------------------------------------------------
541541+// Helper functions
542542+543543+func searchInts(a []index, x index) int {
544544+ // This function body is a manually inlined version of:
545545+ //
546546+ // return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1
547547+ //
548548+ // With better compiler optimizations, this may not be needed in the
549549+ // future, but at the moment this change improves the go/printer
550550+ // benchmark performance by ~30%. This has a direct impact on the
551551+ // speed of gofmt and thus seems worthwhile (2011-04-29).
552552+ // TODO(gri): Remove this when compilers have caught up.
553553+ i, j := 0, len(a)
554554+ for i < j {
555555+ h := i + (j-i)/2 // avoid overflow when computing h
556556+ // i ≤ h < j
557557+ if a[h] <= x {
558558+ i = h + 1
559559+ } else {
560560+ j = h
561561+ }
562562+ }
563563+ return i - 1
564564+}
+336
cue/token/position_test.go
···11+// Copyright 2018 The CUE Authors
22+//
33+// Licensed under the Apache License, Version 2.0 (the "License");
44+// you may not use this file except in compliance with the License.
55+// You may obtain a copy of the License at
66+//
77+// http://www.apache.org/licenses/LICENSE-2.0
88+//
99+// Unless required by applicable law or agreed to in writing, software
1010+// distributed under the License is distributed on an "AS IS" BASIS,
1111+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212+// See the License for the specific language governing permissions and
1313+// limitations under the License.
1414+1515+package token
1616+1717+import (
1818+ "fmt"
1919+ "math/rand"
2020+ "sync"
2121+ "testing"
2222+)
2323+2424+func checkPos(t *testing.T, msg string, got, want Position) {
2525+ if got.Filename != want.Filename {
2626+ t.Errorf("%s: got filename = %q; want %q", msg, got.Filename, want.Filename)
2727+ }
2828+ if got.Offset != want.Offset {
2929+ t.Errorf("%s: got offset = %d; want %d", msg, got.Offset, want.Offset)
3030+ }
3131+ if got.Line != want.Line {
3232+ t.Errorf("%s: got line = %d; want %d", msg, got.Line, want.Line)
3333+ }
3434+ if got.Column != want.Column {
3535+ t.Errorf("%s: got column = %d; want %d", msg, got.Column, want.Column)
3636+ }
3737+}
3838+3939+func TestNoPos(t *testing.T) {
4040+ if NoPos.IsValid() {
4141+ t.Errorf("NoPos should not be valid")
4242+ }
4343+ var fset *FileSet
4444+ checkPos(t, "nil NoPos", fset.Position(NoPos), Position{})
4545+ fset = NewFileSet()
4646+ checkPos(t, "fset NoPos", fset.Position(NoPos), Position{})
4747+}
4848+4949+var tests = []struct {
5050+ filename string
5151+ source []byte // may be nil
5252+ size int
5353+ lines []int
5454+}{
5555+ {"a", []byte{}, 0, []int{}},
5656+ {"b", []byte("01234"), 5, []int{0}},
5757+ {"c", []byte("\n\n\n\n\n\n\n\n\n"), 9, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}},
5858+ {"d", nil, 100, []int{0, 5, 10, 20, 30, 70, 71, 72, 80, 85, 90, 99}},
5959+ {"e", nil, 777, []int{0, 80, 100, 120, 130, 180, 267, 455, 500, 567, 620}},
6060+ {"f", []byte("package p\n\nimport \"fmt\""), 23, []int{0, 10, 11}},
6161+ {"g", []byte("package p\n\nimport \"fmt\"\n"), 24, []int{0, 10, 11}},
6262+ {"h", []byte("package p\n\nimport \"fmt\"\n "), 25, []int{0, 10, 11, 24}},
6363+}
6464+6565+func linecol(lines []int, offs int) (int, int) {
6666+ prevLineOffs := 0
6767+ for line, lineOffs := range lines {
6868+ if offs < lineOffs {
6969+ return line, offs - prevLineOffs + 1
7070+ }
7171+ prevLineOffs = lineOffs
7272+ }
7373+ return len(lines), offs - prevLineOffs + 1
7474+}
7575+7676+func verifyPositions(t *testing.T, fset *FileSet, f *File, lines []int) {
7777+ for offs := 0; offs < f.Size(); offs++ {
7878+ p := f.Pos(offs, 0)
7979+ offs2 := f.Offset(p)
8080+ if offs2 != offs {
8181+ t.Errorf("%s, Offset: got offset %d; want %d", f.Name(), offs2, offs)
8282+ }
8383+ line, col := linecol(lines, offs)
8484+ msg := fmt.Sprintf("%s (offs = %d, p = %d)", f.Name(), offs, p)
8585+ checkPos(t, msg, f.Position(f.Pos(offs, 0)), Position{f.Name(), offs, line, col})
8686+ checkPos(t, msg, fset.Position(p), Position{f.Name(), offs, line, col})
8787+ }
8888+}
8989+9090+func makeTestSource(size int, lines []int) []byte {
9191+ src := make([]byte, size)
9292+ for _, offs := range lines {
9393+ if offs > 0 {
9494+ src[offs-1] = '\n'
9595+ }
9696+ }
9797+ return src
9898+}
9999+100100+func TestPositions(t *testing.T) {
101101+ const delta = 7 // a non-zero base offset increment
102102+ fset := NewFileSet()
103103+ for _, test := range tests {
104104+ // verify consistency of test case
105105+ if test.source != nil && len(test.source) != test.size {
106106+ t.Errorf("%s: inconsistent test case: got file size %d; want %d", test.filename, len(test.source), test.size)
107107+ }
108108+109109+ // add file and verify name and size
110110+ f := fset.AddFile(test.filename, fset.Base()+delta, test.size)
111111+ if f.Name() != test.filename {
112112+ t.Errorf("got filename %q; want %q", f.Name(), test.filename)
113113+ }
114114+ if f.Size() != test.size {
115115+ t.Errorf("%s: got file size %d; want %d", f.Name(), f.Size(), test.size)
116116+ }
117117+ if fset.File(f.Pos(0, 0)) != f {
118118+ t.Errorf("%s: f.Pos(0, 0) was not found in f", f.Name())
119119+ }
120120+121121+ // add lines individually and verify all positions
122122+ for i, offset := range test.lines {
123123+ f.AddLine(offset)
124124+ if f.LineCount() != i+1 {
125125+ t.Errorf("%s, AddLine: got line count %d; want %d", f.Name(), f.LineCount(), i+1)
126126+ }
127127+ // adding the same offset again should be ignored
128128+ f.AddLine(offset)
129129+ if f.LineCount() != i+1 {
130130+ t.Errorf("%s, AddLine: got unchanged line count %d; want %d", f.Name(), f.LineCount(), i+1)
131131+ }
132132+ verifyPositions(t, fset, f, test.lines[0:i+1])
133133+ }
134134+135135+ // add lines with SetLines and verify all positions
136136+ if ok := f.SetLines(test.lines); !ok {
137137+ t.Errorf("%s: SetLines failed", f.Name())
138138+ }
139139+ if f.LineCount() != len(test.lines) {
140140+ t.Errorf("%s, SetLines: got line count %d; want %d", f.Name(), f.LineCount(), len(test.lines))
141141+ }
142142+ verifyPositions(t, fset, f, test.lines)
143143+144144+ // add lines with SetLinesForContent and verify all positions
145145+ src := test.source
146146+ if src == nil {
147147+ // no test source available - create one from scratch
148148+ src = makeTestSource(test.size, test.lines)
149149+ }
150150+ f.SetLinesForContent(src)
151151+ if f.LineCount() != len(test.lines) {
152152+ t.Errorf("%s, SetLinesForContent: got line count %d; want %d", f.Name(), f.LineCount(), len(test.lines))
153153+ }
154154+ verifyPositions(t, fset, f, test.lines)
155155+ }
156156+}
157157+158158+func TestLineInfo(t *testing.T) {
159159+ fset := NewFileSet()
160160+ f := fset.AddFile("foo", fset.Base(), 500)
161161+ lines := []int{0, 42, 77, 100, 210, 220, 277, 300, 333, 401}
162162+ // add lines individually and provide alternative line information
163163+ for _, offs := range lines {
164164+ f.AddLine(offs)
165165+ f.AddLineInfo(offs, "bar", 42)
166166+ }
167167+ // verify positions for all offsets
168168+ for offs := 0; offs <= f.Size(); offs++ {
169169+ p := f.Pos(offs, 0)
170170+ _, col := linecol(lines, offs)
171171+ msg := fmt.Sprintf("%s (offs = %d, p = %d)", f.Name(), offs, p)
172172+ checkPos(t, msg, f.Position(f.Pos(offs, 0)), Position{"bar", offs, 42, col})
173173+ checkPos(t, msg, fset.Position(p), Position{"bar", offs, 42, col})
174174+ }
175175+}
176176+177177+func TestFiles(t *testing.T) {
178178+ fset := NewFileSet()
179179+ for i, test := range tests {
180180+ base := fset.Base()
181181+ if i%2 == 1 {
182182+ // Setting a negative base is equivalent to
183183+ // fset.Base(), so test some of each.
184184+ base = -1
185185+ }
186186+ fset.AddFile(test.filename, base, test.size)
187187+ j := 0
188188+ fset.Iterate(func(f *File) bool {
189189+ if f.Name() != tests[j].filename {
190190+ t.Errorf("got filename = %s; want %s", f.Name(), tests[j].filename)
191191+ }
192192+ j++
193193+ return true
194194+ })
195195+ if j != i+1 {
196196+ t.Errorf("got %d files; want %d", j, i+1)
197197+ }
198198+ }
199199+}
200200+201201+// FileSet.File should return nil if Pos is past the end of the FileSet.
202202+func TestFileSetPastEnd(t *testing.T) {
203203+ fset := NewFileSet()
204204+ for _, test := range tests {
205205+ fset.AddFile(test.filename, fset.Base(), test.size)
206206+ }
207207+ if f := fset.File(toPos(index(fset.Base()))); f != nil {
208208+ t.Errorf("got %v, want nil", f)
209209+ }
210210+}
211211+212212+func TestFileSetCacheUnlikely(t *testing.T) {
213213+ fset := NewFileSet()
214214+ offsets := make(map[string]index)
215215+ for _, test := range tests {
216216+ offsets[test.filename] = index(fset.Base())
217217+ fset.AddFile(test.filename, fset.Base(), test.size)
218218+ }
219219+ for file, pos := range offsets {
220220+ f := fset.File(toPos(pos))
221221+ if f.Name() != file {
222222+ t.Errorf("got %q at position %d, want %q", f.Name(), pos, file)
223223+ }
224224+ }
225225+}
226226+227227+// issue 4345. Test that concurrent use of FileSet.Pos does not trigger a
228228+// race in the FileSet position cache.
229229+func TestFileSetRace(t *testing.T) {
230230+ fset := NewFileSet()
231231+ for i := 0; i < 100; i++ {
232232+ fset.AddFile(fmt.Sprintf("file-%d", i), fset.Base(), 1031)
233233+ }
234234+ max := int32(fset.Base())
235235+ var stop sync.WaitGroup
236236+ r := rand.New(rand.NewSource(7))
237237+ for i := 0; i < 2; i++ {
238238+ r := rand.New(rand.NewSource(r.Int63()))
239239+ stop.Add(1)
240240+ go func() {
241241+ for i := 0; i < 1000; i++ {
242242+ fset.Position(Pos(r.Int31n(max)))
243243+ }
244244+ stop.Done()
245245+ }()
246246+ }
247247+ stop.Wait()
248248+}
249249+250250+// issue 16548. Test that concurrent use of File.AddLine and FileSet.PositionFor
251251+// does not trigger a race in the FileSet position cache.
252252+func TestFileSetRace2(t *testing.T) {
253253+ const N = 1e3
254254+ var (
255255+ fset = NewFileSet()
256256+ file = fset.AddFile("", -1, N)
257257+ ch = make(chan int, 2)
258258+ )
259259+260260+ go func() {
261261+ for i := 0; i < N; i++ {
262262+ file.AddLine(i)
263263+ }
264264+ ch <- 1
265265+ }()
266266+267267+ go func() {
268268+ pos := file.Pos(0, 0)
269269+ for i := 0; i < N; i++ {
270270+ fset.PositionFor(pos, false)
271271+ }
272272+ ch <- 1
273273+ }()
274274+275275+ <-ch
276276+ <-ch
277277+}
278278+279279+func TestPositionFor(t *testing.T) {
280280+ src := []byte(`
281281+foo
282282+b
283283+ar
284284+//line :100
285285+foobar
286286+//line bar:3
287287+done
288288+`)
289289+290290+ const filename = "foo"
291291+ fset := NewFileSet()
292292+ f := fset.AddFile(filename, fset.Base(), len(src))
293293+ f.SetLinesForContent(src)
294294+295295+ // verify position info
296296+ for i, offs := range f.lines {
297297+ got1 := f.PositionFor(f.Pos(int(offs), 0), false)
298298+ got2 := f.PositionFor(f.Pos(int(offs), 0), true)
299299+ got3 := f.Position(f.Pos(int(offs), 0))
300300+ want := Position{filename, int(offs), i + 1, 1}
301301+ checkPos(t, "1. PositionFor unadjusted", got1, want)
302302+ checkPos(t, "1. PositionFor adjusted", got2, want)
303303+ checkPos(t, "1. Position", got3, want)
304304+ }
305305+306306+ // manually add //line info on lines l1, l2
307307+ const l1, l2 = 5, 7
308308+ f.AddLineInfo(int(f.lines[l1-1]), "", 100)
309309+ f.AddLineInfo(int(f.lines[l2-1]), "bar", 3)
310310+311311+ // unadjusted position info must remain unchanged
312312+ for i, offs := range f.lines {
313313+ got1 := f.PositionFor(f.Pos(int(offs), 0), false)
314314+ want := Position{filename, int(offs), i + 1, 1}
315315+ checkPos(t, "2. PositionFor unadjusted", got1, want)
316316+ }
317317+318318+ // adjusted position info should have changed
319319+ for i, offs := range f.lines {
320320+ got2 := f.PositionFor(f.Pos(int(offs), 0), true)
321321+ got3 := f.Position(f.Pos(int(offs), 0))
322322+ want := Position{filename, int(offs), i + 1, 1}
323323+ // manually compute wanted filename and line
324324+ line := want.Line
325325+ if i+1 >= l1 {
326326+ want.Filename = ""
327327+ want.Line = line - l1 + 100
328328+ }
329329+ if i+1 >= l2 {
330330+ want.Filename = "bar"
331331+ want.Line = line - l2 + 3
332332+ }
333333+ checkPos(t, "3. PositionFor adjusted", got2, want)
334334+ checkPos(t, "3. Position", got3, want)
335335+ }
336336+}
+263
cue/token/token.go
···11+// Copyright 2018 The CUE Authors
22+//
33+// Licensed under the Apache License, Version 2.0 (the "License");
44+// you may not use this file except in compliance with the License.
55+// You may obtain a copy of the License at
66+//
77+// http://www.apache.org/licenses/LICENSE-2.0
88+//
99+// Unless required by applicable law or agreed to in writing, software
1010+// distributed under the License is distributed on an "AS IS" BASIS,
1111+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212+// See the License for the specific language governing permissions and
1313+// limitations under the License.
1414+1515+// Package token defines constants representing the lexical tokens of the Go
1616+// programming language and basic operations on tokens (printing, predicates).
1717+package token // import "cuelang.org/go/cue/token"
1818+1919+import "strconv"
2020+2121+// Token is the set of lexical tokens of the CUE configuration language.
2222+type Token int
2323+2424+// The list of tokens.
2525+const (
2626+ // Special tokens
2727+ ILLEGAL Token = iota
2828+ EOF
2929+ COMMENT
3030+3131+ literalBeg
3232+ // Identifiers and basic type literals
3333+ // (these tokens stand for classes of literals)
3434+ IDENT // main, _tmp
3535+ INT // 12_345Mi, 0700, 0xdeadbeef, 1.2M
3636+ FLOAT // 123.45,
3737+ // DURATION // 3m4s TODO
3838+ STRING // "abc"
3939+ INTERPOLATION // a part of a template string, e.g. `"age: \(`
4040+ BOTTOM // _|_
4141+4242+ literalEnd
4343+4444+ operatorBeg
4545+ // Operators and delimiters
4646+ ADD // +
4747+ SUB // -
4848+ MUL // *
4949+ POW // ^
5050+ QUO // /
5151+ REM // %
5252+5353+ IQUO // quo
5454+ IREM // rem
5555+ IDIV // div
5656+ IMOD // mod
5757+5858+ UNIFY // &
5959+ DISJUNCTION // |
6060+6161+ LAND // &&
6262+ LOR // ||
6363+6464+ BIND // =
6565+ EQL // ==
6666+ LSS // <
6767+ GTR // >
6868+ NOT // !
6969+ ARROW // <-
7070+ LAMBDA // ->
7171+7272+ NEQ // !=
7373+ LEQ // <=
7474+ GEQ // >=
7575+7676+ LPAREN // (
7777+ LBRACK // [
7878+ LBRACE // {
7979+ COMMA // ,
8080+ PERIOD // .
8181+ RANGE // ..
8282+ ELLIPSIS // ...
8383+8484+ RPAREN // )
8585+ RBRACK // ]
8686+ RBRACE // }
8787+ SEMICOLON // :
8888+ COLON // :
8989+ operatorEnd
9090+9191+ keywordBeg
9292+9393+ IF
9494+ FOR
9595+ IN
9696+ LET
9797+9898+ TRUE
9999+ FALSE
100100+ NULL
101101+102102+ keywordEnd
103103+)
104104+105105+var tokens = [...]string{
106106+ ILLEGAL: "ILLEGAL",
107107+108108+ EOF: "EOF",
109109+ COMMENT: "COMMENT",
110110+111111+ IDENT: "IDENT",
112112+ INT: "INT",
113113+ FLOAT: "FLOAT",
114114+ // DURATION: "DURATION", // TODO
115115+ STRING: "STRING",
116116+ INTERPOLATION: "INTERPOLATION",
117117+118118+ ADD: "+",
119119+ SUB: "-",
120120+ MUL: "*",
121121+ POW: "^",
122122+ QUO: "/",
123123+ REM: "%",
124124+125125+ IQUO: "quo",
126126+ IREM: "rem",
127127+ IDIV: "div",
128128+ IMOD: "mod",
129129+130130+ UNIFY: "&",
131131+ DISJUNCTION: "|",
132132+133133+ LAND: "&&",
134134+ LOR: "||",
135135+136136+ BIND: "=",
137137+ EQL: "==",
138138+ LSS: "<",
139139+ GTR: ">",
140140+ NOT: "!",
141141+ ARROW: "<-",
142142+ LAMBDA: "->",
143143+144144+ NEQ: "!=",
145145+ LEQ: "<=",
146146+ GEQ: ">=",
147147+148148+ LPAREN: "(",
149149+ LBRACK: "[",
150150+ LBRACE: "{",
151151+ COMMA: ",",
152152+ PERIOD: ".",
153153+ RANGE: "..",
154154+ ELLIPSIS: "...",
155155+156156+ RPAREN: ")",
157157+ RBRACK: "]",
158158+ RBRACE: "}",
159159+ SEMICOLON: ";",
160160+ COLON: ":",
161161+162162+ BOTTOM: "_|_",
163163+164164+ FALSE: "false",
165165+ TRUE: "true",
166166+ NULL: "null",
167167+168168+ FOR: "for",
169169+ IF: "if",
170170+ IN: "in",
171171+ LET: "let",
172172+}
173173+174174+// String returns the string corresponding to the token tok.
175175+// For operators, delimiters, and keywords the string is the actual
176176+// token character sequence (e.g., for the token ADD, the string is
177177+// "+"). For all other tokens the string corresponds to the token
178178+// constant name (e.g. for the token IDENT, the string is "IDENT").
179179+func (tok Token) String() string {
180180+ s := ""
181181+ if 0 <= tok && tok < Token(len(tokens)) {
182182+ s = tokens[tok]
183183+ }
184184+ if s == "" {
185185+ s = "token(" + strconv.Itoa(int(tok)) + ")"
186186+ }
187187+ return s
188188+}
189189+190190+// A set of constants for precedence-based expression parsing.
191191+// Non-operators have lowest precedence, followed by operators
192192+// starting with precedence 1 up to unary operators. The highest
193193+// precedence serves as "catch-all" precedence for selector,
194194+// indexing, and other operator and delimiter tokens.
195195+const (
196196+ LowestPrec = lowestPrec
197197+ UnaryPrec = unaryPrec
198198+ HighestPrec = highestPrec
199199+)
200200+201201+const (
202202+ lowestPrec = 0 // non-operators
203203+ unaryPrec = 8
204204+ highestPrec = 9
205205+)
206206+207207+// Precedence returns the operator precedence of the binary
208208+// operator op. If op is not a binary operator, the result
209209+// is LowestPrecedence.
210210+//
211211+func (tok Token) Precedence() int {
212212+ switch tok {
213213+ case DISJUNCTION:
214214+ return 1
215215+ case UNIFY:
216216+ return 2
217217+ case LOR:
218218+ return 3
219219+ case LAND:
220220+ return 4
221221+ case EQL, NEQ, LSS, LEQ, GTR, GEQ:
222222+ return 5
223223+ case ADD, SUB:
224224+ return 6
225225+ case MUL, QUO, REM, IDIV, IMOD, IQUO, IREM:
226226+ return 7
227227+ case RANGE:
228228+ return 8
229229+ }
230230+ return lowestPrec
231231+}
232232+233233+var keywords map[string]Token
234234+235235+func init() {
236236+ keywords = make(map[string]Token)
237237+ for i := keywordBeg + 1; i < keywordEnd; i++ {
238238+ keywords[tokens[i]] = i
239239+ }
240240+}
241241+242242+// Lookup maps an identifier to its keyword token or IDENT (if not a keyword).
243243+//
244244+func Lookup(ident string) Token {
245245+ if tok, isKeyword := keywords[ident]; isKeyword {
246246+ return tok
247247+ }
248248+ return IDENT
249249+}
250250+251251+// Predicates
252252+253253+// IsLiteral returns true for tokens corresponding to identifiers
254254+// and basic type literals; it returns false otherwise.
255255+func (tok Token) IsLiteral() bool { return literalBeg < tok && tok < literalEnd }
256256+257257+// IsOperator returns true for tokens corresponding to operators and
258258+// delimiters; it returns false otherwise.
259259+func (tok Token) IsOperator() bool { return operatorBeg < tok && tok < operatorEnd }
260260+261261+// IsKeyword returns true for tokens corresponding to keywords;
262262+// it returns false otherwise.
263263+func (tok Token) IsKeyword() bool { return keywordBeg < tok && tok < keywordEnd }