starlight-cli 1.0.21 → 1.0.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-cli",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Starlight Programming Language CLI",
5
5
  "bin": {
6
6
  "starlight": "index.js"
package/src/evaluator.js CHANGED
@@ -4,7 +4,9 @@ const Lexer = require('./lexer');
4
4
  const Parser = require('./parser');
5
5
  const path = require('path');
6
6
 
7
- class ReturnValue { constructor(value) { this.value = value; } }
7
+ class ReturnValue {
8
+ constructor(value) { this.value = value; }
9
+ }
8
10
  class BreakSignal {}
9
11
  class ContinueSignal {}
10
12
 
@@ -47,27 +49,37 @@ class Evaluator {
47
49
 
48
50
  setupBuiltins() {
49
51
  this.global.define('len', arg => {
50
- if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
51
- if (arg && typeof arg === 'object') return Object.keys(arg).length;
52
- return 0;
52
+ if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
53
+ if (arg && typeof arg === 'object') return Object.keys(arg).length;
54
+ return 0;
53
55
  });
54
56
 
55
57
  this.global.define('print', arg => { console.log(arg); return null; });
56
58
  this.global.define('type', arg => {
57
- if (Array.isArray(arg)) return 'array';
58
- return typeof arg;
59
+ if (Array.isArray(arg)) return 'array';
60
+ return typeof arg;
59
61
  });
60
62
  this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
61
63
  this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
62
64
 
63
- this.global.define('ask', prompt => readlineSync.question(prompt + ' '));
64
- this.global.define('num', arg => {
65
- const n = Number(arg);
66
- if (Number.isNaN(n)) throw new Error('Cannot convert value to number');
67
- return n;
65
+ this.global.define('ask', prompt => {
66
+ const readlineSync = require('readline-sync');
67
+ return readlineSync.question(prompt + ' ');
68
68
  });
69
- this.global.define('str', arg => String(arg));
70
- }
69
+ this.global.define('num', arg => {
70
+ const n = Number(arg);
71
+ if (Number.isNaN(n)) {
72
+ throw new Error('Cannot convert value to number');
73
+ }
74
+ return n;
75
+ });
76
+
77
+ this.global.define('str', arg => {
78
+ return String(arg);
79
+ });
80
+
81
+ }
82
+
71
83
 
72
84
  evaluate(node, env = this.global) {
73
85
  switch (node.type) {
@@ -84,7 +96,6 @@ class Evaluator {
84
96
  case 'LogicalExpression': return this.evalLogical(node, env);
85
97
  case 'UnaryExpression': return this.evalUnary(node, env);
86
98
  case 'Literal': return node.value;
87
- case 'TemplateLiteral': return this.evalTemplateLiteral(node, env);
88
99
  case 'Identifier': return env.get(node.name);
89
100
  case 'IfStatement': return this.evalIf(node, env);
90
101
  case 'WhileStatement': return this.evalWhile(node, env);
@@ -110,65 +121,72 @@ class Evaluator {
110
121
 
111
122
  evalProgram(node, env) {
112
123
  let result = null;
113
- for (const stmt of node.body) result = this.evaluate(stmt, env);
124
+ for (const stmt of node.body) {
125
+ result = this.evaluate(stmt, env);
126
+ }
114
127
  return result;
115
128
  }
129
+ evalImport(node, env) {
130
+ const spec = node.path;
131
+ let lib;
116
132
 
117
- evalTemplateLiteral(node, env) {
118
- // node.parts is an array from parser: Literal or expression nodes
119
- let result = '';
120
- for (const part of node.parts) {
121
- if (part.type === 'Literal') {
122
- result += part.value;
123
- } else {
124
- const val = this.evaluate(part, env);
125
- result += val != null ? val.toString() : '';
133
+ try {
134
+ const resolved = require.resolve(spec, {
135
+ paths: [process.cwd()]
136
+ });
137
+ lib = require(resolved);
138
+ } catch (e) {
139
+ const fullPath = path.isAbsolute(spec)
140
+ ? spec
141
+ : path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
142
+
143
+ if (!fs.existsSync(fullPath)) {
144
+ throw new Error(`Import not found: ${spec}`);
126
145
  }
127
- }
128
- return result;
129
- }
130
146
 
147
+ const code = fs.readFileSync(fullPath, 'utf-8');
148
+ const tokens = new Lexer(code).getTokens();
149
+ const ast = new Parser(tokens).parse();
150
+
151
+ const moduleEnv = new Environment(env);
152
+ this.evaluate(ast, moduleEnv);
131
153
 
132
- evalImport(node, env) {
133
- const spec = node.path;
134
- let lib;
135
- try {
136
- const resolved = require.resolve(spec, { paths: [process.cwd()] });
137
- lib = require(resolved);
138
- } catch (e) {
139
- const fullPath = path.isAbsolute(spec)
140
- ? spec
141
- : path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
142
-
143
- if (!fs.existsSync(fullPath)) throw new Error(`Import not found: ${spec}`);
144
-
145
- const code = fs.readFileSync(fullPath, 'utf-8');
146
- const tokens = new Lexer(code).getTokens();
147
- const ast = new Parser(tokens).parse();
148
- const moduleEnv = new Environment(env);
149
- this.evaluate(ast, moduleEnv);
150
-
151
- lib = {};
152
- for (const key of Object.keys(moduleEnv.store)) lib[key] = moduleEnv.store[key];
153
- lib.default = lib;
154
+ lib = {};
155
+ for (const key of Object.keys(moduleEnv.store)) {
156
+ lib[key] = moduleEnv.store[key];
154
157
  }
155
158
 
156
- for (const imp of node.specifiers) {
157
- if (imp.type === 'DefaultImport') env.define(imp.local, lib.default ?? lib);
158
- if (imp.type === 'NamespaceImport') env.define(imp.local, lib);
159
- if (imp.type === 'NamedImport') {
160
- if (!(imp.imported in lib)) throw new Error(`Module '${spec}' has no export '${imp.imported}'`);
161
- env.define(imp.local, lib[imp.imported]);
159
+ lib.default = lib;
160
+ }
161
+
162
+ for (const imp of node.specifiers) {
163
+ if (imp.type === 'DefaultImport') {
164
+ env.define(imp.local, lib.default ?? lib);
165
+ }
166
+ if (imp.type === 'NamespaceImport') {
167
+ env.define(imp.local, lib);
168
+ }
169
+ if (imp.type === 'NamedImport') {
170
+ if (!(imp.imported in lib)) {
171
+ throw new Error(`Module '${spec}' has no export '${imp.imported}'`);
162
172
  }
173
+ env.define(imp.local, lib[imp.imported]);
163
174
  }
164
- return null;
165
175
  }
166
176
 
177
+ return null;
178
+ }
179
+
180
+
167
181
  evalBlock(node, env) {
168
182
  let result = null;
169
183
  for (const stmt of node.body) {
170
- try { result = this.evaluate(stmt, env); }
171
- catch (e) { if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e; else throw e; }
184
+ try {
185
+ result = this.evaluate(stmt, env);
186
+ } catch (e) {
187
+ if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
188
+ throw e;
189
+ }
172
190
  }
173
191
  return result;
174
192
  }
@@ -181,15 +199,27 @@ class Evaluator {
181
199
  evalAssignment(node, env) {
182
200
  const rightVal = this.evaluate(node.right, env);
183
201
  const left = node.left;
202
+
184
203
  if (left.type === 'Identifier') return env.set(left.name, rightVal);
185
- if (left.type === 'MemberExpression') { const obj = this.evaluate(left.object, env); obj[left.property] = rightVal; return rightVal; }
186
- if (left.type === 'IndexExpression') { const obj = this.evaluate(left.object, env); const idx = this.evaluate(left.indexer, env); obj[idx] = rightVal; return rightVal; }
204
+ if (left.type === 'MemberExpression') {
205
+ const obj = this.evaluate(left.object, env);
206
+ obj[left.property] = rightVal;
207
+ return rightVal;
208
+ }
209
+ if (left.type === 'IndexExpression') {
210
+ const obj = this.evaluate(left.object, env);
211
+ const idx = this.evaluate(left.indexer, env);
212
+ obj[idx] = rightVal;
213
+ return rightVal;
214
+ }
215
+
187
216
  throw new Error('Invalid assignment target');
188
217
  }
189
218
 
190
219
  evalCompoundAssignment(node, env) {
191
220
  const left = node.left;
192
221
  let current;
222
+
193
223
  if (left.type === 'Identifier') current = env.get(left.name);
194
224
  else if (left.type === 'MemberExpression') current = this.evalMember(left, env);
195
225
  else if (left.type === 'IndexExpression') current = this.evalIndex(left, env);
@@ -207,7 +237,9 @@ class Evaluator {
207
237
  }
208
238
 
209
239
  if (left.type === 'Identifier') env.set(left.name, computed);
240
+ else if (left.type === 'MemberExpression') this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
210
241
  else this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
242
+
211
243
  return computed;
212
244
  }
213
245
 
@@ -219,7 +251,8 @@ class Evaluator {
219
251
 
220
252
  evalAsk(node, env) {
221
253
  const prompt = this.evaluate(node.prompt, env);
222
- return readlineSync.question(prompt + ' ');
254
+ const input = readlineSync.question(prompt + ' ');
255
+ return input;
223
256
  }
224
257
 
225
258
  evalDefine(node, env) {
@@ -230,25 +263,27 @@ class Evaluator {
230
263
  evalBinary(node, env) {
231
264
  const l = this.evaluate(node.left, env);
232
265
  const r = this.evaluate(node.right, env);
266
+
267
+ if (node.operator === 'SLASH' && r === 0) {
268
+ throw new Error('Division by zero');
269
+ }
270
+
233
271
  switch (node.operator) {
234
- case 'PLUS': return l + r;
235
- case 'MINUS': return l - r;
236
- case 'STAR': return l * r;
237
- case 'SLASH': return l / r;
238
- case 'MOD': return l % r;
239
- case 'EQEQ': return l == r;
240
- case 'NOTEQ': return l != r;
241
- case 'STRICT_EQ': return l === r;
242
- case 'STRICT_NOTEQ': return l !== r;
243
- case 'LT': return l < r;
244
- case 'LTE': return l <= r;
245
- case 'GT': return l > r;
246
- case 'GTE': return l >= r;
247
- case 'LSHIFT': return l << r;
248
- case 'RSHIFT': return l >> r;
249
- default: throw new Error(`Unknown binary operator ${node.operator}`);
272
+ case 'PLUS': return l + r;
273
+ case 'MINUS': return l - r;
274
+ case 'STAR': return l * r;
275
+ case 'SLASH': return l / r;
276
+ case 'MOD': return l % r;
277
+ case 'EQEQ': return l === r;
278
+ case 'NOTEQ': return l !== r;
279
+ case 'LT': return l < r;
280
+ case 'LTE': return l <= r;
281
+ case 'GT': return l > r;
282
+ case 'GTE': return l >= r;
283
+ default: throw new Error(`Unknown binary operator ${node.operator}`);
250
284
  }
251
- }
285
+ }
286
+
252
287
 
253
288
  evalLogical(node, env) {
254
289
  const l = this.evaluate(node.left, env);
@@ -313,7 +348,9 @@ class Evaluator {
313
348
  const args = node.arguments.map(a => this.evaluate(a, env));
314
349
  return calleeEvaluated(...args);
315
350
  }
316
- if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) throw new Error('Call to non-function');
351
+ if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
352
+ throw new Error('Call to non-function');
353
+ }
317
354
  const fn = calleeEvaluated;
318
355
  const callEnv = new Environment(fn.env);
319
356
  fn.params.forEach((p, i) => {
@@ -327,9 +364,18 @@ class Evaluator {
327
364
  evalIndex(node, env) {
328
365
  const obj = this.evaluate(node.object, env);
329
366
  const idx = this.evaluate(node.indexer, env);
330
- if (obj == null) throw new Error('Indexing null/undefined');
367
+
368
+ if (obj == null) throw new Error('Indexing null or undefined');
369
+ if (Array.isArray(obj) && (idx < 0 || idx >= obj.length)) {
370
+ throw new Error('Array index out of bounds');
371
+ }
372
+ if (typeof obj === 'object' && !(idx in obj)) {
373
+ throw new Error(`Property '${idx}' does not exist`);
374
+ }
375
+
331
376
  return obj[idx];
332
- }
377
+ }
378
+
333
379
 
334
380
  evalObject(node, env) {
335
381
  const out = {};
@@ -339,9 +385,11 @@ class Evaluator {
339
385
 
340
386
  evalMember(node, env) {
341
387
  const obj = this.evaluate(node.object, env);
342
- if (obj == null) throw new Error('Member access of null/undefined');
388
+ if (obj == null) throw new Error('Member access of null or undefined');
389
+ if (!(node.property in obj)) throw new Error(`Property '${node.property}' does not exist`);
343
390
  return obj[node.property];
344
- }
391
+ }
392
+
345
393
 
346
394
  evalUpdate(node, env) {
347
395
  const arg = node.argument;
@@ -353,8 +401,15 @@ class Evaluator {
353
401
  };
354
402
  const setValue = (v) => {
355
403
  if (arg.type === 'Identifier') env.set(arg.name, v);
356
- else if (arg.type === 'MemberExpression') { const obj = this.evaluate(arg.object, env); obj[arg.property] = v; }
357
- else if (arg.type === 'IndexExpression') { const obj = this.evaluate(arg.object, env); const idx = this.evaluate(arg.indexer, env); obj[idx] = v; }
404
+ else if (arg.type === 'MemberExpression') {
405
+ const obj = this.evaluate(arg.object, env);
406
+ obj[arg.property] = v;
407
+ }
408
+ else if (arg.type === 'IndexExpression') {
409
+ const obj = this.evaluate(arg.object, env);
410
+ const idx = this.evaluate(arg.indexer, env);
411
+ obj[idx] = v;
412
+ }
358
413
  };
359
414
  const current = getCurrent();
360
415
  const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
@@ -363,4 +418,4 @@ class Evaluator {
363
418
  }
364
419
  }
365
420
 
366
- module.exports = Evaluator;
421
+ module.exports = Evaluator;
package/src/lexer.js CHANGED
@@ -2,10 +2,18 @@ class Lexer {
2
2
  constructor(input) {
3
3
  this.input = input;
4
4
  this.pos = 0;
5
- this.currentChar = input[this.pos];
5
+ this.currentChar = input[0] || null;
6
+ this.line = 1; // current line
7
+ this.column = 1; // current column
6
8
  }
7
9
 
8
10
  advance() {
11
+ if (this.currentChar === '\n') {
12
+ this.line++;
13
+ this.column = 0;
14
+ } else {
15
+ this.column++;
16
+ }
9
17
  this.pos++;
10
18
  this.currentChar = this.pos < this.input.length ? this.input[this.pos] : null;
11
19
  }
@@ -14,152 +22,149 @@ class Lexer {
14
22
  return this.pos + 1 < this.input.length ? this.input[this.pos + 1] : null;
15
23
  }
16
24
 
25
+ error(msg) {
26
+ throw new Error(`${msg} at line ${this.line}, column ${this.column}`);
27
+ }
28
+
17
29
  skipWhitespace() {
18
30
  while (this.currentChar && /\s/.test(this.currentChar)) this.advance();
19
31
  }
20
32
 
21
- matchKeyword(id) {
22
- const keywords = {
23
- 'let': 'LET',
24
- 'define': 'DEFINE',
25
- 'if': 'IF',
26
- 'else': 'ELSE',
27
- 'while': 'WHILE',
28
- 'for': 'FOR',
29
- 'break': 'BREAK',
30
- 'continue': 'CONTINUE',
31
- 'func': 'FUNC',
32
- 'return': 'RETURN',
33
- 'import': 'IMPORT',
34
- 'from': 'FROM',
35
- 'as': 'AS',
36
- 'true': 'TRUE',
37
- 'false': 'FALSE',
38
- 'null': 'NULL',
39
- 'undefined': 'UNDEFINED',
40
- 'ask': 'ASK',
41
- 'sldeploy': 'SLDEPLOY'
42
- };
43
- return keywords[id] || null;
33
+ skipComment() {
34
+ if (this.currentChar === '#') {
35
+ if (this.peek() === '*') {
36
+ this.advance(); this.advance(); // skip #*
37
+ while (this.currentChar !== null) {
38
+ if (this.currentChar === '*' && this.peek() === '#') {
39
+ this.advance(); this.advance();
40
+ return;
41
+ }
42
+ this.advance();
43
+ }
44
+ this.error("Unterminated multi-line comment (#* ... *#)");
45
+ } else {
46
+ while (this.currentChar && this.currentChar !== '\n') this.advance();
47
+ }
48
+ }
44
49
  }
45
50
 
46
51
  number() {
52
+ const startLine = this.line;
53
+ const startCol = this.column;
47
54
  let result = '';
48
- while (this.currentChar && /[0-9.]/.test(this.currentChar)) {
55
+ while (this.currentChar && /[0-9]/.test(this.currentChar)) {
49
56
  result += this.currentChar;
50
57
  this.advance();
51
58
  }
52
- return { type: 'NUMBER', value: parseFloat(result) };
59
+
60
+ if (this.currentChar === '.' && /[0-9]/.test(this.peek())) {
61
+ result += '.'; this.advance();
62
+ while (this.currentChar && /[0-9]/.test(this.currentChar)) {
63
+ result += this.currentChar;
64
+ this.advance();
65
+ }
66
+ return { type: 'NUMBER', value: parseFloat(result), line: startLine, column: startCol };
67
+ }
68
+
69
+ return { type: 'NUMBER', value: parseInt(result), line: startLine, column: startCol };
53
70
  }
54
71
 
55
72
  identifier() {
73
+ const startLine = this.line;
74
+ const startCol = this.column;
56
75
  let result = '';
57
- while (this.currentChar && /[a-zA-Z0-9_]/.test(this.currentChar)) {
76
+ while (this.currentChar && /[A-Za-z0-9_]/.test(this.currentChar)) {
58
77
  result += this.currentChar;
59
78
  this.advance();
60
79
  }
61
- const type = this.matchKeyword(result) || 'IDENTIFIER';
62
- return { type, value: result };
80
+
81
+ const keywords = [
82
+ 'let', 'sldeploy', 'if', 'else', 'while', 'for',
83
+ 'break', 'continue', 'func', 'return', 'true', 'false', 'null',
84
+ 'ask', 'define', 'import', 'from', 'as'
85
+ ];
86
+
87
+ if (keywords.includes(result)) {
88
+ return { type: result.toUpperCase(), value: result, line: startLine, column: startCol };
89
+ }
90
+
91
+ return { type: 'IDENTIFIER', value: result, line: startLine, column: startCol };
63
92
  }
64
93
 
65
- string(quoteType) {
66
- this.advance(); // skip opening quote
94
+ string() {
95
+ const startLine = this.line;
96
+ const startCol = this.column;
97
+ const quote = this.currentChar;
98
+ this.advance();
67
99
  let result = '';
68
- while (this.currentChar && this.currentChar !== quoteType) {
100
+
101
+ while (this.currentChar && this.currentChar !== quote) {
69
102
  if (this.currentChar === '\\') {
70
103
  this.advance();
71
- if (this.currentChar) {
72
- const escapeChars = { n: '\n', r: '\r', t: '\t', '\\': '\\', '"': '"', "'": "'" };
73
- result += escapeChars[this.currentChar] || this.currentChar;
74
- this.advance();
104
+ switch (this.currentChar) {
105
+ case 'n': result += '\n'; break;
106
+ case 't': result += '\t'; break;
107
+ case '"': result += '"'; break;
108
+ case "'": result += "'"; break;
109
+ case '\\': result += '\\'; break;
110
+ default: result += this.currentChar;
75
111
  }
76
112
  } else {
77
113
  result += this.currentChar;
78
- this.advance();
79
114
  }
115
+ this.advance();
80
116
  }
81
- this.advance(); // skip closing quote
82
- return { type: 'STRING', value: result };
83
- }
84
117
 
85
- templateLiteral() {
86
- let result = '';
87
- const tokens = [];
88
- this.advance(); // skip initial backtick
89
- while (this.currentChar !== null) {
90
- if (this.currentChar === '$' && this.peek() === '{') {
91
- if (result) tokens.push({ type: 'TEMPLATE_STRING', value: result });
92
- result = '';
93
- tokens.push({ type: 'DOLLAR_LBRACE' });
94
- this.advance();
95
- this.advance(); // skip "${"
96
- } else if (this.currentChar === '`') {
97
- if (result) tokens.push({ type: 'TEMPLATE_STRING', value: result });
98
- this.advance();
99
- break;
100
- } else {
101
- result += this.currentChar;
102
- this.advance();
103
- }
118
+ if (this.currentChar !== quote) {
119
+ this.error('Unterminated string literal');
104
120
  }
105
- return tokens;
121
+
122
+ this.advance();
123
+ return { type: 'STRING', value: result, line: startLine, column: startCol };
106
124
  }
107
125
 
108
126
  getTokens() {
109
127
  const tokens = [];
128
+
110
129
  while (this.currentChar !== null) {
111
- this.skipWhitespace();
130
+ if (/\s/.test(this.currentChar)) { this.skipWhitespace(); continue; }
131
+ if (this.currentChar === '#') { this.skipComment(); continue; }
132
+ if (/[0-9]/.test(this.currentChar)) { tokens.push(this.number()); continue; }
133
+ if (/[A-Za-z_]/.test(this.currentChar)) { tokens.push(this.identifier()); continue; }
134
+ if (this.currentChar === '"' || this.currentChar === "'") { tokens.push(this.string()); continue; }
112
135
 
113
- if (!this.currentChar) break;
136
+ const char = this.currentChar;
137
+ const next = this.peek();
138
+ const startLine = this.line;
139
+ const startCol = this.column;
114
140
 
115
- if (/[0-9]/.test(this.currentChar)) tokens.push(this.number());
116
- else if (/[a-zA-Z_]/.test(this.currentChar)) tokens.push(this.identifier());
117
- else if (this.currentChar === '"' || this.currentChar === "'") tokens.push(this.string(this.currentChar));
118
- else if (this.currentChar === '`') {
119
- const parts = this.templateLiteral();
120
- tokens.push(...parts);
121
- }
122
- else {
123
- const char = this.currentChar;
124
- switch (char) {
125
- case '+':
126
- if (this.peek() === '+') { tokens.push({ type: 'PLUSPLUS' }); this.advance(); }
127
- else if (this.peek() === '=') { tokens.push({ type: 'PLUSEQ' }); this.advance(); }
128
- else tokens.push({ type: 'PLUS' });
129
- break;
130
- case '-':
131
- if (this.peek() === '-') { tokens.push({ type: 'MINUSMINUS' }); this.advance(); }
132
- else if (this.peek() === '=') { tokens.push({ type: 'MINUSEQ' }); this.advance(); }
133
- else tokens.push({ type: 'MINUS' });
134
- break;
135
- case '*': tokens.push(this.peek() === '=' ? (this.advance(), { type: 'STAREQ' }) : { type: 'STAR' }); break;
136
- case '/': tokens.push(this.peek() === '=' ? (this.advance(), { type: 'SLASHEQ' }) : { type: 'SLASH' }); break;
137
- case '%': tokens.push(this.peek() === '=' ? (this.advance(), { type: 'MODEQ' }) : { type: 'MOD' }); break;
138
- case '=': tokens.push(this.peek() === '=' ? (this.advance(), this.peek() === '=' ? (this.advance(), { type: 'STRICT_EQ' }) : { type: 'EQEQ' }) : { type: 'EQUAL' }); break;
139
- case '!': tokens.push(this.peek() === '=' ? (this.advance(), this.peek() === '=' ? (this.advance(), { type: 'STRICT_NOTEQ' }) : { type: 'NOTEQ' }) : { type: 'NOT' }); break;
140
- case '<': tokens.push(this.peek() === '<' ? (this.advance(), { type: 'LSHIFT' }) : this.peek() === '=' ? (this.advance(), { type: 'LTE' }) : { type: 'LT' }); break;
141
- case '>': tokens.push(this.peek() === '>' ? (this.advance(), { type: 'RSHIFT' }) : this.peek() === '=' ? (this.advance(), { type: 'GTE' }) : { type: 'GT' }); break;
142
- case '&': tokens.push(this.peek() === '&' ? (this.advance(), { type: 'AND' }) : { type: 'AMP' }); break;
143
- case '|': tokens.push(this.peek() === '|' ? (this.advance(), { type: 'OR' }) : { type: 'PIPE' }); break;
144
- case '(': tokens.push({ type: 'LPAREN' }); break;
145
- case ')': tokens.push({ type: 'RPAREN' }); break;
146
- case '{': tokens.push({ type: 'LBRACE' }); break;
147
- case '}': tokens.push({ type: 'RBRACE' }); break;
148
- case '[': tokens.push({ type: 'LBRACKET' }); break;
149
- case ']': tokens.push({ type: 'RBRACKET' }); break;
150
- case ',': tokens.push({ type: 'COMMA' }); break;
151
- case ';': tokens.push({ type: 'SEMICOLON' }); break;
152
- case '.': tokens.push({ type: 'DOT' }); break;
153
- case ':': tokens.push({ type: 'COLON' }); break;
154
- case '*': tokens.push({ type: 'STAR' }); break;
155
- case '$': tokens.push({ type: 'DOLLAR' }); break;
156
- case '?': tokens.push({ type: 'QUESTION' }); break;
157
- default: throw new Error(`Unexpected character: ${char} at position ${this.pos}`);
158
- }
159
- this.advance();
160
- }
141
+ if (char === '=' && next === '=') { tokens.push({ type: 'EQEQ', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
142
+ if (char === '=' && next === '>') { tokens.push({ type: 'ARROW', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
143
+ if (char === '!' && next === '=') { tokens.push({ type: 'NOTEQ', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
144
+ if (char === '<' && next === '=') { tokens.push({ type: 'LTE', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
145
+ if (char === '>' && next === '=') { tokens.push({ type: 'GTE', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
146
+ if (char === '&' && next === '&') { tokens.push({ type: 'AND', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
147
+ if (char === '|' && next === '|') { tokens.push({ type: 'OR', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
148
+ if (char === '+' && next === '+') { tokens.push({ type: 'PLUSPLUS', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
149
+ if (char === '-' && next === '-') { tokens.push({ type: 'MINUSMINUS', line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
150
+
151
+ const map = { '+': 'PLUSEQ', '-': 'MINUSEQ', '*': 'STAREQ', '/': 'SLASHEQ', '%': 'MODEQ' };
152
+ if (next === '=' && map[char]) { tokens.push({ type: map[char], line: startLine, column: startCol }); this.advance(); this.advance(); continue; }
153
+
154
+ const singles = {
155
+ '+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH', '%': 'MOD',
156
+ '=': 'EQUAL', '<': 'LT', '>': 'GT', '!': 'NOT',
157
+ '(': 'LPAREN', ')': 'RPAREN', '{': 'LBRACE', '}': 'RBRACE',
158
+ ';': 'SEMICOLON', ',': 'COMMA', '[': 'LBRACKET', ']': 'RBRACKET',
159
+ ':': 'COLON', '.': 'DOT'
160
+ };
161
+
162
+ if (singles[char]) { tokens.push({ type: singles[char], line: startLine, column: startCol }); this.advance(); continue; }
163
+
164
+ this.error("Unexpected character: " + char);
161
165
  }
162
- tokens.push({ type: 'EOF' });
166
+
167
+ tokens.push({ type: 'EOF', line: this.line, column: this.column });
163
168
  return tokens;
164
169
  }
165
170
  }