Skip to content
← Back

src/lexical/tokenizer.ghul

1
namespace Lexical is
2
use System.Exception
3
4
use Logging
5
use Source
6
7
use System.Globalization.UnicodeCategory
8
9
use Ghul.Pipes
10
11
get_stack_trace() -> string is
12
try
13
return System.Diagnostics.StackTrace()
14
.to_string()
15
.split(['\n']) |>
16
skip(2) |>
17
take(5) |>
18
map(s => s.trim().replace("at ", "")) |> join(" ")
19
catch e: System.Exception
20
return "unknown"
21
yrt
22
si
23
24
class TOKENIZER_EXCEPTION(s: string): Exception is
25
super(s)
26
si
27
28
class TOKEN_MAP is
29
_map: Collections.MAP[string,TOKEN]
30
31
init() is
32
super.init()
33
_map = Collections.MAP[string,TOKEN](223)
34
si
35
36
[s: string]: TOKEN public =>
37
if _map.contains_key(s) then
38
_map[s]
39
else
40
TOKEN.IDENTIFIER
41
fi,
42
= t is
43
_map[s] = t
44
si
45
si
46
47
class TOKEN_PAIR(token: TOKEN, location: LOCATION, value: string init) is
48
value_string: string
49
50
// True when this token is the first on its source line. Stamped by
51
// TOKEN_LOOKAHEAD as tokens are first read, so it survives
52
// speculation replays.
53
first_on_line: bool public
54
55
// True when the token before this one closed a string literal.
56
// Stamped alongside first_on_line. A `;` carrying this, followed
57
// by another string, is separating fragments that would otherwise
58
// chain into one literal, so it is never redundant.
59
follows_string: bool public
60
61
name: string => TOKEN_NAMES[token]
62
63
to_string() -> string =>
64
"{location}: {TOKEN_NAMES[token]} {if value_string.length > 0 then "'{value_string}'" else "" fi}"
65
66
to_short_string() -> string =>
67
"{if value_string.length > 0 /\ value_string !~ TOKEN_NAMES[token] then "{TOKEN_NAMES[token]} \"{value_string}\"" else "'{TOKEN_NAMES[token]}'" fi}"
68
69
init(..) is
70
value_string = value
71
si
72
si
73
74
class TOKENIZER: TokenSource is
75
_prev_count: int
76
77
// Where a literal the input ended inside was reported: a string,
78
// a character literal or an interpolation format with no closing
79
// quote before end of input, which more input would close.
80
end_of_input_error_locations: Collections.LIST[LOCATION]
81
82
_logger: Logger
83
_token_pair: TOKEN_PAIR
84
_symbol_tokens: TOKEN_MAP static
85
_operator_chars: Collections.SET[char] static
86
_operator_tokens: Collections.MAP[string,TOKEN] static
87
_input: IO.TextReader
88
_end_of_file: bool
89
_prev_char: char
90
91
// Whether the last character read was the space that stands for the
92
// end of the input, which moved the cursor nowhere.
93
_read_end_of_file: bool
94
_cursor: LOCATION_CURSOR
95
96
_interpolation_depth: int
97
_expect_format_string: bool
98
99
_trivia: Collections.LIST[TRIVIA]
100
101
// Comments and blank-line markers, in source order. Discarded by the
102
// parser; consumed by the formatter to round-trip non-token content.
103
trivia: Collections.Iterable[TRIVIA] => _trivia
104
105
init() static is
106
_operator_chars = Collections.SET[char]([
107
'!', '$', '%', '^', '&', '*', '-', '+', '=', '|', ':', '@', '~', '#', '\\', '<', '>', '.', '?', '/'
108
]: char )
109
110
_operator_tokens = Collections.MAP[string,TOKEN]()
111
_operator_tokens["="] = TOKEN.ASSIGN
112
_operator_tokens[":"] = TOKEN.COLON
113
_operator_tokens["."] = TOKEN.DOT
114
_operator_tokens["->"] = TOKEN.ARROW_THIN
115
_operator_tokens["=>"] = TOKEN.ARROW_FAT
116
_operator_tokens["|>"] = TOKEN.BAR_ARROW
117
_operator_tokens["~>"] = TOKEN.TILDE_ARROW
118
_operator_tokens["?"] = TOKEN.QUESTION
119
_operator_tokens["@"] = TOKEN.AT
120
_operator_tokens["@@"] = TOKEN.AT_AT
121
_symbol_tokens = TOKEN_MAP()
122
_symbol_tokens["abstract"] = TOKEN.ABSTRACT
123
_symbol_tokens["namespace"] = TOKEN.NAMESPACE
124
_symbol_tokens["class"] = TOKEN.CLASS
125
_symbol_tokens["struct"] = TOKEN.STRUCT
126
_symbol_tokens["union"] = TOKEN.UNION
127
_symbol_tokens["partial"] = TOKEN.PARTIAL
128
_symbol_tokens["impl"] = TOKEN.IMPL
129
_symbol_tokens["enum"] = TOKEN.ENUM
130
_symbol_tokens["public"] = TOKEN.PUBLIC
131
_symbol_tokens["protected"] = TOKEN.PROTECTED
132
_symbol_tokens["private"] = TOKEN.PRIVATE
133
_symbol_tokens["field"] = TOKEN.FIELD
134
_symbol_tokens["static"] = TOKEN.STATIC
135
_symbol_tokens["innate"] = TOKEN.INNATE
136
_symbol_tokens["rec"] = TOKEN.REC
137
_symbol_tokens["if"] = TOKEN.IF
138
_symbol_tokens["else"] = TOKEN.ELSE
139
_symbol_tokens["while"] = TOKEN.WHILE
140
_symbol_tokens["do"] = TOKEN.DO
141
_symbol_tokens["for"] = TOKEN.FOR
142
_symbol_tokens["in"] = TOKEN.IN
143
_symbol_tokens["case"] = TOKEN.CASE
144
_symbol_tokens["when"] = TOKEN.WHEN
145
_symbol_tokens["default"] = TOKEN.DEFAULT
146
_symbol_tokens["break"] = TOKEN.BREAK
147
_symbol_tokens["continue"] = TOKEN.CONTINUE
148
_symbol_tokens["ref"] = TOKEN.REF
149
_symbol_tokens["ptr"] = TOKEN.PTR
150
_symbol_tokens["new"] = TOKEN.NEW
151
_symbol_tokens["throw"] = TOKEN.THROW
152
_symbol_tokens["return"] = TOKEN.RETURN
153
_symbol_tokens["cast"] = TOKEN.CAST
154
_symbol_tokens["try"] = TOKEN.TRY
155
_symbol_tokens["let"] = TOKEN.LET
156
_symbol_tokens["mut"] = TOKEN.MUT
157
_symbol_tokens["await"] = TOKEN.AWAIT
158
_symbol_tokens["catch"] = TOKEN.CATCH
159
_symbol_tokens["finally"] = TOKEN.FINALLY
160
_symbol_tokens["self"] = TOKEN.SELF
161
_symbol_tokens["super"] = TOKEN.SUPER
162
_symbol_tokens["null"] = TOKEN.NULL
163
_symbol_tokens["use"] = TOKEN.USE
164
_symbol_tokens["trait"] = TOKEN.TRAIT
165
_symbol_tokens["isa"] = TOKEN.ISA
166
_symbol_tokens["typeof"] = TOKEN.TYPEOF
167
// _symbol_tokens["operator"] = TOKEN.OPERATOR;
168
_symbol_tokens["is"] = TOKEN.IS
169
_symbol_tokens["si"] = TOKEN.SI
170
_symbol_tokens["then"] = TOKEN.THEN
171
_symbol_tokens["elif"] = TOKEN.ELIF
172
_symbol_tokens["fi"] = TOKEN.FI
173
_symbol_tokens["esac"] = TOKEN.ESAC
174
_symbol_tokens["lav"] = TOKEN.LAV
175
_symbol_tokens["val"] = TOKEN.VAL
176
_symbol_tokens["od"] = TOKEN.OD
177
_symbol_tokens["yield"] = TOKEN.YIELD
178
_symbol_tokens["yrt"] = TOKEN.YRT
179
_symbol_tokens["true"] = TOKEN.TRUE
180
_symbol_tokens["false"] = TOKEN.FALSE
181
_symbol_tokens["assert"] = TOKEN.ASSERT
182
si
183
184
// True when a bare occurrence of `name` would tokenise as a keyword
185
// rather than an identifier, so writing it back out unadorned would
186
// change its meaning. Such a name can only have reached the parser as
187
// an identifier via a backtick escape.
188
is_reserved_word(name: string?) -> bool static is
189
return name? /\ _symbol_tokens[name] != TOKEN.IDENTIFIER
190
si
191
192
// True when a bare occurrence of `name` would tokenise as a numeric
193
// literal rather than an identifier: it is composed only of identifier
194
// characters but begins with a digit. Such a name reached the parser
195
// as an identifier via a backtick escape.
196
is_numeric_identifier(name: string) -> bool static is
197
if name.length == 0 then
198
return false
199
fi
200
201
let first = name[0]
202
203
if !(first >= '0' /\ first <= '9') then
204
return false
205
fi
206
207
for c in name do
208
if !is_identifier_part(c) then
209
return false
210
fi
211
od
212
213
return true
214
si
215
216
// True when `name` is composed entirely of operator characters, so a
217
// bare occurrence would tokenise as an operator and a backtick escape
218
// is needed to refer to it as a plain identifier.
219
is_operator_name(name: string) -> bool static is
220
if name.length == 0 then
221
return false
222
fi
223
224
for c in name do
225
if !is_operator_char(c) then
226
return false
227
fi
228
od
229
230
return true
231
si
232
233
// An operator character is one of the ASCII set above, or any
234
// non-ASCII character Unicode classes as a symbol. A letter is
235
// never a symbol, so no character is both an operator character
236
// and an identifier character.
237
is_operator_char(c: char) -> bool static =>
238
_operator_chars.contains(c) \/
239
(cast int(c) > 0x7E /\ char.is_symbol(c))
240
241
// Identifier characters follow the categories C# draws on, less
242
// the format characters: those render as nothing, so admitting
243
// them would let two names that read alike be different names.
244
// Only the basic plane is covered - a character above it arrives
245
// as a surrogate pair, which this scanner reads as two characters.
246
is_identifier_start(c: char) -> bool static is
247
if (c >= 'a' /\ c <= 'z') \/ (c >= 'A' /\ c <= 'Z') \/ c == '_' then
248
return true
249
fi
250
251
if cast int(c) <= 0x7E then
252
return false
253
fi
254
255
return char.is_letter(c) \/
256
char.get_unicode_category(c) == UnicodeCategory.LETTER_NUMBER
257
si
258
259
is_identifier_part(c: char) -> bool static is
260
if (c >= '0' /\ c <= '9') \/ is_identifier_start(c) then
261
return true
262
fi
263
264
if cast int(c) <= 0x7E then
265
return false
266
fi
267
268
let category = char.get_unicode_category(c)
269
270
return category == UnicodeCategory.DECIMAL_DIGIT_NUMBER \/
271
category == UnicodeCategory.NON_SPACING_MARK \/
272
category == UnicodeCategory.SPACING_COMBINING_MARK \/
273
category == UnicodeCategory.CONNECTOR_PUNCTUATION
274
si
275
276
is_format_char(c: char) -> bool static =>
277
cast int(c) > 0x7E /\
278
char.get_unicode_category(c) == UnicodeCategory.FORMAT
279
280
init(logger: Logger, file_name: string, i: IO.TextReader, is_internal_file: bool) is
281
super.init()
282
283
_logger = logger
284
285
_end_of_file = false
286
end_of_input_error_locations = Collections.LIST[LOCATION]()
287
_input = i
288
289
_trivia = Collections.LIST[TRIVIA]()
290
291
if is_internal_file then
292
_cursor = INTERNAL_LOCATION_CURSOR()
293
else
294
_cursor = LOCATION_CURSOR(file_name)
295
fi
296
297
_token_pair = TOKEN_PAIR(TOKEN.UNKNOWN, location, "")
298
299
skip_shebang_line()
300
si
301
302
// A `#!...` line is only meaningful as the file's first two bytes -
303
// the kernel's own rule for an executable script - so this only ever
304
// runs once, from the constructor, before anything else is read.
305
// `#` and `!` are both operator characters, so without this the line
306
// would otherwise tokenise as a run of operators rather than being
307
// read as a directive to skip.
308
skip_shebang_line() is
309
if _input.peek() != cast int('#') then
310
return
311
fi
312
313
_cursor.start()
314
315
let shebang_location = location
316
317
next_char()
318
319
if _input.peek() != cast int('!') then
320
prev_char('#')
321
return
322
fi
323
324
let buffer = System.Text.StringBuilder("#")
325
let c: char mut
326
do
327
c = next_char()
328
if c != '\n' then
329
buffer.append(c)
330
fi
331
332
if !(!_end_of_file /\ c != '\n') then
333
break
334
fi
335
od
336
337
_trivia.add(
338
TRIVIA.SHEBANG(
339
buffer.to_string().trim_end(),
340
shebang_location
341
)
342
)
343
si
344
345
is_end_of_file: bool => _end_of_file
346
347
location: LOCATION => _cursor.location
348
349
character_location: LOCATION => _cursor.character_location
350
351
advance_cursor(c: char) is
352
if c == cast char(13) then
353
return
354
fi
355
356
_cursor.next_column()
357
358
if c == '\n' then
359
_cursor.next_line()
360
fi
361
si
362
363
next_char() -> char is
364
let c: char mut
365
366
_read_end_of_file = false
367
368
if _prev_char != cast char(0) then
369
c = _prev_char
370
advance_cursor(c)
371
_prev_char = cast char(0)
372
return c
373
fi
374
375
if _end_of_file then
376
_read_end_of_file = true
377
return ' '
378
fi
379
380
let c0: int mut = _input.read()
381
382
if c0 == 13 then
383
c0 = 32
384
fi
385
386
if c0 == -1 \/ c0 == 12 then
387
_end_of_file = true
388
_read_end_of_file = true
389
return ' '
390
fi
391
392
c = cast char(c0)
393
394
_cursor.save()
395
advance_cursor(c)
396
397
return c
398
si
399
400
// Pushing back the end of the input leaves everything as it is: it
401
// moved the cursor nowhere, and reading again gives it again.
402
// Restoring would move the cursor back over the character before
403
// it, cutting the input's last token short by one.
404
prev_char(c: char) is
405
if _read_end_of_file then
406
return
407
fi
408
409
_cursor.restore()
410
_prev_char = c
411
si
412
413
current_string: string =>
414
_token_pair.value_string
415
416
read_escape() -> char is
417
let c: char mut = next_char()
418
let result: int mut = 0
419
if c == 't' then
420
return cast char(9)
421
elif c == 'n' then
422
return '\n'
423
elif c == 'r' then
424
return cast char(13)
425
elif c == '\\' then
426
return '\\'
427
elif c == 'u' then
428
// Exactly four hex digits, which is exactly one UTF-16 code
429
// unit and so exactly one char. Fixed width so that the
430
// escape never consumes text that follows it: the characters
431
// after a code are letters as often as not, and a-f are
432
// letters.
433
let escape_location = character_location
434
435
for i in 0..4 do
436
c = next_char()
437
438
if c >= '0' /\ c <= '9' then
439
result = 16 * result + cast int(c - '0')
440
elif c >= 'a' /\ c <= 'f' then
441
result = 16 * result + cast int(c - 'a') + 10
442
elif c >= 'A' /\ c <= 'F' then
443
result = 16 * result + cast int(c - 'A') + 10
444
else
445
prev_char(c)
446
_logger.lexer_error(escape_location, "expected four hex digits after \\u")
447
return cast char(0)
448
fi
449
od
450
451
return cast char(result)
452
elif c>='0' /\ c<='7' then
453
_logger.warn(character_location, "deprecated-octal-escape", "octal escape is deprecated - write the character as \\uXXXX")
454
455
while c >= '0' /\ c <= '7' do
456
result = 8 * result + cast int(c - '0')
457
c = next_char()
458
od
459
460
prev_char(c)
461
return cast char(result)
462
else
463
return c
464
fi
465
si
466
467
skip_white_space() -> char is
468
let c: char mut = _
469
let newlines mut = 0
470
let blank_location: LOCATION? mut = null
471
do
472
c = next_char()
473
474
if c == '\n' then
475
newlines = newlines + 1
476
if newlines == 2 then
477
blank_location = character_location
478
fi
479
fi
480
481
let is_white_space = (c==' ' \/ c==cast char(9) \/ c=='\n')
482
483
let should_break = is_end_of_file \/ !is_white_space
484
485
if is_end_of_file \/ !is_white_space then
486
break
487
fi
488
od
489
490
if newlines >= 2 /\ blank_location? then
491
_trivia.add(TRIVIA.BLANK_LINE(blank_location))
492
fi
493
494
return c
495
si
496
497
expect_format_specifier() is
498
_expect_format_string = true
499
si
500
501
read_token() -> TOKEN_PAIR is
502
let r: TOKEN mut
503
504
let c mut = skip_white_space()
505
506
if _end_of_file then
507
return TOKEN_PAIR(TOKEN.END_OF_INPUT, location, "")
508
fi
509
510
_cursor.start()
511
512
let _buffer mut = System.Text.StringBuilder()
513
514
if _expect_format_string then
515
_expect_format_string = false
516
517
_buffer = System.Text.StringBuilder()
518
519
while !_end_of_file /\ c != '\n' /\ c != '}' /\ c != '"' do
520
_buffer.append(c)
521
522
c = next_char()
523
od
524
525
if c == '}' then
526
prev_char(c)
527
elif c == '\n' then
528
prev_char(c)
529
_logger.lexer_error(character_location, "newline in interpolation format string")
530
return TOKEN_PAIR(TOKEN.CANCEL_STRING, location, _buffer.to_string())
531
elif c == '"' then
532
_logger.lexer_error(character_location, "expected '}}' after format string")
533
return TOKEN_PAIR(TOKEN.CANCEL_STRING, location, _buffer.to_string())
534
elif _end_of_file then
535
_logger.lexer_error(character_location, "end of file in interpolation format string")
536
end_of_input_error_locations.add(character_location)
537
return TOKEN_PAIR(TOKEN.CANCEL_STRING, location, _buffer.to_string())
538
fi
539
540
return TOKEN_PAIR(TOKEN.FORMAT_STRING, location, _buffer.to_string())
541
542
elif c >= '0' /\ c <= '9' then
543
let is_float mut = false
544
545
_buffer = System.Text.StringBuilder()
546
_buffer.append(c)
547
c = next_char()
548
549
if c=='x' \/ c=='X' then
550
_buffer.append(c)
551
c = next_char()
552
while
553
(c>='0'/\c<='9') \/ (c>='A'/\c<='F') \/ (c>='a'/\c<='f') \/ c == '_'
554
do
555
if c != '_' then
556
_buffer.append(c)
557
fi
558
c = next_char()
559
od
560
else
561
let seen_dot mut = false
562
let pc: char mut = _
563
564
while
565
(c >= '0' /\ c <= '9') \/ c == '.' \/ c == '_'
566
do
567
if c == '.' then
568
if pc == '.' then
569
is_float = false
570
571
// bodge: can only push back one char, so use Unicode '‥' to signal we actually want to push back '..'
572
c = '‥'
573
break
574
elif seen_dot then
575
break
576
else
577
is_float = true
578
seen_dot = true
579
fi
580
else
581
if pc == '.' then
582
_buffer.append('.')
583
fi
584
585
if c != '_' then
586
_buffer.append(c)
587
fi
588
fi
589
590
pc = c
591
c = next_char()
592
od
593
fi
594
595
if is_float then
596
if c == 'e' \/ c == 'E' then
597
_buffer.append(c)
598
c = next_char()
599
600
if c == '-' then
601
_buffer.append(c)
602
c = next_char()
603
fi
604
605
if c >= '0' \/ c <= '9' \/ c == '_' then
606
while (c >= '0' /\ c <= '9') \/ c == '_' do
607
if c != '_' then
608
_buffer.append(c)
609
fi
610
611
c = next_char()
612
od
613
else
614
_logger.lexer_error(location, "expected exponent in float literal")
615
fi
616
fi
617
618
if c == 's' \/ c == 'S' \/ c == 'd' \/ c == 'D' \/ c == 'm' \/ c == 'M' then
619
_buffer.append(c)
620
c = next_char()
621
fi
622
else
623
if c == 'm' \/ c == 'M' then
624
_buffer.append(c)
625
c = next_char()
626
is_float = true
627
elif c == '`' then
628
// A backtick attaches a type suffix that would
629
// otherwise be read as a digit of the literal's own
630
// radix - `b` and `c` are hex digits as well as size
631
// selectors, so in hex they are only ever reachable
632
// this way. The backtick stays in the token text: it
633
// is what tells the classifier the suffix is explicit.
634
_buffer.append(c)
635
c = next_char()
636
637
let suffix_start = _buffer.length
638
639
if c == 's' \/ c == 'S' \/ c == 'u' \/ c == 'U' then
640
_buffer.append(c)
641
c = next_char()
642
fi
643
644
if "bBcCsSiIlLnNwW".contains(c) then
645
_buffer.append(c)
646
c = next_char()
647
fi
648
649
if _buffer.length == suffix_start then
650
_logger.lexer_error(character_location, "expected a type suffix after ` in numeric literal")
651
fi
652
else
653
if c == 's' \/ c == 'S' \/ c == 'u' \/ c == 'U' then
654
_buffer.append(c)
655
c = next_char()
656
fi
657
658
if "bBcCsSiIlLnNwW".contains(c) then
659
_buffer.append(c)
660
c = next_char()
661
fi
662
fi
663
fi
664
665
prev_char(c)
666
667
if is_float then
668
return TOKEN_PAIR(TOKEN.FLOAT_LITERAL, location, _buffer.to_string())
669
else
670
return TOKEN_PAIR(TOKEN.INT_LITERAL, location, _buffer.to_string())
671
fi
672
673
elif is_identifier_start(c) \/ c == '`' then
674
let was_escaped: bool mut = false
675
676
if c == '`' then
677
c = next_char()
678
679
if c == '[' then
680
return TOKEN_PAIR(TOKEN.SQUARE_OPEN_TICK, location, "")
681
elif is_operator_char(c) then
682
let o = read_operator(c)
683
return TOKEN_PAIR(TOKEN.IDENTIFIER, o.location, o.value_string)
684
else
685
was_escaped = true
686
fi
687
fi
688
689
_buffer = System.Text.StringBuilder()
690
691
while is_identifier_part(c) \/ is_format_char(c) do
692
if is_format_char(c) then
693
_logger.lexer_error(
694
character_location,
695
"format character U+{cast int(c):X4} in identifier")
696
else
697
_buffer.append(c)
698
fi
699
700
c = next_char()
701
od
702
703
prev_char(c)
704
let s = _buffer.to_string()
705
706
if was_escaped then
707
if s.length == 0 then
708
_logger.lexer_error(location, "expected an identifier after backtick")
709
fi
710
711
r = TOKEN.IDENTIFIER
712
else
713
r = _symbol_tokens[s]
714
fi
715
716
return TOKEN_PAIR(r, location, s)
717
elif c == '/' then
718
let comment_location = location
719
c = next_char()
720
if c == '/' then
721
read_line_comment(comment_location)
722
723
return read_token()
724
elif c == '*' then
725
read_block_comment(comment_location)
726
727
return read_token()
728
else
729
prev_char(c)
730
return read_operator('/')
731
fi
732
elif is_operator_char(c) then
733
return read_operator(c)
734
fi
735
736
case c
737
738
when '\'' then
739
_buffer = System.Text.StringBuilder()
740
741
c = next_char()
742
while c != cast char(39) do
743
if c == cast char(92) then
744
c = read_escape()
745
_buffer.append(c)
746
c = next_char()
747
else
748
_buffer.append(c)
749
c = next_char()
750
fi
751
752
if _end_of_file then
753
_logger.lexer_error(location, "end of file in character literal")
754
end_of_input_error_locations.add(location)
755
break
756
fi
757
od
758
759
if _buffer.length < 1 then
760
_logger.lexer_error(location, "zero length character literal")
761
elif _buffer.length > 1 then
762
_logger.lexer_error(location, "character literal is too long")
763
fi
764
return TOKEN_PAIR(TOKEN.CHAR_LITERAL, location, _buffer.to_string())
765
766
when '(' then
767
return TOKEN_PAIR(TOKEN.PAREN_OPEN, location, "")
768
769
when ')' then
770
return TOKEN_PAIR(TOKEN.PAREN_CLOSE, location, "")
771
772
when '[' then
773
c = next_char()
774
if _end_of_file then
775
return TOKEN_PAIR(TOKEN.SQUARE_OPEN, location, "")
776
elif c == ']' then
777
return TOKEN_PAIR(TOKEN.ARRAY_DEF, location, "")
778
else
779
prev_char(c)
780
return TOKEN_PAIR(TOKEN.SQUARE_OPEN, location, "")
781
fi
782
783
when ']' then
784
return TOKEN_PAIR(TOKEN.SQUARE_CLOSE, location, "")
785
786
when ',' then
787
return TOKEN_PAIR(TOKEN.COMMA, location, "")
788
789
when ';' then
790
return TOKEN_PAIR(TOKEN.SEMICOLON, location, "")
791
792
when '‥' then
793
return read_operator(System.Text.StringBuilder(".."), next_char())
794
795
when '⟦' then
796
return TOKEN_PAIR(TOKEN.SQUARE_OPEN_TICK, location, "")
797
798
when '"' then
799
return string_enter()
800
801
when '}' then
802
return interpolation_exit('}')
803
804
else
805
return TOKEN_PAIR(TOKEN.UNKNOWN, location, "{c}")
806
esac
807
si
808
809
read_operator(c: char mut) -> TOKEN_PAIR =>
810
read_operator(System.Text.StringBuilder(), c)
811
812
// `seed` carries the characters the caller has already consumed on
813
// this token's behalf. The numeric-literal scanner can push back only
814
// one character, so it hands a `..` following a number over as the
815
// single `‥`; seeding with the two dots lets the scan continue into
816
// whatever operator characters follow, so `1..<2` and `i..<2` produce
817
// the same token.
818
read_operator(seed: System.Text.StringBuilder, c: char mut) -> TOKEN_PAIR is
819
let _buffer = seed
820
let first = if _buffer.length > 0 then _buffer[0] else c fi
821
822
// A `.` immediately after a bare `!` or `?` ends the operator: it
823
// begins a member access (`x!.foo`, `x?.foo`), not a longer infix
824
// operator. Longer operators starting with `!`/`?` (e.g. `!=`) and
825
// dot operators (`..`) are unaffected.
826
while
827
is_operator_char(c) /\
828
!(_buffer.length == 1 /\ (first == '!' \/ first == '?') /\ c == '.')
829
do
830
_buffer.append(c)
831
c = next_char()
832
od
833
834
prev_char(c)
835
836
if _buffer.length < 1 then
837
return TOKEN_PAIR(TOKEN.FIRST, location, "")
838
fi
839
840
let s = _buffer.to_string()
841
842
let r mut = TOKEN.OPERATOR
843
844
if _operator_tokens.contains_key(s) then
845
r = _operator_tokens[s]
846
fi
847
848
return TOKEN_PAIR(r, location, s)
849
si
850
851
read_string_fragment() -> (fragment: string, c: char) is
852
let fragment = System.Text.StringBuilder()
853
854
let c mut = next_char()
855
856
while !_end_of_file /\ c != '\n' /\ c != '"' do
857
if c == cast char(92) then
858
c = read_escape()
859
fragment.append(c)
860
c = next_char()
861
elif c == '{' then
862
c = next_char()
863
864
if c == '{' then
865
fragment.append(c)
866
c = next_char()
867
else
868
prev_char(c)
869
c = '{'
870
871
break
872
fi
873
elif c == '}' then
874
c = next_char()
875
876
if c == '}' then
877
fragment.append(c)
878
c = next_char()
879
else
880
_logger.lexer_error(character_location, "unmatched '}}' in string interpolation")
881
882
fragment.append('}')
883
fi
884
else
885
fragment.append(c)
886
c = next_char()
887
fi
888
od
889
890
if c == '\n' then
891
_logger.lexer_error(character_location, "newline in string literal")
892
elif _end_of_file then
893
_logger.lexer_error(character_location, "end of file in string literal")
894
end_of_input_error_locations.add(character_location)
895
fi
896
897
return (fragment.to_string(), c)
898
si
899
900
// Consume the rest of a line comment whose `//` has been read,
901
// recording it as trivia.
902
read_line_comment(comment_location: LOCATION) is
903
let buffer = System.Text.StringBuilder("//")
904
let c: char mut
905
do
906
c = next_char()
907
if c != '\n' then
908
buffer.append(c)
909
fi
910
911
if !(!_end_of_file/\c!='\n') then
912
break
913
fi
914
od
915
916
_trivia.add(
917
TRIVIA.LINE_COMMENT(
918
buffer.to_string().trim_end(),
919
comment_location
920
)
921
)
922
si
923
924
// Consume the rest of a block comment whose `/*` has been read,
925
// recording it as trivia.
926
read_block_comment(comment_location: LOCATION) is
927
let buffer = System.Text.StringBuilder("/*")
928
let c: char mut
929
do
930
c = next_char()
931
932
if c == '*' then
933
c = next_char()
934
if c == '/' then
935
break
936
fi
937
938
buffer.append('*')
939
fi
940
941
buffer.append(c)
942
943
if _end_of_file then
944
break
945
fi
946
od
947
948
buffer.append("*/")
949
950
_trivia.add(
951
TRIVIA.BLOCK_COMMENT(
952
buffer.to_string(),
953
comment_location
954
)
955
)
956
si
957
958
// Adjacent string-literal concatenation: while a closed `"..."`
959
// fragment is followed (after whitespace or comments) by another
960
// `"`, fold the next fragment into this token. Stops at the first
961
// fragment that doesn't close with `"` (i.e. starts an
962
// interpolation or hits a newline). Comments between fragments are
963
// recorded as trivia and chained across, so stripping them cannot
964
// change what the token means; only a `;` separates two
965
// otherwise-adjacent literals.
966
//
967
// We treat the whole `"..." "..." "..."` run as a single token.
968
// The token's end position is wherever the final fragment
969
// terminated — we snapshot the cursor's location after each
970
// successful fragment close. When the chain fails (we consume
971
// whitespace then see a non-`"` char), the cursor is past the
972
// whitespace, but we return the snapshot taken at the previous
973
// close so the token's location end isn't smeared across the
974
// following whitespace. The stream stays at the non-`"` char;
975
// the next read_token picks up from there normally.
976
chain_adjacent_string_fragments(initial_fragment: string, initial_terminator: char) -> (fragment: string, c: char, loc: Source.LOCATION) is
977
let buffer = System.Text.StringBuilder()
978
buffer.append(initial_fragment)
979
let c mut = initial_terminator
980
let saved_loc mut = location
981
982
let done mut = false
983
while !done /\ c == '"' do
984
let peeked = skip_white_space()
985
if peeked == '"' then
986
let (next_fragment, next_terminator) = read_string_fragment()
987
buffer.append(next_fragment)
988
c = next_terminator
989
saved_loc = location
990
elif
991
peeked == '/' /\
992
(_input.peek() == cast int('/') \/ _input.peek() == cast int('*'))
993
then
994
// A comment counts as the whitespace it sits in, here as
995
// everywhere. The raw peek is sound: the pushback slot is
996
// always empty after skip_white_space hands a char out, so
997
// the reader's next character is the one after the `/`.
998
let comment_location = location
999
1000
if next_char() == '/' then
1001
read_line_comment(comment_location)
1002
else
1003
read_block_comment(comment_location)
1004
fi
1005
else
1006
prev_char(peeked)
1007
done = true
1008
fi
1009
od
1010
1011
return (buffer.to_string(), c, saved_loc)
1012
si
1013
1014
string_enter() -> TOKEN_PAIR is
1015
let (fragment, c) mut = read_string_fragment()
1016
1017
let loc: Source.LOCATION mut
1018
(fragment, c, loc) = chain_adjacent_string_fragments(fragment, c)
1019
1020
// The closing quote settles this before anything else is
1021
// consulted: the adjacency lookahead peeks past a literal that
1022
// already closed, and can reach the end of the file doing it,
1023
// so the end-of-file flag says nothing about this literal once
1024
// its quote has been seen.
1025
if c == '"' then
1026
return TOKEN_PAIR(TOKEN.STRING_LITERAL, loc, fragment)
1027
elif c == '\n' \/ _end_of_file then
1028
_interpolation_depth = 0
1029
1030
return TOKEN_PAIR(TOKEN.CANCEL_STRING, loc, fragment)
1031
elif c == '{' then
1032
_interpolation_depth = _interpolation_depth + 1
1033
1034
return TOKEN_PAIR(TOKEN.ENTER_STRING, loc, fragment)
1035
else
1036
_logger.lexer_error(character_location, "unexpected character '{c}' in string interpolation")
1037
1038
return TOKEN_PAIR(TOKEN.STRING_LITERAL, loc, fragment)
1039
fi
1040
si
1041
1042
interpolation_clear() is
1043
_interpolation_depth = 0
1044
si
1045
1046
interpolation_exit(c: char mut) -> TOKEN_PAIR is
1047
if _interpolation_depth == 0 then
1048
_logger.lexer_error(character_location, "unmatched '\"'")
1049
next_char()
1050
return read_token()
1051
fi
1052
1053
let fragment mut = ""
1054
1055
if c == '}' then
1056
(fragment, c) = read_string_fragment()
1057
fi
1058
1059
let loc: Source.LOCATION mut
1060
(fragment, c, loc) = chain_adjacent_string_fragments(fragment, c)
1061
1062
// Closing quote first, for the same reason as in string_enter.
1063
if c == '"' then
1064
_interpolation_depth = _interpolation_depth - 1
1065
1066
return TOKEN_PAIR(TOKEN.EXIT_STRING, loc, fragment)
1067
elif c == '\n' \/ _end_of_file then
1068
_interpolation_depth = 0
1069
return TOKEN_PAIR(TOKEN.CANCEL_STRING, loc, fragment)
1070
elif c == '{' then
1071
return TOKEN_PAIR(TOKEN.CONTINUE_STRING, loc, fragment)
1072
else
1073
_logger.lexer_error(character_location, "unexpected character '{c}' in string interpolation")
1074
1075
return TOKEN_PAIR(TOKEN.STRING_LITERAL, loc, fragment)
1076
fi
1077
si
1078
si
1079
si