parser-lr 0.8.2 → 0.8.3

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/bin/parser-lr.js CHANGED
@@ -3781,7 +3781,7 @@ const {
3781
3781
  Help,
3782
3782
  } = commander;
3783
3783
 
3784
- var version$1 = "0.8.2";
3784
+ var version$1 = "0.8.3";
3785
3785
  var packageDefinition = {
3786
3786
  version: version$1};
3787
3787
 
@@ -7063,6 +7063,46 @@ function referenceSlotIndex(production, reference) {
7063
7063
  return null;
7064
7064
  }
7065
7065
 
7066
+ /**
7067
+ * Returns whether a CST node was reduced by an empty right-hand side.
7068
+ *
7069
+ * @param node - CST node to test.
7070
+ * @param table - Parse table supplying production metadata.
7071
+ */
7072
+ function isEpsilonNode(node, table) {
7073
+ if (node.isTerminal || node.children.length > 0 || node.productionId === null) {
7074
+ return false;
7075
+ }
7076
+ const production = table.production(node.productionId);
7077
+ return production !== null && production.rhs.length === 0;
7078
+ }
7079
+ /**
7080
+ * Returns whether a CST node is an authored labeled empty alternative.
7081
+ *
7082
+ * @remarks
7083
+ * Labeled epsilons such as `#absent` are first-class grammar alternatives.
7084
+ * They keep identity through default transforms.
7085
+ *
7086
+ * @param node - CST node to test.
7087
+ * @param table - Parse table supplying production metadata.
7088
+ */
7089
+ function isAuthoredEpsilonNode(node, table) {
7090
+ return isEpsilonNode(node, table) && node.variant !== null;
7091
+ }
7092
+ /**
7093
+ * Returns whether a CST node is synthetic unlabeled epsilon scaffolding.
7094
+ *
7095
+ * @remarks
7096
+ * Unlabeled empty productions come from `{…}` / `[…]` desugaring. They are
7097
+ * not authored alternatives and should disappear from AST construction.
7098
+ *
7099
+ * @param node - CST node to test.
7100
+ * @param table - Parse table supplying production metadata.
7101
+ */
7102
+ function isSyntheticEpsilonNode(node, table) {
7103
+ return isEpsilonNode(node, table) && node.variant === null;
7104
+ }
7105
+
7066
7106
  /**
7067
7107
  * Applies CST-to-AST transform rules to a concrete syntax tree.
7068
7108
  */
