starlight-cli 1.1.9 → 1.1.11

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://developerdominex.github.io/starlight-programming-language/
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,90 @@ 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
- }
10386
-
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;
10414
+ this.global.define('isNaN', arg => {
10415
+ return typeof arg !== 'number' || Number.isNaN(arg);
10392
10416
  });
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);
10417
+ this.global.define('random', (min, max) => {
10418
+ if (max === undefined) {
10419
+ // Only one argument random between 0 and min
10420
+ return Math.floor(Math.random() * min);
10399
10421
  }
10422
+ min = Number(min);
10423
+ max = Number(max);
10424
+ if (isNaN(min) || isNaN(max)) return 0;
10425
+ return Math.floor(Math.random() * (max - min)) + min;
10426
+ });
10400
10427
 
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]);
10428
+ this.global.define('map', async (array, fn) => {
10429
+ if (!Array.isArray(array)) {
10430
+ throw new RuntimeError('map() expects an array', null, evaluator.source);
10405
10431
  }
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
10432
 
10417
- let acc;
10418
- let startIndex = 0;
10433
+ const result = [];
10434
+ for (let i = 0; i < array.length; i++) {
10435
+ result.push(
10436
+ await evaluator.callFunction(
10437
+ fn,
10438
+ [array[i], i, array],
10439
+ evaluator.global
10440
+ )
10441
+ );
10442
+ }
10443
+ return result;
10444
+ });
10419
10445
 
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);
10446
+ this.global.define('filter', async (array, fn) => {
10447
+ if (!Array.isArray(array)) {
10448
+ throw new RuntimeError('filter() expects an array', null, evaluator.source);
10425
10449
  }
10426
- acc = array[0];
10427
- startIndex = 1;
10428
- }
10429
10450
 
10430
- for (let i = startIndex; i < array.length; i++) {
10431
- acc = await fn(acc, array[i], i, array);
10432
- }
10451
+ const result = [];
10452
+ for (let i = 0; i < array.length; i++) {
10453
+ if (
10454
+ await evaluator.callFunction(
10455
+ fn,
10456
+ [array[i], i, array],
10457
+ evaluator.global
10458
+ )
10459
+ ) {
10460
+ result.push(array[i]);
10461
+ }
10462
+ }
10463
+ return result;
10464
+ });
10433
10465
 
10434
- return acc;
10435
- });
10466
+ this.global.define('reduce', async (array, fn, initial) => {
10467
+ if (!Array.isArray(array)) {
10468
+ throw new RuntimeError('reduce() expects an array', null, evaluator.source);
10469
+ }
10470
+
10471
+ let acc;
10472
+ let i = 0;
10473
+
10474
+ if (initial !== undefined) {
10475
+ acc = initial;
10476
+ } else {
10477
+ if (array.length === 0) {
10478
+ throw new RuntimeError(
10479
+ 'reduce() of empty array with no initial value',
10480
+ null,
10481
+ evaluator.source
10482
+ );
10483
+ }
10484
+ acc = array[0];
10485
+ i = 1;
10486
+ }
10487
+
10488
+ for (; i < array.length; i++) {
10489
+ acc = await evaluator.callFunction(
10490
+ fn,
10491
+ [acc, array[i], i, array],
10492
+ evaluator.global
10493
+ );
10494
+ }
10495
+
10496
+ return acc;
10497
+ });
10436
10498
 
10437
10499
  this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
10438
10500
  this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
@@ -10586,21 +10648,30 @@ case 'RaceClause':
10586
10648
  const callee = await this.evaluate(node.callee, env);
10587
10649
 
