thyscript 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.
Files changed (49) hide show
  1. package/bin/thy-node.js +332 -0
  2. package/bin/thy.js +828 -0
  3. package/bin/thys.js +25 -0
  4. package/examples/calculator.thy +274 -0
  5. package/examples/calculator.thy.js +628 -0
  6. package/examples/hello.thy +34 -0
  7. package/examples/login.thy +382 -0
  8. package/examples/login.thy.js +710 -0
  9. package/package.json +54 -0
  10. package/scripts/_class.js +18 -0
  11. package/scripts/_class2.js +11 -0
  12. package/scripts/_class3.js +21 -0
  13. package/scripts/_dstruct.js +20 -0
  14. package/scripts/_dstruct2.js +24 -0
  15. package/scripts/_scaffold.js +16 -0
  16. package/scripts/_scaffold2.js +16 -0
  17. package/scripts/_scaffold3.js +26 -0
  18. package/scripts/_tpl_compile.js +13 -0
  19. package/scripts/_tpl_test.js +9 -0
  20. package/scripts/_typeof.js +26 -0
  21. package/scripts/_typeof2.js +25 -0
  22. package/scripts/patch_genmethod.js +44 -0
  23. package/scripts/smoke_test.js +167 -0
  24. package/scripts/smoke_test2.js +151 -0
  25. package/scripts/test_control_panel.js +58 -0
  26. package/src/compiler/CodeGenerator.js +1449 -0
  27. package/src/compiler/JITCompiler.js +325 -0
  28. package/src/database/DatabaseEngine.js +931 -0
  29. package/src/errors/ErrorDetector.js +305 -0
  30. package/src/errors/ErrorMessages.js +497 -0
  31. package/src/index.js +169 -0
  32. package/src/lexer/Lexer.js +342 -0
  33. package/src/lexer/TokenTypes.js +1234 -0
  34. package/src/memory/MemoryManager.js +283 -0
  35. package/src/parser/AST.js +606 -0
  36. package/src/parser/Parser.js +1942 -0
  37. package/src/runtime/ThyRuntime.js +265 -0
  38. package/src/runtime/stdlib/ArrayLib.js +207 -0
  39. package/src/runtime/stdlib/FileLib.js +168 -0
  40. package/src/runtime/stdlib/MathLib.js +411 -0
  41. package/src/runtime/stdlib/StringLib.js +387 -0
  42. package/src/server/ControlPanel.js +1188 -0
  43. package/src/server/HTMLCSSEngine.js +866 -0
  44. package/src/server/SecurityEngine.js +714 -0
  45. package/src/server/ServerEngine.js +738 -0
  46. package/tests/test1_math.thy +82 -0
  47. package/tests/test2_oop.thy +133 -0
  48. package/tests/test3_server.thy +143 -0
  49. package/tests/test4_functions.thy +159 -0