@@ -7188,7 +7228,8 @@ class CstTransformer {
7188
7228
  if (childNode === null) {
7189
7229
  return null;
7190
7230
  }
7191
- if (this.isEpsilonNode(childNode)) {
7231
+ // Drop synthetic unlabeled epsilons; keep authored `#label` empty alts.
7232
+ if (isSyntheticEpsilonNode(childNode, this.table)) {
7192
7233
  continue;
7193
7234
  }
7194
7235
  const child = this.transformNode(childNode);
@@ -7220,7 +7261,7 @@ class CstTransformer {
7220
7261
  }
7221
7262
  let acc = this.buildAstNode(expression.typeName, expression.variant, [left, operator, right], node.location);
7222
7263
  let tail = this.findRepeatTail(node, slots);
7223
- while (tail !== null && !this.isEpsilonNode(tail)) {
7264
+ while (tail !== null && !isSyntheticEpsilonNode(tail, this.table)) {
7224
7265
  const tailSlots = this.resolveReferences(tail, expression.references);
7225
7266
  if (tailSlots === null) {
7226
7267
  return null;
@@ -7244,7 +7285,7 @@ class CstTransformer {
7244
7285
  applyFoldRight(node, expression) {
7245
7286
  const parts = [];
7246
7287
  let current = node;
7247
- while (current !== null && !this.isEpsilonNode(current)) {
7288
+ while (current !== null && !isSyntheticEpsilonNode(current, this.table)) {
7248
7289
  const leftRef = expression.references[0];
7249
7290
  const operatorRef = expression.references[1];
7250
7291
  const rightRef = expression.references[2];
@@ -7286,14 +7327,14 @@ class CstTransformer {
7286
7327
  applyFlatten(node, expression) {
7287
7328
  const items = [];
7288
7329
  let current = node;
7289
- while (current !== null && !this.isEpsilonNode(current)) {
7330
+ while (current !== null && !isSyntheticEpsilonNode(current, this.table)) {
7290
7331
  const head = this.transformFlattenHead(current, expression.head);
7291
7332
  if (head === null) {
7292
7333
  return null;
7293
7334
  }
7294
7335
  items.push(head);
7295
7336
  const tail = this.resolveReferenceNode(current, expression.tail);
7296
- if (tail === null || this.isEpsilonNode(tail)) {
7337
+ if (tail === null || isSyntheticEpsilonNode(tail, this.table)) {
7297
7338
  break;
7298
7339
  }
7299
7340
  current = tail;
@@ -7333,10 +7374,19 @@ class CstTransformer {
7333
7374
  /**
7334
7375
  * Transforms a CST node without explicit transform rules.
7335
7376
  *
7377
+ * @remarks
7378
+ * Authored labeled epsilon alternatives (`#absent`) keep their identity.
7379
+ * Synthetic unlabeled epsilons from `{…}` / `[…]` desugaring are dropped.
7380
+ *
7336
7381
  * @param node - CST node to pass through or unwrap.
7337
7382
  */
7338
7383
  defaultTransform(node) {
7339
- if (this.isEpsilonNode(node)) {
7384
+ // Authored `#label` empty alternatives are first-class nodes.
7385
+ if (isAuthoredEpsilonNode(node, this.table)) {
7386
+ return AstNode.rule(node.symbol, [], node.location, node.variant);
7387
+ }
7388
+ // Synthetic unlabeled epsilons are scaffolding, not AST content.
7389
+ if (isSyntheticEpsilonNode(node, this.table)) {
7340
7390
  return null;
7341
7391
  }
7342
7392
  const children = this.transformChildren(node.children);
@@ -7458,21 +7508,6 @@ class CstTransformer {
7458
7508
  }
7459
7509
  return null;
7460
7510
  }
7461
- /**
7462
- * Returns whether a CST node represents an epsilon production.
7463
- *
7464
- * @param node - CST node to test.
7465
- */
7466
- isEpsilonNode(node) {
7467
- if (node.isTerminal) {
7468
- return false;
7469
- }
7470
- if (node.children.length > 0) {
7471
- return false;
7472
- }
7473
- const production = this.productionFor(node);
7474
- return production !== null && production.rhs.length === 0;
7475
- }
7476
7511
  /**
7477
7512
  * Returns parse table production metadata for a CST node.
7478
7513
  *
@@ -13826,7 +13861,7 @@ function validateGrammarTable(grammar) {
13826
13861
  for (const rule of grammar.transformSchema.rules) {
13827
13862
  validateTransformProductionExists(rule, bnfProductionNames, issues);
13828
13863
  for (const alternative of rule.alternatives) {
13829
- validateTransformExpression(grammar, alternative, issues);
13864
+ validateTransformExpression(grammar, rule, alternative, issues);
13830
13865
  collectPassCollapseWarnings(grammar, rule, alternative, bnf.productions, issues);
13831
13866
  }
13832
13867
  }
@@ -13930,12 +13965,13 @@ function syntheticRepeatPrefix(name) {
13930
13965
  * @param alternative - Transform alternative owning the expression.
13931
13966
  * @param issues - Issue list to append to.
13932
13967
  */
13933
- function validateTransformExpression(grammar, alternative, issues) {
13968
+ function validateTransformExpression(grammar, rule, alternative, issues) {
13934
13969
  const expression = alternative.expression;
13935
13970
  const location = alternative.location ?? null;
13936
13971
  switch (expression.kind) {
13937
13972
  case 'build':
13938
13973
  validateAstTarget(grammar, expression.typeName, expression.variant, location, issues);
13974
+ validateBuildArity(grammar, rule, alternative, expression, location, issues);
13939
13975
  break;
13940
13976
  case 'foldLeft':
13941
13977
  case 'foldRight':
@@ -13944,6 +13980,130 @@ function validateTransformExpression(grammar, alternative, issues) {
13944
13980
  break;
13945
13981
  }
13946
13982
  }
13983
+ /**
13984
+ * Records an error when a build argument list conflicts with a zero-factor shape.
13985
+ *
13986
+ * @remarks
13987
+ * Build argument counts are not required to match grammar or AST slot counts in
13988
+ * general: keywords may be dropped, punctuation may be kept, and child
13989
+ * productions may build a shared parent AST variant. The only coherent checks
13990
+ * are zero-factor cases and empty builds targeting a non-empty AST variant.
13991
+ *
13992
+ * @param grammar - Parsed grammar model.
13993
+ * @param rule - Transform rule owning the build expression.
13994
+ * @param alternative - Transform alternative owning the build expression.
13995
+ * @param expression - Build transform expression.
13996
+ * @param location - Source span of the transform alternative.
13997
+ * @param issues - Issue list to append to.
13998
+ */
13999
+ function validateBuildArity(grammar, rule, alternative, expression, location, issues) {
14000
+ const argumentCount = expression.arguments.length;
14001
+ const production = grammar.production(rule.production);
14002
+ // Reject build arguments on a labeled zero-factor grammar alternative.
14003
+ if (production !== null) {
14004
+ const alternativeExpression = findGrammarAlternativeExpression(production.expression, alternative.label);
14005
+ if (alternativeExpression !== null
14006
+ && isZeroFactorExpression(alternativeExpression)
14007
+ && argumentCount > 0) {
14008
+ issues.push({
14009
+ severity: 'error',
14010
+ message: `transform ${expression.typeName}.${expression.variant} has `
14011
+ + `${String(argumentCount)} argument(s) but ast variant declares 0`,
14012
+ location,
14013
+ });
14014
+ return;
14015
+ }
14016
+ }
14017
+ if (grammar.astSchema === null) {
14018
+ return;
14019
+ }
14020
+ const astType = grammar.astSchema.type(expression.typeName);
14021
+ if (astType === null) {
14022
+ return;
14023
+ }
14024
+ const variantExpression = findAstVariantExpression(astType.expression, expression.variant);
14025
+ if (variantExpression === null) {
14026
+ return;
14027
+ }
14028
+ // Reject build arguments on a zero-factor AST variant.
14029
+ if (isZeroFactorExpression(variantExpression)) {
14030
+ if (argumentCount > 0) {
14031
+ issues.push({
14032
+ severity: 'error',
14033
+ message: `transform ${expression.typeName}.${expression.variant} has `
14034
+ + `${String(argumentCount)} argument(s) but ast variant declares 0`,
14035
+ location,
14036
+ });
14037
+ }
14038
+ return;
14039
+ }
14040
+ // Reject empty builds that target a non-empty AST variant.
14041
+ if (argumentCount === 0) {
14042
+ issues.push({
14043
+ severity: 'error',
14044
+ message: `transform ${expression.typeName}.${expression.variant} has `
14045
+ + `0 argument(s) but ast variant requires 1`,
14046
+ location,
14047
+ });
14048
+ }
14049
+ }
14050
+ /**
14051
+ * Returns whether an expression is a labeled zero-factor alternative.
14052
+ *
14053
+ * @param expression - Grammar or AST alternative expression.
14054
+ */
14055
+ function isZeroFactorExpression(expression) {
14056
+ return expression.kind === 'sequence' && expression.elements.length === 0;
14057
+ }
14058
+ /**
14059
+ * Returns the expression for one labeled grammar alternative.
14060
+ *
14061
+ * @param expression - Production right-hand side expression.
14062
+ * @param label - Transform alternative label.
14063
+ */
14064
+ function findGrammarAlternativeExpression(expression, label) {
14065
+ if (expression.kind === 'choice') {
14066
+ for (const alternative of expression.alternatives) {
14067
+ if (alternative.label === label) {
14068
+ return alternative.expression;
14069
+ }
14070
+ }
14071
+ if (label === 'main' && expression.alternatives.length === 1) {
14072
+ return expression.alternatives[0]?.expression ?? null;
14073
+ }
14074
+ return null;
14075
+ }
14076
+ if (label === 'main') {
14077
+ return expression;
14078
+ }
14079
+ return null;
14080
+ }
14081
+ /**
14082
+ * Returns the expression for one labeled AST alternative.
14083
+ *
14084
+ * @param expression - AST type right-hand side.
14085
+ * @param variant - Variant label to find.
14086
+ */
14087
+ function findAstVariantExpression(expression, variant) {
14088
+ if (expression.kind === 'choice') {
14089
+ for (const alternative of expression.alternatives) {
14090
+ if (alternative.label === variant) {
14091
+ return alternative.expression;
14092
+ }
14093
+ }
14094
+ return null;
14095
+ }
14096
+ if (expression.kind === 'sequence' || expression.kind === 'group') {
14097
+ const inner = expression.kind === 'sequence'
14098
+ ? expression.elements[0]
14099
+ : expression.element;
14100
+ if (inner === undefined) {
14101
+ return null;
14102
+ }
14103
+ return findAstVariantExpression(inner, variant);
14104
+ }
14105
+ return null;
14106
+ }
13947
14107
  /**
13948
14108
  * Records an error when a transform references an undefined AST type or variant.
13949
14109
  *