starlight-cli 1.1.8 → 1.1.10

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
@@ -4,7 +4,7 @@
4
4
  Starlight is a lightweight, developer-oriented programming language designed for **server-side scripting, automation, and general-purpose programming**. It combines a clean, readable syntax inspired by JavaScript and Python with powerful runtime features such as async/await, modules, and interactive I/O.
5
5
 
6
6
  **Official Reference:**
7
- https://starlight-learn-lang.pages.dev/
7
+ https://dominexmacedon.github.io/starlight-programming-language/learn.html
8
8
 
9
9
  ---
10
10
 
package/dist/index.js CHANGED
@@ -10230,7 +10230,13 @@ class RuntimeError extends Error {
10230
10230
  const lines = source.split('\n');
10231
10231
  const srcLine = lines[node.line - 1] || '';
10232
10232
  output += ` ${srcLine}\n`;
10233
- output += ` ${' '.repeat(column - 1)}^\n`;
10233
+ const caretPos =
10234
+ typeof column === 'number' && column > 0
10235
+ ? column - 1
10236
+ : 0;
10237
+
10238
+ output += ` ${' '.repeat(caretPos)}^\n`;
10239
+ ;
10234
10240
  }
10235
10241
 
10236
10242
  super(output);
@@ -10292,6 +10298,34 @@ class Evaluator {
10292
10298
  this.global = new Environment();
10293
10299
  this.setupBuiltins();
10294
10300
  }
