parser-lr 0.7.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 (54) hide show
  1. package/bin/parser-lr.js +404 -874
  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 +8 -4
  18. package/dist/lib/grammar/grammar-from-cst.js.map +1 -1
  19. package/dist/lib/grammar/grammar.json +86 -820
  20. package/dist/lib/grammar/index.d.ts +1 -1
  21. package/dist/lib/grammar/index.d.ts.map +1 -1
  22. package/dist/lib/grammar/production.d.ts +2 -0
  23. package/dist/lib/grammar/production.d.ts.map +1 -1
  24. package/dist/lib/grammar/table-validator.d.ts +12 -2
  25. package/dist/lib/grammar/table-validator.d.ts.map +1 -1
  26. package/dist/lib/grammar/table-validator.js +68 -25
  27. package/dist/lib/grammar/table-validator.js.map +1 -1
  28. package/dist/lib/grammar/transform-rule.d.ts +3 -0
  29. package/dist/lib/grammar/transform-rule.d.ts.map +1 -1
  30. package/dist/lib/grammar-entry.d.ts +1 -1
  31. package/dist/lib/grammar-entry.d.ts.map +1 -1
  32. package/dist/lib/grammar-entry.js.map +1 -1
  33. package/dist/lib/index.d.ts +3 -0
  34. package/dist/lib/index.d.ts.map +1 -1
  35. package/dist/lib/index.js +1 -0
  36. package/dist/lib/index.js.map +1 -1
  37. package/dist/lib/parse-context.d.ts +7 -0
  38. package/dist/lib/parse-context.d.ts.map +1 -1
  39. package/dist/lib/parse-context.js +9 -0
  40. package/dist/lib/parse-context.js.map +1 -1
  41. package/dist/lib/parse-table/table/lr-parse-table.d.ts.map +1 -1
  42. package/dist/lib/parse-table/table/lr-parse-table.js +10 -3
  43. package/dist/lib/parse-table/table/lr-parse-table.js.map +1 -1
  44. package/dist/lib/parser-lr.d.ts +15 -0
  45. package/dist/lib/parser-lr.d.ts.map +1 -1
  46. package/dist/lib/parser-lr.js +38 -9
  47. package/dist/lib/parser-lr.js.map +1 -1
  48. package/docs/The Ferrite Programming Language.md +568 -0
  49. package/docs/grammar.md +55 -3
  50. package/grammars/6502.grammar +2 -2
  51. package/grammars/ferrite.grammar +107 -97
  52. package/grammars/grammar.grammar +19 -140
  53. package/grammars/lisp.grammar +3 -2
  54. 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.7.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.
