all repos

clerk @ e12aa47

missing tooling for ledger/hledger

clerk/journal/lexer/lexer.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
parser,lexxer: add fast paths, 1 month ago
1
package lexer
2
3
import (
4
	"unicode"
5
	"unicode/utf8"
6
	"unsafe"
7
8
	"olexsmir.xyz/clerk/journal/token"
9
)
10
11
type mode uint
12
13
const (
14
	// start of a line, nothing consumed
15
	modeDefault mode = iota
16
17
	// after ; # * % ; at start of line, or anywhere inline
18
	// everything until \n is comment text
19
	modeComment
20
21
	// after lexing a date at column 0
22
	// expects: optional status, optional code, description, comment
23
	modeTransaction
24
25
	// after lexing an indent at start of line
26
	// expects: account name, then two spaces, then amount
27
	modePosting
28
29
	// after ~, period expression
30
	// expects: period, optional description (after 2+ spaces), optional comment
31
	modePeriodic
32
33
	// after =, automates transaction
34
	// expects: expression
35
	modeAutomated
36
37
	// after a directive keyword like account, commodity, include
38
	// expects: rest of directive content
39
	modeDirective
40
)
41
42
type Lexer struct {
43
	file  string
44
	input []byte
45
	mode  mode
46
47
	ch     rune // current rune (0 = EOF/sentinel)
48
	chSize int  // byte size of current rune
49
	pos    int  // current byte offset (points at ch)
50
	rpos   int  // next byte offset to read (one ahead of pos)
51
	col    int  // current column (1-based)
52
	line   int  // current line (1-based)
53
54
	transactionPastStatus bool
55
	postingExpectAccount  bool
56
	readingNoteAfterPipe  bool
57
58
	includePath bool // true after [token.INCLUDE] is omitted
59
60
	// subdirective is set while inside an account/commodity directive block;
61
	// every indented line then lexes as directive content until a
62
	// non-indented line start clears it in lexDefault
63
	subdirective bool
64
}
65
66
func New(file string, input []byte) *Lexer {
67
	l := &Lexer{
68
		file:  file,
69
		input: input,
70
		line:  1,
71
	}
72
	l.advance()
73
	if l.ch == '\uFEFF' { // start of the input
74
		l.advance()
75
	}
76
	return l
77
}
78
79
// Next returns next token in the input
80
func (l *Lexer) Next() token.Token {
81
	switch l.mode {
82
	case modeDefault:
83
		return l.lexDefault()
84
	case modeComment:
85
		return l.lexComment()
86
	case modeTransaction:
87
		return l.lexTransaction()
88
	case modePosting:
89
		return l.lexPosting()
90
	case modePeriodic:
91
		return l.lexPeriodic()
92
	case modeAutomated:
93
		return l.lexAutomated()
94
	case modeDirective:
95
		return l.lexDirective()
96
	}
97
	panic("unreachable")
98
}
99
100
func (l *Lexer) lexDefault() token.Token {
101
	if l.includePath {
102
		l.includePath = false
103
	}
104
	if l.subdirective && l.ch != ' ' && l.ch != '\t' {
105
		l.subdirective = false
106
	}
107
	switch {
108
	case l.ch == 0:
109
		return l.token(token.EOF, "")
110
	case l.ch == '\n':
111
		return l.lexNewline()
112
	case l.ch == '\r':
113
		l.col = 0
114
		l.advance()
115
		return l.lexNewline()
116
	case l.ch == ' ' || l.ch == '\t':
117
		tok := l.lexIndent()
118
		if l.subdirective {
119
			// keep directive mode for every line of a directive block; the
120
			// flag is cleared at the next non-indented line start in lexDefault
121
			l.mode = modeDirective
122
		} else {
123
			l.mode = modePosting
124
		}
125
		l.postingExpectAccount = true
126
		return tok
127
	case l.ch == ';' || l.ch == '#' || l.ch == '%':
128
		l.mode = modeComment
129
		return l.lexSingle(token.SEMICOLON) // todo: ??
130
	case l.ch == '*': // * at col 0 == comment
131
		l.mode = modeComment
132
		return l.lexSingle(token.STAR)
133
	case l.ch == '~':
134
		l.mode = modePeriodic
135
		return l.lexSingle(token.TILDE)
136
	case l.ch == '=':
137
		l.mode = modeAutomated
138
		return l.lexSingle(token.EQ)
139
	case l.ch == '+':
140
		return l.lexSingle(token.PLUS)
141
	case l.ch == '-':
142
		return l.lexSingle(token.MINUS)
143
	case l.ch == '.':
144
		return l.lexSingle(token.TEXT)
145
	case l.ch == '!':
146
		return l.lexSingle(token.BANG)
147
	case l.ch == '@':
148
		return l.lexSingle(token.AT)
149
	case l.isAlpha():
150
		return l.lexKeyword()
151
	case l.isDigit():
152
		if !l.isDate() {
153
			s := l.save()
154
			for l.isDigit() || l.ch == '-' || l.ch == '/' || l.ch == '.' {
155
				l.advance()
156
			}
157
			return token.Token{Type: token.ILLEGAL, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
158
		}
159
		tok := l.lexDate()
160
		l.mode = modeTransaction
161
		l.transactionPastStatus = false
162
		return tok
163
	default:
164
		s := l.save()
165
		l.advance()
166
		return token.Token{Type: token.ILLEGAL, Literal: l.lit(s), Span: l.span(s)}
167
	}
168
}
169
170
func (l *Lexer) lexComment() token.Token {
171
	if l.ch == '\n' || l.ch == '\r' || l.ch == 0 {
172
		l.mode = modeDefault
173
		if l.ch == '\r' {
174
			l.col = 0
175
			l.advance()
176
		}
177
		return l.lexNewline()
178
	}
179
180
	for l.ch == ' ' || l.ch == '\t' {
181
		l.lexWhitespace()
182
	}
183
184
	if l.ch == '\n' || l.ch == '\r' || l.ch == 0 {
185
		l.mode = modeDefault
186
		if l.ch == '\r' {
187
			l.col = 0
188
			l.advance()
189
		}
190
		return l.lexNewline()
191
	}
192
193
	s := l.save()
194
	for l.ch != '\n' && l.ch != '\r' && l.ch != 0 {
195
		l.advance()
196
	}
197
	return token.Token{Type: token.TEXT, Literal: l.lit(s), Span: l.span(s)}
198
}
199
200
func (l *Lexer) lexTransaction() token.Token {
201
	if l.readingNoteAfterPipe {
202
		l.readingNoteAfterPipe = false
203
		return l.lexNote()
204
	}
205
206
	switch l.ch {
207
	case 0:
208
		return l.token(token.EOF, "")
209
	case '\n':
210
		l.mode = modeDefault
211
		return l.lexNewline()
212
	case '\r':
213
		l.col = 0
214
		l.advance()
215
		return l.lexNewline()
216
	case ' ', '\t':
217
		return l.lexWhitespace()
218
	case ';':
219
		l.mode = modeComment
220
		return l.lexSingle(token.SEMICOLON)
221
	case '*':
222
		if !l.transactionPastStatus {
223
			l.transactionPastStatus = true
224
			return l.lexSingle(token.STAR)
225
		}
226
		return l.lexText()
227
	case '!':
228
		if !l.transactionPastStatus {
229
			l.transactionPastStatus = true
230
			return l.lexSingle(token.BANG)
231
		}
232
		return l.lexText()
233
	case '|':
234
		l.transactionPastStatus = true
235
		l.readingNoteAfterPipe = true
236
		return l.lexSingle(token.PIPE)
237
	case '+':
238
		return l.lexSingle(token.PLUS)
239
	case '-':
240
		return l.lexSingle(token.MINUS)
241
	case '=':
242
		return l.lexEquals()
243
	case '"', '\'':
244
		return l.lexString()
245
	default: // description / payee
246
		if l.isDate() { // secondsry date after =
247
			return l.lexDate()
248
		}
249
		return l.lexText()
250
	}
251
}
252
253
func (l *Lexer) lexNote() token.Token {
254
	for l.ch == ' ' || l.ch == '\t' {
255
		l.advance()
256
	}
257
	if l.ch == 0 || l.ch == '\n' || l.ch == '\r' || l.ch == ';' {
258
		return l.Next()
259
	}
260
	s := l.save()
261
	for l.ch != 0 && l.ch != '\n' && l.ch != '\r' && l.ch != ';' {
262
		l.advance()
263
	}
264
	lit := l.lit(s)
265
	for len(lit) > 0 && (lit[len(lit)-1] == ' ' || lit[len(lit)-1] == '\t') {
266
		lit = lit[:len(lit)-1]
267
	}
268
	return token.Token{Type: token.TEXT, Literal: lit, Span: l.span(s)}
269
}
270
271
func (l *Lexer) lexPeriodic() token.Token {
272
	switch l.ch {
273
	case 0:
274
		return l.token(token.EOF, "")
275
	case '\n':
276
		l.mode = modeDefault
277
		return l.lexNewline()
278
	case '\r':
279
		l.col = 0
280
		l.advance()
281
		return l.lexNewline()
282
	case ';':
283
		l.mode = modeComment
284
		return l.lexSingle(token.SEMICOLON)
285
	case ' ', '\t':
286
		return l.lexWhitespace()
287
	default:
288
		return l.lexText()
289
	}
290
}
291
292
func (l *Lexer) lexAutomated() token.Token {
293
	switch l.ch {
294
	case 0:
295
		return l.token(token.EOF, "")
296
	case '\n':
297
		l.mode = modeDefault
298
		return l.lexNewline()
299
	case '\r':
300
		l.col = 0
301
		l.advance()
302
		return l.lexNewline()
303
	case ' ', '\t':
304
		return l.lexWhitespace()
305
	case ';':
306
		l.mode = modeComment
307
		return l.lexSingle(token.SEMICOLON)
308
	default:
309
		return l.lexText()
310
	}
311
}
312
313
func (l *Lexer) lexPosting() token.Token {
314
	switch {
315
	case l.ch == 0:
316
		l.postingExpectAccount = false
317
		return l.token(token.EOF, "")
318
	case l.ch == '\n':
319
		l.postingExpectAccount = false
320
		l.mode = modeDefault
321
		return l.lexNewline()
322
	case l.ch == '\r':
323
		l.postingExpectAccount = false
324
		l.col = 0
325
		l.advance()
326
		l.mode = modeDefault
327
		return l.lexNewline()
328
	case l.ch == ';':
329
		l.postingExpectAccount = false
330
		l.mode = modeComment
331
		return l.lexSingle(token.SEMICOLON)
332
	case l.ch == ' ' || l.ch == '\t':
333
		return l.lexWhitespace()
334
	case l.postingExpectAccount && l.ch == '*':
335
		return l.lexSingle(token.STAR)
336
	case l.postingExpectAccount && l.ch == '!':
337
		return l.lexSingle(token.BANG)
338
	case l.ch == '=':
339
		return l.lexEquals()
340
	case l.ch == '@':
341
		return l.lexAt()
342
	case l.ch == '{':
343
		return l.lexLBrace()
344
	case l.ch == '}':
345
		return l.lexRBrace()
346
	case l.ch == '(':
347
		if !l.postingExpectAccount {
348
			return l.lexParenExpr()
349
		}
350
		return l.lexSingle(token.LPAREN)
351
	case l.ch == ')':
352
		return l.lexSingle(token.RPAREN)
353
	case l.ch == '[':
354
		return l.lexSingle(token.LBRACKET)
355
	case l.ch == ']':
356
		return l.lexSingle(token.RBRACKET)
357
	case l.ch == ':':
358
		return l.lexSingle(token.COLON)
359
	case l.postingExpectAccount && l.ch != '*' && l.ch != '!' && l.ch != '(' && l.ch != '[':
360
		return l.lexAccountNamePosting()
361
	case l.ch == '*': // after account name
362
		return l.lexSingle(token.STAR)
363
	case l.isDigit(), l.ch == '.':
364
		return l.lexNumber()
365
	case l.ch == '-':
366
		return l.lexSingle(token.MINUS)
367
	case l.ch == '+':
368
		return l.lexSingle(token.PLUS)
369
	case l.isCommodityStart():
370
		return l.lexCommodityMark()
371
	case l.ch == '"' || l.ch == '\'':
372
		return l.lexString()
373
	case l.ch >= 'a' && l.ch <= 'z':
374
		return l.lexCommodityMark()
375
	default:
376
		return l.lexAccountNamePosting()
377
	}
378
}
379
380
func (l *Lexer) lexDirective() token.Token {
381
	switch {
382
	case l.ch == '\n', l.ch == 0:
383
		l.mode = modeDefault
384
		return l.lexNewline()
385
	case l.ch == '\r':
386
		l.mode = modeDefault
387
		l.col = 0
388
		l.advance()
389
		return l.lexNewline()
390
	case l.ch == ';':
391
		l.mode = modeComment
392
		return l.lexSingle(token.SEMICOLON)
393
	case l.ch == ' ', l.ch == '\t':
394
		return l.lexWhitespace()
395
	case l.ch == '=':
396
		return l.lexSingle(token.EQ)
397
	case l.ch == '+':
398
		return l.lexSingle(token.PLUS)
399
	case l.ch == '-':
400
		return l.lexSingle(token.MINUS)
401
	case l.ch == '"', l.ch == '\'':
402
		return l.lexString()
403
	case l.ch == ':':
404
		return l.lexSingle(token.COLON)
405
	case l.ch == ')':
406
		return l.lexSingle(token.RPAREN)
407
	case l.ch == ']':
408
		return l.lexSingle(token.RBRACKET)
409
	case l.includePath:
410
		return l.lexPath()
411
	case l.isCommodityStart():
412
		return l.lexCommodityMark()
413
	case l.isTime():
414
		return l.lexTime()
415
	case l.isDate():
416
		return l.lexDate()
417
	case l.isDigit():
418
		return l.lexNumber()
419
	default:
420
		return l.lexAccountNameDirective()
421
	}
422
}
423
424
func (l *Lexer) lexPath() token.Token {
425
	s := l.save()
426
	for l.ch != '\n' && l.ch != '\r' && l.ch != ';' && l.ch != 0 && l.ch != ' ' && l.ch != '\t' {
427
		l.advance()
428
	}
429
	return token.Token{
430
		Type:    token.TEXT,
431
		Literal: l.lit(s),
432
		Span:    l.span(s),
433
	}
434
}
435
436
func (l *Lexer) lexSingle(kind token.Type) token.Token {
437
	s := l.save()
438
	l.advance()
439
	return token.Token{
440
		Type:    kind,
441
		Literal: l.lit(s),
442
		Span:    l.span(s),
443
	}
444
}
445
446
func (l *Lexer) lexNewline() token.Token {
447
	s := l.save()
448
	l.advance()
449
	l.mode = modeDefault
450
	return token.Token{Type: token.NEWLINE, Literal: "\n", Span: l.span(s)}
451
}
452
453
func (l *Lexer) lexWhitespace() token.Token {
454
	s := l.save()
455
	for l.ch == ' ' || l.ch == '\t' {
456
		l.advance()
457
	}
458
	return token.Token{Type: token.WHITESPACE, Literal: l.lit(s), Span: l.span(s)}
459
}
460
461
func (l *Lexer) lexIndent() token.Token {
462
	s := l.save()
463
	for l.ch == ' ' || l.ch == '\t' {
464
		l.advance()
465
	}
466
	return token.Token{Type: token.INDENT, Literal: l.lit(s), Span: l.span(s)}
467
}
468
469
func (l *Lexer) lexEquals() token.Token {
470
	s := l.save()
471
	l.advance()
472
	if l.ch == '=' {
473
		l.advance()
474
		switch l.ch {
475
		case '=':
476
			l.advance()
477
			return token.Token{Type: token.EQEQEQ, Literal: "===", Span: l.span(s)}
478
		case '*':
479
			l.advance()
480
			return token.Token{Type: token.EQEQEQ, Literal: "==*", Span: l.span(s)}
481
		default:
482
			return token.Token{Type: token.EQEQ, Literal: "==", Span: l.span(s)}
483
		}
484
	}
485
	if l.ch == '*' {
486
		l.advance()
487
		return token.Token{Type: token.EQSTAR, Literal: "=*", Span: l.span(s)}
488
	}
489
	return token.Token{Type: token.EQ, Literal: "=", Span: l.span(s)}
490
}
491
492
func (l *Lexer) lexAt() token.Token {
493
	s := l.save()
494
	l.advance()
495
	if l.ch == '@' {
496
		l.advance()
497
		return token.Token{Type: token.ATAT, Literal: "@@", Span: l.span(s)}
498
	}
499
	return token.Token{Type: token.AT, Literal: "@", Span: l.span(s)}
500
}
501
502
func (l *Lexer) lexText() token.Token {
503
	s := l.save()
504
	l.advance()
505
	for l.ch != '\n' && l.ch != '\r' && l.ch != ';' && l.ch != 0 && l.ch != ' ' && l.ch != '\t' {
506
		l.advance()
507
	}
508
	lit := string(l.input[s.offset:l.pos])
509
	return token.Token{Type: token.TEXT, Literal: lit, Span: l.span(s)}
510
}
511
512
// lexAccountNameDirective reads accout name in directive context.
513
// stops at any whitespace, supports multi-word names("Taxi Fare").
514
func (l *Lexer) lexAccountNameDirective() token.Token {
515
	s := l.save()
516
	for l.ch != '\n' && l.ch != '\r' && l.ch != ';' && l.ch != 0 && l.ch != ')' && l.ch != ']' && l.ch != ':' && l.ch != ' ' && l.ch != '\t' {
517
		l.advance()
518
	}
519
	return token.Token{Type: token.TEXT, Literal: l.lit(s), Span: l.span(s)}
520
}
521
522
// lexAccountNamePosting reads an account name in posting context.
523
// stops at two consecutive spaces.
524
func (l *Lexer) lexAccountNamePosting() token.Token {
525
	s := l.save()
526
	for l.ch != '\n' && l.ch != '\r' && l.ch != ';' && l.ch != 0 && l.ch != ')' && l.ch != ']' && l.ch != ':' {
527
		if l.isTwoSpaces() {
528
			break
529
		}
530
		l.advance()
531
	}
532
	if l.ch != ':' {
533
		l.postingExpectAccount = false
534
	}
535
	return token.Token{Type: token.TEXT, Literal: l.lit(s), Span: l.span(s)}
536
}
537
538
func (l *Lexer) lexParenExpr() token.Token {
539
	s := l.save()
540
	depth := 0
541
	for l.ch != '\n' && l.ch != '\r' && l.ch != 0 {
542
		if l.ch == '(' {
543
			depth++
544
		} else if l.ch == ')' {
545
			depth--
546
			if depth == 0 {
547
				l.advance()
548
				break
549
			}
550
		}
551
		l.advance()
552
	}
553
	return token.Token{Type: token.PARENEXPR, Literal: l.lit(s), Span: l.span(s)}
554
}
555
556
func (l *Lexer) lexNumber() token.Token {
557
	s := l.save()
558
	isDecimal := false
559
	for {
560
		if l.isDigit() || l.ch == '.' || l.ch == ',' || l.ch == '_' || l.ch == '\'' {
561
			if l.ch == '.' || l.ch == ',' {
562
				isDecimal = true
563
			}
564
			l.advance()
565
		} else if l.ch == ' ' && (l.peek() >= '0' && l.peek() <= '9') {
566
			isDecimal = true
567
			l.advance()
568
		} else if l.ch == 'e' || l.ch == 'E' {
569
			// exponent: consume only when E[sign]digit, so `10E ` stays an integer
570
			p := l.pos + 1
571
			if p < len(l.input) && (l.input[p] == '+' || l.input[p] == '-') {
572
				p++
573
			}
574
			if p >= len(l.input) || l.input[p] < '0' || l.input[p] > '9' {
575
				break
576
			}
577
			isDecimal = true
578
			l.advance() // e/E
579
			if l.ch == '+' || l.ch == '-' {
580
				l.advance()
581
			}
582
			for l.isDigit() {
583
				l.advance()
584
			}
585
		} else {
586
			break
587
		}
588
	}
589
	lit := l.lit(s)
590
	kind := token.INT
591
	if isDecimal {
592
		kind = token.DECIMAL
593
	}
594
	return token.Token{Type: kind, Literal: lit, Span: l.span(s)}
595
}
596
597
func (l *Lexer) lexKeyword() token.Token {
598
	s := l.save()
599
	for l.ch != 0 && l.ch != '\n' && l.ch != '\r' && l.ch != ' ' && l.ch != '\t' && l.ch != ';' {
600
		l.advance()
601
	}
602
	lit := l.lit(s)
603
	kind := l.keyword(lit)
604
	if kind == token.ILLEGAL { // todo: report an error ??
605
		kind = token.TEXT
606
	} else {
607
		l.mode = modeDirective
608
		l.subdirective = kind == token.ACCOUNT || kind == token.COMMODITY
609
		l.includePath = kind == token.INCLUDE
610
	}
611
	return token.Token{Type: kind, Literal: lit, Span: l.span(s)}
612
}
613
614
func (l *Lexer) lexDate() token.Token {
615
	s := l.save()
616
	for l.isDigit() || (l.isDateSep() && l.peekIsDigit()) {
617
		l.advance()
618
	}
619
	return token.Token{Type: token.DATE, Literal: l.lit(s), Span: l.span(s)}
620
}
621
622
func isSymbolChar(r rune) bool {
623
	return r == '$' || unicode.In(r, unicode.Sc)
624
}
625
626
func (l *Lexer) lexString() token.Token {
627
	s := l.save()
628
	quote := l.ch
629
	l.advance() // consume the quote character
630
	for l.ch != quote && l.ch != '\n' && l.ch != 0 {
631
		l.advance()
632
	}
633
	if l.ch == quote {
634
		l.advance()
635
	}
636
	return token.Token{Type: token.STRING, Literal: l.lit(s), Span: l.span(s)}
637
}
638
639
func (l *Lexer) lexCommodityMark() token.Token {
640
	s := l.save()
641
642
	if l.ch == '"' {
643
		l.advance()
644
		for l.ch != '"' && l.ch != '\n' && l.ch != 0 {
645
			l.advance()
646
		}
647
		if l.ch == '"' {
648
			l.advance()
649
		}
650
		return token.Token{Type: token.COMMODITYMARK, Literal: l.lit(s), Span: l.span(s)}
651
	}
652
653
	if unicode.IsLetter(l.ch) {
654
		for unicode.IsLetter(l.ch) {
655
			l.advance()
656
		}
657
		return token.Token{Type: token.COMMODITYMARK, Literal: l.lit(s), Span: l.span(s)}
658
	}
659
660
	if isSymbolChar(l.ch) {
661
		for isSymbolChar(l.ch) {
662
			l.advance()
663
		}
664
		return token.Token{Type: token.COMMODITYMARK, Literal: l.lit(s), Span: l.span(s)}
665
	}
666
667
	l.advance()
668
	return token.Token{Type: token.COMMODITYMARK, Literal: l.lit(s), Span: l.span(s)}
669
}
670
671
func (l *Lexer) lexLBrace() token.Token {
672
	s := l.save()
673
	l.advance()
674
	if l.ch == '{' {
675
		l.advance()
676
		return token.Token{Type: token.LBRACELBRACE, Literal: "{{", Span: l.span(s)}
677
	}
678
	return token.Token{Type: token.LBRACE, Literal: "{", Span: l.span(s)}
679
}
680
681
func (l *Lexer) lexRBrace() token.Token {
682
	s := l.save()
683
	l.advance()
684
	if l.ch == '}' {
685
		l.advance()
686
		return token.Token{Type: token.RBRACERBRACE, Literal: "}}", Span: l.span(s)}
687
	}
688
	return token.Token{Type: token.RBRACE, Literal: "}", Span: l.span(s)}
689
}
690
691
func (l *Lexer) advance() {
692
	if l.rpos >= len(l.input) {
693
		l.ch = 0
694
		l.chSize = 0
695
	} else if b := l.input[l.rpos]; b < utf8.RuneSelf { // ASCII fast path
696
		l.ch = rune(b)
697
		l.chSize = 1
698
	} else {
699
		l.ch, l.chSize = utf8.DecodeRune(l.input[l.rpos:])
700
	}
701
	l.pos = l.rpos
702
	l.rpos += l.chSize
703
	if l.ch == '\n' || l.ch == '\r' {
704
		l.line++
705
		l.col = 0
706
	} else {
707
		l.col++
708
	}
709
}
710
711
func (l *Lexer) peek() rune {
712
	if l.rpos < len(l.input) && l.input[l.rpos] < utf8.RuneSelf {
713
		return rune(l.input[l.rpos])
714
	}
715
	r, _ := utf8.DecodeRune(l.input[l.rpos:])
716
	return r
717
}
718
719
func (l *Lexer) peekN(n int) byte {
720
	if l.pos+n >= len(l.input) {
721
		return 0
722
	}
723
	return l.input[l.pos+n]
724
}
725
726
func (l *Lexer) isDigit() bool { return l.ch >= '0' && l.ch <= '9' }
727
func (l *Lexer) isAlpha() bool {
728
	return (l.ch >= 'a' && l.ch <= 'z') ||
729
		(l.ch >= 'A' && l.ch <= 'Z')
730
}
731
732
func (l *Lexer) isTwoSpaces() bool {
733
	return l.ch == ' ' && l.rpos < len(l.input) && l.input[l.rpos] == ' '
734
}
735
736
func (l *Lexer) isDateSep() bool { return l.ch == '-' || l.ch == '/' || l.ch == '.' }
737
738
func (l *Lexer) peekIsDigit() bool {
739
	r := l.peek()
740
	return r >= '0' && r <= '9'
741
}
742
743
func (l *Lexer) isCommodityStart() bool {
744
	if l.ch == '$' || (l.ch >= 'A' && l.ch <= 'Z') {
745
		return true
746
	}
747
	if l.ch < utf8.RuneSelf {
748
		return false
749
	}
750
	return unicode.In(l.ch, unicode.Sc) || unicode.IsLetter(l.ch)
751
}
752
753
func (l *Lexer) isDate() bool {
754
	if !l.isDigit() {
755
		return false
756
	}
757
	// YYYY/M/D or YYYY/MM/DD
758
	if l.peekN(1) >= '0' && l.peekN(1) <= '9' &&
759
		l.peekN(2) >= '0' && l.peekN(2) <= '9' &&
760
		l.peekN(3) >= '0' && l.peekN(3) <= '9' {
761
		sep := l.peekN(4)
762
		if sep == '/' || sep == '-' || sep == '.' {
763
			if l.peekN(5) >= '0' && l.peekN(5) <= '9' {
764
				if l.peekN(6) == sep {
765
					return l.peekN(7) >= '0' && l.peekN(7) <= '9'
766
				}
767
				if l.peekN(7) == sep {
768
					return l.peekN(8) >= '0' && l.peekN(8) <= '9'
769
				}
770
			}
771
		}
772
		return false
773
	}
774
	// M/D or MM/DD(year inferred, only / and - separators; . is ambiguous with decimal numbers like 1.01)
775
	if (l.peekN(1) == '/' || l.peekN(1) == '-') &&
776
		l.peekN(2) >= '0' && l.peekN(2) <= '9' &&
777
		l.ch >= '1' && l.ch <= '9' {
778
		return validDay(l.peekN(2), l.peekN(3))
779
	}
780
	if (l.peekN(2) == '/' || l.peekN(2) == '-') &&
781
		l.peekN(3) >= '0' && l.peekN(3) <= '9' {
782
		m := int(l.ch-'0')*10 + int(l.peekN(1)-'0')
783
		return m >= 1 && m <= 12 && validDay(l.peekN(3), l.peekN(4))
784
	}
785
	return false
786
}
787
788
func validDay(first, second byte) bool {
789
	d := int(first - '0')
790
	if second >= '0' && second <= '9' {
791
		d = d*10 + int(second-'0')
792
	}
793
	return d >= 1 && d <= 31
794
}
795
796
func (l *Lexer) isTime() bool {
797
	if !l.isDigit() {
798
		return false
799
	}
800
	return l.peekN(2) == ':'
801
}
802
803
func (l *Lexer) lexTime() token.Token {
804
	s := l.save()
805
	for l.isDigit() || l.ch == ':' {
806
		l.advance()
807
	}
808
	return token.Token{Type: token.TIME, Literal: l.lit(s), Span: l.span(s)}
809
}
810
811
type savedPos struct{ offset, line, col int }
812
813
func (l *Lexer) save() savedPos {
814
	return savedPos{l.pos, l.line, l.col}
815
}
816
817
func (l *Lexer) span(s savedPos) token.Span {
818
	return token.Span{
819
		File:  l.file,
820
		Start: token.Pos{Offset: s.offset, Line: s.line, Col: s.col},
821
		End:   token.Pos{Offset: l.pos, Line: l.line, Col: l.col},
822
	}
823
}
824
825
func (l *Lexer) token(kind token.Type, literal string) token.Token {
826
	s := savedPos{l.pos, l.line, l.col}
827
	return token.Token{Type: kind, Literal: literal, Span: l.span(s)}
828
}
829
830
func (l *Lexer) lit(s savedPos) string {
831
	if s.offset == l.pos {
832
		return ""
833
	}
834
	return unsafe.String(&l.input[s.offset], l.pos-s.offset)
835
}
836
837
func (l *Lexer) keyword(s string) token.Type {
838
	switch s {
839
	case "comment":
840
		return token.COMMENTKW
841
	case "account":
842
		return token.ACCOUNT
843
	case "commodity":
844
		return token.COMMODITY
845
	case "include":
846
		return token.INCLUDE
847
	case "alias":
848
		return token.ALIAS
849
	case "payee":
850
		return token.PAYEE
851
	case "tag":
852
		return token.TAG
853
	case "apply":
854
		return token.APPLY
855
	case "end":
856
		return token.END
857
	case "Y", "year":
858
		return token.YEAR
859
	case "decimal-mark":
860
		return token.DECIMALMARK
861
	case "D":
862
		return token.D
863
	case "P":
864
		return token.P
865
	case "N":
866
		return token.N
867
	case "C":
868
		return token.C
869
	default:
870
		return token.ILLEGAL
871
	}
872
}