lib/std/lang/scanner.rad 17.9 KiB raw
1
//! Lexical scanner for the Radiance programming language.
2
//!
3
//! This module implements a hand-written scanner that tokenizes Radiance
4
//! source code into a stream of tokens for consumption by the parser.
5
@test mod tests;
6
7
use std::char;
8
use std::mem;
9
use std::lang::strings;
10
11
/// Token kinds representing all lexical elements in Radiance.
12
///
13
/// This enum covers operators, keywords, literals, and structural
14
/// elements used by the parser to build the AST.
15
export union TokenKind: Copy {
16
    /// Special end of file token generated when the input is exhausted.
17
    Eof,
18
    /// Special invalid token.
19
    Invalid,
20
21
    LParen,     // (
22
    RParen,     // )
23
    LBrace,     // {
24
    RBrace,     // }
25
    LBracket,   // [
26
    RBracket,   // ]
27
    Comma,      // ,
28
    Dot,        // .
29
    DotDot,     // ..
30
    Minus,      // -
31
    Plus,       // +
32
    Colon,      // :
33
    ColonColon, // ::
34
    Semicolon,  // ;
35
    Slash,      // /
36
    Star,       // *
37
    Percent,    // %
38
    Amp,        // &
39
    Pipe,       // |
40
    Caret,      // ^
41
    Tilde,      // ~
42
    Underscore, // _
43
    Question,   // ?
44
    Bang,       // !
45
    LtGt,       // <>
46
    Equal,      // =
47
    EqualEqual, // ==
48
    Gt,         // >
49
    GtEqual,    // >=
50
    Lt,         // <
51
    LtEqual,    // <=
52
    LtLt,       // <<
53
    GtGt,       // >>
54
    Arrow,      // ->
55
    FatArrow,   // =>
56
57
    // Compound assignment operators.
58
    PlusEqual,    // +=
59
    MinusEqual,   // -=
60
    StarEqual,    // *=
61
    SlashEqual,   // /=
62
    PercentEqual, // %=
63
    AmpEqual,     // &=
64
    PipeEqual,    // |=
65
    CaretEqual,   // ^=
66
    LtLtEqual,    // <<=
67
    GtGtEqual,    // >>=
68
69
    // Boolean operators.
70
    Not, And, Or,
71
72
    /// Eg. `fnord`
73
    Ident,
74
    /// Eg. `@default`
75
    AtIdent,
76
    /// Region name with an apostrophe prefix.
77
    Region,
78
    /// The `log` keyword.
79
    Log,
80
81
    // Literals.
82
    String,     // "fnord"
83
    Char,       // 'f'
84
    Number,     // 42
85
    True,       // true
86
    False,      // false
87
    Nil,        // nil
88
    Undefined,  // undefined
89
90
    // Control flow tokens.
91
    If, Else, Return, Break,
92
    Continue, While, For, In,
93
    Loop, Match, Case, Try, Catch,
94
    Throw, Throws, Panic, Assert,
95
96
    // Variable binding tokens.
97
    Let, Mut, Set, Constant, Align,
98
99
    // Module-related tokens.
100
    Mod, Use, Super,
101
102
    // Type or function attributes.
103
    Export, Static,
104
105
    // Trait-related tokens.
106
    Trait, Instance,
107
108
    // Type-related tokens.
109
    I8, I16, I32, I64, U8, U16, U32, U64,
110
    Opaque, Fn, Bool, Union, Record, As, Unsafe, Where
111
}
112
113
/// A reserved keyword.
114
record Keyword: Copy {
115
    /// Keyword string.
116
    name: *[u8],
117
    /// Corresponding token.
118
    tok: TokenKind,
119
}
120
121
/// Sorted keyword table for binary search.
122
constant KEYWORDS: [Keyword; 53] = [
123
    { name: "align", tok: TokenKind::Align },
124
    { name: "and", tok: TokenKind::And },
125
    { name: "as", tok: TokenKind::As },
126
    { name: "assert", tok: TokenKind::Assert },
127
    { name: "bool", tok: TokenKind::Bool },
128
    { name: "break", tok: TokenKind::Break },
129
    { name: "case", tok: TokenKind::Case },
130
    { name: "catch", tok: TokenKind::Catch },
131
    { name: "constant", tok: TokenKind::Constant },
132
    { name: "continue", tok: TokenKind::Continue },
133
    { name: "else", tok: TokenKind::Else },
134
    { name: "export", tok: TokenKind::Export },
135
    { name: "false", tok: TokenKind::False },
136
    { name: "fn", tok: TokenKind::Fn },
137
    { name: "for", tok: TokenKind::For },
138
    { name: "i16", tok: TokenKind::I16 },
139
    { name: "i32", tok: TokenKind::I32 },
140
    { name: "i64", tok: TokenKind::I64 },
141
    { name: "i8", tok: TokenKind::I8 },
142
    { name: "if", tok: TokenKind::If },
143
    { name: "in", tok: TokenKind::In },
144
    { name: "instance", tok: TokenKind::Instance },
145
    { name: "let", tok: TokenKind::Let },
146
    { name: "log", tok: TokenKind::Log },
147
    { name: "loop", tok: TokenKind::Loop },
148
    { name: "match", tok: TokenKind::Match },
149
    { name: "mod", tok: TokenKind::Mod },
150
    { name: "mut", tok: TokenKind::Mut },
151
    { name: "nil", tok: TokenKind::Nil },
152
    { name: "not", tok: TokenKind::Not },
153
    { name: "opaque", tok: TokenKind::Opaque },
154
    { name: "or", tok: TokenKind::Or },
155
    { name: "panic", tok: TokenKind::Panic },
156
    { name: "record", tok: TokenKind::Record },
157
    { name: "return", tok: TokenKind::Return },
158
    { name: "set", tok: TokenKind::Set },
159
    { name: "static", tok: TokenKind::Static },
160
    { name: "super", tok: TokenKind::Super },
161
    { name: "throw", tok: TokenKind::Throw },
162
    { name: "throws", tok: TokenKind::Throws },
163
    { name: "trait", tok: TokenKind::Trait },
164
    { name: "true", tok: TokenKind::True },
165
    { name: "try", tok: TokenKind::Try },
166
    { name: "u16", tok: TokenKind::U16 },
167
    { name: "u32", tok: TokenKind::U32 },
168
    { name: "u64", tok: TokenKind::U64 },
169
    { name: "u8", tok: TokenKind::U8 },
170
    { name: "undefined", tok: TokenKind::Undefined },
171
    { name: "union", tok: TokenKind::Union },
172
    { name: "unsafe", tok: TokenKind::Unsafe },
173
    { name: "use", tok: TokenKind::Use },
174
    { name: "where", tok: TokenKind::Where },
175
    { name: "while", tok: TokenKind::While },
176
];
177
178
/// Describes where source code originated from.
179
export union SourceLoc: Copy {
180
    /// Source loaded from a file at the given path.
181
    File(*[u8]),
182
    /// Source provided as an inline string (no file path).
183
    String,
184
}
185
186
/// Lexical scanner state for tokenizing Radiance source code.
187
///
188
/// Maintains position information and source buffer reference.
189
export record Scanner: Copy {
190
    /// Origin of the source being scanned.
191
    sourceLoc: SourceLoc,
192
    /// Source buffer.
193
    source: *[u8],
194
    /// Offset of current token into buffer.
195
    token: u32,
196
    /// Offset of current character being scanned.
197
    cursor: u32,
198
}
199
200
/// Individual token with kind, source text, and position.
201
///
202
/// Represents a single lexical element extracted from source,
203
/// including its original text and byte offset for error reporting.
204
export record Token: Copy {
205
    /// Token kind.
206
    kind: TokenKind,
207
    /// Token source string.
208
    source: *[u8],
209
    /// Byte offset of `source` in input buffer.
210
    offset: u32,
211
}
212
213
/// Source code location with line/column information.
214
///
215
/// Used for error reporting and debugging.
216
export record Location: Copy {
217
    /// Origin of the source.
218
    source: SourceLoc,
219
    /// Line number.
220
    line: u16,
221
    /// Column number.
222
    col: u16,
223
}
224
225
/// Create a new scanner object.
226
export fn scanner(sourceLoc: SourceLoc, source: *[u8], pool: &mut strings::Pool) -> Scanner {
227
    // Intern built-in functions and attributes.
228
    strings::intern(pool, "@sizeOf");
229
    strings::intern(pool, "@alignOf");
230
    strings::intern(pool, "@sliceOf");
231
    strings::intern(pool, "@default");
232
    strings::intern(pool, "@intrinsic");
233
    strings::intern(pool, "@test");
234
    // Intern built-in slice methods.
235
    strings::intern(pool, "append");
236
    strings::intern(pool, "delete");
237
238
    return Scanner { sourceLoc, source, token: 0, cursor: 0 };
239
}
240
241
/// Check if we've reached the end of input.
242
export fn isEof(s: &Scanner) -> bool {
243
    return s.cursor >= s.source.len;
244
}
245
246
/// Get the current character, if any.
247
export fn current(s: &Scanner) -> ?u8 {
248
    if isEof(s) {
249
        return nil;
250
    }
251
    return s.source[s.cursor];
252
}
253
254
/// Peek at the next character without advancing the scanner.
255
fn peek(s: &Scanner) -> ?u8 {
256
    if s.cursor + 1 >= s.source.len {
257
        return nil;
258
    }
259
    return s.source[s.cursor + 1];
260
}
261
262
/// Advance scanner and return the character that was consumed.
263
fn advance(s: &mut Scanner) -> u8 {
264
    let ch = s.source[s.cursor];
265
    set s.cursor += 1;
266
    return ch;
267
}
268
269
/// Consume the expected character if it matches the current position.
270
fn consume(s: &mut Scanner, expected: u8) -> bool {
271
    if let c = current(s); c == expected {
272
        advance(s);
273
        return true;
274
    }
275
    return false;
276
}
277
278
/// Create a token from the current scanner state.
279
fn tok(s: &Scanner, kind: TokenKind) -> Token {
280
    return Token { kind, source: &s.source[s.token..s.cursor], offset: s.token };
281
}
282
283
/// Create an invalid token with the given message.
284
export fn invalid(offset: u32, message: *[u8]) -> Token {
285
    return Token { kind: TokenKind::Invalid, source: message, offset };
286
}
287
288
/// Skip whitespace characters and line comments.
289
fn skipWhitespace(s: &mut Scanner) {
290
    while let ch = current(s) {
291
        match ch {
292
            case ' ', '\n', '\r', '\t' => advance(s),
293
            case '/' => {
294
                if let c = peek(s); c == '/' {
295
                    while let ch = current(s); ch <> '\n' {
296
                        advance(s);
297
                    }
298
                } else {
299
                    return;
300
                }
301
            }
302
            else => return,
303
        }
304
    }
305
}
306
307
/// Scan numeric literal (decimal, hex, or binary).
308
fn scanNumber(s: &mut Scanner) -> Token {
309
    // Check for hex literal (`0x` or `0X` prefix).
310
    if s.source[s.cursor - 1] == '0' {
311
        if let ch = current(s); ch == 'x' or ch == 'X' {
312
            advance(s);
313
            // Must have at least one hex digit after `0x`.
314
            if let ch = current(s); not char::isHexDigit(ch) {
315
                return invalid(s.token, "invalid hex literal");
316
            }
317
            while let ch = current(s); char::isHexDigit(ch) {
318
                advance(s);
319
            }
320
            return tok(s, TokenKind::Number);
321
        }
322
        // Check for binary literal (`0b` or `0B` prefix).
323
        if let ch = current(s); ch == 'b' or ch == 'B' {
324
            advance(s);
325
            // Must have at least one binary digit after `0b`.
326
            if let ch = current(s); not char::isBinDigit(ch) {
327
                return invalid(s.token, "invalid binary literal");
328
            }
329
            while let ch = current(s); char::isBinDigit(ch) {
330
                advance(s);
331
            }
332
            return tok(s, TokenKind::Number);
333
        }
334
    }
335
    // Regular decimal number.
336
    while let ch = current(s); char::isDigit(ch) {
337
        advance(s);
338
    }
339
    // Look for decimal part.
340
    if let ch = current(s); ch == '.' {
341
        if let p = peek(s); char::isDigit(p) {
342
            advance(s); // Consume the "."
343
            while let ch = current(s); char::isDigit(ch) {
344
                advance(s);
345
            }
346
        }
347
    }
348
    return tok(s, TokenKind::Number);
349
}
350
351
fn scanDelimited(s: &mut Scanner, delim: u8, kind: TokenKind) -> ?Token {
352
    while let ch = current(s); ch <> delim {
353
        if not char::isPrint(ch) {
354
            return invalid(s.token, "invalid character");
355
        }
356
        if consume(s, '\\') { // Consume escapes
357
            if isEof(s) {
358
                return nil;
359
            }
360
        }
361
        advance(s);
362
    }
363
    if not consume(s, delim) {
364
        return nil;
365
    }
366
    return tok(s, kind);
367
}
368
369
/// Scan string literal enclosed in double quotes.
370
fn scanString(s: &mut Scanner) -> Token {
371
    if let tok = scanDelimited(s, '"', TokenKind::String) {
372
        return tok;
373
    }
374
    return invalid(s.token, "unterminated string");
375
}
376
377
/// Scan an apostrophe-prefixed region name or a quoted character literal.
378
fn scanChar(s: &mut Scanner) -> Token {
379
    if let ch = current(s); char::isAlpha(ch) or ch == '_' {
380
        while let ch = current(s); char::isAlpha(ch) or ch == '_' or char::isDigit(ch) {
381
            advance(s);
382
        }
383
        if consume(s, '\'') {
384
            return tok(s, TokenKind::Char);
385
        }
386
        return tok(s, TokenKind::Region);
387
    }
388
    if let tok = scanDelimited(s, '\'', TokenKind::Char) {
389
        return tok;
390
    }
391
    return invalid(s.token, "unterminated character");
392
}
393
394
/// Scan a keyword or an identifier.
395
fn keywordOrIdent(src: *[u8]) -> TokenKind {
396
    let mut left: u32 = 0;
397
    let mut right: u32 = KEYWORDS.len;
398
399
    while left < right {
400
        let mid = left + ((right - left) / 2);
401
        let kw = &KEYWORDS[mid];
402
        let cmp = mem::cmp(src, kw.name);
403
404
        match cmp {
405
            case -1 => set right = mid,
406
            case 1 => set left = mid + 1,
407
            else => return kw.tok,
408
        }
409
    }
410
    return TokenKind::Ident;
411
}
412
413
/// Scan an identifier, keyword, or label.
414
fn scanIdentifier(s: &mut Scanner, pool: &mut strings::Pool) -> Token {
415
    while let ch = current(s); char::isAlpha(ch) or ch == '_' or char::isDigit(ch) {
416
        advance(s);
417
    }
418
    let ident = &s.source[s.token..s.cursor];
419
    let kind = keywordOrIdent(ident);
420
421
    // Only intern actual identifiers, not keywords.
422
    if kind == TokenKind::Ident {
423
        return Token { kind, source: strings::intern(pool, ident), offset: s.token };
424
    }
425
    return tok(s, kind);
426
}
427
428
/// Scan the next token and intern identifiers in the supplied pool.
429
export fn next(s: &mut Scanner, pool: &mut strings::Pool) -> Token {
430
    skipWhitespace(s);  // Skip any whitespace between tokens.
431
    set s.token = s.cursor; // Token starts at current position.
432
433
    if isEof(s) {
434
        return tok(s, TokenKind::Eof);
435
    }
436
    let c: u8 = advance(s);
437
438
    if char::isDigit(c) {
439
        return scanNumber(s);
440
    }
441
    if char::isAlpha(c) {
442
        return scanIdentifier(s, pool);
443
    }
444
    match c {
445
        case '\'' => return scanChar(s),
446
        case '"'  => return scanString(s),
447
        case '('  => return tok(s, TokenKind::LParen),
448
        case ')'  => return tok(s, TokenKind::RParen),
449
        case '{'  => return tok(s, TokenKind::LBrace),
450
        case '}'  => return tok(s, TokenKind::RBrace),
451
        case '['  => return tok(s, TokenKind::LBracket),
452
        case ']'  => return tok(s, TokenKind::RBracket),
453
        case ';'  => return tok(s, TokenKind::Semicolon),
454
        case ','  => return tok(s, TokenKind::Comma),
455
        case '.'  => {
456
            if consume(s, '.') {
457
                return tok(s, TokenKind::DotDot);
458
            }
459
            return tok(s, TokenKind::Dot);
460
        }
461
        case ':'  => {
462
            if consume(s, ':') {
463
                return tok(s, TokenKind::ColonColon);
464
            }
465
            return tok(s, TokenKind::Colon);
466
        }
467
        case '-'  => {
468
            if consume(s, '>') {
469
                return tok(s, TokenKind::Arrow);
470
            }
471
            if consume(s, '=') {
472
                return tok(s, TokenKind::MinusEqual);
473
            }
474
            return tok(s, TokenKind::Minus);
475
        }
476
        case '+' => {
477
            if consume(s, '=') {
478
                return tok(s, TokenKind::PlusEqual);
479
            }
480
            return tok(s, TokenKind::Plus);
481
        }
482
        case '/' => {
483
            if consume(s, '=') {
484
                return tok(s, TokenKind::SlashEqual);
485
            }
486
            return tok(s, TokenKind::Slash);
487
        }
488
        case '*' => {
489
            if consume(s, '=') {
490
                return tok(s, TokenKind::StarEqual);
491
            }
492
            return tok(s, TokenKind::Star);
493
        }
494
        case '%' => {
495
            if consume(s, '=') {
496
                return tok(s, TokenKind::PercentEqual);
497
            }
498
            return tok(s, TokenKind::Percent);
499
        }
500
        case '&' => {
501
            if consume(s, '=') {
502
                return tok(s, TokenKind::AmpEqual);
503
            }
504
            return tok(s, TokenKind::Amp);
505
        }
506
        case '?' => return tok(s, TokenKind::Question),
507
        case '|' => {
508
            if consume(s, '=') {
509
                return tok(s, TokenKind::PipeEqual);
510
            }
511
            return tok(s, TokenKind::Pipe);
512
        }
513
        case '^' => {
514
            if consume(s, '=') {
515
                return tok(s, TokenKind::CaretEqual);
516
            }
517
            return tok(s, TokenKind::Caret);
518
        }
519
        case '~' => return tok(s, TokenKind::Tilde),
520
        case '!' => return tok(s, TokenKind::Bang),
521
        case '=' => {
522
            if consume(s, '>') {
523
                return tok(s, TokenKind::FatArrow);
524
            }
525
            if consume(s, '=') {
526
                return tok(s, TokenKind::EqualEqual);
527
            }
528
            return tok(s, TokenKind::Equal);
529
        }
530
        case '<' => {
531
            if consume(s, '>') {
532
                return tok(s, TokenKind::LtGt);
533
            }
534
            if consume(s, '<') {
535
                if consume(s, '=') {
536
                    return tok(s, TokenKind::LtLtEqual);
537
                }
538
                return tok(s, TokenKind::LtLt);
539
            }
540
            if consume(s, '=') {
541
                return tok(s, TokenKind::LtEqual);
542
            }
543
            return tok(s, TokenKind::Lt);
544
        }
545
        case '>' => {
546
            if consume(s, '>') {
547
                if consume(s, '=') {
548
                    return tok(s, TokenKind::GtGtEqual);
549
                }
550
                return tok(s, TokenKind::GtGt);
551
            }
552
            if consume(s, '=') {
553
                return tok(s, TokenKind::GtEqual);
554
            }
555
            return tok(s, TokenKind::Gt);
556
        }
557
        case '@' => {
558
            // Scan `@identifier` as a single token.
559
            let ch = current(s) else {
560
                return invalid(s.token, "expected identifier after `@`");
561
            };
562
            if not char::isAlpha(ch) and ch <> '_' {
563
                return invalid(s.token, "expected identifier after `@`");
564
            }
565
            while let ch = current(s); char::isAlpha(ch) or ch == '_' or char::isDigit(ch) {
566
                advance(s);
567
            }
568
            let name = &s.source[s.token..s.cursor];
569
            return Token {
570
                kind: TokenKind::AtIdent,
571
                source: strings::intern(pool, name),
572
                offset: s.token,
573
            };
574
        }
575
        case '_' => {
576
            if let ch = current(s); char::isAlpha(ch) or ch == '_' or char::isDigit(ch) {
577
                // This is part of an identifier like `_foo` or `__start`
578
                return scanIdentifier(s, pool);
579
            }
580
            return tok(s, TokenKind::Underscore);
581
        }
582
        else => return invalid(s.token, "unexpected character"),
583
    }
584
}
585
586
/// Get the source code location from a byte offset.
587
export fn getLocation(sourceLoc: SourceLoc, source: *[u8], offset: u32) -> ?Location {
588
    let mut l: u16 = 1;
589
    let mut c: u16 = 1;
590
591
    if offset >= source.len {
592
        return nil;
593
    }
594
    for ch in &source[..offset] {
595
        if ch == '\n' {
596
            set c = 1;
597
            set l += 1;
598
        } else {
599
            set c += 1;
600
        }
601
    }
602
    return Location { source: sourceLoc, line: l, col: c };
603
}