parser-lr 0.8.0 → 0.8.1

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.
Files changed (47) hide show
  1. package/bin/parser-lr.js +295 -52
  2. package/bin/parser-lr.js.map +1 -1
  3. package/dist/lib/diagnostics/format-diagnostic.d.ts +31 -0
  4. package/dist/lib/diagnostics/format-diagnostic.d.ts.map +1 -0
  5. package/dist/lib/diagnostics/format-diagnostic.js +36 -0
  6. package/dist/lib/diagnostics/format-diagnostic.js.map +1 -0
  7. package/dist/lib/diagnostics/index.d.ts +5 -0
  8. package/dist/lib/diagnostics/index.d.ts.map +1 -0
  9. package/dist/lib/diagnostics/index.js +3 -0
  10. package/dist/lib/diagnostics/index.js.map +1 -0
  11. package/dist/lib/diagnostics/source-position.d.ts +24 -0
  12. package/dist/lib/diagnostics/source-position.d.ts.map +1 -0
  13. package/dist/lib/diagnostics/source-position.js +39 -0
  14. package/dist/lib/diagnostics/source-position.js.map +1 -0
  15. package/dist/lib/grammar/ast-type.d.ts +2 -0
  16. package/dist/lib/grammar/ast-type.d.ts.map +1 -1
  17. package/dist/lib/grammar/grammar-from-cst.js +4 -3
  18. package/dist/lib/grammar/grammar-from-cst.js.map +1 -1
  19. package/dist/lib/grammar/index.d.ts +1 -1
  20. package/dist/lib/grammar/index.d.ts.map +1 -1
  21. package/dist/lib/grammar/production.d.ts +2 -0
  22. package/dist/lib/grammar/production.d.ts.map +1 -1
  23. package/dist/lib/grammar/table-validator.d.ts +12 -2
  24. package/dist/lib/grammar/table-validator.d.ts.map +1 -1
  25. package/dist/lib/grammar/table-validator.js +49 -24
  26. package/dist/lib/grammar/table-validator.js.map +1 -1
  27. package/dist/lib/grammar/transform-rule.d.ts +3 -0
  28. package/dist/lib/grammar/transform-rule.d.ts.map +1 -1
  29. package/dist/lib/grammar-entry.d.ts +1 -1
  30. package/dist/lib/grammar-entry.d.ts.map +1 -1
  31. package/dist/lib/grammar-entry.js.map +1 -1
  32. package/dist/lib/index.d.ts +3 -0
  33. package/dist/lib/index.d.ts.map +1 -1
  34. package/dist/lib/index.js +1 -0
  35. package/dist/lib/index.js.map +1 -1
  36. package/dist/lib/parse-context.d.ts +7 -0
  37. package/dist/lib/parse-context.d.ts.map +1 -1
  38. package/dist/lib/parse-context.js +9 -0
  39. package/dist/lib/parse-context.js.map +1 -1
  40. package/dist/lib/parse-table/table/lr-parse-table.d.ts.map +1 -1
  41. package/dist/lib/parse-table/table/lr-parse-table.js +10 -3
  42. package/dist/lib/parse-table/table/lr-parse-table.js.map +1 -1
  43. package/dist/lib/parser-lr.d.ts +15 -0
  44. package/dist/lib/parser-lr.d.ts.map +1 -1
  45. package/dist/lib/parser-lr.js +38 -9
  46. package/dist/lib/parser-lr.js.map +1 -1
  47. package/package.json +1 -1
package/bin/parser-lr.js CHANGED
@@ -3781,7 +3781,7 @@ const {
3781
3781
  Help,
3782
3782
  } = commander;
3783
3783
 
3784
- var version$1 = "0.8.0";
3784
+ var version$1 = "0.8.1";
3785
3785
  var packageDefinition = {
3786
3786
  version: version$1};
3787
3787
 