10301
+ async callFunction(fn, args, env, node = null) {
10302
+ if (typeof fn === 'function') {
10303
+ const val = await fn(...args);
10304
+ return val === undefined ? null : val; // <<< enforce null
10305
+ }
10306
+
10307
+ if (fn && typeof fn === 'object' && fn.body && fn.params) {
10308
+ const callEnv = new Environment(fn.env);
10309
+
10310
+ for (let i = 0; i < fn.params.length; i++) {
10311
+ const param = fn.params[i];
10312
+ const name = typeof param === 'string' ? param : param.name;
10313
+ callEnv.define(name, args[i]);
10314
+ }
10315
+
10316
+ try {
10317
+ const val = await this.evaluate(fn.body, callEnv);
10318
+ return val === undefined ? null : val; // <<< enforce null
10319
+ } catch (e) {
10320
+ if (e instanceof ReturnValue) return e.value === undefined ? null : e.value; // <<< enforce null
10321
+ throw e;
10322
+ }
10323
+ }
10324
+
10325
+ throw new RuntimeError('Value is not callable', node, this.source);
10326
+ }
10327
+
10328
+
10295
10329
  suggest(name, env) {
10296
10330
  const names = new Set();
10297
10331
  let current = env;
@@ -10365,6 +10399,7 @@ formatValue(value, seen = new Set()) {
10365
10399
 
10366
10400
 
10367
10401
  setupBuiltins() {
10402
+ const evaluator = this;
10368
10403
  this.global.define('len', arg => {
10369
10404
  if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
10370
10405
  if (arg && typeof arg === 'object') return Object.keys(arg).length;
@@ -10376,63 +10411,76 @@ formatValue(value, seen = new Set()) {
10376
10411
  if (Array.isArray(arg)) return 'array';
10377
10412
  return typeof arg;
10378
10413
  });
10379
- this.global.define('map', async (array, fn) => {
10380
- if (!Array.isArray(array)) {
10381
- throw new RuntimeError('map() expects an array', null, this.source);
10382
- }
10383
- if (typeof fn !== 'function') {
10384
- throw new RuntimeError('map() expects a function', null, this.source);
10385
- }
10414
+ this.global.define('map', async (array, fn) => {
10415
+ if (!Array.isArray(array)) {
10416
+ throw new RuntimeError('map() expects an array', null, evaluator.source);
10417
+ }
10386
10418
 
10387
- const result = [];
10388
- for (let i = 0; i < array.length; i++) {
10389
- result.push(await fn(array[i], i, array));
10390
- }
10391
- return result;
10392
- });
10393
- this.global.define('filter', async (array, fn) => {
10394
- if (!Array.isArray(array)) {
10395
- throw new RuntimeError('filter() expects an array', null, this.source);
10396
- }
10397
- if (typeof fn !== 'function') {
10398
- throw new RuntimeError('filter() expects a function', null, this.source);
10399
- }
10419
+ const result = [];
10420
+ for (let i = 0; i < array.length; i++) {
10421
+ result.push(
10422
+ await evaluator.callFunction(
10423
+ fn,
10424
+ [array[i], i, array],
10425
+ evaluator.global
10426
+ )
10427
+ );
10428
+ }
10429
+ return result;
10430
+ });
10400
10431
 
10401
- const result = [];
10402
- for (let i = 0; i < array.length; i++) {
10403
- if (await fn(array[i], i, array)) {
10404
- result.push(array[i]);
10432
+ this.global.define('filter', async (array, fn) => {
10433
+ if (!Array.isArray(array)) {
10434
+ throw new RuntimeError('filter() expects an array', null, evaluator.source);
10405
10435
  }
10406
- }
10407
- return result;
10408
- });
10409
- this.global.define('reduce', async (array, fn, initial) => {
10410
- if (!Array.isArray(array)) {
10411
- throw new RuntimeError('reduce() expects an array', null, this.source);
10412
- }
10413
- if (typeof fn !== 'function') {
10414
- throw new RuntimeError('reduce() expects a function', null, this.source);
10415
- }
10416
10436
 
10417
- let acc;
10418
- let startIndex = 0;
10437
+ const result = [];
10438
+ for (let i = 0; i < array.length; i++) {
10439
+ if (
10440
+ await evaluator.callFunction(
10441
+ fn,
10442
+ [array[i], i, array],
10443
+ evaluator.global
10444
+ )
10445
+ ) {
10446
+ result.push(array[i]);
10447
+ }
10448
+ }
10449
+ return result;
10450
+ });
10419
10451
 
10420
- if (initial !== undefined) {
10421
- acc = initial;
10422
- } else {
10423
- if (array.length === 0) {
10424
- throw new RuntimeError('reduce() of empty array with no initial value', null, this.source);
10452
+ this.global.define('reduce', async (array, fn, initial) => {
10453
+ if (!Array.isArray(array)) {
10454
+ throw new RuntimeError('reduce() expects an array', null, evaluator.source);
10425
10455
  }
10426
- acc = array[0];
10427
- startIndex = 1;
10428
- }
10429
10456
 
10430
- for (let i = startIndex; i < array.length; i++) {
10431
- acc = await fn(acc, array[i], i, array);
10432
- }
10457
+ let acc;
10458
+ let i = 0;
10433
10459
 
10434
- return acc;
10435
- });
10460
+ if (initial !== undefined) {
10461
+ acc = initial;
10462
+ } else {
10463
+ if (array.length === 0) {
10464
+ throw new RuntimeError(
10465
+ 'reduce() of empty array with no initial value',
10466
+ null,
10467
+ evaluator.source
10468
+ );
10469
+ }
10470
+ acc = array[0];
10471
+ i = 1;
10472
+ }
10473
+
10474
+ for (; i < array.length; i++) {
10475
+ acc = await evaluator.callFunction(
10476
+ fn,
10477
+ [acc, array[i], i, array],
10478
+ evaluator.global
10479
+ );
10480
+ }
10481
+
10482
+ return acc;
10483
+ });
10436
10484
 
10437
10485
  this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
10438
10486
  this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
@@ -10477,12 +10525,26 @@ this.global.define('range', (...args) => {
10477
10525
  return readlineSync.question(prompt + ' ');
10478
10526
  });
10479
10527
  this.global.define('num', arg => {
10480
- const n = Number(arg);
10481
- if (Number.isNaN(n)) {
10482
- throw new RuntimeError('Cannot convert value to number', null, this.source);
10528
+ if (arg === null || arg === undefined) return 0;
10529
+
10530
+ const t = typeof arg;
10531
+
10532
+ if (t === 'number') return arg; // already a number
10533
+ if (t === 'boolean') return arg ? 1 : 0;
10534
+ if (t === 'string') {
10535
+ const n = Number(arg);
10536
+ if (!Number.isNaN(n)) return n; // numeric string
10537
+ return 0; // non-numeric string becomes 0
10483
10538
  }
10484
- return n;
10539
+ if (Array.isArray(arg)) return arg.length; // array → length
10540
+ if (t === 'object') return Object.keys(arg).length; // object → number of keys
10541
+
10542
+ // fallback for anything else
10543
+ const n = Number(arg);
10544
+ if (!Number.isNaN(n)) return n;
10545
+ return 0;
10485
10546
  });
10547
+
10486
10548
  this.global.define('fetch', async (url, options = {}) => {
10487
10549
  const res = await fetch(url, options);
10488
10550
  return {
@@ -10572,21 +10634,30 @@ case 'RaceClause':
10572
10634
  const callee = await this.evaluate(node.callee, env);
10573
10635
 
10574
10636
  if (typeof callee === 'object' && callee.body) {
10575
- const evaluator = this;
10576
- const Constructor = function(...args) {
10637
+ const evaluator = this;
10638
+
10639
+ const Constructor = function (...args) {
10577
10640
  const newEnv = new Environment(callee.env);
10578
10641
  newEnv.define('this', this);
10642
+
10579
10643
  for (let i = 0; i < callee.params.length; i++) {
10580
- newEnv.define(callee.params[i], args[i]);
10644
+ const param = callee.params[i];
10645
+ const paramName = typeof param === 'string' ? param : param.name;
10646
+ newEnv.define(paramName, args[i]);
10581
10647
  }
10582
- return evaluator.evaluate(callee.body, newEnv);
10648
+
10649
+ return (async () => {
10650
+ await evaluator.evaluate(callee.body, newEnv);
10651
+ return this;
10652
+ })();
10583
10653
  };
10584
10654
 
10585
10655
  const args = [];
10586
10656
  for (const a of node.arguments) args.push(await this.evaluate(a, env));
10587
- return new Constructor(...args);
10657
+ return await new Constructor(...args);
10588
10658
  }
10589
10659
 
10660
+ // native JS constructor fallback
10590
10661
  if (typeof callee !== 'function') {
10591
10662
  throw new RuntimeError('NewExpression callee is not a function', node, this.source);
10592
10663
  }
@@ -10808,27 +10879,31 @@ evalArrowFunction(node, env) {
10808
10879
  return async function (...args) {
10809
10880
  const localEnv = new Environment(env);
10810
10881
 
10882
+ // Bind parameters safely
10811
10883
  node.params.forEach((p, i) => {
10812
- localEnv.define(p.name, args[i]);
10884
+ const paramName = typeof p === 'string' ? p : p.name;
10885
+ localEnv.define(paramName, args[i]);
10813
10886
  });
10814
10887
 
10815
10888
  try {
10816
10889
  if (node.isBlock) {
10890
+ // Block body
10817
10891
  const result = await evaluator.evaluate(node.body, localEnv);
10818
- return result ?? null;
10892
+ return result === undefined ? null : result; // ensure null instead of undefined
10819
10893
  } else {
10820
- return await evaluator.evaluate(node.body, localEnv);
10894
+ // Expression body
10895
+ const result = await evaluator.evaluate(node.body, localEnv);
10896
+ return result === undefined ? null : result; // ensure null instead of undefined
10821
10897
  }
10822
10898
  } catch (err) {
10823
- if (err instanceof ReturnValue) {
10824
- return err.value;
10825
- }
10899
+ if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
10826
10900
  throw err;
10827
10901
  }
10828
10902
  };
10829
10903
  }
10830
10904
 
10831
10905
 
10906
+
10832
10907
  async evalAssignment(node, env) {
10833
10908
  const rightVal = await this.evaluate(node.right, env);
10834
10909
  const left = node.left;
@@ -10924,7 +10999,30 @@ async evalBinary(node, env) {
10924
10999
  }
10925
11000
 
10926
11001
  switch (node.operator) {
10927
- case 'PLUS': return l + r;
11002
+ case 'PLUS': {
11003
+ if (Array.isArray(l) && Array.isArray(r)) {
11004
+ return l.concat(r);
11005
+ }
11006
+
11007
+ if (typeof l === 'string' || typeof r === 'string') {
11008
+ return String(l) + String(r);
11009
+ }
11010
+
11011
+ if (typeof l === 'number' && typeof r === 'number') {
11012
+ return l + r;
11013
+ }
11014
+
11015
+ if (typeof l === 'object' && typeof r === 'object') {
11016
+ return { ...l, ...r };
11017
+ }
11018
+
11019
+ throw new RuntimeError(
11020
+ `Unsupported operands for +: ${typeof l} and ${typeof r}`,
11021
+ node,
11022
+ this.source
11023
+ );
11024
+ }
11025
+
10928
11026
  case 'MINUS': return l - r;
10929
11027
  case 'STAR': return l * r;
10930
11028
  case 'SLASH': return l / r;
@@ -11146,18 +11244,26 @@ async evalIndex(node, env) {
11146
11244
  async evalObject(node, env) {
11147
11245
  const out = {};
11148
11246
  for (const p of node.props) {
11149
- if (!p.key || !p.value) {
11150
- throw new RuntimeError('Invalid object property', node, this.source);
11247
+ if (!p.key) {
11248
+ throw new RuntimeError('Object property must have a key', node, this.source);
11151
11249
  }
11250
+
11152
11251
  const key = await this.evaluate(p.key, env);
11153
- const value = await this.evaluate(p.value, env);
11154
- out[key] = value; // dynamic property assignment
11252
+ let value = null;
11253
+
11254
+ if (p.value) {
11255
+ value = await this.evaluate(p.value, env);
11256
+ if (value === undefined) value = null; // <- force null instead of undefined
11257
+ }
11258
+
11259
+ out[key] = value;
11155
11260
  }
11156
11261
  return out;
11157
11262
  }
11158
11263
 
11159
11264
 
11160
11265
 
11266
+
11161
11267
  async evalMember(node, env) {
11162
11268
  const obj = await this.evaluate(node.object, env);
11163
11269
 
@@ -11197,7 +11303,7 @@ async evalUpdate(node, env) {
11197
11303
 
11198
11304
  }
11199
11305
 
11200
- module.exports = Evaluator;
11306
+ module.exports = Evaluator;
11201
11307
 
11202
11308
  /***/ }),
11203
11309
 
@@ -11692,14 +11798,18 @@ ifStatement() {
11692
11798
  test = this.expression();
11693
11799
  }
11694
11800
 
11695
- const consequent = this.block();
11801
+ const consequent = this.statementOrBlock();
11696
11802
 
11697
11803
  let alternate = null;
11698
11804
  if (this.current.type === 'ELSE') {
11699
- this.eat('ELSE');
11700
- if (this.current.type === 'IF') alternate = this.ifStatement();
11701
- else alternate = this.block();
11805
+ this.eat('ELSE');
11806
+ if (this.current.type === 'IF') {
11807
+ alternate = this.ifStatement();
11808
+ } else {
11809
+ alternate = this.statementOrBlock();
11702
11810
  }
11811
+ }
11812
+
11703
11813
 
11704
11814
  return {
11705
11815
  type: 'IfStatement',
@@ -11933,6 +12043,12 @@ returnStatement() {
11933
12043
 
11934
12044
  return { type: 'ReturnStatement', argument, line: t.line, column: t.column };
11935
12045
  }
12046
+ statementOrBlock() {
12047
+ if (this.current.type === 'LBRACE') {
12048
+ return this.block();
12049
+ }
12050
+ return this.statement();
12051
+ }
11936
12052
 
11937
12053
  block() {
11938
12054
  const t = this.current; // LBRACE token
@@ -12142,13 +12258,7 @@ postfix() {
12142
12258
  continue;
12143
12259
  }
12144
12260
 
12145
- if (node.type === 'Identifier' && t.type === 'IDENTIFIER') {
12146
- const argNode = { type: 'Identifier', name: t.value, line: t.line, column: t.column };
12147
- this.eat('IDENTIFIER');
12148
- node = { type: 'CallExpression', callee: node, arguments: [argNode], line: node.line, column: node.column };
12149
- continue;
12150
- }
12151
-
12261
+
12152
12262
  break;
12153
12263
  }
12154
12264
  return node;
@@ -12302,21 +12412,56 @@ arrowFunction(params) {
12302
12412
  }
12303
12413
 
12304
12414
  if (t.type === 'LBRACE') {
12305
- const startLine = t.line;
12306
- const startCol = t.column;
12307
- this.eat('LBRACE');
12308
- const props = [];
12309
- while (this.current.type !== 'RBRACE') {
12310
- const key = this.expression();
12311
- this.eat('COLON');
12312
- const value = this.expression();
12313
- props.push({ key, value });
12314
- if (this.current.type === 'COMMA') this.eat('COMMA');
12415
+ const startLine = t.line;
12416
+ const startCol = t.column;
12417
+ this.eat('LBRACE');
12418
+
12419
+ const props = [];
12420
+
12421
+ while (this.current.type !== 'RBRACE') {
12422
+ let key;
12423
+
12424
+ if (this.current.type === 'IDENTIFIER') {
12425
+ const k = this.current;
12426
+ this.eat('IDENTIFIER');
12427
+ key = {
12428
+ type: 'Literal',
12429
+ value: k.value,
12430
+ line: k.line,
12431
+ column: k.column
12432
+ };
12315
12433
  }
12316
- this.eat('RBRACE');
12317
- return { type: 'ObjectExpression', props, line: startLine, column: startCol };
12434
+ else if (this.current.type === 'STRING') {
12435
+ const k = this.current;
12436
+ this.eat('STRING');
12437
+ key = {
12438
+ type: 'Literal',
12439
+ value: k.value,
12440
+ line: k.line,
12441
+ column: k.column
12442
+ };
12443
+ }
12444
+ else {
12445
+ throw new ParseError(
12446
+ 'Invalid object key',
12447
+ this.current,
12448
+ this.source
12449
+ );
12450
+ }
12451
+
12452
+ this.eat('COLON');
12453
+ const value = this.expression();
12454
+
12455
+ props.push({ key, value });
12456
+
12457
+ if (this.current.type === 'COMMA') this.eat('COMMA');
12318
12458
  }
12319
12459
 
12460
+ this.eat('RBRACE');
12461
+ return { type: 'ObjectExpression', props, line: startLine, column: startCol };
12462
+ }
12463
+
12464
+
12320
12465
  throw new ParseError(
12321
12466
  `Unexpected token '${t.type}'`,
12322
12467
  t,
@@ -12507,7 +12652,7 @@ const Lexer = __nccwpck_require__(211);
12507
12652
  const Parser = __nccwpck_require__(222);
12508
12653
  const Evaluator = __nccwpck_require__(112);
12509
12654
 
12510
- const VERSION = '1.1.8';
12655
+ const VERSION = '1.1.10';
12511
12656
 
12512
12657
  const COLOR = {
12513
12658
  reset: '\x1b[0m',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-cli",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "Starlight Programming Language CLI",
5
5
  "bin": {
6
6
  "starlight": "index.js"
package/src/evaluator.js CHANGED
@@ -20,7 +20,13 @@ class RuntimeError extends Error {
20
20
  const lines = source.split('\n');
21
21
  const srcLine = lines[node.line - 1] || '';
22
22
  output += ` ${srcLine}\n`;
23
- output += ` ${' '.repeat(column - 1)}^\n`;
23
+ const caretPos =
24
+ typeof column === 'number' && column > 0
25
+ ? column - 1
26
+ : 0;
27
+
28
+ output += ` ${' '.repeat(caretPos)}^\n`;
29
+ ;
24
30
  }
25
31
 
26
32
  super(output);
@@ -82,6 +88,34 @@ class Evaluator {
82
88
  this.global = new Environment();
83
89
  this.setupBuiltins();
84
90
  }
91
+ async callFunction(fn, args, env, node = null) {
92
+ if (typeof fn === 'function') {
93
+ const val = await fn(...args);
94
+ return val === undefined ? null : val; // <<< enforce null
95
+ }
96
+
97
+ if (fn && typeof fn === 'object' && fn.body && fn.params) {
98
+ const callEnv = new Environment(fn.env);
99
+
100
+ for (let i = 0; i < fn.params.length; i++) {
101
+ const param = fn.params[i];
102
+ const name = typeof param === 'string' ? param : param.name;
103
+ callEnv.define(name, args[i]);
104
+ }
105
+
106
+ try {
107
+ const val = await this.evaluate(fn.body, callEnv);
108
+ return val === undefined ? null : val; // <<< enforce null
109
+ } catch (e) {
110
+ if (e instanceof ReturnValue) return e.value === undefined ? null : e.value; // <<< enforce null
111
+ throw e;
112
+ }
113
+ }
114
+
115
+ throw new RuntimeError('Value is not callable', node, this.source);
116
+ }
117
+
118
+
85
119
  suggest(name, env) {
86
120
  const names = new Set();
87
121
  let current = env;
@@ -155,6 +189,7 @@ formatValue(value, seen = new Set()) {
155
189
 
156
190
 
157
191
  setupBuiltins() {
192
+ const evaluator = this;
158
193
  this.global.define('len', arg => {
159
194
  if (Array.isArray(arg) || typeof arg === 'string') return arg.length;
160
195
  if (arg && typeof arg === 'object') return Object.keys(arg).length;
@@ -166,63 +201,76 @@ formatValue(value, seen = new Set()) {
166
201
  if (Array.isArray(arg)) return 'array';
167
202
  return typeof arg;
168
203
  });
169
- this.global.define('map', async (array, fn) => {
170
- if (!Array.isArray(array)) {
171
- throw new RuntimeError('map() expects an array', null, this.source);
172
- }
173
- if (typeof fn !== 'function') {
174
- throw new RuntimeError('map() expects a function', null, this.source);
175
- }
204
+ this.global.define('map', async (array, fn) => {
205
+ if (!Array.isArray(array)) {
206
+ throw new RuntimeError('map() expects an array', null, evaluator.source);
207
+ }
176
208
 
177
- const result = [];
178
- for (let i = 0; i < array.length; i++) {
179
- result.push(await fn(array[i], i, array));
180
- }
181
- return result;
182
- });
183
- this.global.define('filter', async (array, fn) => {
184
- if (!Array.isArray(array)) {
185
- throw new RuntimeError('filter() expects an array', null, this.source);
186
- }
187
- if (typeof fn !== 'function') {
188
- throw new RuntimeError('filter() expects a function', null, this.source);
189
- }
209
+ const result = [];
210
+ for (let i = 0; i < array.length; i++) {
211
+ result.push(
212
+ await evaluator.callFunction(
213
+ fn,
214
+ [array[i], i, array],
215
+ evaluator.global
216
+ )
217
+ );
218
+ }
219
+ return result;
220
+ });
190
221
 
191
- const result = [];
192
- for (let i = 0; i < array.length; i++) {
193
- if (await fn(array[i], i, array)) {
194
- result.push(array[i]);
222
+ this.global.define('filter', async (array, fn) => {
223
+ if (!Array.isArray(array)) {
224
+ throw new RuntimeError('filter() expects an array', null, evaluator.source);
195
225
  }
196
- }
197
- return result;
198
- });
199
- this.global.define('reduce', async (array, fn, initial) => {
200
- if (!Array.isArray(array)) {
201
- throw new RuntimeError('reduce() expects an array', null, this.source);
202
- }
203
- if (typeof fn !== 'function') {
204
- throw new RuntimeError('reduce() expects a function', null, this.source);
205
- }
206
226
 
207
- let acc;
208
- let startIndex = 0;
227
+ const result = [];
228
+ for (let i = 0; i < array.length; i++) {
229
+ if (
230
+ await evaluator.callFunction(
231
+ fn,
232
+ [array[i], i, array],
233
+ evaluator.global
234
+ )
235
+ ) {
236
+ result.push(array[i]);
237
+ }
238
+ }
239
+ return result;
240
+ });
241
+
242
+ this.global.define('reduce', async (array, fn, initial) => {
243
+ if (!Array.isArray(array)) {
244
+ throw new RuntimeError('reduce() expects an array', null, evaluator.source);
245
+ }
209
246
 
210
- if (initial !== undefined) {
211
- acc = initial;
212
- } else {
213
- if (array.length === 0) {
214
- throw new RuntimeError('reduce() of empty array with no initial value', null, this.source);
247
+ let acc;
248
+ let i = 0;
249
+
250
+ if (initial !== undefined) {
251
+ acc = initial;
252
+ } else {
253
+ if (array.length === 0) {
254
+ throw new RuntimeError(
255
+ 'reduce() of empty array with no initial value',
256
+ null,
257
+ evaluator.source
258
+ );
259
+ }
260
+ acc = array[0];
261
+ i = 1;
215
262
  }
216
- acc = array[0];
217
- startIndex = 1;
218
- }
219
263
 
220
- for (let i = startIndex; i < array.length; i++) {
221
- acc = await fn(acc, array[i], i, array);
222
- }
264
+ for (; i < array.length; i++) {
265
+ acc = await evaluator.callFunction(
266
+ fn,
267
+ [acc, array[i], i, array],
268
+ evaluator.global
269
+ );
270
+ }
223
271
 
224
- return acc;
225
- });
272
+ return acc;
273
+ });
226
274
 
227
275
  this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
228
276
  this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
@@ -267,12 +315,26 @@ this.global.define('range', (...args) => {
267
315
  return readlineSync.question(prompt + ' ');
268
316
  });
269
317
  this.global.define('num', arg => {
270
- const n = Number(arg);
271
- if (Number.isNaN(n)) {
272
- throw new RuntimeError('Cannot convert value to number', null, this.source);
318
+ if (arg === null || arg === undefined) return 0;
319
+
320
+ const t = typeof arg;
321
+
322
+ if (t === 'number') return arg; // already a number
323
+ if (t === 'boolean') return arg ? 1 : 0;
324
+ if (t === 'string') {
325
+ const n = Number(arg);
326
+ if (!Number.isNaN(n)) return n; // numeric string
327
+ return 0; // non-numeric string becomes 0
273
328
  }
274
- return n;
329
+ if (Array.isArray(arg)) return arg.length; // array → length
330
+ if (t === 'object') return Object.keys(arg).length; // object → number of keys
331
+
332
+ // fallback for anything else
333
+ const n = Number(arg);
334
+ if (!Number.isNaN(n)) return n;
335
+ return 0;
275
336
  });
337
+
276
338
  this.global.define('fetch', async (url, options = {}) => {
277
339
  const res = await fetch(url, options);
278
340
  return {
@@ -362,21 +424,30 @@ case 'RaceClause':
362
424
  const callee = await this.evaluate(node.callee, env);
363
425
 
364
426
  if (typeof callee === 'object' && callee.body) {
365
- const evaluator = this;
366
- const Constructor = function(...args) {
427
+ const evaluator = this;
428
+
429
+ const Constructor = function (...args) {
367
430
  const newEnv = new Environment(callee.env);
368
431
  newEnv.define('this', this);
432
+
369
433
  for (let i = 0; i < callee.params.length; i++) {
370
- newEnv.define(callee.params[i], args[i]);
434
+ const param = callee.params[i];
435
+ const paramName = typeof param === 'string' ? param : param.name;
436
+ newEnv.define(paramName, args[i]);
371
437
  }
372
- return evaluator.evaluate(callee.body, newEnv);
438
+
439
+ return (async () => {
440
+ await evaluator.evaluate(callee.body, newEnv);
441
+ return this;
442
+ })();
373
443
  };
374
444
 
375
445
  const args = [];
376
446
  for (const a of node.arguments) args.push(await this.evaluate(a, env));
377
- return new Constructor(...args);
447
+ return await new Constructor(...args);
378
448
  }
379
449
 
450
+ // native JS constructor fallback
380
451
  if (typeof callee !== 'function') {
381
452
  throw new RuntimeError('NewExpression callee is not a function', node, this.source);
382
453
  }
@@ -598,27 +669,31 @@ evalArrowFunction(node, env) {
598
669
  return async function (...args) {
599
670
  const localEnv = new Environment(env);
600
671
 
672
+ // Bind parameters safely
601
673
  node.params.forEach((p, i) => {
602
- localEnv.define(p.name, args[i]);
674
+ const paramName = typeof p === 'string' ? p : p.name;
675
+ localEnv.define(paramName, args[i]);
603
676
  });
604
677
 
605
678
  try {
606
679
  if (node.isBlock) {
680
+ // Block body
607
681
  const result = await evaluator.evaluate(node.body, localEnv);
608
- return result ?? null;
682
+ return result === undefined ? null : result; // ensure null instead of undefined
609
683
  } else {
610
- return await evaluator.evaluate(node.body, localEnv);
684
+ // Expression body
685
+ const result = await evaluator.evaluate(node.body, localEnv);
686
+ return result === undefined ? null : result; // ensure null instead of undefined
611
687
  }
612
688
  } catch (err) {
613
- if (err instanceof ReturnValue) {
614
- return err.value;
615
- }
689
+ if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
616
690
  throw err;
617
691
  }
618
692
  };
619
693
  }
620
694
 
621
695
 
696
+
622
697
  async evalAssignment(node, env) {
623
698
  const rightVal = await this.evaluate(node.right, env);
624
699
  const left = node.left;
@@ -714,7 +789,30 @@ async evalBinary(node, env) {
714
789
  }
715
790
 
716
791
  switch (node.operator) {
717
- case 'PLUS': return l + r;
792
+ case 'PLUS': {
793
+ if (Array.isArray(l) && Array.isArray(r)) {
794
+ return l.concat(r);
795
+ }
796
+
797
+ if (typeof l === 'string' || typeof r === 'string') {
798
+ return String(l) + String(r);
799
+ }
800
+
801
+ if (typeof l === 'number' && typeof r === 'number') {
802
+ return l + r;
803
+ }
804
+
805
+ if (typeof l === 'object' && typeof r === 'object') {
806
+ return { ...l, ...r };
807
+ }
808
+
809
+ throw new RuntimeError(
810
+ `Unsupported operands for +: ${typeof l} and ${typeof r}`,
811
+ node,
812
+ this.source
813
+ );
814
+ }
815
+
718
816
  case 'MINUS': return l - r;
719
817
  case 'STAR': return l * r;
720
818
  case 'SLASH': return l / r;
@@ -936,18 +1034,26 @@ async evalIndex(node, env) {
936
1034
  async evalObject(node, env) {
937
1035
  const out = {};
938
1036
  for (const p of node.props) {
939
- if (!p.key || !p.value) {
940
- throw new RuntimeError('Invalid object property', node, this.source);
1037
+ if (!p.key) {
1038
+ throw new RuntimeError('Object property must have a key', node, this.source);
941
1039
  }
1040
+
942
1041
  const key = await this.evaluate(p.key, env);
943
- const value = await this.evaluate(p.value, env);
944
- out[key] = value; // dynamic property assignment
1042
+ let value = null;
1043
+
1044
+ if (p.value) {
1045
+ value = await this.evaluate(p.value, env);
1046
+ if (value === undefined) value = null; // <- force null instead of undefined
1047
+ }
1048
+
1049
+ out[key] = value;
945
1050
  }
946
1051
  return out;
947
1052
  }
948
1053
 
949
1054
 
950
1055
 
1056
+
951
1057
  async evalMember(node, env) {
952
1058
  const obj = await this.evaluate(node.object, env);
953
1059
 
@@ -987,4 +1093,4 @@ async evalUpdate(node, env) {
987
1093
 
988
1094
  }
989
1095
 
990
- module.exports = Evaluator;
1096
+ module.exports = Evaluator;
package/src/parser.js CHANGED
@@ -256,14 +256,18 @@ ifStatement() {
256
256
  test = this.expression();
257
257
  }
258
258
 
259
- const consequent = this.block();
259
+ const consequent = this.statementOrBlock();
260
260
 
261
261
  let alternate = null;
262
262
  if (this.current.type === 'ELSE') {
263
- this.eat('ELSE');
264
- if (this.current.type === 'IF') alternate = this.ifStatement();
265
- else alternate = this.block();
263
+ this.eat('ELSE');
264
+ if (this.current.type === 'IF') {
265
+ alternate = this.ifStatement();
266
+ } else {
267
+ alternate = this.statementOrBlock();
266
268
  }
269
+ }
270
+
267
271
 
268
272
  return {
269
273
  type: 'IfStatement',
@@ -497,6 +501,12 @@ returnStatement() {
497
501
 
498
502
  return { type: 'ReturnStatement', argument, line: t.line, column: t.column };
499
503
  }
504
+ statementOrBlock() {
505
+ if (this.current.type === 'LBRACE') {
506
+ return this.block();
507
+ }
508
+ return this.statement();
509
+ }
500
510
 
501
511
  block() {
502
512
  const t = this.current; // LBRACE token
@@ -706,13 +716,7 @@ postfix() {
706
716
  continue;
707
717
  }
708
718
 
709
- if (node.type === 'Identifier' && t.type === 'IDENTIFIER') {
710
- const argNode = { type: 'Identifier', name: t.value, line: t.line, column: t.column };
711
- this.eat('IDENTIFIER');
712
- node = { type: 'CallExpression', callee: node, arguments: [argNode], line: node.line, column: node.column };
713
- continue;
714
- }
715
-
719
+
716
720
  break;
717
721
  }
718
722
  return node;
@@ -866,21 +870,56 @@ arrowFunction(params) {
866
870
  }
867
871
 
868
872
  if (t.type === 'LBRACE') {
869
- const startLine = t.line;
870
- const startCol = t.column;
871
- this.eat('LBRACE');
872
- const props = [];
873
- while (this.current.type !== 'RBRACE') {
874
- const key = this.expression();
875
- this.eat('COLON');
876
- const value = this.expression();
877
- props.push({ key, value });
878
- if (this.current.type === 'COMMA') this.eat('COMMA');
873
+ const startLine = t.line;
874
+ const startCol = t.column;
875
+ this.eat('LBRACE');
876
+
877
+ const props = [];
878
+
879
+ while (this.current.type !== 'RBRACE') {
880
+ let key;
881
+
882
+ if (this.current.type === 'IDENTIFIER') {
883
+ const k = this.current;
884
+ this.eat('IDENTIFIER');
885
+ key = {
886
+ type: 'Literal',
887
+ value: k.value,
888
+ line: k.line,
889
+ column: k.column
890
+ };
879
891
  }
880
- this.eat('RBRACE');
881
- return { type: 'ObjectExpression', props, line: startLine, column: startCol };
892
+ else if (this.current.type === 'STRING') {
893
+ const k = this.current;
894
+ this.eat('STRING');
895
+ key = {
896
+ type: 'Literal',
897
+ value: k.value,
898
+ line: k.line,
899
+ column: k.column
900
+ };
901
+ }
902
+ else {
903
+ throw new ParseError(
904
+ 'Invalid object key',
905
+ this.current,
906
+ this.source
907
+ );
908
+ }
909
+
910
+ this.eat('COLON');
911
+ const value = this.expression();
912
+
913
+ props.push({ key, value });
914
+
915
+ if (this.current.type === 'COMMA') this.eat('COMMA');
882
916
  }
883
917
 
918
+ this.eat('RBRACE');
919
+ return { type: 'ObjectExpression', props, line: startLine, column: startCol };
920
+ }
921
+
922
+
884
923
  throw new ParseError(
885
924
  `Unexpected token '${t.type}'`,
886
925
  t,
package/src/starlight.js CHANGED
@@ -12,7 +12,7 @@ const Lexer = require('./lexer');
12
12
  const Parser = require('./parser');
13
13
  const Evaluator = require('./evaluator');
14
14
 
15
- const VERSION = '1.1.8';
15
+ const VERSION = '1.1.10';
16
16
 
17
17
  const COLOR = {
18
18
  reset: '\x1b[0m',