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/cjs/index.js +175 -44
- package/dist/cjs/runtime.js +7 -9
- package/dist/esm/index.js +175 -44
- package/dist/esm/runtime.js +7 -9
- package/package.json +3 -2
- package/src/analyze.js +111 -14
- package/src/codegen.js +75 -20
- package/src/emit.js +14 -2
- package/src/lexer.js +38 -16
- package/src/parser.js +9 -2
- package/src/plan.js +58 -8
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
|
|
1006
|
-
for (const
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
2202
|
-
|
|
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
|
|
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])))}
|
package/dist/esm/runtime.js
CHANGED
|
@@ -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.
|
|
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
|
|
577
|
-
*
|
|
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
|
-
|
|
585
|
-
const
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
|
696
|
+
return table
|
|
600
697
|
}
|
|
601
698
|
|
|
602
699
|
export { firstOf }
|