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/dist/index.js +291 -253
- package/package.json +1 -1
- package/src/evaluator.js +141 -86
- package/src/lexer.js +114 -109
- package/src/parser.js +35 -55
- package/src/starlight.js +1 -1
package/package.json
CHANGED
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 {
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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 =>
|
|
64
|
-
|
|
65
|
-
|
|
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('
|
|
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)
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 {
|
|
171
|
-
|
|
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') {
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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') {
|
|
357
|
-
|
|
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[
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
55
|
+
while (this.currentChar && /[0-9]/.test(this.currentChar)) {
|
|
49
56
|
result += this.currentChar;
|
|
50
57
|
this.advance();
|
|
51
58
|
}
|
|
52
|
-
|
|
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 && /[
|
|
76
|
+
while (this.currentChar && /[A-Za-z0-9_]/.test(this.currentChar)) {
|
|
58
77
|
result += this.currentChar;
|
|
59
78
|
this.advance();
|
|
60
79
|
}
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
66
|
-
this.
|
|
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
|
-
|
|
100
|
+
|
|
101
|
+
while (this.currentChar && this.currentChar !== quote) {
|
|
69
102
|
if (this.currentChar === '\\') {
|
|
70
103
|
this.advance();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
result +=
|
|
74
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
+
const char = this.currentChar;
|
|
137
|
+
const next = this.peek();
|
|
138
|
+
const startLine = this.line;
|
|
139
|
+
const startCol = this.column;
|
|
114
140
|
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
166
|
+
|
|
167
|
+
tokens.push({ type: 'EOF', line: this.line, column: this.column });
|
|
163
168
|
return tokens;
|
|
164
169
|
}
|
|
165
170
|
}
|