starlight-cli 1.0.46 → 1.0.48
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 +530 -173
- package/package.json +1 -1
- package/src/evaluator.js +185 -62
- package/src/lexer.js +33 -8
- package/src/parser.js +289 -92
- package/src/starlight.js +23 -11
package/package.json
CHANGED
package/src/evaluator.js
CHANGED
|
@@ -9,6 +9,29 @@ class ReturnValue {
|
|
|
9
9
|
}
|
|
10
10
|
class BreakSignal {}
|
|
11
11
|
class ContinueSignal {}
|
|
12
|
+
class RuntimeError extends Error {
|
|
13
|
+
constructor(message, node, source) {
|
|
14
|
+
const line = node?.line ?? '?';
|
|
15
|
+
const column = node?.column ?? '?';
|
|
16
|
+
|
|
17
|
+
let output = ` ${message} at line ${line}, column ${column}\n`;
|
|
18
|
+
|
|
19
|
+
if (source && node?.line != null) {
|
|
20
|
+
const lines = source.split('\n');
|
|
21
|
+
const srcLine = lines[node.line - 1] || '';
|
|
22
|
+
output += ` ${srcLine}\n`;
|
|
23
|
+
output += ` ${' '.repeat(column - 1)}^\n`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
super(output);
|
|
27
|
+
|
|
28
|
+
this.name = 'RuntimeError';
|
|
29
|
+
this.line = line;
|
|
30
|
+
this.column = column;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
12
35
|
|
|
13
36
|
class Environment {
|
|
14
37
|
constructor(parent = null) {
|
|
@@ -22,11 +45,11 @@ class Environment {
|
|
|
22
45
|
return false;
|
|
23
46
|
}
|
|
24
47
|
|
|
25
|
-
get(name) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
get(name, node) {
|
|
49
|
+
if (name in this.store) return this.store[name];
|
|
50
|
+
if (this.parent) return this.parent.get(name, node);
|
|
51
|
+
throw new RuntimeError(`Undefined variable: ${name}`, node);
|
|
52
|
+
}
|
|
30
53
|
|
|
31
54
|
set(name, value) {
|
|
32
55
|
if (name in this.store) { this.store[name] = value; return value; }
|
|
@@ -42,7 +65,8 @@ class Environment {
|
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
class Evaluator {
|
|
45
|
-
constructor() {
|
|
68
|
+
constructor(source = '') {
|
|
69
|
+
this.source = source;
|
|
46
70
|
this.global = new Environment();
|
|
47
71
|
this.setupBuiltins();
|
|
48
72
|
}
|
|
@@ -129,11 +153,11 @@ this.global.define('range', (...args) => {
|
|
|
129
153
|
end = Number(args[1]);
|
|
130
154
|
step = Number(args[2]);
|
|
131
155
|
} else {
|
|
132
|
-
throw new
|
|
156
|
+
throw new RuntimeError('range() expects 1 to 3 arguments', null, this.source);
|
|
133
157
|
}
|
|
134
158
|
|
|
135
159
|
if (step === 0) {
|
|
136
|
-
throw new
|
|
160
|
+
throw new RuntimeError('range() step cannot be 0', null, this.source);
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
const result = [];
|
|
@@ -157,7 +181,7 @@ this.global.define('range', (...args) => {
|
|
|
157
181
|
this.global.define('num', arg => {
|
|
158
182
|
const n = Number(arg);
|
|
159
183
|
if (Number.isNaN(n)) {
|
|
160
|
-
throw new
|
|
184
|
+
throw new RuntimeError('Cannot convert value to number', null, this.source);
|
|
161
185
|
}
|
|
162
186
|
return n;
|
|
163
187
|
});
|
|
@@ -210,7 +234,7 @@ async evaluate(node, env = this.global) {
|
|
|
210
234
|
case 'LogicalExpression': return await this.evalLogical(node, env);
|
|
211
235
|
case 'UnaryExpression': return await this.evalUnary(node, env);
|
|
212
236
|
case 'Literal': return node.value;
|
|
213
|
-
case 'Identifier': return env.get(node.name);
|
|
237
|
+
case 'Identifier': return env.get(node.name, node);
|
|
214
238
|
case 'IfStatement': return await this.evalIf(node, env);
|
|
215
239
|
case 'WhileStatement': return await this.evalWhile(node, env);
|
|
216
240
|
case 'ForStatement':
|
|
@@ -233,14 +257,8 @@ case 'DoTrackStatement':
|
|
|
233
257
|
case 'ArrayExpression':
|
|
234
258
|
return await Promise.all(node.elements.map(el => this.evaluate(el, env)));
|
|
235
259
|
case 'IndexExpression': return await this.evalIndex(node, env);
|
|
236
|
-
case 'ObjectExpression':
|
|
237
|
-
|
|
238
|
-
for (const p of node.props) {
|
|
239
|
-
const key = await this.evaluate(p.key, env);
|
|
240
|
-
out[key] = await this.evaluate(p.value, env);
|
|
241
|
-
}
|
|
242
|
-
return out;
|
|
243
|
-
}
|
|
260
|
+
case 'ObjectExpression': return await this.evalObject(node, env);
|
|
261
|
+
|
|
244
262
|
|
|
245
263
|
case 'MemberExpression': return await this.evalMember(node, env);
|
|
246
264
|
case 'UpdateExpression': return await this.evalUpdate(node, env);
|
|
@@ -268,7 +286,7 @@ case 'DoTrackStatement':
|
|
|
268
286
|
}
|
|
269
287
|
|
|
270
288
|
if (typeof callee !== 'function') {
|
|
271
|
-
throw new
|
|
289
|
+
throw new RuntimeError('NewExpression callee is not a function', node, this.source);
|
|
272
290
|
}
|
|
273
291
|
|
|
274
292
|
const args = [];
|
|
@@ -277,31 +295,51 @@ case 'DoTrackStatement':
|
|
|
277
295
|
}
|
|
278
296
|
|
|
279
297
|
default:
|
|
280
|
-
|
|
298
|
+
throw new RuntimeError(`Unknown node type in evaluator: ${node.type}`, node, this.source);
|
|
299
|
+
|
|
281
300
|
}
|
|
282
301
|
}
|
|
283
302
|
|
|
284
303
|
async evalProgram(node, env) {
|
|
285
304
|
let result = null;
|
|
286
305
|
for (const stmt of node.body) {
|
|
287
|
-
|
|
306
|
+
try {
|
|
307
|
+
result = await this.evaluate(stmt, env);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
if (e instanceof RuntimeError || e instanceof BreakSignal || e instanceof ContinueSignal || e instanceof ReturnValue) {
|
|
310
|
+
throw e;
|
|
311
|
+
}
|
|
312
|
+
throw new RuntimeError(e.message || 'Error in program', stmt, this.source);
|
|
313
|
+
}
|
|
288
314
|
}
|
|
289
315
|
return result;
|
|
290
316
|
}
|
|
291
317
|
|
|
318
|
+
|
|
292
319
|
async evalDoTrack(node, env) {
|
|
293
320
|
try {
|
|
294
321
|
return await this.evaluate(node.body, env);
|
|
295
322
|
} catch (err) {
|
|
296
|
-
if (!node.handler)
|
|
323
|
+
if (!node.handler) {
|
|
324
|
+
// Wrap any raw error into RuntimeError with line info
|
|
325
|
+
if (err instanceof RuntimeError) throw err;
|
|
326
|
+
throw new RuntimeError(err.message || 'Error in doTrack body', node.body, this.source);
|
|
327
|
+
}
|
|
297
328
|
|
|
298
329
|
const trackEnv = new Environment(env);
|
|
299
330
|
trackEnv.define('error', err);
|
|
300
331
|
|
|
301
|
-
|
|
332
|
+
try {
|
|
333
|
+
return await this.evaluate(node.handler, trackEnv);
|
|
334
|
+
} catch (handlerErr) {
|
|
335
|
+
// Wrap handler errors as well
|
|
336
|
+
if (handlerErr instanceof RuntimeError) throw handlerErr;
|
|
337
|
+
throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler, this.source);
|
|
338
|
+
}
|
|
302
339
|
}
|
|
303
340
|
}
|
|
304
341
|
|
|
342
|
+
|
|
305
343
|
async evalImport(node, env) {
|
|
306
344
|
const spec = node.path;
|
|
307
345
|
let lib;
|
|
@@ -317,7 +355,7 @@ async evalImport(node, env) {
|
|
|
317
355
|
: path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
|
|
318
356
|
|
|
319
357
|
if (!fs.existsSync(fullPath)) {
|
|
320
|
-
throw new
|
|
358
|
+
throw new RuntimeError(`Import not found: ${spec}`, node, this.source);
|
|
321
359
|
}
|
|
322
360
|
|
|
323
361
|
const code = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -344,7 +382,7 @@ async evalImport(node, env) {
|
|
|
344
382
|
}
|
|
345
383
|
if (imp.type === 'NamedImport') {
|
|
346
384
|
if (!(imp.imported in lib)) {
|
|
347
|
-
throw new
|
|
385
|
+
throw new RuntimeError(`Module '${spec}' has no export '${imp.imported}'`, node, this.source);
|
|
348
386
|
}
|
|
349
387
|
env.define(imp.local, lib[imp.imported]);
|
|
350
388
|
}
|
|
@@ -360,18 +398,32 @@ async evalBlock(node, env) {
|
|
|
360
398
|
result = await this.evaluate(stmt, env);
|
|
361
399
|
} catch (e) {
|
|
362
400
|
if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
|
|
363
|
-
|
|
401
|
+
// Wrap any other error in RuntimeError with the current block node
|
|
402
|
+
throw new RuntimeError(e.message || 'Error in block', stmt, this.source);
|
|
364
403
|
}
|
|
365
404
|
}
|
|
366
405
|
return result;
|
|
367
406
|
}
|
|
368
407
|
|
|
408
|
+
|
|
369
409
|
async evalVarDeclaration(node, env) {
|
|
410
|
+
if (!node.expr) {
|
|
411
|
+
throw new RuntimeError('Variable declaration requires an initializer', node, this.source);
|
|
412
|
+
}
|
|
413
|
+
|
|
370
414
|
const val = await this.evaluate(node.expr, env);
|
|
371
415
|
return env.define(node.id, val);
|
|
372
416
|
}
|
|
373
417
|
|
|
418
|
+
|
|
374
419
|
evalArrowFunction(node, env) {
|
|
420
|
+
if (!node.body) {
|
|
421
|
+
throw new RuntimeError('Arrow function missing body', node, this.source);
|
|
422
|
+
}
|
|
423
|
+
if (!Array.isArray(node.params)) {
|
|
424
|
+
throw new RuntimeError('Invalid arrow function parameters', node, this.source);
|
|
425
|
+
}
|
|
426
|
+
|
|
375
427
|
return {
|
|
376
428
|
params: node.params,
|
|
377
429
|
body: node.body,
|
|
@@ -381,24 +433,29 @@ evalArrowFunction(node, env) {
|
|
|
381
433
|
};
|
|
382
434
|
}
|
|
383
435
|
|
|
436
|
+
|
|
384
437
|
async evalAssignment(node, env) {
|
|
385
438
|
const rightVal = await this.evaluate(node.right, env);
|
|
386
439
|
const left = node.left;
|
|
387
440
|
|
|
388
441
|
if (left.type === 'Identifier') return env.set(left.name, rightVal);
|
|
389
442
|
if (left.type === 'MemberExpression') {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
443
|
+
const obj = await this.evaluate(left.object, env);
|
|
444
|
+
if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
|
|
445
|
+
obj[left.property] = rightVal;
|
|
446
|
+
return rightVal;
|
|
447
|
+
}
|
|
448
|
+
if (left.type === 'IndexExpression') {
|
|
449
|
+
const obj = await this.evaluate(left.object, env);
|
|
450
|
+
const idx = await this.evaluate(left.indexer, env);
|
|
451
|
+
if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
|
|
452
|
+
obj[idx] = rightVal;
|
|
453
|
+
return rightVal;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
throw new RuntimeError('Invalid assignment target', node, this.source);
|
|
400
458
|
|
|
401
|
-
throw new Error('Invalid assignment target');
|
|
402
459
|
}
|
|
403
460
|
|
|
404
461
|
async evalCompoundAssignment(node, env) {
|
|
@@ -408,7 +465,8 @@ async evalCompoundAssignment(node, env) {
|
|
|
408
465
|
if (left.type === 'Identifier') current = env.get(left.name);
|
|
409
466
|
else if (left.type === 'MemberExpression') current = await this.evalMember(left, env);
|
|
410
467
|
else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env);
|
|
411
|
-
else throw new
|
|
468
|
+
else throw new RuntimeError('Invalid compound assignment target', node, this.source);
|
|
469
|
+
|
|
412
470
|
|
|
413
471
|
const rhs = await this.evaluate(node.right, env);
|
|
414
472
|
let computed;
|
|
@@ -418,7 +476,8 @@ async evalCompoundAssignment(node, env) {
|
|
|
418
476
|
case 'STAREQ': computed = current * rhs; break;
|
|
419
477
|
case 'SLASHEQ': computed = current / rhs; break;
|
|
420
478
|
case 'MODEQ': computed = current % rhs; break;
|
|
421
|
-
default: throw new
|
|
479
|
+
default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node, this.source);
|
|
480
|
+
|
|
422
481
|
}
|
|
423
482
|
|
|
424
483
|
if (left.type === 'Identifier') env.set(left.name, computed);
|
|
@@ -437,21 +496,33 @@ async evalSldeploy(node, env) {
|
|
|
437
496
|
|
|
438
497
|
async evalAsk(node, env) {
|
|
439
498
|
const prompt = await this.evaluate(node.prompt, env);
|
|
499
|
+
|
|
500
|
+
if (typeof prompt !== 'string') {
|
|
501
|
+
throw new RuntimeError('ask() prompt must be a string', node, this.source);
|
|
502
|
+
}
|
|
503
|
+
|
|
440
504
|
const input = readlineSync.question(prompt + ' ');
|
|
441
505
|
return input;
|
|
442
506
|
}
|
|
443
507
|
|
|
508
|
+
|
|
444
509
|
async evalDefine(node, env) {
|
|
510
|
+
if (!node.id || typeof node.id !== 'string') {
|
|
511
|
+
throw new RuntimeError('Invalid identifier in define statement', node, this.source);
|
|
512
|
+
}
|
|
513
|
+
|
|
445
514
|
const val = node.expr ? await this.evaluate(node.expr, env) : null;
|
|
446
|
-
return
|
|
515
|
+
return env.define(node.id, val);
|
|
516
|
+
|
|
447
517
|
}
|
|
448
518
|
|
|
519
|
+
|
|
449
520
|
async evalBinary(node, env) {
|
|
450
521
|
const l = await this.evaluate(node.left, env);
|
|
451
522
|
const r = await this.evaluate(node.right, env);
|
|
452
523
|
|
|
453
524
|
if (node.operator === 'SLASH' && r === 0) {
|
|
454
|
-
throw new
|
|
525
|
+
throw new RuntimeError('Division by zero', node, this.source);
|
|
455
526
|
}
|
|
456
527
|
|
|
457
528
|
switch (node.operator) {
|
|
@@ -466,7 +537,8 @@ async evalBinary(node, env) {
|
|
|
466
537
|
case 'LTE': return l <= r;
|
|
467
538
|
case 'GT': return l > r;
|
|
468
539
|
case 'GTE': return l >= r;
|
|
469
|
-
default: throw new
|
|
540
|
+
default: throw new RuntimeError(`Unknown binary operator: ${node.operator}`, node, this.source);
|
|
541
|
+
|
|
470
542
|
}
|
|
471
543
|
}
|
|
472
544
|
|
|
@@ -474,7 +546,8 @@ async evalLogical(node, env) {
|
|
|
474
546
|
const l = await this.evaluate(node.left, env);
|
|
475
547
|
if (node.operator === 'AND') return l && await this.evaluate(node.right, env);
|
|
476
548
|
if (node.operator === 'OR') return l || await this.evaluate(node.right, env);
|
|
477
|
-
throw new
|
|
549
|
+
throw new RuntimeError(`Unknown logical operator: ${node.operator}`, node, this.source);
|
|
550
|
+
|
|
478
551
|
}
|
|
479
552
|
|
|
480
553
|
async evalUnary(node, env) {
|
|
@@ -483,21 +556,43 @@ async evalUnary(node, env) {
|
|
|
483
556
|
case 'NOT': return !val;
|
|
484
557
|
case 'MINUS': return -val;
|
|
485
558
|
case 'PLUS': return +val;
|
|
486
|
-
default: throw new
|
|
559
|
+
default: throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node, this.source);
|
|
560
|
+
|
|
487
561
|
}
|
|
488
562
|
}
|
|
489
563
|
|
|
490
564
|
async evalIf(node, env) {
|
|
491
565
|
const test = await this.evaluate(node.test, env);
|
|
492
|
-
|
|
493
|
-
if (
|
|
566
|
+
|
|
567
|
+
if (typeof test !== 'boolean') {
|
|
568
|
+
throw new RuntimeError('If condition must evaluate to a boolean', node.test, this.source);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (test) {
|
|
572
|
+
return await this.evaluate(node.consequent, env);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (node.alternate) {
|
|
576
|
+
return await this.evaluate(node.alternate, env);
|
|
577
|
+
}
|
|
578
|
+
|
|
494
579
|
return null;
|
|
495
580
|
}
|
|
496
581
|
|
|
582
|
+
|
|
497
583
|
async evalWhile(node, env) {
|
|
498
|
-
while (
|
|
499
|
-
|
|
500
|
-
|
|
584
|
+
while (true) {
|
|
585
|
+
const test = await this.evaluate(node.test, env);
|
|
586
|
+
|
|
587
|
+
if (typeof test !== 'boolean') {
|
|
588
|
+
throw new RuntimeError('While condition must evaluate to a boolean', node.test, this.source);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (!test) break;
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
await this.evaluate(node.body, env);
|
|
595
|
+
} catch (e) {
|
|
501
596
|
if (e instanceof BreakSignal) break;
|
|
502
597
|
if (e instanceof ContinueSignal) continue;
|
|
503
598
|
throw e;
|
|
@@ -505,6 +600,7 @@ async evalWhile(node, env) {
|
|
|
505
600
|
}
|
|
506
601
|
return null;
|
|
507
602
|
}
|
|
603
|
+
|
|
508
604
|
async evalFor(node, env) {
|
|
509
605
|
// -------------------------------
|
|
510
606
|
// Python-style: for x in iterable (with optional 'let')
|
|
@@ -513,7 +609,7 @@ async evalFor(node, env) {
|
|
|
513
609
|
const iterable = await this.evaluate(node.iterable, env);
|
|
514
610
|
|
|
515
611
|
if (iterable == null || typeof iterable !== 'object') {
|
|
516
|
-
throw new
|
|
612
|
+
throw new RuntimeError('Cannot iterate over non-iterable', node, this.source);
|
|
517
613
|
}
|
|
518
614
|
|
|
519
615
|
const loopVar = node.variable; // STRING from parser
|
|
@@ -578,9 +674,26 @@ async evalFor(node, env) {
|
|
|
578
674
|
|
|
579
675
|
return null;
|
|
580
676
|
}
|
|
581
|
-
|
|
582
677
|
evalFunctionDeclaration(node, env) {
|
|
583
|
-
|
|
678
|
+
if (!node.name || typeof node.name !== 'string') {
|
|
679
|
+
throw new RuntimeError('Function declaration requires a valid name', node, this.source);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (!Array.isArray(node.params)) {
|
|
683
|
+
throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (!node.body) {
|
|
687
|
+
throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const fn = {
|
|
691
|
+
params: node.params,
|
|
692
|
+
body: node.body,
|
|
693
|
+
env,
|
|
694
|
+
async: node.async || false
|
|
695
|
+
};
|
|
696
|
+
|
|
584
697
|
env.define(node.name, fn);
|
|
585
698
|
return null;
|
|
586
699
|
}
|
|
@@ -590,10 +703,10 @@ async evalCall(node, env) {
|
|
|
590
703
|
if (typeof calleeEvaluated === 'function') {
|
|
591
704
|
const args = [];
|
|
592
705
|
for (const a of node.arguments) args.push(await this.evaluate(a, env));
|
|
593
|
-
return calleeEvaluated(...args);
|
|
706
|
+
return await calleeEvaluated(...args);
|
|
594
707
|
}
|
|
595
708
|
if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
|
|
596
|
-
throw new
|
|
709
|
+
throw new RuntimeError('Call to non-function', node, this.source);
|
|
597
710
|
}
|
|
598
711
|
|
|
599
712
|
const fn = calleeEvaluated;
|
|
@@ -606,7 +719,7 @@ async evalCall(node, env) {
|
|
|
606
719
|
|
|
607
720
|
try {
|
|
608
721
|
const result = await this.evaluate(fn.body, callEnv);
|
|
609
|
-
return
|
|
722
|
+
return result;
|
|
610
723
|
} catch (e) {
|
|
611
724
|
if (e instanceof ReturnValue) return e.value;
|
|
612
725
|
throw e;
|
|
@@ -617,12 +730,13 @@ async evalIndex(node, env) {
|
|
|
617
730
|
const obj = await this.evaluate(node.object, env);
|
|
618
731
|
const idx = await this.evaluate(node.indexer, env);
|
|
619
732
|
|
|
620
|
-
if (obj == null) throw new
|
|
733
|
+
if (obj == null) throw new RuntimeError('Indexing null or undefined', node, this.source);
|
|
734
|
+
|
|
621
735
|
if (Array.isArray(obj) && (idx < 0 || idx >= obj.length)) {
|
|
622
|
-
throw new
|
|
736
|
+
throw new RuntimeError('Array index out of bounds', node, this.source);
|
|
623
737
|
}
|
|
624
738
|
if (typeof obj === 'object' && !(idx in obj)) {
|
|
625
|
-
throw new
|
|
739
|
+
throw new RuntimeError(`Property '${idx}' does not exist`, node, this.source);
|
|
626
740
|
}
|
|
627
741
|
|
|
628
742
|
return obj[idx];
|
|
@@ -630,14 +744,22 @@ async evalIndex(node, env) {
|
|
|
630
744
|
|
|
631
745
|
async evalObject(node, env) {
|
|
632
746
|
const out = {};
|
|
633
|
-
for (const p of node.props)
|
|
747
|
+
for (const p of node.props) {
|
|
748
|
+
if (!p.key || !p.value) {
|
|
749
|
+
throw new RuntimeError('Invalid object property', node, this.source);
|
|
750
|
+
}
|
|
751
|
+
const key = await this.evaluate(p.key, env);
|
|
752
|
+
const value = await this.evaluate(p.value, env);
|
|
753
|
+
out[key] = value;
|
|
754
|
+
}
|
|
634
755
|
return out;
|
|
635
756
|
}
|
|
636
757
|
|
|
758
|
+
|
|
637
759
|
async evalMember(node, env) {
|
|
638
760
|
const obj = await this.evaluate(node.object, env);
|
|
639
|
-
|
|
640
|
-
|
|
761
|
+
if (obj == null) throw new RuntimeError('Member access of null or undefined', node, this.source);
|
|
762
|
+
if (!(node.property in obj)) throw new RuntimeError(`Property '${node.property}' does not exist`, node, this.source);
|
|
641
763
|
return obj[node.property];
|
|
642
764
|
}
|
|
643
765
|
|
|
@@ -647,7 +769,8 @@ async evalUpdate(node, env) {
|
|
|
647
769
|
if (arg.type === 'Identifier') return env.get(arg.name);
|
|
648
770
|
if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
|
|
649
771
|
if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
|
|
650
|
-
throw new
|
|
772
|
+
throw new RuntimeError('Invalid update target', node, this.source);
|
|
773
|
+
|
|
651
774
|
};
|
|
652
775
|
const setValue = async (v) => {
|
|
653
776
|
if (arg.type === 'Identifier') env.set(arg.name, v);
|
package/src/lexer.js
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
|
+
class LexerError extends Error {
|
|
2
|
+
constructor(message, line, column, source = '') {
|
|
3
|
+
let output = `SyntaxError: ${message}\n`;
|
|
4
|
+
output += ` at line ${line}, column ${column}\n`;
|
|
5
|
+
|
|
6
|
+
if (source && line != null) {
|
|
7
|
+
const lines = source.split('\n');
|
|
8
|
+
const srcLine = lines[line - 1] || '';
|
|
9
|
+
output += ` ${srcLine}\n`;
|
|
10
|
+
output += ` ${' '.repeat(column - 1)}^\n`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
super(output);
|
|
14
|
+
this.name = 'SyntaxError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
1
18
|
class Lexer {
|
|
2
19
|
constructor(input) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
this.input = input;
|
|
21
|
+
this.source = input;
|
|
22
|
+
this.pos = 0;
|
|
23
|
+
this.currentChar = input[0] || null;
|
|
24
|
+
this.line = 1;
|
|
25
|
+
this.column = 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
|
|
10
29
|
advance() {
|
|
11
30
|
if (this.currentChar === '\n') {
|
|
@@ -23,8 +42,14 @@ class Lexer {
|
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
error(msg) {
|
|
26
|
-
|
|
27
|
-
|
|
45
|
+
throw new LexerError(
|
|
46
|
+
msg,
|
|
47
|
+
this.line,
|
|
48
|
+
this.column,
|
|
49
|
+
this.source
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
|
|
29
54
|
skipWhitespace() {
|
|
30
55
|
while (this.currentChar && /\s/.test(this.currentChar)) this.advance();
|