starlight-cli 1.0.20 → 1.0.22

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.20",
3
+ "version": "1.0.22",
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) {
@@ -236,16 +269,12 @@ class Evaluator {
236
269
  case 'STAR': return l * r;
237
270
  case 'SLASH': return l / r;
238
271
  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;
272
+ case 'EQEQ': return l === r;
273
+ case 'NOTEQ': return l !== r;
243
274
  case 'LT': return l < r;
244
275
  case 'LTE': return l <= r;
245
276
  case 'GT': return l > r;
246
277
  case 'GTE': return l >= r;
247
- case 'LSHIFT': return l << r;
248
- case 'RSHIFT': return l >> r;
249
278
  default: throw new Error(`Unknown binary operator ${node.operator}`);
250
279
  }
251
280
  }
@@ -313,7 +342,9 @@ class Evaluator {
313
342
  const args = node.arguments.map(a => this.evaluate(a, env));
314
343
  return calleeEvaluated(...args);
315
344
  }
316
- if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) throw new Error('Call to non-function');
345
+ if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
346
+ throw new Error('Call to non-function');
347
+ }
317
348
  const fn = calleeEvaluated;
318
349
  const callEnv = new Environment(fn.env);
319
350
  fn.params.forEach((p, i) => {
@@ -353,8 +384,15 @@ class Evaluator {
353
384
  };
354
385
  const setValue = (v) => {
355
386
  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; }
387
+ else if (arg.type === 'MemberExpression') {
388
+ const obj = this.evaluate(arg.object, env);
389
+ obj[arg.property] = v;
390
+ }
391
+ else if (arg.type === 'IndexExpression') {
392
+ const obj = this.evaluate(arg.object, env);
393
+ const idx = this.evaluate(arg.indexer, env);
394
+ obj[idx] = v;
395
+ }
358
396
  };
359
397
  const current = getCurrent();
360
398
  const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
@@ -363,4 +401,4 @@ class Evaluator {
363
401
  }
364
402
  }
365
403
 
366
- module.exports = Evaluator;
404
+ module.exports = Evaluator;
package/src/lexer.js CHANGED
@@ -3,19 +3,27 @@ class Lexer {
3
3
  this.input = input;
4
4
  this.pos = 0;
5
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
  }
12
20
 
