starlight-cli 1.0.2 → 1.0.4
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 +2556 -0
- package/dist/read.cs.js +123 -0
- package/dist/read.ps1 +128 -0
- package/dist/read.sh +137 -0
- package/index.js +3 -5
- package/install.js +3 -6
- package/package.json +10 -3
- package/src/evaluator.js +400 -0
- package/src/lexer.js +157 -0
- package/src/parser.js +441 -0
- package/src/starlight.js +117 -0
package/src/evaluator.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
const readlineSync = require('readline-sync');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const Lexer = require('./lexer');
|
|
4
|
+
const Parser = require('./parser');
|
|
5
|
+
|
|
6
|
+
class ReturnValue {
|
|
7
|
+
constructor(value) { this.value = value; }
|
|
8
|
+
}
|
|
9
|
+
class BreakSignal {}
|
|
10
|
+
class ContinueSignal {}
|
|
11
|
+
|
|
12
|
+
class Environment {
|
|
13
|
+
constructor(parent = null) {
|
|
14
|
+
this.store = Object.create(null);
|
|
15
|
+
this.parent = parent;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
has(name) {
|
|
19
|
+
if (name in this.store) return true;
|
|
20
|
+
if (this.parent) return this.parent.has(name);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get(name) {
|
|
25
|
+
if (name in this.store) return this.store[name];
|
|
26
|
+
if (this.parent) return this.parent.get(name);
|
|
27
|
+
throw new Error(`Undefined variable: ${name}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set(name, value) {
|
|
31
|
+
if (name in this.store) { this.store[name] = value; return value; }
|
|
32
|
+
if (this.parent && this.parent.has(name)) { return this.parent.set(name, value); }
|
|
33
|
+
this.store[name] = value;
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
define(name, value) {
|
|
38
|
+
this.store[name] = value;
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class Evaluator {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.global = new Environment();
|
|
46
|
+
this.setupBuiltins();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setupBuiltins() {
|
|
50
|
+
this.global.define('len', arg => {
|
|
51
|
+
if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
|
|
52
|
+
if (arg && typeof arg === 'object') return Object.keys(arg).length;
|
|
53
|
+
return 0;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.global.define('print', arg => { console.log(arg); return null; });
|
|
57
|
+
this.global.define('type', arg => {
|
|
58
|
+
if (Array.isArray(arg)) return 'array';
|
|
59
|
+
return typeof arg;
|
|
60
|
+
});
|
|
61
|
+
this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
|
|
62
|
+
this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
|
|
63
|
+
|
|
64
|
+
this.global.define('ask', prompt => {
|
|
65
|
+
const readlineSync = require('readline-sync');
|
|
66
|
+
return readlineSync.question(prompt + ' ');
|
|
67
|
+
});
|
|
68
|
+
this.global.define('num', arg => {
|
|
69
|
+
const n = Number(arg);
|
|
70
|
+
if (Number.isNaN(n)) {
|
|
71
|
+
throw new Error('Cannot convert value to number');
|
|
72
|
+
}
|
|
73
|
+
return n;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.global.define('str', arg => {
|
|
77
|
+
return String(arg);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
evaluate(node, env = this.global) {
|
|
84
|
+
switch (node.type) {
|
|
85
|
+
case 'Program': return this.evalProgram(node, env);
|
|
86
|
+
case 'BlockStatement': return this.evalBlock(node, new Environment(env));
|
|
87
|
+
case 'VarDeclaration': return this.evalVarDeclaration(node, env);
|
|
88
|
+
case 'AssignmentExpression': return this.evalAssignment(node, env);
|
|
89
|
+
case 'CompoundAssignment': return this.evalCompoundAssignment(node, env);
|
|
90
|
+
case 'SldeployStatement': return this.evalSldeploy(node, env);
|
|
91
|
+
case 'AskStatement': return this.evalAsk(node, env);
|
|
92
|
+
case 'DefineStatement': return this.evalDefine(node, env);
|
|
93
|
+
case 'ExpressionStatement': return this.evaluate(node.expression, env);
|
|
94
|
+
case 'BinaryExpression': return this.evalBinary(node, env);
|
|
95
|
+
case 'LogicalExpression': return this.evalLogical(node, env);
|
|
96
|
+
case 'UnaryExpression': return this.evalUnary(node, env);
|
|
97
|
+
case 'Literal': return node.value;
|
|
98
|
+
case 'Identifier': return env.get(node.name);
|
|
99
|
+
case 'IfStatement': return this.evalIf(node, env);
|
|
100
|
+
case 'WhileStatement': return this.evalWhile(node, env);
|
|
101
|
+
case 'ForStatement': return this.evalFor(node, env);
|
|
102
|
+
case 'BreakStatement': throw new BreakSignal();
|
|
103
|
+
case 'ContinueStatement': throw new ContinueSignal();
|
|
104
|
+
case 'ImportStatement': return this.evalImport(node, env);
|
|
105
|
+
case 'FunctionDeclaration': return this.evalFunctionDeclaration(node, env);
|
|
106
|
+
case 'CallExpression': return this.evalCall(node, env);
|
|
107
|
+
case 'ReturnStatement': {
|
|
108
|
+
const val = node.argument ? this.evaluate(node.argument, env) : null;
|
|
109
|
+
throw new ReturnValue(val);
|
|
110
|
+
}
|
|
111
|
+
case 'ArrayExpression': return node.elements.map(el => this.evaluate(el, env));
|
|
112
|
+
case 'IndexExpression': return this.evalIndex(node, env);
|
|
113
|
+
case 'ObjectExpression': return this.evalObject(node, env);
|
|
114
|
+
case 'MemberExpression': return this.evalMember(node, env);
|
|
115
|
+
case 'UpdateExpression': return this.evalUpdate(node, env);
|
|
116
|
+
default:
|
|
117
|
+
throw new Error(`Unknown node type in evaluator: ${node.type}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
evalProgram(node, env) {
|
|
122
|
+
let result = null;
|
|
123
|
+
for (const stmt of node.body) {
|
|
124
|
+
result = this.evaluate(stmt, env);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
evalImport(node, env) {
|
|
129
|
+
const path = node.path;
|
|
130
|
+
let lib;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
lib = require(path);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
const fullPath = path.endsWith('.sl') ? path : path + '.sl';
|
|
136
|
+
|
|
137
|
+
if (!fs.existsSync(fullPath)) {
|
|
138
|
+
throw new Error(`Import not found: ${path}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const code = fs.readFileSync(fullPath, 'utf-8');
|
|
142
|
+
const tokens = new Lexer(code).getTokens();
|
|
143
|
+
const ast = new Parser(tokens).parse();
|
|
144
|
+
|
|
145
|
+
const moduleEnv = new Environment(env);
|
|
146
|
+
this.evaluate(ast, moduleEnv);
|
|
147
|
+
|
|
148
|
+
lib = {};
|
|
149
|
+
for (const key of Object.keys(moduleEnv.store)) {
|
|
150
|
+
lib[key] = moduleEnv.store[key];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// JS-style default export fallback
|
|
154
|
+
lib.default = lib;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const spec of node.specifiers) {
|
|
158
|
+
if (spec.type === 'DefaultImport') {
|
|
159
|
+
env.define(spec.local, lib.default ?? lib);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (spec.type === 'NamespaceImport') {
|
|
163
|
+
env.define(spec.local, lib);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (spec.type === 'NamedImport') {
|
|
167
|
+
if (!(spec.imported in lib)) {
|
|
168
|
+
throw new Error(`Module '${path}' has no export '${spec.imported}'`);
|
|
169
|
+
}
|
|
170
|
+
env.define(spec.local, lib[spec.imported]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
evalBlock(node, env) {
|
|
178
|
+
let result = null;
|
|
179
|
+
for (const stmt of node.body) {
|
|
180
|
+
try {
|
|
181
|
+
result = this.evaluate(stmt, env);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
evalVarDeclaration(node, env) {
|
|
191
|
+
const val = this.evaluate(node.expr, env);
|
|
192
|
+
return env.define(node.id, val);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
evalAssignment(node, env) {
|
|
196
|
+
const rightVal = this.evaluate(node.right, env);
|
|
197
|
+
const left = node.left;
|
|
198
|
+
|
|
199
|
+
if (left.type === 'Identifier') return env.set(left.name, rightVal);
|
|
200
|
+
if (left.type === 'MemberExpression') {
|
|
201
|
+
const obj = this.evaluate(left.object, env);
|
|
202
|
+
obj[left.property] = rightVal;
|
|
203
|
+
return rightVal;
|
|
204
|
+
}
|
|
205
|
+
if (left.type === 'IndexExpression') {
|
|
206
|
+
const obj = this.evaluate(left.object, env);
|
|
207
|
+
const idx = this.evaluate(left.indexer, env);
|
|
208
|
+
obj[idx] = rightVal;
|
|
209
|
+
return rightVal;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
throw new Error('Invalid assignment target');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
evalCompoundAssignment(node, env) {
|
|
216
|
+
const left = node.left;
|
|
217
|
+
let current;
|
|
218
|
+
|
|
219
|
+
if (left.type === 'Identifier') current = env.get(left.name);
|
|
220
|
+
else if (left.type === 'MemberExpression') current = this.evalMember(left, env);
|
|
221
|
+
else if (left.type === 'IndexExpression') current = this.evalIndex(left, env);
|
|
222
|
+
else throw new Error('Invalid compound assignment target');
|
|
223
|
+
|
|
224
|
+
const rhs = this.evaluate(node.right, env);
|
|
225
|
+
let computed;
|
|
226
|
+
switch (node.operator) {
|
|
227
|
+
case 'PLUSEQ': computed = current + rhs; break;
|
|
228
|
+
case 'MINUSEQ': computed = current - rhs; break;
|
|
229
|
+
case 'STAREQ': computed = current * rhs; break;
|
|
230
|
+
case 'SLASHEQ': computed = current / rhs; break;
|
|
231
|
+
case 'MODEQ': computed = current % rhs; break;
|
|
232
|
+
default: throw new Error('Unknown compound operator');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (left.type === 'Identifier') env.set(left.name, computed);
|
|
236
|
+
else if (left.type === 'MemberExpression') this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
|
|
237
|
+
else this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
|
|
238
|
+
|
|
239
|
+
return computed;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
evalSldeploy(node, env) {
|
|
243
|
+
const val = this.evaluate(node.expr, env);
|
|
244
|
+
console.log(val);
|
|
245
|
+
return val;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
evalAsk(node, env) {
|
|
249
|
+
const prompt = this.evaluate(node.prompt, env);
|
|
250
|
+
const input = readlineSync.question(prompt + ' ');
|
|
251
|
+
return input;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
evalDefine(node, env) {
|
|
255
|
+
const val = node.expr ? this.evaluate(node.expr, env) : null;
|
|
256
|
+
return this.global.define(node.id, val);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
evalBinary(node, env) {
|
|
260
|
+
const l = this.evaluate(node.left, env);
|
|
261
|
+
const r = this.evaluate(node.right, env);
|
|
262
|
+
switch (node.operator) {
|
|
263
|
+
case 'PLUS': return l + r;
|
|
264
|
+
case 'MINUS': return l - r;
|
|
265
|
+
case 'STAR': return l * r;
|
|
266
|
+
case 'SLASH': return l / r;
|
|
267
|
+
case 'MOD': return l % r;
|
|
268
|
+
case 'EQEQ': return l === r;
|
|
269
|
+
case 'NOTEQ': return l !== r;
|
|
270
|
+
case 'LT': return l < r;
|
|
271
|
+
case 'LTE': return l <= r;
|
|
272
|
+
case 'GT': return l > r;
|
|
273
|
+
case 'GTE': return l >= r;
|
|
274
|
+
default: throw new Error(`Unknown binary operator ${node.operator}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
evalLogical(node, env) {
|
|
279
|
+
const l = this.evaluate(node.left, env);
|
|
280
|
+
if (node.operator === 'AND') return l && this.evaluate(node.right, env);
|
|
281
|
+
if (node.operator === 'OR') return l || this.evaluate(node.right, env);
|
|
282
|
+
throw new Error(`Unknown logical operator ${node.operator}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
evalUnary(node, env) {
|
|
286
|
+
const val = this.evaluate(node.argument, env);
|
|
287
|
+
switch (node.operator) {
|
|
288
|
+
case 'NOT': return !val;
|
|
289
|
+
case 'MINUS': return -val;
|
|
290
|
+
case 'PLUS': return +val;
|
|
291
|
+
default: throw new Error(`Unknown unary operator ${node.operator}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
evalIf(node, env) {
|
|
296
|
+
const test = this.evaluate(node.test, env);
|
|
297
|
+
if (test) return this.evaluate(node.consequent, env);
|
|
298
|
+
if (node.alternate) return this.evaluate(node.alternate, env);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
evalWhile(node, env) {
|
|
303
|
+
while (this.evaluate(node.test, env)) {
|
|
304
|
+
try { this.evaluate(node.body, env); }
|
|
305
|
+
catch (e) {
|
|
306
|
+
if (e instanceof BreakSignal) break;
|
|
307
|
+
if (e instanceof ContinueSignal) continue;
|
|
308
|
+
throw e;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
evalFor(node, env) {
|
|
315
|
+
const local = new Environment(env);
|
|
316
|
+
if (node.init) this.evaluate(node.init, local);
|
|
317
|
+
while (!node.test || this.evaluate(node.test, local)) {
|
|
318
|
+
try { this.evaluate(node.body, local); }
|
|
319
|
+
catch (e) {
|
|
320
|
+
if (e instanceof BreakSignal) break;
|
|
321
|
+
if (e instanceof ContinueSignal) { if (node.update) this.evaluate(node.update, local); continue; }
|
|
322
|
+
throw e;
|
|
323
|
+
}
|
|
324
|
+
if (node.update) this.evaluate(node.update, local);
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
evalFunctionDeclaration(node, env) {
|
|
330
|
+
const fn = { params: node.params, body: node.body, env };
|
|
331
|
+
env.define(node.name, fn);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
evalCall(node, env) {
|
|
336
|
+
const calleeEvaluated = this.evaluate(node.callee, env);
|
|
337
|
+
if (typeof calleeEvaluated === 'function') {
|
|
338
|
+
const args = node.arguments.map(a => this.evaluate(a, env));
|
|
339
|
+
return calleeEvaluated(...args);
|
|
340
|
+
}
|
|
341
|
+
if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
|
|
342
|
+
throw new Error('Call to non-function');
|
|
343
|
+
}
|
|
344
|
+
const fn = calleeEvaluated;
|
|
345
|
+
const callEnv = new Environment(fn.env);
|
|
346
|
+
fn.params.forEach((p, i) => {
|
|
347
|
+
const argVal = node.arguments[i] ? this.evaluate(node.arguments[i], env) : null;
|
|
348
|
+
callEnv.define(p, argVal);
|
|
349
|
+
});
|
|
350
|
+
try { return this.evaluate(fn.body, callEnv); }
|
|
351
|
+
catch (e) { if (e instanceof ReturnValue) return e.value; throw e; }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
evalIndex(node, env) {
|
|
355
|
+
const obj = this.evaluate(node.object, env);
|
|
356
|
+
const idx = this.evaluate(node.indexer, env);
|
|
357
|
+
if (obj == null) throw new Error('Indexing null/undefined');
|
|
358
|
+
return obj[idx];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
evalObject(node, env) {
|
|
362
|
+
const out = {};
|
|
363
|
+
for (const p of node.props) out[p.key] = this.evaluate(p.value, env);
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
evalMember(node, env) {
|
|
368
|
+
const obj = this.evaluate(node.object, env);
|
|
369
|
+
if (obj == null) throw new Error('Member access of null/undefined');
|
|
370
|
+
return obj[node.property];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
evalUpdate(node, env) {
|
|
374
|
+
const arg = node.argument;
|
|
375
|
+
const getCurrent = () => {
|
|
376
|
+
if (arg.type === 'Identifier') return env.get(arg.name);
|
|
377
|
+
if (arg.type === 'MemberExpression') return this.evalMember(arg, env);
|
|
378
|
+
if (arg.type === 'IndexExpression') return this.evalIndex(arg, env);
|
|
379
|
+
throw new Error('Invalid update target');
|
|
380
|
+
};
|
|
381
|
+
const setValue = (v) => {
|
|
382
|
+
if (arg.type === 'Identifier') env.set(arg.name, v);
|
|
383
|
+
else if (arg.type === 'MemberExpression') {
|
|
384
|
+
const obj = this.evaluate(arg.object, env);
|
|
385
|
+
obj[arg.property] = v;
|
|
386
|
+
}
|
|
387
|
+
else if (arg.type === 'IndexExpression') {
|
|
388
|
+
const obj = this.evaluate(arg.object, env);
|
|
389
|
+
const idx = this.evaluate(arg.indexer, env);
|
|
390
|
+
obj[idx] = v;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
const current = getCurrent();
|
|
394
|
+
const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
|
|
395
|
+
if (node.prefix) { setValue(newVal); return newVal; }
|
|
396
|
+
else { setValue(newVal); return current; }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
module.exports = Evaluator;
|
package/src/lexer.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
class Lexer {
|
|
2
|
+
constructor(input) {
|
|
3
|
+
this.input = input;
|
|
4
|
+
this.pos = 0;
|
|
5
|
+
this.currentChar = input[0] || null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
advance() {
|
|
9
|
+
this.pos++;
|
|
10
|
+
this.currentChar = this.pos < this.input.length ? this.input[this.pos] : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
peek() {
|
|
14
|
+
return this.pos + 1 < this.input.length ? this.input[this.pos + 1] : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
error(msg) {
|
|
18
|
+
throw new Error(`LEXER ERROR: ${msg} at position ${this.pos}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
skipWhitespace() {
|
|
22
|
+
while (this.currentChar && /\s/.test(this.currentChar)) this.advance();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
skipComment() {
|
|
26
|
+
if (this.currentChar === '#') {
|
|
27
|
+
if (this.peek() === '*') {
|
|
28
|
+
this.advance(); this.advance(); // skip #*
|
|
29
|
+
while (this.currentChar !== null) {
|
|
30
|
+
if (this.currentChar === '*' && this.peek() === '#') {
|
|
31
|
+
this.advance(); this.advance();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.advance();
|
|
35
|
+
}
|
|
36
|
+
this.error("Unterminated multi-line comment (#* ... *#)");
|
|
37
|
+
} else {
|
|
38
|
+
while (this.currentChar && this.currentChar !== '\n') this.advance();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
number() {
|
|
44
|
+
let result = '';
|
|
45
|
+
while (this.currentChar && /[0-9]/.test(this.currentChar)) {
|
|
46
|
+
result += this.currentChar;
|
|
47
|
+
this.advance();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.currentChar === '.' && /[0-9]/.test(this.peek())) {
|
|
51
|
+
result += '.'; this.advance();
|
|
52
|
+
while (this.currentChar && /[0-9]/.test(this.currentChar)) {
|
|
53
|
+
result += this.currentChar;
|
|
54
|
+
this.advance();
|
|
55
|
+
}
|
|
56
|
+
return { type: 'NUMBER', value: parseFloat(result) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { type: 'NUMBER', value: parseInt(result) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
identifier() {
|
|
63
|
+
let result = '';
|
|
64
|
+
while (this.currentChar && /[A-Za-z0-9_]/.test(this.currentChar)) {
|
|
65
|
+
result += this.currentChar;
|
|
66
|
+
this.advance();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const keywords = [
|
|
70
|
+
'let', 'sldeploy', 'if', 'else', 'while', 'for',
|
|
71
|
+
'break', 'continue', 'func', 'return', 'true', 'false', 'null',
|
|
72
|
+
'ask', 'define', 'import', 'from', 'as'
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
if (keywords.includes(result)) {
|
|
76
|
+
return { type: result.toUpperCase(), value: result };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { type: 'IDENTIFIER', value: result };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
string() {
|
|
84
|
+
const quote = this.currentChar;
|
|
85
|
+
this.advance();
|
|
86
|
+
let result = '';
|
|
87
|
+
|
|
88
|
+
while (this.currentChar && this.currentChar !== quote) {
|
|
89
|
+
if (this.currentChar === '\\') {
|
|
90
|
+
this.advance();
|
|
91
|
+
switch (this.currentChar) {
|
|
92
|
+
case 'n': result += '\n'; break;
|
|
93
|
+
case 't': result += '\t'; break;
|
|
94
|
+
case '"': result += '"'; break;
|
|
95
|
+
case "'": result += "'"; break;
|
|
96
|
+
case '\\': result += '\\'; break;
|
|
97
|
+
default: result += this.currentChar;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
result += this.currentChar;
|
|
101
|
+
}
|
|
102
|
+
this.advance();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.currentChar !== quote) {
|
|
106
|
+
this.error('Unterminated string literal');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.advance();
|
|
110
|
+
return { type: 'STRING', value: result };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getTokens() {
|
|
114
|
+
const tokens = [];
|
|
115
|
+
|
|
116
|
+
while (this.currentChar !== null) {
|
|
117
|
+
if (/\s/.test(this.currentChar)) { this.skipWhitespace(); continue; }
|
|
118
|
+
if (this.currentChar === '#') { this.skipComment(); continue; }
|
|
119
|
+
if (/[0-9]/.test(this.currentChar)) { tokens.push(this.number()); continue; }
|
|
120
|
+
if (/[A-Za-z_]/.test(this.currentChar)) { tokens.push(this.identifier()); continue; }
|
|
121
|
+
if (this.currentChar === '"' || this.currentChar === "'") { tokens.push(this.string()); continue; }
|
|
122
|
+
|
|
123
|
+
const char = this.currentChar;
|
|
124
|
+
const next = this.peek();
|
|
125
|
+
|
|
126
|
+
if (char === '=' && next === '=') { tokens.push({ type: 'EQEQ' }); this.advance(); this.advance(); continue; }
|
|
127
|
+
if (char === '=' && next === '>') { tokens.push({ type: 'ARROW' }); this.advance(); this.advance(); continue; }
|
|
128
|
+
if (char === '!' && next === '=') { tokens.push({ type: 'NOTEQ' }); this.advance(); this.advance(); continue; }
|
|
129
|
+
if (char === '<' && next === '=') { tokens.push({ type: 'LTE' }); this.advance(); this.advance(); continue; }
|
|
130
|
+
if (char === '>' && next === '=') { tokens.push({ type: 'GTE' }); this.advance(); this.advance(); continue; }
|
|
131
|
+
if (char === '&' && next === '&') { tokens.push({ type: 'AND' }); this.advance(); this.advance(); continue; }
|
|
132
|
+
if (char === '|' && next === '|') { tokens.push({ type: 'OR' }); this.advance(); this.advance(); continue; }
|
|
133
|
+
if (char === '+' && next === '+') { tokens.push({ type: 'PLUSPLUS' }); this.advance(); this.advance(); continue; }
|
|
134
|
+
if (char === '-' && next === '-') { tokens.push({ type: 'MINUSMINUS' }); this.advance(); this.advance(); continue; }
|
|
135
|
+
|
|
136
|
+
const map = { '+': 'PLUSEQ', '-': 'MINUSEQ', '*': 'STAREQ', '/': 'SLASHEQ', '%': 'MODEQ' };
|
|
137
|
+
if (next === '=' && map[char]) { tokens.push({ type: map[char] }); this.advance(); this.advance(); continue; }
|
|
138
|
+
|
|
139
|
+
const singles = {
|
|
140
|
+
'+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH', '%': 'MOD',
|
|
141
|
+
'=': 'EQUAL', '<': 'LT', '>': 'GT', '!': 'NOT',
|
|
142
|
+
'(': 'LPAREN', ')': 'RPAREN', '{': 'LBRACE', '}': 'RBRACE',
|
|
143
|
+
';': 'SEMICOLON', ',': 'COMMA', '[': 'LBRACKET', ']': 'RBRACKET',
|
|
144
|
+
':': 'COLON', '.': 'DOT'
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (singles[char]) { tokens.push({ type: singles[char] }); this.advance(); continue; }
|
|
148
|
+
|
|
149
|
+
this.error("Unexpected character: " + char);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
tokens.push({ type: 'EOF' });
|
|
153
|
+
return tokens;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = Lexer;
|