starlight-cli 1.1.2 → 1.1.3

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/README.md CHANGED
@@ -237,7 +237,53 @@ server()
237
237
  ```
238
238
 
239
239
  ---
240
+ ## Start & Race Statements
240
241
 
242
+ Starlight includes a **`start` / `race`** control structure similar to JavaScript `switch`, with **fall-through behavior**. The `start` statement evaluates a discriminant expression and executes all `race` clauses **starting from the first match** until the end, unless a `break` is used.
243
+
244
+ ### Syntax
245
+
246
+ ```
247
+ start (<discriminant>) {
248
+ race (<value>) {
249
+ # body
250
+ }
251
+
252
+ race (<value>) {
253
+ # body
254
+ }
255
+
256
+ ```
257
+
258
+
259
+
260
+ - `start` – evaluates the discriminant expression.
261
+ - `race` – each clause is checked; execution starts at the first matching `race` and continues to subsequent races (fall-through).
262
+ - Optional `break` can stop execution early (if implemented).
263
+
264
+ ### Example
265
+
266
+ ```
267
+ define x = 2;
268
+
269
+ start (x) {
270
+ race (1) { sldeploy("Race 1 executed"); }
271
+ race (2) { sldeploy("Race 2 executed"); }
272
+ race (3) { sldeploy("Race 3 executed"); }
273
+ }
274
+
275
+ sldeploy("Done with start statement");
276
+
277
+ ```
278
+
279
+ **Expected Output:**
280
+
281
+ ```
282
+ Race 2 executed
283
+ Race 3 executed
284
+ Done with start statement
285
+
286
+ ```
241
287
  ## Why Starlight?
242
288
 
243
289
  Starlight is designed for developers who want:
package/dist/index.js CHANGED
@@ -1392,7 +1392,6 @@ class Environment {
1392
1392
  if (name in this.store) return this.store[name];
1393
1393
  if (this.parent) return this.parent.get(name, node, source);
1394
1394
 
1395
- // Only suggest for top-level variable/function names
1396
1395
  let suggestion = null;
1397
1396
  if (node && source) {
1398
1397
  suggestion = this.suggest?.(name, this) || null;
@@ -1427,7 +1426,6 @@ class Evaluator {
1427
1426
  this.setupBuiltins();
1428
1427
  }
1429
1428
  suggest(name, env) {
1430
- // Collect all variable/function names from the environment chain
1431
1429
  const names = new Set();
1432
1430
  let current = env;
1433
1431
  while (current) {
@@ -1441,11 +1439,10 @@ suggest(name, env) {
1441
1439
  let bestScore = Infinity;
1442
1440
 
1443
1441
  for (const item of names) {
1444
- // simple edit distance approximation
1445
1442
  const dist = Math.abs(item.length - name.length) +
1446
1443
  [...name].filter((c, i) => c !== item[i]).length;
1447
1444
 
1448
- if (dist < bestScore && dist <= 2) { // max distance 2
1445
+ if (dist < bestScore && dist <= 2) {
1449
1446
  bestScore = dist;
1450
1447
  best = item;
1451
1448
  }
@@ -1457,39 +1454,31 @@ suggest(name, env) {
1457
1454
  formatValue(value, seen = new Set()) {
1458
1455
  const color = __nccwpck_require__(55);
1459
1456
 
1460
- // Handle circular references
1461
1457
  if (typeof value === 'object' && value !== null) {
1462
1458
  if (seen.has(value)) return color.red('[Circular]');
1463
1459
  seen.add(value);
1464
1460
  }
1465
1461
 
1466
- // Python-style null / undefined
1467
1462
  if (value === null) return color.yellow('None');
1468
1463
  if (value === undefined) return color.yellow('undefined');
1469
1464
 
1470
1465
  const t = typeof value;
1471
1466
 
1472
- // Strings (no quotes)
1473
1467
  if (t === 'string') return color.cyan(value);
1474
1468
 
1475
- // Numbers
1476
1469
  if (t === 'number') return color.green(String(value));
1477
1470
 
1478
- // Booleans
1479
1471
  if (t === 'boolean') return color.yellow(value ? 'true' : 'false');
1480
1472
 
1481
- // Native JS functions
1482
1473
  if (t === 'function') {
1483
1474
  return color.magenta(value.name ? `<function ${value.name}>` : '<function>');
1484
1475
  }
1485
1476
 
1486
- // Arrays
1487
1477
  if (Array.isArray(value)) {
1488
1478
  const items = value.map(v => this.formatValue(v, seen));
1489
1479
  return color.white('[ ' + items.join(', ') + ' ]');
1490
1480
  }
1491
1481
 
1492
- // Objects (including user-defined functions)
1493
1482
  if (t === 'object') {
1494
1483
  if (value.params && value.body) {
1495
1484
  return color.magenta(value.name ? `<function ${value.name}>` : '<function>');
@@ -1501,7 +1490,6 @@ formatValue(value, seen = new Set()) {
1501
1490
  return color.magenta('{ ') + entries.join(color.magenta(', ')) + color.magenta(' }');
1502
1491
  }
1503
1492
 
1504
- // Fallback
1505
1493
  try {
1506
1494
  return String(value);
1507
1495
  } catch {
@@ -1581,7 +1569,6 @@ this.global.define('fetch', async (url, options = {}) => {
1581
1569
  };
1582
1570
  });
1583
1571
 
1584
- // GET request
1585
1572
  this.global.define('get', async (url) => {
1586
1573
  const res = await fetch(url);
1587
1574
  return await res.json();
@@ -1628,6 +1615,11 @@ case 'ForInStatement':
1628
1615
  return await this.evalFor(node, env);
1629
1616
  case 'DoTrackStatement':
1630
1617
  return await this.evalDoTrack(node, env);
1618
+ case 'StartStatement':
1619
+ return await this.evalStartStatement(node, env);
1620
+
1621
+ case 'RaceClause':
1622
+ return await this.evalRaceClause(node, env);
1631
1623
 
1632
1624
  case 'BreakStatement': throw new BreakSignal();
1633
1625
  case 'ContinueStatement': throw new ContinueSignal();
@@ -1656,14 +1648,14 @@ case 'DoTrackStatement':
1656
1648
  const callee = await this.evaluate(node.callee, env);
1657
1649
 
1658
1650
  if (typeof callee === 'object' && callee.body) {
1659
- const evaluator = this; // <- capture the current evaluator
1651
+ const evaluator = this;
1660
1652
  const Constructor = function(...args) {
1661
1653
  const newEnv = new Environment(callee.env);
1662
1654
  newEnv.define('this', this);
1663
1655
  for (let i = 0; i < callee.params.length; i++) {
1664
1656
  newEnv.define(callee.params[i], args[i]);
1665
1657
  }
1666
- return evaluator.evaluate(callee.body, newEnv); // use captured evaluator
1658
+ return evaluator.evaluate(callee.body, newEnv);
1667
1659
  };
1668
1660
 
1669
1661
  const args = [];
@@ -1701,13 +1693,78 @@ async evalProgram(node, env) {
1701
1693
  return result;
1702
1694
  }
1703
1695
 
1696
+ async evalStartStatement(node, env) {
1697
+ try {
1698
+ const value = await this.evaluate(node.discriminant, env);
1699
+ let executing = false;
1700
+ for (const c of node.cases) {
1701
+ try {
1702
+ if (!executing) {
1703
+ const testValue = await this.evaluate(c.test, env);
1704
+ if (testValue === value) executing = true;
1705
+ }
1706
+
1707
+ if (executing) {
1708
+ await this.evaluate(c.consequent, new Environment(env));
1709
+ }
1710
+ } catch (caseErr) {
1711
+ if (caseErr instanceof BreakSignal) break;
1712
+ if (caseErr instanceof RuntimeError ||
1713
+ caseErr instanceof ReturnValue ||
1714
+ caseErr instanceof ContinueSignal) {
1715
+ throw caseErr; // propagate signals
1716
+ }
1717
+
1718
+ throw new RuntimeError(
1719
+ caseErr.message || 'Error evaluating case in start statement',
1720
+ c,
1721
+ this.source
1722
+ );
1723
+ }
1724
+ }
1725
+
1726
+ return null;
1727
+ } catch (err) {
1728
+ if (err instanceof RuntimeError ||
1729
+ err instanceof ReturnValue ||
1730
+ err instanceof BreakSignal ||
1731
+ err instanceof ContinueSignal) {
1732
+ throw err;
1733
+ }
1734
+ throw new RuntimeError(
1735
+ err.message || 'Error evaluating start statement',
1736
+ node,
1737
+ this.source
1738
+ );
1739
+ }
1740
+ }
1741
+
1742
+
1743
+ async evalRaceClause(node, env) {
1744
+ try {
1745
+ const testValue = await this.evaluate(node.test, env);
1746
+ const result = await this.evaluate(node.consequent, new Environment(env));
1747
+ return { testValue, result };
1748
+ } catch (err) {
1749
+ if (err instanceof RuntimeError ||
1750
+ err instanceof ReturnValue ||
1751
+ err instanceof BreakSignal ||
1752
+ err instanceof ContinueSignal) {
1753
+ throw err;
1754
+ }
1755
+ throw new RuntimeError(
1756
+ err.message || 'Error evaluating race clause',
1757
+ node,
1758
+ this.source
1759
+ );
1760
+ }
1761
+ }
1704
1762
 
1705
1763
  async evalDoTrack(node, env) {
1706
1764
  try {
1707
1765
  return await this.evaluate(node.body, env);
1708
1766
  } catch (err) {
1709
1767
  if (!node.handler) {
1710
- // Wrap any raw error into RuntimeError with line info
1711
1768
  if (err instanceof RuntimeError) throw err;
1712
1769
  throw new RuntimeError(err.message || 'Error in doTrack body', node.body, this.source);
1713
1770
  }
@@ -1718,7 +1775,6 @@ async evalDoTrack(node, env) {
1718
1775
  try {
1719
1776
  return await this.evaluate(node.handler, trackEnv);
1720
1777
  } catch (handlerErr) {
1721
- // Wrap handler errors as well
1722
1778
  if (handlerErr instanceof RuntimeError) throw handlerErr;
1723
1779
  throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler, this.source);
1724
1780
  }
@@ -2002,7 +2058,6 @@ async evalWhile(node, env) {
2002
2058
  return null;
2003
2059
  }
2004
2060
  async evalFor(node, env) {
2005
- // Python-style: for x in iterable (with optional 'let')
2006
2061
  if (node.type === 'ForInStatement') {
2007
2062
  const iterable = await this.evaluate(node.iterable, env);
2008
2063
 
@@ -2012,8 +2067,6 @@ async evalFor(node, env) {
2012
2067
 
2013
2068
  const loopVar = node.variable; // string name of the loop variable
2014
2069
  const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
2015
-
2016
- // Arrays: iterate over elements
2017
2070
  if (Array.isArray(iterable)) {
2018
2071
  for (const value of iterable) {
2019
2072
  const loopEnv = createLoopEnv();
@@ -2028,7 +2081,6 @@ async evalFor(node, env) {
2028
2081
  }
2029
2082
  }
2030
2083
  }
2031
- // Objects: iterate over keys
2032
2084
  else {
2033
2085
  for (const key of Object.keys(iterable)) {
2034
2086
  const loopEnv = createLoopEnv();
@@ -2047,7 +2099,6 @@ async evalFor(node, env) {
2047
2099
  return null;
2048
2100
  }
2049
2101
 
2050
- // C-style for loop (classic JS style)
2051
2102
  const local = new Environment(env);
2052
2103
 
2053
2104
  if (node.init) await this.evaluate(node.init, local);
@@ -2133,19 +2184,14 @@ async evalIndex(node, env) {
2133
2184
  const idx = await this.evaluate(node.indexer, env);
2134
2185
 
2135
2186
  if (obj == null) throw new RuntimeError('Cannot index null or undefined', node, this.source);
2136
-
2137
- // Array access: return undefined if out of bounds
2138
2187
  if (Array.isArray(obj)) {
2139
2188
  if (idx < 0 || idx >= obj.length) return undefined;
2140
2189
  return obj[idx];
2141
2190
  }
2142
-
2143
- // Object access: return undefined if property missing
2144
2191
  if (typeof obj === 'object') {
2145
2192
  return obj[idx]; // undefined if missing
2146
2193
  }
2147
2194
 
2148
- // Fallback for non-object non-array
2149
2195
  return undefined;
2150
2196
  }
2151
2197
  async evalObject(node, env) {
@@ -2239,7 +2285,7 @@ class Lexer {
2239
2285
  'break', 'continue', 'func', 'return',
2240
2286
  'true', 'false', 'null',
2241
2287
  'ask', 'define', 'import', 'from', 'as',
2242
- 'async', 'await', 'new', 'in', 'do', 'track'
2288
+ 'async', 'await', 'new', 'in', 'do', 'track', 'start', 'race'
2243
2289
  ];
2244
2290
  }
2245
2291
 
@@ -2502,6 +2548,7 @@ class Parser {
2502
2548
  case 'WHILE': return this.whileStatement();
2503
2549
  case 'FOR': return this.forStatement();
2504
2550
  case 'DO': return this.doTrackStatement();
2551
+ case 'START': return this.startStatement();
2505
2552
  case 'BREAK': return this.breakStatement();
2506
2553
  case 'CONTINUE': return this.continueStatement();
2507
2554
  case 'FUNC': return this.funcDeclaration();
@@ -2513,9 +2560,9 @@ class Parser {
2513
2560
  }
2514
2561
  }
2515
2562
  varDeclaration() {
2516
- const t = this.current; // LET token
2563
+ const t = this.current;
2517
2564
  this.eat('LET');
2518
- const idToken = this.current; // identifier token
2565
+ const idToken = this.current;
2519
2566
  const id = idToken.value;
2520
2567
  this.eat('IDENTIFIER');
2521
2568
 
@@ -2525,7 +2572,6 @@ varDeclaration() {
2525
2572
  expr = this.expression();
2526
2573
  }
2527
2574
 
2528
- // semicolon optional
2529
2575
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
2530
2576
 
2531
2577
  return {
@@ -2536,9 +2582,49 @@ varDeclaration() {
2536
2582
  column: t.column
2537
2583
  };
2538
2584
  }
2585
+ startStatement() {
2586
+ const t = this.current;
2587
+ this.eat('START');
2588
+
2589
+ const discriminant = this.expression();
2590
+
2591
+ this.eat('LBRACE');
2592
+
2593
+ const cases = [];
2594
+
2595
+ while (this.current.type === 'RACE') {
2596
+ cases.push(this.raceClause());
2597
+ }
2598
+
2599
+ this.eat('RBRACE');
2600
+
2601
+ return {
2602
+ type: 'StartStatement',
2603
+ discriminant,
2604
+ cases,
2605
+ line: t.line,
2606
+ column: t.column
2607
+ };
2608
+ }
2609
+ raceClause() {
2610
+ const t = this.current;
2611
+ this.eat('RACE');
2612
+
2613
+ const test = this.expression();
2614
+
2615
+ const consequent = this.block();
2616
+
2617
+ return {
2618
+ type: 'RaceClause',
2619
+ test,
2620
+ consequent,
2621
+ line: t.line,
2622
+ column: t.column
2623
+ };
2624
+ }
2539
2625
 
2540
2626
  sldeployStatement() {
2541
- const t = this.current; // SLDEPLOY token
2627
+ const t = this.current;
2542
2628
  this.eat('SLDEPLOY');
2543
2629
  const expr = this.expression();
2544
2630
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
@@ -2551,7 +2637,7 @@ sldeployStatement() {
2551
2637
  }
2552
2638
 
2553
2639
  doTrackStatement() {
2554
- const t = this.current; // DO token
2640
+ const t = this.current;
2555
2641
  this.eat('DO');
2556
2642
 
2557
2643
  const body = this.block();
@@ -2572,9 +2658,9 @@ doTrackStatement() {
2572
2658
  }
2573
2659
 
2574
2660
  defineStatement() {
2575
- const t = this.current; // DEFINE token
2661
+ const t = this.current;
2576
2662
  this.eat('DEFINE');
2577
- const idToken = this.current; // identifier token
2663
+ const idToken = this.current;
2578
2664
  const id = idToken.value;
2579
2665
  this.eat('IDENTIFIER');
2580
2666
 
@@ -2596,7 +2682,7 @@ defineStatement() {
2596
2682
  }
2597
2683
 
2598
2684
  asyncFuncDeclaration() {
2599
- const t = this.current; // first token of function (identifier)
2685
+ const t = this.current;
2600
2686
  const name = this.current.value;
2601
2687
  this.eat('IDENTIFIER');
2602
2688
 
@@ -2614,7 +2700,6 @@ asyncFuncDeclaration() {
2614
2700
  }
2615
2701
  this.eat('RPAREN');
2616
2702
  } else {
2617
- // no parentheses: single param as Python style
2618
2703
  if (this.current.type === 'IDENTIFIER') {
2619
2704
  params.push(this.current.value);
2620
2705
  this.eat('IDENTIFIER');
@@ -2634,7 +2719,7 @@ asyncFuncDeclaration() {
2634
2719
  }
2635
2720
 
2636
2721
  ifStatement() {
2637
- const t = this.current; // IF token
2722
+ const t = this.current;
2638
2723
  this.eat('IF');
2639
2724
 
2640
2725
  let test;
@@ -2643,7 +2728,6 @@ ifStatement() {
2643
2728
  test = this.expression();
2644
2729
  this.eat('RPAREN');
2645
2730
  } else {
2646
- // Python style: no parentheses
2647
2731
  test = this.expression();
2648
2732
  }
2649
2733
 
@@ -2667,7 +2751,7 @@ ifStatement() {
2667
2751
  }
2668
2752
 
2669
2753
  whileStatement() {
2670
- const t = this.current; // WHILE token
2754
+ const t = this.current;
2671
2755
  this.eat('WHILE');
2672
2756
 
2673
2757
  let test;
@@ -2690,7 +2774,7 @@ whileStatement() {
2690
2774
  }
2691
2775
 
2692
2776
  importStatement() {
2693
- const t = this.current; // IMPORT token
2777
+ const t = this.current;
2694
2778
  this.eat('IMPORT');
2695
2779
 
2696
2780
  let specifiers = [];
@@ -2749,7 +2833,6 @@ forStatement() {
2749
2833
  const t = this.current; // FOR token
2750
2834
  this.eat('FOR');
2751
2835
 
2752
- // --- Python-style: for variable in iterable (supports optional 'let') ---
2753
2836
  if ((this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
2754
2837
  (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')) {
2755
2838
 
@@ -2790,7 +2873,6 @@ forStatement() {
2790
2873
  };
2791
2874
  }
2792
2875
 
2793
- // --- C-style: for(init; test; update) ---
2794
2876
  let init = null;
2795
2877
  let test = null;
2796
2878
  let update = null;
@@ -2798,22 +2880,18 @@ forStatement() {
2798
2880
  if (this.current.type === 'LPAREN') {
2799
2881
  this.eat('LPAREN');
2800
2882
 
2801
- // init
2802
2883
  if (this.current.type !== 'SEMICOLON') {
2803
2884
  init = this.current.type === 'LET' ? this.varDeclaration() : this.expressionStatement();
2804
2885
  } else {
2805
2886
  this.eat('SEMICOLON');
2806
2887
  }
2807
2888
 
2808
- // test
2809
2889
  if (this.current.type !== 'SEMICOLON') test = this.expression();
2810
2890
  this.eat('SEMICOLON');
2811
2891
 
2812
- // update
2813
2892
  if (this.current.type !== 'RPAREN') update = this.expression();
2814
2893
  this.eat('RPAREN');
2815
2894
  } else {
2816
- // fallback: single expression (rare, mostly handled above)
2817
2895
  init = this.expression();
2818
2896
  if (this.current.type === 'IN') {
2819
2897
  this.eat('IN');
@@ -2836,21 +2914,19 @@ forStatement() {
2836
2914
  breakStatement() {
2837
2915
  const t = this.current; // BREAK token
2838
2916
  this.eat('BREAK');
2839
- // Python-style: no semicolon needed, ignore if present
2840
2917
  if (this.current.type === 'SEMICOLON') this.advance();
2841
2918
  return { type: 'BreakStatement', line: t.line, column: t.column };
2842
2919
  }
2843
2920
 
2844
2921
  continueStatement() {
2845
- const t = this.current; // CONTINUE token
2922
+ const t = this.current;
2846
2923
  this.eat('CONTINUE');
2847
- // Python-style: no semicolon needed, ignore if present
2848
2924
  if (this.current.type === 'SEMICOLON') this.advance();
2849
2925
  return { type: 'ContinueStatement', line: t.line, column: t.column };
2850
2926
  }
2851
2927
 
2852
2928
  funcDeclaration() {
2853
- const t = this.current; // FUNC token
2929
+ const t = this.current;
2854
2930
  this.eat('FUNC');
2855
2931
  const nameToken = this.current;
2856
2932
  const name = nameToken.value;
@@ -2872,7 +2948,6 @@ funcDeclaration() {
2872
2948
  }
2873
2949
  this.eat('RPAREN');
2874
2950
  } else {
2875
- // Python-style: single param without parentheses
2876
2951
  if (this.current.type === 'IDENTIFIER') {
2877
2952
  const paramToken = this.current;
2878
2953
  params.push({ name: paramToken.value, line: paramToken.line, column: paramToken.column });
@@ -2893,7 +2968,6 @@ returnStatement() {
2893
2968
  argument = this.expression();
2894
2969
  }
2895
2970
 
2896
- // semicolon optional
2897
2971
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
2898
2972
 
2899
2973
  return { type: 'ReturnStatement', argument, line: t.line, column: t.column };
@@ -2911,9 +2985,8 @@ block() {
2911
2985
  }
2912
2986
 
2913
2987
  expressionStatement() {
2914
- const exprToken = this.current; // first token of the expression
2988
+ const exprToken = this.current;
2915
2989
  const expr = this.expression();
2916
- // semicolon optional
2917
2990
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
2918
2991
  return { type: 'ExpressionStatement', expression: expr, line: exprToken.line, column: exprToken.column };
2919
2992
  }
@@ -3026,7 +3099,6 @@ unary() {
3026
3099
  };
3027
3100
  }
3028
3101
 
3029
- // Python-like: ignore ++ and -- if not used
3030
3102
  if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
3031
3103
  const op = t.type;
3032
3104
  this.eat(op);
@@ -3066,7 +3138,7 @@ postfix() {
3066
3138
  const args = [];
3067
3139
  while (this.current.type !== 'RPAREN' && this.current.type !== 'EOF') {
3068
3140
  args.push(this.expression());
3069
- if (this.current.type === 'COMMA') this.eat('COMMA'); // optional comma
3141
+ if (this.current.type === 'COMMA') this.eat('COMMA');
3070
3142
  }
3071
3143
  if (this.current.type === 'RPAREN') this.eat('RPAREN');
3072
3144
  node = { type: 'CallExpression', callee: node, arguments: args, line: startLine, column: startCol };
@@ -3243,11 +3315,11 @@ arrowFunction(params) {
3243
3315
  this.eat('LBRACE');
3244
3316
  const props = [];
3245
3317
  while (this.current.type !== 'RBRACE') {
3246
- const key = this.expression(); // Flexible key: can be any expression
3318
+ const key = this.expression();
3247
3319
  this.eat('COLON');
3248
3320
  const value = this.expression();
3249
3321
  props.push({ key, value });
3250
- if (this.current.type === 'COMMA') this.eat('COMMA'); // optional trailing comma
3322
+ if (this.current.type === 'COMMA') this.eat('COMMA');
3251
3323
  }
3252
3324
  this.eat('RBRACE');
3253
3325
  return { type: 'ObjectExpression', props, line: startLine, column: startCol };
@@ -3440,7 +3512,7 @@ const Lexer = __nccwpck_require__(211);
3440
3512
  const Parser = __nccwpck_require__(222);
3441
3513
  const Evaluator = __nccwpck_require__(112);
3442
3514
 
3443
- const VERSION = '1.1.2';
3515
+ const VERSION = '1.1.3';
3444
3516
 
3445
3517
  const COLOR = {
3446
3518
  reset: '\x1b[0m',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-cli",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Starlight Programming Language CLI",
5
5
  "bin": {
6
6
  "starlight": "index.js"
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
  }
@@ -659,7 +715,6 @@ async evalWhile(node, env) {
659
715
  return null;
660
716
  }
661
717
  async evalFor(node, env) {
662
- // Python-style: for x in iterable (with optional 'let')
663
718
  if (node.type === 'ForInStatement') {
664
719
  const iterable = await this.evaluate(node.iterable, env);
665
720
 
@@ -669,8 +724,6 @@ async evalFor(node, env) {
669
724
 
670
725
  const loopVar = node.variable; // string name of the loop variable
671
726
  const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
672
-
673
- // Arrays: iterate over elements
674
727
  if (Array.isArray(iterable)) {
675
728
  for (const value of iterable) {
676
729
  const loopEnv = createLoopEnv();
@@ -685,7 +738,6 @@ async evalFor(node, env) {
685
738
  }
686
739
  }
687
740
  }
688
- // Objects: iterate over keys
689
741
  else {
690
742
  for (const key of Object.keys(iterable)) {
691
743
  const loopEnv = createLoopEnv();
@@ -704,7 +756,6 @@ async evalFor(node, env) {
704
756
  return null;
705
757
  }
706
758
 
707
- // C-style for loop (classic JS style)
708
759
  const local = new Environment(env);
709
760
 
710
761
  if (node.init) await this.evaluate(node.init, local);
@@ -790,19 +841,14 @@ async evalIndex(node, env) {
790
841
  const idx = await this.evaluate(node.indexer, env);
791
842
 
792
843
  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
844
  if (Array.isArray(obj)) {
796
845
  if (idx < 0 || idx >= obj.length) return undefined;
797
846
  return obj[idx];
798
847
  }
799
-
800
- // Object access: return undefined if property missing
801
848
  if (typeof obj === 'object') {
802
849
  return obj[idx]; // undefined if missing
803
850
  }
804
851
 
805
- // Fallback for non-object non-array
806
852
  return undefined;
807
853
  }
808
854
  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 };
@@ -814,11 +840,11 @@ arrowFunction(params) {
814
840
  this.eat('LBRACE');
815
841
  const props = [];
816
842
  while (this.current.type !== 'RBRACE') {
817
- const key = this.expression(); // Flexible key: can be any expression
843
+ const key = this.expression();
818
844
  this.eat('COLON');
819
845
  const value = this.expression();
820
846
  props.push({ key, value });
821
- if (this.current.type === 'COMMA') this.eat('COMMA'); // optional trailing comma
847
+ if (this.current.type === 'COMMA') this.eat('COMMA');
822
848
  }
823
849
  this.eat('RBRACE');
824
850
  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.3';
13
13
 
14
14
  const COLOR = {
15
15
  reset: '\x1b[0m',