File size: 4,586 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
/**
 * Lexer for the CUDA-like kernel language.
 *
 * Emits a token stream with source offsets so parser and lowering errors can point
 * at a line. Whitespace and comments are skipped; nothing is evaluated here.
 */

export const TokenType = {
  IDENT: 'ident',
  KEYWORD: 'keyword',
  INT: 'int',
  FLOAT: 'float',
  PUNCT: 'punct',
  EOF: 'eof',
};

const KEYWORDS = new Set([
  '__global__',
  '__device__',
  '__shared__',
  'void',
  'int',
  'uint',
  'float',
  'double',
  'const',
  'return',
  'if',
  'else',
  'while',
  'for',
  'break',
  'continue',
  'true',
  'false',
]);

/** Multi-character operators are matched before single-character ones. */
const PUNCTUATORS = [
  '&&', '||', '==', '!=', '<=', '>=', '<<', '>>', '+=', '-=', '*=', '/=', '%=',
  '++', '--', '->',
  '{', '}', '(', ')', '[', ']', ';', ',', '<', '>', '=', '+', '-', '*', '/', '%', '!', '&', '|', '^', '~', '.',
];

export class LexError extends Error {
  constructor(message, line, column) {
    super(`${message} (line ${line}, column ${column})`);
    this.name = 'LexError';
    this.line = line;
    this.column = column;
  }
}

const isDigit = (c) => c >= '0' && c <= '9';
const isIdentStart = (c) => /[A-Za-z_]/.test(c);
const isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);

export function tokenize(source) {
  const tokens = [];
  let i = 0;
  let line = 1;
  let lineStart = 0;

  const column = (offset) => offset - lineStart + 1;

  while (i < source.length) {
    const c = source[i];

    if (c === '\n') {
      i += 1;
      line += 1;
      lineStart = i;
      continue;
    }
    if (c === ' ' || c === '\t' || c === '\r') {
      i += 1;
      continue;
    }
    // Comments run to end of line.
    if (c === '/' && source[i + 1] === '/') {
      while (i < source.length && source[i] !== '\n') i += 1;
      continue;
    }
    if (c === '/' && source[i + 1] === '*') {
      const startLine = line;
      const startColumn = column(i);
      i += 2;
      for (;;) {
        if (i >= source.length) throw new LexError('Unterminated block comment', startLine, startColumn);
        if (source[i] === '\n') {
          line += 1;
          i += 1;
          lineStart = i;
          continue;
        }
        if (source[i] === '*' && source[i + 1] === '/') {
          i += 2;
          break;
        }
        i += 1;
      }
      continue;
    }

    // Identifiers, keywords and __global__-style qualifiers.
    if (isIdentStart(c)) {
      const start = i;
      while (i < source.length && isIdentPart(source[i])) i += 1;
      const value = source.slice(start, i);
      tokens.push({
        type: KEYWORDS.has(value) ? TokenType.KEYWORD : TokenType.IDENT,
        value,
        line,
        column: column(start),
        offset: start,
      });
      continue;
    }

    // Numbers: decimal and 0x hex, with optional fractional part and f suffix.
    if (isDigit(c) || (c === '.' && isDigit(source[i + 1]))) {
      const start = i;
      let isFloat = false;
      if (c === '0' && (source[i + 1] === 'x' || source[i + 1] === 'X')) {
        i += 2;
        while (i < source.length && /[0-9a-fA-F]/.test(source[i])) i += 1;
      } else {
        while (i < source.length && isDigit(source[i])) i += 1;
        if (source[i] === '.') {
          isFloat = true;
          i += 1;
          while (i < source.length && isDigit(source[i])) i += 1;
        }
        if (source[i] === 'e' || source[i] === 'E') {
          isFloat = true;
          i += 1;
          if (source[i] === '+' || source[i] === '-') i += 1;
          while (i < source.length && isDigit(source[i])) i += 1;
        }
      }
      if (source[i] === 'f' || source[i] === 'F') {
        isFloat = true;
        i += 1;
      }
      const text = source.slice(start, i);
      if (i < source.length && isIdentStart(source[i])) {
        throw new LexError(`Malformed numeric literal '${text}${source[i]}'`, line, column(start));
      }
      tokens.push({ type: isFloat ? TokenType.FLOAT : TokenType.INT, value: text, line, column: column(start), offset: start });
      continue;
    }

    // Punctuators, longest match first.
    const punct = PUNCTUATORS.find((p) => source.startsWith(p, i));
    if (punct) {
      const start = i;
      i += punct.length;
      tokens.push({ type: TokenType.PUNCT, value: punct, line, column: column(start), offset: start });
      continue;
    }

    throw new LexError(`Unexpected character '${c}'`, line, column(i));
  }

  tokens.push({ type: TokenType.EOF, value: '<end of input>', line, column: column(i), offset: i });
  return tokens;
}