starlight-cli 1.0.21 → 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/dist/index.js +259 -238
- package/package.json +1 -1
- package/src/evaluator.js +109 -71
- 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) {
|
|
@@ -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
|
|
240
|
-
case 'NOTEQ': return l
|
|
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)
|
|
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') {
|
|
357
|
-
|
|
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
|
@@ -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
|
}
|