package/src/index.js ADDED
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+ /**
3
+ * ThyScript — Programming Language
4
+ * A blazing-fast programming language built on Node.js
5
+ * for machine learning, physics simulation, web, game engines,
6
+ * full-stack applications, and complex algorithms.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { performance } = require('perf_hooks');
12
+
13
+ const Lexer = require('./lexer/Lexer');
14
+ const TokenType = require('./lexer/TokenTypes');
15
+ const Parser = require('./parser/Parser');
16
+ const AST = require('./parser/AST');
17
+ const JITCompiler = require('./compiler/JITCompiler');
18
+ const CodeGenerator = require('./compiler/CodeGenerator');
19
+ const ThyRuntime = require('./runtime/ThyRuntime');
20
+ const ErrorDetector = require('./errors/ErrorDetector');
21
+ const ErrorMessages = require('./errors/ErrorMessages');
22
+ const MemoryManager = require('./memory/MemoryManager');
23
+ const ServerEngine = require('./server/ServerEngine');
24
+ const SecurityEngine = require('./server/SecurityEngine');
25
+ const HTMLCSSEngine = require('./server/HTMLCSSEngine');
26
+ const DatabaseEngine = require('./database/DatabaseEngine');
27
+
28
+ // Standard library modules
29
+ const MathLib = require('./runtime/stdlib/MathLib');
30
+ const StringLib = require('./runtime/stdlib/StringLib');
31
+ const ArrayLib = require('./runtime/stdlib/ArrayLib');
32
+ const FileLib = require('./runtime/stdlib/FileLib');
33
+
34
+ const VERSION = '1.0.0';
35
+
36
+ /**
37
+ * Compile ThyScript source code to optimized Node.js JavaScript
38
+ * @param {string} source - ThyScript source code
39
+ * @param {string} [filename='<anonymous>'] - Source file name
40
+ * @param {Object} [options={}] - Compiler options
41
+ * @returns {string} Compiled JavaScript code
42
+ */
43
+ function compile(source, filename = '<anonymous>', options = {}) {
44
+ // 1. Lexical analysis
45
+ const lexer = new Lexer(source, filename);
46
+ const tokens = lexer.tokenize();
47
+
48
+ // 2. Syntax analysis (Parsing)
49
+ const parser = new Parser(tokens, source, filename);
50
+ const parseResult = parser.parse();
51
+ const ast = parseResult.ast || parseResult;
52
+
53
+ // 3. Optional Error & Bug Detection
54
+ if (options.checkErrors !== false) {
55
+ const detector = new ErrorDetector(options);
56
+ const scanResult = detector.scan(tokens, ast, source, filename);
57
+ if (!scanResult.isValid && options.strict) {
58
+ const firstErr = scanResult.errors[0];
59
+ throw new Error(`[ThyScript Compiler Error] ${detector.formatError(firstErr, source)}`);
60
+ }
61
+ }
62
+
63
+ // 4. JIT Code Generation
64
+ const compiler = new JITCompiler(ast, source, filename, options);
65
+ return compiler.compile();
66
+ }
67
+
68
+ /**
69
+ * Run a ThyScript file directly (.thy)
70
+ * @param {string} filePath - Path to .thy file
71
+ * @param {Object} [options={}] - Execution options
72
+ * @returns {Promise<any>} Execution result
73
+ */
74
+ async function run(filePath, options = {}) {
75
+ const resolvedPath = path.resolve(filePath);
76
+ if (!fs.existsSync(resolvedPath)) {
77
+ throw new Error(`[ThyScript] File not found: ${resolvedPath}`);
78
+ }
79
+
80
+ const source = fs.readFileSync(resolvedPath, 'utf8');
81
+
82
+ // Initialize runtime
83
+ const runtime = new ThyRuntime(options);
84
+ await runtime.init();
85
+
86
+ // Lex & Parse
87
+ const t0 = performance.now();
88
+ const lexer = new Lexer(source, resolvedPath);
89
+ const tokens = lexer.tokenize();
90
+
91
+ const parser = new Parser(tokens, source, resolvedPath);
92
+ const parseResult = parser.parse();
93
+ const ast = parseResult.ast || parseResult;
94
+
95
+ // Compile
96
+ const compiler = new JITCompiler(ast, source, resolvedPath, options);
97
+ const compiledJs = compiler.compile();
98
+
99
+ // Execute in sandbox with runtime context
100
+ const context = runtime.buildContext(options.context || {});
101
+ const result = await compiler.execute(compiledJs, context);
102
+
103
+ return {
104
+ result,
105
+ stats: compiler.getStats(),
106
+ compiledCode: compiledJs,
107
+ runtime,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Execute ThyScript source string directly
113
+ * @param {string} source - ThyScript code
114
+ * @param {Object} [options={}] - Execution options
115
+ * @returns {Promise<any>} Execution result
116
+ */
117
+ async function runSource(source, options = {}) {
118
+ const runtime = new ThyRuntime(options);
119
+ await runtime.init();
120
+
121
+ const lexer = new Lexer(source, options.filename || '<anonymous>');
122
+ const tokens = lexer.tokenize();
123
+
124
+ const parser = new Parser(tokens, source, options.filename || '<anonymous>');
125
+ const parseResult = parser.parse();
126
+ const ast = parseResult.ast || parseResult;
127
+
128
+ const compiler = new JITCompiler(ast, source, options.filename || '<anonymous>', options);
129
+ const compiledJs = compiler.compile();
130
+
131
+ const context = runtime.buildContext(options.context || {});
132
+ const result = await compiler.execute(compiledJs, context);
133
+
134
+ return {
135
+ result,
136
+ stats: compiler.getStats(),
137
+ compiledCode: compiledJs,
138
+ };
139
+ }
140
+
141
+ module.exports = {
142
+ VERSION,
143
+ // Core Compiler Pipeline
144
+ Lexer,
145
+ TokenType,
146
+ Parser,
147
+ AST,
148
+ JITCompiler,
149
+ CodeGenerator,
150
+ // Runtime & Engines
151
+ ThyRuntime,
152
+ MemoryManager,
153
+ ServerEngine,
154
+ SecurityEngine,
155
+ HTMLCSSEngine,
156
+ DatabaseEngine,
157
+ // Error System
158
+ ErrorDetector,
159
+ ErrorMessages,
160
+ // Standard Libraries
161
+ MathLib,
162
+ StringLib,
163
+ ArrayLib,
164
+ FileLib,
165
+ // High-level API
166
+ compile,
167
+ run,
168
+ runSource,
169
+ };
@@ -0,0 +1,342 @@
1
+ 'use strict';
2
+ /**
3
+ * ThyScript Lexer
4
+ * Tokenizes ThyScript source code into a stream of tokens
5
+ * Supports all ThyScript syntax including HTML blocks, math ops, and keywords
6
+ */
7
+
8
+ const TokenType = require('./TokenTypes');
9
+
10
+ class Lexer {
11
+ constructor(source, filename = '<anonymous>') {
12
+ this.source = source;
13
+ this.filename = filename;
14
+ this.pos = 0;
15
+ this.line = 1;
16
+ this.col = 1;
17
+ this.tokens = [];
18
+ this.errors = [];
19
+ }
20
+
21
+ error(msg) {
22
+ const err = { message: msg, line: this.line, col: this.col, filename: this.filename };
23
+ this.errors.push(err);
24
+ throw new Error(`[ThyScript Lexer] ${this.filename}:${this.line}:${this.col} - ${msg}`);
25
+ }
26
+
27
+ peek(offset = 0) { return this.source[this.pos + offset]; }
28
+ advance() {
29
+ const ch = this.source[this.pos++];
30
+ if (ch === '\n') { this.line++; this.col = 1; } else { this.col++; }
31
+ return ch;
32
+ }
33
+ isEOF() { return this.pos >= this.source.length; }
34
+
35
+ skipWhitespaceAndComments() {
36
+ while (!this.isEOF()) {
37
+ const ch = this.peek();
38
+ // Skip whitespace (not newlines - they may be significant)
39
+ if (ch === ' ' || ch === '\r' || ch === '\t') { this.advance(); continue; }
40
+ // Single line comment //
41
+ if (ch === '/' && this.peek(1) === '/') {
42
+ while (!this.isEOF() && this.peek() !== '\n') this.advance();
43
+ continue;
44
+ }
45
+ // Block comment /* */
46
+ if (ch === '/' && this.peek(1) === '*') {
47
+ this.advance(); this.advance();
48
+ while (!this.isEOF()) {
49
+ if (this.peek() === '*' && this.peek(1) === '/') { this.advance(); this.advance(); break; }
50
+ this.advance();
51
+ }
52
+ continue;
53
+ }
54
+ break;
55
+ }
56
+ }
57
+
58
+ readString(quote) {
59
+ this.advance(); // consume opening quote
60
+ let str = '';
61
+ while (!this.isEOF() && this.peek() !== quote) {
62
+ if (this.peek() === '\\') {
63
+ this.advance();
64
+ const esc = this.advance();
65
+ switch (esc) {
66
+ case 'n': str += '\n'; break;
67
+ case 't': str += '\t'; break;
68
+ case 'r': str += '\r'; break;
69
+ case '\\': str += '\\'; break;
70
+ case '"': str += '"'; break;
71
+ case "'": str += "'"; break;
72
+ case '`': str += '`'; break;
73
+ default: str += '\\' + esc;
74
+ }
75
+ } else {
76
+ str += this.advance();
77
+ }
78
+ }
79
+ if (this.isEOF()) this.error('Unterminated string literal');
80
+ this.advance(); // consume closing quote
81
+ return str;
82
+ }
83
+
84
+ readTemplateLiteral() {
85
+ this.advance(); // consume `
86
+ let parts = [];
87
+ let current = '';
88
+ while (!this.isEOF() && this.peek() !== '`') {
89
+ if (this.peek() === '$' && this.peek(1) === '{') {
90
+ if (current) parts.push({ type: 'str', value: current });
91
+ current = '';
92
+ this.advance(); this.advance(); // consume ${
93
+ let depth = 1;
94
+ let expr = '';
95
+ while (!this.isEOF() && depth > 0) {
96
+ if (this.peek() === '{') depth++;
97
+ if (this.peek() === '}') { depth--; if (depth === 0) break; }
98
+ expr += this.advance();
99
+ }
100
+ this.advance(); // consume }
101
+ parts.push({ type: 'expr', value: expr });
102
+ } else {
103
+ current += this.advance();
104
+ }
105
+ }
106
+ if (this.isEOF()) this.error('Unterminated template literal');
107
+ this.advance(); // consume `
108
+ if (current) parts.push({ type: 'str', value: current });
109
+ return parts;
110
+ }
111
+
112
+ readNumber() {
113
+ let num = '';
114
+ let isFloat = false;
115
+ let isBinary = false;
116
+ let isHex = false;
117
+ let isOctal = false;
118
+
119
+ if (this.peek() === '0') {
120
+ if (this.peek(1) === 'x' || this.peek(1) === 'X') {
121
+ num += this.advance() + this.advance();
122
+ isHex = true;
123
+ while (!this.isEOF() && /[0-9a-fA-F_]/.test(this.peek())) num += this.advance();
124
+ } else if (this.peek(1) === 'b' || this.peek(1) === 'B') {
125
+ num += this.advance() + this.advance();
126
+ isBinary = true;
127
+ while (!this.isEOF() && /[01_]/.test(this.peek())) num += this.advance();
128
+ } else if (this.peek(1) === 'o' || this.peek(1) === 'O') {
129
+ num += this.advance() + this.advance();
130
+ isOctal = true;
131
+ while (!this.isEOF() && /[0-7_]/.test(this.peek())) num += this.advance();
132
+ } else {
133
+ num += this.advance();
134
+ }
135
+ }
136
+
137
+ if (!isHex && !isBinary && !isOctal) {
138
+ while (!this.isEOF() && /[0-9_]/.test(this.peek())) num += this.advance();
139
+ if (!this.isEOF() && this.peek() === '.' && /[0-9]/.test(this.peek(1))) {
140
+ isFloat = true;
141
+ num += this.advance();
142
+ while (!this.isEOF() && /[0-9_]/.test(this.peek())) num += this.advance();
143
+ }
144
+ if (!this.isEOF() && (this.peek() === 'e' || this.peek() === 'E')) {
145
+ isFloat = true;
146
+ num += this.advance();
147
+ if (!this.isEOF() && (this.peek() === '+' || this.peek() === '-')) num += this.advance();
148
+ while (!this.isEOF() && /[0-9]/.test(this.peek())) num += this.advance();
149
+ }
150
+ }
151
+
152
+ const cleanNum = num.replace(/_/g, '');
153
+ return { raw: num, value: isFloat ? parseFloat(cleanNum) : parseInt(cleanNum), isFloat };
154
+ }
155
+
156
+ readIdentifierOrKeyword() {
157
+ let id = '';
158
+ while (!this.isEOF() && /[a-zA-Z0-9_$]/.test(this.peek())) id += this.advance();
159
+ const kw = TokenType.KEYWORDS[id];
160
+ return kw ? { type: kw, value: id } : { type: TokenType.IDENTIFIER, value: id };
161
+ }
162
+
163
+ readHtmlBlock() {
164
+ // Reads everything after classHtml(name): until end()
165
+ let html = '';
166
+ // Skip until next line
167
+ while (!this.isEOF() && this.peek() !== '\n') this.advance();
168
+ if (!this.isEOF() && this.peek() === '\n') {
169
+ this.advance(); // consume newline
170
+ }
171
+
172
+ while (!this.isEOF()) {
173
+ // Check if current position/line starts with end() (ignoring indentation)
174
+ let p = this.pos;
175
+ while (p < this.source.length && (this.source[p] === ' ' || this.source[p] === '\t')) {
176
+ p++;
177
+ }
178
+ if (this.source.slice(p, p + 5) === 'end()') {
179
+ // Stop before indentation so end() is tokenized naturally
180
+ break;
181
+ }
182
+ html += this.advance();
183
+ }
184
+ return html.trim();
185
+ }
186
+
187
+ tokenize() {
188
+ while (!this.isEOF()) {
189
+ this.skipWhitespaceAndComments();
190
+ if (this.isEOF()) break;
191
+
192
+ const startLine = this.line;
193
+ const startCol = this.col;
194
+ const ch = this.peek();
195
+
196
+ // Newlines
197
+ if (ch === '\n') {
198
+ this.advance();
199
+ this.tokens.push({ type: TokenType.NEWLINE, value: '\n', line: startLine, col: startCol });
200
+ continue;
201
+ }
202
+
203
+ // Numbers
204
+ if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(this.peek(1)))) {
205
+ const num = this.readNumber();
206
+ this.tokens.push({ type: num.isFloat ? TokenType.FLOAT : TokenType.NUMBER, value: num.value, raw: num.raw, line: startLine, col: startCol });
207
+ continue;
208
+ }
209
+
210
+ // Strings
211
+ if (ch === '"' || ch === "'") {
212
+ const str = this.readString(ch);
213
+ this.tokens.push({ type: TokenType.STRING, value: str, line: startLine, col: startCol });
214
+ continue;
215
+ }
216
+
217
+ // Template literals
218
+ if (ch === '`') {
219
+ const parts = this.readTemplateLiteral();
220
+ this.tokens.push({ type: TokenType.TEMPLATE_LITERAL, value: parts, line: startLine, col: startCol });
221
+ continue;
222
+ }
223
+
224
+ // Identifiers and keywords
225
+ if (/[a-zA-Z_$]/.test(ch)) {
226
+ const tok = this.readIdentifierOrKeyword();
227
+ this.tokens.push({ ...tok, line: startLine, col: startCol });
228
+
229
+ if (tok.type === TokenType.KW_CLASS_HTML) {
230
+ this.skipWhitespaceAndComments();
231
+ if (this.peek() === '(') {
232
+ this.advance(); // consume (
233
+ this.tokens.push({ type: TokenType.LPAREN, value: '(', line: this.line, col: this.col });
234
+ this.skipWhitespaceAndComments();
235
+ let name = '';
236
+ while (!this.isEOF() && this.peek() !== ')') {
237
+ name += this.advance();
238
+ }
239
+ name = name.trim();
240
+ this.tokens.push({ type: TokenType.IDENTIFIER, value: name, line: this.line, col: this.col });
241
+ if (this.peek() === ')') {
242
+ this.advance(); // consume )
243
+ this.tokens.push({ type: TokenType.RPAREN, value: ')', line: this.line, col: this.col });
244
+ }
245
+ this.skipWhitespaceAndComments();
246
+ if (this.peek() === ':') {
247
+ this.advance(); // consume :
248
+ this.tokens.push({ type: TokenType.COLON, value: ':', line: this.line, col: this.col });
249
+ const html = this.readHtmlBlock();
250
+ this.tokens.push({ type: TokenType.STRING, value: html, line: this.line, col: this.col });
251
+ }
252
+ }
253
+ }
254
+ continue;
255
+ }
256
+
257
+ // Operators and punctuation
258
+ const op = this.readOperator();
259
+ if (op) {
260
+ this.tokens.push({ ...op, line: startLine, col: startCol });
261
+ continue;
262
+ }
263
+
264
+ this.error(`Unknown character '${ch}' (code ${ch.charCodeAt(0)})`);
265
+ }
266
+
267
+ this.tokens.push({ type: TokenType.EOF, value: null, line: this.line, col: this.col });
268
+ return this.tokens;
269
+ }
270
+
271
+ readOperator() {
272
+ const ch = this.advance();
273
+ const next = this.peek();
274
+
275
+ switch (ch) {
276
+ case '+': if (next === '+') { this.advance(); return { type: TokenType.PLUS_PLUS, value: '++' }; }
277
+ if (next === '=') { this.advance(); return { type: TokenType.PLUS_ASSIGN, value: '+=' }; }
278
+ return { type: TokenType.PLUS, value: '+' };
279
+ case '-': if (next === '-') { this.advance(); return { type: TokenType.MINUS_MINUS, value: '--' }; }
280
+ if (next === '=') { this.advance(); return { type: TokenType.MINUS_ASSIGN, value: '-=' }; }
281
+ if (next === '>') { this.advance(); return { type: TokenType.ARROW, value: '->' }; }
282
+ return { type: TokenType.MINUS, value: '-' };
283
+ case '*': if (next === '*') { this.advance(); return { type: TokenType.POWER, value: '**' }; }
284
+ if (next === '=') { this.advance(); return { type: TokenType.MUL_ASSIGN, value: '*=' }; }
285
+ return { type: TokenType.STAR, value: '*' };
286
+ case '/': if (next === '=') { this.advance(); return { type: TokenType.DIV_ASSIGN, value: '/=' }; }
287
+ return { type: TokenType.SLASH, value: '/' };
288
+ case '%': if (next === '=') { this.advance(); return { type: TokenType.MOD_ASSIGN, value: '%=' }; }
289
+ return { type: TokenType.PERCENT, value: '%' };
290
+ case '^': return { type: TokenType.CARET, value: '^' };
291
+ case '&': if (next === '&') { this.advance(); return { type: TokenType.AND_AND, value: '&&' }; }
292
+ return { type: TokenType.AMPERSAND, value: '&' };
293
+ case '|': if (next === '|') { this.advance(); return { type: TokenType.OR_OR, value: '||' }; }
294
+ return { type: TokenType.PIPE, value: '|' };
295
+ case '!': if (next === '=') {
296
+ this.advance();
297
+ if (this.peek() === '=') { this.advance(); return { type: TokenType.NOT_EQ_STRICT, value: '!==' }; }
298
+ return { type: TokenType.NOT_EQ, value: '!=' };
299
+ }
300
+ return { type: TokenType.NOT, value: '!' };
301
+ case '=': if (next === '=') {
302
+ this.advance();
303
+ if (this.peek() === '=') { this.advance(); return { type: TokenType.EQ_STRICT, value: '===' }; }
304
+ return { type: TokenType.EQ_EQ, value: '==' };
305
+ }
306
+ if (next === '>') { this.advance(); return { type: TokenType.FAT_ARROW, value: '=>' }; }
307
+ return { type: TokenType.ASSIGN, value: '=' };
308
+ case '<': if (next === '=') { this.advance(); return { type: TokenType.LT_EQ, value: '<=' }; }
309
+ if (next === '<') { this.advance(); return { type: TokenType.SHIFT_LEFT, value: '<<' }; }
310
+ return { type: TokenType.LT, value: '<' };
311
+ case '>': if (next === '=') { this.advance(); return { type: TokenType.GT_EQ, value: '>=' }; }
312
+ if (next === '>') { this.advance(); return { type: TokenType.SHIFT_RIGHT, value: '>>' }; }
313
+ return { type: TokenType.GT, value: '>' };
314
+ case '(': return { type: TokenType.LPAREN, value: '(' };
315
+ case ')': return { type: TokenType.RPAREN, value: ')' };
316
+ case '{': return { type: TokenType.LBRACE, value: '{' };
317
+ case '}': return { type: TokenType.RBRACE, value: '}' };
318
+ case '[': return { type: TokenType.LBRACKET, value: '[' };
319
+ case ']': return { type: TokenType.RBRACKET, value: ']' };
320
+ case '.': if (next === '.' && this.peek(1) === '.') { this.advance(); this.advance(); return { type: TokenType.SPREAD, value: '...' }; }
321
+ return { type: TokenType.DOT, value: '.' };
322
+ case ',': return { type: TokenType.COMMA, value: ',' };
323
+ case ':': return { type: TokenType.COLON, value: ':' };
324
+ case ';': return { type: TokenType.SEMICOLON, value: ';' };
325
+ case '#': {
326
+ let comment = '';
327
+ while (!this.isEOF() && this.peek() !== '\n') comment += this.advance();
328
+ return { type: TokenType.COMMENT, value: comment };
329
+ }
330
+ case '@': return { type: TokenType.AT, value: '@' };
331
+ case '$': return { type: TokenType.DOLLAR, value: '$' };
332
+ case '?': if (next === '?') { this.advance(); return { type: TokenType.NULL_COALESCE, value: '??' }; }
333
+ if (next === '.') { this.advance(); return { type: TokenType.OPTIONAL_CHAIN, value: '?.' }; }
334
+ return { type: TokenType.QUESTION, value: '?' };
335
+ case '\\': return { type: TokenType.BACKSLASH, value: '\\' };
336
+ case '~': return { type: TokenType.TILDE, value: '~' };
337
+ default: return null;
338
+ }
339
+ }
340
+ }
341
+
342
+ module.exports = Lexer;