grammatik 0.1.0 → 0.1.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.
package/dist/esm/index.js CHANGED
@@ -357,7 +357,7 @@ function createLexer(defs, options = {}) {
357
357
  return id;
358
358
  };
359
359
  for (const def of defs) for (const mode of modesOf(def)) idOf(mode);
360
- const compiled = defs.map((def) => {
360
+ const compiled = defs.map((def, id) => {
361
361
  if (!def.name) throw new Error("createLexer: every token definition needs a name");
362
362
  if (!def.pattern && !def.literal) throw new Error(`createLexer: token '${def.name}' needs a pattern or a literal`);
363
363
  if (def.pattern && def.literal) throw new Error(`createLexer: token '${def.name}' has both a pattern and a literal; pick one`);
@@ -372,6 +372,11 @@ function createLexer(defs, options = {}) {
372
372
  }
373
373
  return {
374
374
  name: def.name,
375
+ // The declaration index. `createParser` derives the identical mapping
376
+ // from `tokenNames` independently (same array, same order), so a
377
+ // combinator that resolves a `consume` target's id at build time agrees
378
+ // with what a token actually carries without the two ever comparing notes.
379
+ id,
375
380
  skip: !!def.skip,
376
381
  literal: def.literal,
377
382
  longerAlt: def.longerAlt || null,
@@ -470,14 +475,7 @@ function createLexer(defs, options = {}) {
470
475
  }
471
476
  if (!winner.skip) {
472
477
  const image = winner.literal !== void 0 ? winner.literal : winner === def && matched !== null ? matched : text.slice(offset, end);
473
- tokens.push({
474
- type: winner.name,
475
- image,
476
- start: offset,
477
- end,
478
- line: trackLines ? line : 0,
479
- col: trackLines ? offset - lineStart + 1 : 0
480
- });
478
+ tokens.push(trackLines ? { type: winner.name, image, start: offset, end, line, col: offset - lineStart + 1 } : { type: winner.name, id: winner.id, image, start: offset, end });
481
479
  }
482
480
  if (trackLines) {
483
481
  for (let j = offset; j < end; j++) {
@@ -753,6 +751,47 @@ function isPlainParser(node) {
753
751
  const op = opOf(node);
754
752
  return op !== null && op !== "label" && op !== "action";
755
753
  }
754
+ function deepEqual(a, b) {
755
+ if (a === b) return true;
756
+ if (Array.isArray(a) || Array.isArray(b)) {
757
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
758
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
759
+ return true;
760
+ }
761
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
762
+ const keysA = Object.keys(a);
763
+ const keysB = Object.keys(b);
764
+ if (keysA.length !== keysB.length) return false;
765
+ for (const key of keysA) {
766
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) return false;
767
+ }
768
+ return true;
769
+ }
770
+ function recognizeAltPrefixIdiom(branches) {
771
+ if (!Array.isArray(branches) || branches.length < 2) return null;
772
+ if (!branches.every((b) => opOf(b) === "seq" && Array.isArray(b.seq))) return null;
773
+ const seqs = branches.map((b) => b.seq);
774
+ const minLen = Math.min(...seqs.map((s) => s.length));
775
+ let n = 0;
776
+ while (n < minLen) {
777
+ const candidate = seqs[0][n];
778
+ const op = opOf(candidate);
779
+ if (op === "label" || op === "action") break;
780
+ if (!seqs.every((s) => deepEqual(s[n], candidate))) break;
781
+ n++;
782
+ }
783
+ if (n === 0) return null;
784
+ if (!seqs.every((s) => s.slice(n).some((child) => opOf(child) === "action"))) return null;
785
+ const prefix = seqs[0].slice(0, n);
786
+ const remainders = seqs.map((s) => ({ seq: s.slice(n) }));
787
+ return {
788
+ seq: [
789
+ ...prefix,
790
+ { label: ["__grammatik_rest", { alt: remainders }] },
791
+ { action: { val: "__grammatik_rest" } }
792
+ ]
793
+ };
794
+ }
756
795
  var BAIL = /* @__PURE__ */ Symbol("grammatik.bail");
757
796
  function rebaseOnLabel(logic, name) {
758
797
  if (Array.isArray(logic)) {
@@ -1002,16 +1041,38 @@ function analyze(grammar, options = {}) {
1002
1041
  return { firsts, errors, leftRecursive };
1003
1042
  }
1004
1043
  function buildDispatch(branches, firsts) {
1005
- const table = /* @__PURE__ */ new Map();
1006
- for (const branch of branches) {
1007
- const first = firstOf(branch, firsts);
1044
+ const perBranch = branches.map((branch) => firstOf(branch, firsts));
1045
+ for (const first of perBranch) {
1008
1046
  if (first.nullable || first.unknown || first.tokens.size === 0) return null;
1009
- for (const token of first.tokens) {
1010
- if (table.has(token)) return null;
1011
- table.set(token, branch);
1047
+ }
1048
+ const byToken = /* @__PURE__ */ new Map();
1049
+ for (let i = 0; i < branches.length; i++) {
1050
+ for (const token of perBranch[i].tokens) {
1051
+ let list = byToken.get(token);
1052
+ if (list === void 0) {
1053
+ list = [];
1054
+ byToken.set(token, list);
1055
+ }
1056
+ list.push(branches[i]);
1012
1057
  }
1013
1058
  }
1014
- return table.size > 0 ? table : null;
1059
+ if (byToken.size === 0) return null;
1060
+ const canonical = /* @__PURE__ */ new Map();
1061
+ const table = /* @__PURE__ */ new Map();
1062
+ for (const [token, list] of byToken) {
1063
+ if (list.length === 1) {
1064
+ table.set(token, list[0]);
1065
+ continue;
1066
+ }
1067
+ const key = list.map((branch) => branches.indexOf(branch)).join(",");
1068
+ let group = canonical.get(key);
1069
+ if (group === void 0) {
1070
+ group = list;
1071
+ canonical.set(key, group);
1072
+ }
1073
+ table.set(token, group);
1074
+ }
1075
+ return table;
1015
1076
  }
1016
1077
 
1017
1078
  // src/plan.js
@@ -1046,7 +1107,7 @@ function mayReturnContext(logic) {
1046
1107
  return false;
1047
1108
  }
1048
1109
  function createPlanner(config) {
1049
- const { engine, rules, firsts, memo, maxSteps, unsafeEval = true } = config;
1110
+ const { engine, rules, firsts, memo, maxSteps, unsafeEval = true, tokenIds = null } = config;
1050
1111
  const slots = /* @__PURE__ */ new Map();
1051
1112
  const planCache = /* @__PURE__ */ new WeakMap();
1052
1113
  const opCaches = /* @__PURE__ */ new Map();
@@ -1129,9 +1190,10 @@ function createPlanner(config) {
1129
1190
  }
1130
1191
  const buildConsume = (arg) => {
1131
1192
  const name = typeof arg === "string" ? arg : arg[0];
1193
+ const id = tokenIds ? tokenIds.get(name) : void 0;
1132
1194
  return (state) => {
1133
1195
  const token = state.tokens[state.idx];
1134
- if (token !== void 0 && token.type === name) {
1196
+ if (token !== void 0 && (token.id === id || token.id === void 0 && token.type === name)) {
1135
1197
  state.idx++;
1136
1198
  return token;
1137
1199
  }
@@ -1227,10 +1289,11 @@ function createPlanner(config) {
1227
1289
  const buildOneOf = (arg) => {
1228
1290
  const names = Array.isArray(arg) ? arg : [arg];
1229
1291
  const set = new Set(names);
1292
+ const idSet = tokenIds ? new Set(names.map((n) => tokenIds.get(n))) : null;
1230
1293
  const count = names.length;
1231
1294
  return (state) => {
1232
1295
  const token = state.tokens[state.idx];
1233
- if (token !== void 0 && set.has(token.type)) {
1296
+ if (token !== void 0 && (idSet ? token.id !== void 0 ? idSet.has(token.id) : set.has(token.type) : set.has(token.type))) {
1234
1297
  state.idx++;
1235
1298
  return token;
1236
1299
  }
@@ -1506,6 +1569,7 @@ function createPlanner(config) {
1506
1569
  const kinds = [];
1507
1570
  const names = [];
1508
1571
  const tokens = [];
1572
+ const tokenIdList = [];
1509
1573
  const fns = [];
1510
1574
  const actionLogic = [];
1511
1575
  let hasAction = false;
@@ -1526,11 +1590,13 @@ function createPlanner(config) {
1526
1590
  kinds.push(LABELLED_TERMINAL);
1527
1591
  names.push(name);
1528
1592
  tokens.push(terminal2);
1593
+ tokenIdList.push(tokenIds ? tokenIds.get(terminal2) : void 0);
1529
1594
  fns.push(null);
1530
1595
  } else {
1531
1596
  kinds.push(LABELLED);
1532
1597
  names.push(name);
1533
1598
  tokens.push(null);
1599
+ tokenIdList.push(void 0);
1534
1600
  fns.push(plan(inner));
1535
1601
  }
1536
1602
  continue;
@@ -1539,6 +1605,7 @@ function createPlanner(config) {
1539
1605
  kinds.push(ACTION);
1540
1606
  names.push(null);
1541
1607
  tokens.push(null);
1608
+ tokenIdList.push(void 0);
1542
1609
  fns.push(planAction(child.action));
1543
1610
  actionLogic.push(child.action);
1544
1611
  hasAction = true;
@@ -1549,11 +1616,13 @@ function createPlanner(config) {
1549
1616
  kinds.push(TERMINAL);
1550
1617
  names.push(null);
1551
1618
  tokens.push(terminal);
1619
+ tokenIdList.push(tokenIds ? tokenIds.get(terminal) : void 0);
1552
1620
  fns.push(null);
1553
1621
  } else {
1554
1622
  kinds.push(PARSER);
1555
1623
  names.push(null);
1556
1624
  tokens.push(null);
1625
+ tokenIdList.push(void 0);
1557
1626
  fns.push(plan(child));
1558
1627
  }
1559
1628
  }
@@ -1574,7 +1643,7 @@ function createPlanner(config) {
1574
1643
  let value;
1575
1644
  if (kind === TERMINAL || kind === LABELLED_TERMINAL) {
1576
1645
  const token = state.tokens[state.idx];
1577
- if (token !== void 0 && token.type === tokens[i]) {
1646
+ if (token !== void 0 && (token.id === tokenIdList[i] || token.id === void 0 && token.type === tokens[i])) {
1578
1647
  state.idx++;
1579
1648
  value = token;
1580
1649
  } else {
@@ -1604,18 +1673,44 @@ function createPlanner(config) {
1604
1673
  }
1605
1674
  function planAlt(branches) {
1606
1675
  if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches");
1676
+ const factored = recognizeAltPrefixIdiom(branches);
1677
+ if (factored !== null) return planUncached(factored);
1607
1678
  const planned = branches.map(plan);
1608
1679
  const table = firsts ? buildDispatch(branches, firsts) : null;
1609
1680
  if (table !== null) {
1610
1681
  const byToken = /* @__PURE__ */ Object.create(null);
1611
- for (const [token, branch] of table) byToken[token] = planned[branches.indexOf(branch)];
1682
+ const byId = tokenIds ? [] : null;
1683
+ const groupClosures = /* @__PURE__ */ new Map();
1684
+ for (const [token, branch] of table) {
1685
+ let closure = groupClosures.get(branch);
1686
+ if (closure === void 0) {
1687
+ closure = Array.isArray(branch) ? (() => {
1688
+ const group = branch.map((b) => planned[branches.indexOf(b)]);
1689
+ return (state) => {
1690
+ const start = state.idx;
1691
+ for (let i = 0; i < group.length; i++) {
1692
+ const value = group[i](state);
1693
+ if (value !== FAIL) return value;
1694
+ state.idx = start;
1695
+ }
1696
+ return FAIL;
1697
+ };
1698
+ })() : planned[branches.indexOf(branch)];
1699
+ groupClosures.set(branch, closure);
1700
+ }
1701
+ byToken[token] = closure;
1702
+ if (byId) {
1703
+ const id = tokenIds.get(token);
1704
+ if (id !== void 0) byId[id] = closure;
1705
+ }
1706
+ }
1612
1707
  const expectedTokens = [...table.keys()];
1613
1708
  const expectedCount = expectedTokens.length;
1614
1709
  return (state) => {
1615
1710
  tick(state);
1616
1711
  const token = state.tokens[state.idx];
1617
1712
  if (token !== void 0) {
1618
- const branch = byToken[token.type];
1713
+ const branch = byId && token.id !== void 0 ? byId[token.id] : byToken[token.type];
1619
1714
  if (branch !== void 0) return branch(state);
1620
1715
  }
1621
1716
  for (let i = 0; i < expectedCount; i++) expect(state, expectedTokens[i]);
@@ -1754,7 +1849,12 @@ var createValueMethods = () => ({
1754
1849
 
1755
1850
  // src/codegen.js
1756
1851
  function createCodegen(config) {
1757
- const { planner, rules, firsts, memo, maxSteps } = config;
1852
+ const { planner, rules, firsts, memo, maxSteps, tokenIds = null } = config;
1853
+ const typeCheck = (tokenVar, name) => {
1854
+ const id = tokenIds ? tokenIds.get(name) : void 0;
1855
+ if (id === void 0) return `${tokenVar}.type === ${JSON.stringify(name)}`;
1856
+ return `(${tokenVar}.id === ${id} || (${tokenVar}.id === undefined && ${tokenVar}.type === ${JSON.stringify(name)}))`;
1857
+ };
1758
1858
  const HELPERS = [];
1759
1859
  const ORIGINS = [];
1760
1860
  const RULES = [];
@@ -1796,7 +1896,7 @@ function createCodegen(config) {
1796
1896
  out.push(
1797
1897
  `let ${result};`,
1798
1898
  `const ${token} = c.tokens[c.idx];`,
1799
- `if (${token} !== undefined && ${token}.type === ${JSON.stringify(name)}) { c.idx++; ${result} = ${token}; }`,
1899
+ `if (${token} !== undefined && ${typeCheck(token, name)}) { c.idx++; ${result} = ${token}; }`,
1800
1900
  `else { E(c, ${JSON.stringify(name)}); ${result} = F; }`
1801
1901
  );
1802
1902
  return result;
@@ -1892,7 +1992,7 @@ function createCodegen(config) {
1892
1992
  case "oneOf": {
1893
1993
  const names = Array.isArray(arg) ? arg : [arg];
1894
1994
  const token = nextId();
1895
- const test = names.map((n) => `${token}.type === ${JSON.stringify(n)}`).join(" || ");
1995
+ const test = names.map((n) => typeCheck(token, n)).join(" || ");
1896
1996
  out.push(
1897
1997
  `let ${result};`,
1898
1998
  `const ${token} = c.tokens[c.idx];`,
@@ -2171,24 +2271,51 @@ function createCodegen(config) {
2171
2271
  out.push(`${result} = ${hasAction ? actionResult : "[" + collected.join(", ") + "]"};`, "}");
2172
2272
  return result;
2173
2273
  }
2274
+ function genOrderedBacktrack(list, out, bindingsVar) {
2275
+ const result = nextId();
2276
+ const label2 = "L" + nextId();
2277
+ const start = nextId();
2278
+ out.push(`let ${result};`, `${label2}: {`, `const ${start} = c.idx;`);
2279
+ for (const branch of list) {
2280
+ out.push("{");
2281
+ const value = gen(branch, out, bindingsVar);
2282
+ out.push(`if (${value} !== F) { ${result} = ${value}; break ${label2}; }`, `c.idx = ${start};`, "}");
2283
+ }
2284
+ out.push(`${result} = F;`, "}");
2285
+ return result;
2286
+ }
2174
2287
  function genAlt(branches, out, result, bindingsVar) {
2175
2288
  if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches");
2289
+ const factored = recognizeAltPrefixIdiom(branches);
2290
+ if (factored !== null) return gen(factored, out, bindingsVar);
2176
2291
  const table = firsts ? buildDispatch(branches, firsts) : null;
2177
2292
  const label2 = "L" + nextId();
2178
2293
  if (table !== null) {
2179
2294
  const expected = [...table.keys()];
2295
+ const expectedIds = tokenIds ? expected.map((t) => tokenIds.get(t)) : null;
2296
+ const numeric = expectedIds !== null && expectedIds.every((id) => id !== void 0);
2180
2297
  const token = nextId();
2181
2298
  out.push(tickSrc, `let ${result};`, `${label2}: {`, `const ${token} = c.tokens[c.idx];`);
2182
2299
  out.push(`if (${token} === undefined) { ${expected.map((t) => `E(c, ${JSON.stringify(t)});`).join(" ")} ${result} = F; break ${label2}; }`);
2183
- out.push(`switch (${token}.type) {`);
2300
+ let discriminant;
2301
+ if (numeric) {
2302
+ discriminant = nextId();
2303
+ out.push(`const ${discriminant} = ${token}.id !== undefined ? ${token}.id : I[${token}.type];`);
2304
+ out.push(`switch (${discriminant}) {`);
2305
+ } else {
2306
+ discriminant = `${token}.type`;
2307
+ out.push(`switch (${discriminant}) {`);
2308
+ }
2184
2309
  const byBranch = /* @__PURE__ */ new Map();
2185
- for (const [tokenName, branch] of table) {
2310
+ for (const tokenName of expected) {
2311
+ const branch = table.get(tokenName);
2186
2312
  if (!byBranch.has(branch)) byBranch.set(branch, []);
2187
2313
  byBranch.get(branch).push(tokenName);
2188
2314
  }
2315
+ const caseLabel = (t) => numeric ? tokenIds.get(t) : JSON.stringify(t);
2189
2316
  for (const [branch, tokenNames] of byBranch) {
2190
- out.push(tokenNames.map((t) => `case ${JSON.stringify(t)}:`).join(" ") + " {");
2191
- const value = gen(branch, out, bindingsVar);
2317
+ out.push(tokenNames.map((t) => `case ${caseLabel(t)}:`).join(" ") + " {");
2318
+ const value = Array.isArray(branch) ? genOrderedBacktrack(branch, out, bindingsVar) : gen(branch, out, bindingsVar);
2192
2319
  out.push(`${result} = ${value}; break ${label2};`, "}");
2193
2320
  }
2194
2321
  out.push(
@@ -2198,16 +2325,10 @@ function createCodegen(config) {
2198
2325
  );
2199
2326
  return result;
2200
2327
  }
2201
- const start = nextId();
2202
- out.push(tickSrc, `let ${result};`, `${label2}: {`, `const ${start} = c.idx;`);
2203
- for (const branch of branches) {
2204
- out.push("{");
2205
- const value = gen(branch, out, bindingsVar);
2206
- out.push(`if (${value} !== F) { ${result} = ${value}; break ${label2}; }`, `c.idx = ${start};`, "}");
2207
- }
2208
- out.push(`${result} = F;`, "}");
2209
- return result;
2328
+ out.push(tickSrc);
2329
+ return genOrderedBacktrack(branches, out, bindingsVar);
2210
2330
  }
2331
+ const idByName = tokenIds ? Object.fromEntries(tokenIds) : null;
2211
2332
  function buildRule(name) {
2212
2333
  const out = [];
2213
2334
  const value = gen(rules[name], out, null);
@@ -2217,8 +2338,8 @@ return ${value};
2217
2338
  }`;
2218
2339
  SOURCES[ruleIndex.get(name)] = body;
2219
2340
  const source = `return ${body}`;
2220
- const factory = new Function("F", "E", "P", "T", "R", "H", source);
2221
- return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS);
2341
+ const factory = new Function("F", "E", "P", "T", "R", "H", "I", source);
2342
+ return factory(FAIL, expect, progressError, stepsError, RULES, HELPERS, idByName);
2222
2343
  }
2223
2344
  function progressError(op, at) {
2224
2345
  throw new Error(
@@ -2300,9 +2421,10 @@ function createParser(spec, options = {}) {
2300
2421
  for (const [name, method] of Object.entries(extraMethods || {})) engine.addMethod(name, method);
2301
2422
  const startRule = rules[start];
2302
2423
  const firsts = ll1 ? analysis.firsts : null;
2303
- const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval });
2424
+ const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]));
2425
+ const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval, tokenIds });
2304
2426
  engine.__jlGrammar = { planner };
2305
- const codegen = compile && unsafeEval ? createCodegen({ planner, rules, firsts, memo, maxSteps }) : null;
2427
+ const codegen = compile && unsafeEval ? createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds }) : null;
2306
2428
  const compiledStart = codegen && codegen.compile(start);
2307
2429
  if (codegen && !engine.disableInterpretedOptimization) {
2308
2430
  for (const name of Object.keys(rules)) {
@@ -2745,12 +2867,13 @@ function emitModule(spec, options = {}) {
2745
2867
  const analysis = analyze(rules, { tokenNames: lexer.tokenNames });
2746
2868
  if (validate && analysis.errors.length > 0) throw new GrammarError(analysis.errors);
2747
2869
  const firsts = ll1 ? analysis.firsts : null;
2870
+ const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]));
2748
2871
  const engine = { build: () => {
2749
2872
  throw new Error("not compiled during emit");
2750
2873
  }, run: () => {
2751
2874
  } };
2752
- const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false });
2753
- const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps });
2875
+ const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval: false, tokenIds });
2876
+ const codegen = createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds });
2754
2877
  codegen.compile(start);
2755
2878
  const reached = [...codegen.ruleIndex.entries()].sort((a, b) => a[1] - b[1]);
2756
2879
  const unsupported = codegen.ORIGINS.map((origin, index) => ({ origin, index })).filter(({ origin }) => origin.kind === "unknownRule");
@@ -2893,6 +3016,14 @@ const H = [
2893
3016
  ${helpers.map((source, index) => ` /* ${index} */ ${source}`).join(",\n")}
2894
3017
  ]
2895
3018
 
3019
+ /**
3020
+ * Name -> id, the same numbering \`lexer\` assigns. A generated \`alt\`'s switch
3021
+ * dispatches on a token's id when it has one; this is only read as a fallback,
3022
+ * for a hand-built token from \`parseTokens\` that bypassed this file's own
3023
+ * lexer and so has no id of its own.
3024
+ */
3025
+ const I = ${literal(Object.fromEntries(tokenIds))}
3026
+
2896
3027
  ${ruleSources}
2897
3028
 
2898
3029
  const RULES = ${literal(Object.fromEntries(reached.map(([name, index]) => [name, index])))}
@@ -409,7 +409,7 @@ function createLexer(defs, options = {}) {
409
409
  return id;
410
410
  };
411
411
  for (const def of defs) for (const mode of modesOf(def)) idOf(mode);
412
- const compiled = defs.map((def) => {
412
+ const compiled = defs.map((def, id) => {
413
413
  if (!def.name) throw new Error("createLexer: every token definition needs a name");
414
414
  if (!def.pattern && !def.literal) throw new Error(`createLexer: token '${def.name}' needs a pattern or a literal`);
415
415
  if (def.pattern && def.literal) throw new Error(`createLexer: token '${def.name}' has both a pattern and a literal; pick one`);
@@ -424,6 +424,11 @@ function createLexer(defs, options = {}) {
424
424
  }
425
425
  return {
426
426
  name: def.name,
427
+ // The declaration index. `createParser` derives the identical mapping
428
+ // from `tokenNames` independently (same array, same order), so a
429
+ // combinator that resolves a `consume` target's id at build time agrees
430
+ // with what a token actually carries without the two ever comparing notes.
431
+ id,
427
432
  skip: !!def.skip,
428
433
  literal: def.literal,
429
434
  longerAlt: def.longerAlt || null,
@@ -522,14 +527,7 @@ function createLexer(defs, options = {}) {
522
527
  }
523
528
  if (!winner.skip) {
524
529
  const image = winner.literal !== void 0 ? winner.literal : winner === def && matched !== null ? matched : text.slice(offset, end);
525
- tokens.push({
526
- type: winner.name,
527
- image,
528
- start: offset,
529
- end,
530
- line: trackLines ? line : 0,
531
- col: trackLines ? offset - lineStart + 1 : 0
532
- });
530
+ tokens.push(trackLines ? { type: winner.name, image, start: offset, end, line, col: offset - lineStart + 1 } : { type: winner.name, id: winner.id, image, start: offset, end });
533
531
  }
534
532
  if (trackLines) {
535
533
  for (let j = offset; j < end; j++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grammatik",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A parser toolkit whose grammars are data: tokens, rules and semantic actions all serializable, compiled ahead of time.",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -38,7 +38,8 @@
38
38
  "bench:compare": "node bench/compare-codex.js",
39
39
  "bench:formula": "node bench/peggy/build.js && node bench/formula.js",
40
40
  "bench:generated": "node bench/peggy/build.js && node bench/generate-formula.js && node bench/generated.js",
41
- "bench:json-parsers": "node bench/jsonParsers/run.js"
41
+ "bench:json-parsers": "node bench/jsonParsers/run.js",
42
+ "build:site": "node site/build.js"
42
43
  },
43
44
  "keywords": [
44
45
  "parser",
package/src/analyze.js CHANGED
@@ -263,6 +263,82 @@ function isPlainParser (node) {
263
263
  return op !== null && op !== 'label' && op !== 'action'
264
264
  }
265
265
 
266
+ /** Structural equality over plain JSON — every shape a grammar node can be. */
267
+ function deepEqual (a, b) {
268
+ if (a === b) return true
269
+ if (Array.isArray(a) || Array.isArray(b)) {
270
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
271
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false
272
+ return true
273
+ }
274
+ if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false
275
+ const keysA = Object.keys(a)
276
+ const keysB = Object.keys(b)
277
+ if (keysA.length !== keysB.length) return false
278
+ for (const key of keysA) {
279
+ if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) return false
280
+ }
281
+ return true
282
+ }
283
+
284
+ /**
285
+ * Left-factors an `alt` whose branches are all `seq` and share a literal
286
+ * leading run of nodes — `alt(seq(P, X, actionX), seq(P, Y, actionY))`
287
+ * becomes `seq(P, alt(seq(X, actionX), seq(Y, actionY)))`, so a shared,
288
+ * possibly multi-token prefix is matched once per attempt instead of once per
289
+ * colliding branch.
290
+ *
291
+ * Two things have to hold for the rewrite to be safe, not just structurally
292
+ * possible:
293
+ *
294
+ * - The shared prefix cannot contain a `label` or an `action`. A `seq`'s
295
+ * bindings are scoped to itself; hoisting a labelled node out from under
296
+ * the branch that reads it would strand the binding in a different scope
297
+ * than the action that expects to see it.
298
+ * - Every branch must already end its own decision with an `action`
299
+ * somewhere in its remainder (`seq` yields that action's value instead of
300
+ * the flat array of its children). That is what lets the rewritten form
301
+ * reproduce the original value exactly, by wrapping the factored `alt` in
302
+ * one more `label`/`action` pair rather than redesigning what the branch
303
+ * returns.
304
+ *
305
+ * A branch that is a strict prefix of another (nothing left after the shared
306
+ * run) fails the second check on its own — its remainder is empty, so it has
307
+ * no action left to find — and the whole rewrite backs off rather than guess
308
+ * what an author meant by writing one alternative as a prefix of another.
309
+ *
310
+ * @param {any[]} branches
311
+ * @returns {any|null}
312
+ */
313
+ export function recognizeAltPrefixIdiom (branches) {
314
+ if (!Array.isArray(branches) || branches.length < 2) return null
315
+ if (!branches.every((b) => opOf(b) === 'seq' && Array.isArray(b.seq))) return null
316
+
317
+ const seqs = branches.map((b) => b.seq)
318
+ const minLen = Math.min(...seqs.map((s) => s.length))
319
+
320
+ let n = 0
321
+ while (n < minLen) {
322
+ const candidate = seqs[0][n]
323
+ const op = opOf(candidate)
324
+ if (op === 'label' || op === 'action') break
325
+ if (!seqs.every((s) => deepEqual(s[n], candidate))) break
326
+ n++
327
+ }
328
+ if (n === 0) return null
329
+ if (!seqs.every((s) => s.slice(n).some((child) => opOf(child) === 'action'))) return null
330
+
331
+ const prefix = seqs[0].slice(0, n)
332
+ const remainders = seqs.map((s) => ({ seq: s.slice(n) }))
333
+ return {
334
+ seq: [
335
+ ...prefix,
336
+ { label: ['__grammatik_rest', { alt: remainders }] },
337
+ { action: { val: '__grammatik_rest' } }
338
+ ]
339
+ }
340
+ }
341
+
266
342
  const BAIL = Symbol('grammatik.bail')
267
343
 
268
344
  /**
@@ -573,30 +649,51 @@ export function analyze (grammar, options = {}) {
573
649
  }
574
650
 
575
651
  /**
576
- * Builds an O(1) dispatch table for an `alt`, or null when the branches are not
577
- * LL(1)-separable and we must fall back to ordered backtracking.
652
+ * Builds an O(1) dispatch table for an `alt`, or null when no branch has a
653
+ * known, non-nullable FIRST set (a nullable/opaque branch could match
654
+ * anything, so no table is safe at all).
655
+ *
656
+ * A token whose FIRST set membership is unique to one branch maps straight to
657
+ * that branch. A token shared by more than one branch — the choice needs more
658
+ * than one token of lookahead there — maps to an array of the colliding
659
+ * branches, in their original `alt` order; a caller resolves that case with
660
+ * ordered backtracking over just that subset, rather than over every branch.
661
+ * Equal-content collision groups are the same array instance, so a caller
662
+ * that dedupes on the table's values (e.g. to share generated code between
663
+ * case labels) gets that for free.
578
664
  *
579
665
  * @param {any[]} branches
580
666
  * @param {Map<string, First>} firsts
581
667
  * @returns {Map<string, any>|null}
582
668
  */
583
669
  export function buildDispatch (branches, firsts) {
584
- /** @type {Map<string, any>} */
585
- const table = new Map()
586
-
587
- for (const branch of branches) {
588
- const first = firstOf(branch, firsts)
589
- // A nullable or opaque branch could match anything, so no table is safe.
670
+ const perBranch = branches.map((branch) => firstOf(branch, firsts))
671
+ for (const first of perBranch) {
590
672
  if (first.nullable || first.unknown || first.tokens.size === 0) return null
591
- for (const token of first.tokens) {
592
- // Overlapping FIRST sets mean the choice needs more than one token of
593
- // lookahead; ordered backtracking still handles it correctly.
594
- if (table.has(token)) return null
595
- table.set(token, branch)
673
+ }
674
+
675
+ /** @type {Map<string, any[]>} */
676
+ const byToken = new Map()
677
+ for (let i = 0; i < branches.length; i++) {
678
+ for (const token of perBranch[i].tokens) {
679
+ let list = byToken.get(token)
680
+ if (list === undefined) { list = []; byToken.set(token, list) }
681
+ list.push(branches[i])
596
682
  }
597
683
  }
684
+ if (byToken.size === 0) return null
685
+
686
+ const canonical = new Map()
687
+ const table = new Map()
688
+ for (const [token, list] of byToken) {
689
+ if (list.length === 1) { table.set(token, list[0]); continue }
690
+ const key = list.map((branch) => branches.indexOf(branch)).join(',')
691
+ let group = canonical.get(key)
692
+ if (group === undefined) { group = list; canonical.set(key, group) }
693
+ table.set(token, group)
694
+ }
598
695
 
599
- return table.size > 0 ? table : null
696
+ return table
600
697
  }
601
698
 
602
699
  export { firstOf }