starlight-cli 1.1.10 → 1.1.12

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/dist/index.js CHANGED
@@ -10220,10 +10220,9 @@ class ReturnValue {
10220
10220
  class BreakSignal {}
10221
10221
  class ContinueSignal {}
10222
10222
  class RuntimeError extends Error {
10223
- constructor(message, node, source) {
10223
+ constructor(message, node, source, env = null) {
10224
10224
  const line = node?.line ?? '?';
10225
10225
  const column = node?.column ?? '?';
10226
-
10227
10226
  let output = ` ${message} at line ${line}, column ${column}\n`;
10228
10227
 
10229
10228
  if (source && node?.line != null) {
@@ -10231,23 +10230,55 @@ class RuntimeError extends Error {
10231
10230
  const srcLine = lines[node.line - 1] || '';
10232
10231
  output += ` ${srcLine}\n`;
10233
10232
  const caretPos =
10234
- typeof column === 'number' && column > 0
10235
- ? column - 1
10236
- : 0;
10233
+ typeof column === 'number' && column > 0
10234
+ ? column - 1
10235
+ : 0;
10236
+ output += ` ${' '.repeat(caretPos)}^\n`;
10237
+ }
10237
10238
 
10238
- output += ` ${' '.repeat(caretPos)}^\n`;
10239
- ;
10239
+ if (env && message.startsWith('Undefined variable:')) {
10240
+ const nameMatch = message.match(/"(.+?)"/);
10241
+ if (nameMatch) {
10242
+ const name = nameMatch[1];
10243
+ const suggestion = RuntimeError.suggest(name, env);
10244
+ if (suggestion) {
10245
+ output += `Did you mean "${suggestion}"?\n`;
10246
+ }
10247
+ }
10240
10248
  }
10241
10249
 
10242
10250
  super(output);
10243
-
10244
10251
  this.name = 'RuntimeError';
10245
10252
  this.line = line;
10246
10253
  this.column = column;
10247
10254
  }
10248
- }
10249
10255
 
10256
+ static suggest(name, env) {
10257
+ const names = new Set();
10258
+ let current = env;
10259
+ while (current) {
10260
+ for (const key of Object.keys(current.store)) {
10261
+ names.add(key);
10262
+ }
10263
+ current = current.parent;
10264
+ }
10265
+
10266
+ let best = null;
10267
+ let bestScore = Infinity;
10268
+
10269
+ for (const item of names) {
10270
+ const dist = Math.abs(item.length - name.length) +
10271
+ [...name].filter((c, i) => c !== item[i]).length;
10272
+
10273
+ if (dist < bestScore && dist <= 2) {
10274
+ bestScore = dist;
10275
+ best = item;
10276
+ }
10277
+ }
10250
10278
 
10279
+ return best;
10280
+ }
10281
+ }
10251
10282
 
10252
10283
  class Environment {
10253
10284
  constructor(parent = null) {
@@ -10265,16 +10296,7 @@ class Environment {
10265
10296
  if (name in this.store) return this.store[name];
10266
10297
  if (this.parent) return this.parent.get(name, node, source);
10267
10298
 
10268
- let suggestion = null;
10269
- if (node && source) {
10270
- suggestion = this.suggest?.(name, this) || null;
10271
- }
10272
-
10273
- const message = suggestion
10274
- ? `Undefined variable: "${name}". Did you mean "${suggestion}"?`
10275
- : `Undefined variable: "${name}"`;
10276
-
10277
- throw new RuntimeError(message, node, source);
10299
+ throw new RuntimeError(`Undefined variable: "${name}"`, node, source, this);
10278
10300
  }
10279
10301
 
10280
10302
 
@@ -10326,31 +10348,7 @@ async callFunction(fn, args, env, node = null) {
10326
10348
  }
10327
10349
 
10328
10350
 
10329
- suggest(name, env) {
10330
- const names = new Set();
10331
- let current = env;
10332
- while (current) {
10333
- for (const key of Object.keys(current.store)) {
10334
- names.add(key);
10335
- }
10336
- current = current.parent;
10337
- }
10338
-
10339
- let best = null;
10340
- let bestScore = Infinity;
10341
-
10342
- for (const item of names) {
10343
- const dist = Math.abs(item.length - name.length) +
10344
- [...name].filter((c, i) => c !== item[i]).length;
10345
-
10346
- if (dist < bestScore && dist <= 2) {
10347
- bestScore = dist;
10348
- best = item;
10349
- }
10350
- }
10351
10351
 
10352
- return best;
10353
- }
10354
10352
  formatValue(value, seen = new Set()) {
10355
10353
  const color = __nccwpck_require__(55);
10356
10354
 
@@ -10411,6 +10409,20 @@ formatValue(value, seen = new Set()) {
10411
10409
  if (Array.isArray(arg)) return 'array';
10412
10410
  return typeof arg;
10413
10411
  });
10412
+ this.global.define('isNaN', arg => {
10413
+ return typeof arg !== 'number' || Number.isNaN(arg);
10414
+ });
10415
+ this.global.define('random', (min, max) => {
10416
+ if (max === undefined) {
10417
+ // Only one argument → random between 0 and min
10418
+ return Math.floor(Math.random() * min);
10419
+ }
10420
+ min = Number(min);
10421
+ max = Number(max);
10422
+ if (isNaN(min) || isNaN(max)) return 0;
10423
+ return Math.floor(Math.random() * (max - min)) + min;
10424
+ });
10425
+
10414
10426
  this.global.define('map', async (array, fn) => {
10415
10427
  if (!Array.isArray(array)) {
10416
10428
  throw new RuntimeError('map() expects an array', null, evaluator.source);
@@ -10679,10 +10691,17 @@ async evalProgram(node, env) {
10679
10691
  try {
10680
10692
  result = await this.evaluate(stmt, env);
10681
10693
  } catch (e) {
10694
+ // Re-throw known runtime control signals
10682
10695
  if (e instanceof RuntimeError || e instanceof BreakSignal || e instanceof ContinueSignal || e instanceof ReturnValue) {
10683
10696
  throw e;
10684
10697
  }
10685
- throw new RuntimeError(e.message || 'Error in program', stmt, this.source);
10698
+ // Wrap unexpected errors with RuntimeError including env for suggestions
10699
+ throw new RuntimeError(
10700
+ e.message || 'Error in program',
10701
+ stmt,
10702
+ this.source,
10703
+ env
10704
+ );
10686
10705
  }
10687
10706
  }
10688
10707
  return result;
@@ -10692,6 +10711,7 @@ async evalStartStatement(node, env) {
10692
10711
  try {
10693
10712
  const value = await this.evaluate(node.discriminant, env);
10694
10713
  let executing = false;
10714
+
10695
10715
  for (const c of node.cases) {
10696
10716
  try {
10697
10717
  if (!executing) {
@@ -10709,11 +10729,11 @@ async evalStartStatement(node, env) {
10709
10729
  caseErr instanceof ContinueSignal) {
10710
10730
  throw caseErr; // propagate signals
10711
10731
  }
10712
-
10713
10732
  throw new RuntimeError(
10714
10733
  caseErr.message || 'Error evaluating case in start statement',
10715
10734
  c,
10716
- this.source
10735
+ this.source,
10736
+ env
10717
10737
  );
10718
10738
  }
10719
10739
  }
@@ -10729,28 +10749,31 @@ async evalStartStatement(node, env) {
10729
10749
  throw new RuntimeError(
10730
10750
  err.message || 'Error evaluating start statement',
10731
10751
  node,
10732
- this.source
10752
+ this.source,
10753
+ env
10733
10754
  );
10734
10755
  }
10735
10756
  }
10736
10757
 
10737
-
10738
10758
  async evalRaceClause(node, env) {
10739
10759
  try {
10740
10760
  const testValue = await this.evaluate(node.test, env);
10741
10761
  const result = await this.evaluate(node.consequent, new Environment(env));
10742
10762
  return { testValue, result };
10743
10763
  } catch (err) {
10744
- if (err instanceof RuntimeError ||
10745
- err instanceof ReturnValue ||
10746
- err instanceof BreakSignal ||
10747
- err instanceof ContinueSignal) {
10764
+ if (
10765
+ err instanceof RuntimeError ||
10766
+ err instanceof ReturnValue ||
10767
+ err instanceof BreakSignal ||
10768
+ err instanceof ContinueSignal
10769
+ ) {
10748
10770
  throw err;
10749
10771
  }
10750
10772
  throw new RuntimeError(
10751
10773
  err.message || 'Error evaluating race clause',
10752
10774
  node,
10753
- this.source
10775
+ this.source,
10776
+ env
10754
10777
  );
10755
10778
  }
10756
10779
  }
@@ -10761,7 +10784,12 @@ async evalDoTrack(node, env) {
10761
10784
  } catch (err) {
10762
10785
  if (!node.handler) {
10763
10786
  if (err instanceof RuntimeError) throw err;
10764
- throw new RuntimeError(err.message || 'Error in doTrack body', node.body, this.source);
10787
+ throw new RuntimeError(
10788
+ err.message || 'Error in doTrack body',
10789
+ node.body,
10790
+ this.source,
10791
+ env
10792
+ );
10765
10793
  }
10766
10794
 
10767
10795
  const trackEnv = new Environment(env);
@@ -10771,12 +10799,16 @@ async evalDoTrack(node, env) {
10771
10799
  return await this.evaluate(node.handler, trackEnv);
10772
10800
  } catch (handlerErr) {
10773
10801
  if (handlerErr instanceof RuntimeError) throw handlerErr;
10774
- throw new RuntimeError(handlerErr.message || 'Error in doTrack handler', node.handler, this.source);
10802
+ throw new RuntimeError(
10803
+ handlerErr.message || 'Error in doTrack handler',
10804
+ node.handler,
10805
+ this.source,
10806
+ trackEnv
10807
+ );
10775
10808
  }
10776
10809
  }
10777
10810
  }
10778
10811
 
10779
-
10780
10812
  async evalImport(node, env) {
10781
10813
  const spec = node.path;
10782
10814
  let lib;
@@ -10792,34 +10824,51 @@ async evalImport(node, env) {
10792
10824
  : path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
10793
10825
 
10794
10826
  if (!fs.existsSync(fullPath)) {
10795
- throw new RuntimeError(`Import not found: ${spec}`, node, this.source);
10827
+ throw new RuntimeError(
10828
+ `Import not found: ${spec}`,
10829
+ node,
10830
+ this.source,
10831
+ env
10832
+ );
10796
10833
  }
10797
10834
 
10798
- const code = fs.readFileSync(fullPath, 'utf-8');
10799
- const tokens = new Lexer(code).getTokens();
10800
- const ast = new Parser(tokens).parse();
10835
+ try {
10836
+ const code = fs.readFileSync(fullPath, 'utf-8');
10837
+ const tokens = new Lexer(code).getTokens();
10838
+ const ast = new Parser(tokens).parse();
10801
10839
 
10802
- const moduleEnv = new Environment(env);
10803
- await this.evaluate(ast, moduleEnv);
10840
+ const moduleEnv = new Environment(env);
10841
+ await this.evaluate(ast, moduleEnv);
10804
10842
 
10805
- lib = {};
10806
- for (const key of Object.keys(moduleEnv.store)) {
10807
- lib[key] = moduleEnv.store[key];
10808
- }
10843
+ lib = {};
10844
+ for (const key of Object.keys(moduleEnv.store)) {
10845
+ lib[key] = moduleEnv.store[key];
10846
+ }
10809
10847
 
10810
- lib.default = lib;
10848
+ lib.default = lib;
10849
+ } catch (parseErr) {
10850
+ throw new RuntimeError(
10851
+ parseErr.message || `Failed to import module: ${spec}`,
10852
+ node,
10853
+ this.source,
10854
+ env
10855
+ );
10856
+ }
10811
10857
  }
10812
10858
 
10813
10859
  for (const imp of node.specifiers) {
10814
10860
  if (imp.type === 'DefaultImport') {
10815
10861
  env.define(imp.local, lib.default ?? lib);
10816
- }
10817
- if (imp.type === 'NamespaceImport') {
10862
+ } else if (imp.type === 'NamespaceImport') {
10818
10863
  env.define(imp.local, lib);
10819
- }
10820
- if (imp.type === 'NamedImport') {
10864
+ } else if (imp.type === 'NamedImport') {
10821
10865
  if (!(imp.imported in lib)) {
10822
- throw new RuntimeError(`Module '${spec}' has no export '${imp.imported}'`, node, this.source);
10866
+ throw new RuntimeError(
10867
+ `Module '${spec}' has no export '${imp.imported}'`,
10868
+ node,
10869
+ this.source,
10870
+ env
10871
+ );
10823
10872
  }
10824
10873
  env.define(imp.local, lib[imp.imported]);
10825
10874
  }
@@ -10827,7 +10876,6 @@ async evalImport(node, env) {
10827
10876
 
10828
10877
  return null;
10829
10878
  }
10830
-
10831
10879
  async evalBlock(node, env) {
10832
10880
  let result = null;
10833
10881
  for (const stmt of node.body) {
@@ -10846,32 +10894,55 @@ async evalBlock(node, env) {
10846
10894
  throw new RuntimeError(
10847
10895
  e.message || 'Error in block',
10848
10896
  stmt,
10849
- this.source
10897
+ this.source,
10898
+ env // pass env for suggestions
10850
10899
  );
10851
10900
  }
10852
10901
  }
10853
10902
  return result;
10854
10903
  }
10855
10904
 
10856
-
10857
-
10858
10905
  async evalVarDeclaration(node, env) {
10859
10906
  if (!node.expr) {
10860
- throw new RuntimeError('Variable declaration requires an initializer', node, this.source);
10907
+ throw new RuntimeError(
10908
+ 'Variable declaration requires an initializer',
10909
+ node,
10910
+ this.source,
10911
+ env // pass env for suggestions
10912
+ );
10861
10913
  }
10862
10914
 
10863
- const val = await this.evaluate(node.expr, env);
10864
- return env.define(node.id, val);
10915
+ try {
10916
+ const val = await this.evaluate(node.expr, env);
10917
+ return env.define(node.id, val);
10918
+ } catch (e) {
10919
+ if (e instanceof RuntimeError) throw e;
10920
+ throw new RuntimeError(
10921
+ e.message || 'Error evaluating variable declaration',
10922
+ node,
10923
+ this.source,
10924
+ env
10925
+ );
10926
+ }
10865
10927
  }
10866
10928
 
10867
-
10868
10929
  evalArrowFunction(node, env) {
10869
10930
  if (!node.body) {
10870
- throw new RuntimeError('Arrow function missing body', node, this.source);
10931
+ throw new RuntimeError(
10932
+ 'Arrow function missing body',
10933
+ node,
10934
+ this.source,
10935
+ env
10936
+ );
10871
10937
  }
10872
10938
 
10873
10939
  if (!Array.isArray(node.params)) {
10874
- throw new RuntimeError('Invalid arrow function parameters', node, this.source);
10940
+ throw new RuntimeError(
10941
+ 'Invalid arrow function parameters',
10942
+ node,
10943
+ this.source,
10944
+ env
10945
+ );
10875
10946
  }
10876
10947
 
10877
10948
  const evaluator = this;
@@ -10879,7 +10950,6 @@ evalArrowFunction(node, env) {
10879
10950
  return async function (...args) {
10880
10951
  const localEnv = new Environment(env);
10881
10952
 
10882
- // Bind parameters safely
10883
10953
  node.params.forEach((p, i) => {
10884
10954
  const paramName = typeof p === 'string' ? p : p.name;
10885
10955
  localEnv.define(paramName, args[i]);
@@ -10887,78 +10957,128 @@ evalArrowFunction(node, env) {
10887
10957
 
10888
10958
  try {
10889
10959
  if (node.isBlock) {
10890
- // Block body
10891
10960
  const result = await evaluator.evaluate(node.body, localEnv);
10892
- return result === undefined ? null : result; // ensure null instead of undefined
10961
+ return result === undefined ? null : result;
10893
10962
  } else {
10894
- // Expression body
10895
10963
  const result = await evaluator.evaluate(node.body, localEnv);
10896
- return result === undefined ? null : result; // ensure null instead of undefined
10964
+ return result === undefined ? null : result;
10897
10965
  }
10898
10966
  } catch (err) {
10899
10967
  if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
10900
- throw err;
10968
+ if (err instanceof RuntimeError) throw err; // preserve RuntimeErrors
10969
+ throw new RuntimeError(
10970
+ err.message || 'Error evaluating arrow function',
10971
+ node,
10972
+ evaluator.source,
10973
+ localEnv
10974
+ );
10901
10975
  }
10902
10976
  };
10903
10977
  }
10904
10978
 
10905
-
10906
-
10907
10979
  async evalAssignment(node, env) {
10908
10980
  const rightVal = await this.evaluate(node.right, env);
10909
10981
  const left = node.left;
10910
10982
 
10911
- if (left.type === 'Identifier') return env.set(left.name, rightVal);
10983
+ try {
10984
+ if (left.type === 'Identifier') return env.set(left.name, rightVal);
10985
+
10986
+ if (left.type === 'MemberExpression') {
10987
+ const obj = await this.evaluate(left.object, env);
10988
+ if (obj == null) throw new RuntimeError(
10989
+ 'Cannot assign to null or undefined',
10990
+ node,
10991
+ this.source,
10992
+ env
10993
+ );
10994
+ obj[left.property] = rightVal; // dynamic creation allowed
10995
+ return rightVal;
10996
+ }
10912
10997
 
10913
- if (left.type === 'MemberExpression') {
10914
- const obj = await this.evaluate(left.object, env);
10915
- if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
10916
- obj[left.property] = rightVal; // dynamic creation of new properties allowed
10917
- return rightVal;
10918
- }
10998
+ if (left.type === 'IndexExpression') {
10999
+ const obj = await this.evaluate(left.object, env);
11000
+ const idx = await this.evaluate(left.indexer, env);
11001
+ if (obj == null) throw new RuntimeError(
11002
+ 'Cannot assign to null or undefined',
11003
+ node,
11004
+ this.source,
11005
+ env
11006
+ );
11007
+ obj[idx] = rightVal; // dynamic creation allowed
11008
+ return rightVal;
11009
+ }
10919
11010
 
10920
- if (left.type === 'IndexExpression') {
10921
- const obj = await this.evaluate(left.object, env);
10922
- const idx = await this.evaluate(left.indexer, env);
10923
- if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
10924
- obj[idx] = rightVal; // dynamic creation allowed
10925
- return rightVal;
10926
- }
11011
+ throw new RuntimeError(
11012
+ 'Invalid assignment target',
11013
+ node,
11014
+ this.source,
11015
+ env
11016
+ );
10927
11017
 
10928
- throw new RuntimeError('Invalid assignment target', node, this.source);
11018
+ } catch (e) {
11019
+ if (e instanceof RuntimeError) throw e;
11020
+ throw new RuntimeError(
11021
+ e.message || 'Error in assignment',
11022
+ node,
11023
+ this.source,
11024
+ env
11025
+ );
11026
+ }
10929
11027
  }
10930
11028
 
10931
11029
  async evalCompoundAssignment(node, env) {
10932
11030
  const left = node.left;
10933
11031
  let current;
10934
11032
 
10935
- if (left.type === 'Identifier') current = env.get(left.name, left, this.source);
10936
- else if (left.type === 'MemberExpression') current = await this.evalMember(left, env) ?? 0;
10937
- else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env) ?? 0;
10938
- else throw new RuntimeError('Invalid compound assignment target', node, this.source);
11033
+ try {
11034
+ if (left.type === 'Identifier') current = env.get(left.name, left, this.source);
11035
+ else if (left.type === 'MemberExpression') current = await this.evalMember(left, env) ?? 0;
11036
+ else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env) ?? 0;
11037
+ else throw new RuntimeError(
11038
+ 'Invalid compound assignment target',
11039
+ node,
11040
+ this.source,
11041
+ env
11042
+ );
10939
11043
 
10940
- const rhs = await this.evaluate(node.right, env);
10941
- let computed;
11044
+ const rhs = await this.evaluate(node.right, env);
11045
+ let computed;
11046
+
11047
+ switch (node.operator) {
11048
+ case 'PLUSEQ': computed = current + rhs; break;
11049
+ case 'MINUSEQ': computed = current - rhs; break;
11050
+ case 'STAREQ': computed = current * rhs; break;
11051
+ case 'SLASHEQ': computed = current / rhs; break;
11052
+ case 'MODEQ': computed = current % rhs; break;
11053
+ default: throw new RuntimeError(
11054
+ `Unknown compound operator: ${node.operator}`,
11055
+ node,
11056
+ this.source,
11057
+ env
11058
+ );
11059
+ }
10942
11060
 
10943
- switch (node.operator) {
10944
- case 'PLUSEQ': computed = current + rhs; break;
10945
- case 'MINUSEQ': computed = current - rhs; break;
10946
- case 'STAREQ': computed = current * rhs; break;
10947
- case 'SLASHEQ': computed = current / rhs; break;
10948
- case 'MODEQ': computed = current % rhs; break;
10949
- default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node, this.source);
10950
- }
11061
+ if (left.type === 'Identifier') env.set(left.name, computed);
11062
+ else if (left.type === 'MemberExpression' || left.type === 'IndexExpression') {
11063
+ await this.evalAssignment(
11064
+ { left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' },
11065
+ env
11066
+ );
11067
+ }
10951
11068
 
10952
- if (left.type === 'Identifier') env.set(left.name, computed);
10953
- else if (left.type === 'MemberExpression')
10954
- await this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
10955
- else
10956
- await this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
11069
+ return computed;
10957
11070
 
10958
- return computed;
11071
+ } catch (e) {
11072
+ if (e instanceof RuntimeError) throw e;
11073
+ throw new RuntimeError(
11074
+ e.message || 'Error in compound assignment',
11075
+ node,
11076
+ this.source,
11077
+ env
11078
+ );
11079
+ }
10959
11080
  }
10960
11081
 
10961
-
10962
11082
  async evalSldeploy(node, env) {
10963
11083
  const val = await this.evaluate(node.expr, env);
10964
11084
  console.log(this.formatValue(val));
@@ -10966,111 +11086,162 @@ async evalSldeploy(node, env) {
10966
11086
  }
10967
11087
 
10968
11088
 
10969
-
10970
11089
  async evalAsk(node, env) {
10971
- const prompt = await this.evaluate(node.prompt, env);
10972
-
10973
- if (typeof prompt !== 'string') {
10974
- throw new RuntimeError('ask() prompt must be a string', node, this.source);
10975
- }
11090
+ try {
11091
+ const prompt = await this.evaluate(node.prompt, env);
10976
11092
 
10977
- const input = readlineSync.question(prompt + ' ');
10978
- return input;
10979
- }
11093
+ if (typeof prompt !== 'string') {
11094
+ throw new RuntimeError('ask() prompt must be a string', node, this.source, env);
11095
+ }
10980
11096
 
11097
+ const input = readlineSync.question(prompt + ' ');
11098
+ return input;
10981
11099
 
10982
- async evalDefine(node, env) {
10983
- if (!node.id || typeof node.id !== 'string') {
10984
- throw new RuntimeError('Invalid identifier in define statement', node, this.source);
11100
+ } catch (e) {
11101
+ if (e instanceof RuntimeError) throw e;
11102
+ throw new RuntimeError(
11103
+ e.message || 'Error evaluating ask()',
11104
+ node,
11105
+ this.source,
11106
+ env
11107
+ );
10985
11108
  }
10986
-
10987
- const val = node.expr ? await this.evaluate(node.expr, env) : null;
10988
- return env.define(node.id, val);
10989
-
10990
11109
  }
10991
11110
 
11111
+ async evalDefine(node, env) {
11112
+ try {
11113
+ if (!node.id || typeof node.id !== 'string') {
11114
+ throw new RuntimeError('Invalid identifier in define statement', node, this.source, env);
11115
+ }
10992
11116
 
10993
- async evalBinary(node, env) {
10994
- const l = await this.evaluate(node.left, env);
10995
- const r = await this.evaluate(node.right, env);
10996
-
10997
- if (node.operator === 'SLASH' && r === 0) {
10998
- throw new RuntimeError('Division by zero', node, this.source);
10999
- }
11000
-
11001
- switch (node.operator) {
11002
- case 'PLUS': {
11003
- if (Array.isArray(l) && Array.isArray(r)) {
11004
- return l.concat(r);
11005
- }
11117
+ const val = node.expr ? await this.evaluate(node.expr, env) : null;
11118
+ return env.define(node.id, val);
11006
11119
 
11007
- if (typeof l === 'string' || typeof r === 'string') {
11008
- return String(l) + String(r);
11120
+ } catch (e) {
11121
+ if (e instanceof RuntimeError) throw e;
11122
+ throw new RuntimeError(
11123
+ e.message || 'Error in define statement',
11124
+ node,
11125
+ this.source,
11126
+ env
11127
+ );
11009
11128
  }
11129
+ }
11010
11130
 
11011
- if (typeof l === 'number' && typeof r === 'number') {
11012
- return l + r;
11013
- }
11131
+ async evalBinary(node, env) {
11132
+ try {
11133
+ const l = await this.evaluate(node.left, env);
11134
+ const r = await this.evaluate(node.right, env);
11014
11135
 
11015
- if (typeof l === 'object' && typeof r === 'object') {
11016
- return { ...l, ...r };
11017
- }
11136
+ if (node.operator === 'SLASH' && r === 0) {
11137
+ throw new RuntimeError('Division by zero', node, this.source, env);
11138
+ }
11018
11139
 
11019
- throw new RuntimeError(
11020
- `Unsupported operands for +: ${typeof l} and ${typeof r}`,
11021
- node,
11022
- this.source
11023
- );
11024
- }
11140
+ switch (node.operator) {
11141
+ case 'PLUS': {
11142
+ if (Array.isArray(l) && Array.isArray(r)) return l.concat(r);
11143
+ if (typeof l === 'string' || typeof r === 'string') return String(l) + String(r);
11144
+ if (typeof l === 'number' && typeof r === 'number') return l + r;
11145
+ if (typeof l === 'object' && typeof r === 'object') return { ...l, ...r };
11025
11146
 
11026
- case 'MINUS': return l - r;
11027
- case 'STAR': return l * r;
11028
- case 'SLASH': return l / r;
11029
- case 'MOD': return l % r;
11030
- case 'EQEQ': return l === r;
11031
- case 'NOTEQ': return l !== r;
11032
- case 'LT': return l < r;
11033
- case 'LTE': return l <= r;
11034
- case 'GT': return l > r;
11035
- case 'GTE': return l >= r;
11036
- default: throw new RuntimeError(`Unknown binary operator: ${node.operator}`, node, this.source);
11147
+ throw new RuntimeError(
11148
+ `Unsupported operands for +: ${typeof l} and ${typeof r}`,
11149
+ node,
11150
+ this.source,
11151
+ env
11152
+ );
11153
+ }
11154
+ case 'MINUS': return l - r;
11155
+ case 'STAR': return l * r;
11156
+ case 'SLASH': return l / r;
11157
+ case 'MOD': return l % r;
11158
+ case 'EQEQ': return l === r;
11159
+ case 'NOTEQ': return l !== r;
11160
+ case 'LT': return l < r;
11161
+ case 'LTE': return l <= r;
11162
+ case 'GT': return l > r;
11163
+ case 'GTE': return l >= r;
11164
+ default:
11165
+ throw new RuntimeError(
11166
+ `Unknown binary operator: ${node.operator}`,
11167
+ node,
11168
+ this.source,
11169
+ env
11170
+ );
11171
+ }
11037
11172
 
11173
+ } catch (e) {
11174
+ if (e instanceof RuntimeError) throw e;
11175
+ throw new RuntimeError(
11176
+ e.message || 'Error evaluating binary expression',
11177
+ node,
11178
+ this.source,
11179
+ env
11180
+ );
11038
11181
  }
11039
11182
  }
11040
-
11041
11183
  async evalLogical(node, env) {
11042
- const l = await this.evaluate(node.left, env);
11043
-
11044
- switch(node.operator) {
11045
- case 'AND': return l && await this.evaluate(node.right, env);
11046
- case 'OR': return l || await this.evaluate(node.right, env);
11047
- case '??': {
11048
- const r = await this.evaluate(node.right, env);
11049
- return (l !== null && l !== undefined) ? l : r;
11184
+ try {
11185
+ const l = await this.evaluate(node.left, env);
11186
+
11187
+ switch (node.operator) {
11188
+ case 'AND': return l && await this.evaluate(node.right, env);
11189
+ case 'OR': return l || await this.evaluate(node.right, env);
11190
+ case '??': {
11191
+ const r = await this.evaluate(node.right, env);
11192
+ return (l !== null && l !== undefined) ? l : r;
11193
+ }
11194
+ default:
11195
+ throw new RuntimeError(
11196
+ `Unknown logical operator: ${node.operator}`,
11197
+ node,
11198
+ this.source,
11199
+ env
11200
+ );
11050
11201
  }
11051
- default:
11052
- throw new RuntimeError(`Unknown logical operator: ${node.operator}`, node, this.source);
11202
+
11203
+ } catch (e) {
11204
+ if (e instanceof RuntimeError) throw e;
11205
+ throw new RuntimeError(
11206
+ e.message || 'Error evaluating logical expression',
11207
+ node,
11208
+ this.source,
11209
+ env
11210
+ );
11053
11211
  }
11054
11212
  }
11055
11213
 
11056
-
11057
11214
  async evalUnary(node, env) {
11058
- const val = await this.evaluate(node.argument, env);
11059
- switch (node.operator) {
11060
- case 'NOT': return !val;
11061
- case 'MINUS': return -val;
11062
- case 'PLUS': return +val;
11063
- default: throw new RuntimeError(`Unknown unary operator: ${node.operator}`, node, this.source);
11215
+ try {
11216
+ const val = await this.evaluate(node.argument, env);
11217
+
11218
+ switch (node.operator) {
11219
+ case 'NOT': return !val;
11220
+ case 'MINUS': return -val;
11221
+ case 'PLUS': return +val;
11222
+ default:
11223
+ throw new RuntimeError(
11224
+ `Unknown unary operator: ${node.operator}`,
11225
+ node,
11226
+ this.source,
11227
+ env
11228
+ );
11229
+ }
11064
11230
 
11231
+ } catch (e) {
11232
+ if (e instanceof RuntimeError) throw e;
11233
+ throw new RuntimeError(
11234
+ e.message || 'Error evaluating unary expression',
11235
+ node,
11236
+ this.source,
11237
+ env
11238
+ );
11065
11239
  }
11066
11240
  }
11067
11241
 
11068
11242
  async evalIf(node, env) {
11069
- const test = await this.evaluate(node.test, env);
11070
-
11071
- if (typeof test !== 'boolean') {
11072
- throw new RuntimeError('If condition must evaluate to a boolean', node.test, this.source);
11073
- }
11243
+ let test = await this.evaluate(node.test, env);
11244
+ test = !!test; // coerce to boolean
11074
11245
 
11075
11246
  if (test) {
11076
11247
  return await this.evaluate(node.consequent, env);
@@ -11083,222 +11254,344 @@ async evalIf(node, env) {
11083
11254
  return null;
11084
11255
  }
11085
11256
 
11086
-
11087
11257
  async evalWhile(node, env) {
11088
- while (true) {
11089
- const test = await this.evaluate(node.test, env);
11258
+ try {
11259
+ while (true) {
11260
+ let test = await this.evaluate(node.test, env);
11261
+ test = !!test;
11262
+
11263
+ if (!test) break;
11090
11264
 
11091
- if (typeof test !== 'boolean') {
11092
- throw new RuntimeError('While condition must evaluate to a boolean', node.test, this.source);
11265
+ try {
11266
+ await this.evaluate(node.body, env);
11267
+ } catch (e) {
11268
+ if (e instanceof BreakSignal) break;
11269
+ if (e instanceof ContinueSignal) continue;
11270
+ throw e;
11271
+ }
11093
11272
  }
11094
11273
 
11095
- if (!test) break;
11274
+ return null;
11096
11275
 
11097
- try {
11098
- await this.evaluate(node.body, env);
11099
- } catch (e) {
11100
- if (e instanceof BreakSignal) break;
11101
- if (e instanceof ContinueSignal) continue;
11102
- throw e;
11103
- }
11276
+ } catch (e) {
11277
+ if (e instanceof RuntimeError || e instanceof BreakSignal || e instanceof ContinueSignal) throw e;
11278
+ throw new RuntimeError(
11279
+ e.message || 'Error evaluating while loop',
11280
+ node,
11281
+ this.source,
11282
+ env
11283
+ );
11104
11284
  }
11105
- return null;
11106
11285
  }
11107
11286
  async evalFor(node, env) {
11108
- if (node.type === 'ForInStatement') {
11109
- const iterable = await this.evaluate(node.iterable, env);
11110
-
11111
- if (iterable == null || typeof iterable !== 'object') {
11112
- throw new RuntimeError('Cannot iterate over non-iterable', node, this.source);
11113
- }
11114
-
11115
- const loopVar = node.variable; // string name of the loop variable
11116
- const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
11117
- if (Array.isArray(iterable)) {
11118
- for (const value of iterable) {
11119
- const loopEnv = createLoopEnv();
11120
- loopEnv.define(loopVar, value);
11121
-
11122
- try {
11123
- await this.evaluate(node.body, loopEnv);
11124
- } catch (e) {
11125
- if (e instanceof BreakSignal) break;
11126
- if (e instanceof ContinueSignal) continue;
11127
- throw e;
11128
- }
11287
+ try {
11288
+ // ForInStatement
11289
+ if (node.type === 'ForInStatement') {
11290
+ const iterable = await this.evaluate(node.iterable, env);
11291
+
11292
+ if (iterable == null || typeof iterable !== 'object') {
11293
+ throw new RuntimeError(
11294
+ 'Cannot iterate over non-iterable',
11295
+ node,
11296
+ this.source,
11297
+ env
11298
+ );
11129
11299
  }
11130
- }
11131
- else {
11132
- for (const key of Object.keys(iterable)) {
11133
- const loopEnv = createLoopEnv();
11134
- loopEnv.define(loopVar, key);
11135
-
11136
- try {
11137
- await this.evaluate(node.body, loopEnv);
11138
- } catch (e) {
11139
- if (e instanceof BreakSignal) break;
11140
- if (e instanceof ContinueSignal) continue;
11141
- throw e;
11300
+
11301
+ const loopVar = node.variable; // string name of the loop variable
11302
+ const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
11303
+
11304
+ if (Array.isArray(iterable)) {
11305
+ for (const value of iterable) {
11306
+ const loopEnv = createLoopEnv();
11307
+ loopEnv.define(loopVar, value);
11308
+
11309
+ try {
11310
+ await this.evaluate(node.body, loopEnv);
11311
+ } catch (e) {
11312
+ if (e instanceof BreakSignal) break;
11313
+ if (e instanceof ContinueSignal) continue;
11314
+ throw e;
11315
+ }
11316
+ }
11317
+ } else {
11318
+ for (const key of Object.keys(iterable)) {
11319
+ const loopEnv = createLoopEnv();
11320
+ loopEnv.define(loopVar, key);
11321
+
11322
+ try {
11323
+ await this.evaluate(node.body, loopEnv);
11324
+ } catch (e) {
11325
+ if (e instanceof BreakSignal) break;
11326
+ if (e instanceof ContinueSignal) continue;
11327
+ throw e;
11328
+ }
11142
11329
  }
11143
11330
  }
11144
- }
11145
11331
 
11146
- return null;
11147
- }
11332
+ return null;
11333
+ }
11148
11334
 
11149
- const local = new Environment(env);
11335
+ // Standard for loop
11336
+ const local = new Environment(env);
11150
11337
 
11151
- if (node.init) await this.evaluate(node.init, local);
11338
+ if (node.init) await this.evaluate(node.init, local);
11152
11339
 
11153
- while (!node.test || await this.evaluate(node.test, local)) {
11154
- try {
11155
- await this.evaluate(node.body, local);
11156
- } catch (e) {
11157
- if (e instanceof BreakSignal) break;
11158
- if (e instanceof ContinueSignal) {
11159
- if (node.update) await this.evaluate(node.update, local);
11160
- continue;
11340
+ while (!node.test || await this.evaluate(node.test, local)) {
11341
+ try {
11342
+ await this.evaluate(node.body, local);
11343
+ } catch (e) {
11344
+ if (e instanceof BreakSignal) break;
11345
+ if (e instanceof ContinueSignal) {
11346
+ if (node.update) await this.evaluate(node.update, local);
11347
+ continue;
11348
+ }
11349
+ throw e;
11161
11350
  }
11162
- throw e;
11351
+
11352
+ if (node.update) await this.evaluate(node.update, local);
11163
11353
  }
11164
11354
 
11165
- if (node.update) await this.evaluate(node.update, local);
11166
- }
11355
+ return null;
11167
11356
 
11168
- return null;
11357
+ } catch (err) {
11358
+ if (err instanceof RuntimeError || err instanceof BreakSignal || err instanceof ContinueSignal) throw err;
11359
+ throw new RuntimeError(
11360
+ err.message || 'Error evaluating for loop',
11361
+ node,
11362
+ this.source,
11363
+ env
11364
+ );
11365
+ }
11169
11366
  }
11170
11367
 
11171
11368
  evalFunctionDeclaration(node, env) {
11172
- if (!node.name || typeof node.name !== 'string') {
11173
- throw new RuntimeError('Function declaration requires a valid name', node, this.source);
11174
- }
11369
+ try {
11370
+ if (!node.name || typeof node.name !== 'string') {
11371
+ throw new RuntimeError('Function declaration requires a valid name', node, this.source, env);
11372
+ }
11175
11373
 
11176
- if (!Array.isArray(node.params)) {
11177
- throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source);
11178
- }
11374
+ if (!Array.isArray(node.params)) {
11375
+ throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source, env);
11376
+ }
11179
11377
 
11180
- if (!node.body) {
11181
- throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source);
11182
- }
11378
+ if (!node.body) {
11379
+ throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source, env);
11380
+ }
11183
11381
 
11184
- const fn = {
11185
- params: node.params,
11186
- body: node.body,
11187
- env,
11188
- async: node.async || false
11189
- };
11382
+ const fn = {
11383
+ params: node.params,
11384
+ body: node.body,
11385
+ env,
11386
+ async: node.async || false
11387
+ };
11190
11388
 
11191
- env.define(node.name, fn);
11192
- return null;
11193
- }
11389
+ env.define(node.name, fn);
11390
+ return null;
11194
11391
 
11195
- async evalCall(node, env) {
11196
- const calleeEvaluated = await this.evaluate(node.callee, env);
11197
- if (typeof calleeEvaluated === 'function') {
11198
- const args = [];
11199
- for (const a of node.arguments) args.push(await this.evaluate(a, env));
11200
- return await calleeEvaluated(...args);
11201
- }
11202
- if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
11203
- throw new RuntimeError('Call to non-function', node, this.source);
11392
+ } catch (err) {
11393
+ if (err instanceof RuntimeError) throw err;
11394
+ throw new RuntimeError(
11395
+ err.message || 'Error defining function',
11396
+ node,
11397
+ this.source,
11398
+ env
11399
+ );
11204
11400
  }
11401
+ }
11402
+ async evalCall(node, env) {
11403
+ try {
11404
+ const calleeEvaluated = await this.evaluate(node.callee, env);
11205
11405
 
11206
- const fn = calleeEvaluated;
11207
- const callEnv = new Environment(fn.env);
11406
+ // Native JS function
11407
+ if (typeof calleeEvaluated === 'function') {
11408
+ const args = [];
11409
+ for (const a of node.arguments) args.push(await this.evaluate(a, env));
11410
+ return await calleeEvaluated(...args);
11411
+ }
11208
11412
 
11209
- for (let i = 0; i < fn.params.length; i++) {
11210
- const argVal = node.arguments[i]
11211
- ? await this.evaluate(node.arguments[i], env)
11212
- : null;
11413
+ // Not a callable object
11414
+ if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
11415
+ throw new RuntimeError(
11416
+ 'Call to non-function',
11417
+ node,
11418
+ this.source,
11419
+ env
11420
+ );
11421
+ }
11213
11422
 
11214
- const param = fn.params[i];
11215
- const paramName = typeof param === 'string' ? param : param.name;
11423
+ const fn = calleeEvaluated;
11424
+ const callEnv = new Environment(fn.env);
11216
11425
 
11217
- callEnv.define(paramName, argVal);
11218
- }
11426
+ for (let i = 0; i < fn.params.length; i++) {
11427
+ const argVal = node.arguments[i]
11428
+ ? await this.evaluate(node.arguments[i], env)
11429
+ : null;
11219
11430
 
11220
- try {
11221
- const result = await this.evaluate(fn.body, callEnv);
11222
- return result;
11223
- } catch (e) {
11224
- if (e instanceof ReturnValue) return e.value;
11225
- throw e;
11431
+ const param = fn.params[i];
11432
+ const paramName = typeof param === 'string' ? param : param.name;
11433
+ callEnv.define(paramName, argVal);
11434
+ }
11435
+
11436
+ try {
11437
+ const result = await this.evaluate(fn.body, callEnv);
11438
+ return result === undefined ? null : result; // enforce null instead of undefined
11439
+ } catch (e) {
11440
+ if (e instanceof ReturnValue) return e.value === undefined ? null : e.value;
11441
+ throw e;
11442
+ }
11443
+
11444
+ } catch (err) {
11445
+ if (err instanceof RuntimeError) throw err;
11446
+ throw new RuntimeError(
11447
+ err.message || 'Error during function call',
11448
+ node,
11449
+ this.source,
11450
+ env
11451
+ );
11226
11452
  }
11227
11453
  }
11228
11454
 
11229
11455
  async evalIndex(node, env) {
11230
- const obj = await this.evaluate(node.object, env);
11231
- const idx = await this.evaluate(node.indexer, env);
11232
-
11233
- if (obj == null) throw new RuntimeError('Cannot index null or undefined', node, this.source);
11234
- if (Array.isArray(obj)) {
11235
- if (idx < 0 || idx >= obj.length) return undefined;
11236
- return obj[idx];
11237
- }
11238
- if (typeof obj === 'object') {
11239
- return obj[idx]; // undefined if missing
11240
- }
11456
+ try {
11457
+ const obj = await this.evaluate(node.object, env);
11458
+ const idx = await this.evaluate(node.indexer, env);
11241
11459
 
11242
- return undefined;
11243
- }
11244
- async evalObject(node, env) {
11245
- const out = {};
11246
- for (const p of node.props) {
11247
- if (!p.key) {
11248
- throw new RuntimeError('Object property must have a key', node, this.source);
11460
+ if (obj == null) {
11461
+ throw new RuntimeError(
11462
+ 'Cannot index null or undefined',
11463
+ node,
11464
+ this.source,
11465
+ env
11466
+ );
11249
11467
  }
11250
11468
 
11251
- const key = await this.evaluate(p.key, env);
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
11469
+ if (Array.isArray(obj)) {
11470
+ if (idx < 0 || idx >= obj.length) return undefined;
11471
+ return obj[idx];
11257
11472
  }
11258
11473
 
11259
- out[key] = value;
11474
+ if (typeof obj === 'object') return obj[idx]; // undefined if missing
11475
+
11476
+ return undefined;
11477
+ } catch (err) {
11478
+ if (err instanceof RuntimeError) throw err;
11479
+ throw new RuntimeError(
11480
+ err.message || 'Error during index access',
11481
+ node,
11482
+ this.source,
11483
+ env
11484
+ );
11260
11485
  }
11261
- return out;
11262
11486
  }
11263
11487
 
11488
+ async evalObject(node, env) {
11489
+ try {
11490
+ const out = {};
11491
+ for (const p of node.props) {
11492
+ if (!p.key) {
11493
+ throw new RuntimeError(
11494
+ 'Object property must have a key',
11495
+ node,
11496
+ this.source,
11497
+ env
11498
+ );
11499
+ }
11500
+
11501
+ const key = await this.evaluate(p.key, env);
11502
+ let value = null;
11264
11503
 
11504
+ if (p.value) {
11505
+ value = await this.evaluate(p.value, env);
11506
+ if (value === undefined) value = null; // force null instead of undefined
11507
+ }
11508
+
11509
+ out[key] = value;
11510
+ }
11511
+ return out;
11512
+ } catch (err) {
11513
+ if (err instanceof RuntimeError) throw err;
11514
+ throw new RuntimeError(
11515
+ err.message || 'Error evaluating object literal',
11516
+ node,
11517
+ this.source,
11518
+ env
11519
+ );
11520
+ }
11521
+ }
11265
11522
 
11266
11523
 
11267
11524
  async evalMember(node, env) {
11268
11525
  const obj = await this.evaluate(node.object, env);
11269
11526
 
11270
- if (obj == null) throw new RuntimeError('Member access of null or undefined', node, this.source);
11527
+ if (obj == null) {
11528
+ throw new RuntimeError('Member access of null or undefined', node, this.source);
11529
+ }
11530
+
11531
+ const prop = obj[node.property];
11271
11532
 
11272
- return obj[node.property];
11273
- }
11533
+ if (typeof prop === 'function') {
11534
+ return prop.bind(obj);
11535
+ }
11274
11536
 
11537
+ return prop;
11538
+ }
11275
11539
 
11276
11540
  async evalUpdate(node, env) {
11277
11541
  const arg = node.argument;
11278
- const getCurrent = async () => {
11279
- if (arg.type === 'Identifier') return env.get(arg.name, arg, this.source);
11280
- if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
11281
- if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
11282
- throw new RuntimeError('Invalid update target', node, this.source);
11283
11542
 
11543
+ const getCurrent = async () => {
11544
+ try {
11545
+ if (arg.type === 'Identifier') return env.get(arg.name, arg, this.source);
11546
+ if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
11547
+ if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
11548
+ throw new RuntimeError('Invalid update target', node, this.source, env);
11549
+ } catch (err) {
11550
+ if (err instanceof RuntimeError) throw err;
11551
+ throw new RuntimeError(
11552
+ err.message || 'Error accessing update target',
11553
+ node,
11554
+ this.source,
11555
+ env
11556
+ );
11557
+ }
11284
11558
  };
11559
+
11285
11560
  const setValue = async (v) => {
11286
- if (arg.type === 'Identifier') env.set(arg.name, v);
11287
- else if (arg.type === 'MemberExpression') {
11288
- const obj = await this.evaluate(arg.object, env);
11289
- obj[arg.property] = v;
11290
- } else if (arg.type === 'IndexExpression') {
11291
- const obj = await this.evaluate(arg.object, env);
11292
- const idx = await this.evaluate(arg.indexer, env);
11293
- obj[idx] = v;
11561
+ try {
11562
+ if (arg.type === 'Identifier') {
11563
+ env.set(arg.name, v);
11564
+ } else if (arg.type === 'MemberExpression') {
11565
+ const obj = await this.evaluate(arg.object, env);
11566
+ if (obj == null) throw new RuntimeError('Cannot update property of null or undefined', node, this.source, env);
11567
+ obj[arg.property] = v;
11568
+ } else if (arg.type === 'IndexExpression') {
11569
+ const obj = await this.evaluate(arg.object, env);
11570
+ const idx = await this.evaluate(arg.indexer, env);
11571
+ if (obj == null) throw new RuntimeError('Cannot update index of null or undefined', node, this.source, env);
11572
+ obj[idx] = v;
11573
+ }
11574
+ } catch (err) {
11575
+ if (err instanceof RuntimeError) throw err;
11576
+ throw new RuntimeError(
11577
+ err.message || 'Error setting update target',
11578
+ node,
11579
+ this.source,
11580
+ env
11581
+ );
11294
11582
  }
11295
11583
  };
11296
11584
 
11297
11585
  const current = await getCurrent();
11298
- const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
11586
+ const newVal = node.operator === 'PLUSPLUS' ? current + 1 : current - 1;
11299
11587
 
11300
- if (node.prefix) { await setValue(newVal); return newVal; }
11301
- else { await setValue(newVal); return current; }
11588
+ if (node.prefix) {
11589
+ await setValue(newVal);
11590
+ return newVal;
11591
+ } else {
11592
+ await setValue(newVal);
11593
+ return current;
11594
+ }
11302
11595
  }
11303
11596
 
11304
11597
  }
@@ -11340,7 +11633,7 @@ class Lexer {
11340
11633
  'break', 'continue', 'func', 'return',
11341
11634
  'true', 'false', 'null',
11342
11635
  'ask', 'define', 'import', 'from', 'as',
11343
- 'async', 'await', 'new', 'in', 'do', 'track', 'start', 'race'
11636
+ 'async', 'await', 'new', 'in', 'do', 'track', 'start', 'race', 'not', 'and', 'or'
11344
11637
  ];
11345
11638
  }
11346
11639
 
@@ -11423,6 +11716,8 @@ class Lexer {
11423
11716
  }
11424
11717
 
11425
11718
  if (this.keywords.includes(result)) {
11719
+ if (result === 'and') return { type: 'AND', value: 'and', line: startLine, column: startCol };
11720
+ if (result === 'or') return { type: 'OR', value: 'or', line: startLine, column: startCol };
11426
11721
  return { type: result.toUpperCase(), value: result, line: startLine, column: startCol };
11427
11722
  }
11428
11723
 
@@ -11538,15 +11833,19 @@ module.exports = Lexer;
11538
11833
  /***/ }),
11539
11834
 
11540
11835
  /***/ 222:
11541
- /***/ ((module) => {
11836
+ /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
11542
11837
 
11543
11838
  class ParseError extends Error {
11544
- constructor(message, token, source) {
11839
+ constructor(message, token, source, suggestion = null) {
11545
11840
  const line = token?.line ?? '?';
11546
11841
  const column = token?.column ?? '?';
11547
11842
 
11548
11843
  let output = `${message}\n`;
11549
11844
 
11845
+ if (suggestion) {
11846
+ output += `Hint: ${suggestion}\n`;
11847
+ }
11848
+
11550
11849
  if (source && token?.line != null) {
11551
11850
  const lines = source.split('\n');
11552
11851
  const srcLine = lines[token.line - 1] || '';
@@ -11562,6 +11861,7 @@ class ParseError extends Error {
11562
11861
  }
11563
11862
  }
11564
11863
 
11864
+
11565
11865
  class Parser {
11566
11866
  constructor(tokens, source = '') {
11567
11867
  this.tokens = tokens;
@@ -11629,17 +11929,49 @@ class Parser {
11629
11929
  varDeclaration() {
11630
11930
  const t = this.current;
11631
11931
  this.eat('LET');
11932
+
11933
+ if (this.current.type !== 'IDENTIFIER') {
11934
+ throw new ParseError(
11935
+ "Expected variable name after 'let'",
11936
+ this.current,
11937
+ this.source,
11938
+ "Variable declarations must be followed by an identifier, e.g. let x = 5"
11939
+ );
11940
+ }
11941
+
11632
11942
  const idToken = this.current;
11633
11943
  const id = idToken.value;
11634
11944
  this.eat('IDENTIFIER');
11635
11945
 
11636
11946
  let expr = null;
11947
+
11948
+ if (this.current.type === 'EQEQ') {
11949
+ throw new ParseError(
11950
+ "Invalid '==' in variable declaration",
11951
+ this.current,
11952
+ this.source,
11953
+ "Did you mean '=' for assignment?"
11954
+ );
11955
+ }
11956
+
11637
11957
  if (this.current.type === 'EQUAL') {
11638
11958
  this.eat('EQUAL');
11959
+
11960
+ if (this.current.type === 'SEMICOLON') {
11961
+ throw new ParseError(
11962
+ "Expected expression after '='",
11963
+ this.current,
11964
+ this.source,
11965
+ "Assignments require a value, e.g. let x = 10"
11966
+ );
11967
+ }
11968
+
11639
11969
  expr = this.expression();
11640
11970
  }
11641
11971
 
11642
- if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
11972
+ if (this.current.type === 'SEMICOLON') {
11973
+ this.eat('SEMICOLON');
11974
+ }
11643
11975
 
11644
11976
  return {
11645
11977
  type: 'VarDeclaration',
@@ -11649,12 +11981,31 @@ varDeclaration() {
11649
11981
  column: t.column
11650
11982
  };
11651
11983
  }
11984
+
11652
11985
  startStatement() {
11653
11986
  const t = this.current;
11654
11987
  this.eat('START');
11655
11988
 
11989
+ if (this.current.type === 'LBRACE') {
11990
+ throw new ParseError(
11991
+ "Expected expression after 'start'",
11992
+ this.current,
11993
+ this.source,
11994
+ "The 'start' statement requires a discriminant expression"
11995
+ );
11996
+ }
11997
+
11656
11998
  const discriminant = this.expression();
11657
11999
 
12000
+ if (this.current.type !== 'LBRACE') {
12001
+ throw new ParseError(
12002
+ "Expected '{' to start start-block",
12003
+ this.current,
12004
+ this.source,
12005
+ "Start blocks must be enclosed in braces"
12006
+ );
12007
+ }
12008
+
11658
12009
  this.eat('LBRACE');
11659
12010
 
11660
12011
  const cases = [];
@@ -11663,6 +12014,24 @@ startStatement() {
11663
12014
  cases.push(this.raceClause());
11664
12015
  }
11665
12016
 
12017
+ if (cases.length === 0) {
12018
+ throw new ParseError(
12019
+ "Start statement must contain at least one 'race' clause",
12020
+ this.current,
12021
+ this.source,
12022
+ "Use 'race <condition> { ... }' inside start blocks"
12023
+ );
12024
+ }
12025
+
12026
+ if (this.current.type !== 'RBRACE') {
12027
+ throw new ParseError(
12028
+ "Expected '}' to close start block",
12029
+ this.current,
12030
+ this.source,
12031
+ "Did you forget to close the start block?"
12032
+ );
12033
+ }
12034
+
11666
12035
  this.eat('RBRACE');
11667
12036
 
11668
12037
  return {
@@ -11677,8 +12046,26 @@ raceClause() {
11677
12046
  const t = this.current;
11678
12047
  this.eat('RACE');
11679
12048
 
12049
+ if (this.current.type === 'LBRACE') {
12050
+ throw new ParseError(
12051
+ "Expected condition after 'race'",
12052
+ this.current,
12053
+ this.source,
12054
+ "Race clauses require a condition before the block"
12055
+ );
12056
+ }
12057
+
11680
12058
  const test = this.expression();
11681
12059
 
12060
+ if (this.current.type !== 'LBRACE') {
12061
+ throw new ParseError(
12062
+ "Expected '{' after race condition",
12063
+ this.current,
12064
+ this.source,
12065
+ "Race clauses must use a block: race condition { ... }"
12066
+ );
12067
+ }
12068
+
11682
12069
  const consequent = this.block();
11683
12070
 
11684
12071
  return {
@@ -11690,11 +12077,26 @@ raceClause() {
11690
12077
  };
11691
12078
  }
11692
12079
 
12080
+
11693
12081
  sldeployStatement() {
11694
12082
  const t = this.current;
11695
12083
  this.eat('SLDEPLOY');
12084
+
12085
+ if (this.current.type === 'SEMICOLON') {
12086
+ throw new ParseError(
12087
+ "Expected expression after 'sldeploy'",
12088
+ this.current,
12089
+ this.source,
12090
+ "sldeploy requires a value or expression to deploy"
12091
+ );
12092
+ }
12093
+
11696
12094
  const expr = this.expression();
11697
- if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
12095
+
12096
+ if (this.current.type === 'SEMICOLON') {
12097
+ this.eat('SEMICOLON');
12098
+ }
12099
+
11698
12100
  return {
11699
12101
  type: 'SldeployStatement',
11700
12102
  expr,
@@ -11707,11 +12109,31 @@ doTrackStatement() {
11707
12109
  const t = this.current;
11708
12110
  this.eat('DO');
11709
12111
 
12112
+ if (this.current.type !== 'LBRACE') {
12113
+ throw new ParseError(
12114
+ "Expected '{' after 'do'",
12115
+ this.current,
12116
+ this.source,
12117
+ "The 'do' statement must be followed by a block"
12118
+ );
12119
+ }
12120
+
11710
12121
  const body = this.block();
11711
12122
 
11712
12123
  let handler = null;
12124
+
11713
12125
  if (this.current.type === 'TRACK') {
11714
12126
  this.eat('TRACK');
12127
+
12128
+ if (this.current.type !== 'LBRACE') {
12129
+ throw new ParseError(
12130
+ "Expected '{' after 'track'",
12131
+ this.current,
12132
+ this.source,
12133
+ "Track handlers must be blocks"
12134
+ );
12135
+ }
12136
+
11715
12137
  handler = this.block();
11716
12138
  }
11717
12139
 
@@ -11727,17 +12149,49 @@ doTrackStatement() {
11727
12149
  defineStatement() {
11728
12150
  const t = this.current;
11729
12151
  this.eat('DEFINE');
12152
+
12153
+ if (this.current.type !== 'IDENTIFIER') {
12154
+ throw new ParseError(
12155
+ "Expected identifier after 'define'",
12156
+ this.current,
12157
+ this.source,
12158
+ "Definitions must be followed by a name, e.g. define PI = 3.14"
12159
+ );
12160
+ }
12161
+
11730
12162
  const idToken = this.current;
11731
12163
  const id = idToken.value;
11732
12164
  this.eat('IDENTIFIER');
11733
12165
 
11734
12166
  let expr = null;
12167
+
12168
+ if (this.current.type === 'EQEQ') {
12169
+ throw new ParseError(
12170
+ "Invalid '==' in define statement",
12171
+ this.current,
12172
+ this.source,
12173
+ "Did you mean '=' to assign a value?"
12174
+ );
12175
+ }
12176
+
11735
12177
  if (this.current.type === 'EQUAL') {
11736
12178
  this.eat('EQUAL');
12179
+
12180
+ if (this.current.type === 'SEMICOLON') {
12181
+ throw new ParseError(
12182
+ "Expected expression after '='",
12183
+ this.current,
12184
+ this.source,
12185
+ "Definitions require a value, e.g. define X = 10"
12186
+ );
12187
+ }
12188
+
11737
12189
  expr = this.expression();
11738
12190
  }
11739
12191
 
11740
- if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
12192
+ if (this.current.type === 'SEMICOLON') {
12193
+ this.eat('SEMICOLON');
12194
+ }
11741
12195
 
11742
12196
  return {
11743
12197
  type: 'DefineStatement',
@@ -11750,30 +12204,80 @@ defineStatement() {
11750
12204
 
11751
12205
  asyncFuncDeclaration() {
11752
12206
  const t = this.current;
12207
+
12208
+ if (this.current.type !== 'IDENTIFIER') {
12209
+ throw new ParseError(
12210
+ "Expected function name after 'async func'",
12211
+ this.current,
12212
+ this.source,
12213
+ "Async functions must have a name"
12214
+ );
12215
+ }
12216
+
11753
12217
  const name = this.current.value;
11754
12218
  this.eat('IDENTIFIER');
11755
12219
 
11756
12220
  let params = [];
12221
+
11757
12222
  if (this.current.type === 'LPAREN') {
11758
12223
  this.eat('LPAREN');
12224
+
11759
12225
  if (this.current.type !== 'RPAREN') {
12226
+ if (this.current.type !== 'IDENTIFIER') {
12227
+ throw new ParseError(
12228
+ "Expected parameter name",
12229
+ this.current,
12230
+ this.source,
12231
+ "Function parameters must be identifiers"
12232
+ );
12233
+ }
12234
+
11760
12235
  params.push(this.current.value);
11761
12236
  this.eat('IDENTIFIER');
12237
+
11762
12238
  while (this.current.type === 'COMMA') {
11763
12239
  this.eat('COMMA');
12240
+
12241
+ if (this.current.type !== 'IDENTIFIER') {
12242
+ throw new ParseError(
12243
+ "Expected parameter name after ','",
12244
+ this.current,
12245
+ this.source,
12246
+ "Separate parameters with commas"
12247
+ );
12248
+ }
12249
+
11764
12250
  params.push(this.current.value);
11765
12251
  this.eat('IDENTIFIER');
11766
12252
  }
11767
12253
  }
11768
- this.eat('RPAREN');
11769
- } else {
11770
- if (this.current.type === 'IDENTIFIER') {
11771
- params.push(this.current.value);
11772
- this.eat('IDENTIFIER');
12254
+
12255
+ if (this.current.type !== 'RPAREN') {
12256
+ throw new ParseError(
12257
+ "Expected ')' after function parameters",
12258
+ this.current,
12259
+ this.source,
12260
+ "Did you forget to close the parameter list?"
12261
+ );
11773
12262
  }
12263
+
12264
+ this.eat('RPAREN');
12265
+ } else if (this.current.type === 'IDENTIFIER') {
12266
+ params.push(this.current.value);
12267
+ this.eat('IDENTIFIER');
12268
+ }
12269
+
12270
+ if (this.current.type !== 'LBRACE') {
12271
+ throw new ParseError(
12272
+ "Expected '{' to start function body",
12273
+ this.current,
12274
+ this.source,
12275
+ "Function declarations require a block body"
12276
+ );
11774
12277
  }
11775
12278
 
11776
12279
  const body = this.block();
12280
+
11777
12281
  return {
11778
12282
  type: 'FunctionDeclaration',
11779
12283
  name,
@@ -11784,32 +12288,62 @@ asyncFuncDeclaration() {
11784
12288
  column: t.column
11785
12289
  };
11786
12290
  }
11787
-
11788
12291
  ifStatement() {
11789
12292
  const t = this.current;
11790
12293
  this.eat('IF');
11791
12294
 
11792
12295
  let test;
12296
+
11793
12297
  if (this.current.type === 'LPAREN') {
11794
12298
  this.eat('LPAREN');
12299
+
12300
+ if (this.current.type === 'RPAREN') {
12301
+ throw new ParseError(
12302
+ "Missing condition in if statement",
12303
+ this.current,
12304
+ this.source,
12305
+ "If statements require a condition"
12306
+ );
12307
+ }
12308
+
11795
12309
  test = this.expression();
12310
+
12311
+ if (this.current.type !== 'RPAREN') {
12312
+ throw new ParseError(
12313
+ "Expected ')' after if condition",
12314
+ this.current,
12315
+ this.source,
12316
+ "Did you forget to close the condition?"
12317
+ );
12318
+ }
12319
+
11796
12320
  this.eat('RPAREN');
11797
12321
  } else {
11798
12322
  test = this.expression();
11799
12323
  }
11800
12324
 
12325
+ if (this.current.type === 'EQUAL') {
12326
+ throw new ParseError(
12327
+ "Invalid assignment in if condition",
12328
+ this.current,
12329
+ this.source,
12330
+ "Did you mean '==' for comparison?"
12331
+ );
12332
+ }
12333
+
11801
12334
  const consequent = this.statementOrBlock();
11802
12335
 
11803
12336
  let alternate = null;
12337
+
11804
12338
  if (this.current.type === 'ELSE') {
11805
- this.eat('ELSE');
11806
- if (this.current.type === 'IF') {
11807
- alternate = this.ifStatement();
11808
- } else {
11809
- alternate = this.statementOrBlock();
11810
- }
11811
- }
12339
+ this.eat('ELSE');
11812
12340
 
12341
+ if (this.current.type === 'IF') {
12342
+ alternate = this.ifStatement();
12343
+ } else {
12344
+ alternate = this.statementOrBlock();
12345
+ }
12346
+ }
11813
12347
 
11814
12348
  return {
11815
12349
  type: 'IfStatement',
@@ -11821,21 +12355,83 @@ ifStatement() {
11821
12355
  };
11822
12356
  }
11823
12357
 
12358
+ parseExpressionOnly() {
12359
+ return this.expression();
12360
+ }
12361
+
11824
12362
  whileStatement() {
11825
12363
  const t = this.current;
11826
12364
  this.eat('WHILE');
11827
12365
 
11828
12366
  let test;
12367
+
11829
12368
  if (this.current.type === 'LPAREN') {
11830
12369
  this.eat('LPAREN');
12370
+
12371
+ if (this.current.type === 'RPAREN') {
12372
+ throw new ParseError(
12373
+ "Missing condition in while statement",
12374
+ this.current,
12375
+ this.source,
12376
+ "While loops require a condition"
12377
+ );
12378
+ }
12379
+
11831
12380
  test = this.expression();
11832
- this.eat('RPAREN');
11833
- } else {
11834
- test = this.expression();
11835
- }
11836
12381
 
11837
- const body = this.block();
11838
- return {
12382
+ if (this.current.type !== 'RPAREN') {
12383
+ throw new ParseError(
12384
+ "Expected ')' after while condition",
12385
+ this.current,
12386
+ this.source,
12387
+ "Did you forget to close the condition?"
12388
+ );
12389
+ }
12390
+
12391
+ this.eat('RPAREN');
12392
+ }
12393
+ else {
12394
+ const exprTokens = [];
12395
+ let braceFound = false;
12396
+ let depth = 0;
12397
+
12398
+ while (this.current.type !== 'EOF') {
12399
+ if (this.current.type === 'LBRACE' && depth === 0) {
12400
+ braceFound = true;
12401
+ break;
12402
+ }
12403
+ if (this.current.type === 'LPAREN') depth++;
12404
+ if (this.current.type === 'RPAREN') depth--;
12405
+ exprTokens.push(this.current);
12406
+ this.advance();
12407
+ }
12408
+
12409
+ if (!braceFound) {
12410
+ throw new ParseError(
12411
+ "Expected '{' after while condition",
12412
+ this.current,
12413
+ this.source,
12414
+ "While loops must be followed by a block"
12415
+ );
12416
+ }
12417
+
12418
+ const Parser = __nccwpck_require__(222);
12419
+ const exprParser = new Parser(exprTokens, this.source);
12420
+ test = exprParser.parseExpressionOnly();
12421
+ }
12422
+
12423
+ if (this.current.type !== 'LBRACE') {
12424
+ throw new ParseError(
12425
+ "Expected '{' to start while loop body",
12426
+ this.current,
12427
+ this.source,
12428
+ "While loop bodies must be enclosed in braces"
12429
+ );
12430
+ }
12431
+
12432
+ const body = this.block();
12433
+
12434
+ return {
11839
12435
  type: 'WhileStatement',
11840
12436
  test,
11841
12437
  body,
@@ -11849,15 +12445,62 @@ importStatement() {
11849
12445
  this.eat('IMPORT');
11850
12446
 
11851
12447
  let specifiers = [];
12448
+
11852
12449
  if (this.current.type === 'STAR') {
11853
12450
  this.eat('STAR');
12451
+
12452
+ if (this.current.type !== 'AS') {
12453
+ throw new ParseError(
12454
+ "Expected 'as' after '*' in import",
12455
+ this.current,
12456
+ this.source,
12457
+ "Namespace imports require 'as', e.g. import * as name from 'mod'"
12458
+ );
12459
+ }
12460
+
11854
12461
  this.eat('AS');
12462
+
12463
+ if (this.current.type !== 'IDENTIFIER') {
12464
+ throw new ParseError(
12465
+ "Expected identifier after 'as'",
12466
+ this.current,
12467
+ this.source,
12468
+ "The namespace must have a local name"
12469
+ );
12470
+ }
12471
+
11855
12472
  const name = this.current.value;
11856
12473
  this.eat('IDENTIFIER');
11857
- specifiers.push({ type: 'NamespaceImport', local: name, line: t.line, column: t.column });
12474
+
12475
+ specifiers.push({
12476
+ type: 'NamespaceImport',
12477
+ local: name,
12478
+ line: t.line,
12479
+ column: t.column
12480
+ });
12481
+
11858
12482
  } else if (this.current.type === 'LBRACE') {
11859
12483
  this.eat('LBRACE');
12484
+
12485
+ if (this.current.type === 'RBRACE') {
12486
+ throw new ParseError(
12487
+ "Empty import specifier list",
12488
+ this.current,
12489
+ this.source,
12490
+ "Specify at least one name inside '{ }'"
12491
+ );
12492
+ }
12493
+
11860
12494
  while (this.current.type !== 'RBRACE') {
12495
+ if (this.current.type !== 'IDENTIFIER') {
12496
+ throw new ParseError(
12497
+ "Expected identifier in import specifier",
12498
+ this.current,
12499
+ this.source,
12500
+ "Import names must be identifiers"
12501
+ );
12502
+ }
12503
+
11861
12504
  const importedName = this.current.value;
11862
12505
  const importedLine = this.current.line;
11863
12506
  const importedColumn = this.current.column;
@@ -11866,27 +12509,77 @@ importStatement() {
11866
12509
  let localName = importedName;
11867
12510
  if (this.current.type === 'AS') {
11868
12511
  this.eat('AS');
12512
+
12513
+ if (this.current.type !== 'IDENTIFIER') {
12514
+ throw new ParseError(
12515
+ "Expected identifier after 'as'",
12516
+ this.current,
12517
+ this.source,
12518
+ "Aliases must be valid identifiers"
12519
+ );
12520
+ }
12521
+
11869
12522
  localName = this.current.value;
11870
12523
  this.eat('IDENTIFIER');
11871
12524
  }
11872
12525
 
11873
- specifiers.push({ type: 'NamedImport', imported: importedName, local: localName, line: importedLine, column: importedColumn });
12526
+ specifiers.push({
12527
+ type: 'NamedImport',
12528
+ imported: importedName,
12529
+ local: localName,
12530
+ line: importedLine,
12531
+ column: importedColumn
12532
+ });
12533
+
11874
12534
  if (this.current.type === 'COMMA') this.eat('COMMA');
11875
12535
  }
12536
+
11876
12537
  this.eat('RBRACE');
12538
+
11877
12539
  } else if (this.current.type === 'IDENTIFIER') {
11878
12540
  const localName = this.current.value;
11879
12541
  const localLine = this.current.line;
11880
12542
  const localColumn = this.current.column;
11881
12543
  this.eat('IDENTIFIER');
11882
- specifiers.push({ type: 'DefaultImport', local: localName, line: localLine, column: localColumn });
12544
+
12545
+ specifiers.push({
12546
+ type: 'DefaultImport',
12547
+ local: localName,
12548
+ line: localLine,
12549
+ column: localColumn
12550
+ });
12551
+
11883
12552
  } else {
11884
- throw new Error(`Unexpected token in import at line ${this.current.line}, column ${this.current.column}`);
12553
+ throw new ParseError(
12554
+ "Invalid import syntax",
12555
+ this.current,
12556
+ this.source,
12557
+ "Use import name, import { a }, or import * as name"
12558
+ );
12559
+ }
12560
+
12561
+ if (this.current.type !== 'FROM') {
12562
+ throw new ParseError(
12563
+ "Expected 'from' in import statement",
12564
+ this.current,
12565
+ this.source,
12566
+ "Imports must specify a source module"
12567
+ );
11885
12568
  }
11886
12569
 
11887
12570
  this.eat('FROM');
12571
+
11888
12572
  const pathToken = this.current;
11889
- if (pathToken.type !== 'STRING') throw new Error(`Expected string after FROM at line ${this.current.line}, column ${this.current.column}`);
12573
+
12574
+ if (pathToken.type !== 'STRING') {
12575
+ throw new ParseError(
12576
+ "Expected string after 'from'",
12577
+ pathToken,
12578
+ this.source,
12579
+ "Module paths must be strings"
12580
+ );
12581
+ }
12582
+
11890
12583
  this.eat('STRING');
11891
12584
 
11892
12585
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
@@ -11899,14 +12592,14 @@ importStatement() {
11899
12592
  column: t.column
11900
12593
  };
11901
12594
  }
11902
-
11903
12595
  forStatement() {
11904
- const t = this.current; // FOR token
12596
+ const t = this.current;
11905
12597
  this.eat('FOR');
11906
12598
 
11907
- if ((this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
11908
- (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')) {
11909
-
12599
+ if (
12600
+ (this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
12601
+ (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')
12602
+ ) {
11910
12603
  let variable;
11911
12604
  let variableLine, variableColumn;
11912
12605
  let iterable;
@@ -11915,22 +12608,45 @@ forStatement() {
11915
12608
  if (this.current.type === 'LET') {
11916
12609
  letKeyword = true;
11917
12610
  this.eat('LET');
11918
- variableLine = this.current.line;
11919
- variableColumn = this.current.column;
11920
- variable = this.current.value;
11921
- this.eat('IDENTIFIER');
11922
- this.eat('IN');
11923
- iterable = this.expression();
11924
- } else {
11925
- variableLine = this.current.line;
11926
- variableColumn = this.current.column;
11927
- variable = this.current.value;
11928
- this.eat('IDENTIFIER');
11929
- this.eat('IN');
11930
- iterable = this.expression();
12611
+ }
12612
+
12613
+ if (this.current.type !== 'IDENTIFIER') {
12614
+ throw new ParseError(
12615
+ "Expected identifier in for-in loop",
12616
+ this.current,
12617
+ this.source,
12618
+ "for-in loops require a loop variable"
12619
+ );
12620
+ }
12621
+
12622
+ variableLine = this.current.line;
12623
+ variableColumn = this.current.column;
12624
+ variable = this.current.value;
12625
+ this.eat('IDENTIFIER');
12626
+
12627
+ if (this.current.type !== 'IN') {
12628
+ throw new ParseError(
12629
+ "Expected 'in' in for-in loop",
12630
+ this.current,
12631
+ this.source,
12632
+ "Use: for item in iterable { ... }"
12633
+ );
12634
+ }
12635
+
12636
+ this.eat('IN');
12637
+ iterable = this.expression();
12638
+
12639
+ if (this.current.type !== 'LBRACE') {
12640
+ throw new ParseError(
12641
+ "Expected '{' after for-in iterable",
12642
+ this.current,
12643
+ this.source,
12644
+ "For-in loops require a block body"
12645
+ );
11931
12646
  }
11932
12647
 
11933
12648
  const body = this.block();
12649
+
11934
12650
  return {
11935
12651
  type: 'ForInStatement',
11936
12652
  variable,
@@ -11952,25 +12668,53 @@ forStatement() {
11952
12668
  this.eat('LPAREN');
11953
12669
 
11954
12670
  if (this.current.type !== 'SEMICOLON') {
11955
- init = this.current.type === 'LET' ? this.varDeclaration() : this.expressionStatement();
12671
+ init = this.current.type === 'LET'
12672
+ ? this.varDeclaration()
12673
+ : this.expressionStatement();
11956
12674
  } else {
11957
12675
  this.eat('SEMICOLON');
11958
12676
  }
11959
12677
 
11960
- if (this.current.type !== 'SEMICOLON') test = this.expression();
12678
+ if (this.current.type !== 'SEMICOLON') {
12679
+ test = this.expression();
12680
+ }
12681
+
11961
12682
  this.eat('SEMICOLON');
11962
12683
 
11963
- if (this.current.type !== 'RPAREN') update = this.expression();
12684
+ if (this.current.type !== 'RPAREN') {
12685
+ update = this.expression();
12686
+ }
12687
+
12688
+ if (this.current.type !== 'RPAREN') {
12689
+ throw new ParseError(
12690
+ "Expected ')' after for loop clauses",
12691
+ this.current,
12692
+ this.source,
12693
+ "Did you forget to close the for loop header?"
12694
+ );
12695
+ }
12696
+
11964
12697
  this.eat('RPAREN');
11965
12698
  } else {
11966
- init = this.expression();
11967
- if (this.current.type === 'IN') {
11968
- this.eat('IN');
11969
- test = this.expression(); // iterable
11970
- }
12699
+ throw new ParseError(
12700
+ "Expected '(' after 'for'",
12701
+ this.current,
12702
+ this.source,
12703
+ "Classic for loops require parentheses"
12704
+ );
12705
+ }
12706
+
12707
+ if (this.current.type !== 'LBRACE') {
12708
+ throw new ParseError(
12709
+ "Expected '{' to start for loop body",
12710
+ this.current,
12711
+ this.source,
12712
+ "For loops require a block body"
12713
+ );
11971
12714
  }
11972
12715
 
11973
12716
  const body = this.block();
12717
+
11974
12718
  return {
11975
12719
  type: 'ForStatement',
11976
12720
  init,
@@ -11995,41 +12739,111 @@ continueStatement() {
11995
12739
  if (this.current.type === 'SEMICOLON') this.advance();
11996
12740
  return { type: 'ContinueStatement', line: t.line, column: t.column };
11997
12741
  }
11998
-
11999
12742
  funcDeclaration() {
12000
12743
  const t = this.current;
12001
12744
  this.eat('FUNC');
12745
+
12746
+ if (this.current.type !== 'IDENTIFIER') {
12747
+ throw new ParseError(
12748
+ "Expected function name after 'func'",
12749
+ this.current,
12750
+ this.source,
12751
+ "Functions must have a name, e.g. func add(a, b) { ... }"
12752
+ );
12753
+ }
12754
+
12002
12755
  const nameToken = this.current;
12003
12756
  const name = nameToken.value;
12004
12757
  this.eat('IDENTIFIER');
12005
12758
 
12006
12759
  let params = [];
12760
+
12007
12761
  if (this.current.type === 'LPAREN') {
12008
12762
  this.eat('LPAREN');
12763
+
12009
12764
  if (this.current.type !== 'RPAREN') {
12010
- const paramToken = this.current;
12011
- params.push({ name: paramToken.value, line: paramToken.line, column: paramToken.column });
12765
+ if (this.current.type !== 'IDENTIFIER') {
12766
+ throw new ParseError(
12767
+ "Expected parameter name",
12768
+ this.current,
12769
+ this.source,
12770
+ "Function parameters must be identifiers"
12771
+ );
12772
+ }
12773
+
12774
+ let paramToken = this.current;
12775
+ params.push({
12776
+ name: paramToken.value,
12777
+ line: paramToken.line,
12778
+ column: paramToken.column
12779
+ });
12012
12780
  this.eat('IDENTIFIER');
12781
+
12013
12782
  while (this.current.type === 'COMMA') {
12014
12783
  this.eat('COMMA');
12015
- const paramToken2 = this.current;
12016
- params.push({ name: paramToken2.value, line: paramToken2.line, column: paramToken2.column });
12784
+
12785
+ if (this.current.type !== 'IDENTIFIER') {
12786
+ throw new ParseError(
12787
+ "Expected parameter name after ','",
12788
+ this.current,
12789
+ this.source,
12790
+ "Each parameter must be an identifier"
12791
+ );
12792
+ }
12793
+
12794
+ paramToken = this.current;
12795
+ params.push({
12796
+ name: paramToken.value,
12797
+ line: paramToken.line,
12798
+ column: paramToken.column
12799
+ });
12017
12800
  this.eat('IDENTIFIER');
12018
12801
  }
12019
12802
  }
12020
- this.eat('RPAREN');
12021
- } else {
12022
- if (this.current.type === 'IDENTIFIER') {
12023
- const paramToken = this.current;
12024
- params.push({ name: paramToken.value, line: paramToken.line, column: paramToken.column });
12025
- this.eat('IDENTIFIER');
12803
+
12804
+ if (this.current.type !== 'RPAREN') {
12805
+ throw new ParseError(
12806
+ "Expected ')' after function parameters",
12807
+ this.current,
12808
+ this.source,
12809
+ "Did you forget to close the parameter list?"
12810
+ );
12026
12811
  }
12812
+
12813
+ this.eat('RPAREN');
12814
+ }
12815
+ else if (this.current.type === 'IDENTIFIER') {
12816
+ const paramToken = this.current;
12817
+ params.push({
12818
+ name: paramToken.value,
12819
+ line: paramToken.line,
12820
+ column: paramToken.column
12821
+ });
12822
+ this.eat('IDENTIFIER');
12823
+ }
12824
+
12825
+ if (this.current.type !== 'LBRACE') {
12826
+ throw new ParseError(
12827
+ "Expected '{' to start function body",
12828
+ this.current,
12829
+ this.source,
12830
+ "Functions must have a block body"
12831
+ );
12027
12832
  }
12028
12833
 
12029
12834
  const body = this.block();
12030
- return { type: 'FunctionDeclaration', name, params, body, line: t.line, column: t.column };
12835
+
12836
+ return {
12837
+ type: 'FunctionDeclaration',
12838
+ name,
12839
+ params,
12840
+ body,
12841
+ line: t.line,
12842
+ column: t.column
12843
+ };
12031
12844
  }
12032
12845
 
12846
+
12033
12847
  returnStatement() {
12034
12848
  const t = this.current; // RETURN token
12035
12849
  this.eat('RETURN');
@@ -12049,18 +12863,35 @@ statementOrBlock() {
12049
12863
  }
12050
12864
  return this.statement();
12051
12865
  }
12052
-
12053
12866
  block() {
12054
12867
  const t = this.current; // LBRACE token
12055
12868
  this.eat('LBRACE');
12869
+
12056
12870
  const body = [];
12871
+
12057
12872
  while (this.current.type !== 'RBRACE') {
12873
+ if (this.current.type === 'EOF') {
12874
+ throw new ParseError(
12875
+ "Unterminated block",
12876
+ this.current,
12877
+ this.source,
12878
+ "Did you forget to close the block with '}'?"
12879
+ );
12880
+ }
12058
12881
  body.push(this.statement());
12059
12882
  }
12883
+
12060
12884
  this.eat('RBRACE');
12061
- return { type: 'BlockStatement', body, line: t.line, column: t.column };
12885
+
12886
+ return {
12887
+ type: 'BlockStatement',
12888
+ body,
12889
+ line: t.line,
12890
+ column: t.column
12891
+ };
12062
12892
  }
12063
12893
 
12894
+
12064
12895
  expressionStatement() {
12065
12896
  const exprToken = this.current;
12066
12897
  const expr = this.expression();
@@ -12071,7 +12902,6 @@ expressionStatement() {
12071
12902
  expression() {
12072
12903
  return this.assignment();
12073
12904
  }
12074
-
12075
12905
  assignment() {
12076
12906
  const node = this.ternary();
12077
12907
  const compoundOps = ['PLUSEQ', 'MINUSEQ', 'STAREQ', 'SLASHEQ', 'MODEQ'];
@@ -12082,29 +12912,76 @@ assignment() {
12082
12912
  const op = t.type;
12083
12913
  this.eat(op);
12084
12914
  const right = this.assignment();
12085
- return { type: 'CompoundAssignment', operator: op, left: node, right, line: t.line, column: t.column };
12915
+
12916
+ return {
12917
+ type: 'CompoundAssignment',
12918
+ operator: op,
12919
+ left: node,
12920
+ right,
12921
+ line: t.line,
12922
+ column: t.column
12923
+ };
12924
+ }
12925
+
12926
+ if (t.type === 'EQEQ') {
12927
+ throw new ParseError(
12928
+ "Unexpected '==' in assignment",
12929
+ t,
12930
+ this.source,
12931
+ "Did you mean '=' to assign a value?"
12932
+ );
12086
12933
  }
12087
12934
 
12088
12935
  if (t.type === 'EQUAL') {
12089
12936
  this.eat('EQUAL');
12090
12937
  const right = this.assignment();
12091
- return { type: 'AssignmentExpression', left: node, right, line: t.line, column: t.column };
12938
+
12939
+ return {
12940
+ type: 'AssignmentExpression',
12941
+ left: node,
12942
+ right,
12943
+ line: t.line,
12944
+ column: t.column
12945
+ };
12092
12946
  }
12093
12947
 
12094
12948
  return node;
12095
12949
  }
12950
+
12096
12951
  ternary() {
12097
12952
  let node = this.nullishCoalescing();
12953
+
12098
12954
  while (this.current.type === 'QUESTION') {
12099
12955
  const t = this.current;
12100
12956
  this.eat('QUESTION');
12101
- const consequent = this.expression();
12957
+
12958
+ const consequent = this.expression();
12959
+
12960
+ if (this.current.type !== 'COLON') {
12961
+ throw new ParseError(
12962
+ "Expected ':' in conditional expression",
12963
+ this.current,
12964
+ this.source,
12965
+ "Ternary expressions must follow the form: condition ? a : b"
12966
+ );
12967
+ }
12968
+
12102
12969
  this.eat('COLON');
12103
- const alternate = this.expression();
12104
- node = { type: 'ConditionalExpression', test: node, consequent, alternate, line: t.line, column: t.column };
12970
+ const alternate = this.expression();
12971
+
12972
+ node = {
12973
+ type: 'ConditionalExpression',
12974
+ test: node,
12975
+ consequent,
12976
+ alternate,
12977
+ line: t.line,
12978
+ column: t.column
12979
+ };
12105
12980
  }
12981
+
12106
12982
  return node;
12107
12983
  }
12984
+
12108
12985
  nullishCoalescing() {
12109
12986
  let node = this.logicalOr();
12110
12987
  while (this.current.type === 'NULLISH_COALESCING') {
@@ -12187,6 +13064,16 @@ unary() {
12187
13064
  if (['NOT', 'MINUS', 'PLUS'].includes(t.type)) {
12188
13065
  const op = t.type;
12189
13066
  this.eat(op);
13067
+
13068
+ if (this.current.type === 'EOF') {
13069
+ throw new ParseError(
13070
+ "Missing operand for unary operator",
13071
+ this.current,
13072
+ this.source,
13073
+ "Unary operators must be followed by an expression"
13074
+ );
13075
+ }
13076
+
12190
13077
  return {
12191
13078
  type: 'UnaryExpression',
12192
13079
  operator: op,
@@ -12199,7 +13086,18 @@ unary() {
12199
13086
  if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
12200
13087
  const op = t.type;
12201
13088
  this.eat(op);
13089
+
12202
13090
  const argument = this.unary();
13091
+
13092
+ if (!argument || !argument.type) {
13093
+ throw new ParseError(
13094
+ "Invalid operand for update operator",
13095
+ t,
13096
+ this.source,
13097
+ "Increment and decrement operators must apply to a variable"
13098
+ );
13099
+ }
13100
+
12203
13101
  return {
12204
13102
  type: 'UpdateExpression',
12205
13103
  operator: op,
@@ -12212,9 +13110,9 @@ unary() {
12212
13110
 
12213
13111
  return this.postfix();
12214
13112
  }
12215
-
12216
13113
  postfix() {
12217
13114
  let node = this.primary();
13115
+
12218
13116
  while (true) {
12219
13117
  const t = this.current;
12220
13118
 
@@ -12222,9 +13120,27 @@ postfix() {
12222
13120
  const startLine = t.line;
12223
13121
  const startCol = t.column;
12224
13122
  this.eat('LBRACKET');
13123
+
12225
13124
  const index = this.expression();
12226
- if (this.current.type === 'RBRACKET') this.eat('RBRACKET');
12227
- node = { type: 'IndexExpression', object: node, indexer: index, line: startLine, column: startCol };
13125
+
13126
+ if (this.current.type !== 'RBRACKET') {
13127
+ throw new ParseError(
13128
+ "Expected ']' after index expression",
13129
+ this.current,
13130
+ this.source,
13131
+ "Index access must be closed, e.g. arr[0]"
13132
+ );
13133
+ }
13134
+
13135
+ this.eat('RBRACKET');
13136
+
13137
+ node = {
13138
+ type: 'IndexExpression',
13139
+ object: node,
13140
+ indexer: index,
13141
+ line: startLine,
13142
+ column: startCol
13143
+ };
12228
13144
  continue;
12229
13145
  }
12230
13146
 
@@ -12232,13 +13148,46 @@ postfix() {
12232
13148
  const startLine = t.line;
12233
13149
  const startCol = t.column;
12234
13150
  this.eat('LPAREN');
13151
+
12235
13152
  const args = [];
12236
- while (this.current.type !== 'RPAREN' && this.current.type !== 'EOF') {
13153
+
13154
+ while (this.current.type !== 'RPAREN') {
13155
+ if (this.current.type === 'EOF') {
13156
+ throw new ParseError(
13157
+ "Unterminated function call",
13158
+ this.current,
13159
+ this.source,
13160
+ "Did you forget to close ')'?"
13161
+ );
13162
+ }
13163
+
12237
13164
  args.push(this.expression());
12238
- if (this.current.type === 'COMMA') this.eat('COMMA');
13165
+
13166
+ if (this.current.type === 'COMMA') {
13167
+ this.eat('COMMA');
13168
+ } else {
13169
+ break;
13170
+ }
13171
+ }
13172
+
13173
+ if (this.current.type !== 'RPAREN') {
13174
+ throw new ParseError(
13175
+ "Expected ')' after function arguments",
13176
+ this.current,
13177
+ this.source,
13178
+ "Function calls must be closed with ')'"
13179
+ );
12239
13180
  }
12240
- if (this.current.type === 'RPAREN') this.eat('RPAREN');
12241
- node = { type: 'CallExpression', callee: node, arguments: args, line: startLine, column: startCol };
13181
+
13182
+ this.eat('RPAREN');
13183
+
13184
+ node = {
13185
+ type: 'CallExpression',
13186
+ callee: node,
13187
+ arguments: args,
13188
+ line: startLine,
13189
+ column: startCol
13190
+ };
12242
13191
  continue;
12243
13192
  }
12244
13193
 
@@ -12246,21 +13195,46 @@ postfix() {
12246
13195
  const startLine = t.line;
12247
13196
  const startCol = t.column;
12248
13197
  this.eat('DOT');
12249
- const property = this.current.value || '';
12250
- if (this.current.type === 'IDENTIFIER') this.eat('IDENTIFIER');
12251
- node = { type: 'MemberExpression', object: node, property, line: startLine, column: startCol };
13198
+
13199
+ if (this.current.type !== 'IDENTIFIER') {
13200
+ throw new ParseError(
13201
+ "Expected property name after '.'",
13202
+ this.current,
13203
+ this.source,
13204
+ "Member access requires a property name, e.g. obj.value"
13205
+ );
13206
+ }
13207
+
13208
+ const property = this.current.value;
13209
+ this.eat('IDENTIFIER');
13210
+
13211
+ node = {
13212
+ type: 'MemberExpression',
13213
+ object: node,
13214
+ property,
13215
+ line: startLine,
13216
+ column: startCol
13217
+ };
12252
13218
  continue;
12253
13219
  }
12254
13220
 
12255
13221
  if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
12256
13222
  this.eat(t.type);
12257
- node = { type: 'UpdateExpression', operator: t.type, argument: node, prefix: false, line: t.line, column: t.column };
13223
+
13224
+ node = {
13225
+ type: 'UpdateExpression',
13226
+ operator: t.type,
13227
+ argument: node,
13228
+ prefix: false,
13229
+ line: t.line,
13230
+ column: t.column
13231
+ };
12258
13232
  continue;
12259
13233
  }
12260
13234
 
12261
-
12262
13235
  break;
12263
13236
  }
13237
+
12264
13238
  return node;
12265
13239
  }
12266
13240
 
@@ -12272,10 +13246,19 @@ arrowFunction(params) {
12272
13246
  let isBlock = false;
12273
13247
 
12274
13248
  if (this.current.type === 'LBRACE') {
12275
- body = this.block();
13249
+ body = this.block();
12276
13250
  isBlock = true;
12277
- } else {
12278
- body = this.expression();
13251
+ }
13252
+ else if (this.current.type === 'EOF') {
13253
+ throw new ParseError(
13254
+ "Missing arrow function body",
13255
+ this.current,
13256
+ this.source,
13257
+ "Arrow functions require a body, e.g. x => x * 2"
13258
+ );
13259
+ }
13260
+ else {
13261
+ body = this.expression();
12279
13262
  }
12280
13263
 
12281
13264
  const startLine = params.length > 0 ? params[0].line : t.line;
@@ -12291,75 +13274,116 @@ arrowFunction(params) {
12291
13274
  };
12292
13275
  }
12293
13276
 
12294
-
12295
- primary() {
13277
+ primary() {
12296
13278
  const t = this.current;
12297
13279
 
12298
- if (t.type === 'NUMBER') {
12299
- this.eat('NUMBER');
12300
- return { type: 'Literal', value: t.value, line: t.line, column: t.column };
13280
+ if (t.type === 'NUMBER') {
13281
+ this.eat('NUMBER');
13282
+ return { type: 'Literal', value: t.value, line: t.line, column: t.column };
12301
13283
  }
12302
13284
 
12303
- if (t.type === 'STRING') {
12304
- this.eat('STRING');
12305
- return { type: 'Literal', value: t.value, line: t.line, column: t.column };
13285
+ if (t.type === 'STRING') {
13286
+ this.eat('STRING');
13287
+ return { type: 'Literal', value: t.value, line: t.line, column: t.column };
12306
13288
  }
12307
13289
 
12308
- if (t.type === 'TRUE') {
12309
- this.eat('TRUE');
12310
- return { type: 'Literal', value: true, line: t.line, column: t.column };
13290
+ if (t.type === 'TRUE') {
13291
+ this.eat('TRUE');
13292
+ return { type: 'Literal', value: true, line: t.line, column: t.column };
12311
13293
  }
12312
13294
 
12313
- if (t.type === 'FALSE') {
12314
- this.eat('FALSE');
12315
- return { type: 'Literal', value: false, line: t.line, column: t.column };
13295
+ if (t.type === 'FALSE') {
13296
+ this.eat('FALSE');
13297
+ return { type: 'Literal', value: false, line: t.line, column: t.column };
12316
13298
  }
12317
13299
 
12318
- if (t.type === 'NULL') {
12319
- this.eat('NULL');
12320
- return { type: 'Literal', value: null, line: t.line, column: t.column };
13300
+ if (t.type === 'NULL') {
13301
+ this.eat('NULL');
13302
+ return { type: 'Literal', value: null, line: t.line, column: t.column };
12321
13303
  }
12322
13304
 
12323
13305
  if (t.type === 'AWAIT') {
12324
13306
  this.eat('AWAIT');
12325
- const argument = this.expression();
13307
+ const argument = this.expression();
12326
13308
  return { type: 'AwaitExpression', argument, line: t.line, column: t.column };
12327
13309
  }
12328
13310
 
12329
13311
  if (t.type === 'NEW') {
12330
13312
  this.eat('NEW');
12331
13313
  const callee = this.primary();
13314
+
13315
+ if (this.current.type !== 'LPAREN') {
13316
+ throw new ParseError(
13317
+ "Expected '(' after 'new'",
13318
+ this.current,
13319
+ this.source,
13320
+ "Use 'new ClassName(...)'"
13321
+ );
13322
+ }
13323
+
12332
13324
  this.eat('LPAREN');
12333
13325
  const args = [];
12334
13326
  if (this.current.type !== 'RPAREN') {
12335
13327
  args.push(this.expression());
12336
13328
  while (this.current.type === 'COMMA') {
12337
13329
  this.eat('COMMA');
12338
- if (this.current.type !== 'RPAREN') args.push(this.expression());
13330
+ if (this.current.type === 'RPAREN') break;
13331
+ args.push(this.expression());
12339
13332
  }
12340
13333
  }
13334
+
13335
+ if (this.current.type !== 'RPAREN') {
13336
+ throw new ParseError(
13337
+ "Expected ')' to close arguments for 'new'",
13338
+ this.current,
13339
+ this.source
13340
+ );
13341
+ }
13342
+
12341
13343
  this.eat('RPAREN');
13344
+
12342
13345
  return { type: 'NewExpression', callee, arguments: args, line: t.line, column: t.column };
12343
13346
  }
12344
13347
 
13348
+ // ---- ask(...) function call ----
12345
13349
  if (t.type === 'ASK') {
12346
13350
  this.eat('ASK');
13351
+
13352
+ if (this.current.type !== 'LPAREN') {
13353
+ throw new ParseError(
13354
+ "Expected '(' after 'ask'",
13355
+ this.current,
13356
+ this.source
13357
+ );
13358
+ }
13359
+
12347
13360
  this.eat('LPAREN');
12348
13361
  const args = [];
12349
13362
  if (this.current.type !== 'RPAREN') {
12350
13363
  args.push(this.expression());
12351
13364
  while (this.current.type === 'COMMA') {
12352
13365
  this.eat('COMMA');
12353
- if (this.current.type !== 'RPAREN') args.push(this.expression());
13366
+ if (this.current.type === 'RPAREN') break;
13367
+ args.push(this.expression());
12354
13368
  }
12355
13369
  }
13370
+
13371
+ if (this.current.type !== 'RPAREN') {
13372
+ throw new ParseError(
13373
+ "Expected ')' after arguments to 'ask'",
13374
+ this.current,
13375
+ this.source
13376
+ );
13377
+ }
13378
+
12356
13379
  this.eat('RPAREN');
12357
- return {
12358
- type: 'CallExpression',
12359
- callee: { type: 'Identifier', name: 'ask', line: t.line, column: t.column },
12360
- arguments: args,
12361
- line: t.line,
12362
- column: t.column
13380
+
13381
+ return {
13382
+ type: 'CallExpression',
13383
+ callee: { type: 'Identifier', name: 'ask', line: t.line, column: t.column },
13384
+ arguments: args,
13385
+ line: t.line,
13386
+ column: t.column
12363
13387
  };
12364
13388
  }
12365
13389
 
@@ -12378,89 +13402,120 @@ arrowFunction(params) {
12378
13402
  const startLine = t.line;
12379
13403
  const startCol = t.column;
12380
13404
  this.eat('LPAREN');
12381
- const elements = [];
12382
13405
 
13406
+ const elements = [];
12383
13407
  if (this.current.type !== 'RPAREN') {
12384
13408
  elements.push(this.expression());
12385
13409
  while (this.current.type === 'COMMA') {
12386
13410
  this.eat('COMMA');
12387
- if (this.current.type !== 'RPAREN') elements.push(this.expression());
13411
+ if (this.current.type === 'RPAREN') break;
13412
+ elements.push(this.expression());
12388
13413
  }
12389
13414
  }
12390
13415
 
13416
+ if (this.current.type !== 'RPAREN') {
13417
+ throw new ParseError(
13418
+ "Expected ')' after expression",
13419
+ this.current,
13420
+ this.source
13421
+ );
13422
+ }
13423
+
12391
13424
  this.eat('RPAREN');
12392
13425
 
12393
- if (this.current.type === 'ARROW') return this.arrowFunction(elements);
13426
+ if (this.current.type === 'ARROW') {
13427
+ return this.arrowFunction(elements);
13428
+ }
12394
13429
 
12395
- return elements.length === 1 ? elements[0] : { type: 'ArrayExpression', elements, line: startLine, column: startCol };
13430
+ return elements.length === 1
13431
+ ? elements[0]
13432
+ : { type: 'ArrayExpression', elements, line: startLine, column: startCol };
12396
13433
  }
12397
13434
 
12398
13435
  if (t.type === 'LBRACKET') {
12399
13436
  const startLine = t.line;
12400
13437
  const startCol = t.column;
12401
13438
  this.eat('LBRACKET');
13439
+
12402
13440
  const elements = [];
12403
13441
  if (this.current.type !== 'RBRACKET') {
12404
13442
  elements.push(this.expression());
12405
13443
  while (this.current.type === 'COMMA') {
12406
13444
  this.eat('COMMA');
12407
- if (this.current.type !== 'RBRACKET') elements.push(this.expression());
13445
+ if (this.current.type === 'RBRACKET') break;
13446
+ elements.push(this.expression());
12408
13447
  }
12409
13448
  }
13449
+
13450
+ if (this.current.type !== 'RBRACKET') {
13451
+ throw new ParseError(
13452
+ "Expected ']' after array elements",
13453
+ this.current,
13454
+ this.source
13455
+ );
13456
+ }
13457
+
12410
13458
  this.eat('RBRACKET');
13459
+
12411
13460
  return { type: 'ArrayExpression', elements, line: startLine, column: startCol };
12412
13461
  }
12413
13462
 
12414
13463
  if (t.type === 'LBRACE') {
12415
- const startLine = t.line;
12416
- const startCol = t.column;
12417
- this.eat('LBRACE');
13464
+ const startLine = t.line;
13465
+ const startCol = t.column;
13466
+ this.eat('LBRACE');
12418
13467
 
12419
- const props = [];
13468
+ const props = [];
12420
13469
 
12421
- while (this.current.type !== 'RBRACE') {
12422
- let key;
13470
+ while (this.current.type !== 'RBRACE') {
13471
+ if (this.current.type === 'EOF') {
13472
+ throw new ParseError(
13473
+ "Unterminated object literal",
13474
+ this.current,
13475
+ this.source,
13476
+ "Did you forget to close '}'?"
13477
+ );
13478
+ }
12423
13479
 
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
- };
12433
- }
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
- }
13480
+ let key;
12451
13481
 
12452
- this.eat('COLON');
12453
- const value = this.expression();
13482
+ if (this.current.type === 'IDENTIFIER') {
13483
+ const k = this.current;
13484
+ this.eat('IDENTIFIER');
13485
+ key = { type: 'Literal', value: k.value, line: k.line, column: k.column };
13486
+ }
13487
+ else if (this.current.type === 'STRING') {
13488
+ const k = this.current;
13489
+ this.eat('STRING');
13490
+ key = { type: 'Literal', value: k.value, line: k.line, column: k.column };
13491
+ }
13492
+ else {
13493
+ throw new ParseError(
13494
+ 'Invalid object key',
13495
+ this.current,
13496
+ this.source,
13497
+ "Object keys must be identifiers or strings"
13498
+ );
13499
+ }
12454
13500
 
12455
- props.push({ key, value });
13501
+ if (this.current.type !== 'COLON') {
13502
+ throw new ParseError(
13503
+ "Expected ':' after object key",
13504
+ this.current,
13505
+ this.source
13506
+ );
13507
+ }
12456
13508
 
12457
- if (this.current.type === 'COMMA') this.eat('COMMA');
12458
- }
13509
+ this.eat('COLON');
13510
+ const value = this.expression();
13511
+ props.push({ key, value });
12459
13512
 
12460
- this.eat('RBRACE');
12461
- return { type: 'ObjectExpression', props, line: startLine, column: startCol };
12462
- }
13513
+ if (this.current.type === 'COMMA') this.eat('COMMA');
13514
+ }
12463
13515
 
13516
+ this.eat('RBRACE');
13517
+ return { type: 'ObjectExpression', props, line: startLine, column: startCol };
13518
+ }
12464
13519
 
12465
13520
  throw new ParseError(
12466
13521
  `Unexpected token '${t.type}'`,
@@ -12469,7 +13524,6 @@ arrowFunction(params) {
12469
13524
  );
12470
13525
  }
12471
13526
 
12472
-
12473
13527
  }
12474
13528
 
12475
13529
  module.exports = Parser;
@@ -12652,7 +13706,7 @@ const Lexer = __nccwpck_require__(211);
12652
13706
  const Parser = __nccwpck_require__(222);
12653
13707
  const Evaluator = __nccwpck_require__(112);
12654
13708
 
12655
- const VERSION = '1.1.10';
13709
+ const VERSION = '1.1.12';
12656
13710
 
12657
13711
  const COLOR = {
12658
13712
  reset: '\x1b[0m',