parseman 0.14.1 → 0.15.0

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/index.js CHANGED
@@ -27,6 +27,63 @@ function any() {
27
27
  function empty() {
28
28
  return { kind: "empty" };
29
29
  }
30
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
31
+ if (seen.has(p)) return true;
32
+ seen.add(p);
33
+ const me = (c) => matchesEmpty(c, seen);
34
+ const d = p._def;
35
+ switch (d.tag) {
36
+ case "literal":
37
+ return d.value.length === 0;
38
+ case "keywords":
39
+ return false;
40
+ case "regex":
41
+ try {
42
+ const m = new RegExp(d.source).exec("");
43
+ return m != null && m[0] === "";
44
+ } catch {
45
+ return true;
46
+ }
47
+ case "many":
48
+ case "optional":
49
+ case "not":
50
+ return true;
51
+ // zero repetitions / absent / lookahead
52
+ case "oneOrMore":
53
+ return me(d.parser);
54
+ case "sequence":
55
+ return d.parsers.every(me);
56
+ case "choice":
57
+ return d.parsers.some(me);
58
+ case "transform":
59
+ case "label":
60
+ case "trivia":
61
+ case "expect":
62
+ case "withCtx":
63
+ case "node":
64
+ case "grammar":
65
+ case "recover":
66
+ return me(d.parser);
67
+ case "skip":
68
+ return me(d.main);
69
+ case "lazy":
70
+ try {
71
+ return me(d.thunk());
72
+ } catch {
73
+ return true;
74
+ }
75
+ default:
76
+ return true;
77
+ }
78
+ }
79
+ function sequenceFirstSet(parsers) {
80
+ let fs = empty();
81
+ for (const p of parsers) {
82
+ fs = union(fs, p._meta.firstSet);
83
+ if (!matchesEmpty(p)) return fs;
84
+ }
85
+ return fs;
86
+ }
30
87
  function mergeRanges(ranges) {
31
88
  if (ranges.length === 0) return [];
32
89
  const sorted = [...ranges].sort((a, b) => a.lo - b.lo);
@@ -848,16 +905,19 @@ function scanTrivia(input, cur, ctx) {
848
905
  // src/combinators/sequence.ts
849
906
  function sequence(...parsers) {
850
907
  const meta = {
851
- firstSet: parsers[0]?._meta.firstSet ?? empty(),
908
+ // Union through the nullable prefix — a leading optional()/many() lets a later
909
+ // term's first char start the sequence. Just `parsers[0]` under-approximates.
910
+ firstSet: sequenceFirstSet(parsers),
852
911
  canMatchNewline: parsers.some((p) => p._meta.canMatchNewline),
853
912
  isTrivia: false
854
913
  };
914
+ const def = { tag: "sequence", parsers };
855
915
  return {
856
916
  _tag: "sequence",
857
917
  _meta: meta,
858
- _def: { tag: "sequence", parsers },
918
+ _def: def,
859
919
  parse(input, pos, ctx) {
860
- const values = [];
920
+ const values = def.valueUnused ? void 0 : [];
861
921
  let cur = pos;
862
922
  for (let i = 0; i < parsers.length; i++) {
863
923
  if (ctx.trivia && i > 0) {
@@ -877,17 +937,18 @@ function sequence(...parsers) {
877
937
  } else {
878
938
  rollbackTrivia(ctx, mark);
879
939
  }
880
- values.push(result2.value);
940
+ if (values !== void 0) values.push(result2.value);
881
941
  continue;
882
942
  }
883
943
  const result = parsers[i].parse(input, cur, ctx);
884
944
  if (!result.ok) return result;
885
- values.push(result.value);
945
+ if (values !== void 0) values.push(result.value);
886
946
  cur = result.span.end;
887
947
  }
888
948
  return {
889
949
  ok: true,
890
- value: values,
950
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
951
+ value: values ?? void 0,
891
952
  span: { start: pos, end: cur }
892
953
  };
893
954
  }
@@ -928,20 +989,21 @@ function many(combinator) {
928
989
  canMatchNewline: combinator._meta.canMatchNewline,
929
990
  isTrivia: false
930
991
  };
992
+ const def = { tag: "many", parser: combinator, min: 0 };
931
993
  return {
932
994
  _tag: "many",
933
995
  _meta: meta,
934
- _def: { tag: "many", parser: combinator, min: 0 },
996
+ _def: def,
935
997
  parse(input, pos, ctx) {
936
- const values = [];
998
+ const values = def.valueUnused ? void 0 : [];
937
999
  let cur = pos;
938
1000
  while (cur < input.length) {
939
1001
  const item = repItem(combinator, input, cur, ctx);
940
1002
  if (item === "stop" || "fail" in item) break;
941
- values.push(item.value);
1003
+ if (values !== void 0) values.push(item.value);
942
1004
  cur = item.end;
943
1005
  }
944
- return { ok: true, value: values, span: { start: pos, end: cur } };
1006
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
945
1007
  }
946
1008
  };
947
1009
  }
@@ -951,22 +1013,23 @@ function oneOrMore(combinator) {
951
1013
  canMatchNewline: combinator._meta.canMatchNewline,
952
1014
  isTrivia: false
953
1015
  };
1016
+ const def = { tag: "oneOrMore", parser: combinator, min: 1 };
954
1017
  return {
955
1018
  _tag: "oneOrMore",
956
1019
  _meta: meta,
957
- _def: { tag: "oneOrMore", parser: combinator, min: 1 },
1020
+ _def: def,
958
1021
  parse(input, pos, ctx) {
959
1022
  const first = combinator.parse(input, pos, ctx);
960
1023
  if (!first.ok) return first;
961
- const values = [first.value];
1024
+ const values = def.valueUnused ? void 0 : [first.value];
962
1025
  let cur = first.span.end;
963
1026
  while (cur < input.length) {
964
1027
  const item = repItem(combinator, input, cur, ctx);
965
1028
  if (item === "stop" || "fail" in item) break;
966
- values.push(item.value);
1029
+ if (values !== void 0) values.push(item.value);
967
1030
  cur = item.end;
968
1031
  }
969
- return { ok: true, value: values, span: { start: pos, end: cur } };
1032
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
970
1033
  }
971
1034
  };
972
1035
  }
@@ -1074,6 +1137,90 @@ function ref() {
1074
1137
  return slot;
1075
1138
  }
1076
1139
 
1140
+ // src/compiler/value-usage.ts
1141
+ function markUnusedValues(root) {
1142
+ const seenConsumed = /* @__PURE__ */ new WeakSet();
1143
+ const seenUnconsumed = /* @__PURE__ */ new WeakSet();
1144
+ const mark = (def, consumed) => {
1145
+ if (consumed) def.valueUnused = false;
1146
+ else if (def.valueUnused !== false) def.valueUnused = true;
1147
+ };
1148
+ const visit = (c, consumed) => {
1149
+ const seen = consumed ? seenConsumed : seenUnconsumed;
1150
+ if (seen.has(c)) return;
1151
+ seen.add(c);
1152
+ const def = c._def;
1153
+ switch (def.tag) {
1154
+ case "many":
1155
+ case "oneOrMore":
1156
+ mark(def, consumed);
1157
+ visit(def.parser, consumed);
1158
+ return;
1159
+ case "optional":
1160
+ visit(def.parser, consumed);
1161
+ return;
1162
+ case "sequence":
1163
+ mark(def, consumed);
1164
+ for (const p of def.parsers) visit(p, consumed);
1165
+ return;
1166
+ case "choice":
1167
+ for (const p of def.parsers) visit(p, consumed);
1168
+ return;
1169
+ case "node":
1170
+ visit(def.parser, false);
1171
+ return;
1172
+ case "transform":
1173
+ visit(def.parser, true);
1174
+ return;
1175
+ case "skip":
1176
+ visit(def.main, consumed);
1177
+ visit(def.skipped, false);
1178
+ return;
1179
+ case "sepBy":
1180
+ visit(def.parser, true);
1181
+ visit(def.separator, false);
1182
+ return;
1183
+ case "not":
1184
+ visit(def.parser, false);
1185
+ return;
1186
+ case "grammar":
1187
+ visit(def.parser, consumed);
1188
+ if (def.triviaParser) visit(def.triviaParser, false);
1189
+ return;
1190
+ case "recover":
1191
+ visit(def.parser, consumed);
1192
+ visit(def.sentinel, false);
1193
+ return;
1194
+ case "label":
1195
+ case "trivia":
1196
+ case "withCtx":
1197
+ case "expect":
1198
+ visit(def.parser, consumed);
1199
+ return;
1200
+ case "lazy":
1201
+ return;
1202
+ default:
1203
+ return;
1204
+ }
1205
+ };
1206
+ const resolveRoot = (c) => {
1207
+ const guard2 = /* @__PURE__ */ new Set();
1208
+ let cur = c;
1209
+ while (!guard2.has(cur)) {
1210
+ const d = cur._def;
1211
+ if (d.tag !== "lazy") return cur;
1212
+ guard2.add(cur);
1213
+ try {
1214
+ cur = d.thunk();
1215
+ } catch {
1216
+ return c;
1217
+ }
1218
+ }
1219
+ return cur;
1220
+ };
1221
+ visit(resolveRoot(root), true);
1222
+ }
1223
+
1077
1224
  // src/combinators/parser.ts
1078
1225
  function rules(factory) {
1079
1226
  const cache = {};
@@ -1103,6 +1250,9 @@ function rules(factory) {
1103
1250
  cache[key] = parser2;
1104
1251
  }
1105
1252
  }
1253
+ for (const key of Object.keys(definitions)) {
1254
+ markUnusedValues(cache[key]);
1255
+ }
1106
1256
  return cache;
1107
1257
  }
1108
1258
 
@@ -1319,7 +1469,12 @@ function parser(opts, root) {
1319
1469
  function noTrivia(root) {
1320
1470
  return parser({ trivia: null }, root);
1321
1471
  }
1472
+ var _analyzed = /* @__PURE__ */ new WeakSet();
1322
1473
  function parse(combinator, input, opts = {}) {
1474
+ if (!_analyzed.has(combinator)) {
1475
+ _analyzed.add(combinator);
1476
+ markUnusedValues(combinator);
1477
+ }
1323
1478
  const trackLines = opts.trackLines ?? false;
1324
1479
  const _errors = opts.recover ? [] : void 0;
1325
1480
  const _probe = opts.recover ? { offset: input.length, best: null } : void 0;
@@ -3276,6 +3431,7 @@ function emitSeqValues(def, ctx, pos) {
3276
3431
  }
3277
3432
  function emitSeq(def, ctx, pos) {
3278
3433
  const { stmts, endVar, valueVars } = emitSeqValues(def, ctx, pos);
3434
+ if (def.valueUnused) return { stmts, valueVar: "undefined", endVar };
3279
3435
  const arrV = v(ctx, "_arr");
3280
3436
  stmts.push(`${ind(ctx)}const ${arrV} = [${valueVars.join(", ")}]`);
3281
3437
  return { stmts, valueVar: arrV, endVar };
@@ -3482,7 +3638,8 @@ function emitFirstMatch(def, ctx, pos) {
3482
3638
  const autoNot = def.autoNot[i];
3483
3639
  const atStart = failsAtStart(p);
3484
3640
  const staticFx = armStaticExpected(ctx, p);
3485
- const fsGuard = needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3641
+ const deferRuleName = ctx.deferFirstSetRefs ? p._ruleName : void 0;
3642
+ const fsGuard = deferRuleName !== void 0 ? `/*@FS:${deferRuleName}:${codeV}@*/true` : needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3486
3643
  let gateCond = null;
3487
3644
  if (gate) {
3488
3645
  const gateIdx = ctx.mapFns.length;
@@ -3573,19 +3730,18 @@ function emitLiteralCondition(litVal, pos) {
3573
3730
  return checks.join(" && ");
3574
3731
  }
3575
3732
  function emitMany(def, ctx, pos) {
3733
+ const wantValue = !def.valueUnused;
3576
3734
  const arrV = v(ctx, "_arr");
3577
3735
  const curV = v(ctx, "_cur");
3578
3736
  const stmts = [
3579
- `${ind(ctx)}const ${arrV} = []`,
3737
+ ...wantValue ? [`${ind(ctx)}const ${arrV} = []`] : [],
3580
3738
  `${ind(ctx)}let ${curV} = ${pos}`
3581
3739
  ];
3582
3740
  if (def.min === 1) {
3583
3741
  const firstR = emit(def.parser, ctx, curV);
3584
3742
  stmts.push(...firstR.stmts);
3585
- stmts.push(
3586
- `${ind(ctx)}${arrV}.push(${firstR.valueVar})`,
3587
- `${ind(ctx)}${curV} = ${firstR.endVar}`
3588
- );
3743
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
3744
+ stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
3589
3745
  }
3590
3746
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
3591
3747
  ctx.indent++;
@@ -3616,13 +3772,13 @@ function emitMany(def, ctx, pos) {
3616
3772
  const { stmts: iterStmts, okVar: iterOk, valVar: iterVal, endVar: iterEnd } = emitFallible(def.parser, ctx, itemPos, true);
3617
3773
  stmts.push(
3618
3774
  ...iterStmts,
3619
- `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`,
3620
- `${ind(ctx)}${arrV}.push(${iterVal})`,
3621
- `${ind(ctx)}${curV} = ${iterEnd}`
3775
+ `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`
3622
3776
  );
3777
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
3778
+ stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
3623
3779
  ctx.indent--;
3624
3780
  stmts.push(`${ind(ctx)}}`);
3625
- return { stmts, valueVar: arrV, endVar: curV };
3781
+ return { stmts, valueVar: wantValue ? arrV : "undefined", endVar: curV };
3626
3782
  }
3627
3783
  function emitOptional(def, ctx, pos) {
3628
3784
  const valV = v(ctx, "_opt");
@@ -4232,6 +4388,7 @@ function ruleDependencies(ruleMap) {
4232
4388
  return deps;
4233
4389
  }
4234
4390
  function compile(combinator, mapFnSources) {
4391
+ markUnusedValues(combinator);
4235
4392
  const ctx = {
4236
4393
  vars: 0,
4237
4394
  indent: 1,
@@ -4314,8 +4471,17 @@ function compile(combinator, mapFnSources) {
4314
4471
  }
4315
4472
  };
4316
4473
  }
4474
+ var RULE_NAME_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
4475
+ function assertRuleName(name) {
4476
+ if (!RULE_NAME_RE.test(name)) {
4477
+ throw new Error(
4478
+ `parseman: grammar rule name ${JSON.stringify(name)} is not a valid JS identifier. Rule names compile to _r_<Name> functions and dispatch guards \u2014 use letters, digits, _ or $ (and don't start with a digit).`
4479
+ );
4480
+ }
4481
+ }
4317
4482
  function compileLinkable(ruleMapArg, ns) {
4318
4483
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
4484
+ for (const [, rule] of ruleMapArg) markUnusedValues(rule);
4319
4485
  const ruleMap = ruleMapArg.filter(([, val]) => {
4320
4486
  const d = val._def;
4321
4487
  if (d.tag !== "lazy") return true;
@@ -4346,13 +4512,15 @@ function compileLinkable(ruleMapArg, ns) {
4346
4512
  namedFnDecls: [],
4347
4513
  capturing: ruleMap.some(([, rule]) => hasNodeDef(rule)),
4348
4514
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
4349
- ns
4515
+ ns,
4516
+ deferFirstSetRefs: true
4350
4517
  };
4351
4518
  const seen = /* @__PURE__ */ new Map();
4352
4519
  const ruleNames = /* @__PURE__ */ new Map();
4353
4520
  const fnNameToKey = /* @__PURE__ */ new Map();
4354
4521
  for (const [key, rule] of ruleMap) {
4355
- const base = `_r_${key.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4522
+ assertRuleName(key);
4523
+ const base = `_r_${key}`;
4356
4524
  const n = seen.get(base) ?? 0;
4357
4525
  seen.set(base, n + 1);
4358
4526
  const fn = n === 0 ? base : `${base}$${n}`;
@@ -4376,7 +4544,8 @@ function compileLinkable(ruleMapArg, ns) {
4376
4544
  if (resolved === void 0) {
4377
4545
  const name = p._ruleName;
4378
4546
  if (name && !ctx.namedParsers.has(p)) {
4379
- const fn = `_r_${name.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4547
+ assertRuleName(name);
4548
+ const fn = `_r_${name}`;
4380
4549
  ruleNames.set(p, fn);
4381
4550
  ctx.namedParsers.set(p, fn);
4382
4551
  }
@@ -4445,12 +4614,26 @@ function compileLinkable(ruleMapArg, ns) {
4445
4614
  `}`
4446
4615
  ].join("\n"));
4447
4616
  }
4617
+ const firstSets = /* @__PURE__ */ new Map();
4618
+ for (const [key, rule] of ruleMap) {
4619
+ let resolved = rule;
4620
+ const d = rule._def;
4621
+ if (d.tag === "lazy") {
4622
+ try {
4623
+ resolved = d.thunk();
4624
+ } catch {
4625
+ resolved = rule;
4626
+ }
4627
+ }
4628
+ firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
4629
+ }
4448
4630
  return {
4449
4631
  ns,
4450
4632
  keys: perEntry.map((e) => e.key),
4451
4633
  prelude,
4452
4634
  ruleFns,
4453
4635
  wrappers,
4636
+ firstSets,
4454
4637
  deps: ruleDependencies(ruleMap),
4455
4638
  needsEmptyTl: !!ctx.needsEmptyTl,
4456
4639
  needsCollator: ctx.needsCollator,
@@ -4551,7 +4734,23 @@ function fusedBody(pieces) {
4551
4734
  ...[...winner].map(([k, p]) => p.ruleFns.get(k))
4552
4735
  ];
4553
4736
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
4554
- const body = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4737
+ const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4738
+ const finalFS = /* @__PURE__ */ new Map();
4739
+ for (const [k, p] of winner) {
4740
+ const fs = p.firstSets?.get(k);
4741
+ if (fs) finalFS.set(k, fs);
4742
+ }
4743
+ const body = rawBody.replace(
4744
+ // Rule name + code-point var are both JS identifiers (rule names are validated
4745
+ // at compile time — see assertRuleName in codegen), so an identifier class
4746
+ // matches every well-formed placeholder.
4747
+ /\/\*@FS:([A-Za-z0-9_$]+):([A-Za-z0-9_$]+)@\*\/true/g,
4748
+ (_m, name, codevar) => {
4749
+ const fs = finalFS.get(name);
4750
+ if (!fs || fs.kind === "any" || fs.kind === "empty") return "true";
4751
+ return `(${firstSetCond(codevar, fs)})`;
4752
+ }
4753
+ );
4555
4754
  const env = {};
4556
4755
  for (const p of contributing) {
4557
4756
  if (p.mfFns.length) env[`${p.ns}mf`] = p.mfFns;