starlight-cli 1.0.47 → 1.0.49

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.47",
3
+ "version": "1.0.49",
4
4
  "description": "Starlight Programming Language CLI",
5
5
  "bin": {
6
6
  "starlight": "index.js"
package/src/evaluator.js CHANGED
@@ -10,14 +10,29 @@ class ReturnValue {
10
10
  class BreakSignal {}
11
11
  class ContinueSignal {}
12
12
  class RuntimeError extends Error {
13
- constructor(message, node) {
14
- const line = node?.line ?? '?';
15
- const column = node?.column ?? '?';
16
- super(`RuntimeError: ${message} at line ${line}, column ${column}`);
17
- this.name = 'RuntimeError';
18
- }
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
+ }
19
32
  }
20
33
 
34
+
35
+
21
36
  class Environment {
22
37
  constructor(parent = null) {
23
38
  this.store = Object.create(null);
@@ -30,11 +45,12 @@ class Environment {
30
45
  return false;
31
46
  }
32
47
 
33
- get(name) {
34
- if (name in this.store) return this.store[name];
35
- if (this.parent) return this.parent.get(name);
36
- throw new RuntimeError(`Undefined variable: ${name}`);
37
- }
48
+ get(name, node, source) {
49
+ if (name in this.store) return this.store[name];
50
+ if (this.parent) return this.parent.get(name, node, source);
51
+ throw new RuntimeError(`Undefined variable: ${name}`, node, source);
52
+ }
53
+
38
54
 
39
55
  set(name, value) {
40
56
  if (name in this.store) { this.store[name] = value; return value; }
@@ -50,7 +66,8 @@ class Environment {
50
66
  }
51
67
 
52
68
  class Evaluator {
53
- constructor() {
69
+ constructor(source = '') {
70
+ this.source = source;
54
71
  this.global = new Environment();
55
72
  this.setupBuiltins();
56
73
  }
@@ -137,11 +154,11 @@ this.global.define('range', (...args) => {
137
154
  end = Number(args[1]);
138
155
  step = Number(args[2]);
139
156
  } else {
140
- throw new RuntimeError('range() expects 1 to 3 arguments');
157
+ throw new RuntimeError('range() expects 1 to 3 arguments', null, this.source);
141
158
  }
142
159
 
143
160
  if (step === 0) {
144
- throw new RuntimeError('range() step cannot be 0');
161
+ throw new RuntimeError('range() step cannot be 0', null, this.source);
145
162
  }
146
163
 
147
164
  const result = [];
@@ -165,7 +182,7 @@ this.global.define('range', (...args) => {
165
182
  this.global.define('num', arg => {
166
183
  const n = Number(arg);
167
184
  if (Number.isNaN(n)) {
168
- throw new RuntimeError('Cannot convert value to number');
185
+ throw new RuntimeError('Cannot convert value to number', null, this.source);
169
186
  }
170
187
  return n;
171
188
  });
@@ -218,7 +235,7 @@ async evaluate(node, env = this.global) {
218
235
  case 'LogicalExpression': return await this.evalLogical(node, env);
219
236
  case 'UnaryExpression': return await this.evalUnary(node, env);
220
237
  case 'Literal': return node.value;
221
- case 'Identifier': return env.get(node.name);
238
+ case 'Identifier': return env.get(node.name, node, this.source);
222
239
  case 'IfStatement': return await this.evalIf(node, env);
223
240
  case 'WhileStatement': return await this.evalWhile(node, env);
224
241
  case 'ForStatement':
@@ -241,14 +258,8 @@ case 'DoTrackStatement':
241
258
  case 'ArrayExpression':
242
259
  return await Promise.all(node.elements.map(el => this.evaluate(el, env)));
243
260
  case 'IndexExpression': return await this.evalIndex(node, env);
244
- case 'ObjectExpression': {
245
- const out = {};
246
- for (const p of node.props) {
247
- const key = await this.evaluate(p.key, env);
248
- out[key] = await this.evaluate(p.value, env);
249
- }
250
- return out;
251
- }
261
+ case 'ObjectExpression': return await this.evalObject(node, env);
262
+
252
263
 
253
264
  case 'MemberExpression': return await this.evalMember(node, env);
254
265
  case 'UpdateExpression': return await this.evalUpdate(node, env);
@@ -276,7 +287,7 @@ case 'DoTrackStatement':
276
287
  }
277
288
 
278
289
  if (typeof callee !== 'function') {
279
- throw new RuntimeError('NewExpression callee is not a function', node);
290
+ throw new RuntimeError('NewExpression callee is not a function', node, this.source);
280
291
  }
281
292
 
282
293
  const args = [];
@@ -285,7 +296,7 @@ case 'DoTrackStatement':
285
296
  }
286
297
 
287
298
  default:
288
- throw new RuntimeError(`Unknown node type in evaluator: ${node.type}`, node);
299
+ throw new RuntimeError(`Unknown node type in evaluator: ${node.type}`, node, this.source);
289
300
 
290
301
  }
291
302
  }
@@ -293,11 +304,19 @@ case 'DoTrackStatement':
293
304
  async evalProgram(node, env) {
294
305
  let result = null;
295
306
  for (const stmt of node.body) {
296
- result = await this.evaluate(stmt, env);
307
+ try {
308
+ result = await this.evaluate(stmt, env);
309
+ } catch (e) {
310
+ if (e instanceof RuntimeError || e instanceof BreakSignal || e instanceof ContinueSignal || e instanceof ReturnValue) {
311
+ throw e;
312
+ }
313
+ throw new RuntimeError(e.message || 'Error in program', stmt, this.source);
314
+ }
297
315
  }
298
316
  return result;
299
317
  }
300
318
 
319
+
301
320
  async evalDoTrack(node, env) {
302
321
  try {
303
322
  return await this.evaluate(node.body, env);
@@ -305,7 +324,7 @@ async evalDoTrack(node, env) {
305
324
  if (!node.handler) {
306
325
  // Wrap any raw error into RuntimeError with line info
307
326
  if (err instanceof RuntimeError) throw err;
308
- throw new RuntimeError(err.message || 'Error in doTrack body', node.body);
327
+ throw new RuntimeError(err.message || 'Error in doTrack body', node.body, this.source);
309
328
  }
310
329
 
311
330
  const trackEnv = new Environment(env);
@@ -316,7 +335,7 @@ async evalDoTrack(node, env) {
316
335
  } catch (handlerErr) {
317
336
  // Wrap handler errors as well
318
337
  if (handlerErr instanceof RuntimeError) throw handlerErr;
319
- throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler);
338
+ throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler, this.source);
320
339
  }
321
340
  }
322
341
  }
@@ -337,7 +356,7 @@ async evalImport(node, env) {
337
356
  : path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
338
357
 
339
358
  if (!fs.existsSync(fullPath)) {
340
- throw new RuntimeError(`Import not found: ${spec}`, node);
359
+ throw new RuntimeError(`Import not found: ${spec}`, node, this.source);
341
360
  }
342
361
 
343
362
  const code = fs.readFileSync(fullPath, 'utf-8');
@@ -364,7 +383,7 @@ async evalImport(node, env) {
364
383
  }
365
384
  if (imp.type === 'NamedImport') {
366
385
  if (!(imp.imported in lib)) {
367
- throw new RuntimeError(`Module '${spec}' has no export '${imp.imported}'`, node);
386
+ throw new RuntimeError(`Module '${spec}' has no export '${imp.imported}'`, node, this.source);
368
387
  }
369
388
  env.define(imp.local, lib[imp.imported]);
370
389
  }
@@ -381,7 +400,7 @@ async evalBlock(node, env) {
381
400
  } catch (e) {
382
401
  if (e instanceof ReturnValue || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
383
402
  // Wrap any other error in RuntimeError with the current block node
384
- throw new RuntimeError(e.message || 'Error in block', stmt);
403
+ throw new RuntimeError(e.message || 'Error in block', stmt, this.source);
385
404
  }
386
405
  }
387
406
  return result;
@@ -389,11 +408,23 @@ async evalBlock(node, env) {
389
408
 
390
409
 
391
410
  async evalVarDeclaration(node, env) {
411
+ if (!node.expr) {
412
+ throw new RuntimeError('Variable declaration requires an initializer', node, this.source);
413
+ }
414
+
392
415
  const val = await this.evaluate(node.expr, env);
393
416
  return env.define(node.id, val);
394
417
  }
395
418
 
419
+
396
420
  evalArrowFunction(node, env) {
421
+ if (!node.body) {
422
+ throw new RuntimeError('Arrow function missing body', node, this.source);
423
+ }
424
+ if (!Array.isArray(node.params)) {
425
+ throw new RuntimeError('Invalid arrow function parameters', node, this.source);
426
+ }
427
+
397
428
  return {
398
429
  params: node.params,
399
430
  body: node.body,
@@ -403,24 +434,28 @@ evalArrowFunction(node, env) {
403
434
  };
404
435
  }
405
436
 
437
+
406
438
  async evalAssignment(node, env) {
407
439
  const rightVal = await this.evaluate(node.right, env);
408
440
  const left = node.left;
409
441
 
410
442
  if (left.type === 'Identifier') return env.set(left.name, rightVal);
411
443
  if (left.type === 'MemberExpression') {
412
- const obj = await this.evaluate(left.object, env);
413
- obj[left.property] = rightVal;
414
- return rightVal;
415
- }
416
- if (left.type === 'IndexExpression') {
417
- const obj = await this.evaluate(left.object, env);
418
- const idx = await this.evaluate(left.indexer, env);
419
- obj[idx] = rightVal;
420
- return rightVal;
421
- }
444
+ const obj = await this.evaluate(left.object, env);
445
+ if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
446
+ obj[left.property] = rightVal;
447
+ return rightVal;
448
+ }
449
+ if (left.type === 'IndexExpression') {
450
+ const obj = await this.evaluate(left.object, env);
451
+ const idx = await this.evaluate(left.indexer, env);
452
+ if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
453
+ obj[idx] = rightVal;
454
+ return rightVal;
455
+ }
456
+
422
457
 
423
- throw new RuntimeError('Invalid assignment target', node);
458
+ throw new RuntimeError('Invalid assignment target', node, this.source);
424
459
 
425
460
  }
426
461
 
@@ -428,10 +463,10 @@ async evalCompoundAssignment(node, env) {
428
463
  const left = node.left;
429
464
  let current;
430
465
 
431
- if (left.type === 'Identifier') current = env.get(left.name);
466
+ if (left.type === 'Identifier') current = env.get(left.name, left, this.source);
432
467
  else if (left.type === 'MemberExpression') current = await this.evalMember(left, env);
433
468
  else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env);
434
- else throw new RuntimeError('Invalid compound assignment target', node);
469
+ else throw new RuntimeError('Invalid compound assignment target', node, this.source);
435
470
 
436
471
 
437
472
  const rhs = await this.evaluate(node.right, env);
@@ -442,7 +477,7 @@ async evalCompoundAssignment(node, env) {
442
477
  case 'STAREQ': computed = current * rhs; break;
443
478
  case 'SLASHEQ': computed = current / rhs; break;
444
479
  case 'MODEQ': computed = current % rhs; break;
445
- default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node);
480
+ default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node, this.source);
446
481
 
447
482
  }
448
483
 
@@ -462,21 +497,33 @@ async evalSldeploy(node, env) {
462
497
 
463
498
  async evalAsk(node, env) {
464
499
  const prompt = await this.evaluate(node.prompt, env);
500
+
501
+ if (typeof prompt !== 'string') {
502
+ throw new RuntimeError('ask() prompt must be a string', node, this.source);
503
+ }
504
+
465
505
  const input = readlineSync.question(prompt + ' ');
466
506
  return input;
467
507
  }
468
508
 
509
+
469
510
  async evalDefine(node, env) {
511
+ if (!node.id || typeof node.id !== 'string') {
512
+ throw new RuntimeError('Invalid identifier in define statement', node, this.source);
513
+ }
514
+
470
515
  const val = node.expr ? await this.evaluate(node.expr, env) : null;
471
- return this.global.define(node.id, val);
516
+ return env.define(node.id, val);
517
+
472
518
  }
473
519
 
520
+
474
521
  async evalBinary(node, env) {
475
522
  const l = await this.evaluate(node.left, env);
476
523
  const r = await this.evaluate(node.right, env);
477
524
 
478
525
  if (node.operator === 'SLASH' && r === 0) {
479
- throw new RuntimeError('Division by zero', node);
526
+ throw new RuntimeError('Division by zero', node, this.source);
480
527
  }
481
528
 
482
529
  switch (node.operator) {
@@ -491,7 +538,7 @@ async evalBinary(node, env) {
491
538
  case 'LTE': return l <= r;
492
539
  case 'GT': return l > r;
493
540
  case 'GTE': return l >= r;
494
- default: throw new RuntimeError(`Unknown binary operator: ${node.operator}`, node);
541
+ default: throw new RuntimeError(`Unknown binary operator: ${node.operator}`, node, this.source);
495
542
 
496
543
  }
497
544
  }
@@ -500,7 +547,7 @@ async evalLogical(node, env) {
500
547
  const l = await this.evaluate(node.left, env);
501
548
  if (node.operator === 'AND') return l && await this.evaluate(node.right, env);
502
549
  if (node.operator === 'OR') return l || await this.evaluate(node.right, env);
503
- throw new RuntimeError(`Unknown logical operator: ${node.operator}`, node);
550
+ throw new RuntimeError(`Unknown logical operator: ${node.operator}`, node, this.source);
504
551
 
505
552
  }
506
553
 
@@ -510,22 +557,43 @@ async evalUnary(node, env) {
510
557
  case 'NOT': return !val;
511
558
  case 'MINUS': return -val;
512
559
  case 'PLUS': return +val;
513
- default: throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node);
560
+ default: throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node, this.source);
514
561
 
515
562
  }
516
563
  }
517
564
 
518
565
  async evalIf(node, env) {
519
566
  const test = await this.evaluate(node.test, env);
520
- if (test) return await this.evaluate(node.consequent, env);
521
- if (node.alternate) return await this.evaluate(node.alternate, env);
567
+
568
+ if (typeof test !== 'boolean') {
569
+ throw new RuntimeError('If condition must evaluate to a boolean', node.test, this.source);
570
+ }
571
+
572
+ if (test) {
573
+ return await this.evaluate(node.consequent, env);
574
+ }
575
+
576
+ if (node.alternate) {
577
+ return await this.evaluate(node.alternate, env);
578
+ }
579
+
522
580
  return null;
523
581
  }
524
582
 
583
+
525
584
  async evalWhile(node, env) {
526
- while (await this.evaluate(node.test, env)) {
527
- try { await this.evaluate(node.body, env); }
528
- catch (e) {
585
+ while (true) {
586
+ const test = await this.evaluate(node.test, env);
587
+
588
+ if (typeof test !== 'boolean') {
589
+ throw new RuntimeError('While condition must evaluate to a boolean', node.test, this.source);
590
+ }
591
+
592
+ if (!test) break;
593
+
594
+ try {
595
+ await this.evaluate(node.body, env);
596
+ } catch (e) {
529
597
  if (e instanceof BreakSignal) break;
530
598
  if (e instanceof ContinueSignal) continue;
531
599
  throw e;
@@ -533,6 +601,7 @@ async evalWhile(node, env) {
533
601
  }
534
602
  return null;
535
603
  }
604
+
536
605
  async evalFor(node, env) {
537
606
  // -------------------------------
538
607
  // Python-style: for x in iterable (with optional 'let')
@@ -541,7 +610,7 @@ async evalFor(node, env) {
541
610
  const iterable = await this.evaluate(node.iterable, env);
542
611
 
543
612
  if (iterable == null || typeof iterable !== 'object') {
544
- throw new RuntimeError('Cannot iterate over non-iterable', node);
613
+ throw new RuntimeError('Cannot iterate over non-iterable', node, this.source);
545
614
  }
546
615
 
547
616
  const loopVar = node.variable; // STRING from parser
@@ -606,9 +675,26 @@ async evalFor(node, env) {
606
675
 
607
676
  return null;
608
677
  }
609
-
610
678
  evalFunctionDeclaration(node, env) {
611
- const fn = { params: node.params, body: node.body, env, async: node.async || false };
679
+ if (!node.name || typeof node.name !== 'string') {
680
+ throw new RuntimeError('Function declaration requires a valid name', node, this.source);
681
+ }
682
+
683
+ if (!Array.isArray(node.params)) {
684
+ throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source);
685
+ }
686
+
687
+ if (!node.body) {
688
+ throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source);
689
+ }
690
+
691
+ const fn = {
692
+ params: node.params,
693
+ body: node.body,
694
+ env,
695
+ async: node.async || false
696
+ };
697
+
612
698
  env.define(node.name, fn);
613
699
  return null;
614
700
  }
@@ -618,10 +704,10 @@ async evalCall(node, env) {
618
704
  if (typeof calleeEvaluated === 'function') {
619
705
  const args = [];
620
706
  for (const a of node.arguments) args.push(await this.evaluate(a, env));
621
- return calleeEvaluated(...args);
707
+ return await calleeEvaluated(...args);
622
708
  }
623
709
  if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
624
- throw new RuntimeError('Call to non-function', node);
710
+ throw new RuntimeError('Call to non-function', node, this.source);
625
711
  }
626
712
 
627
713
  const fn = calleeEvaluated;
@@ -634,7 +720,7 @@ async evalCall(node, env) {
634
720
 
635
721
  try {
636
722
  const result = await this.evaluate(fn.body, callEnv);
637
- return fn.arrow ? result : result;
723
+ return result;
638
724
  } catch (e) {
639
725
  if (e instanceof ReturnValue) return e.value;
640
726
  throw e;
@@ -645,13 +731,13 @@ async evalIndex(node, env) {
645
731
  const obj = await this.evaluate(node.object, env);
646
732
  const idx = await this.evaluate(node.indexer, env);
647
733
 
648
- if (obj == null) throw new RuntimeError('Indexing null or undefined', node);
734
+ if (obj == null) throw new RuntimeError('Indexing null or undefined', node, this.source);
649
735
 
650
736
  if (Array.isArray(obj) && (idx < 0 || idx >= obj.length)) {
651
- throw new RuntimeError('Array index out of bounds', node);
737
+ throw new RuntimeError('Array index out of bounds', node, this.source);
652
738
  }
653
739
  if (typeof obj === 'object' && !(idx in obj)) {
654
- throw new RuntimeError(`Property '${idx}' does not exist`, node);
740
+ throw new RuntimeError(`Property '${idx}' does not exist`, node, this.source);
655
741
  }
656
742
 
657
743
  return obj[idx];
@@ -661,7 +747,7 @@ async evalObject(node, env) {
661
747
  const out = {};
662
748
  for (const p of node.props) {
663
749
  if (!p.key || !p.value) {
664
- throw new RuntimeError('Invalid object property', node);
750
+ throw new RuntimeError('Invalid object property', node, this.source);
665
751
  }
666
752
  const key = await this.evaluate(p.key, env);
667
753
  const value = await this.evaluate(p.value, env);
@@ -673,18 +759,18 @@ async evalObject(node, env) {
673
759
 
674
760
  async evalMember(node, env) {
675
761
  const obj = await this.evaluate(node.object, env);
676
- if (obj == null) throw new RuntimeError('Member access of null or undefined', node);
677
- if (!(node.property in obj)) throw new RuntimeError(`Property '${node.property}' does not exist`, node);
762
+ if (obj == null) throw new RuntimeError('Member access of null or undefined', node, this.source);
763
+ if (!(node.property in obj)) throw new RuntimeError(`Property '${node.property}' does not exist`, node, this.source);
678
764
  return obj[node.property];
679
765
  }
680
766
 
681
767
  async evalUpdate(node, env) {
682
768
  const arg = node.argument;
683
769
  const getCurrent = async () => {
684
- if (arg.type === 'Identifier') return env.get(arg.name);
770
+ if (arg.type === 'Identifier') return env.get(arg.name, arg, this.source);
685
771
  if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
686
772
  if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
687
- throw new RuntimeError('Invalid update target', node);
773
+ throw new RuntimeError('Invalid update target', node, this.source);
688
774
 
689
775
  };
690
776
  const setValue = async (v) => {