starlight-cli 1.1.11 → 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
 
@@ -10693,10 +10691,17 @@ async evalProgram(node, env) {
10693
10691
  try {
10694
10692
  result = await this.evaluate(stmt, env);
10695
10693
  } catch (e) {
10694
+ // Re-throw known runtime control signals
10696
10695
  if (e instanceof RuntimeError || e instanceof BreakSignal || e instanceof ContinueSignal || e instanceof ReturnValue) {
10697
10696
  throw e;
10698
10697
  }
10699
- 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
+ );
10700
10705
  }
10701
10706
  }
10702
10707
  return result;
@@ -10706,6 +10711,7 @@ async evalStartStatement(node, env) {
10706
10711
  try {
10707
10712
  const value = await this.evaluate(node.discriminant, env);
10708
10713
  let executing = false;
10714
+
10709
10715
  for (const c of node.cases) {
10710
10716
  try {
10711
10717
  if (!executing) {
@@ -10723,11 +10729,11 @@ async evalStartStatement(node, env) {
10723
10729
  caseErr instanceof ContinueSignal) {
10724
10730
  throw caseErr; // propagate signals
10725
10731
  }
10726
-
10727
10732
  throw new RuntimeError(
10728
10733
  caseErr.message || 'Error evaluating case in start statement',
10729
10734
  c,
10730
- this.source
10735
+ this.source,
10736
+ env
10731
10737
  );
10732
10738
  }
10733
10739
  }
@@ -10743,28 +10749,31 @@ async evalStartStatement(node, env) {
10743
10749
  throw new RuntimeError(
10744
10750
  err.message || 'Error evaluating start statement',
10745
10751
  node,
10746
- this.source
10752
+ this.source,
10753
+ env
10747
10754
  );
10748
10755
  }
10749
10756
  }
10750
10757
 
10751
-
10752
10758
  async evalRaceClause(node, env) {
10753
10759
  try {
10754
10760
  const testValue = await this.evaluate(node.test, env);
10755
10761
  const result = await this.evaluate(node.consequent, new Environment(env));
10756
10762
  return { testValue, result };
10757
10763
  } catch (err) {
10758
- if (err instanceof RuntimeError ||
10759
- err instanceof ReturnValue ||
10760
- err instanceof BreakSignal ||
10761
- err instanceof ContinueSignal) {
10764
+ if (
10765
+ err instanceof RuntimeError ||
10766
+ err instanceof ReturnValue ||
10767
+ err instanceof BreakSignal ||
10768
+ err instanceof ContinueSignal
10769
+ ) {
10762
10770
  throw err;
10763
10771
  }
10764
10772
  throw new RuntimeError(
10765
10773
  err.message || 'Error evaluating race clause',
10766
10774
  node,
10767
- this.source
10775
+ this.source,
10776
+ env
10768
10777
  );
10769
10778
  }
10770
10779
  }
@@ -10775,7 +10784,12 @@ async evalDoTrack(node, env) {
10775
10784
  } catch (err) {
10776
10785
  if (!node.handler) {
10777
10786
  if (err instanceof RuntimeError) throw err;
10778
- 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
+ );
10779
10793
  }
10780
10794
 
10781
10795
  const trackEnv = new Environment(env);
@@ -10785,12 +10799,16 @@ async evalDoTrack(node, env) {
10785
10799
  return await this.evaluate(node.handler, trackEnv);
10786
10800
  } catch (handlerErr) {
10787
10801
  if (handlerErr instanceof RuntimeError) throw handlerErr;
10788
- 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
+ );
10789
10808
  }
10790
10809
  }
10791
10810
  }
10792
10811
 
10793
-
10794
10812
  async evalImport(node, env) {
10795
10813
  const spec = node.path;
10796
10814
  let lib;
@@ -10806,34 +10824,51 @@ async evalImport(node, env) {
10806
10824
  : path.join(process.cwd(), spec.endsWith('.sl') ? spec : spec + '.sl');
10807
10825
 
10808
10826
  if (!fs.existsSync(fullPath)) {
10809
- 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
+ );
10810
10833
  }
10811
10834
 
10812
- const code = fs.readFileSync(fullPath, 'utf-8');
10813
- const tokens = new Lexer(code).getTokens();
10814
- 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();
10815
10839
 
10816
- const moduleEnv = new Environment(env);
10817
- await this.evaluate(ast, moduleEnv);
10840
+ const moduleEnv = new Environment(env);
10841
+ await this.evaluate(ast, moduleEnv);
10818
10842
 
10819
- lib = {};
10820
- for (const key of Object.keys(moduleEnv.store)) {
10821
- lib[key] = moduleEnv.store[key];
10822
- }
10843
+ lib = {};
10844
+ for (const key of Object.keys(moduleEnv.store)) {
10845
+ lib[key] = moduleEnv.store[key];
10846
+ }
10823
10847
 
10824
- 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
+ }
10825
10857
  }
10826
10858
 
10827
10859
  for (const imp of node.specifiers) {
10828
10860
  if (imp.type === 'DefaultImport') {
10829
10861
  env.define(imp.local, lib.default ?? lib);
10830
- }
10831
- if (imp.type === 'NamespaceImport') {
10862
+ } else if (imp.type === 'NamespaceImport') {
10832
10863
  env.define(imp.local, lib);
10833
- }
10834
- if (imp.type === 'NamedImport') {
10864
+ } else if (imp.type === 'NamedImport') {
10835
10865
  if (!(imp.imported in lib)) {
10836
- 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
+ );
10837
10872
  }
10838
10873
  env.define(imp.local, lib[imp.imported]);
10839
10874
  }
@@ -10841,7 +10876,6 @@ async evalImport(node, env) {
10841
10876
 
10842
10877
  return null;
10843
10878
  }
10844
-
10845
10879
  async evalBlock(node, env) {
10846
10880
  let result = null;
10847
10881
  for (const stmt of node.body) {
@@ -10860,32 +10894,55 @@ async evalBlock(node, env) {
10860
10894
  throw new RuntimeError(
10861
10895
  e.message || 'Error in block',
10862
10896
  stmt,
10863
- this.source
10897
+ this.source,
10898
+ env // pass env for suggestions
10864
10899
  );
10865
10900
  }
10866
10901
  }
10867
10902
  return result;
10868
10903
  }
10869
10904
 
10870
-
10871
-
10872
10905
  async evalVarDeclaration(node, env) {
10873
10906
  if (!node.expr) {
10874
- 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
+ );
10875
10913
  }
10876
10914
 
10877
- const val = await this.evaluate(node.expr, env);
10878
- 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
+ }
10879
10927
  }
10880
10928
 
10881
-
10882
10929
  evalArrowFunction(node, env) {
10883
10930
  if (!node.body) {
10884
- 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
+ );
10885
10937
  }
10886
10938
 
