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