@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/CHANGELOG.md +20 -0
- package/README.md +915 -597
- package/bin/flux.js +1 -1395
- package/dist/flux-cli.js +9564 -0
- package/dist/flux.cjs.js +1 -1
- package/dist/flux.esm.js +1 -1
- package/dist/flux.min.js +1 -1
- package/package.json +24 -16
- package/scripts/build.js +28 -29
- package/src/bundler.js +0 -216
- package/src/checker.js +0 -322
- package/src/codegen.js +0 -832
- package/src/css-preprocessor.js +0 -399
- package/src/jsx.js +0 -480
- package/src/lexer.js +0 -518
- package/src/linter.js +0 -784
- package/src/mangler.js +0 -280
- package/src/parser.js +0 -1708
- package/src/sourcemap.js +0 -82
- package/src/test-runner.js +0 -239
- package/src/transpiler.js +0 -172
- package/src/type-checker.js +0 -1206
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 };
|