10887
10939
  if (!Array.isArray(node.params)) {
10888
- 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
+ );
10889
10946
  }
10890
10947
 
10891
10948
  const evaluator = this;
@@ -10893,7 +10950,6 @@ evalArrowFunction(node, env) {
10893
10950
  return async function (...args) {
10894
10951
  const localEnv = new Environment(env);
10895
10952
 
10896
- // Bind parameters safely
10897
10953
  node.params.forEach((p, i) => {
10898
10954
  const paramName = typeof p === 'string' ? p : p.name;
10899
10955
  localEnv.define(paramName, args[i]);
@@ -10901,78 +10957,128 @@ evalArrowFunction(node, env) {
10901
10957
 
10902
10958
  try {
10903
10959
  if (node.isBlock) {
10904
- // Block body
10905
10960
  const result = await evaluator.evaluate(node.body, localEnv);
10906
- return result === undefined ? null : result; // ensure null instead of undefined
10961
+ return result === undefined ? null : result;
10907
10962
  } else {
10908
- // Expression body
10909
10963
  const result = await evaluator.evaluate(node.body, localEnv);
10910
- return result === undefined ? null : result; // ensure null instead of undefined
10964
+ return result === undefined ? null : result;
10911
10965
  }
10912
10966
  } catch (err) {
10913
10967
  if (err instanceof ReturnValue) return err.value === undefined ? null : err.value;
10914
- 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
+ );
10915
10975
  }
10916
10976
  };
10917
10977
  }
10918
10978
 
10919
-
10920
-
10921
10979
  async evalAssignment(node, env) {
10922
10980
  const rightVal = await this.evaluate(node.right, env);
10923
10981
  const left = node.left;
10924
10982
 
10925
- 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
+ }
10997
+
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
+ }
10926
11010
 
10927
- if (left.type === 'MemberExpression') {
10928
- const obj = await this.evaluate(left.object, env);
10929
- if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
10930
- obj[left.property] = rightVal; // dynamic creation of new properties allowed
10931
- return rightVal;
10932
- }
11011
+ throw new RuntimeError(
11012
+ 'Invalid assignment target',
11013
+ node,
11014
+ this.source,
11015
+ env
11016
+ );
10933
11017
 
10934
- if (left.type === 'IndexExpression') {
10935
- const obj = await this.evaluate(left.object, env);
10936
- const idx = await this.evaluate(left.indexer, env);
10937
- if (obj == null) throw new RuntimeError('Cannot assign to null or undefined', node, this.source);
10938
- obj[idx] = rightVal; // dynamic creation allowed
10939
- return rightVal;
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
+ );
10940
11026
  }
10941
-
10942
- throw new RuntimeError('Invalid assignment target', node, this.source);
10943
11027
  }
10944
11028
 
10945
11029
  async evalCompoundAssignment(node, env) {
10946
11030
  const left = node.left;
10947
11031
  let current;
10948
11032
 
10949
- if (left.type === 'Identifier') current = env.get(left.name, left, this.source);
10950
- else if (left.type === 'MemberExpression') current = await this.evalMember(left, env) ?? 0;
10951
- else if (left.type === 'IndexExpression') current = await this.evalIndex(left, env) ?? 0;
10952
- 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
+ );
10953
11043
 
10954
- const rhs = await this.evaluate(node.right, env);
10955
- 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
+ }
10956
11060
 
10957
- switch (node.operator) {
10958
- case 'PLUSEQ': computed = current + rhs; break;
10959
- case 'MINUSEQ': computed = current - rhs; break;
10960
- case 'STAREQ': computed = current * rhs; break;
10961
- case 'SLASHEQ': computed = current / rhs; break;
10962
- case 'MODEQ': computed = current % rhs; break;
10963
- default: throw new RuntimeError(`Unknown compound operator: ${node.operator}`, node, this.source);
10964
- }
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
+ }
10965
11068
 
10966
- if (left.type === 'Identifier') env.set(left.name, computed);
10967
- else if (left.type === 'MemberExpression')
10968
- await this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
10969
- else
10970
- await this.evalAssignment({ left, right: { type: 'Literal', value: computed }, type: 'AssignmentExpression' }, env);
11069
+ return computed;
10971
11070
 
10972
- 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
+ }
10973
11080
  }
10974
11081
 
10975
-
10976
11082
  async evalSldeploy(node, env) {
10977
11083
  const val = await this.evaluate(node.expr, env);
10978
11084
  console.log(this.formatValue(val));
@@ -10980,108 +11086,162 @@ async evalSldeploy(node, env) {
10980
11086
  }
10981
11087
 
10982
11088
 
10983
-
10984
11089
  async evalAsk(node, env) {
10985
- const prompt = await this.evaluate(node.prompt, env);
10986
-
10987
- if (typeof prompt !== 'string') {
10988
- throw new RuntimeError('ask() prompt must be a string', node, this.source);
10989
- }
11090
+ try {
11091
+ const prompt = await this.evaluate(node.prompt, env);
10990
11092
 
10991
- const input = readlineSync.question(prompt + ' ');
10992
- return input;
10993
- }
11093
+ if (typeof prompt !== 'string') {
11094
+ throw new RuntimeError('ask() prompt must be a string', node, this.source, env);
11095
+ }
10994
11096
 
11097
+ const input = readlineSync.question(prompt + ' ');
11098
+ return input;
10995
11099
 
10996
- async evalDefine(node, env) {
10997
- if (!node.id || typeof node.id !== 'string') {
10998
- 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
+ );
10999
11108
  }
11000
-
11001
- const val = node.expr ? await this.evaluate(node.expr, env) : null;
11002
- return env.define(node.id, val);
11003
-
11004
11109
  }
11005
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
+ }
11006
11116
 
11007
- async evalBinary(node, env) {
11008
- const l = await this.evaluate(node.left, env);
11009
- const r = await this.evaluate(node.right, env);
11010
-
11011
- if (node.operator === 'SLASH' && r === 0) {
11012
- throw new RuntimeError('Division by zero', node, this.source);
11013
- }
11014
-
11015
- switch (node.operator) {
11016
- case 'PLUS': {
11017
- if (Array.isArray(l) && Array.isArray(r)) {
11018
- return l.concat(r);
11019
- }
11117
+ const val = node.expr ? await this.evaluate(node.expr, env) : null;
11118
+ return env.define(node.id, val);
11020
11119
 
11021
- if (typeof l === 'string' || typeof r === 'string') {
11022
- 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
+ );
11023
11128
  }
11129
+ }
11024
11130
 
11025
- if (typeof l === 'number' && typeof r === 'number') {
11026
- return l + r;
11027
- }
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);
11028
11135
 
11029
- if (typeof l === 'object' && typeof r === 'object') {
11030
- return { ...l, ...r };
11031
- }
11136
+ if (node.operator === 'SLASH' && r === 0) {
11137
+ throw new RuntimeError('Division by zero', node, this.source, env);
11138
+ }
11032
11139
 