13
- peek(n = 1) {
14
- return this.pos + n < this.input.length ? this.input[this.pos + n] : null;
21
+ peek() {
22
+ return this.pos + 1 < this.input.length ? this.input[this.pos + 1] : null;
15
23
  }
16
24
 
17
25
  error(msg) {
18
- throw new Error(`LEXER ERROR: ${msg} at position ${this.pos}`);
26
+ throw new Error(`${msg} at line ${this.line}, column ${this.column}`);
19
27
  }
20
28
 
21
29
  skipWhitespace() {
@@ -25,7 +33,7 @@ class Lexer {
25
33
  skipComment() {
26
34
  if (this.currentChar === '#') {
27
35
  if (this.peek() === '*') {
28
- this.advance(); this.advance(); // #*
36
+ this.advance(); this.advance(); // skip #*
29
37
  while (this.currentChar !== null) {
30
38
  if (this.currentChar === '*' && this.peek() === '#') {
31
39
  this.advance(); this.advance();
@@ -33,7 +41,7 @@ class Lexer {
33
41
  }
34
42
  this.advance();
35
43
  }
36
- this.error('Unterminated multi-line comment (#* ... *#)');
44
+ this.error("Unterminated multi-line comment (#* ... *#)");
37
45
  } else {
38
46
  while (this.currentChar && this.currentChar !== '\n') this.advance();
39
47
  }
@@ -41,6 +49,8 @@ class Lexer {
41
49
  }
42
50
 
43
51
  number() {
52
+ const startLine = this.line;
53
+ const startCol = this.column;
44
54
  let result = '';
45
55
  while (this.currentChar && /[0-9]/.test(this.currentChar)) {
46
56
  result += this.currentChar;
@@ -48,32 +58,20 @@ class Lexer {
48
58
  }
49
59
 
50
60
  if (this.currentChar === '.' && /[0-9]/.test(this.peek())) {
51
- result += '.';
52
- this.advance();
61
+ result += '.'; this.advance();
53
62
  while (this.currentChar && /[0-9]/.test(this.currentChar)) {
54
63
  result += this.currentChar;
55
64
  this.advance();
56
65
  }
66
+ return { type: 'NUMBER', value: parseFloat(result), line: startLine, column: startCol };
57
67
  }
58
68
 
59
- if (this.currentChar && /[eE]/.test(this.currentChar)) {
60
- result += this.currentChar;
61
- this.advance();
62
- if (this.currentChar === '+' || this.currentChar === '-') {
63
- result += this.currentChar;
64
- this.advance();
65
- }
66
- if (!/[0-9]/.test(this.currentChar)) this.error('Invalid exponent in number');
67
- while (this.currentChar && /[0-9]/.test(this.currentChar)) {
68
- result += this.currentChar;
69
- this.advance();
70
- }
71
- }
72
-
73
- return { type: 'NUMBER', value: parseFloat(result) };
69
+ return { type: 'NUMBER', value: parseInt(result), line: startLine, column: startCol };
74
70
  }
75
71
 
76
72
  identifier() {
73
+ const startLine = this.line;
74
+ const startCol = this.column;
77
75
  let result = '';
78
76
  while (this.currentChar && /[A-Za-z0-9_]/.test(this.currentChar)) {
79
77
  result += this.currentChar;
@@ -82,19 +80,20 @@ class Lexer {
82
80
 
83
81
  const keywords = [
84
82
  'let', 'sldeploy', 'if', 'else', 'while', 'for',
85
- 'break', 'continue', 'func', 'return',
86
- 'true', 'false', 'null', 'undefined',
83
+ 'break', 'continue', 'func', 'return', 'true', 'false', 'null',
87
84
  'ask', 'define', 'import', 'from', 'as'
88
85
  ];
89
86
 
90
87
  if (keywords.includes(result)) {
91
- return { type: result.toUpperCase(), value: result };
88
+ return { type: result.toUpperCase(), value: result, line: startLine, column: startCol };
92
89
  }
93
90
 
94
- return { type: 'IDENTIFIER', value: result };
91
+ return { type: 'IDENTIFIER', value: result, line: startLine, column: startCol };
95
92
  }
96
93
 
97
94
  string() {
95
+ const startLine = this.line;
96
+ const startCol = this.column;
98
97
  const quote = this.currentChar;
99
98
  this.advance();
100
99
  let result = '';
@@ -102,58 +101,26 @@ class Lexer {
102
101
  while (this.currentChar && this.currentChar !== quote) {
103
102
  if (this.currentChar === '\\') {
104
103
  this.advance();
105
- const map = { n: '\n', t: '\t', '"': '"', "'": "'", '\\': '\\' };
106
- result += map[this.currentChar] ?? this.currentChar;
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;
111
+ }
107
112
  } else {
108
113
  result += this.currentChar;
109
114
  }
110
115
  this.advance();
111
116
  }
112
117
 
113
- if (this.currentChar !== quote) this.error('Unterminated string literal');
114
-
115
- this.advance();
116
- return { type: 'STRING', value: result };
117
- }
118
-
119
-
120
- templateString(tokens) {
121
- this.advance(); // skip opening `
122
-
123
- let buffer = '';
124
-
125
- while (this.currentChar !== null) {
126
- if (this.currentChar === '`') {
127
- if (buffer.length > 0) {
128
- tokens.push({ type: 'TEMPLATE_STRING', value: buffer });
129
- }
130
- this.advance();
131
- return;
132
- }
133
-
134
- if (this.currentChar === '$' && this.peek() === '{') {
135
- if (buffer.length > 0) {
136
- tokens.push({ type: 'TEMPLATE_STRING', value: buffer });
137
- buffer = '';
138
- }
139
- this.advance(); // $
140
- this.advance(); // {
141
- tokens.push({ type: 'DOLLAR_LBRACE' });
142
- return;
143
- }
144
-
145
- if (this.currentChar === '\\') {
146
- this.advance();
147
- buffer += this.currentChar;
148
- this.advance();
149
- continue;
150
- }
151
-
152
- buffer += this.currentChar;
153
- this.advance();
118
+ if (this.currentChar !== quote) {
119
+ this.error('Unterminated string literal');
154
120
  }
155
121
 
156
- this.error('Unterminated template string');
122
+ this.advance();
123
+ return { type: 'STRING', value: result, line: startLine, column: startCol };
157
124
  }
158
125
 
159
126
  getTokens() {
@@ -166,56 +133,38 @@ class Lexer {
166
133
  if (/[A-Za-z_]/.test(this.currentChar)) { tokens.push(this.identifier()); continue; }
167
134
  if (this.currentChar === '"' || this.currentChar === "'") { tokens.push(this.string()); continue; }
168
135
 
169
- if (this.currentChar === '`') {
170
- this.templateString(tokens);
171
- continue;
172
- }
173
-
174
136
  const char = this.currentChar;
175
137
  const next = this.peek();
176
- const next2 = this.peek(2);
177
-
178
- if (char === '=' && next === '=' && next2 === '=') { tokens.push({ type: 'STRICT_EQ' }); this.advance(); this.advance(); this.advance(); continue; }
179
- if (char === '!' && next === '=' && next2 === '=') { tokens.push({ type: 'STRICT_NOTEQ' }); this.advance(); this.advance(); this.advance(); continue; }
180
- if (char === '=' && next === '=') { tokens.push({ type: 'EQEQ' }); this.advance(); this.advance(); continue; }
181
- if (char === '=' && next === '>') { tokens.push({ type: 'ARROW' }); this.advance(); this.advance(); continue; }
182
- if (char === '!' && next === '=') { tokens.push({ type: 'NOTEQ' }); this.advance(); this.advance(); continue; }
183
- if (char === '<' && next === '=') { tokens.push({ type: 'LTE' }); this.advance(); this.advance(); continue; }
184
- if (char === '>' && next === '=') { tokens.push({ type: 'GTE' }); this.advance(); this.advance(); continue; }
185
- if (char === '<' && next === '<') { tokens.push({ type: 'LSHIFT' }); this.advance(); this.advance(); continue; }
186
- if (char === '>' && next === '>') { tokens.push({ type: 'RSHIFT' }); this.advance(); this.advance(); continue; }
187
- if (char === '&' && next === '&') { tokens.push({ type: 'AND' }); this.advance(); this.advance(); continue; }
188
- if (char === '|' && next === '|') { tokens.push({ type: 'OR' }); this.advance(); this.advance(); continue; }
189
- if (char === '+' && next === '+') { tokens.push({ type: 'PLUSPLUS' }); this.advance(); this.advance(); continue; }
190
- if (char === '-' && next === '-') { tokens.push({ type: 'MINUSMINUS' }); this.advance(); this.advance(); continue; }
191
-
192
- const compound = { '+': 'PLUSEQ', '-': 'MINUSEQ', '*': 'STAREQ', '/': 'SLASHEQ', '%': 'MODEQ' };
193
- if (next === '=' && compound[char]) {
194
- tokens.push({ type: compound[char] });
195
- this.advance(); this.advance();
196
- continue;
197
- }
138
+ const startLine = this.line;
139
+ const startCol = this.column;
140
+
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; }
198
153
 
199
154
  const singles = {
200
155
  '+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH', '%': 'MOD',
201
156
  '=': 'EQUAL', '<': 'LT', '>': 'GT', '!': 'NOT',
202
- '(': 'LPAREN', ')': 'RPAREN',
203
- '{': 'LBRACE', '}': 'RBRACE',
204
- '[': 'LBRACKET', ']': 'RBRACKET',
205
- ';': 'SEMICOLON', ',': 'COMMA',
157
+ '(': 'LPAREN', ')': 'RPAREN', '{': 'LBRACE', '}': 'RBRACE',
158
+ ';': 'SEMICOLON', ',': 'COMMA', '[': 'LBRACKET', ']': 'RBRACKET',
206
159
  ':': 'COLON', '.': 'DOT'
207
160
  };
208
161
 
209
- if (singles[char]) {
210
- tokens.push({ type: singles[char] });
211
- this.advance();
212
- continue;
213
- }
162
+ if (singles[char]) { tokens.push({ type: singles[char], line: startLine, column: startCol }); this.advance(); continue; }
214
163
 
215
- this.error(`Unexpected character: ${char}`);
164
+ this.error("Unexpected character: " + char);
216
165
  }
217
166
 
218
- tokens.push({ type: 'EOF' });
167
+ tokens.push({ type: 'EOF', line: this.line, column: this.column });
219
168
  return tokens;
220
169
  }
221
170
  }