@@ -5920,6 +5920,80 @@ function buildLalrGotoTargets(grammar, analysis, lr1Collection, lr1ToLalr) {
5920
5920
  Lr1ItemSetBuilder.buildLr1ToLalrMap = buildLr1ToLalrMap;
5921
5921
  })(Lr1ItemSetBuilder || (Lr1ItemSetBuilder = {}));
5922
5922
 
5923
+ /**
5924
+ * Converts a source offset into a 1-based line and column.
5925
+ *
5926
+ * @param source - Full source text.
5927
+ * @param offset - Zero-based UTF-16 code unit offset.
5928
+ * @returns Line and column for the offset, clamped to the source bounds.
5929
+ */
5930
+ function offsetToPosition(source, offset) {
5931
+ // Clamp out-of-range offsets to the nearest valid index.
5932
+ const clamped = Math.max(0, Math.min(offset, source.length));
5933
+ let line = 1;
5934
+ let column = 1;
5935
+ // Scan characters before the offset, advancing line on each newline.
5936
+ for (let index = 0; index < clamped; index += 1) {
5937
+ if (source[index] === '\n') {
5938
+ line += 1;
5939
+ column = 1;
5940
+ continue;
5941
+ }
5942
+ column += 1;
5943
+ }
5944
+ return { line, column };
5945
+ }
5946
+ /**
5947
+ * Returns the start offset from a location or raw offset value.
5948
+ *
5949
+ * @param location - Optional source span.
5950
+ * @param offset - Optional raw offset when no span is available.
5951
+ */
5952
+ function resolveDiagnosticOffset(location, offset) {
5953
+ if (location !== null && location !== undefined) {
5954
+ return location.offset;
5955
+ }
5956
+ if (offset === null || offset === undefined) {
5957
+ return null;
5958
+ }
5959
+ return offset;
5960
+ }
5961
+
5962
+ /**
5963
+ * Formats a diagnostic as `path:line:column: severity: message` when possible.
5964
+ *
5965
+ * @param options - Severity, message, and optional source position inputs.
5966
+ * @returns One diagnostic line without a trailing newline.
5967
+ */
5968
+ function formatDiagnostic(options) {
5969
+ const prefix = formatDiagnosticLocationPrefix(options);
5970
+ const body = `${options.severity}: ${options.message}`;
5971
+ if (prefix === null) {
5972
+ return body;
5973
+ }
5974
+ return `${prefix}: ${body}`;
5975
+ }
5976
+ /**
5977
+ * Builds the `path:line:column` or `path` prefix for a diagnostic.
5978
+ *
5979
+ * @param options - Path and optional source position inputs.
5980
+ * @returns Location prefix, or null when no path is available.
5981
+ */
5982
+ function formatDiagnosticLocationPrefix(options) {
5983
+ const path = options.path ?? null;
5984
+ if (path === null || path.length === 0) {
5985
+ return null;
5986
+ }
5987
+ const offset = resolveDiagnosticOffset(options.location, options.offset);
5988
+ const source = options.source ?? null;
5989
+ // Prefer path:line:column when both source text and an offset are known.
5990
+ if (source !== null && offset !== null) {
5991
+ const position = offsetToPosition(source, offset);
5992
+ return `${path}:${String(position.line)}:${String(position.column)}`;
5993
+ }
5994
+ return path;
5995
+ }
5996
+
5923
5997
  /**
5924
5998
  * LR parse table with ACTION and GOTO entries plus any detected conflicts.
5925
5999
  */
