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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-cli",
3
- "version": "1.0.46",
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
@@ -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
- if (name in this.store) return this.store[name];
27
- if (this.parent) return this.parent.get(name);
28
- throw new Error(`Undefined variable: ${name}`);
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 Error('range() expects 1 to 3 arguments');
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 Error('range() step cannot be 0');
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 Error('Cannot convert value to number');
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
- const out = {};
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 Error('NewExpression callee is not a function');
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
- throw new Error(`Unknown node type in evaluator: ${node.type}`);
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
- 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
+ }
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) throw err;
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
- return await this.evaluate(node.handler, trackEnv);
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 Error(`Import not found: ${spec}`);
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 Error(`Module '${spec}' has no export '${imp.imported}'`);
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
- throw e;
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
- const obj = await this.evaluate(left.object, env);
391
- obj[left.property] = rightVal;
392
- return rightVal;
393
- }
394
- if (left.type === 'IndexExpression') {
395
- const obj = await this.evaluate(left.object, env);
396
- const idx = await this.evaluate(left.indexer, env);
397
- obj[idx] = rightVal;
398
- return rightVal;
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 Error('Invalid compound assignment target');
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 Error('Unknown compound operator');
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 this.global.define(node.id, val);
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 Error('Division by zero');
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 Error(`Unknown binary operator ${node.operator}`);
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 Error(`Unknown logical operator ${node.operator}`);
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 Error(`Unknown unary operator ${node.operator}`);
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
- if (test) return await this.evaluate(node.consequent, env);
493
- 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
+
494
579
  return null;
495
580
  }
496
581
 
582
+
497
583
  async evalWhile(node, env) {
498
- while (await this.evaluate(node.test, env)) {
499
- try { await this.evaluate(node.body, env); }
500
- 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) {
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 Error('Cannot iterate over non-iterable');
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
- 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
+
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 Error('Call to non-function');
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 fn.arrow ? result : result;
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 Error('Indexing null or undefined');
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 Error('Array index out of bounds');
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 Error(`Property '${idx}' does not exist`);
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) out[p.key] = await this.evaluate(p.value, env);
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
- if (obj == null) throw new Error('Member access of null or undefined');
640
- if (!(node.property in obj)) throw new Error(`Property '${node.property}' does not exist`);
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 Error('Invalid update target');
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
- this.input = input;
4
- this.pos = 0;
5
- this.currentChar = input[0] || null;
6
- this.line = 1; // current line
7
- this.column = 1; // current column
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
- throw new Error(`${msg} at line ${this.line}, column ${this.column}`);
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();