11033
- throw new RuntimeError(
11034
- `Unsupported operands for +: ${typeof l} and ${typeof r}`,
11035
- node,
11036
- this.source
11037
- );
11038
- }
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 };
11039
11146
 
11040
- case 'MINUS': return l - r;
11041
- case 'STAR': return l * r;
11042
- case 'SLASH': return l / r;
11043
- case 'MOD': return l % r;
11044
- case 'EQEQ': return l === r;
11045
- case 'NOTEQ': return l !== r;
11046
- case 'LT': return l < r;
11047
- case 'LTE': return l <= r;
11048
- case 'GT': return l > r;
11049
- case 'GTE': return l >= r;
11050
- 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
+ }
11051
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
+ );
11052
11181
  }
11053
11182
  }
11054
-
11055
11183
  async evalLogical(node, env) {
11056
- const l = await this.evaluate(node.left, env);
11057
-
11058
- switch(node.operator) {
11059
- case 'AND': return l && await this.evaluate(node.right, env);
11060
- case 'OR': return l || await this.evaluate(node.right, env);
11061
- case '??': {
11062
- const r = await this.evaluate(node.right, env);
11063
- 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
+ );
11064
11201
  }
11065
- default:
11066
- 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
+ );
11067
11211
  }
11068
11212
  }
11069
11213
 
11070
-
11071
11214
  async evalUnary(node, env) {
11072
- const val = await this.evaluate(node.argument, env);
11073
- switch (node.operator) {
11074
- case 'NOT': return !val;
11075
- case 'MINUS': return -val;
11076
- case 'PLUS': return +val;
11077
- 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
+ }
11078
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
+ );
11079
11239
  }
11080
11240
  }
11241
+
11081
11242
  async evalIf(node, env) {
11082
11243
  let test = await this.evaluate(node.test, env);
11083
-
11084
- test = !!test;
11244
+ test = !!test; // coerce to boolean
11085
11245
 
11086
11246
  if (test) {
11087
11247
  return await this.evaluate(node.consequent, env);
@@ -11095,183 +11255,270 @@ async evalIf(node, env) {
11095
11255
  }
11096
11256
 
11097
11257
  async evalWhile(node, env) {
11098
- while (true) {
11099
- let test = await this.evaluate(node.test, env);
11100
-
11101
- test = !!test;
11258
+ try {
11259
+ while (true) {
11260
+ let test = await this.evaluate(node.test, env);
11261
+ test = !!test;
11102
11262
 
11103
- if (!test) break;
11263
+ if (!test) break;
11104
11264
 
11105
- try {
11106
- await this.evaluate(node.body, env);
11107
- } catch (e) {
11108
- if (e instanceof BreakSignal) break;
11109
- if (e instanceof ContinueSignal) continue;
11110
- throw e;
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
+ }
11111
11272
  }
11112
- }
11113
11273
 
11114
- return null;
11115
- }
11274
+ return null;
11116
11275
 
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
+ );
11284
+ }
11285
+ }
11117
11286
  async evalFor(node, env) {
11118
- if (node.type === 'ForInStatement') {
11119
- const iterable = await this.evaluate(node.iterable, env);
11120
-
11121
- if (iterable == null || typeof iterable !== 'object') {
11122
- throw new RuntimeError('Cannot iterate over non-iterable', node, this.source);
11123
- }
11124
-
11125
- const loopVar = node.variable; // string name of the loop variable
11126
- const createLoopEnv = () => node.letKeyword ? new Environment(env) : env;
11127
- if (Array.isArray(iterable)) {
11128
- for (const value of iterable) {
11129
- const loopEnv = createLoopEnv();
11130
- loopEnv.define(loopVar, value);
11131
-
11132
- try {
11133
- await this.evaluate(node.body, loopEnv);
11134
- } catch (e) {
11135
- if (e instanceof BreakSignal) break;
11136
- if (e instanceof ContinueSignal) continue;
11137
- throw e;
11138
- }
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
+ );
11139
11299
  }
11140
- }
11141
- else {
11142
- for (const key of Object.keys(iterable)) {
11143
- const loopEnv = createLoopEnv();
11144
- loopEnv.define(loopVar, key);
11145
-
11146
- try {
11147
- await this.evaluate(node.body, loopEnv);
11148
- } catch (e) {
11149
- if (e instanceof BreakSignal) break;
11150
- if (e instanceof ContinueSignal) continue;
11151
- 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
+ }
11152
11329
  }
11153
11330
  }
11154
- }
11155
11331
 
11156
- return null;
11157
- }
11332
+ return null;
11333
+ }
11158
11334
 
11159
- const local = new Environment(env);
11335
+ // Standard for loop
11336
+ const local = new Environment(env);
11160
11337
 
11161
- if (node.init) await this.evaluate(node.init, local);
11338
+ if (node.init) await this.evaluate(node.init, local);
11162
11339
 
11163
- while (!node.test || await this.evaluate(node.test, local)) {
11164
- try {
11165
- await this.evaluate(node.body, local);
11166
- } catch (e) {
11167
- if (e instanceof BreakSignal) break;
11168
- if (e instanceof ContinueSignal) {
11169
- if (node.update) await this.evaluate(node.update, local);
11170
- 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;
11171
11350
  }
11172
- throw e;
11351
+
11352
+ if (node.update) await this.evaluate(node.update, local);
11173
11353
  }
11174
11354
 
11175
- if (node.update) await this.evaluate(node.update, local);
11176
- }
11355
+ return null;
11177
11356
 
11178
- 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
+ }
11179
11366
  }
11180
11367
 
11181
11368
  evalFunctionDeclaration(node, env) {
11182
- if (!node.name || typeof node.name !== 'string') {
11183
- throw new RuntimeError('Function declaration requires a valid name', node, this.source);
11184
- }
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
+ }
11185
11373
 
11186
- if (!Array.isArray(node.params)) {
11187
- throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source);
11188
- }
11374
+ if (!Array.isArray(node.params)) {
11375
+ throw new RuntimeError(`Invalid parameter list in function '${node.name}'`, node, this.source, env);
11376
+ }
11189
11377
 
11190
- if (!node.body) {
11191
- throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source);
11192
- }
11378
+ if (!node.body) {
11379
+ throw new RuntimeError(`Function '${node.name}' has no body`, node, this.source, env);
11380
+ }
11193
11381
 
11194
- const fn = {
11195
- params: node.params,
11196
- body: node.body,
11197
- env,
11198
- async: node.async || false
11199
- };
11382
+ const fn = {
11383
+ params: node.params,
11384
+ body: node.body,
11385
+ env,
11386
+ async: node.async || false
11387
+ };
11200
11388
 
11201
- env.define(node.name, fn);
11202
- return null;
11203
- }
11389
+ env.define(node.name, fn);
11390
+ return null;
11204
11391
 
