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.
@@ -34,6 +34,63 @@ function any() {
34
34
  function empty() {
35
35
  return { kind: "empty" };
36
36
  }
37
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
38
+ if (seen.has(p)) return true;
39
+ seen.add(p);
40
+ const me = (c) => matchesEmpty(c, seen);
41
+ const d = p._def;
42
+ switch (d.tag) {
43
+ case "literal":
44
+ return d.value.length === 0;
45
+ case "keywords":
46
+ return false;
47
+ case "regex":
48
+ try {
49
+ const m = new RegExp(d.source).exec("");
50
+ return m != null && m[0] === "";
51
+ } catch {
52
+ return true;
53
+ }
54
+ case "many":
55
+ case "optional":
56
+ case "not":
57
+ return true;
58
+ // zero repetitions / absent / lookahead
59
+ case "oneOrMore":
60
+ return me(d.parser);
61
+ case "sequence":
62
+ return d.parsers.every(me);
63
+ case "choice":
64
+ return d.parsers.some(me);
65
+ case "transform":
66
+ case "label":
67
+ case "trivia":
68
+ case "expect":
69
+ case "withCtx":
70
+ case "node":
71
+ case "grammar":
72
+ case "recover":
73
+ return me(d.parser);
74
+ case "skip":
75
+ return me(d.main);
76
+ case "lazy":
77
+ try {
78
+ return me(d.thunk());
79
+ } catch {
80
+ return true;
81
+ }
82
+ default:
83
+ return true;
84
+ }
85
+ }
86
+ function sequenceFirstSet(parsers) {
87
+ let fs2 = empty();
88
+ for (const p of parsers) {
89
+ fs2 = union(fs2, p._meta.firstSet);
90
+ if (!matchesEmpty(p)) return fs2;
91
+ }
92
+ return fs2;
93
+ }
37
94
  function mergeRanges(ranges) {
38
95
  if (ranges.length === 0) return [];
39
96
  const sorted = [...ranges].sort((a, b) => a.lo - b.lo);
@@ -882,16 +939,19 @@ function scanTrivia(input, cur, ctx) {
882
939
  // src/combinators/sequence.ts
883
940
  function sequence(...parsers) {
884
941
  const meta = {
885
- firstSet: parsers[0]?._meta.firstSet ?? empty(),
942
+ // Union through the nullable prefix — a leading optional()/many() lets a later
943
+ // term's first char start the sequence. Just `parsers[0]` under-approximates.
944
+ firstSet: sequenceFirstSet(parsers),
886
945
  canMatchNewline: parsers.some((p) => p._meta.canMatchNewline),
887
946
  isTrivia: false
888
947
  };
948
+ const def = { tag: "sequence", parsers };
889
949
  return {
890
950
  _tag: "sequence",
891
951
  _meta: meta,
892
- _def: { tag: "sequence", parsers },
952
+ _def: def,
893
953
  parse(input, pos, ctx) {
894
- const values = [];
954
+ const values = def.valueUnused ? void 0 : [];
895
955
  let cur = pos;
896
956
  for (let i = 0; i < parsers.length; i++) {
897
957
  if (ctx.trivia && i > 0) {
@@ -911,17 +971,18 @@ function sequence(...parsers) {
911
971
  } else {
912
972
  rollbackTrivia(ctx, mark);
913
973
  }
914
- values.push(result2.value);
974
+ if (values !== void 0) values.push(result2.value);
915
975
  continue;
916
976
  }
917
977
  const result = parsers[i].parse(input, cur, ctx);
918
978
  if (!result.ok) return result;
919
- values.push(result.value);
979
+ if (values !== void 0) values.push(result.value);
920
980
  cur = result.span.end;
921
981
  }
922
982
  return {
923
983
  ok: true,
924
- value: values,
984
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
985
+ value: values ?? void 0,
925
986
  span: { start: pos, end: cur }
926
987
  };
927
988
  }
@@ -962,20 +1023,21 @@ function many(combinator) {
962
1023
  canMatchNewline: combinator._meta.canMatchNewline,
963
1024
  isTrivia: false
964
1025
  };
1026
+ const def = { tag: "many", parser: combinator, min: 0 };
965
1027
  return {
966
1028
  _tag: "many",
967
1029
  _meta: meta,
968
- _def: { tag: "many", parser: combinator, min: 0 },
1030
+ _def: def,
969
1031
  parse(input, pos, ctx) {
970
- const values = [];
1032
+ const values = def.valueUnused ? void 0 : [];
971
1033
  let cur = pos;
972
1034
  while (cur < input.length) {
973
1035
  const item = repItem(combinator, input, cur, ctx);
974
1036
  if (item === "stop" || "fail" in item) break;
975
- values.push(item.value);
1037
+ if (values !== void 0) values.push(item.value);
976
1038
  cur = item.end;
977
1039
  }
978
- return { ok: true, value: values, span: { start: pos, end: cur } };
1040
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
979
1041
  }
980
1042
  };
981
1043
  }
@@ -985,22 +1047,23 @@ function oneOrMore(combinator) {
985
1047
  canMatchNewline: combinator._meta.canMatchNewline,
986
1048
  isTrivia: false
987
1049
  };
1050
+ const def = { tag: "oneOrMore", parser: combinator, min: 1 };
988
1051
  return {
989
1052
  _tag: "oneOrMore",
990
1053
  _meta: meta,
991
- _def: { tag: "oneOrMore", parser: combinator, min: 1 },
1054
+ _def: def,
992
1055
  parse(input, pos, ctx) {
993
1056
  const first = combinator.parse(input, pos, ctx);
994
1057
  if (!first.ok) return first;
995
- const values = [first.value];
1058
+ const values = def.valueUnused ? void 0 : [first.value];
996
1059
  let cur = first.span.end;
997
1060
  while (cur < input.length) {
998
1061
  const item = repItem(combinator, input, cur, ctx);
999
1062
  if (item === "stop" || "fail" in item) break;
1000
- values.push(item.value);
1063
+ if (values !== void 0) values.push(item.value);
1001
1064
  cur = item.end;
1002
1065
  }
1003
- return { ok: true, value: values, span: { start: pos, end: cur } };
1066
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1004
1067
  }
1005
1068
  };
1006
1069
  }
@@ -1078,6 +1141,90 @@ function sepBy(combinator, separator) {
1078
1141
  };
1079
1142
  }
1080
1143
 
1144
+ // src/compiler/value-usage.ts
1145
+ function markUnusedValues(root) {
1146
+ const seenConsumed = /* @__PURE__ */ new WeakSet();
1147
+ const seenUnconsumed = /* @__PURE__ */ new WeakSet();
1148
+ const mark = (def, consumed) => {
1149
+ if (consumed) def.valueUnused = false;
1150
+ else if (def.valueUnused !== false) def.valueUnused = true;
1151
+ };
1152
+ const visit = (c, consumed) => {
1153
+ const seen = consumed ? seenConsumed : seenUnconsumed;
1154
+ if (seen.has(c)) return;
1155
+ seen.add(c);
1156
+ const def = c._def;
1157
+ switch (def.tag) {
1158
+ case "many":
1159
+ case "oneOrMore":
1160
+ mark(def, consumed);
1161
+ visit(def.parser, consumed);
1162
+ return;
1163
+ case "optional":
1164
+ visit(def.parser, consumed);
1165
+ return;
1166
+ case "sequence":
1167
+ mark(def, consumed);
1168
+ for (const p of def.parsers) visit(p, consumed);
1169
+ return;
1170
+ case "choice":
1171
+ for (const p of def.parsers) visit(p, consumed);
1172
+ return;
1173
+ case "node":
1174
+ visit(def.parser, false);
1175
+ return;
1176
+ case "transform":
1177
+ visit(def.parser, true);
1178
+ return;
1179
+ case "skip":
1180
+ visit(def.main, consumed);
1181
+ visit(def.skipped, false);
1182
+ return;
1183
+ case "sepBy":
1184
+ visit(def.parser, true);
1185
+ visit(def.separator, false);
1186
+ return;
1187
+ case "not":
1188
+ visit(def.parser, false);
1189
+ return;
1190
+ case "grammar":
1191
+ visit(def.parser, consumed);
1192
+ if (def.triviaParser) visit(def.triviaParser, false);
1193
+ return;
1194
+ case "recover":
1195
+ visit(def.parser, consumed);
1196
+ visit(def.sentinel, false);
1197
+ return;
1198
+ case "label":
1199
+ case "trivia":
1200
+ case "withCtx":
1201
+ case "expect":
1202
+ visit(def.parser, consumed);
1203
+ return;
1204
+ case "lazy":
1205
+ return;
1206
+ default:
1207
+ return;
1208
+ }
1209
+ };
1210
+ const resolveRoot = (c) => {
1211
+ const guard2 = /* @__PURE__ */ new Set();
1212
+ let cur = c;
1213
+ while (!guard2.has(cur)) {
1214
+ const d = cur._def;
1215
+ if (d.tag !== "lazy") return cur;
1216
+ guard2.add(cur);
1217
+ try {
1218
+ cur = d.thunk();
1219
+ } catch {
1220
+ return c;
1221
+ }
1222
+ }
1223
+ return cur;
1224
+ };
1225
+ visit(resolveRoot(root), true);
1226
+ }
1227
+
1081
1228
  // src/combinators/not.ts
1082
1229
  function not(combinator) {
1083
1230
  const meta = {
@@ -3216,6 +3363,7 @@ function emitSeqValues(def, ctx, pos) {
3216
3363
  }
3217
3364
  function emitSeq(def, ctx, pos) {
3218
3365
  const { stmts, endVar, valueVars } = emitSeqValues(def, ctx, pos);
3366
+ if (def.valueUnused) return { stmts, valueVar: "undefined", endVar };
3219
3367
  const arrV = v(ctx, "_arr");
3220
3368
  stmts.push(`${ind(ctx)}const ${arrV} = [${valueVars.join(", ")}]`);
3221
3369
  return { stmts, valueVar: arrV, endVar };
@@ -3422,7 +3570,8 @@ function emitFirstMatch(def, ctx, pos) {
3422
3570
  const autoNot = def.autoNot[i];
3423
3571
  const atStart = failsAtStart(p);
3424
3572
  const staticFx = armStaticExpected(ctx, p);
3425
- const fsGuard = needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3573
+ const deferRuleName = ctx.deferFirstSetRefs ? p._ruleName : void 0;
3574
+ const fsGuard = deferRuleName !== void 0 ? `/*@FS:${deferRuleName}:${codeV}@*/true` : needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3426
3575
  let gateCond = null;
3427
3576
  if (gate) {
3428
3577
  const gateIdx = ctx.mapFns.length;
@@ -3513,19 +3662,18 @@ function emitLiteralCondition(litVal, pos) {
3513
3662
  return checks.join(" && ");
3514
3663
  }
3515
3664
  function emitMany(def, ctx, pos) {
3665
+ const wantValue = !def.valueUnused;
3516
3666
  const arrV = v(ctx, "_arr");
3517
3667
  const curV = v(ctx, "_cur");
3518
3668
  const stmts = [
3519
- `${ind(ctx)}const ${arrV} = []`,
3669
+ ...wantValue ? [`${ind(ctx)}const ${arrV} = []`] : [],
3520
3670
  `${ind(ctx)}let ${curV} = ${pos}`
3521
3671
  ];
3522
3672
  if (def.min === 1) {
3523
3673
  const firstR = emit(def.parser, ctx, curV);
3524
3674
  stmts.push(...firstR.stmts);
3525
- stmts.push(
3526
- `${ind(ctx)}${arrV}.push(${firstR.valueVar})`,
3527
- `${ind(ctx)}${curV} = ${firstR.endVar}`
3528
- );
3675
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
3676
+ stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
3529
3677
  }
3530
3678
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
3531
3679
  ctx.indent++;
@@ -3556,13 +3704,13 @@ function emitMany(def, ctx, pos) {
3556
3704
  const { stmts: iterStmts, okVar: iterOk, valVar: iterVal, endVar: iterEnd } = emitFallible(def.parser, ctx, itemPos, true);
3557
3705
  stmts.push(
3558
3706
  ...iterStmts,
3559
- `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`,
3560
- `${ind(ctx)}${arrV}.push(${iterVal})`,
3561
- `${ind(ctx)}${curV} = ${iterEnd}`
3707
+ `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`
3562
3708
  );
3709
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
3710
+ stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
3563
3711
  ctx.indent--;
3564
3712
  stmts.push(`${ind(ctx)}}`);
3565
- return { stmts, valueVar: arrV, endVar: curV };
3713
+ return { stmts, valueVar: wantValue ? arrV : "undefined", endVar: curV };
3566
3714
  }
3567
3715
  function emitOptional(def, ctx, pos) {
3568
3716
  const valV = v(ctx, "_opt");
@@ -4172,6 +4320,7 @@ function ruleDependencies(ruleMap) {
4172
4320
  return deps;
4173
4321
  }
4174
4322
  function compile(combinator, mapFnSources) {
4323
+ markUnusedValues(combinator);
4175
4324
  const ctx = {
4176
4325
  vars: 0,
4177
4326
  indent: 1,
@@ -4254,7 +4403,16 @@ function compile(combinator, mapFnSources) {
4254
4403
  }
4255
4404
  };
4256
4405
  }
4406
+ var RULE_NAME_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
4407
+ function assertRuleName(name) {
4408
+ if (!RULE_NAME_RE.test(name)) {
4409
+ throw new Error(
4410
+ `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).`
4411
+ );
4412
+ }
4413
+ }
4257
4414
  function compileRuleMap(ruleMap) {
4415
+ for (const [, rule] of ruleMap) markUnusedValues(rule);
4258
4416
  const ctx = {
4259
4417
  vars: 0,
4260
4418
  indent: 1,
@@ -4279,7 +4437,8 @@ function compileRuleMap(ruleMap) {
4279
4437
  const seenNames = /* @__PURE__ */ new Map();
4280
4438
  const ruleNames = /* @__PURE__ */ new Map();
4281
4439
  for (const [key, rule] of ruleMap) {
4282
- const base = `_r_${key.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4440
+ assertRuleName(key);
4441
+ const base = `_r_${key}`;
4283
4442
  const n = seenNames.get(base) ?? 0;
4284
4443
  seenNames.set(base, n + 1);
4285
4444
  ruleNames.set(rule, n === 0 ? base : `${base}$${n}`);
@@ -4330,6 +4489,7 @@ function compileRuleMap(ruleMap) {
4330
4489
  }
4331
4490
  function compileLinkable(ruleMapArg, ns) {
4332
4491
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
4492
+ for (const [, rule] of ruleMapArg) markUnusedValues(rule);
4333
4493
  const ruleMap = ruleMapArg.filter(([, val]) => {
4334
4494
  const d = val._def;
4335
4495
  if (d.tag !== "lazy") return true;
@@ -4360,13 +4520,15 @@ function compileLinkable(ruleMapArg, ns) {
4360
4520
  namedFnDecls: [],
4361
4521
  capturing: ruleMap.some(([, rule]) => hasNodeDef(rule)),
4362
4522
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
4363
- ns
4523
+ ns,
4524
+ deferFirstSetRefs: true
4364
4525
  };
4365
4526
  const seen = /* @__PURE__ */ new Map();
4366
4527
  const ruleNames = /* @__PURE__ */ new Map();
4367
4528
  const fnNameToKey = /* @__PURE__ */ new Map();
4368
4529
  for (const [key, rule] of ruleMap) {
4369
- const base = `_r_${key.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4530
+ assertRuleName(key);
4531
+ const base = `_r_${key}`;
4370
4532
  const n = seen.get(base) ?? 0;
4371
4533
  seen.set(base, n + 1);
4372
4534
  const fn = n === 0 ? base : `${base}$${n}`;
@@ -4390,7 +4552,8 @@ function compileLinkable(ruleMapArg, ns) {
4390
4552
  if (resolved === void 0) {
4391
4553
  const name = p._ruleName;
4392
4554
  if (name && !ctx.namedParsers.has(p)) {
4393
- const fn = `_r_${name.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4555
+ assertRuleName(name);
4556
+ const fn = `_r_${name}`;
4394
4557
  ruleNames.set(p, fn);
4395
4558
  ctx.namedParsers.set(p, fn);
4396
4559
  }
@@ -4459,12 +4622,26 @@ function compileLinkable(ruleMapArg, ns) {
4459
4622
  `}`
4460
4623
  ].join("\n"));
4461
4624
  }
4625
+ const firstSets = /* @__PURE__ */ new Map();
4626
+ for (const [key, rule] of ruleMap) {
4627
+ let resolved = rule;
4628
+ const d = rule._def;
4629
+ if (d.tag === "lazy") {
4630
+ try {
4631
+ resolved = d.thunk();
4632
+ } catch {
4633
+ resolved = rule;
4634
+ }
4635
+ }
4636
+ firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
4637
+ }
4462
4638
  return {
4463
4639
  ns,
4464
4640
  keys: perEntry.map((e) => e.key),
4465
4641
  prelude,
4466
4642
  ruleFns,
4467
4643
  wrappers,
4644
+ firstSets,
4468
4645
  deps: ruleDependencies(ruleMap),
4469
4646
  needsEmptyTl: !!ctx.needsEmptyTl,
4470
4647
  needsCollator: ctx.needsCollator,
@@ -4534,7 +4711,23 @@ function fusedBody(pieces) {
4534
4711
  ...[...winner].map(([k, p]) => p.ruleFns.get(k))
4535
4712
  ];
4536
4713
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
4537
- const body = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4714
+ const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4715
+ const finalFS = /* @__PURE__ */ new Map();
4716
+ for (const [k, p] of winner) {
4717
+ const fs2 = p.firstSets?.get(k);
4718
+ if (fs2) finalFS.set(k, fs2);
4719
+ }
4720
+ const body = rawBody.replace(
4721
+ // Rule name + code-point var are both JS identifiers (rule names are validated
4722
+ // at compile time — see assertRuleName in codegen), so an identifier class
4723
+ // matches every well-formed placeholder.
4724
+ /\/\*@FS:([A-Za-z0-9_$]+):([A-Za-z0-9_$]+)@\*\/true/g,
4725
+ (_m, name, codevar) => {
4726
+ const fs2 = finalFS.get(name);
4727
+ if (!fs2 || fs2.kind === "any" || fs2.kind === "empty") return "true";
4728
+ return `(${firstSetCond(codevar, fs2)})`;
4729
+ }
4730
+ );
4538
4731
  const env = {};
4539
4732
  for (const p of contributing) {
4540
4733
  if (p.mfFns.length) env[`${p.ns}mf`] = p.mfFns;
@@ -5314,7 +5507,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
5314
5507
  const nsFor = (label2) => `_${createHash("sha1").update(`${id}#${label2}`).digest("hex").slice(0, 8)}_`;
5315
5508
  const serializePieces = (p) => {
5316
5509
  const mapLit = (m) => `new Map([${[...m].map(([k, v2]) => `[${JSON.stringify(k)}, ${JSON.stringify(v2)}]`).join(", ")}])`;
5317
- return `{ ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude)}, ruleFns: ${mapLit(p.ruleFns)}, wrappers: ${mapLit(p.wrappers)}, deps: ${mapLit(p.deps)}, needsEmptyTl: ${p.needsEmptyTl}, needsCollator: ${p.needsCollator}, mfFns: [], buildFns: [] }`;
5510
+ return `{ ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude)}, ruleFns: ${mapLit(p.ruleFns)}, wrappers: ${mapLit(p.wrappers)}, firstSets: ${mapLit(p.firstSets)}, deps: ${mapLit(p.deps)}, needsEmptyTl: ${p.needsEmptyTl}, needsCollator: ${p.needsCollator}, mfFns: [], buildFns: [] }`;
5318
5511
  };
5319
5512
  const serializeList = (list) => `[${list.map(serializePieces).join(", ")}]`;
5320
5513
  const withCarriedPieces = (grammarExpr, list) => `/* @__PURE__ */ (() => { const _g = ${grammarExpr}; Object.defineProperty(_g, Symbol.for('parseman.composedPieces'), { value: ${serializeList(list)} }); return _g })()`;