@@ -5984,13 +6058,19 @@ class LrParseTable {
5984
6058
  function formatParseConflictWarning(conflict) {
5985
6059
  const tokenLabel = formatConflictTokenLabel(conflict.symbol);
5986
6060
  if (conflict.kind === 'shift-reduce') {
5987
- return `state ${String(conflict.state)}: shift/reduce conflict on ${tokenLabel} resolved as shift`;
6061
+ return formatDiagnostic({
6062
+ severity: 'warning',
6063
+ message: `state ${String(conflict.state)}: shift/reduce conflict on ${tokenLabel} resolved as shift`,
6064
+ });
5988
6065
  }
5989
6066
  const keptProduction = conflict.existing.kind === 'reduce'
5990
6067
  ? conflict.existing.productionId
5991
6068
  : conflict.incoming.productionId;
5992
- return `state ${String(conflict.state)}: reduce/reduce conflict on ${tokenLabel} `
5993
- + `resolved using rule ${String(keptProduction)}`;
6069
+ return formatDiagnostic({
6070
+ severity: 'warning',
6071
+ message: `state ${String(conflict.state)}: reduce/reduce conflict on ${tokenLabel} `
6072
+ + `resolved using rule ${String(keptProduction)}`,
6073
+ });
5994
6074
  }
5995
6075
  /**
5996
6076
  * Returns a readable terminal label for conflict warning text.
@@ -7535,6 +7615,42 @@ class ParserLr {
7535
7615
  }
7536
7616
  return parseWithTable(this.table, tokens);
7537
7617
  }
7618
+ /**
7619
+ * Parses a token stream and retains syntax-error offset details.
7620
+ *
7621
+ * @param tokens - Token stream ending with `$eof`.
7622
+ * @returns Tree and optional syntax-error details.
7623
+ */
7624
+ parseResult(tokens) {
7625
+ if (this.table === null) {
7626
+ return {
7627
+ tree: null,
7628
+ errorOffset: 0,
7629
+ errorMessage: 'Parse table has no parser entries',
7630
+ };
7631
+ }
7632
+ const result = parseWithTableResult(this.table, tokens);
7633
+ if (result.cst === null) {
7634
+ return {
7635
+ tree: null,
7636
+ errorOffset: result.errorOffset,
7637
+ errorMessage: result.errorMessage,
7638
+ };
7639
+ }
7640
+ if (this.grammar.transformSchema === null) {
7641
+ return {
7642
+ tree: result.cst,
7643
+ errorOffset: null,
7644
+ errorMessage: null,
7645
+ };
7646
+ }
7647
+ const tree = transformCst(result.cst, this.grammar.transformSchema, this.table);
7648
+ return {
7649
+ tree,
7650
+ errorOffset: tree === null ? result.errorOffset : null,
7651
+ errorMessage: tree === null ? 'AST transform failed' : null,
7652
+ };
7653
+ }
7538
7654
  /**
7539
7655
  * Parses a token stream into an AST when transform rules are declared.
7540
7656
  *
@@ -7542,14 +7658,7 @@ class ParserLr {
7542
7658
  * @returns Transformed AST, CST when no transform section exists, or null on failure.
7543
7659
  */
7544
7660
  parseAst(tokens) {
7545
- const cst = this.parseCst(tokens);
7546
- if (cst === null) {
7547
- return null;
7548
- }
7549
- if (this.grammar.transformSchema === null || this.table === null) {
7550
- return cst;
7551
- }
7552
- return transformCst(cst, this.grammar.transformSchema, this.table);
7661
+ return this.parseResult(tokens).tree;
7553
7662
  }
7554
7663
  /**
7555
7664
  * Parses a token stream, applying CST-to-AST transforms when configured.
@@ -7680,6 +7789,15 @@ class ParseContext {
7680
7789
  parse(tokens) {
7681
7790
  return this.parser.parse(tokens);
7682
7791
  }
7792
+ /**
7793
+ * Parses a token stream and retains syntax-error offset details.
7794
+ *
7795
+ * @param tokens - Token stream ending with `$eof`.
7796
+ * @returns Tree and optional syntax-error details.
7797
+ */
7798
+ parseResult(tokens) {
7799
+ return this.parser.parseResult(tokens);
7800
+ }
7683
7801
  /**
7684
7802
  * Lexes and parses source text into an AST, applying transforms when present.
7685
7803
  *
@@ -8031,7 +8149,7 @@ function parseOptionalTransformSection(node) {
8031
8149
  function parseProduction(node) {
8032
8150
  const name = identifierText(requireChild(node, 'identifier'));
8033
8151
  const expression = parseExpressionNode(findExpressionChild(node));
8034
- return { name, expression };
8152
+ return { name, expression, location: node.location };
8035
8153
  }
8036
8154
  /**
8037
8155
  * Parses one AST type (`identifier = expression ;`).
@@ -8041,7 +8159,7 @@ function parseProduction(node) {
8041
8159
  function parseAstType(node) {
8042
8160
  const name = identifierText(requireChild(node, 'identifier'));
8043
8161
  const expression = parseExpressionNode(findExpressionChild(node));
8044
- return { name, expression };
8162
+ return { name, expression, location: node.location };
8045
8163
  }
8046
8164
  /**
8047
8165
  * Parses one transform rule.
@@ -8053,6 +8171,7 @@ function parseTransformRule(node) {
8053
8171
  return {
8054
8172
  production,
8055
8173
  alternatives: collectRepeated(node, 'labeled_transform').map(parseLabeledTransform),
8174
+ location: node.location,
8056
8175
  };
8057
8176
  }
8058
8177
  /**
@@ -8063,7 +8182,7 @@ function parseTransformRule(node) {
8063
8182
  function parseLabeledTransform(node) {
8064
8183
  const label = identifierText(requireChild(node, 'identifier'));
8065
8184
  const expression = parseTransformExpr(requireChild(node, 'transform_expr'));
8066
- return { label, expression };
8185
+ return { label, expression, location: node.location };
8067
8186
  }
8068
8187
  /**
8069
8188
  * Parses a transform expression.
@@ -13539,8 +13658,8 @@ function validateGrammarTable(grammar) {
13539
13658
  for (const rule of grammar.transformSchema.rules) {
13540
13659
  validateTransformProductionExists(rule, bnfProductionNames, issues);
13541
13660
  for (const alternative of rule.alternatives) {
13542
- validateTransformExpression(grammar, alternative.expression, issues);
13543
- collectPassCollapseWarnings(grammar, rule, alternative.expression, bnf.productions, issues);
13661
+ validateTransformExpression(grammar, alternative, issues);
13662
+ collectPassCollapseWarnings(grammar, rule, alternative, bnf.productions, issues);
13544
13663
  }
13545
13664
  }
13546
13665
  }
@@ -13559,37 +13678,47 @@ function validateGrammarTable(grammar) {
13559
13678
  */
13560
13679
  function collectDuplicateDefinitionWarnings(grammar, issues) {
13561
13680
  // Flag repeated production names in the grammar section.
13562
- reportDuplicateNames(grammar.productions.map((production) => production.name), 'grammar', 'production', issues);
13681
+ reportDuplicateDefinitions(grammar.productions.map((production) => ({
13682
+ name: production.name,
13683
+ location: production.location ?? null,
13684
+ })), 'grammar', 'production', issues);
13563
13685
  // Flag repeated ast type names.
13564
13686
  if (grammar.astSchema !== null) {
13565
- reportDuplicateNames(grammar.astSchema.types.map((type) => type.name), 'ast', 'type', issues);
13687
+ reportDuplicateDefinitions(grammar.astSchema.types.map((type) => ({
13688
+ name: type.name,
13689
+ location: type.location ?? null,
13690
+ })), 'ast', 'type', issues);
13566
13691
  }
13567
13692
  // Flag repeated transform rules for the same production.
13568
13693
  if (grammar.transformSchema !== null) {
13569
- reportDuplicateNames(grammar.transformSchema.rules.map((rule) => rule.production), 'transform', 'rule', issues);
13694
+ reportDuplicateDefinitions(grammar.transformSchema.rules.map((rule) => ({
13695
+ name: rule.production,
13696
+ location: rule.location ?? null,
13697
+ })), 'transform', 'rule', issues);
13570
13698
  }
13571
13699
  }
13572
13700
  /**
13573
13701
  * Records a warning for each name that appears more than once.
13574
13702
  *
13575
- * @param names - Declared names in source order.
13703
+ * @param definitions - Declared names and locations in source order.
13576
13704
  * @param section - Grammar section name for the message.
13577
13705
  * @param kind - Declaration kind for the message.
13578
13706
  * @param issues - Issue list to append to.
13579
13707
  */
13580
- function reportDuplicateNames(names, section, kind, issues) {
13708
+ function reportDuplicateDefinitions(definitions, section, kind, issues) {
13581
13709
  const seen = new Set();
13582
13710
  const reported = new Set();
13583
- for (const name of names) {
13584
- if (seen.has(name) && !reported.has(name)) {
13711
+ for (const definition of definitions) {
13712
+ if (seen.has(definition.name) && !reported.has(definition.name)) {
13585
13713
  issues.push({
13586
13714
  severity: 'warning',
13587
- message: `duplicate ${section} ${kind} ${JSON.stringify(name)}; `
13715
+ message: `duplicate ${section} ${kind} ${JSON.stringify(definition.name)}; `
13588
13716
  + `the later definition overrides the earlier one`,
13717
+ location: definition.location,
13589
13718
  });
13590
- reported.add(name);
13719
+ reported.add(definition.name);
13591
13720
  }
13592
- seen.add(name);
13721
+ seen.add(definition.name);
13593
13722
  }
13594
13723
  }
13595
13724
  /**
@@ -13612,6 +13741,7 @@ function validateTransformProductionExists(rule, bnfProductionNames, issues) {
13612
13741
  issues.push({
13613
13742
  severity: 'error',
13614
13743
  message: `transform rule for unknown production ${JSON.stringify(rule.production)}`,
13744
+ location: rule.location ?? null,
13615
13745
  });
13616
13746
  }
13617
13747
  }
@@ -13629,18 +13759,20 @@ function syntheticRepeatPrefix(name) {
13629
13759
  * Validates one transform expression against the grammar AST schema.
13630
13760
  *
13631
13761
  * @param grammar - Parsed grammar model.
13632
- * @param expression - Transform expression to validate.
13762
+ * @param alternative - Transform alternative owning the expression.
13633
13763
  * @param issues - Issue list to append to.
13634
13764
  */
13635
- function validateTransformExpression(grammar, expression, issues) {
13765
+ function validateTransformExpression(grammar, alternative, issues) {
13766
+ const expression = alternative.expression;
13767
+ const location = alternative.location ?? null;
13636
13768
  switch (expression.kind) {
13637
13769
  case 'build':
13638
- validateAstTarget(grammar, expression.typeName, expression.variant, issues);
13770
+ validateAstTarget(grammar, expression.typeName, expression.variant, location, issues);
13639
13771
  break;
13640
13772
  case 'foldLeft':
13641
13773
  case 'foldRight':
13642
13774
  case 'flatten':
13643
- validateAstTarget(grammar, expression.typeName, expression.variant, issues);
13775
+ validateAstTarget(grammar, expression.typeName, expression.variant, location, issues);
13644
13776
  break;
13645
13777
  }
13646
13778
  }
@@ -13650,13 +13782,15 @@ function validateTransformExpression(grammar, expression, issues) {
13650
13782
  * @param grammar - Parsed grammar model.
13651
13783
  * @param typeName - AST type name from a transform expression.
13652
13784
  * @param variant - AST variant label from a transform expression.
13785
+ * @param location - Source span of the transform alternative.
13653
13786
  * @param issues - Issue list to append to.
13654
13787
  */
13655
- function validateAstTarget(grammar, typeName, variant, issues) {
13788
+ function validateAstTarget(grammar, typeName, variant, location, issues) {
13656
13789
  if (grammar.astSchema === null) {
13657
13790
  issues.push({
13658
13791
  severity: 'error',
13659
13792
  message: `transform references ${typeName}.${variant} but the grammar has no ast section`,
13793
+ location,
13660
13794
  });
13661
13795
  return;
13662
13796
  }
@@ -13665,6 +13799,7 @@ function validateAstTarget(grammar, typeName, variant, issues) {
13665
13799
  issues.push({
13666
13800
  severity: 'error',
13667
13801
  message: `transform references undefined ast type ${JSON.stringify(typeName)}`,
13802
+ location,
13668
13803
  });
13669
13804
  return;
13670
13805
  }
@@ -13673,6 +13808,7 @@ function validateAstTarget(grammar, typeName, variant, issues) {
13673
13808
  issues.push({
13674
13809
  severity: 'error',
13675
13810
  message: `transform references ${typeName}.${variant} which is not declared in ast`,
13811
+ location,
13676
13812
  });
13677
13813
  }
13678
13814
  }
@@ -13703,11 +13839,12 @@ function collectAstVariants(expression) {
13703
13839
  *
13704
13840
  * @param grammar - Parsed grammar model.
13705
13841
  * @param rule - Parent production transform rule.
13706
- * @param expression - Transform expression for one alternative.
13842
+ * @param alternative - Transform alternative containing the pass expression.
13707
13843
  * @param bnfProductions - Desugared BNF productions.
13708
13844
  * @param issues - Issue list to append to.
13709
13845
  */
13710
- function collectPassCollapseWarnings(grammar, rule, expression, bnfProductions, issues) {
13846
+ function collectPassCollapseWarnings(grammar, rule, alternative, bnfProductions, issues) {
13847
+ const expression = alternative.expression;
13711
13848
  if (expression.kind !== 'pass') {
13712
13849
  return;
13713
13850
  }
@@ -13730,6 +13867,7 @@ function collectPassCollapseWarnings(grammar, rule, expression, bnfProductions,
13730
13867
  message: `pass(${expression.reference}) on ${rule.production} binds ${boundSymbol} `
13731
13868
  + `which has no transform rule and can match a single terminal; `
13732
13869
  + `add a transform for ${boundSymbol} or use build`,
13870
+ location: alternative.location ?? null,
13733
13871
  });
13734
13872
  }
13735
13873
  /**
@@ -13786,14 +13924,19 @@ function isTerminalSymbol(symbol) {
13786
13924
  return symbol?.kind === 'terminal' || symbol?.kind === 'token';
13787
13925
  }
13788
13926
  /**
13789
- * Formats validation issues as stderr warning or error lines.
13927
+ * Formats validation issues as stderr diagnostic lines.
13790
13928
  *
13791
13929
  * @param issues - Validation issues to format.
13930
+ * @param options - Optional grammar path and source text for line numbers.
13792
13931
  */
13793
- function formatTableValidationIssues(issues) {
13794
- return issues.map((issue) => issue.severity === 'error'
13795
- ? `error: ${issue.message}`
13796
- : `warning: ${issue.message}`);
13932
+ function formatTableValidationIssues(issues, options = {}) {
13933
+ return issues.map((issue) => formatDiagnostic({
13934
+ severity: issue.severity,
13935
+ message: issue.message,
13936
+ path: options.path,
13937
+ source: options.source,
13938
+ location: issue.location,
13939
+ }));
13797
13940
  }
13798
13941
 
13799
13942
  /**
@@ -13829,6 +13972,58 @@ function parseContextFromSources(options) {
13829
13972
  throw new ParseContextError('required: grammar source or table JSON');
13830
13973
  }
13831
13974
 
13975
+ /**
13976
+ * Rewrites a thrown source error into a positioned CLI diagnostic error.
13977
+ *
13978
+ * @param error - Thrown value from grammar or input reading.
13979
+ * @param path - Source file path.
13980
+ * @param source - Full source text used for line/column mapping.
13981
+ * @returns Positioned ParserLrError when the failure has an offset; otherwise the original value.
13982
+ */
13983
+ function locateSourceError(error, path, source) {
13984
+ const offset = sourceErrorOffset(error);
13985
+ if (offset === null) {
13986
+ return error;
13987
+ }
13988
+ return new ParserLrError(formatDiagnostic({
13989
+ severity: 'error',
13990
+ message: stripEmbeddedOffset(errorMessage(error)),
13991
+ path,
13992
+ source,
13993
+ offset,
13994
+ }));
13995
+ }
13996
+ /**
13997
+ * Returns a source offset from a known offset-bearing error.
13998
+ *
13999
+ * @param error - Thrown value to inspect.
14000
+ */
14001
+ function sourceErrorOffset(error) {
14002
+ if (error instanceof ReadGrammarError || error instanceof LexerError) {
14003
+ return error.offset;
14004
+ }
14005
+ return null;
14006
+ }
14007
+ /**
14008
+ * Returns a human-readable message from a thrown value.
14009
+ *
14010
+ * @param error - Thrown value.
14011
+ */
14012
+ function errorMessage(error) {
14013
+ if (error instanceof Error) {
14014
+ return error.message;
14015
+ }
14016
+ return String(error);
14017
+ }
14018
+ /**
14019
+ * Removes an embedded `at offset N` suffix so the diagnostic formatter owns the position.
14020
+ *
14021
+ * @param message - Original error message.
14022
+ */
14023
+ function stripEmbeddedOffset(message) {
14024
+ return message.replace(/\s+at offset \d+\s*$/, '');
14025
+ }
14026
+
13832
14027
  /**
13833
14028
  * Reads a UTF-8 text file from disk.
13834
14029
  *
@@ -13892,11 +14087,21 @@ function registerParseCommand(program) {
13892
14087
  }
13893
14088
  const context = await loadContextFromPaths(options);
13894
14089
  logProgress(`lexing ${options.input}`);
13895
- const tokens = await context.lexChunkStreamAsync(readTextChunks(options.input));
14090
+ const tokens = await lexInputFile(context, options.input);
13896
14091
  logProgress('parsing');
13897
- const tree = context.parse(tokens);
14092
+ const result = context.parseResult(tokens);
14093
+ if (result.tree === null) {
14094
+ const source = await readTextFile(options.input);
14095
+ throw new ParserLrError(formatDiagnostic({
14096
+ severity: 'error',
14097
+ message: result.errorMessage ?? 'Parse failed',
14098
+ path: options.input,
14099
+ source,
14100
+ offset: result.errorOffset,
14101
+ }));
14102
+ }
13898
14103
  logProgress(`formatting output (${options.format})`);
13899
- const output = formatParseOutput(tree, options.format);
14104
+ const output = formatParseOutput(result.tree, options.format);
13900
14105
  // Write output to disk or stdout.
13901
14106
  if (options.output !== undefined) {
13902
14107
  logProgress(`writing ${options.output}`);
@@ -13908,6 +14113,21 @@ function registerParseCommand(program) {
13908
14113
  }
13909
14114
  });
13910
14115
  }
14116
+ /**
14117
+ * Lexes an input file and rewrites offset-bearing lexer failures with line numbers.
14118
+ *
14119
+ * @param context - Loaded parse context.
14120
+ * @param path - Input file path.
14121
+ */
14122
+ async function lexInputFile(context, path) {
14123
+ try {
14124
+ return await context.lexChunkStreamAsync(readTextChunks(path));
14125
+ }
14126
+ catch (error) {
14127
+ const source = await readTextFile(path);
14128
+ throw locateSourceError(error, path, source);
14129
+ }
14130
+ }
13911
14131
  /**
13912
14132
  * Reads grammar or table files and builds a parse context.
13913
14133
  *
@@ -13918,8 +14138,13 @@ async function loadContextFromPaths(options) {
13918
14138
  if (options.grammar !== undefined) {
13919
14139
  logProgress(`reading grammar ${options.grammar}`);
13920
14140
  const grammarSource = await readTextFile(options.grammar);
13921
- logProgress('building parse table');
13922
- return parseContextFromGrammar(grammarSource);
14141
+ try {
14142
+ logProgress('building parse table');
14143
+ return parseContextFromGrammar(grammarSource);
14144
+ }
14145
+ catch (error) {
14146
+ throw locateSourceError(error, options.grammar, grammarSource);
14147
+ }
13923
14148
  }
13924
14149
  logProgress(`reading table ${options.table}`);
13925
14150
  const tableJson = await readTextFile(options.table);
@@ -13945,9 +14170,9 @@ function registerTableCommands(program) {
13945
14170
  .action(async (options) => {
13946
14171
  logProgress(`reading grammar ${options.grammar}`);
13947
14172
  const grammarSource = await readTextFile(options.grammar);
14173
+ const grammar = readGrammarAtPath(options.grammar, grammarSource);
13948
14174
  logProgress(`building ${options.algorithm} parse table`);
13949
- const grammar = readGrammar(grammarSource);
13950
- writeGrammarValidationMessages(grammar, false);
14175
+ writeGrammarValidationMessages(grammar, options.grammar, grammarSource, false);
13951
14176
  const context = parseContextFromSources({
13952
14177
  grammarSource,
13953
14178
  algorithm: options.algorithm,
@@ -13976,27 +14201,45 @@ function registerTableCommands(program) {
13976
14201
  .action(async (options) => {
13977
14202
  logProgress(`reading grammar ${options.grammar}`);
13978
14203
  const grammarSource = await readTextFile(options.grammar);
13979
- const grammar = readGrammar(grammarSource);
13980
- const exitCode = writeGrammarValidationMessages(grammar, options.strict === true);
14204
+ const grammar = readGrammarAtPath(options.grammar, grammarSource);
14205
+ const exitCode = writeGrammarValidationMessages(grammar, options.grammar, grammarSource, options.strict === true);
13981
14206
  process.exitCode = exitCode;
13982
14207
  });
13983
14208
  }
14209
+ /**
14210
+ * Parses a grammar file and rewrites offset-bearing failures with line numbers.
14211
+ *
14212
+ * @param path - Grammar file path.
14213
+ * @param source - Grammar file text.
14214
+ */
14215
+ function readGrammarAtPath(path, source) {
14216
+ try {
14217
+ return readGrammar(source);
14218
+ }
14219
+ catch (error) {
14220
+ throw locateSourceError(error, path, source);
14221
+ }
14222
+ }
13984
14223
  /**
13985
14224
  * Writes grammar validation messages to stderr and returns a process exit code.
13986
14225
  *
13987
14226
  * @param grammar - Parsed grammar model.
14227
+ * @param path - Grammar file path for diagnostics.
14228
+ * @param source - Grammar file text for line/column mapping.
13988
14229
  * @param strict - Whether warnings should fail validation.
13989
14230
  */
13990
- function writeGrammarValidationMessages(grammar, strict) {
14231
+ function writeGrammarValidationMessages(grammar, path, source, strict) {
13991
14232
  const issues = validateGrammarTable(grammar);
13992
14233
  let hasError = false;
13993
14234
  let hasWarning = false;
13994
- for (const line of formatTableValidationIssues(issues)) {
14235
+ for (const line of formatTableValidationIssues(issues, { path, source })) {
13995
14236
  process.stderr.write(`${line}\n`);
13996
- if (line.startsWith('error:')) {
14237
+ }
14238
+ for (const issue of issues) {
14239
+ if (issue.severity === 'error') {
13997
14240
  hasError = true;
13998
14241
  }
13999
- if (line.startsWith('warning:')) {
14242
+ if (issue.severity === 'warning') {
14000
14243
  hasWarning = true;
14001
14244
  }
14002
14245
  }