naidejs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lexer.js ADDED
@@ -0,0 +1,389 @@
1
+ import { T, KEYWORDS } from './tokens.js';
2
+
3
+ class Token {
4
+ constructor(type, value, line, col) {
5
+ this.type = type;
6
+ this.value = value;
7
+ this.line = line;
8
+ this.col = col;
9
+ }
10
+ }
11
+
12
+ export class Lexer {
13
+ constructor(source) {
14
+ this.source = source;
15
+ this.pos = 0;
16
+ this.line = 1;
17
+ this.col = 1;
18
+ this.tokens = [];
19
+ this.indentStack = [0];
20
+ this.atLineStart = true;
21
+ }
22
+
23
+ peek() {
24
+ return this.pos < this.source.length ? this.source[this.pos] : null;
25
+ }
26
+
27
+ advance() {
28
+ const ch = this.source[this.pos++];
29
+ if (ch === '\n') {
30
+ this.line++;
31
+ this.col = 1;
32
+ } else {
33
+ this.col++;
34
+ }
35
+ return ch;
36
+ }
37
+
38
+ lookAhead(n = 1) {
39
+ return this.pos + n < this.source.length ? this.source[this.pos + n] : null;
40
+ }
41
+
42
+ makeToken(type, value) {
43
+ return new Token(type, value, this.line, this.col);
44
+ }
45
+
46
+ tokenize() {
47
+ while (this.pos < this.source.length) {
48
+ if (this.atLineStart) {
49
+ this.handleIndentation();
50
+ this.atLineStart = false;
51
+ }
52
+
53
+ const ch = this.peek();
54
+ if (ch === null) break;
55
+
56
+ if (ch === '\n') {
57
+ this.advance();
58
+ // Skip consecutive newlines
59
+ if (this.tokens.length > 0 && this.tokens[this.tokens.length - 1].type !== T.NEWLINE) {
60
+ this.tokens.push(this.makeToken(T.NEWLINE, '\n'));
61
+ }
62
+ this.atLineStart = true;
63
+ continue;
64
+ }
65
+
66
+ if (ch === '\r') {
67
+ this.advance();
68
+ continue;
69
+ }
70
+
71
+ if (ch === ' ' || ch === '\t') {
72
+ this.advance();
73
+ continue;
74
+ }
75
+
76
+ if (ch === '#') {
77
+ this.skipComment();
78
+ continue;
79
+ }
80
+
81
+ if (ch === '"' || ch === "'") {
82
+ this.readString(ch);
83
+ continue;
84
+ }
85
+
86
+ if (ch === '`') {
87
+ this.readTemplateString();
88
+ continue;
89
+ }
90
+
91
+ if (this.isDigit(ch)) {
92
+ this.readNumber();
93
+ continue;
94
+ }
95
+
96
+ if (this.isIdentStart(ch)) {
97
+ this.readIdentifier();
98
+ continue;
99
+ }
100
+
101
+ this.readOperator();
102
+ }
103
+
104
+ // Emit remaining DEDENTs
105
+ while (this.indentStack.length > 1) {
106
+ this.indentStack.pop();
107
+ this.tokens.push(this.makeToken(T.DEDENT, ''));
108
+ }
109
+
110
+ this.tokens.push(this.makeToken(T.EOF, ''));
111
+ return this.tokens;
112
+ }
113
+
114
+ handleIndentation() {
115
+ let indent = 0;
116
+ while (this.pos < this.source.length) {
117
+ const ch = this.source[this.pos];
118
+ if (ch === ' ') {
119
+ indent++;
120
+ this.pos++;
121
+ this.col++;
122
+ } else if (ch === '\t') {
123
+ indent += 2;
124
+ this.pos++;
125
+ this.col++;
126
+ } else {
127
+ break;
128
+ }
129
+ }
130
+
131
+ // Skip blank lines and comment-only lines
132
+ if (this.pos >= this.source.length || this.source[this.pos] === '\n' || this.source[this.pos] === '#') {
133
+ return;
134
+ }
135
+
136
+ const currentIndent = this.indentStack[this.indentStack.length - 1];
137
+
138
+ if (indent > currentIndent) {
139
+ this.indentStack.push(indent);
140
+ this.tokens.push(this.makeToken(T.INDENT, indent));
141
+ } else if (indent < currentIndent) {
142
+ while (this.indentStack.length > 1 && this.indentStack[this.indentStack.length - 1] > indent) {
143
+ this.indentStack.pop();
144
+ this.tokens.push(this.makeToken(T.DEDENT, ''));
145
+ }
146
+ }
147
+ }
148
+
149
+ skipComment() {
150
+ while (this.pos < this.source.length && this.source[this.pos] !== '\n') {
151
+ this.advance();
152
+ }
153
+ }
154
+
155
+ readString(quote) {
156
+ const startLine = this.line;
157
+ const startCol = this.col;
158
+ this.advance(); // skip opening quote
159
+
160
+ let parts = [];
161
+ let current = '';
162
+
163
+ while (this.pos < this.source.length && this.peek() !== quote) {
164
+ if (this.peek() === '\\') {
165
+ this.advance();
166
+ const esc = this.advance();
167
+ switch (esc) {
168
+ case 'n': current += '\n'; break;
169
+ case 't': current += '\t'; break;
170
+ case 'r': current += '\r'; break;
171
+ case '\\': current += '\\'; break;
172
+ case '"': current += '"'; break;
173
+ case "'": current += "'"; break;
174
+ case '{': current += '{'; break;
175
+ default: current += '\\' + esc;
176
+ }
177
+ } else if (this.peek() === '{' && quote === '"') {
178
+ // String interpolation - only in double quotes
179
+ if (current) {
180
+ parts.push({ type: 'text', value: current });
181
+ current = '';
182
+ }
183
+ this.advance(); // skip {
184
+ let expr = '';
185
+ let depth = 1;
186
+ while (this.pos < this.source.length && depth > 0) {
187
+ if (this.peek() === '{') depth++;
188
+ if (this.peek() === '}') depth--;
189
+ if (depth > 0) expr += this.advance();
190
+ else this.advance(); // skip closing }
191
+ }
192
+ parts.push({ type: 'expr', value: expr.trim() });
193
+ } else {
194
+ current += this.advance();
195
+ }
196
+ }
197
+
198
+ if (this.peek() === quote) this.advance(); // skip closing quote
199
+
200
+ if (current) parts.push({ type: 'text', value: current });
201
+
202
+ if (parts.length === 0) {
203
+ this.tokens.push(new Token(T.STRING, { parts: [{ type: 'text', value: '' }], raw: '' }, startLine, startCol));
204
+ } else if (parts.length === 1 && parts[0].type === 'text') {
205
+ this.tokens.push(new Token(T.STRING, { parts, raw: parts[0].value }, startLine, startCol));
206
+ } else {
207
+ this.tokens.push(new Token(T.STRING, { parts, raw: null }, startLine, startCol));
208
+ }
209
+ }
210
+
211
+ readTemplateString() {
212
+ const startLine = this.line;
213
+ const startCol = this.col;
214
+ this.advance(); // skip `
215
+ let value = '';
216
+ while (this.pos < this.source.length && this.peek() !== '`') {
217
+ if (this.peek() === '\\') {
218
+ this.advance();
219
+ value += this.advance();
220
+ } else {
221
+ value += this.advance();
222
+ }
223
+ }
224
+ if (this.peek() === '`') this.advance();
225
+ this.tokens.push(new Token(T.STRING, { parts: [{ type: 'text', value }], raw: value }, startLine, startCol));
226
+ }
227
+
228
+ readNumber() {
229
+ const startLine = this.line;
230
+ const startCol = this.col;
231
+ let num = '';
232
+ let isFloat = false;
233
+
234
+ while (this.pos < this.source.length && (this.isDigit(this.peek()) || this.peek() === '.' || this.peek() === '_')) {
235
+ if (this.peek() === '.') {
236
+ if (isFloat) break;
237
+ if (this.lookAhead() === '.') break; // range operator ..
238
+ isFloat = true;
239
+ }
240
+ if (this.peek() !== '_') {
241
+ num += this.peek();
242
+ }
243
+ this.advance();
244
+ }
245
+
246
+ this.tokens.push(new Token(T.NUMBER, num, startLine, startCol));
247
+ }
248
+
249
+ readIdentifier() {
250
+ const startLine = this.line;
251
+ const startCol = this.col;
252
+ let ident = '';
253
+
254
+ while (this.pos < this.source.length && this.isIdentChar(this.peek())) {
255
+ ident += this.advance();
256
+ }
257
+
258
+ // Check for compound keywords
259
+ if (ident === 'fn' && this.peek() === '.') {
260
+ const savedPos = this.pos;
261
+ const savedCol = this.col;
262
+ this.advance(); // skip .
263
+ let sub = '';
264
+ while (this.pos < this.source.length && this.isIdentChar(this.peek())) {
265
+ sub += this.advance();
266
+ }
267
+ if (sub === 'async') {
268
+ this.tokens.push(new Token(T.FN_ASYNC, 'fn.async', startLine, startCol));
269
+ return;
270
+ }
271
+ // Restore if not a compound keyword
272
+ this.pos = savedPos;
273
+ this.col = savedCol;
274
+ }
275
+
276
+ if (ident === 'await' && this.peek() === '.') {
277
+ const savedPos = this.pos;
278
+ const savedCol = this.col;
279
+ this.advance();
280
+ let sub = '';
281
+ while (this.pos < this.source.length && this.isIdentChar(this.peek())) {
282
+ sub += this.advance();
283
+ }
284
+ if (sub === 'all') {
285
+ this.tokens.push(new Token(T.AWAIT_ALL, 'await.all', startLine, startCol));
286
+ return;
287
+ }
288
+ this.pos = savedPos;
289
+ this.col = savedCol;
290
+ }
291
+
292
+ if (ident === 'ret' && this.peek() === '.') {
293
+ // ret.status, ret.json etc - keep as IDENT with dot access
294
+ this.tokens.push(new Token(T.RET, ident, startLine, startCol));
295
+ return;
296
+ }
297
+
298
+ const keyword = KEYWORDS[ident];
299
+ if (keyword) {
300
+ this.tokens.push(new Token(keyword, ident, startLine, startCol));
301
+ } else {
302
+ this.tokens.push(new Token(T.IDENT, ident, startLine, startCol));
303
+ }
304
+ }
305
+
306
+ readOperator() {
307
+ const startLine = this.line;
308
+ const startCol = this.col;
309
+ const ch = this.advance();
310
+
311
+ switch (ch) {
312
+ case '=':
313
+ if (this.peek() === '=') { this.advance(); this.tokens.push(new Token(T.EQ, '==', startLine, startCol)); }
314
+ else if (this.peek() === '>') { this.advance(); this.tokens.push(new Token(T.FAT_ARROW, '=>', startLine, startCol)); }
315
+ else this.tokens.push(new Token(T.ASSIGN, '=', startLine, startCol));
316
+ break;
317
+ case '+':
318
+ if (this.peek() === '=') { this.advance(); this.tokens.push(new Token(T.PLUS_ASSIGN, '+=', startLine, startCol)); }
319
+ else this.tokens.push(new Token(T.PLUS, '+', startLine, startCol));
320
+ break;
321
+ case '-':
322
+ if (this.peek() === '=') { this.advance(); this.tokens.push(new Token(T.MINUS_ASSIGN, '-=', startLine, startCol)); }
323
+ else if (this.peek() === '>') { this.advance(); this.tokens.push(new Token(T.ARROW, '->', startLine, startCol)); }
324
+ else this.tokens.push(new Token(T.MINUS, '-', startLine, startCol));
325
+ break;
326
+ case '*':
327
+ this.tokens.push(new Token(T.STAR, '*', startLine, startCol));
328
+ break;
329
+ case '/':
330
+ this.tokens.push(new Token(T.SLASH, '/', startLine, startCol));
331
+ break;
332
+ case '%':
333
+ this.tokens.push(new Token(T.PERCENT, '%', startLine, startCol));
334
+ break;
335
+ case '!':
336
+ if (this.peek() === '=') { this.advance(); this.tokens.push(new Token(T.NEQ, '!=', startLine, startCol)); }
337
+ else this.tokens.push(new Token(T.NOT, '!', startLine, startCol));
338
+ break;
339
+ case '>':
340
+ if (this.peek() === '=') { this.advance(); this.tokens.push(new Token(T.GTE, '>=', startLine, startCol)); }
341
+ else this.tokens.push(new Token(T.GT, '>', startLine, startCol));
342
+ break;
343
+ case '<':
344
+ if (this.peek() === '=') { this.advance(); this.tokens.push(new Token(T.LTE, '<=', startLine, startCol)); }
345
+ else this.tokens.push(new Token(T.LT, '<', startLine, startCol));
346
+ break;
347
+ case '|':
348
+ if (this.peek() === '>') { this.advance(); this.tokens.push(new Token(T.PIPE, '|>', startLine, startCol)); }
349
+ else this.tokens.push(new Token(T.IDENT, '|', startLine, startCol));
350
+ break;
351
+ case '?':
352
+ if (this.peek() === '?') { this.advance(); this.tokens.push(new Token(T.NULLISH, '??', startLine, startCol)); }
353
+ else if (this.peek() === '.') { this.advance(); this.tokens.push(new Token(T.OPTIONAL, '?.', startLine, startCol)); }
354
+ else this.tokens.push(new Token(T.IDENT, '?', startLine, startCol));
355
+ break;
356
+ case '.':
357
+ if (this.peek() === '.') {
358
+ this.advance();
359
+ if (this.peek() === '.') { this.advance(); this.tokens.push(new Token(T.SPREAD, '...', startLine, startCol)); }
360
+ else this.tokens.push(new Token(T.RANGE, '..', startLine, startCol));
361
+ } else {
362
+ this.tokens.push(new Token(T.DOT, '.', startLine, startCol));
363
+ }
364
+ break;
365
+ case '(': this.tokens.push(new Token(T.LPAREN, '(', startLine, startCol)); break;
366
+ case ')': this.tokens.push(new Token(T.RPAREN, ')', startLine, startCol)); break;
367
+ case '[': this.tokens.push(new Token(T.LBRACKET, '[', startLine, startCol)); break;
368
+ case ']': this.tokens.push(new Token(T.RBRACKET, ']', startLine, startCol)); break;
369
+ case '{': this.tokens.push(new Token(T.LBRACE, '{', startLine, startCol)); break;
370
+ case '}': this.tokens.push(new Token(T.RBRACE, '}', startLine, startCol)); break;
371
+ case ':': this.tokens.push(new Token(T.COLON, ':', startLine, startCol)); break;
372
+ case ',': this.tokens.push(new Token(T.COMMA, ',', startLine, startCol)); break;
373
+ default:
374
+ throw new Error(`Unexpected character '${ch}' at line ${startLine}:${startCol}`);
375
+ }
376
+ }
377
+
378
+ isDigit(ch) {
379
+ return ch >= '0' && ch <= '9';
380
+ }
381
+
382
+ isIdentStart(ch) {
383
+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$';
384
+ }
385
+
386
+ isIdentChar(ch) {
387
+ return this.isIdentStart(ch) || this.isDigit(ch);
388
+ }
389
+ }