11205
- async evalCall(node, env) {
11206
- const calleeEvaluated = await this.evaluate(node.callee, env);
11207
- if (typeof calleeEvaluated === 'function') {
11208
- const args = [];
11209
- for (const a of node.arguments) args.push(await this.evaluate(a, env));
11210
- return await calleeEvaluated(...args);
11211
- }
11212
- if (!calleeEvaluated || typeof calleeEvaluated !== 'object' || !calleeEvaluated.body) {
11213
- 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
+ );
11214
11400
  }
11215
-
11216
- const fn = calleeEvaluated;
11217
- const callEnv = new Environment(fn.env);
11218
-
11219
- for (let i = 0; i < fn.params.length; i++) {
11220
- const argVal = node.arguments[i]
11221
- ? await this.evaluate(node.arguments[i], env)
11222
- : null;
11223
-
11224
- const param = fn.params[i];
11225
- const paramName = typeof param === 'string' ? param : param.name;
11226
-
11227
- callEnv.define(paramName, argVal);
11228
11401
  }
11229
-
11402
+ async evalCall(node, env) {
11230
11403
  try {
11231
- const result = await this.evaluate(fn.body, callEnv);
11232
- return result;
11233
- } catch (e) {
11234
- if (e instanceof ReturnValue) return e.value;
11235
- throw e;
11404
+ const calleeEvaluated = await this.evaluate(node.callee, env);
11405
+
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
+ }
11412
+
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
+ }
11422
+
11423
+ const fn = calleeEvaluated;
11424
+ const callEnv = new Environment(fn.env);
11425
+
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;
11430
+
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
+ );
11236
11452
  }
11237
11453
  }
11238
11454
 
11239
11455
  async evalIndex(node, env) {
11240
- const obj = await this.evaluate(node.object, env);
11241
- const idx = await this.evaluate(node.indexer, env);
11242
-
11243
- if (obj == null) throw new RuntimeError('Cannot index null or undefined', node, this.source);
11244
- if (Array.isArray(obj)) {
11245
- if (idx < 0 || idx >= obj.length) return undefined;
11246
- return obj[idx];
11247
- }
11248
- if (typeof obj === 'object') {
11249
- return obj[idx]; // undefined if missing
11250
- }
11456
+ try {
11457
+ const obj = await this.evaluate(node.object, env);
11458
+ const idx = await this.evaluate(node.indexer, env);
11251
11459
 
11252
- return undefined;
11253
- }
11254
- async evalObject(node, env) {
11255
- const out = {};
11256
- for (const p of node.props) {
11257
- if (!p.key) {
11258
- 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
+ );
11259
11467
  }
11260
11468
 
11261
- const key = await this.evaluate(p.key, env);
11262
- let value = null;
11263
-
11264
- if (p.value) {
11265
- value = await this.evaluate(p.value, env);
11266
- if (value === undefined) value = null; // <- force null instead of undefined
11469
+ if (Array.isArray(obj)) {
11470
+ if (idx < 0 || idx >= obj.length) return undefined;
11471
+ return obj[idx];
11267
11472
  }
11268
11473
 
11269
- 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
+ );
11270
11485
  }
11271
- return out;
11272
11486
  }
11273
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;
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
+ }
11274
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
+ }
11275
11522
 
11276
11523
 
11277
11524
  async evalMember(node, env) {
@@ -11290,33 +11537,61 @@ async evalMember(node, env) {
11290
11537
  return prop;
11291
11538
  }
11292
11539
 
11293
-
11294
11540
  async evalUpdate(node, env) {
11295
11541
  const arg = node.argument;
11296
- const getCurrent = async () => {
11297
- if (arg.type === 'Identifier') return env.get(arg.name, arg, this.source);
11298
- if (arg.type === 'MemberExpression') return await this.evalMember(arg, env);
11299
- if (arg.type === 'IndexExpression') return await this.evalIndex(arg, env);
11300
- throw new RuntimeError('Invalid update target', node, this.source);
11301
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
+ }
11302
11558
  };
11559
+
11303
11560
  const setValue = async (v) => {
11304
- if (arg.type === 'Identifier') env.set(arg.name, v);
11305
- else if (arg.type === 'MemberExpression') {
11306
- const obj = await this.evaluate(arg.object, env);
11307
- obj[arg.property] = v;
11308
- } else if (arg.type === 'IndexExpression') {
11309
- const obj = await this.evaluate(arg.object, env);
11310
- const idx = await this.evaluate(arg.indexer, env);
11311
- 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
+ );
11312
11582
  }
11313
11583
  };
11314
11584
 
11315
11585
  const current = await getCurrent();
11316
- const newVal = (node.operator === 'PLUSPLUS') ? current + 1 : current - 1;
11586
+ const newVal = node.operator === 'PLUSPLUS' ? current + 1 : current - 1;
11317
11587
 
11318
- if (node.prefix) { await setValue(newVal); return newVal; }
11319
- 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
+ }
11320
11595
  }
11321
11596
 
11322
11597
  }
@@ -11561,12 +11836,16 @@ module.exports = Lexer;
11561
11836
  /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
11562
11837
 