@@ -8241,8 +8360,11 @@ function parseExpressionNode(node) {
8241
8360
  * @param node - `choice` CST node.
8242
8361
  */
8243
8362
  function parseChoice(node) {
8363
+ // Parse each labeled or unlabeled alternative.
8244
8364
  const alternatives = collectAlternatives(node).map(parseLabeledAlternative);
8245
- if (alternatives.length === 1) {
8365
+ // Collapse a sole unlabeled alternative; keep labeled singles as a choice
8366
+ // so AST / transform variants remain visible to validation and consumers.
8367
+ if (alternatives.length === 1 && alternatives[0].label === null) {
8246
8368
  return alternatives[0].expression;
8247
8369
  }
8248
8370
  return {
@@ -13126,134 +13248,35 @@ var gotos = [
13126
13248
  var ast = [
13127
13249
  {
13128
13250
  name: "grammar_file",
13129
- expression: {
13130
- kind: "sequence",
13131
- elements: [
13132
- {
13133
- kind: "reference",
13134
- name: "name_decl"
13135
- },
13136
- {
13137
- kind: "repeat",
13138
- element: {
13139
- kind: "reference",
13140
- name: "section"
13141
- }
13142
- },
13143
- {
13144
- kind: "reference",
13145
- name: "start_decl"
13146
- },
13147
- {
13148
- kind: "reference",
13149
- name: "grammar_section"
13150
- },
13151
- {
13152
- kind: "optional",
13153
- element: {
13154
- kind: "reference",
13155
- name: "ast_section"
13156
- }
13157
- },
13158
- {
13159
- kind: "optional",
13160
- element: {
13161
- kind: "reference",
13162
- name: "transform_section"
13163
- }
13164
- }
13165
- ]
13166
- }
13167
- },
13168
- {
13169
- name: "name_decl",
13170
- expression: {
13171
- kind: "sequence",
13172
- elements: [
13173
- {
13174
- kind: "reference",
13175
- name: "name_kw"
13176
- },
13177
- {
13178
- kind: "reference",
13179
- name: "grammar_name"
13180
- },
13181
- {
13182
- kind: "reference",
13183
- name: "semicolon"
13184
- }
13185
- ]
13186
- }
13187
- },
13188
- {
13189
- name: "grammar_name",
13190
13251
  expression: {
13191
13252
  kind: "choice",
13192
13253
  alternatives: [
13193
13254
  {
13194
- label: null,
13195
- expression: {
13196
- kind: "reference",
13197
- name: "identifier"
13198
- }
13199
- },
13200
- {
13201
- label: null,
13255
+ label: "sections",
13202
13256
  expression: {
13203
- kind: "reference",
13204
- name: "string_literal"
13257
+ kind: "repeat",
13258
+ element: {
13259
+ kind: "reference",
13260
+ name: "section"
13261
+ }
13205
13262
  }
13206
13263
  }
13207
13264
  ]
13208
13265
  }
13209
13266
  },
13210
13267
  {
13211
- name: "section",
13268
+ name: "token_section",
13212
13269
  expression: {
13213
13270
  kind: "choice",
13214
13271
  alternatives: [
13215
13272
  {
13216
- label: null,
13217
- expression: {
13218
- kind: "reference",
13219
- name: "token_section"
13220
- }
13221
- },
13222
- {
13223
- label: null,
13273
+ label: "definitions",
13224
13274
  expression: {
13225
- kind: "reference",
13226
- name: "skip_section"
13227
- }
13228
- },
13229
- {
13230
- label: null,
13231
- expression: {
13232
- kind: "reference",
13233
- name: "states_section"
13234
- }
13235
- }
13236
- ]
13237
- }
13238
- },
13239
- {
13240
- name: "token_section",
13241
- expression: {
13242
- kind: "sequence",
13243
- elements: [
13244
- {
13245
- kind: "reference",
13246
- name: "tokens_kw"
13247
- },
13248
- {
13249
- kind: "reference",
13250
- name: "token_def"
13251
- },
13252
- {
13253
- kind: "repeat",
13254
- element: {
13255
- kind: "reference",
13256
- name: "token_def"
13275
+ kind: "repeat",
13276
+ element: {
13277
+ kind: "reference",
13278
+ name: "token_def"
13279
+ }
13257
13280
  }
13258
13281
  }
13259
13282
  ]
@@ -13262,109 +13285,17 @@ var ast = [
13262
13285
  {
13263
13286
  name: "skip_section",
13264
13287
  expression: {
13265
- kind: "sequence",
13266
- elements: [
13267
- {
13268
- kind: "reference",
13269
- name: "skip_kw"
13270
- },
13271
- {
13272
- kind: "reference",
13273
- name: "token_def"
13274
- },
13275
- {
13276
- kind: "repeat",
13277
- element: {
13278
- kind: "reference",
13279
- name: "token_def"
13280
- }
13281
- }
13282
- ]
13283
- }
13284
- },
13285
- {
13286
- name: "states_section",
13287
- expression: {
13288
- kind: "sequence",
13289
- elements: [
13290
- {
13291
- kind: "reference",
13292
- name: "states_kw"
13293
- },
13294
- {
13295
- kind: "reference",
13296
- name: "state_name"
13297
- },
13288
+ kind: "choice",
13289
+ alternatives: [
13298
13290
  {
13299
- kind: "repeat",
13300
- element: {
13301
- kind: "sequence",
13302
- elements: [
13303
- {
13304
- kind: "reference",
13305
- name: "comma"
13306
- },
13307
- {
13308
- kind: "reference",
13309
- name: "state_name"
13310
- }
13311
- ]
13291
+ label: "definitions",
13292
+ expression: {
13293
+ kind: "repeat",
13294
+ element: {
13295
+ kind: "reference",
13296
+ name: "token_def"
13297
+ }
13312
13298
  }
13313
- },
13314
- {
13315
- kind: "reference",
13316
- name: "semicolon"
13317
- }
13318
- ]
13319
- }
13320
- },
13321
- {
13322
- name: "token_def",
13323
- expression: {
13324
- kind: "sequence",
13325
- elements: [
13326
- {
13327
- kind: "reference",
13328
- name: "identifier"
13329
- },
13330
- {
13331
- kind: "reference",
13332
- name: "equal"
13333
- },
13334
- {
13335
- kind: "reference",
13336
- name: "regex_literal"
13337
- },
13338
- {
13339
- kind: "reference",
13340
- name: "semicolon"
13341
- }
13342
- ]
13343
- }
13344
- },
13345
- {
13346
- name: "state_name",
13347
- expression: {
13348
- kind: "reference",
13349
- name: "identifier"
13350
- }
13351
- },
13352
- {
13353
- name: "start_decl",
13354
- expression: {
13355
- kind: "sequence",
13356
- elements: [
13357
- {
13358
- kind: "reference",
13359
- name: "start_kw"
13360
- },
13361
- {
13362
- kind: "reference",
13363
- name: "identifier"
13364
- },
13365
- {
13366
- kind: "reference",
13367
- name: "semicolon"
13368
13299
  }
13369
13300
  ]
13370
13301
  }
@@ -13372,21 +13303,16 @@ var ast = [
13372
13303
  {
13373
13304
  name: "grammar_section",
13374
13305
  expression: {
13375
- kind: "sequence",
13376
- elements: [
13377
- {
13378
- kind: "reference",
13379
- name: "grammar_kw"
13380
- },
13381
- {
13382
- kind: "reference",
13383
- name: "production"
13384
- },
13306
+ kind: "choice",
13307
+ alternatives: [
13385
13308
  {
13386
- kind: "repeat",
13387
- element: {
13388
- kind: "reference",
13389
- name: "production"
13309
+ label: "productions",
13310
+ expression: {
13311
+ kind: "repeat",
13312
+ element: {
13313
+ kind: "reference",
13314
+ name: "production"
13315
+ }
13390
13316
  }
13391
13317
  }
13392
13318
  ]
@@ -13395,21 +13321,16 @@ var ast = [
13395
13321
  {
13396
13322
  name: "ast_section",
13397
13323
  expression: {
13398
- kind: "sequence",
13399
- elements: [
13400
- {
13401
- kind: "reference",
13402
- name: "ast_kw"
13403
- },
13404
- {
13405
- kind: "reference",
13406
- name: "ast_type"
13407
- },
13324
+ kind: "choice",
13325
+ alternatives: [
13408
13326
  {
13409
- kind: "repeat",
13410
- element: {
13411
- kind: "reference",
13412
- name: "ast_type"
13327
+ label: "types",
13328
+ expression: {
13329
+ kind: "repeat",
13330
+ element: {
13331
+ kind: "reference",
13332
+ name: "ast_type"
13333
+ }
13413
13334
  }
13414
13335
  }
13415
13336
  ]
@@ -13418,621 +13339,88 @@ var ast = [
13418
13339
  {
13419
13340
  name: "transform_section",
13420
13341
  expression: {
13421
- kind: "sequence",
13422
- elements: [
13423
- {
13424
- kind: "reference",
13425
- name: "transform_kw"
13426
- },
13427
- {
13428
- kind: "reference",
13429
- name: "transform_rule"
13430
- },
13342
+ kind: "choice",
13343
+ alternatives: [
13431
13344
  {
13432
- kind: "repeat",
13433
- element: {
13434
- kind: "reference",
13435
- name: "transform_rule"
13345
+ label: "rules",
13346
+ expression: {
13347
+ kind: "repeat",
13348
+ element: {
13349
+ kind: "reference",
13350
+ name: "transform_rule"
13351
+ }
13436
13352
  }
13437
13353
  }
13438
13354
  ]
13439
13355
  }
13440
13356
  },
13441
- {
13442
- name: "production",
13443
- expression: {
13444
- kind: "sequence",
13445
- elements: [
13446
- {
13447
- kind: "reference",
13448
- name: "identifier"
13449
- },
13450
- {
13451
- kind: "reference",
13452
- name: "equal"
13453
- },
13454
- {
13455
- kind: "reference",
13456
- name: "expression"
13457
- },
13458
- {
13459
- kind: "reference",
13460
- name: "semicolon"
13461
- }
13462
- ]
13463
- }
13464
- },
13465
- {
13466
- name: "ast_type",
13467
- expression: {
13468
- kind: "sequence",
13469
- elements: [
13470
- {
13471
- kind: "reference",
13472
- name: "identifier"
13473
- },
13474
- {
13475
- kind: "reference",
13476
- name: "equal"
13477
- },
13478
- {
13479
- kind: "reference",
13480
- name: "expression"
13481
- },
13482
- {
13483
- kind: "reference",
13484
- name: "semicolon"
13485
- }
13486
- ]
13487
- }
13488
- },
13489
13357
  {
13490
13358
  name: "transform_rule",
13491
- expression: {
13492
- kind: "sequence",
13493
- elements: [
13494
- {
13495
- kind: "reference",
13496
- name: "identifier"
13497
- },
13498
- {
13499
- kind: "reference",
13500
- name: "arrow"
13501
- },
13502
- {
13503
- kind: "reference",
13504
- name: "labeled_transform"
13505
- },
13506
- {
13507
- kind: "repeat",
13508
- element: {
13509
- kind: "sequence",
13510
- elements: [
13511
- {
13512
- kind: "reference",
13513
- name: "bar"
13514
- },
13515
- {
13516
- kind: "reference",
13517
- name: "labeled_transform"
13518
- }
13519
- ]
13520
- }
13521
- },
13522
- {
13523
- kind: "reference",
13524
- name: "semicolon"
13525
- }
13526
- ]
13527
- }
13528
- },
13529
- {
13530
- name: "labeled_transform",
13531
- expression: {
13532
- kind: "sequence",
13533
- elements: [
13534
- {
13535
- kind: "reference",
13536
- name: "hash"
13537
- },
13538
- {
13539
- kind: "reference",
13540
- name: "identifier"
13541
- },
13542
- {
13543
- kind: "reference",
13544
- name: "transform_expr"
13545
- }
13546
- ]
13547
- }
13548
- },
13549
- {
13550
- name: "transform_expr",
13551
13359
  expression: {
13552
13360
  kind: "choice",
13553
13361
  alternatives: [
13554
13362
  {
13555
- label: null,
13556
- expression: {
13557
- kind: "reference",
13558
- name: "drop_kw"
13559
- }
13560
- },
13561
- {
13562
- label: null,
13563
- expression: {
13564
- kind: "reference",
13565
- name: "pass_expr"
13566
- }
13567
- },
13568
- {
13569
- label: null,
13570
- expression: {
13571
- kind: "reference",
13572
- name: "fold_left_expr"
13573
- }
13574
- },
13575
- {
13576
- label: null,
13577
- expression: {
13578
- kind: "reference",
13579
- name: "fold_right_expr"
13580
- }
13581
- },
13582
- {
13583
- label: null,
13584
- expression: {
13585
- kind: "reference",
13586
- name: "flatten_expr"
13587
- }
13588
- },
13589
- {
13590
- label: null,
13363
+ label: "alternatives",
13591
13364
  expression: {
13592
- kind: "reference",
13593
- name: "build_expr"
13594
- }
13595
- }
13596
- ]
13597
- }
13598
- },
13599
- {
13600
- name: "pass_expr",
13601
- expression: {
13602
- kind: "sequence",
13603
- elements: [
13604
- {
13605
- kind: "reference",
13606
- name: "pass_kw"
13607
- },
13608
- {
13609
- kind: "reference",
13610
- name: "lpar"
13611
- },
13612
- {
13613
- kind: "reference",
13614
- name: "reference"
13615
- },
13616
- {
13617
- kind: "reference",
13618
- name: "rpar"
13619
- }
13620
- ]
13621
- }
13622
- },
13623
- {
13624
- name: "fold_left_expr",
13625
- expression: {
13626
- kind: "sequence",
13627
- elements: [
13628
- {
13629
- kind: "reference",
13630
- name: "fold_left_kw"
13631
- },
13632
- {
13633
- kind: "reference",
13634
- name: "lpar"
13635
- },
13636
- {
13637
- kind: "reference",
13638
- name: "build_target"
13639
- },
13640
- {
13641
- kind: "reference",
13642
- name: "comma"
13643
- },
13644
- {
13645
- kind: "reference",
13646
- name: "reference_list"
13647
- },
13648
- {
13649
- kind: "reference",
13650
- name: "rpar"
13651
- }
13652
- ]
13653
- }
13654
- },
13655
- {
13656
- name: "fold_right_expr",
13657
- expression: {
13658
- kind: "sequence",
13659
- elements: [
13660
- {
13661
- kind: "reference",
13662
- name: "fold_right_kw"
13663
- },
13664
- {
13665
- kind: "reference",
13666
- name: "lpar"
13667
- },
13668
- {
13669
- kind: "reference",
13670
- name: "build_target"
13671
- },
13672
- {
13673
- kind: "reference",
13674
- name: "comma"
13675
- },
13676
- {
13677
- kind: "reference",
13678
- name: "reference_list"
13679
- },
13680
- {
13681
- kind: "reference",
13682
- name: "rpar"
13683
- }
13684
- ]
13685
- }
13686
- },
13687
- {
13688
- name: "flatten_expr",
13689
- expression: {
13690
- kind: "sequence",
13691
- elements: [
13692
- {
13693
- kind: "reference",
13694
- name: "flatten_kw"
13695
- },
13696
- {
13697
- kind: "reference",
13698
- name: "lpar"
13699
- },
13700
- {
13701
- kind: "reference",
13702
- name: "build_target"
13703
- },
13704
- {
13705
- kind: "reference",
13706
- name: "comma"
13707
- },
13708
- {
13709
- kind: "reference",
13710
- name: "reference"
13711
- },
13712
- {
13713
- kind: "reference",
13714
- name: "comma"
13715
- },
13716
- {
13717
- kind: "reference",
13718
- name: "reference"
13719
- },
13720
- {
13721
- kind: "reference",
13722
- name: "rpar"
13723
- }
13724
- ]
13725
- }
13726
- },
13727
- {
13728
- name: "build_expr",
13729
- expression: {
13730
- kind: "sequence",
13731
- elements: [
13732
- {
13733
- kind: "reference",
13734
- name: "build_target"
13735
- },
13736
- {
13737
- kind: "optional",
13738
- element: {
13739
- kind: "sequence",
13740
- elements: [
13741
- {
13742
- kind: "reference",
13743
- name: "lpar"
13744
- },
13745
- {
13746
- kind: "reference",
13747
- name: "reference_list"
13748
- },
13749
- {
13750
- kind: "reference",
13751
- name: "rpar"
13752
- }
13753
- ]
13365
+ kind: "repeat",
13366
+ element: {
13367
+ kind: "reference",
13368
+ name: "labeled_transform"
13369
+ }
13754
13370
  }
13755
13371
  }
13756
13372
  ]
13757
13373
  }
13758
13374
  },
13759
- {
13760
- name: "build_target",
13761
- expression: {
13762
- kind: "sequence",
13763
- elements: [
13764
- {
13765
- kind: "reference",
13766
- name: "identifier"
13767
- },
13768
- {
13769
- kind: "reference",
13770
- name: "dot"
13771
- },
13772
- {
13773
- kind: "reference",
13774
- name: "hash"
13775
- },
13776
- {
13777
- kind: "reference",
13778
- name: "identifier"
13779
- }
13780
- ]
13781
- }
13782
- },
13783
13375
  {
13784
13376
  name: "reference_list",
13785
13377
  expression: {
13786
- kind: "sequence",
13787
- elements: [
13788
- {
13789
- kind: "reference",
13790
- name: "reference"
13791
- },
13378
+ kind: "choice",
13379
+ alternatives: [
13792
13380
  {
13793
- kind: "repeat",
13794
- element: {
13795
- kind: "sequence",
13796
- elements: [
13797
- {
13798
- kind: "reference",
13799
- name: "comma"
13800
- },
13801
- {
13802
- kind: "reference",
13803
- name: "reference"
13804
- }
13805
- ]
13381
+ label: "references",
13382
+ expression: {
13383
+ kind: "repeat",
13384
+ element: {
13385
+ kind: "reference",
13386
+ name: "reference"
13387
+ }
13806
13388
  }
13807
13389
  }
13808
13390
  ]
13809
13391
  }
13810
13392
  },
13811
- {
13812
- name: "reference",
13813
- expression: {
13814
- kind: "reference",
13815
- name: "identifier"
13816
- }
13817
- },
13818
- {
13819
- name: "expression",
13820
- expression: {
13821
- kind: "reference",
13822
- name: "choice"
13823
- }
13824
- },
13825
13393
  {
13826
13394
  name: "choice",
13827
- expression: {
13828
- kind: "sequence",
13829
- elements: [
13830
- {
13831
- kind: "reference",
13832
- name: "alternative"
13833
- },
13834
- {
13835
- kind: "repeat",
13836
- element: {
13837
- kind: "sequence",
13838
- elements: [
13839
- {
13840
- kind: "reference",
13841
- name: "bar"
13842
- },
13843
- {
13844
- kind: "reference",
13845
- name: "alternative"
13846
- }
13847
- ]
13848
- }
13849
- }
13850
- ]
13851
- }
13852
- },
13853
- {
13854
- name: "alternative",
13855
- expression: {
13856
- kind: "reference",
13857
- name: "labeled_alternative"
13858
- }
13859
- },
13860
- {
13861
- name: "labeled_alternative",
13862
- expression: {
13863
- kind: "sequence",
13864
- elements: [
13865
- {
13866
- kind: "optional",
13867
- element: {
13868
- kind: "sequence",
13869
- elements: [
13870
- {
13871
- kind: "reference",
13872
- name: "hash"
13873
- },
13874
- {
13875
- kind: "reference",
13876
- name: "identifier"
13877
- }
13878
- ]
13879
- }
13880
- },
13881
- {
13882
- kind: "reference",
13883
- name: "sequence"
13884
- }
13885
- ]
13886
- }
13887
- },
13888
- {
13889
- name: "sequence",
13890
- expression: {
13891
- kind: "sequence",
13892
- elements: [
13893
- {
13894
- kind: "reference",
13895
- name: "factor"
13896
- },
13897
- {
13898
- kind: "repeat",
13899
- element: {
13900
- kind: "reference",
13901
- name: "factor"
13902
- }
13903
- }
13904
- ]
13905
- }
13906
- },
13907
- {
13908
- name: "factor",
13909
13395
  expression: {
13910
13396
  kind: "choice",
13911
13397
  alternatives: [
13912
13398
  {
13913
- label: null,
13914
- expression: {
13915
- kind: "reference",
13916
- name: "bracketed"
13917
- }
13918
- },
13919
- {
13920
- label: null,
13399
+ label: "alternatives",
13921
13400
  expression: {
13922
- kind: "reference",
13923
- name: "repeat"
13924
- }
13925
- },
13926
- {
13927
- label: null,
13928
- expression: {
13929
- kind: "reference",
13930
- name: "group"
13931
- }
13932
- },
13933
- {
13934
- label: null,
13935
- expression: {
13936
- kind: "reference",
13937
- name: "primary"
13938
- }
13939
- }
13940
- ]
13941
- }
13942
- },
13943
- {
13944
- name: "bracketed",
13945
- expression: {
13946
- kind: "sequence",
13947
- elements: [
13948
- {
13949
- kind: "reference",
13950
- name: "lbracket"
13951
- },
13952
- {
13953
- kind: "reference",
13954
- name: "expression"
13955
- },
13956
- {
13957
- kind: "reference",
13958
- name: "rbracket"
13959
- },
13960
- {
13961
- kind: "optional",
13962
- element: {
13963
- kind: "sequence",
13964
- elements: [
13965
- {
13966
- kind: "reference",
13967
- name: "colon"
13968
- },
13969
- {
13970
- kind: "reference",
13971
- name: "identifier"
13972
- }
13973
- ]
13401
+ kind: "repeat",
13402
+ element: {
13403
+ kind: "reference",
13404
+ name: "alternative"
13405
+ }
13974
13406
  }
13975
13407
  }
13976
13408
  ]
13977
13409
  }
13978
13410
  },
13979
13411
  {
13980
- name: "repeat",
13981
- expression: {
13982
- kind: "sequence",
13983
- elements: [
13984
- {
13985
- kind: "reference",
13986
- name: "lbrace"
13987
- },
13988
- {
13989
- kind: "reference",
13990
- name: "expression"
13991
- },
13992
- {
13993
- kind: "reference",
13994
- name: "rbrace"
13995
- }
13996
- ]
13997
- }
13998
- },
13999
- {
14000
- name: "group",
14001
- expression: {
14002
- kind: "sequence",
14003
- elements: [
14004
- {
14005
- kind: "reference",
14006
- name: "lpar"
14007
- },
14008
- {
14009
- kind: "reference",
14010
- name: "expression"
14011
- },
14012
- {
14013
- kind: "reference",
14014
- name: "rpar"
14015
- }
14016
- ]
14017
- }
14018
- },
14019
- {
14020
- name: "primary",
13412
+ name: "sequence",
14021
13413
  expression: {
14022
13414
  kind: "choice",
14023
13415
  alternatives: [
14024
13416
  {
14025
- label: null,
14026
- expression: {
14027
- kind: "reference",
14028
- name: "string_literal"
14029
- }
14030
- },
14031
- {
14032
- label: null,
13417
+ label: "factors",
14033
13418
  expression: {
14034
- kind: "reference",
14035
- name: "identifier"
13419
+ kind: "repeat",
13420
+ element: {
13421
+ kind: "reference",
13422
+ name: "factor"
13423
+ }
14036
13424
  }
14037
13425
  }
14038
13426
  ]
@@ -14056,7 +13444,7 @@ var transform = [
14056
13444
  ]
14057
13445
  },
14058
13446
  {
14059
- production: "token_section$repeat_1",
13447
+ production: "token_section",
14060
13448
  alternatives: [
14061
13449
  {
14062
13450
  label: "main",
@@ -14071,7 +13459,7 @@ var transform = [
14071
13459
  ]
14072
13460
  },
14073
13461
  {
14074
- production: "skip_section$repeat_2",
13462
+ production: "skip_section",
14075
13463
  alternatives: [
14076
13464
  {
14077
13465
  label: "main",
@@ -14086,7 +13474,7 @@ var transform = [
14086
13474
  ]
14087
13475
  },
14088
13476
  {
14089
- production: "grammar_section$repeat_4",
13477
+ production: "grammar_section",
14090
13478
  alternatives: [
14091
13479
  {
14092
13480
  label: "main",
@@ -14101,7 +13489,7 @@ var transform = [
14101
13489
  ]
14102
13490
  },
14103
13491
  {
14104
- production: "ast_section$repeat_5",
13492
+ production: "ast_section",
14105
13493
  alternatives: [
14106
13494
  {
14107
13495
  label: "main",
@@ -14116,7 +13504,7 @@ var transform = [
14116
13504
  ]
14117
13505
  },
14118
13506
  {
14119
- production: "transform_section$repeat_6",
13507
+ production: "transform_section",
14120
13508
  alternatives: [
14121
13509
  {
14122
13510
  label: "main",
@@ -14131,7 +13519,7 @@ var transform = [
14131
13519
  ]
14132
13520
  },
14133
13521
  {
14134
- production: "transform_rule$repeat_7",
13522
+ production: "transform_rule",
14135
13523
  alternatives: [
14136
13524
  {
14137
13525
  label: "main",
@@ -14146,7 +13534,7 @@ var transform = [
14146
13534
  ]
14147
13535
  },
14148
13536
  {
14149
- production: "reference_list$repeat_8",
13537
+ production: "reference_list",
14150
13538
  alternatives: [
14151
13539
  {
14152
13540
  label: "main",
@@ -14161,7 +13549,7 @@ var transform = [
14161
13549
  ]
14162
13550
  },
14163
13551
  {
14164
- production: "choice$repeat_9",
13552
+ production: "choice",
14165
13553
  alternatives: [
14166
13554
  {
14167
13555
  label: "main",
@@ -14176,7 +13564,7 @@ var transform = [
14176
13564
  ]
14177
13565
  },
14178
13566
  {
14179
- production: "sequence$repeat_10",
13567
+ production: "sequence",
14180
13568
  alternatives: [
14181
13569
  {
14182
13570
  label: "main",
@@ -14270,8 +13658,8 @@ function validateGrammarTable(grammar) {
14270
13658
  for (const rule of grammar.transformSchema.rules) {
14271
13659
  validateTransformProductionExists(rule, bnfProductionNames, issues);
14272
13660
  for (const alternative of rule.alternatives) {
14273
- validateTransformExpression(grammar, alternative.expression, issues);
14274
- collectPassCollapseWarnings(grammar, rule, alternative.expression, bnf.productions, issues);
13661
+ validateTransformExpression(grammar, alternative, issues);
13662
+ collectPassCollapseWarnings(grammar, rule, alternative, bnf.productions, issues);
14275
13663
  }
14276
13664
  }
14277
13665
  }
@@ -14290,37 +13678,47 @@ function validateGrammarTable(grammar) {
14290
13678
  */
14291
13679
  function collectDuplicateDefinitionWarnings(grammar, issues) {
14292
13680
  // Flag repeated production names in the grammar section.
14293
- 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);
14294
13685
  // Flag repeated ast type names.
14295
13686
  if (grammar.astSchema !== null) {
14296
- 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);
14297
13691
  }
14298
13692
  // Flag repeated transform rules for the same production.
14299
13693
  if (grammar.transformSchema !== null) {
14300
- 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);
14301
13698
  }
14302
13699
  }
14303
13700
  /**
14304
13701
  * Records a warning for each name that appears more than once.
14305
13702
  *
14306
- * @param names - Declared names in source order.
13703
+ * @param definitions - Declared names and locations in source order.
14307
13704
  * @param section - Grammar section name for the message.
14308
13705
  * @param kind - Declaration kind for the message.
14309
13706
  * @param issues - Issue list to append to.
14310
13707
  */
14311
- function reportDuplicateNames(names, section, kind, issues) {
13708
+ function reportDuplicateDefinitions(definitions, section, kind, issues) {
14312
13709
  const seen = new Set();
14313
13710
  const reported = new Set();
14314
- for (const name of names) {
14315
- if (seen.has(name) && !reported.has(name)) {
13711
+ for (const definition of definitions) {
13712
+ if (seen.has(definition.name) && !reported.has(definition.name)) {
14316
13713
  issues.push({
14317
13714
  severity: 'warning',
14318
- message: `duplicate ${section} ${kind} ${JSON.stringify(name)}; `
13715
+ message: `duplicate ${section} ${kind} ${JSON.stringify(definition.name)}; `
14319
13716
  + `the later definition overrides the earlier one`,
13717
+ location: definition.location,
14320
13718
  });
14321
- reported.add(name);
13719
+ reported.add(definition.name);
14322
13720
  }
14323
- seen.add(name);
13721
+ seen.add(definition.name);
14324
13722
  }
14325
13723
  }
14326
13724
  /**
@@ -14331,29 +13729,50 @@ function reportDuplicateNames(names, section, kind, issues) {
14331
13729
  * @param issues - Issue list to append to.
14332
13730
  */
14333
13731
  function validateTransformProductionExists(rule, bnfProductionNames, issues) {
14334
- if (!bnfProductionNames.has(rule.production)) {
13732
+ // Accept direct production names.
13733
+ if (bnfProductionNames.has(rule.production)) {
13734
+ return;
13735
+ }
13736
+ // Match authored `$repeat_0` aliases to globally numbered repeats.
13737
+ const repeatPrefix = syntheticRepeatPrefix(rule.production);
13738
+ const matchesRepeat = repeatPrefix !== null
13739
+ && [...bnfProductionNames].some((productionName) => syntheticRepeatPrefix(productionName) === repeatPrefix);
13740
+ if (!matchesRepeat) {
14335
13741
  issues.push({
14336
13742
  severity: 'error',
14337
13743
  message: `transform rule for unknown production ${JSON.stringify(rule.production)}`,
13744
+ location: rule.location ?? null,
14338
13745
  });
14339
13746
  }
14340
13747
  }
13748
+ /**
13749
+ * Returns the stable prefix of a numbered synthetic repeat production.
13750
+ *
13751
+ * @param name - Production or transform-rule name.
13752
+ * @returns Prefix ending in `$repeat`, or null for a regular production.
13753
+ */
13754
+ function syntheticRepeatPrefix(name) {
13755
+ const match = /^(.+\$repeat)_\d+$/.exec(name);
13756
+ return match?.[1] ?? null;
13757
+ }
14341
13758
  /**
14342
13759
  * Validates one transform expression against the grammar AST schema.
14343
13760
  *
14344
13761
  * @param grammar - Parsed grammar model.
14345
- * @param expression - Transform expression to validate.
13762
+ * @param alternative - Transform alternative owning the expression.
14346
13763
  * @param issues - Issue list to append to.
14347
13764
  */
14348
- function validateTransformExpression(grammar, expression, issues) {
13765
+ function validateTransformExpression(grammar, alternative, issues) {
13766
+ const expression = alternative.expression;
13767
+ const location = alternative.location ?? null;
14349
13768
  switch (expression.kind) {
14350
13769
  case 'build':
14351
- validateAstTarget(grammar, expression.typeName, expression.variant, issues);
13770
+ validateAstTarget(grammar, expression.typeName, expression.variant, location, issues);
14352
13771
  break;
14353
13772
  case 'foldLeft':
14354
13773
  case 'foldRight':
14355
13774
  case 'flatten':
14356
- validateAstTarget(grammar, expression.typeName, expression.variant, issues);
13775
+ validateAstTarget(grammar, expression.typeName, expression.variant, location, issues);
14357
13776
  break;
14358
13777
  }
14359
13778
  }
@@ -14363,13 +13782,15 @@ function validateTransformExpression(grammar, expression, issues) {
14363
13782
  * @param grammar - Parsed grammar model.
14364
13783
  * @param typeName - AST type name from a transform expression.
14365
13784
  * @param variant - AST variant label from a transform expression.
13785
+ * @param location - Source span of the transform alternative.
14366
13786
  * @param issues - Issue list to append to.
14367
13787
  */
14368
- function validateAstTarget(grammar, typeName, variant, issues) {
13788
+ function validateAstTarget(grammar, typeName, variant, location, issues) {
14369
13789
  if (grammar.astSchema === null) {
14370
13790
  issues.push({
14371
13791
  severity: 'error',
14372
13792
  message: `transform references ${typeName}.${variant} but the grammar has no ast section`,
13793
+ location,
14373
13794
  });
14374
13795
  return;
14375
13796
  }
@@ -14378,6 +13799,7 @@ function validateAstTarget(grammar, typeName, variant, issues) {
14378
13799
  issues.push({
14379
13800
  severity: 'error',
14380
13801
  message: `transform references undefined ast type ${JSON.stringify(typeName)}`,
13802
+ location,
14381
13803
  });
14382
13804
  return;
14383
13805
  }
@@ -14386,6 +13808,7 @@ function validateAstTarget(grammar, typeName, variant, issues) {
14386
13808
  issues.push({
14387
13809
  severity: 'error',
14388
13810
  message: `transform references ${typeName}.${variant} which is not declared in ast`,
13811
+ location,
14389
13812
  });
14390
13813
  }
14391
13814
  }
@@ -14416,11 +13839,12 @@ function collectAstVariants(expression) {
14416
13839
  *
14417
13840
  * @param grammar - Parsed grammar model.
14418
13841
  * @param rule - Parent production transform rule.
14419
- * @param expression - Transform expression for one alternative.
13842
+ * @param alternative - Transform alternative containing the pass expression.
14420
13843
  * @param bnfProductions - Desugared BNF productions.
14421
13844
  * @param issues - Issue list to append to.
14422
13845
  */
14423
- function collectPassCollapseWarnings(grammar, rule, expression, bnfProductions, issues) {
13846
+ function collectPassCollapseWarnings(grammar, rule, alternative, bnfProductions, issues) {
13847
+ const expression = alternative.expression;
14424
13848
  if (expression.kind !== 'pass') {
14425
13849
  return;
14426
13850
  }
@@ -14443,6 +13867,7 @@ function collectPassCollapseWarnings(grammar, rule, expression, bnfProductions,
14443
13867
  message: `pass(${expression.reference}) on ${rule.production} binds ${boundSymbol} `
14444
13868
  + `which has no transform rule and can match a single terminal; `
14445
13869
  + `add a transform for ${boundSymbol} or use build`,
13870
+ location: alternative.location ?? null,
14446
13871
  });
14447
13872
  }
14448
13873
  /**
@@ -14499,14 +13924,19 @@ function isTerminalSymbol(symbol) {
14499
13924
  return symbol?.kind === 'terminal' || symbol?.kind === 'token';
14500
13925
  }
14501
13926
  /**
14502
- * Formats validation issues as stderr warning or error lines.
13927
+ * Formats validation issues as stderr diagnostic lines.
14503
13928
  *
14504
13929
  * @param issues - Validation issues to format.
13930
+ * @param options - Optional grammar path and source text for line numbers.
14505
13931
  */
14506
- function formatTableValidationIssues(issues) {
14507
- return issues.map((issue) => issue.severity === 'error'
14508
- ? `error: ${issue.message}`
14509
- : `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
+ }));
14510
13940
  }
14511
13941
 
14512
13942
  /**
@@ -14542,6 +13972,58 @@ function parseContextFromSources(options) {
14542
13972
  throw new ParseContextError('required: grammar source or table JSON');
14543
13973
  }
14544
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
+
14545
14027
  /**
14546
14028
  * Reads a UTF-8 text file from disk.
14547
14029
  *
@@ -14605,11 +14087,21 @@ function registerParseCommand(program) {
14605
14087
  }
14606
14088
  const context = await loadContextFromPaths(options);
14607
14089
  logProgress(`lexing ${options.input}`);
14608
- const tokens = await context.lexChunkStreamAsync(readTextChunks(options.input));
14090
+ const tokens = await lexInputFile(context, options.input);
14609
14091
  logProgress('parsing');
14610
- 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
+ }
14611
14103
  logProgress(`formatting output (${options.format})`);
14612
- const output = formatParseOutput(tree, options.format);
14104
+ const output = formatParseOutput(result.tree, options.format);
14613
14105
  // Write output to disk or stdout.
14614
14106
  if (options.output !== undefined) {
14615
14107
  logProgress(`writing ${options.output}`);
@@ -14621,6 +14113,21 @@ function registerParseCommand(program) {
14621
14113
  }
14622
14114
  });
14623
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
+ }
14624
14131
  /**
14625
14132
  * Reads grammar or table files and builds a parse context.
14626
14133
  *
@@ -14631,8 +14138,13 @@ async function loadContextFromPaths(options) {
14631
14138
  if (options.grammar !== undefined) {
14632
14139
  logProgress(`reading grammar ${options.grammar}`);
14633
14140
  const grammarSource = await readTextFile(options.grammar);
14634
- logProgress('building parse table');
14635
- 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
+ }
14636
14148
  }
14637
14149
  logProgress(`reading table ${options.table}`);
14638
14150
  const tableJson = await readTextFile(options.table);
@@ -14658,9 +14170,9 @@ function registerTableCommands(program) {
14658
14170
  .action(async (options) => {
14659
14171
  logProgress(`reading grammar ${options.grammar}`);
14660
14172
  const grammarSource = await readTextFile(options.grammar);
14173
+ const grammar = readGrammarAtPath(options.grammar, grammarSource);
14661
14174
  logProgress(`building ${options.algorithm} parse table`);
14662
- const grammar = readGrammar(grammarSource);
14663
- writeGrammarValidationMessages(grammar, false);
14175
+ writeGrammarValidationMessages(grammar, options.grammar, grammarSource, false);
14664
14176
  const context = parseContextFromSources({
14665
14177
  grammarSource,
14666
14178
  algorithm: options.algorithm,
@@ -14689,27 +14201,45 @@ function registerTableCommands(program) {
14689
14201
  .action(async (options) => {
14690
14202
  logProgress(`reading grammar ${options.grammar}`);
14691
14203
  const grammarSource = await readTextFile(options.grammar);
14692
- const grammar = readGrammar(grammarSource);
14693
- 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);
14694
14206
  process.exitCode = exitCode;
14695
14207
  });
14696
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
+ }
14697
14223
  /**
14698
14224
  * Writes grammar validation messages to stderr and returns a process exit code.
14699
14225
  *
14700
14226
  * @param grammar - Parsed grammar model.
14227
+ * @param path - Grammar file path for diagnostics.
14228
+ * @param source - Grammar file text for line/column mapping.
14701
14229
  * @param strict - Whether warnings should fail validation.
14702
14230
  */
14703
- function writeGrammarValidationMessages(grammar, strict) {
14231
+ function writeGrammarValidationMessages(grammar, path, source, strict) {
14704
14232
  const issues = validateGrammarTable(grammar);
14705
14233
  let hasError = false;
14706
14234
  let hasWarning = false;
14707
- for (const line of formatTableValidationIssues(issues)) {
14235
+ for (const line of formatTableValidationIssues(issues, { path, source })) {
14708
14236
  process.stderr.write(`${line}\n`);
14709
- if (line.startsWith('error:')) {
14237
+ }
14238
+ for (const issue of issues) {
14239
+ if (issue.severity === 'error') {
14710
14240
  hasError = true;
14711
14241
  }
14712
- if (line.startsWith('warning:')) {
14242
+ if (issue.severity === 'warning') {
14713
14243
  hasWarning = true;
14714
14244
  }
14715
14245
  }