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.cjs CHANGED
@@ -124,6 +124,63 @@ function any() {
124
124
  function empty() {
125
125
  return { kind: "empty" };
126
126
  }
127
+ function matchesEmpty(p, seen = /* @__PURE__ */ new Set()) {
128
+ if (seen.has(p)) return true;
129
+ seen.add(p);
130
+ const me = (c) => matchesEmpty(c, seen);
131
+ const d = p._def;
132
+ switch (d.tag) {
133
+ case "literal":
134
+ return d.value.length === 0;
135
+ case "keywords":
136
+ return false;
137
+ case "regex":
138
+ try {
139
+ const m = new RegExp(d.source).exec("");
140
+ return m != null && m[0] === "";
141
+ } catch {
142
+ return true;
143
+ }
144
+ case "many":
145
+ case "optional":
146
+ case "not":
147
+ return true;
148
+ // zero repetitions / absent / lookahead
149
+ case "oneOrMore":
150
+ return me(d.parser);
151
+ case "sequence":
152
+ return d.parsers.every(me);
153
+ case "choice":
154
+ return d.parsers.some(me);
155
+ case "transform":
156
+ case "label":
157
+ case "trivia":
158
+ case "expect":
159
+ case "withCtx":
160
+ case "node":
161
+ case "grammar":
162
+ case "recover":
163
+ return me(d.parser);
164
+ case "skip":
165
+ return me(d.main);
166
+ case "lazy":
167
+ try {
168
+ return me(d.thunk());
169
+ } catch {
170
+ return true;
171
+ }
172
+ default:
173
+ return true;
174
+ }
175
+ }
176
+ function sequenceFirstSet(parsers) {
177
+ let fs = empty();
178
+ for (const p of parsers) {
179
+ fs = union(fs, p._meta.firstSet);
180
+ if (!matchesEmpty(p)) return fs;
181
+ }
182
+ return fs;
183
+ }
127
184
  function mergeRanges(ranges) {
128
185
  if (ranges.length === 0) return [];
129
186
  const sorted = [...ranges].sort((a, b) => a.lo - b.lo);
@@ -945,16 +1002,19 @@ function scanTrivia(input, cur, ctx) {
945
1002
  // src/combinators/sequence.ts
946
1003
  function sequence(...parsers) {
947
1004
  const meta = {
948
- firstSet: parsers[0]?._meta.firstSet ?? empty(),
1005
+ // Union through the nullable prefix — a leading optional()/many() lets a later
1006
+ // term's first char start the sequence. Just `parsers[0]` under-approximates.
1007
+ firstSet: sequenceFirstSet(parsers),
949
1008
  canMatchNewline: parsers.some((p) => p._meta.canMatchNewline),
950
1009
  isTrivia: false
951
1010
  };
1011
+ const def = { tag: "sequence", parsers };
952
1012
  return {
953
1013
  _tag: "sequence",
954
1014
  _meta: meta,
955
- _def: { tag: "sequence", parsers },
1015
+ _def: def,
956
1016
  parse(input, pos, ctx) {
957
- const values = [];
1017
+ const values = def.valueUnused ? void 0 : [];
958
1018
  let cur = pos;
959
1019
  for (let i = 0; i < parsers.length; i++) {
960
1020
  if (ctx.trivia && i > 0) {
@@ -974,17 +1034,18 @@ function sequence(...parsers) {
974
1034
  } else {
975
1035
  rollbackTrivia(ctx, mark);
976
1036
  }
977
- values.push(result2.value);
1037
+ if (values !== void 0) values.push(result2.value);
978
1038
  continue;
979
1039
  }
980
1040
  const result = parsers[i].parse(input, cur, ctx);
981
1041
  if (!result.ok) return result;
982
- values.push(result.value);
1042
+ if (values !== void 0) values.push(result.value);
983
1043
  cur = result.span.end;
984
1044
  }
985
1045
  return {
986
1046
  ok: true,
987
- value: values,
1047
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
1048
+ value: values ?? void 0,
988
1049
  span: { start: pos, end: cur }
989
1050
  };
990
1051
  }
@@ -1025,20 +1086,21 @@ function many(combinator) {
1025
1086
  canMatchNewline: combinator._meta.canMatchNewline,
1026
1087
  isTrivia: false
1027
1088
  };
1089
+ const def = { tag: "many", parser: combinator, min: 0 };
1028
1090
  return {
1029
1091
  _tag: "many",
1030
1092
  _meta: meta,
1031
- _def: { tag: "many", parser: combinator, min: 0 },
1093
+ _def: def,
1032
1094
  parse(input, pos, ctx) {
1033
- const values = [];
1095
+ const values = def.valueUnused ? void 0 : [];
1034
1096
  let cur = pos;
1035
1097
  while (cur < input.length) {
1036
1098
  const item = repItem(combinator, input, cur, ctx);
1037
1099
  if (item === "stop" || "fail" in item) break;
1038
- values.push(item.value);
1100
+ if (values !== void 0) values.push(item.value);
1039
1101
  cur = item.end;
1040
1102
  }
1041
- return { ok: true, value: values, span: { start: pos, end: cur } };
1103
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1042
1104
  }
1043
1105
  };
1044
1106
  }
@@ -1048,22 +1110,23 @@ function oneOrMore(combinator) {
1048
1110
  canMatchNewline: combinator._meta.canMatchNewline,
1049
1111
  isTrivia: false
1050
1112
  };
1113
+ const def = { tag: "oneOrMore", parser: combinator, min: 1 };
1051
1114
  return {
1052
1115
  _tag: "oneOrMore",
1053
1116
  _meta: meta,
1054
- _def: { tag: "oneOrMore", parser: combinator, min: 1 },
1117
+ _def: def,
1055
1118
  parse(input, pos, ctx) {
1056
1119
  const first = combinator.parse(input, pos, ctx);
1057
1120
  if (!first.ok) return first;
1058
- const values = [first.value];
1121
+ const values = def.valueUnused ? void 0 : [first.value];
1059
1122
  let cur = first.span.end;
1060
1123
  while (cur < input.length) {
1061
1124
  const item = repItem(combinator, input, cur, ctx);
1062
1125
  if (item === "stop" || "fail" in item) break;
1063
- values.push(item.value);
1126
+ if (values !== void 0) values.push(item.value);
1064
1127
  cur = item.end;
1065
1128
  }
1066
- return { ok: true, value: values, span: { start: pos, end: cur } };
1129
+ return { ok: true, value: values ?? void 0, span: { start: pos, end: cur } };
1067
1130
  }
1068
1131
  };
1069
1132
  }
@@ -1171,6 +1234,90 @@ function ref() {
1171
1234
  return slot;
1172
1235
  }
1173
1236
 
1237
+ // src/compiler/value-usage.ts
1238
+ function markUnusedValues(root) {
1239
+ const seenConsumed = /* @__PURE__ */ new WeakSet();
1240
+ const seenUnconsumed = /* @__PURE__ */ new WeakSet();
1241
+ const mark = (def, consumed) => {
1242
+ if (consumed) def.valueUnused = false;
1243
+ else if (def.valueUnused !== false) def.valueUnused = true;
1244
+ };
1245
+ const visit = (c, consumed) => {
1246
+ const seen = consumed ? seenConsumed : seenUnconsumed;
1247
+ if (seen.has(c)) return;
1248
+ seen.add(c);
1249
+ const def = c._def;
1250
+ switch (def.tag) {
1251
+ case "many":
1252
+ case "oneOrMore":
1253
+ mark(def, consumed);
1254
+ visit(def.parser, consumed);
1255
+ return;
1256
+ case "optional":
1257
+ visit(def.parser, consumed);
1258
+ return;
1259
+ case "sequence":
1260
+ mark(def, consumed);
1261
+ for (const p of def.parsers) visit(p, consumed);
1262
+ return;
1263
+ case "choice":
1264
+ for (const p of def.parsers) visit(p, consumed);
1265
+ return;
1266
+ case "node":
1267
+ visit(def.parser, false);
1268
+ return;
1269
+ case "transform":
1270
+ visit(def.parser, true);
1271
+ return;
1272
+ case "skip":
1273
+ visit(def.main, consumed);
1274
+ visit(def.skipped, false);
1275
+ return;
1276
+ case "sepBy":
1277
+ visit(def.parser, true);
1278
+ visit(def.separator, false);
1279
+ return;
1280
+ case "not":
1281
+ visit(def.parser, false);
1282
+ return;
1283
+ case "grammar":
1284
+ visit(def.parser, consumed);
1285
+ if (def.triviaParser) visit(def.triviaParser, false);
1286
+ return;
1287
+ case "recover":
1288
+ visit(def.parser, consumed);
1289
+ visit(def.sentinel, false);
1290
+ return;
1291
+ case "label":
1292
+ case "trivia":
1293
+ case "withCtx":
1294
+ case "expect":
1295
+ visit(def.parser, consumed);
1296
+ return;
1297
+ case "lazy":
1298
+ return;
1299
+ default:
1300
+ return;
1301
+ }
1302
+ };
1303
+ const resolveRoot = (c) => {
1304
+ const guard2 = /* @__PURE__ */ new Set();
1305
+ let cur = c;
1306
+ while (!guard2.has(cur)) {
1307
+ const d = cur._def;
1308
+ if (d.tag !== "lazy") return cur;
1309
+ guard2.add(cur);
1310
+ try {
1311
+ cur = d.thunk();
1312
+ } catch {
1313
+ return c;
1314
+ }
1315
+ }
1316
+ return cur;
1317
+ };
1318
+ visit(resolveRoot(root), true);
1319
+ }
1320
+
1174
1321
  // src/combinators/parser.ts
1175
1322
  function rules(factory) {
1176
1323
  const cache = {};
@@ -1200,6 +1347,9 @@ function rules(factory) {
1200
1347
  cache[key] = parser2;
1201
1348
  }
1202
1349
  }
1350
+ for (const key of Object.keys(definitions)) {
1351
+ markUnusedValues(cache[key]);
1352
+ }
1203
1353
  return cache;
1204
1354
  }
1205
1355
 
@@ -1416,7 +1566,12 @@ function parser(opts, root) {
1416
1566
  function noTrivia(root) {
1417
1567
  return parser({ trivia: null }, root);
1418
1568
  }
1569
+ var _analyzed = /* @__PURE__ */ new WeakSet();
1419
1570
  function parse(combinator, input, opts = {}) {
1571
+ if (!_analyzed.has(combinator)) {
1572
+ _analyzed.add(combinator);
1573
+ markUnusedValues(combinator);
1574
+ }
1420
1575
  const trackLines = opts.trackLines ?? false;
1421
1576
  const _errors = opts.recover ? [] : void 0;
1422
1577
  const _probe = opts.recover ? { offset: input.length, best: null } : void 0;
@@ -3373,6 +3528,7 @@ function emitSeqValues(def, ctx, pos) {
3373
3528
  }
3374
3529
  function emitSeq(def, ctx, pos) {
3375
3530
  const { stmts, endVar, valueVars } = emitSeqValues(def, ctx, pos);
3531
+ if (def.valueUnused) return { stmts, valueVar: "undefined", endVar };
3376
3532
  const arrV = v(ctx, "_arr");
3377
3533
  stmts.push(`${ind(ctx)}const ${arrV} = [${valueVars.join(", ")}]`);
3378
3534
  return { stmts, valueVar: arrV, endVar };
@@ -3579,7 +3735,8 @@ function emitFirstMatch(def, ctx, pos) {
3579
3735
  const autoNot = def.autoNot[i];
3580
3736
  const atStart = failsAtStart(p);
3581
3737
  const staticFx = armStaticExpected(ctx, p);
3582
- const fsGuard = needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3738
+ const deferRuleName = ctx.deferFirstSetRefs ? p._ruleName : void 0;
3739
+ const fsGuard = deferRuleName !== void 0 ? `/*@FS:${deferRuleName}:${codeV}@*/true` : needsFirstSetGuard(p) ? firstSetCond(codeV, p._meta.firstSet) : null;
3583
3740
  let gateCond = null;
3584
3741
  if (gate) {
3585
3742
  const gateIdx = ctx.mapFns.length;
@@ -3670,19 +3827,18 @@ function emitLiteralCondition(litVal, pos) {
3670
3827
  return checks.join(" && ");
3671
3828
  }
3672
3829
  function emitMany(def, ctx, pos) {
3830
+ const wantValue = !def.valueUnused;
3673
3831
  const arrV = v(ctx, "_arr");
3674
3832
  const curV = v(ctx, "_cur");
3675
3833
  const stmts = [
3676
- `${ind(ctx)}const ${arrV} = []`,
3834
+ ...wantValue ? [`${ind(ctx)}const ${arrV} = []`] : [],
3677
3835
  `${ind(ctx)}let ${curV} = ${pos}`
3678
3836
  ];
3679
3837
  if (def.min === 1) {
3680
3838
  const firstR = emit(def.parser, ctx, curV);
3681
3839
  stmts.push(...firstR.stmts);
3682
- stmts.push(
3683
- `${ind(ctx)}${arrV}.push(${firstR.valueVar})`,
3684
- `${ind(ctx)}${curV} = ${firstR.endVar}`
3685
- );
3840
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${firstR.valueVar})`);
3841
+ stmts.push(`${ind(ctx)}${curV} = ${firstR.endVar}`);
3686
3842
  }
3687
3843
  stmts.push(`${ind(ctx)}while (${curV} < input.length) {`);
3688
3844
  ctx.indent++;
@@ -3713,13 +3869,13 @@ function emitMany(def, ctx, pos) {
3713
3869
  const { stmts: iterStmts, okVar: iterOk, valVar: iterVal, endVar: iterEnd } = emitFallible(def.parser, ctx, itemPos, true);
3714
3870
  stmts.push(
3715
3871
  ...iterStmts,
3716
- `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`,
3717
- `${ind(ctx)}${arrV}.push(${iterVal})`,
3718
- `${ind(ctx)}${curV} = ${iterEnd}`
3872
+ `${ind(ctx)}if (!${iterOk} || ${iterEnd} <= ${itemPos}) { ${rollback}break }`
3719
3873
  );
3874
+ if (wantValue) stmts.push(`${ind(ctx)}${arrV}.push(${iterVal})`);
3875
+ stmts.push(`${ind(ctx)}${curV} = ${iterEnd}`);
3720
3876
  ctx.indent--;
3721
3877
  stmts.push(`${ind(ctx)}}`);
3722
- return { stmts, valueVar: arrV, endVar: curV };
3878
+ return { stmts, valueVar: wantValue ? arrV : "undefined", endVar: curV };
3723
3879
  }
3724
3880
  function emitOptional(def, ctx, pos) {
3725
3881
  const valV = v(ctx, "_opt");
@@ -4329,6 +4485,7 @@ function ruleDependencies(ruleMap) {
4329
4485
  return deps;
4330
4486
  }
4331
4487
  function compile(combinator, mapFnSources) {
4488
+ markUnusedValues(combinator);
4332
4489
  const ctx = {
4333
4490
  vars: 0,
4334
4491
  indent: 1,
@@ -4411,8 +4568,17 @@ function compile(combinator, mapFnSources) {
4411
4568
  }
4412
4569
  };
4413
4570
  }
4571
+ var RULE_NAME_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
4572
+ function assertRuleName(name) {
4573
+ if (!RULE_NAME_RE.test(name)) {
4574
+ throw new Error(
4575
+ `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).`
4576
+ );
4577
+ }
4578
+ }
4414
4579
  function compileLinkable(ruleMapArg, ns) {
4415
4580
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
4581
+ for (const [, rule] of ruleMapArg) markUnusedValues(rule);
4416
4582
  const ruleMap = ruleMapArg.filter(([, val]) => {
4417
4583
  const d = val._def;
4418
4584
  if (d.tag !== "lazy") return true;
@@ -4443,13 +4609,15 @@ function compileLinkable(ruleMapArg, ns) {
4443
4609
  namedFnDecls: [],
4444
4610
  capturing: ruleMap.some(([, rule]) => hasNodeDef(rule)),
4445
4611
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
4446
- ns
4612
+ ns,
4613
+ deferFirstSetRefs: true
4447
4614
  };
4448
4615
  const seen = /* @__PURE__ */ new Map();
4449
4616
  const ruleNames = /* @__PURE__ */ new Map();
4450
4617
  const fnNameToKey = /* @__PURE__ */ new Map();
4451
4618
  for (const [key, rule] of ruleMap) {
4452
- const base = `_r_${key.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4619
+ assertRuleName(key);
4620
+ const base = `_r_${key}`;
4453
4621
  const n = seen.get(base) ?? 0;
4454
4622
  seen.set(base, n + 1);
4455
4623
  const fn = n === 0 ? base : `${base}$${n}`;
@@ -4473,7 +4641,8 @@ function compileLinkable(ruleMapArg, ns) {
4473
4641
  if (resolved === void 0) {
4474
4642
  const name = p._ruleName;
4475
4643
  if (name && !ctx.namedParsers.has(p)) {
4476
- const fn = `_r_${name.replace(/[^A-Za-z0-9_$]/g, "_")}`;
4644
+ assertRuleName(name);
4645
+ const fn = `_r_${name}`;
4477
4646
  ruleNames.set(p, fn);
4478
4647
  ctx.namedParsers.set(p, fn);
4479
4648
  }
@@ -4542,12 +4711,26 @@ function compileLinkable(ruleMapArg, ns) {
4542
4711
  `}`
4543
4712
  ].join("\n"));
4544
4713
  }
4714
+ const firstSets = /* @__PURE__ */ new Map();
4715
+ for (const [key, rule] of ruleMap) {
4716
+ let resolved = rule;
4717
+ const d = rule._def;
4718
+ if (d.tag === "lazy") {
4719
+ try {
4720
+ resolved = d.thunk();
4721
+ } catch {
4722
+ resolved = rule;
4723
+ }
4724
+ }
4725
+ firstSets.set(key, canMatchEmptyAtStart(resolved) ? { kind: "any" } : resolved._meta.firstSet);
4726
+ }
4545
4727
  return {
4546
4728
  ns,
4547
4729
  keys: perEntry.map((e) => e.key),
4548
4730
  prelude,
4549
4731
  ruleFns,
4550
4732
  wrappers,
4733
+ firstSets,
4551
4734
  deps: ruleDependencies(ruleMap),
4552
4735
  needsEmptyTl: !!ctx.needsEmptyTl,
4553
4736
  needsCollator: ctx.needsCollator,
@@ -4648,7 +4831,23 @@ function fusedBody(pieces) {
4648
4831
  ...[...winner].map(([k, p]) => p.ruleFns.get(k))
4649
4832
  ];
4650
4833
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
4651
- const body = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4834
+ const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
4835
+ const finalFS = /* @__PURE__ */ new Map();
4836
+ for (const [k, p] of winner) {
4837
+ const fs = p.firstSets?.get(k);
4838
+ if (fs) finalFS.set(k, fs);
4839
+ }
4840
+ const body = rawBody.replace(
4841
+ // Rule name + code-point var are both JS identifiers (rule names are validated
4842
+ // at compile time — see assertRuleName in codegen), so an identifier class
4843
+ // matches every well-formed placeholder.
4844
+ /\/\*@FS:([A-Za-z0-9_$]+):([A-Za-z0-9_$]+)@\*\/true/g,
4845
+ (_m, name, codevar) => {
4846
+ const fs = finalFS.get(name);
4847
+ if (!fs || fs.kind === "any" || fs.kind === "empty") return "true";
4848
+ return `(${firstSetCond(codevar, fs)})`;
4849
+ }
4850
+ );
4652
4851
  const env = {};
4653
4852
  for (const p of contributing) {
4654
4853
  if (p.mfFns.length) env[`${p.ns}mf`] = p.mfFns;