11563
11838
  class ParseError extends Error {
11564
- constructor(message, token, source) {
11839
+ constructor(message, token, source, suggestion = null) {
11565
11840
  const line = token?.line ?? '?';
11566
11841
  const column = token?.column ?? '?';
11567
11842
 
11568
11843
  let output = `${message}\n`;
11569
11844
 
11845
+ if (suggestion) {
11846
+ output += `Hint: ${suggestion}\n`;
11847
+ }
11848
+
11570
11849
  if (source && token?.line != null) {
11571
11850
  const lines = source.split('\n');
11572
11851
  const srcLine = lines[token.line - 1] || '';
@@ -11582,6 +11861,7 @@ class ParseError extends Error {
11582
11861
  }
11583
11862
  }
11584
11863
 
11864
+
11585
11865
  class Parser {
11586
11866
  constructor(tokens, source = '') {
11587
11867
  this.tokens = tokens;
@@ -11649,17 +11929,49 @@ class Parser {
11649
11929
  varDeclaration() {
11650
11930
  const t = this.current;
11651
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
+
11652
11942
  const idToken = this.current;
11653
11943
  const id = idToken.value;
11654
11944
  this.eat('IDENTIFIER');
11655
11945
 
11656
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
+
11657
11957
  if (this.current.type === 'EQUAL') {
11658
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
+
11659
11969
  expr = this.expression();
11660
11970
  }
11661
11971
 
11662
- if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
11972
+ if (this.current.type === 'SEMICOLON') {
11973
+ this.eat('SEMICOLON');
11974
+ }
11663
11975
 
11664
11976
  return {
11665
11977
  type: 'VarDeclaration',
@@ -11669,12 +11981,31 @@ varDeclaration() {
11669
11981
  column: t.column
11670
11982
  };
11671
11983
  }
11984
+
11672
11985
  startStatement() {
11673
11986
  const t = this.current;
11674
11987
  this.eat('START');
11675
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
+
11676
11998
  const discriminant = this.expression();
11677
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
+
11678
12009
  this.eat('LBRACE');
11679
12010
 
11680
12011
  const cases = [];
@@ -11683,6 +12014,24 @@ startStatement() {
11683
12014
  cases.push(this.raceClause());
11684
12015
  }
11685
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
+
11686
12035
  this.eat('RBRACE');
11687
12036
 
11688
12037
  return {
@@ -11697,8 +12046,26 @@ raceClause() {
11697
12046
  const t = this.current;
11698
12047
  this.eat('RACE');
11699
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
+
11700
12058
  const test = this.expression();
11701
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
+
11702
12069
  const consequent = this.block();
11703
12070
 
11704
12071
  return {
@@ -11710,11 +12077,26 @@ raceClause() {
11710
12077
  };
11711
12078
  }
11712
12079
 
12080
+
11713
12081
  sldeployStatement() {
11714
12082
  const t = this.current;
11715
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
+
11716
12094
  const expr = this.expression();
11717
- if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
12095
+
12096
+ if (this.current.type === 'SEMICOLON') {
12097
+ this.eat('SEMICOLON');
12098
+ }
12099
+
11718
12100
  return {
11719
12101
  type: 'SldeployStatement',
11720
12102
  expr,
@@ -11727,11 +12109,31 @@ doTrackStatement() {
11727
12109
  const t = this.current;
11728
12110
  this.eat('DO');
11729
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
+
11730
12121
  const body = this.block();
11731
12122
 
11732
12123
  let handler = null;
12124
+
11733
12125
  if (this.current.type === 'TRACK') {
11734
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
+
11735
12137
  handler = this.block();
11736
12138
  }
11737
12139
 
@@ -11747,17 +12149,49 @@ doTrackStatement() {
11747
12149
  defineStatement() {
11748
12150
  const t = this.current;
11749
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
+
11750
12162
  const idToken = this.current;
11751
12163
  const id = idToken.value;
11752
12164
  this.eat('IDENTIFIER');
11753
12165
 
11754
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
+
11755
12177
  if (this.current.type === 'EQUAL') {
11756
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
+
11757
12189
  expr = this.expression();
11758
12190
  }
11759
12191
 
11760
- if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
12192
+ if (this.current.type === 'SEMICOLON') {
12193
+ this.eat('SEMICOLON');
12194
+ }
11761
12195
 
11762
12196
  return {
11763
12197
  type: 'DefineStatement',
@@ -11770,30 +12204,80 @@ defineStatement() {
11770
12204
 
11771
12205
  asyncFuncDeclaration() {
11772
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
+
11773
12217
  const name = this.current.value;
11774
12218
  this.eat('IDENTIFIER');
11775
12219
 
11776
12220
  let params = [];
12221
+
11777
12222
  if (this.current.type === 'LPAREN') {
11778
12223
  this.eat('LPAREN');
12224
+
11779
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
+
11780
12235
  params.push(this.current.value);
11781
12236
  this.eat('IDENTIFIER');
12237
+
11782
12238
  while (this.current.type === 'COMMA') {
11783
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
+
11784
12250
  params.push(this.current.value);
11785
12251
  this.eat('IDENTIFIER');
11786
12252
  }
11787
12253
  }
11788
- this.eat('RPAREN');
11789
- } else {
11790
- if (this.current.type === 'IDENTIFIER') {
11791
- params.push(this.current.value);
11792
- 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
+ );
11793
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
+ );
11794
12277
  }
11795
12278
 
11796
12279
  const body = this.block();
12280
+
11797
12281
  return {
11798
12282
  type: 'FunctionDeclaration',
11799
12283
  name,
@@ -11804,32 +12288,62 @@ asyncFuncDeclaration() {
11804
12288
  column: t.column
11805
12289
  };
11806
12290
  }
11807
-
11808
12291
  ifStatement() {
11809
12292
  const t = this.current;
11810
12293
  this.eat('IF');
11811
12294
 
11812
12295
  let test;
12296
+
11813
12297
  if (this.current.type === 'LPAREN') {
11814
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
+
11815
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
+
11816
12320
  this.eat('RPAREN');
11817
12321
  } else {
11818
12322
  test = this.expression();
11819
12323
  }
11820
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
+
11821
12334
  const consequent = this.statementOrBlock();
11822
12335
 
11823
12336
  let alternate = null;
12337
+
11824
12338
  if (this.current.type === 'ELSE') {
11825
- this.eat('ELSE');
11826
- if (this.current.type === 'IF') {
11827
- alternate = this.ifStatement();
11828
- } else {
11829
- alternate = this.statementOrBlock();
11830
- }
11831
- }
12339
+ this.eat('ELSE');
11832
12340
 
12341
+ if (this.current.type === 'IF') {
12342
+ alternate = this.ifStatement();
12343
+ } else {
12344
+ alternate = this.statementOrBlock();
12345
+ }
12346
+ }
11833
12347
 
11834
12348
  return {
11835
12349
  type: 'IfStatement',
@@ -11840,6 +12354,7 @@ ifStatement() {
11840
12354
  column: t.column
11841
12355
  };
11842
12356
  }
12357
+
11843
12358
  parseExpressionOnly() {
11844
12359
  return this.expression();
11845
12360
  }
@@ -11852,13 +12367,30 @@ whileStatement() {
11852
12367
 
11853
12368
  if (this.current.type === 'LPAREN') {
11854
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
+
11855
12380
  test = this.expression();
12381
+
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
+
11856
12391
  this.eat('RPAREN');
11857
12392
  }
11858
12393
  else {
11859
- const startPos = this.pos;
11860
- const startToken = this.current;
11861
-
11862
12394
  const exprTokens = [];
11863
12395
  let braceFound = false;
11864
12396
  let depth = 0;
@@ -11878,7 +12410,8 @@ whileStatement() {
11878
12410
  throw new ParseError(
11879
12411
  "Expected '{' after while condition",
11880
12412
  this.current,
11881
- this.source
12413
+ this.source,
12414
+ "While loops must be followed by a block"
11882
12415
  );
11883
12416
  }
11884
12417
 
@@ -11887,8 +12420,18 @@ whileStatement() {
11887
12420
  test = exprParser.parseExpressionOnly();
11888
12421
  }
11889
12422
 
11890
- const body = this.block();
11891
- return {
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 {
11892
12435
  type: 'WhileStatement',
11893
12436
  test,
11894
12437
  body,
@@ -11897,21 +12440,67 @@ whileStatement() {
11897
12440
  };
11898
12441
  }
11899
12442
 
11900
-
11901
12443
  importStatement() {
11902
12444
  const t = this.current;
11903
12445
  this.eat('IMPORT');
11904
12446
 
11905
12447
  let specifiers = [];
12448
+
11906
12449
  if (this.current.type === 'STAR') {
11907
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
+
11908
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
+
11909
12472
  const name = this.current.value;
11910
12473
  this.eat('IDENTIFIER');
11911
- 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
+
11912
12482
  } else if (this.current.type === 'LBRACE') {
11913
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
+
11914
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
+
11915
12504
  const importedName = this.current.value;
11916
12505
  const importedLine = this.current.line;
11917
12506
  const importedColumn = this.current.column;
@@ -11920,27 +12509,77 @@ importStatement() {
11920
12509
  let localName = importedName;
11921
12510
  if (this.current.type === 'AS') {
11922
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
+
11923
12522
  localName = this.current.value;
11924
12523
  this.eat('IDENTIFIER');
11925
12524
  }
11926
12525
 
11927
- 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
+
11928
12534
  if (this.current.type === 'COMMA') this.eat('COMMA');
11929
12535
  }
12536
+
11930
12537
  this.eat('RBRACE');
12538
+
11931
12539
  } else if (this.current.type === 'IDENTIFIER') {
11932
12540
  const localName = this.current.value;
11933
12541
  const localLine = this.current.line;
11934
12542
  const localColumn = this.current.column;
11935
12543
  this.eat('IDENTIFIER');
11936
- 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
+
11937
12552
  } else {
11938
- 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
+ );
11939
12568
  }
11940
12569
 
11941
12570
  this.eat('FROM');
12571
+
11942
12572
  const pathToken = this.current;
11943
- 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
+
11944
12583
  this.eat('STRING');
11945
12584
 
11946
12585
  if (this.current.type === 'SEMICOLON') this.eat('SEMICOLON');
@@ -11953,14 +12592,14 @@ importStatement() {
11953
12592
  column: t.column
11954
12593
  };
11955
12594
  }
11956
-
11957
12595
  forStatement() {
11958
- const t = this.current; // FOR token
12596
+ const t = this.current;
11959
12597
  this.eat('FOR');
11960
12598
 
11961
- if ((this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
11962
- (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')) {
11963
-
12599
+ if (
12600
+ (this.current.type === 'LET' && this.peekType() === 'IDENTIFIER' && this.peekType(2) === 'IN') ||
12601
+ (this.current.type === 'IDENTIFIER' && this.peekType() === 'IN')
12602
+ ) {
11964
12603
  let variable;
11965
12604
  let variableLine, variableColumn;
11966
12605
  let iterable;
@@ -11969,22 +12608,45 @@ forStatement() {
11969
12608
  if (this.current.type === 'LET') {
11970
12609
  letKeyword = true;
11971
12610
  this.eat('LET');
11972
- variableLine = this.current.line;
11973
- variableColumn = this.current.column;
11974
- variable = this.current.value;
11975
- this.eat('IDENTIFIER');
11976
- this.eat('IN');
11977
- iterable = this.expression();
11978
- } else {
11979
- variableLine = this.current.line;
11980
- variableColumn = this.current.column;
11981
- variable = this.current.value;
11982
- this.eat('IDENTIFIER');
11983
- this.eat('IN');
11984
- 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
+ );
11985
12646
  }
11986
12647
 
11987
12648
  const body = this.block();
12649
+
11988
12650
  return {
11989
12651
  type: 'ForInStatement',
11990
12652
  variable,
@@ -12006,25 +12668,53 @@ forStatement() {
12006
12668
  this.eat('LPAREN');
12007
12669
 
12008
12670
  if (this.current.type !== 'SEMICOLON') {
12009
- init = this.current.type === 'LET' ? this.varDeclaration() : this.expressionStatement();
12671
+ init = this.current.type === 'LET'
12672
+ ? this.varDeclaration()
12673
+ : this.expressionStatement();
12010
12674
  } else {
12011
12675
  this.eat('SEMICOLON');
12012
12676
  }
12013
12677
 
12014
- if (this.current.type !== 'SEMICOLON') test = this.expression();
12678
+ if (this.current.type !== 'SEMICOLON') {
12679
+ test = this.expression();
12680
+ }
12681
+
12015
12682
  this.eat('SEMICOLON');
12016
12683
 
12017
- 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
+
12018
12697
  this.eat('RPAREN');
12019
12698
  } else {
12020
- init = this.expression();
12021
- if (this.current.type === 'IN') {
12022
- this.eat('IN');
12023
- test = this.expression(); // iterable
12024
- }
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
+ );
12025
12714
  }
12026
12715
 
12027
12716
  const body = this.block();
12717
+
12028
12718
  return {
12029
12719
  type: 'ForStatement',
12030
12720
  init,
@@ -12049,41 +12739,111 @@ continueStatement() {
12049
12739
  if (this.current.type === 'SEMICOLON') this.advance();
12050
12740
  return { type: 'ContinueStatement', line: t.line, column: t.column };
12051
12741
  }
12052
-
12053
12742
  funcDeclaration() {
12054
12743
  const t = this.current;
12055
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
+
12056
12755
  const nameToken = this.current;
12057
12756
  const name = nameToken.value;
12058
12757
  this.eat('IDENTIFIER');
12059
12758
 
12060
12759
  let params = [];
12760
+
12061
12761
  if (this.current.type === 'LPAREN') {
12062
12762
  this.eat('LPAREN');
12763
+
12063
12764
  if (this.current.type !== 'RPAREN') {
12064
- const paramToken = this.current;
12065
- 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
+ });
12066
12780
  this.eat('IDENTIFIER');
12781
+
12067
12782
  while (this.current.type === 'COMMA') {
12068
12783
  this.eat('COMMA');
12069
- const paramToken2 = this.current;
12070
- 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
+ });
12071
12800
  this.eat('IDENTIFIER');
12072
12801
  }
12073
12802
  }
12074
- this.eat('RPAREN');
12075
- } else {
12076
- if (this.current.type === 'IDENTIFIER') {
12077
- const paramToken = this.current;
12078
- params.push({ name: paramToken.value, line: paramToken.line, column: paramToken.column });
12079
- 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
+ );
12080
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
+ );
12081
12832
  }
12082
12833
 
12083
12834
  const body = this.block();
12084
- 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
+ };
12085
12844
  }
12086
12845
 
12846
+
12087
12847
  returnStatement() {
12088
12848
  const t = this.current; // RETURN token
12089
12849
  this.eat('RETURN');
@@ -12103,18 +12863,35 @@ statementOrBlock() {
12103
12863
  }
12104
12864
  return this.statement();
12105
12865
  }
12106
-
12107
12866
  block() {
12108
12867
  const t = this.current; // LBRACE token
12109
12868
  this.eat('LBRACE');
12869
+
12110
12870
  const body = [];
12871
+
12111
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
+ }
12112
12881
  body.push(this.statement());
12113
12882
  }
12883
+
12114
12884
  this.eat('RBRACE');
12115
- 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
+ };
12116
12892
  }
12117
12893
 
12894
+
12118
12895
  expressionStatement() {
12119
12896
  const exprToken = this.current;
12120
12897
  const expr = this.expression();
@@ -12125,7 +12902,6 @@ expressionStatement() {
12125
12902
  expression() {
12126
12903
  return this.assignment();
12127
12904
  }
12128
-
12129
12905
  assignment() {
12130
12906
  const node = this.ternary();
12131
12907
  const compoundOps = ['PLUSEQ', 'MINUSEQ', 'STAREQ', 'SLASHEQ', 'MODEQ'];
@@ -12136,29 +12912,76 @@ assignment() {
12136
12912
  const op = t.type;
12137
12913
  this.eat(op);
12138
12914
  const right = this.assignment();
12139
- 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
+ );
12140
12933
  }
12141
12934
 
12142
12935
  if (t.type === 'EQUAL') {
12143
12936
  this.eat('EQUAL');
12144
12937
  const right = this.assignment();
12145
- 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
+ };
12146
12946
  }
12147
12947
 
12148
12948
  return node;
12149
12949
  }
12950
+
12150
12951
  ternary() {
12151
12952
  let node = this.nullishCoalescing();
12953
+
12152
12954
  while (this.current.type === 'QUESTION') {
12153
12955
  const t = this.current;
12154
12956
  this.eat('QUESTION');
12155
- 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
+
12156
12969
  this.eat('COLON');
12157
- const alternate = this.expression();
12158
- 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
+ };
12159
12980
  }
12981
+
12160
12982
  return node;
12161
12983
  }
12984
+
12162
12985
  nullishCoalescing() {
12163
12986
  let node = this.logicalOr();
12164
12987
  while (this.current.type === 'NULLISH_COALESCING') {
@@ -12241,6 +13064,16 @@ unary() {
12241
13064
  if (['NOT', 'MINUS', 'PLUS'].includes(t.type)) {
12242
13065
  const op = t.type;
12243
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
+
12244
13077
  return {
12245
13078
  type: 'UnaryExpression',
12246
13079
  operator: op,
@@ -12253,7 +13086,18 @@ unary() {
12253
13086
  if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
12254
13087
  const op = t.type;
12255
13088
  this.eat(op);
13089
+
12256
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
+
12257
13101
  return {
12258
13102
  type: 'UpdateExpression',
12259
13103
  operator: op,
@@ -12266,9 +13110,9 @@ unary() {
12266
13110
 
12267
13111
  return this.postfix();
12268
13112
  }
12269
-
12270
13113
  postfix() {
12271
13114
  let node = this.primary();
13115
+
12272
13116
  while (true) {
12273
13117
  const t = this.current;
12274
13118
 
@@ -12276,9 +13120,27 @@ postfix() {
12276
13120
  const startLine = t.line;
12277
13121
  const startCol = t.column;
12278
13122
  this.eat('LBRACKET');
13123
+
12279
13124
  const index = this.expression();
12280
- if (this.current.type === 'RBRACKET') this.eat('RBRACKET');
12281
- 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
+ };
12282
13144
  continue;
12283
13145
  }
12284
13146
 
@@ -12286,13 +13148,46 @@ postfix() {
12286
13148
  const startLine = t.line;
12287
13149
  const startCol = t.column;
12288
13150
  this.eat('LPAREN');
13151
+
12289
13152
  const args = [];
12290
- 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
+
12291
13164
  args.push(this.expression());
12292
- if (this.current.type === 'COMMA') this.eat('COMMA');
13165
+
13166
+ if (this.current.type === 'COMMA') {
13167
+ this.eat('COMMA');
13168
+ } else {
13169
+ break;
13170
+ }
12293
13171
  }
12294
- if (this.current.type === 'RPAREN') this.eat('RPAREN');
12295
- node = { type: 'CallExpression', callee: node, arguments: args, line: startLine, column: startCol };
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
+ );
13180
+ }
13181
+
13182
+ this.eat('RPAREN');
13183
+
13184
+ node = {
13185
+ type: 'CallExpression',
13186
+ callee: node,
13187
+ arguments: args,
13188
+ line: startLine,
13189
+ column: startCol
13190
+ };
12296
13191
  continue;
12297
13192
  }
12298
13193
 
@@ -12300,21 +13195,46 @@ postfix() {
12300
13195
  const startLine = t.line;
12301
13196
  const startCol = t.column;
12302
13197
  this.eat('DOT');
12303
- const property = this.current.value || '';
12304
- if (this.current.type === 'IDENTIFIER') this.eat('IDENTIFIER');
12305
- 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
+ };
12306
13218
  continue;
12307
13219
  }
12308
13220
 
12309
13221
  if (t.type === 'PLUSPLUS' || t.type === 'MINUSMINUS') {
12310
13222
  this.eat(t.type);
12311
- 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
+ };
12312
13232
  continue;
12313
13233
  }
12314
13234
 
12315
-
12316
13235
  break;
12317
13236
  }
13237
+
12318
13238
  return node;
12319
13239
  }
12320
13240
 
@@ -12326,10 +13246,19 @@ arrowFunction(params) {
12326
13246
  let isBlock = false;
12327
13247
 
12328
13248
  if (this.current.type === 'LBRACE') {
12329
- body = this.block();
13249
+ body = this.block();
12330
13250
  isBlock = true;
12331
- } else {
12332
- 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();
12333
13262
  }
12334
13263
 
12335
13264
  const startLine = params.length > 0 ? params[0].line : t.line;
@@ -12345,75 +13274,116 @@ arrowFunction(params) {
12345
13274
  };
12346
13275
  }
12347
13276
 
12348
-
12349
- primary() {
13277
+ primary() {
12350
13278
  const t = this.current;
12351
13279
 
12352
- if (t.type === 'NUMBER') {
12353
- this.eat('NUMBER');
12354
- 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 };
12355
13283
  }
12356
13284
 
12357
- if (t.type === 'STRING') {
12358
- this.eat('STRING');
12359
- 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 };
12360
13288
  }
12361
13289
 
12362
- if (t.type === 'TRUE') {
12363
- this.eat('TRUE');
12364
- 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 };
12365
13293
  }
12366
13294
 
12367
- if (t.type === 'FALSE') {
12368
- this.eat('FALSE');
12369
- 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 };
12370
13298
  }
12371
13299
 
12372
- if (t.type === 'NULL') {
12373
- this.eat('NULL');
12374
- 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 };
12375
13303
  }
12376
13304
 
12377
13305
  if (t.type === 'AWAIT') {
12378
13306
  this.eat('AWAIT');
12379
- const argument = this.expression();
13307
+ const argument = this.expression();
12380
13308
  return { type: 'AwaitExpression', argument, line: t.line, column: t.column };
12381
13309
  }
12382
13310
 
12383
13311
  if (t.type === 'NEW') {
12384
13312
  this.eat('NEW');
12385
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
+
12386
13324
  this.eat('LPAREN');
12387
13325
  const args = [];
12388
13326
  if (this.current.type !== 'RPAREN') {
12389
13327
  args.push(this.expression());
12390
13328
  while (this.current.type === 'COMMA') {
12391
13329
  this.eat('COMMA');
12392
- if (this.current.type !== 'RPAREN') args.push(this.expression());
13330
+ if (this.current.type === 'RPAREN') break;
13331
+ args.push(this.expression());
12393
13332
  }
12394
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
+
12395
13343
  this.eat('RPAREN');
13344
+
12396
13345
  return { type: 'NewExpression', callee, arguments: args, line: t.line, column: t.column };
12397
13346
  }
12398
13347
 
13348
+ // ---- ask(...) function call ----
12399
13349
  if (t.type === 'ASK') {
12400
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
+
12401
13360
  this.eat('LPAREN');
12402
13361
  const args = [];
12403
13362
  if (this.current.type !== 'RPAREN') {
12404
13363
  args.push(this.expression());
12405
13364
  while (this.current.type === 'COMMA') {
12406
13365
  this.eat('COMMA');
12407
- if (this.current.type !== 'RPAREN') args.push(this.expression());
13366
+ if (this.current.type === 'RPAREN') break;
13367
+ args.push(this.expression());
12408
13368
  }
12409
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
+
12410
13379
  this.eat('RPAREN');
12411
- return {
12412
- type: 'CallExpression',
12413
- callee: { type: 'Identifier', name: 'ask', line: t.line, column: t.column },
12414
- arguments: args,
12415
- line: t.line,
12416
- 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
12417
13387
  };
12418
13388
  }
12419
13389
 
@@ -12432,89 +13402,120 @@ arrowFunction(params) {
12432
13402
  const startLine = t.line;
12433
13403
  const startCol = t.column;
12434
13404
  this.eat('LPAREN');
12435
- const elements = [];
12436
13405
 
13406
+ const elements = [];
12437
13407
  if (this.current.type !== 'RPAREN') {
12438
13408
  elements.push(this.expression());
12439
13409
  while (this.current.type === 'COMMA') {
12440
13410
  this.eat('COMMA');
12441
- if (this.current.type !== 'RPAREN') elements.push(this.expression());
13411
+ if (this.current.type === 'RPAREN') break;
13412
+ elements.push(this.expression());
12442
13413
  }
12443
13414
  }
12444
13415
 
13416
+ if (this.current.type !== 'RPAREN') {
13417
+ throw new ParseError(
13418
+ "Expected ')' after expression",
13419
+ this.current,
13420
+ this.source
13421
+ );
13422
+ }
13423
+
12445
13424
  this.eat('RPAREN');
12446
13425
 
12447
- if (this.current.type === 'ARROW') return this.arrowFunction(elements);
13426
+ if (this.current.type === 'ARROW') {
13427
+ return this.arrowFunction(elements);
13428
+ }
12448
13429
 
12449
- 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 };
12450
13433
  }
12451
13434
 
12452
13435
  if (t.type === 'LBRACKET') {
12453
13436
  const startLine = t.line;
12454
13437
  const startCol = t.column;
12455
13438
  this.eat('LBRACKET');
13439
+
12456
13440
  const elements = [];
12457
13441
  if (this.current.type !== 'RBRACKET') {
12458
13442
  elements.push(this.expression());
12459
13443
  while (this.current.type === 'COMMA') {
12460
13444
  this.eat('COMMA');
12461
- if (this.current.type !== 'RBRACKET') elements.push(this.expression());
13445
+ if (this.current.type === 'RBRACKET') break;
13446
+ elements.push(this.expression());
12462
13447
  }
12463
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
+
12464
13458
  this.eat('RBRACKET');
13459
+
12465
13460
  return { type: 'ArrayExpression', elements, line: startLine, column: startCol };
12466
13461
  }
12467
13462
 
12468
13463
  if (t.type === 'LBRACE') {
12469
- const startLine = t.line;
12470
- const startCol = t.column;
12471
- this.eat('LBRACE');
13464
+ const startLine = t.line;
13465
+ const startCol = t.column;
13466
+ this.eat('LBRACE');
12472
13467
 
12473
- const props = [];
13468
+ const props = [];
12474
13469
 
12475
- while (this.current.type !== 'RBRACE') {
12476
- 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
+ }
12477
13479
 
12478
- if (this.current.type === 'IDENTIFIER') {
12479
- const k = this.current;
12480
- this.eat('IDENTIFIER');
12481
- key = {
12482
- type: 'Literal',
12483
- value: k.value,
12484
- line: k.line,
12485
- column: k.column
12486
- };
12487
- }
12488
- else if (this.current.type === 'STRING') {
12489
- const k = this.current;
12490
- this.eat('STRING');
12491
- key = {
12492
- type: 'Literal',
12493
- value: k.value,
12494
- line: k.line,
12495
- column: k.column
12496
- };
12497
- }
12498
- else {
12499
- throw new ParseError(
12500
- 'Invalid object key',
12501
- this.current,
12502
- this.source
12503
- );
12504
- }
13480
+ let key;
12505
13481
 
12506
- this.eat('COLON');
12507
- 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
+ }
12508
13500
 
12509
- 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
+ }
12510
13508
 
12511
- if (this.current.type === 'COMMA') this.eat('COMMA');
12512
- }
13509
+ this.eat('COLON');
13510
+ const value = this.expression();
13511
+ props.push({ key, value });
12513
13512
 
12514
- this.eat('RBRACE');
12515
- return { type: 'ObjectExpression', props, line: startLine, column: startCol };
12516
- }
13513
+ if (this.current.type === 'COMMA') this.eat('COMMA');
13514
+ }
12517
13515
 
13516
+ this.eat('RBRACE');
13517
+ return { type: 'ObjectExpression', props, line: startLine, column: startCol };
13518
+ }
12518
13519
 
12519
13520
  throw new ParseError(
12520
13521
  `Unexpected token '${t.type}'`,
@@ -12523,7 +13524,6 @@ arrowFunction(params) {
12523
13524
  );
12524
13525
  }
12525
13526
 
12526
-
12527
13527
  }
12528
13528
 
12529
13529
  module.exports = Parser;
@@ -12706,7 +13706,7 @@ const Lexer = __nccwpck_require__(211);
12706
13706
  const Parser = __nccwpck_require__(222);
12707
13707
  const Evaluator = __nccwpck_require__(112);
12708
13708
 
12709
- const VERSION = '1.1.11';
13709
+ const VERSION = '1.1.12';
12710
13710
 
12711
13711
  const COLOR = {
12712
13712
  reset: '\x1b[0m',