starlight-cli 1.1.2 → 1.1.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/src/evaluator.js CHANGED
@@ -49,7 +49,6 @@ class Environment {
49
49
  if (name in this.store) return this.store[name];
50
50
  if (this.parent) return this.parent.get(name, node, source);
51
51
 
52
- // Only suggest for top-level variable/function names
53
52
  let suggestion = null;
54
53
  if (node && source) {
55
54
  suggestion = this.suggest?.(name, this) || null;
@@ -84,7 +83,6 @@ class Evaluator {
84
83
  this.setupBuiltins();
85
84
  }
86
85
  suggest(name, env) {
87
- // Collect all variable/function names from the environment chain
88
86
  const names = new Set();
89
87
  let current = env;
90
88
  while (current) {
@@ -98,11 +96,10 @@ suggest(name, env) {
98
96
  let bestScore = Infinity;
99
97
 
100
98
  for (const item of names) {
101
- // simple edit distance approximation
102
99
  const dist = Math.abs(item.length - name.length) +
103
100
  [...name].filter((c, i) => c !== item[i]).length;
104
101
 
105
- if (dist < bestScore && dist <= 2) { // max distance 2
102
+ if (dist < bestScore && dist <= 2) {
106
103
  bestScore = dist;
107
104
  best = item;
108
105
  }
@@ -114,39 +111,31 @@ suggest(name, env) {
114
111
  formatValue(value, seen = new Set()) {
115
112
  const color = require('starlight-color');
116
113
 
117
- // Handle circular references
118
114
  if (typeof value === 'object' && value !== null) {
119
115
  if (seen.has(value)) return color.red('[Circular]');
120
116
  seen.add(value);
121
117
  }
122
118
 
123
- // Python-style null / undefined
124
119
  if (value === null) return color.yellow('None');
125
120
  if (value === undefined) return color.yellow('undefined');
126
121
 
127
122
  const t = typeof value;
128
123
 
129
- // Strings (no quotes)
130
124
  if (t === 'string') return color.cyan(value);
131
125
 
132
- // Numbers
133
126
  if (t === 'number') return color.green(String(value));
134
127
 
135
- // Booleans
136
128
  if (t === 'boolean') return color.yellow(value ? 'true' : 'false');
137
129
 
138
- // Native JS functions
139
130
  if (t === 'function') {
140
131
  return color.magenta(value.name ? `<function ${value.name}>` : '<function>');
141
132
  }
142
133
 
143
- // Arrays
144
134
  if (Array.isArray(value)) {
145
135
  const items = value.map(v => this.formatValue(v, seen));
146
136
  return color.white('[ ' + items.join(', ') + ' ]');
147
137
  }
148
138
 
149
- // Objects (including user-defined functions)
150
139
  if (t === 'object') {
151
140
  if (value.params && value.body) {
152
141
  return color.magenta(value.name ? `<function ${value.name}>` : '<function>');
@@ -158,7 +147,6 @@ formatValue(value, seen = new Set()) {
158
147
  return color.magenta('{ ') + entries.join(color.magenta(', ')) + color.magenta(' }');
159
148
  }
160
149
 
161
- // Fallback
162
150
  try {
163
151
  return String(value);
164
152
  } catch {
@@ -238,7 +226,6 @@ this.global.define('fetch', async (url, options = {}) => {
238
226
  };
239
227
  });
240
228
 
241
- // GET request
242
229
  this.global.define('get', async (url) => {
243
230
  const res = await fetch(url);
244
231
  return await res.json();
@@ -285,6 +272,11 @@ case 'ForInStatement':
285
272
  return await this.evalFor(node, env);
286
273
  case 'DoTrackStatement':
287
274
  return await this.evalDoTrack(node, env);
275
+ case 'StartStatement':
276
+ return await this.evalStartStatement(node, env);
277
+
278
+ case 'RaceClause':
279
+ return await this.evalRaceClause(node, env);
288
280
 
289
281
  case 'BreakStatement': throw new BreakSignal();
290
282
  case 'ContinueStatement': throw new ContinueSignal();
@@ -313,14 +305,14 @@ case 'DoTrackStatement':
313
305
  const callee = await this.evaluate(node.callee, env);
314
306
 
315
307
  if (typeof callee === 'object' && callee.body) {
316
- const evaluator = this; // <- capture the current evaluator
308
+ const evaluator = this;
317
309
  const Constructor = function(...args) {
318
310
  const newEnv = new Environment(callee.env);
319
311
  newEnv.define('this', this);
320
312
  for (let i = 0; i < callee.params.length; i++) {
321
313
  newEnv.define(callee.params[i], args[i]);
322
314
  }
323
- return evaluator.evaluate(callee.body, newEnv); // use captured evaluator
315
+ return evaluator.evaluate(callee.body, newEnv);
324
316
  };
325
317
 
326
318
  const args = [];
@@ -358,13 +350,78 @@ async evalProgram(node, env) {
358
350
  return result;
359
351
  }
360
352
 
353
+ async evalStartStatement(node, env) {
354
+ try {
355
+ const value = await this.evaluate(node.discriminant, env);
356
+ let executing = false;
357
+ for (const c of node.cases) {
358
+ try {
359
+ if (!executing) {
360
+ const testValue = await this.evaluate(c.test, env);
361
+ if (testValue === value) executing = true;
362
+ }
363
+
364
+ if (executing) {
365
+ await this.evaluate(c.consequent, new Environment(env));
366
+ }
367
+ } catch (caseErr) {
368
+ if (caseErr instanceof BreakSignal) break;
369
+ if (caseErr instanceof RuntimeError ||
370
+ caseErr instanceof ReturnValue ||
371
+ caseErr instanceof ContinueSignal) {
372
+ throw caseErr; // propagate signals
373
+ }
374
+
375
+ throw new RuntimeError(
376
+ caseErr.message || 'Error evaluating case in start statement',
377
+ c,
378
+ this.source
379
+ );
380
+ }
381
+ }
382
+
383
+ return null;
384
+ } catch (err) {
385
+ if (err instanceof RuntimeError ||
386
+ err instanceof ReturnValue ||
387
+ err instanceof BreakSignal ||
388
+ err instanceof ContinueSignal) {
389
+ throw err;
390
+ }
391
+ throw new RuntimeError(
392
+ err.message || 'Error evaluating start statement',
393
+ node,
394
+ this.source
395
+ );
396
+ }
397
+ }
398
+
399
+
400
+ async evalRaceClause(node, env) {
401
+ try {
402
+ const testValue = await this.evaluate(node.test, env);
403
+ const result = await this.evaluate(node.consequent, new Environment(env));
404
+ return { testValue, result };
405
+ } catch (err) {
406
+ if (err instanceof RuntimeError ||
407
+ err instanceof ReturnValue ||
408
+ err instanceof BreakSignal ||
409
+ err instanceof ContinueSignal) {
410
+ throw err;
411
+ }
412
+ throw new RuntimeError(
413
+ err.message || 'Error evaluating race clause',
414
+ node,
415
+ this.source
416
+ );
417
+ }
418
+ }
361
419
 
362
420
  async evalDoTrack(node, env) {
363
421
  try {
364
422
  return await this.evaluate(node.body, env);
365
423
  } catch (err) {
366
424
  if (!node.handler) {
367
- // Wrap any raw error into RuntimeError with line info
368
425
  if (err instanceof RuntimeError) throw err;
369
426
  throw new RuntimeError(err.message || 'Error in doTrack body', node.body, this.source);
370
427
  }
@@ -375,7 +432,6 @@ async evalDoTrack(node, env) {
375
432
  try {
376
433
  return await this.evaluate(node.handler, trackEnv);
377
434
  } catch (handlerErr) {
378
- // Wrap handler errors as well
379
435
  if (handlerErr instanceof RuntimeError) throw handlerErr;
380
436
  throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler, this.source);
381
437
  }
@@ -475,16 +531,33 @@ evalArrowFunction(node, env) {
475
531
  if (!node.body) {
476
532
  throw new RuntimeError('Arrow function missing body', node, this.source);
477
533
  }
534
+
478
535
  if (!Array.isArray(node.params)) {
479
536
  throw new RuntimeError('Invalid arrow function parameters', node, this.source);
480
537
  }
481
538
 
482
- return {
483
- params: node.params,
484
- body: node.body,
485
- env: env,
486
- arrow: true,
487
- async: node.async || false
539
+ const evaluator = this;
540
+
541
+ return async function (...args) {
542
+ const localEnv = new Environment(env);
543
+
544
+ node.params.forEach((p, i) => {
545
+ localEnv.define(p.name, args[i]);
546
+ });
547
+
548
+ try {
549
+ if (node.isBlock) {
550
+ const result = await evaluator.evaluate(node.body, localEnv);
551
+ return result ?? null;
552
+ } else {
553
+ return await evaluator.evaluate(node.body, localEnv);
554
+ }
555
+ } catch (err) {
556
+ if (err instanceof ReturnValue) {
557
+ return err.value;
558
+ }
559
+ throw err;
560
+ }
488
561
  };
489
562
  }
490
563
 
@@ -659,7 +732,6 @@ async evalWhile(node, env) {
659
732
  return null;
660
733
  }
661
734
  async evalFor(node, env) {
662
- // Python-style: for x in iterable (with optional 'let')
663
735
  if (node.type === 'ForInStatement') {
664
736
  const iterable = await this.evaluate(node.iterable, env);
665
737
 
@@ -669,8 +741,6 @@ async evalFor(node, env) {
669
741
 
670
742
  const loopVar = node.variable; // string name of the loop variable
671
743
  const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
672
-
673
- // Arrays: iterate over elements
674
744
  if (Array.isArray(iterable)) {
675
745
  for (const value of iterable) {
676
746
  const loopEnv = createLoopEnv();
@@ -685,7 +755,6 @@ async evalFor(node, env) {
685
755
  }
686
756
  }
687
757
  }
688
- // Objects: iterate over keys
689
758
  else {
690
759
  for (const key of Object.keys(iterable)) {
691
760
  const loopEnv = createLoopEnv();
@@ -704,7 +773,6 @@ async evalFor(node, env) {
704
773
  return null;
705
774
  }
706
775
 
707
- // C-style for loop (classic JS style)
708
776
  const local = new Environment(env);
709
777
 
710
778
  if (node.init) await this.evaluate(node.init, local);
@@ -790,19 +858,14 @@ async evalIndex(node, env) {
790
858
  const idx = await this.evaluate(node.indexer, env);
791
859
 
792
860
  if (obj == null) throw new RuntimeError('Cannot index null or undefined', node, this.source);
793
-
794
- // Array access: return undefined if out of bounds
795
861
  if (Array.isArray(obj)) {
796
862
  if (idx < 0 || idx >= obj.length) return undefined;
797
863
  return obj[idx];
798
864
  }
799
-
800
- // Object access: return undefined if property missing
801
865
  if (typeof obj === 'object') {
802
866
  return obj[idx]; // undefined if missing
803
867
  }
804
868
 
805
- // Fallback for non-object non-array
806
869
  return undefined;
807
870
  }
808
871
  async evalObject(node, env) {
package/src/lexer.js CHANGED
@@ -28,7 +28,7 @@ class Lexer {
28
28
  'break', 'continue', 'func', 'return',
29
29
  'true', 'false', 'null',
30
30
  'ask', 'define', 'import', 'from', 'as',
31
- 'async', 'await', 'new', 'in', 'do', 'track'
31
+ 'async', 'await', 'new', 'in', 'do', 'track', 'start', 'race'
32
32
  ];
33
33
  }
34
34
 
package/src/parser.js CHANGED
@@ -73,6 +73,7 @@ class Parser {
73
73
  case 'WHILE': return this.whileStatement();
74
74
  case 'FOR': return this.forStatement();
75
75
  case 'DO': return this.doTrackStatement();
76
+ case 'START': return this.startStatement();
76
77
  case 'BREAK': return this.breakStatement();
77
78
  case 'CONTINUE': return this.continueStatement();
78
79
  case 'FUNC': return this.funcDeclaration();
@@ -84,9 +85,9 @@ class Parser {
84
85
  }
85
86
  }
86
87
  varDeclaration() {
87
- const t = this.current; // LET token
88
+ const t = this.current;
88
89
  this.eat('LET');
89
- const idToken = this.current; // identifier token
90
+ const idToken = this.current;
90
91
  const id = idToken.value;
91
92
  this.eat('IDENTIFIER');
92
93
 
@@ -96,7 +97,6 @@ varDeclaration() {
96
97
  expr = this.expression();
97
98
  }
98
99
 
99
- // semicolon optional
100
100
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
101
101
 
102
102
  return {
@@ -107,9 +107,49 @@ varDeclaration() {
107
107
  column: t.column
108
108
  };
109
109
  }
110
+ startStatement() {
111
+ const t = this.current;
112
+ this.eat('START');
113
+
114
+ const discriminant = this.expression();
115
+
116
+ this.eat('LBRACE');
117
+
118
+ const cases = [];
119
+
120
+ while (this.current.type === 'RACE') {
121
+ cases.push(this.raceClause());
122
+ }
123
+
124
+ this.eat('RBRACE');
125
+
126
+ return {
127
+ type: 'StartStatement',
128
+ discriminant,
129
+ cases,
130
+ line: t.line,
131
+ column: t.column
132
+ };
133
+ }
134
+ raceClause() {
135
+ const t = this.current;
136
+ this.eat('RACE');
137
+
138
+ const test = this.expression();
139
+
140
+ const consequent = this.block();
141
+
142
+ return {
143
+ type: 'RaceClause',
144
+ test,
145
+ consequent,
146
+ line: t.line,
147
+ column: t.column
148
+ };
149
+ }
110
150
 
111
151
  sldeployStatement() {
112
- const t = this.current; // SLDEPLOY token
152
+ const t = this.current;
113
153
  this.eat('SLDEPLOY');
114
154
  const expr = this.expression();
115
155
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
@@ -122,7 +162,7 @@ sldeployStatement() {
122
162
  }
123
163
 
124
164
  doTrackStatement() {
125
- const t = this.current; // DO token
165
+ const t = this.current;
126
166
  this.eat('DO');
127
167
 
128
168
  const body = this.block();
@@ -143,9 +183,9 @@ doTrackStatement() {
143
183
  }
144
184
 
145
185
  defineStatement() {
146
- const t = this.current; // DEFINE token
186
+ const t = this.current;
147
187
  this.eat('DEFINE');
148
- const idToken = this.current; // identifier token
188
+ const idToken = this.current;
149
189
  const id = idToken.value;
150
190
  this.eat('IDENTIFIER');
151
191
 
@@ -167,7 +207,7 @@ defineStatement() {
167
207
  }
168
208
 
169
209
  asyncFuncDeclaration() {
170
- const t = this.current; // first token of function (identifier)
210
+ const t = this.current;
171
211
  const name = this.current.value;
172
212
  this.eat('IDENTIFIER');
173
213
 
@@ -185,7 +225,6 @@ asyncFuncDeclaration() {
185
225
  }
186
226
  this.eat('RPAREN');
187
227
  } else {
188
- // no parentheses: single param as Python style
189
228
  if (this.current.type === 'IDENTIFIER') {
190
229
  params.push(this.current.value);
191
230
  this.eat('IDENTIFIER');
@@ -205,7 +244,7 @@ asyncFuncDeclaration() {
205
244
  }
206
245
 
207
246
  ifStatement() {
208
- const t = this.current; // IF token
247
+ const t = this.current;
209
248
  this.eat('IF');
210
249
 
211
250
  let test;
@@ -214,7 +253,6 @@ ifStatement() {
214
253
  test = this.expression();
215
254
  this.eat('RPAREN');
216
255
  } else {
217
- // Python style: no parentheses
218
256
  test = this.expression();
219
257
  }
220
258
 
@@ -238,7 +276,7 @@ ifStatement() {
238
276
  }
239
277
 
240
278
  whileStatement() {
241
- const t = this.current; // WHILE token
279
+ const t = this.current;
242
280
  this.eat('WHILE');
243
281
 
244
282
  let test;
@@ -261,7 +299,7 @@ whileStatement() {
261
299
  }
262
300
 
263
301
  importStatement() {
264
- const t = this.current; // IMPORT token
302
+ const t = this.current;
265
303
  this.eat('IMPORT');
266
304
 
267
305
  let specifiers = [];
@@ -320,7 +358,6 @@ forStatement() {
320
358
  const t = this.current; // FOR token
321
359
  this.eat('FOR');
322
360
 
323
- // --- Python-style: for variable in iterable (supports optional 'let') ---
324
361
  if ((this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
325
362
  (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')) {
326
363
 
@@ -361,7 +398,6 @@ forStatement() {
361
398
  };
362
399
  }
363
400
 
364
- // --- C-style: for(init; test; update) ---
365
401
  let init = null;
366
402
  let test = null;
367
403
  let update = null;
@@ -369,22 +405,18 @@ forStatement() {
369
405
  if (this.current.type === 'LPAREN') {
370
406
  this.eat('LPAREN');
371
407
 
372
- // init
373
408
  if (this.current.type !== 'SEMICOLON') {
374
409
  init = this.current.type === 'LET' ? this.varDeclaration() : this.expressionStatement();
375
410
  } else {
376
411
  this.eat('SEMICOLON');
377
412
  }
378
413
 
379
- // test
380
414
  if (this.current.type !== 'SEMICOLON') test = this.expression();
381
415
  this.eat('SEMICOLON');
382
416
 
383
- // update
384
417
  if (this.current.type !== 'RPAREN') update = this.expression();
385
418
  this.eat('RPAREN');
386
419
  } else {
387
- // fallback: single expression (rare, mostly handled above)
388
420
  init = this.expression();
389
421
  if (this.current.type === 'IN') {
390
422
  this.eat('IN');
@@ -407,21 +439,19 @@ forStatement() {
407
439
  breakStatement() {
408
440
  const t = this.current; // BREAK token
409
441
  this.eat('BREAK');
410
- // Python-style: no semicolon needed, ignore if present
411
442
  if (this.current.type === 'SEMICOLON') this.advance();
412
443
  return { type: 'BreakStatement', line: t.line, column: t.column };
413
444
  }
414
445
 
415
446
  continueStatement() {
416
- const t = this.current; // CONTINUE token
447
+ const t = this.current;
417
448
  this.eat('CONTINUE');
418
- // Python-style: no semicolon needed, ignore if present
419
449
  if (this.current.type === 'SEMICOLON') this.advance();
420
450
  return { type: 'ContinueStatement', line: t.line, column: t.column };
421
451
  }
422
452
 
423
453
  funcDeclaration() {
424
- const t = this.current; // FUNC token
454
+ const t = this.current;
425
455
  this.eat('FUNC');
426
456
  const nameToken = this.current;
427
457
  const name = nameToken.value;
@@ -443,7 +473,6 @@ funcDeclaration() {
443
473
  }
444
474
  this.eat('RPAREN');
445
475
  } else {
446
- // Python-style: single param without parentheses
447
476
  if (this.current.type === 'IDENTIFIER') {
448
477
  const paramToken = this.current;
449
478
  params.push({ name: paramToken.value, line: paramToken.line, column: paramToken.column });
@@ -464,7 +493,6 @@ returnStatement() {
464
493
  argument = this.expression();
465
494
  }
466
495
 
467
- // semicolon optional
468
496
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
469
497
 
470
498
  return { type: 'ReturnStatement', argument, line: t.line, column: t.column };
@@ -482,9 +510,8 @@ block() {
482
510
  }
483
511
 
484
512
  expressionStatement() {
485
- const exprToken = this.current; // first token of the expression
513
+ const exprToken = this.current;
486
514
  const expr = this.expression();
487
- // semicolon optional
488
515
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
489
516
  return { type: 'ExpressionStatement', expression: expr, line: exprToken.line, column: exprToken.column };
490
517
  }
@@ -597,7 +624,6 @@ unary() {
597
624
  };
598
625
  }
599
626
 
600
- // Python-like: ignore ++ and -- if not used
601
627
  if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
602
628
  const op = t.type;
603
629
  this.eat(op);
@@ -637,7 +663,7 @@ postfix() {
637
663
  const args = [];
638
664
  while (this.current.type !== 'RPAREN' && this.current.type !== 'EOF') {
639
665
  args.push(this.expression());
640
- if (this.current.type === 'COMMA') this.eat('COMMA'); // optional comma
666
+ if (this.current.type === 'COMMA') this.eat('COMMA');
641
667
  }
642
668
  if (this.current.type === 'RPAREN') this.eat('RPAREN');
643
669
  node = { type: 'CallExpression', callee: node, arguments: args, line: startLine, column: startCol };
@@ -674,15 +700,26 @@ postfix() {
674
700
 
675
701
  arrowFunction(params) {
676
702
  const t = this.current;
677
- if (t.type === 'ARROW') this.eat('ARROW');
678
- const body = this.expression();
703
+ this.eat('ARROW');
704
+
705
+ let body;
706
+ let isBlock = false;
707
+
708
+ if (this.current.type === 'LBRACE') {
709
+ body = this.block();
710
+ isBlock = true;
711
+ } else {
712
+ body = this.expression();
713
+ }
714
+
679
715
  const startLine = params.length > 0 ? params[0].line : t.line;
680
- const startCol = params.length > 0 ? params[0].column : t.column;
716
+ const startCol = params.length > 0 ? params[0].column : t.column;
681
717
 
682
718
  return {
683
719
  type: 'ArrowFunctionExpression',
684
720
  params,
685
721
  body,
722
+ isBlock,
686
723
  line: startLine,
687
724
  column: startCol
688
725
  };
@@ -814,11 +851,11 @@ arrowFunction(params) {
814
851
  this.eat('LBRACE');
815
852
  const props = [];
816
853
  while (this.current.type !== 'RBRACE') {
817
- const key = this.expression(); // Flexible key: can be any expression
854
+ const key = this.expression();
818
855
  this.eat('COLON');
819
856
  const value = this.expression();
820
857
  props.push({ key, value });
821
- if (this.current.type === 'COMMA') this.eat('COMMA'); // optional trailing comma
858
+ if (this.current.type === 'COMMA') this.eat('COMMA');
822
859
  }
823
860
  this.eat('RBRACE');
824
861
  return { type: 'ObjectExpression', props, line: startLine, column: startCol };
package/src/starlight.js CHANGED
@@ -9,7 +9,7 @@ const Lexer = require('./lexer');
9
9
  const Parser = require('./parser');
10
10
  const Evaluator = require('./evaluator');
11
11
 
12
- const VERSION = '1.1.2';
12
+ const VERSION = '1.1.4';
13
13
 
14
14
  const COLOR = {
15
15
  reset: '\x1b[0m',