@xnoxs/flux-lang 3.2.0 → 3.2.2

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 DELETED
@@ -1,518 +0,0 @@
1
- 'use strict';
2
-
3
- // ── Token type constants ──────────────────────────────────────────────────────
4
- const T = {
5
- // Literals
6
- NUMBER: 'NUMBER', STRING: 'STRING', BOOL: 'BOOL', NULL: 'NULL', IDENT: 'IDENT',
7
-
8
- // Keywords — declarations
9
- VAR: 'VAR', VAL: 'VAL', FN: 'FN', RETURN: 'RETURN',
10
-
11
- // Keywords — control flow
12
- IF: 'IF', ELSE: 'ELSE', FOR: 'FOR', IN: 'IN',
13
- WHILE: 'WHILE', BREAK: 'BREAK', CONTINUE: 'CONTINUE',
14
- DO: 'DO',
15
-
16
- // Keywords — OOP
17
- CLASS: 'CLASS', EXTENDS: 'EXTENDS', SELF: 'SELF', NEW: 'NEW',
18
- INTERFACE: 'INTERFACE', IMPLEMENTS: 'IMPLEMENTS',
19
- PRIVATE: 'PRIVATE', PUBLIC: 'PUBLIC', PROTECTED: 'PROTECTED',
20
- READONLY: 'READONLY', STATIC: 'STATIC', ABSTRACT: 'ABSTRACT',
21
- OVERRIDE: 'OVERRIDE',
22
-
23
- // Keywords — pattern matching
24
- MATCH: 'MATCH', WHEN: 'WHEN',
25
-
26
- // Keywords — modules
27
- IMPORT: 'IMPORT', EXPORT: 'EXPORT', FROM: 'FROM', AS: 'AS', DEFAULT: 'DEFAULT',
28
-
29
- // Keywords — logic
30
- AND: 'AND', OR: 'OR', NOT: 'NOT',
31
-
32
- // Keywords — async
33
- ASYNC: 'ASYNC', AWAIT: 'AWAIT',
34
-
35
- // Keywords — error handling
36
- TRY: 'TRY', CATCH: 'CATCH', FINALLY: 'FINALLY', THROW: 'THROW',
37
-
38
- // Keywords — type declarations
39
- TYPEOF: 'TYPEOF', INSTANCEOF: 'INSTANCEOF', TYPE: 'TYPE',
40
- ENUM: 'ENUM', SATISFIES: 'SATISFIES', IS: 'IS', CONST: 'CONST',
41
-
42
- // Operators
43
- PLUS: 'PLUS', MINUS: 'MINUS', STAR: 'STAR', SLASH: 'SLASH', PERCENT: 'PERCENT',
44
- REGEX: 'REGEX',
45
- STARSTAR: 'STARSTAR',
46
- EQ: 'EQ', EQEQ: 'EQEQ', NEQ: 'NEQ', EQEQEQ: 'EQEQEQ', NEQEQ: 'NEQEQ',
47
- LT: 'LT', LTE: 'LTE', GT: 'GT', GTE: 'GTE',
48
- PLUSEQ: 'PLUSEQ', MINUSEQ: 'MINUSEQ', STAREQ: 'STAREQ', SLASHEQ: 'SLASHEQ',
49
- PERCENTEQ: 'PERCENTEQ',
50
- PLUSPLUS: 'PLUSPLUS', // ++
51
- MINUSMINUS: 'MINUSMINUS', // --
52
- AMPERSAND: 'AMPERSAND', // & (bitwise AND / intersection type)
53
- ANDAND: 'ANDAND', // && (logical AND symbol)
54
- PIPEB: 'PIPEB', // | (bitwise OR / union type annotation)
55
- OROR: 'OROR', // || (logical OR symbol)
56
- CARET: 'CARET', // ^ (bitwise XOR)
57
- TILDE: 'TILDE', // ~ (bitwise NOT, unary)
58
- LSHIFT: 'LSHIFT', // << (left shift)
59
- RSHIFT: 'RSHIFT', // >> (right shift)
60
- ARROW: 'ARROW', // -> (inline fn body / lambda)
61
- FATARROW: 'FATARROW', // => (match arm / legacy lambda)
62
- PIPE: 'PIPE', // |> (pipe)
63
- DOTDOT: 'DOTDOT', // .. (range)
64
- DOTDOTDOT: 'DOTDOTDOT', // ... (spread / rest)
65
- WILDCARD: 'WILDCARD', // _
66
- NULLISH: 'NULLISH', // ?? (nullish coalescing)
67
- QUESTIONDOT: 'QUESTIONDOT', // ?. (optional chaining)
68
- BANG: 'BANG', // ! (non-null assertion postfix)
69
- AT: 'AT', // @ (decorator)
70
-
71
- // Punctuation
72
- LPAREN: 'LPAREN', RPAREN: 'RPAREN',
73
- LBRACKET: 'LBRACKET', RBRACKET: 'RBRACKET',
74
- LBRACE: 'LBRACE', RBRACE: 'RBRACE',
75
- COMMA: 'COMMA', DOT: 'DOT', COLON: 'COLON', QUESTION: 'QUESTION',
76
-
77
- // Structural
78
- NEWLINE: 'NEWLINE', INDENT: 'INDENT', DEDENT: 'DEDENT', EOF: 'EOF',
79
- };
80
-
81
- // Backward compat alias used by other modules
82
- const TokenType = T;
83
-
84
- const KEYWORDS = {
85
- var: T.VAR, val: T.VAL, fn: T.FN, return: T.RETURN,
86
- if: T.IF, else: T.ELSE, for: T.FOR, in: T.IN,
87
- while: T.WHILE, break: T.BREAK, continue: T.CONTINUE, do: T.DO,
88
- class: T.CLASS, extends: T.EXTENDS, self: T.SELF, new: T.NEW,
89
- interface: T.INTERFACE, implements: T.IMPLEMENTS,
90
- private: T.PRIVATE, public: T.PUBLIC, protected: T.PROTECTED,
91
- readonly: T.READONLY, static: T.STATIC, abstract: T.ABSTRACT,
92
- override: T.OVERRIDE,
93
- match: T.MATCH, when: T.WHEN,
94
- import: T.IMPORT, export: T.EXPORT, from: T.FROM, as: T.AS, default: T.DEFAULT,
95
- and: T.AND, or: T.OR, not: T.NOT,
96
- async: T.ASYNC, await: T.AWAIT,
97
- try: T.TRY, catch: T.CATCH, finally: T.FINALLY, throw: T.THROW,
98
- typeof: T.TYPEOF, instanceof: T.INSTANCEOF, type: T.TYPE,
99
- enum: T.ENUM, satisfies: T.SATISFIES, is: T.IS, const: T.CONST,
100
- true: '__TRUE__', false: '__FALSE__', null: '__NULL__',
101
- };
102
-
103
- // ── LexerError ────────────────────────────────────────────────────────────────
104
- class LexerError extends Error {
105
- constructor(msg, line, col) {
106
- super(msg);
107
- this.name = 'LexerError';
108
- this.line = line;
109
- this.col = col;
110
- }
111
- }
112
-
113
- // ── Lexer ─────────────────────────────────────────────────────────────────────
114
- class Lexer {
115
- constructor(source) {
116
- this.src = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
117
- this.pos = 0;
118
- this.line = 1;
119
- this.col = 1;
120
- this.tokens = [];
121
- this.indentStack = [0];
122
- this.nestDepth = 0; // inside ()[]{} → skip NEWLINE/INDENT/DEDENT
123
- }
124
-
125
- err(msg) { throw new LexerError(msg, this.line, this.col); }
126
- ch(n = 0) { return this.src[this.pos + n] || ''; }
127
- adv() {
128
- const c = this.src[this.pos++];
129
- c === '\n' ? (this.line++, this.col = 1) : this.col++;
130
- return c;
131
- }
132
- tok(type, value, l, c) {
133
- this.tokens.push({ type, value: value !== undefined ? value : type,
134
- line: l || this.line, col: c || this.col });
135
- }
136
-
137
- // ── Indentation ────────────────────────────────────────────────
138
- applyIndent(indent) {
139
- if (this.nestDepth > 0) return;
140
- const top = this.indentStack[this.indentStack.length - 1];
141
- if (indent > top) {
142
- this.indentStack.push(indent);
143
- this.tok(T.INDENT, indent, this.line, 1);
144
- } else if (indent < top) {
145
- while (this.indentStack.length > 1 &&
146
- this.indentStack[this.indentStack.length - 1] > indent) {
147
- this.indentStack.pop();
148
- this.tok(T.DEDENT, null, this.line, 1);
149
- }
150
- if (this.indentStack[this.indentStack.length - 1] !== indent)
151
- this.err(`Inconsistent indentation (${indent} spaces)`);
152
- }
153
- }
154
-
155
- // ── Backtick string: `raw multiline, no interpolation` ───────
156
- // Kurung kurawal { } tidak di-interpolasi — aman untuk CSS/HTML
157
- scanBacktick(l, c) {
158
- this.adv(); // consume opening `
159
- let s = '';
160
- while (this.pos < this.src.length && this.ch() !== '`') {
161
- if (this.ch() === '\\') {
162
- this.adv();
163
- const e = this.adv();
164
- s += ({ n:'\n', t:'\t', '`':'`', '\\':'\\' }[e] || ('\\'+e));
165
- } else {
166
- s += this.adv();
167
- }
168
- }
169
- if (this.ch() !== '`') this.err('Unterminated backtick string');
170
- this.adv(); // consume closing `
171
- // Trim leading/trailing newlines & normalize indentation
172
- const lines = s.split('\n');
173
- const trimmed = lines.map(ln => ln.trimEnd());
174
- // Remove leading/trailing empty lines
175
- while (trimmed.length && !trimmed[0].trim()) trimmed.shift();
176
- while (trimmed.length && !trimmed[trimmed.length - 1].trim()) trimmed.pop();
177
- // Find minimum indent of non-empty lines
178
- const minIndent = trimmed.filter(ln => ln.trim()).reduce((m, ln) => {
179
- const ind = ln.match(/^(\s*)/)[1].length;
180
- return Math.min(m, ind);
181
- }, Infinity);
182
- const dedented = trimmed.map(ln => ln.slice(minIndent === Infinity ? 0 : minIndent));
183
- this.tok(T.STRING, dedented.join('\n'), l, c);
184
- }
185
-
186
- // ── String: "text {expr} text" ────────────────────────────────
187
- scanStr(l, c) {
188
- this.adv(); // consume opening "
189
- const parts = [];
190
- let text = '';
191
-
192
- while (this.pos < this.src.length && this.ch() !== '"') {
193
- if (this.ch() === '\\') {
194
- this.adv();
195
- const e = this.adv();
196
- text += ({ n:'\n', t:'\t', '"':'"', "'":"'", '\\':'\\', '{':'{', '}':'}' }[e] || ('\\'+e));
197
- } else if (this.ch() === '{') {
198
- parts.push({ type:'text', value:text }); text = '';
199
- this.adv(); // {
200
- let depth = 1, expr = '';
201
- while (this.pos < this.src.length) {
202
- if (this.ch() === '{') depth++;
203
- if (this.ch() === '}') { depth--; if (depth === 0) break; }
204
- // Unescape \" and \' inside expression (they are escapes from outer string)
205
- if (this.ch() === '\\' && (this.src[this.pos+1] === '"' || this.src[this.pos+1] === "'")) {
206
- this.adv(); // skip backslash
207
- expr += this.adv(); // keep the quote char
208
- } else {
209
- expr += this.adv();
210
- }
211
- }
212
- if (this.ch() !== '}') this.err('Unclosed string interpolation');
213
- this.adv(); // }
214
- parts.push({ type:'expr', value:expr.trim() });
215
- } else {
216
- text += this.adv();
217
- }
218
- }
219
- if (this.ch() !== '"') this.err('Unterminated string');
220
- this.adv(); // closing "
221
- parts.push({ type:'text', value:text });
222
-
223
- if (parts.some(p => p.type === 'expr'))
224
- this.tok(T.STRING, { template:true, parts }, l, c);
225
- else
226
- this.tok(T.STRING, parts.map(p => p.value).join(''), l, c);
227
- }
228
-
229
- // ── Regex literal: /pattern/flags ─────────────────────────────
230
- // Opening '/' has already been consumed by the main tokenize loop.
231
- scanRegexBody(l, c) {
232
- let pattern = '';
233
- let inClass = false;
234
- while (this.pos < this.src.length) {
235
- const ch = this.ch();
236
- if (ch === '\n') this.err('Unterminated regex literal');
237
- if (ch === '\\') {
238
- pattern += this.adv();
239
- if (this.pos < this.src.length) pattern += this.adv();
240
- continue;
241
- }
242
- if (ch === '[') { inClass = true; pattern += this.adv(); continue; }
243
- if (ch === ']') { inClass = false; pattern += this.adv(); continue; }
244
- if (ch === '/' && !inClass) break;
245
- pattern += this.adv();
246
- }
247
- if (this.ch() !== '/') this.err('Unterminated regex literal');
248
- this.adv(); // consume closing /
249
- let flags = '';
250
- while (/[gimsuy]/.test(this.ch())) flags += this.adv();
251
- this.tok(T.REGEX, { pattern, flags }, l, c);
252
- }
253
-
254
- // ── Block comment /* ... */ ────────────────────────────────────
255
- scanBlockComment() {
256
- this.adv(); this.adv(); // consume /*
257
- while (this.pos < this.src.length) {
258
- if (this.ch() === '*' && this.ch(1) === '/') { this.adv(); this.adv(); return; }
259
- this.adv();
260
- }
261
- this.err('Unterminated block comment');
262
- }
263
-
264
- // ── Main scan ─────────────────────────────────────────────────
265
- tokenize() {
266
- let bol = true; // beginning of line
267
-
268
- while (this.pos < this.src.length) {
269
- // ─ Measure indentation at line start ──────────────────────
270
- if (bol) {
271
- bol = false;
272
- let indent = 0;
273
- while (this.ch() === ' ' || this.ch() === '\t') {
274
- indent += this.ch() === '\t' ? 4 : 1;
275
- this.adv();
276
- }
277
- // Skip blank / comment-only lines
278
- if (this.ch() === '\n' || !this.ch()) {
279
- if (this.ch() === '\n') { this.adv(); bol = true; }
280
- continue;
281
- }
282
- if (this.ch() === '/' && this.ch(1) === '/') {
283
- while (this.pos < this.src.length && this.ch() !== '\n') this.adv();
284
- if (this.ch() === '\n') { this.adv(); bol = true; }
285
- continue;
286
- }
287
- if (this.ch() === '/' && this.ch(1) === '*') {
288
- this.scanBlockComment();
289
- continue;
290
- }
291
- // Continuation lines: starting with |>, . or ?. — these are
292
- // method-chain / pipe continuations, NOT new statements.
293
- // Skip INDENT/DEDENT tracking and remove the preceding NEWLINE so
294
- // the expression parser sees them as part of the same expression.
295
- const isContinuation = (this.ch() === '|' && this.ch(1) === '>')
296
- || this.ch() === '.'
297
- || (this.ch() === '?' && this.ch(1) === '.');
298
- if (isContinuation) {
299
- // Remove the NEWLINE emitted at the end of the previous line
300
- const last = this.tokens[this.tokens.length - 1];
301
- if (last && last.type === T.NEWLINE) this.tokens.pop();
302
- // Do NOT call applyIndent — no INDENT/DEDENT for continuation lines
303
- } else {
304
- this.applyIndent(indent);
305
- }
306
- }
307
-
308
- const l = this.line, c = this.col, cur = this.ch();
309
-
310
- // ─ Newline ────────────────────────────────────────────────
311
- if (cur === '\n') {
312
- this.adv(); bol = true;
313
- if (this.nestDepth === 0) {
314
- const last = this.tokens[this.tokens.length - 1];
315
- if (last && last.type !== T.NEWLINE &&
316
- last.type !== T.INDENT && last.type !== T.DEDENT)
317
- this.tok(T.NEWLINE, null, l, c);
318
- }
319
- continue;
320
- }
321
-
322
- // ─ Whitespace ─────────────────────────────────────────────
323
- if (cur === ' ' || cur === '\t') { this.adv(); continue; }
324
-
325
- // ─ Line comment ───────────────────────────────────────────
326
- if (cur === '/' && this.ch(1) === '/') {
327
- while (this.pos < this.src.length && this.ch() !== '\n') this.adv();
328
- continue;
329
- }
330
-
331
- // ─ Block comment ──────────────────────────────────────────
332
- if (cur === '/' && this.ch(1) === '*') {
333
- this.scanBlockComment();
334
- continue;
335
- }
336
-
337
- // ─ Number ─────────────────────────────────────────────────
338
- // Note: cur = this.ch() does NOT advance pos; this.ch() still points to cur.
339
- // this.ch(1) is the character AFTER cur.
340
- if (cur >= '0' && cur <= '9') {
341
- // Hex: 0xFF — cur='0', next char is 'x'
342
- if (cur === '0' && (this.ch(1) === 'x' || this.ch(1) === 'X')) {
343
- this.adv(); // skip '0'
344
- this.adv(); // skip 'x'
345
- let h = '';
346
- while (/[0-9a-fA-F_]/.test(this.ch())) {
347
- const c2 = this.adv();
348
- if (c2 !== '_') h += c2;
349
- }
350
- this.tok(T.NUMBER, parseInt(h || '0', 16), l, c);
351
- continue;
352
- }
353
- // Binary: 0b1010 — cur='0', next char is 'b'
354
- if (cur === '0' && (this.ch(1) === 'b' || this.ch(1) === 'B')) {
355
- this.adv(); // skip '0'
356
- this.adv(); // skip 'b'
357
- let b = '';
358
- while (/[01_]/.test(this.ch())) {
359
- const c2 = this.adv();
360
- if (c2 !== '_') b += c2;
361
- }
362
- this.tok(T.NUMBER, parseInt(b || '0', 2), l, c);
363
- continue;
364
- }
365
- // Decimal with optional _ separators: 1_000_000
366
- // Also handles scientific notation: 1e9, 1e-9, 1.5E+10
367
- // this.ch() still points to cur (first digit) — start n empty,
368
- // let the while loop consume it via this.adv()
369
- let n = '';
370
- while (this.pos < this.src.length) {
371
- if (this.ch() === '.' && this.ch(1) === '.') break;
372
- if ((this.ch() >= '0' && this.ch() <= '9') || this.ch() === '.') n += this.adv();
373
- else if (this.ch() === '_') { this.adv(); } // numeric separator — skip
374
- else if ((this.ch() === 'e' || this.ch() === 'E') && n.length > 0) {
375
- // Scientific notation exponent
376
- n += this.adv(); // consume 'e' or 'E'
377
- if (this.ch() === '+' || this.ch() === '-') n += this.adv(); // optional sign
378
- while (this.ch() >= '0' && this.ch() <= '9') n += this.adv(); // exponent digits
379
- break;
380
- }
381
- else break;
382
- }
383
- this.tok(T.NUMBER, parseFloat(n), l, c);
384
- continue;
385
- }
386
-
387
- // ─ String ─────────────────────────────────────────────────
388
- if (cur === '"') { this.scanStr(l, c); continue; }
389
- if (cur === '`') { this.scanBacktick(l, c); continue; }
390
- if (cur === "'") {
391
- this.adv();
392
- let s = '';
393
- while (this.pos < this.src.length && this.ch() !== "'") {
394
- if (this.ch() === '\\') {
395
- this.adv();
396
- const e = this.adv();
397
- s += ({ n:'\n', t:'\t', r:'\r', "'":"'", '\\':'\\' }[e] || ('\\'+e));
398
- } else {
399
- s += this.adv();
400
- }
401
- }
402
- if (this.ch() !== "'") this.err('Unterminated string');
403
- this.adv(); this.tok(T.STRING, s, l, c);
404
- continue;
405
- }
406
-
407
- // ─ Identifiers / keywords ─────────────────────────────────
408
- if ((cur >= 'a' && cur <= 'z') || (cur >= 'A' && cur <= 'Z') || cur === '_') {
409
- let word = '';
410
- while (/[a-zA-Z0-9_]/.test(this.ch())) word += this.adv();
411
-
412
- if (word === '_' && !/[a-zA-Z0-9_]/.test(this.ch())) {
413
- this.tok(T.WILDCARD, '_', l, c); continue;
414
- }
415
- const kw = Object.prototype.hasOwnProperty.call(KEYWORDS, word) ? KEYWORDS[word] : undefined;
416
- if (kw === '__TRUE__') this.tok(T.BOOL, true, l, c);
417
- else if (kw === '__FALSE__') this.tok(T.BOOL, false, l, c);
418
- else if (kw === '__NULL__') this.tok(T.NULL, null, l, c);
419
- else if (kw) this.tok(kw, word, l, c);
420
- else this.tok(T.IDENT, word, l, c);
421
- continue;
422
- }
423
-
424
- // ─ Operators & punctuation ────────────────────────────────
425
- this.adv();
426
- switch (cur) {
427
- case '+': this.ch()==='+' ? (this.adv(), this.tok(T.PLUSPLUS, '++',l,c))
428
- : this.ch()==='=' ? (this.adv(), this.tok(T.PLUSEQ, '+=',l,c)) : this.tok(T.PLUS, '+',l,c); break;
429
- case '-': this.ch()==='-' ? (this.adv(), this.tok(T.MINUSMINUS,'--',l,c))
430
- : this.ch()==='>' ? (this.adv(), this.tok(T.ARROW, '->',l,c))
431
- : this.ch()==='=' ? (this.adv(), this.tok(T.MINUSEQ, '-=',l,c)) : this.tok(T.MINUS,'-',l,c); break;
432
- case '*': this.ch()==='*' ? (this.adv(), this.tok(T.STARSTAR, '**',l,c))
433
- : this.ch()==='=' ? (this.adv(), this.tok(T.STAREQ, '*=',l,c)) : this.tok(T.STAR, '*',l,c); break;
434
- case '/':
435
- if (this.ch() === '=') { this.adv(); this.tok(T.SLASHEQ, '/=', l, c); }
436
- else {
437
- // Context heuristic: '/' is division after a value-producing token,
438
- // otherwise it starts a regex literal.
439
- const _last = this.tokens[this.tokens.length - 1];
440
- const _afterVal = _last && (
441
- _last.type === T.IDENT || _last.type === T.NUMBER || _last.type === T.STRING ||
442
- _last.type === T.BOOL || _last.type === T.NULL || _last.type === T.REGEX ||
443
- _last.type === T.RPAREN || _last.type === T.RBRACKET ||
444
- _last.type === T.PLUSPLUS || _last.type === T.MINUSMINUS || _last.type === T.BANG
445
- );
446
- if (_afterVal) this.tok(T.SLASH, '/', l, c);
447
- else this.scanRegexBody(l, c);
448
- }
449
- break;
450
- case '%': this.ch()==='=' ? (this.adv(), this.tok(T.PERCENTEQ,'%=',l,c)) : this.tok(T.PERCENT,'%',l,c); break;
451
- case '=':
452
- if (this.ch()==='=' && this.ch(1)==='=') { this.adv(); this.adv(); this.tok(T.EQEQEQ,'===',l,c); }
453
- else if (this.ch()==='=') { this.adv(); this.tok(T.EQEQ,'==',l,c); }
454
- else if (this.ch()==='>') { this.adv(); this.tok(T.FATARROW,'=>',l,c); }
455
- else { this.tok(T.EQ,'=',l,c); }
456
- break;
457
- case '!':
458
- if (this.ch()==='=' && this.ch(1)==='=') { this.adv(); this.adv(); this.tok(T.NEQEQ,'!==',l,c); }
459
- else if (this.ch()==='=') { this.adv(); this.tok(T.NEQ,'!=',l,c); }
460
- else { this.tok(T.BANG,'!',l,c); }
461
- break;
462
- case '<': this.ch()==='<' ? (this.adv(), this.tok(T.LSHIFT, '<<',l,c))
463
- : this.ch()==='=' ? (this.adv(), this.tok(T.LTE, '<=',l,c)) : this.tok(T.LT, '<',l,c); break;
464
- case '>': this.ch()==='>' ? (this.adv(), this.tok(T.RSHIFT, '>>',l,c))
465
- : this.ch()==='=' ? (this.adv(), this.tok(T.GTE, '>=',l,c)) : this.tok(T.GT, '>',l,c); break;
466
- case '.':
467
- if (this.ch() === '.' && this.ch(1) === '.') {
468
- this.adv(); this.adv(); this.tok(T.DOTDOTDOT, '...', l, c);
469
- } else if (this.ch() === '.') {
470
- this.adv(); this.tok(T.DOTDOT, '..', l, c);
471
- } else {
472
- this.tok(T.DOT, '.', l, c);
473
- }
474
- break;
475
- case '?':
476
- if (this.ch() === '.') { this.adv(); this.tok(T.QUESTIONDOT, '?.', l, c); }
477
- else if (this.ch() === '?') { this.adv(); this.tok(T.NULLISH, '??', l, c); }
478
- else this.tok(T.QUESTION, '?', l, c);
479
- break;
480
- case '|':
481
- if (this.ch() === '>') { this.adv(); this.tok(T.PIPE, '|>', l, c); }
482
- else if (this.ch() === '|') { this.adv(); this.tok(T.OROR, '||', l, c); }
483
- else { this.tok(T.PIPEB, '|', l, c); }
484
- break;
485
- case '&':
486
- if (this.ch() === '&') { this.adv(); this.tok(T.ANDAND,'&&',l,c); }
487
- else { this.tok(T.AMPERSAND,'&',l,c); }
488
- break;
489
- case '^': this.tok(T.CARET, '^',l,c); break;
490
- case '~': this.tok(T.TILDE, '~',l,c); break;
491
- case '@': this.tok(T.AT, '@',l,c); break;
492
- case ';': /* optional statement separator — ignored */ break;
493
- case '(': this.nestDepth++; this.tok(T.LPAREN, '(',l,c); break;
494
- case ')': this.nestDepth--; this.tok(T.RPAREN, ')',l,c); break;
495
- case '[': this.nestDepth++; this.tok(T.LBRACKET, '[',l,c); break;
496
- case ']': this.nestDepth--; this.tok(T.RBRACKET, ']',l,c); break;
497
- case '{': this.nestDepth++; this.tok(T.LBRACE, '{',l,c); break;
498
- case '}': this.nestDepth--; this.tok(T.RBRACE, '}',l,c); break;
499
- case ',': this.tok(T.COMMA,',',l,c); break;
500
- case ':': this.tok(T.COLON,':',l,c); break;
501
- default: this.err(`Unknown character: '${cur}'`);
502
- }
503
- }
504
-
505
- // Close any remaining open blocks
506
- while (this.indentStack.length > 1) {
507
- this.indentStack.pop();
508
- this.tok(T.DEDENT, null, this.line, 1);
509
- }
510
- const last = this.tokens[this.tokens.length - 1];
511
- if (last && last.type !== T.NEWLINE && last.type !== T.DEDENT)
512
- this.tok(T.NEWLINE, null, this.line, this.col);
513
- this.tok(T.EOF, null, this.line, this.col);
514
- return this.tokens;
515
- }
516
- }
517
-
518
- module.exports = { Lexer, T, TokenType: T };