10588
10650
  if (typeof callee === 'object' && callee.body) {
10589
- const evaluator = this;
10590
- const Constructor = function(...args) {
10651
+ const evaluator = this;
10652
+
10653
+ const Constructor = function (...args) {
10591
10654
  const newEnv = new Environment(callee.env);
10592
10655
  newEnv.define('this', this);
10656
+
10593
10657
  for (let i = 0; i < callee.params.length; i++) {
10594
- newEnv.define(callee.params[i], args[i]);
10658
+ const param = callee.params[i];
10659
+ const paramName = typeof param === 'string' ? param : param.name;
10660
+ newEnv.define(paramName, args[i]);
10595
10661
  }
10596
- return evaluator.evaluate(callee.body, newEnv);
10662
+
10663
+ return (async () => {
10664
+ await evaluator.evaluate(callee.body, newEnv);
10665
+ return this;
10666
+ })();
10597
10667
  };
10598
10668
 
10599
10669
  const args = [];
10600
10670
  for (const a of node.arguments) args.push(await this.evaluate(a, env));
10601
- return new Constructor(...args);
10671
+ return await new Constructor(...args);
10602
10672
  }
10603
10673
 
10674
+ // native JS constructor fallback
10604
10675
  if (typeof callee !== 'function') {
10605
10676
  throw new RuntimeError('NewExpression callee is not a function', node, this.source);
10606
10677
  }
@@ -10822,27 +10893,31 @@ evalArrowFunction(node, env) {
10822
10893
  return async function (...args) {
10823
10894
  const localEnv = new Environment(env);
10824
10895
 
10896
+ // Bind parameters safely
10825
10897
  node.params.forEach((p, i) => {
10826
- localEnv.define(p.name, args[i]);
10898
+ const paramName = typeof p === 'string' ? p : p.name;
10899
+ localEnv.define(paramName, args[i]);
10827
10900
  });
10828
10901
 
10829
10902
  try {
10830
10903
  if (node.isBlock) {
10904
+ // Block body
10831
10905
  const result = await evaluator.evaluate(node.body, localEnv);
10832
- return result ?? null;
10906
+ return result === undefined ? null : result; // ensure null instead of undefined
10833
10907
  } else {
10834
- return await evaluator.evaluate(node.body, localEnv);
10908
+ // Expression body
10909
+ const result = await evaluator.evaluate(node.body, localEnv);
10910
+ return result === undefined ? null : result; // ensure null instead of undefined
10835
10911
  }
10836
10912
  } catch (err) {
10837
- if (err instanceof ReturnValue) {
10838
- return err.value;
10839
- }
10913
+ if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
10840
10914
  throw err;
10841
10915
  }
10842
10916
  };
10843
10917
  }
10844
10918
 
10845
10919
 
10920
+
10846
10921
  async evalAssignment(node, env) {
10847
10922
  const rightVal = await this.evaluate(node.right, env);
10848
10923
  const left = node.left;
@@ -11003,13 +11078,10 @@ async evalUnary(node, env) {
11003
11078
 
11004
11079
  }
11005
11080
  }
11006
-
11007
11081
  async evalIf(node, env) {
11008
- const test = await this.evaluate(node.test, env);
11082
+ let test = await this.evaluate(node.test, env);
11009
11083
 
11010
- if (typeof test !== 'boolean') {
11011
- throw new RuntimeError('If condition must evaluate to a boolean', node.test, this.source);
11012
- }
11084
+ test = !!test;
11013
11085
 
11014
11086
  if (test) {
11015
11087
  return await this.evaluate(node.consequent, env);
@@ -11022,27 +11094,26 @@ async evalIf(node, env) {
11022
11094
  return null;
11023
11095
  }
11024
11096
 
11025
-
11026
11097
  async evalWhile(node, env) {
11027
11098
  while (true) {
11028
- const test = await this.evaluate(node.test, env);
11099
+ let test = await this.evaluate(node.test, env);
11029
11100
 
11030
- if (typeof test !== 'boolean') {
11031
- throw new RuntimeError('While condition must evaluate to a boolean', node.test, this.source);
11032
- }
11101
+ test = !!test;
11033
11102
 
11034
11103
  if (!test) break;
11035
11104
 
11036
11105
  try {
11037
11106
  await this.evaluate(node.body, env);
11038
11107
  } catch (e) {
11039
- if (e instanceof BreakSignal) break;
11040
- if (e instanceof ContinueSignal) continue;
11041
- throw e;
11108
+ if (e instanceof BreakSignal) break;
11109
+ if (e instanceof ContinueSignal) continue;
11110
+ throw e;
11042
11111
  }
11043
11112
  }
11113
+
11044
11114
  return null;
11045
11115
  }
11116
+
11046
11117
  async evalFor(node, env) {
11047
11118
  if (node.type === 'ForInStatement') {
11048
11119
  const iterable = await this.evaluate(node.iterable, env);
@@ -11183,24 +11254,40 @@ async evalIndex(node, env) {
11183
11254
  async evalObject(node, env) {
11184
11255
  const out = {};
11185
11256
  for (const p of node.props) {
11186
- if (!p.key || !p.value) {
11187
- throw new RuntimeError('Invalid object property', node, this.source);
11257
+ if (!p.key) {
11258
+ throw new RuntimeError('Object property must have a key', node, this.source);
11188
11259
  }
11260
+
11189
11261
  const key = await this.evaluate(p.key, env);
11190
- const value = await this.evaluate(p.value, env);
11191
- out[key] = value; // dynamic property assignment
11262
+ let value = null;
11263
+
11264
+ if (p.value) {
11265
+ value = await this.evaluate(p.value, env);
11266
+ if (value === undefined) value = null; // <- force null instead of undefined
11267
+ }
11268
+
11269
+ out[key] = value;
11192
11270
  }
11193
11271
  return out;
11194
11272
  }
11195
11273
 
11196
11274
 
11197
11275
 
11276
+
11198
11277
  async evalMember(node, env) {
11199
11278
  const obj = await this.evaluate(node.object, env);
11200
11279
 
11201
- if (obj == null) throw new RuntimeError('Member access of null or undefined', node, this.source);
11280
+ if (obj == null) {
11281
+ throw new RuntimeError('Member access of null or undefined', node, this.source);
11282
+ }
11283
+
11284
+ const prop = obj[node.property];
11202
11285
 
11203
- return obj[node.property];
11286
+ if (typeof prop === 'function') {
11287
+ return prop.bind(obj);
11288
+ }
11289
+
11290
+ return prop;
11204
11291
  }
11205
11292
 
11206
11293
 
@@ -11234,7 +11321,7 @@ async evalUpdate(node, env) {
11234
11321
 
11235
11322
  }
11236
11323
 
11237
- module.exports = Evaluator;
11324
+ module.exports = Evaluator;
11238
11325
 
11239
11326
  /***/ }),
11240
11327
 
@@ -11271,7 +11358,7 @@ class Lexer {
11271
11358
  'break', 'continue', 'func', 'return',
11272
11359
  'true', 'false', 'null',
11273
11360
  'ask', 'define', 'import', 'from', 'as',
11274
- 'async', 'await', 'new', 'in', 'do', 'track', 'start', 'race'
11361
+ 'async', 'await', 'new', 'in', 'do', 'track', 'start', 'race', 'not', 'and', 'or'
11275
11362
  ];
11276
11363
  }
11277
11364
 
@@ -11354,6 +11441,8 @@ class Lexer {
11354
11441
  }
11355
11442
 
11356
11443
  if (this.keywords.includes(result)) {
11444
+ if (result === 'and') return { type: 'AND', value: 'and', line: startLine, column: startCol };
11445
+ if (result === 'or') return { type: 'OR', value: 'or', line: startLine, column: startCol };
11357
11446
  return { type: result.toUpperCase(), value: result, line: startLine, column: startCol };
11358
11447
  }
11359
11448
 
@@ -11469,7 +11558,7 @@ module.exports = Lexer;
11469
11558
  /***/ }),
11470
11559
 
11471
11560
  /***/ 222:
11472
- /***/ ((module) => {
11561
+ /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
11473
11562
 
11474
11563
  class ParseError extends Error {
11475
11564
  constructor(message, token, source) {
@@ -11729,14 +11818,18 @@ ifStatement() {
11729
11818
  test = this.expression();
11730
11819
  }
11731
11820
 
11732
- const consequent = this.block();
11821
+ const consequent = this.statementOrBlock();
11733
11822
 
11734
11823
  let alternate = null;
11735
11824
  if (this.current.type === 'ELSE') {
11736
- this.eat('ELSE');
11737
- if (this.current.type === 'IF') alternate = this.ifStatement();
11738
- else alternate = this.block();
11825
+ this.eat('ELSE');
11826
+ if (this.current.type === 'IF') {
11827
+ alternate = this.ifStatement();
11828
+ } else {
11829
+ alternate = this.statementOrBlock();
11739
11830
  }
11831
+ }
11832
+
11740
11833
 
11741
11834
  return {
11742
11835
  type: 'IfStatement',
@@ -11747,18 +11840,51 @@ ifStatement() {
11747
11840
  column: t.column
11748
11841
  };
11749
11842
  }
11843
+ parseExpressionOnly() {
11844
+ return this.expression();
11845
+ }
11750
11846
 
11751
11847
  whileStatement() {
11752
11848
  const t = this.current;
11753
11849
  this.eat('WHILE');
11754
11850
 
11755
11851
  let test;
11852
+
11756
11853
  if (this.current.type === 'LPAREN') {
11757
11854
  this.eat('LPAREN');
11758
11855
  test = this.expression();
11759
11856
  this.eat('RPAREN');
11760
- } else {
11761
- test = this.expression();
11857
+ }
11858
+ else {
11859
+ const startPos = this.pos;
11860
+ const startToken = this.current;
11861
+
11862
+ const exprTokens = [];
11863
+ let braceFound = false;
11864
+ let depth = 0;
11865
+
11866
+ while (this.current.type !== 'EOF') {
11867
+ if (this.current.type === 'LBRACE' && depth === 0) {
11868
+ braceFound = true;
11869
+ break;
11870
+ }
11871
+ if (this.current.type === 'LPAREN') depth++;
11872
+ if (this.current.type === 'RPAREN') depth--;
11873
+ exprTokens.push(this.current);
11874
+ this.advance();
11875
+ }
11876
+
11877
+ if (!braceFound) {
11878
+ throw new ParseError(
11879
+ "Expected '{' after while condition",
11880
+ this.current,
11881
+ this.source
11882
+ );
11883
+ }
11884
+
11885
+ const Parser = __nccwpck_require__(222);
11886
+ const exprParser = new Parser(exprTokens, this.source);
11887
+ test = exprParser.parseExpressionOnly();
11762
11888
  }
11763
11889
 
11764
11890
  const body = this.block();
@@ -11771,6 +11897,7 @@ whileStatement() {
11771
11897
  };
11772
11898
  }
11773
11899
 
11900
+
11774
11901
  importStatement() {
11775
11902
  const t = this.current;
11776
11903
  this.eat('IMPORT');
@@ -11970,6 +12097,12 @@ returnStatement() {
11970
12097
 
11971
12098
  return { type: 'ReturnStatement', argument, line: t.line, column: t.column };
11972
12099
  }
12100
+ statementOrBlock() {
12101
+ if (this.current.type === 'LBRACE') {
12102
+ return this.block();
12103
+ }
12104
+ return this.statement();
12105
+ }
11973
12106
 
11974
12107
  block() {
11975
12108
  const t = this.current; // LBRACE token
@@ -12179,13 +12312,7 @@ postfix() {
12179
12312
  continue;
12180
12313
  }
12181
12314
 
12182
- if (node.type === 'Identifier' && t.type === 'IDENTIFIER') {
12183
- const argNode = { type: 'Identifier', name: t.value, line: t.line, column: t.column };
12184
- this.eat('IDENTIFIER');
12185
- node = { type: 'CallExpression', callee: node, arguments: [argNode], line: node.line, column: node.column };
12186
- continue;
12187
- }
12188
-
12315
+
12189
12316
  break;
12190
12317
  }
12191
12318
  return node;
@@ -12339,21 +12466,56 @@ arrowFunction(params) {
12339
12466
  }
12340
12467
 
12341
12468
  if (t.type === 'LBRACE') {
12342
- const startLine = t.line;
12343
- const startCol = t.column;
12344
- this.eat('LBRACE');
12345
- const props = [];
12346
- while (this.current.type !== 'RBRACE') {
12347
- const key = this.expression();
12348
- this.eat('COLON');
12349
- const value = this.expression();
12350
- props.push({ key, value });
12351
- if (this.current.type === 'COMMA') this.eat('COMMA');
12469
+ const startLine = t.line;
12470
+ const startCol = t.column;
12471
+ this.eat('LBRACE');
12472
+
12473
+ const props = [];
12474
+
12475
+ while (this.current.type !== 'RBRACE') {
12476
+ let key;
12477
+
12478
+ if (this.current.type === 'IDENTIFIER') {
12479
+ const k = this.current;
12480
+ this.eat('IDENTIFIER');
12481
+ key = {
12482
+ type: 'Literal',
12483
+ value: k.value,
12484
+ line: k.line,
12485
+ column: k.column
12486
+ };
12352
12487
  }
12353
- this.eat('RBRACE');
12354
- return { type: 'ObjectExpression', props, line: startLine, column: startCol };
12488
+ else if (this.current.type === 'STRING') {
12489
+ const k = this.current;
12490
+ this.eat('STRING');
12491
+ key = {
12492
+ type: 'Literal',
12493
+ value: k.value,
12494
+ line: k.line,
12495
+ column: k.column
12496
+ };
12497
+ }
12498
+ else {
12499
+ throw new ParseError(
12500
+ 'Invalid object key',
12501
+ this.current,
12502
+ this.source
12503
+ );
12504
+ }
12505
+
12506
+ this.eat('COLON');
12507
+ const value = this.expression();
12508
+
12509
+ props.push({ key, value });
12510
+
12511
+ if (this.current.type === 'COMMA') this.eat('COMMA');
12355
12512
  }
12356
12513
 
12514
+ this.eat('RBRACE');
12515
+ return { type: 'ObjectExpression', props, line: startLine, column: startCol };
12516
+ }
12517
+
12518
+
12357
12519
  throw new ParseError(
12358
12520
  `Unexpected token '${t.type}'`,
12359
12521
  t,
@@ -12544,7 +12706,7 @@ const Lexer = __nccwpck_require__(211);
12544
12706
  const Parser = __nccwpck_require__(222);
12545
12707
  const Evaluator = __nccwpck_require__(112);
12546
12708
 
12547
- const VERSION = '1.1.9';
12709
+ const VERSION = '1.1.11';
12548
12710
 
12549
12711
  const COLOR = {
12550
12712
  reset: '\x1b[0m',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "starlight-cli",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
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,90 @@ 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
- }
176
-
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;
204
+ this.global.define('isNaN', arg => {
205
+ return typeof arg !== 'number' || Number.isNaN(arg);
206
+ });
207
+ this.global.define('random', (min, max) => {
208
+ if (max === undefined) {
209
+ // Only one argument random between 0 and min
210
+ return Math.floor(Math.random() * min);
211
+ }
212
+ min = Number(min);
213
+ max = Number(max);
214
+ if (isNaN(min) || isNaN(max)) return 0;
215
+ return Math.floor(Math.random() * (max - min)) + min;
182
216
  });
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
- }
190
217
 
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]);
218
+ this.global.define('map', async (array, fn) => {
219
+ if (!Array.isArray(array)) {
220
+ throw new RuntimeError('map() expects an array', null, evaluator.source);
195
221
  }
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
222
 
207
- let acc;
208
- let startIndex = 0;
223
+ const result = [];
224
+ for (let i = 0; i < array.length; i++) {
225
+ result.push(
226
+ await evaluator.callFunction(
227
+ fn,
228
+ [array[i], i, array],
229
+ evaluator.global
230
+ )
231
+ );
232
+ }
233
+ return result;
234
+ });
209
235
 
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);
236
+ this.global.define('filter', async (array, fn) => {
237
+ if (!Array.isArray(array)) {
238
+ throw new RuntimeError('filter() expects an array', null, evaluator.source);
215
239
  }
216
- acc = array[0];
217
- startIndex = 1;
218
- }
219
240
 
220
- for (let i = startIndex; i < array.length; i++) {
221
- acc = await fn(acc, array[i], i, array);
222
- }
241
+ const result = [];
242
+ for (let i = 0; i < array.length; i++) {
243
+ if (
244
+ await evaluator.callFunction(
245
+ fn,
246
+ [array[i], i, array],
247
+ evaluator.global
248
+ )
249
+ ) {
250
+ result.push(array[i]);
251
+ }
252
+ }
253
+ return result;
254
+ });
223
255
 
224
- return acc;
225
- });
256
+ this.global.define('reduce', async (array, fn, initial) => {
257
+ if (!Array.isArray(array)) {
258
+ throw new RuntimeError('reduce() expects an array', null, evaluator.source);
259
+ }
260
+
261
+ let acc;
262
+ let i = 0;
263
+
264
+ if (initial !== undefined) {
265
+ acc = initial;
266
+ } else {
267
+ if (array.length === 0) {
268
+ throw new RuntimeError(
269
+ 'reduce() of empty array with no initial value',
270
+ null,
271
+ evaluator.source
272
+ );
273
+ }
274
+ acc = array[0];
275
+ i = 1;
276
+ }
277
+
278
+ for (; i < array.length; i++) {
279
+ acc = await evaluator.callFunction(
280
+ fn,
281
+ [acc, array[i], i, array],
282
+ evaluator.global
283
+ );
284
+ }
285
+
286
+ return acc;
287
+ });
226
288
 
227
289
  this.global.define('keys', arg => arg && typeof arg === 'object' ? Object.keys(arg) : []);
228
290
  this.global.define('values', arg => arg && typeof arg === 'object' ? Object.values(arg) : []);
@@ -376,21 +438,30 @@ case 'RaceClause':
376
438
  const callee = await this.evaluate(node.callee, env);
377
439
 
378
440
  if (typeof callee === 'object' && callee.body) {
379
- const evaluator = this;
380
- const Constructor = function(...args) {
441
+ const evaluator = this;
442
+
443
+ const Constructor = function (...args) {
381
444
  const newEnv = new Environment(callee.env);
382
445
  newEnv.define('this', this);
446
+
383
447
  for (let i = 0; i < callee.params.length; i++) {
384
- newEnv.define(callee.params[i], args[i]);
448
+ const param = callee.params[i];
449
+ const paramName = typeof param === 'string' ? param : param.name;
450
+ newEnv.define(paramName, args[i]);
385
451
  }
386
- return evaluator.evaluate(callee.body, newEnv);
452
+
453
+ return (async () => {
454
+ await evaluator.evaluate(callee.body, newEnv);
455
+ return this;
456
+ })();
387
457
  };
388
458
 
389
459
  const args = [];
390
460
  for (const a of node.arguments) args.push(await this.evaluate(a, env));
391
- return new Constructor(...args);
461
+ return await new Constructor(...args);
392
462
  }
393
463
 
464
+ // native JS constructor fallback
394
465
  if (typeof callee !== 'function') {
395
466
  throw new RuntimeError('NewExpression callee is not a function', node, this.source);
396
467
  }
@@ -612,27 +683,31 @@ evalArrowFunction(node, env) {
612
683
  return async function (...args) {
613
684
  const localEnv = new Environment(env);
614
685
 
686
+ // Bind parameters safely
615
687
  node.params.forEach((p, i) => {
616
- localEnv.define(p.name, args[i]);
688
+ const paramName = typeof p === 'string' ? p : p.name;
689
+ localEnv.define(paramName, args[i]);
617
690
  });
618
691
 
619
692
  try {
620
693
  if (node.isBlock) {
694
+ // Block body
621
695
  const result = await evaluator.evaluate(node.body, localEnv);
622
- return result ?? null;
696
+ return result === undefined ? null : result; // ensure null instead of undefined
623
697
  } else {
624
- return await evaluator.evaluate(node.body, localEnv);
698
+ // Expression body
699
+ const result = await evaluator.evaluate(node.body, localEnv);
700
+ return result === undefined ? null : result; // ensure null instead of undefined
625
701
  }
626
702
  } catch (err) {
627
- if (err instanceof ReturnValue) {
628
- return err.value;
629
- }
703
+ if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
630
704
  throw err;
631
705
  }
632
706
  };
633
707
  }
634
708
 
635
709
 
710
+
636
711
  async evalAssignment(node, env) {
637
712
  const rightVal = await this.evaluate(node.right, env);
638
713
  const left = node.left;
@@ -793,13 +868,10 @@ async evalUnary(node, env) {
793
868
 
794
869
  }
795
870
  }
796
-
797
871
  async evalIf(node, env) {
798
- const test = await this.evaluate(node.test, env);
872
+ let test = await this.evaluate(node.test, env);
799
873
 
800
- if (typeof test !== 'boolean') {
801
- throw new RuntimeError('If condition must evaluate to a boolean', node.test, this.source);
802
- }
874
+ test = !!test;
803
875
 
804
876
  if (test) {
805
877
  return await this.evaluate(node.consequent, env);
@@ -812,27 +884,26 @@ async evalIf(node, env) {
812
884
  return null;
813
885
  }
814
886
 
815
-
816
887
  async evalWhile(node, env) {
817
888
  while (true) {
818
- const test = await this.evaluate(node.test, env);
889
+ let test = await this.evaluate(node.test, env);
819
890
 
820
- if (typeof test !== 'boolean') {
821
- throw new RuntimeError('While condition must evaluate to a boolean', node.test, this.source);
822
- }
891
+ test = !!test;
823
892
 
824
893
  if (!test) break;
825
894
 
826
895
  try {
827
896
  await this.evaluate(node.body, env);
828
897
  } catch (e) {
829
- if (e instanceof BreakSignal) break;
830
- if (e instanceof ContinueSignal) continue;
831
- throw e;
898
+ if (e instanceof BreakSignal) break;
899
+ if (e instanceof ContinueSignal) continue;
900
+ throw e;
832
901
  }
833
902
  }
903
+
834
904
  return null;
835
905
  }
906
+
836
907
  async evalFor(node, env) {
837
908
  if (node.type === 'ForInStatement') {
838
909
  const iterable = await this.evaluate(node.iterable, env);
@@ -973,24 +1044,40 @@ async evalIndex(node, env) {
973
1044
  async evalObject(node, env) {
974
1045
  const out = {};
975
1046
  for (const p of node.props) {
976
- if (!p.key || !p.value) {
977
- throw new RuntimeError('Invalid object property', node, this.source);
1047
+ if (!p.key) {
1048
+ throw new RuntimeError('Object property must have a key', node, this.source);
978
1049
  }
1050
+
979
1051
  const key = await this.evaluate(p.key, env);
980
- const value = await this.evaluate(p.value, env);
981
- out[key] = value; // dynamic property assignment
1052
+ let value = null;
1053
+
1054
+ if (p.value) {
1055
+ value = await this.evaluate(p.value, env);
1056
+ if (value === undefined) value = null; // <- force null instead of undefined
1057
+ }
1058
+
1059
+ out[key] = value;
982
1060
  }
983
1061
  return out;
984
1062
  }
985
1063
 
986
1064
 
987
1065
 
1066
+
988
1067
  async evalMember(node, env) {
989
1068
  const obj = await this.evaluate(node.object, env);
990
1069
 
991
- if (obj == null) throw new RuntimeError('Member access of null or undefined', node, this.source);
1070
+ if (obj == null) {
1071
+ throw new RuntimeError('Member access of null or undefined', node, this.source);
1072
+ }
1073
+
1074
+ const prop = obj[node.property];
1075
+
1076
+ if (typeof prop === 'function') {
1077
+ return prop.bind(obj);
1078
+ }
992
1079
 
993
- return obj[node.property];
1080
+ return prop;
994
1081
  }
995
1082
 
996
1083
 
@@ -1024,4 +1111,4 @@ async evalUpdate(node, env) {
1024
1111
 
1025
1112
  }
1026
1113
 
1027
- module.exports = Evaluator;
1114
+ module.exports = Evaluator;
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', 'start', 'race'
31
+ 'async', 'await', 'new', 'in', 'do', 'track', 'start', 'race', 'not', 'and', 'or'
32
32
  ];
33
33
  }
34
34
 
@@ -111,6 +111,8 @@ class Lexer {
111
111
  }
112
112
 
113
113
  if (this.keywords.includes(result)) {
114
+ if (result === 'and') return { type: 'AND', value: 'and', line: startLine, column: startCol };
115
+ if (result === 'or') return { type: 'OR', value: 'or', line: startLine, column: startCol };
114
116
  return { type: result.toUpperCase(), value: result, line: startLine, column: startCol };
115
117
  }
116
118
 
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',
@@ -274,18 +278,51 @@ ifStatement() {
274
278
  column: t.column
275
279
  };
276
280
  }
281
+ parseExpressionOnly() {
282
+ return this.expression();
283
+ }
277
284
 
278
285
  whileStatement() {
279
286
  const t = this.current;
280
287
  this.eat('WHILE');
281
288
 
282
289
  let test;
290
+
283
291
  if (this.current.type === 'LPAREN') {
284
292
  this.eat('LPAREN');
285
293
  test = this.expression();
286
294
  this.eat('RPAREN');
287
- } else {
288
- test = this.expression();
295
+ }
296
+ else {
297
+ const startPos = this.pos;
298
+ const startToken = this.current;
299
+
300
+ const exprTokens = [];
301
+ let braceFound = false;
302
+ let depth = 0;
303
+
304
+ while (this.current.type !== 'EOF') {
305
+ if (this.current.type === 'LBRACE' && depth === 0) {
306
+ braceFound = true;
307
+ break;
308
+ }
309
+ if (this.current.type === 'LPAREN') depth++;
310
+ if (this.current.type === 'RPAREN') depth--;
311
+ exprTokens.push(this.current);
312
+ this.advance();
313
+ }
314
+
315
+ if (!braceFound) {
316
+ throw new ParseError(
317
+ "Expected '{' after while condition",
318
+ this.current,
319
+ this.source
320
+ );
321
+ }
322
+
323
+ const Parser = require('./parser');
324
+ const exprParser = new Parser(exprTokens, this.source);
325
+ test = exprParser.parseExpressionOnly();
289
326
  }
290
327
 
291
328
  const body = this.block();
@@ -298,6 +335,7 @@ whileStatement() {
298
335
  };
299
336
  }
300
337
 
338
+
301
339
  importStatement() {
302
340
  const t = this.current;
303
341
  this.eat('IMPORT');
@@ -497,6 +535,12 @@ returnStatement() {
497
535
 
498
536
  return { type: 'ReturnStatement', argument, line: t.line, column: t.column };
499
537
  }
538
+ statementOrBlock() {
539
+ if (this.current.type === 'LBRACE') {
540
+ return this.block();
541
+ }
542
+ return this.statement();
543
+ }
500
544
 
501
545
  block() {
502
546
  const t = this.current; // LBRACE token
@@ -706,13 +750,7 @@ postfix() {
706
750
  continue;
707
751
  }
708
752
 
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
-
753
+
716
754
  break;
717
755
  }
718
756
  return node;
@@ -866,21 +904,56 @@ arrowFunction(params) {
866
904
  }
867
905
 
868
906
  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');
907
+ const startLine = t.line;
908
+ const startCol = t.column;
909
+ this.eat('LBRACE');
910
+
911
+ const props = [];
912
+
913
+ while (this.current.type !== 'RBRACE') {
914
+ let key;
915
+
916
+ if (this.current.type === 'IDENTIFIER') {
917
+ const k = this.current;
918
+ this.eat('IDENTIFIER');
919
+ key = {
920
+ type: 'Literal',
921
+ value: k.value,
922
+ line: k.line,
923
+ column: k.column
924
+ };
879
925
  }
880
- this.eat('RBRACE');
881
- return { type: 'ObjectExpression', props, line: startLine, column: startCol };
926
+ else if (this.current.type === 'STRING') {
927
+ const k = this.current;
928
+ this.eat('STRING');
929
+ key = {
930
+ type: 'Literal',
931
+ value: k.value,
932
+ line: k.line,
933
+ column: k.column
934
+ };
935
+ }
936
+ else {
937
+ throw new ParseError(
938
+ 'Invalid object key',
939
+ this.current,
940
+ this.source
941
+ );
942
+ }
943
+
944
+ this.eat('COLON');
945
+ const value = this.expression();
946
+
947
+ props.push({ key, value });
948
+
949
+ if (this.current.type === 'COMMA') this.eat('COMMA');
882
950
  }
883
951
 
952
+ this.eat('RBRACE');
953
+ return { type: 'ObjectExpression', props, line: startLine, column: startCol };
954
+ }
955
+
956
+
884
957
  throw new ParseError(
885
958
  `Unexpected token '${t.type}'`,
886
959
  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.9';
15
+ const VERSION = '1.1.11';
16
16
 
17
17
  const COLOR = {
18
18
  reset: '\x1b[0m',