@tabnas/parser 0.2.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/rules.js ADDED
@@ -0,0 +1,923 @@
1
+ "use strict";
2
+ /* Copyright (c) 2013-2026 Richard Rodger, MIT License */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.makeRuleSpec = exports.makeNoRule = exports.makeRule = exports.AltMatch = exports.RuleSpec = exports.Rule = void 0;
5
+ const types_1 = require("./types");
6
+ const utility_1 = require("./utility");
7
+ const error_1 = require("./error");
8
+ // A single application of a RuleSpec on the parser stack; lives through an "open" then "close" pass.
9
+ class Rule {
10
+ constructor(spec, ctx, node) {
11
+ this.i = -1; // Unique rule id within this parse run.
12
+ this.name = types_1.EMPTY; // Rule name (matches its RuleSpec).
13
+ this.node = null; // Value node this rule is building.
14
+ this.state = types_1.OPEN; // Current phase: open ('o') or close ('c').
15
+ this.n = Object.create(null); // Named counters tracked across the rule.
16
+ this.d = -1; // Stack depth at which this rule was pushed.
17
+ this.u = Object.create(null); // Custom user props (not propagated to children).
18
+ this.k = Object.create(null); // Custom keep props (propagated via push/replace).
19
+ this.bo = false; // Has before-open actions.
20
+ this.ao = false; // Has after-open actions.
21
+ this.bc = false; // Has before-close actions.
22
+ this.ac = false; // Has after-close actions.
23
+ this.oN = 0; // Count of tokens matched in the open phase.
24
+ this.cN = 0; // Count of tokens matched in the close phase.
25
+ this.need = 0; // Reserved counter for grammar/plugin use.
26
+ this.i = ctx.uI++; // Rule ids are unique only to the parse run.
27
+ this.name = spec.name;
28
+ this.spec = spec;
29
+ this.child = ctx.NORULE;
30
+ this.parent = ctx.NORULE;
31
+ this.prev = ctx.NORULE;
32
+ this.next = ctx.NORULE;
33
+ this._NOTOKEN = ctx.NOTOKEN;
34
+ this.o = [];
35
+ this.c = [];
36
+ this.node = node;
37
+ this.d = ctx.rsI;
38
+ this.bo = null != spec.def.bo;
39
+ this.ao = null != spec.def.ao;
40
+ this.bc = null != spec.def.bc;
41
+ this.ac = null != spec.def.ac;
42
+ }
43
+ // Legacy aliases for o[0], o[1], c[0], c[1] and the count fields.
44
+ // Maintained so existing grammar/plugin code that reads r.o0/r.o1/r.os
45
+ // (and r.c0/r.c1/r.cs) continues to work unchanged.
46
+ get o0() { return this.o[0] ?? this._NOTOKEN; }
47
+ set o0(v) { this.o[0] = v; }
48
+ get o1() { return this.o[1] ?? this._NOTOKEN; }
49
+ set o1(v) { this.o[1] = v; }
50
+ get c0() { return this.c[0] ?? this._NOTOKEN; }
51
+ set c0(v) { this.c[0] = v; }
52
+ get c1() { return this.c[1] ?? this._NOTOKEN; }
53
+ set c1(v) { this.c[1] = v; }
54
+ get os() { return this.oN; }
55
+ set os(v) { this.oN = v; }
56
+ get cs() { return this.cN; }
57
+ set cs(v) { this.cN = v; }
58
+ process(ctx, lex) {
59
+ let rule = this.spec.process(this, ctx, lex, this.state);
60
+ return rule;
61
+ }
62
+ eq(counter, limit = 0) {
63
+ let value = this.n[counter];
64
+ return null == value || value === limit;
65
+ }
66
+ lt(counter, limit = 0) {
67
+ let value = this.n[counter];
68
+ return null == value || value < limit;
69
+ }
70
+ gt(counter, limit = 0) {
71
+ let value = this.n[counter];
72
+ return null == value || value > limit;
73
+ }
74
+ lte(counter, limit = 0) {
75
+ let value = this.n[counter];
76
+ return null == value || value <= limit;
77
+ }
78
+ gte(counter, limit = 0) {
79
+ let value = this.n[counter];
80
+ return null == value || value >= limit;
81
+ }
82
+ toString() {
83
+ return '[Rule ' + this.name + '~' + this.i + ']';
84
+ }
85
+ }
86
+ exports.Rule = Rule;
87
+ const makeRule = (...params) => new Rule(...params);
88
+ exports.makeRule = makeRule;
89
+ const makeNoRule = (j, ctx) => makeRule(makeRuleSpec(j, ctx.cfg, {}), ctx);
90
+ exports.makeNoRule = makeNoRule;
91
+ // Result of matching one parse alternate against the current tokens (built from current tokens and AltSpec).
92
+ class AltMatch {
93
+ constructor() {
94
+ this.p = types_1.EMPTY; // Push rule (by name).
95
+ this.r = types_1.EMPTY; // Replace rule (by name).
96
+ this.b = 0; // Backtrack: move token position backward.
97
+ }
98
+ }
99
+ exports.AltMatch = AltMatch;
100
+ const makeAltMatch = (...params) => new AltMatch(...params);
101
+ const PALT = makeAltMatch(); // Only one alt object is created.
102
+ const EMPTY_ALT = makeAltMatch();
103
+ // Reusable definition of a rule: its open/close alternates, lifecycle actions, and collated token lookups.
104
+ class RuleSpec {
105
+ constructor(j, cfg, def) {
106
+ this.name = types_1.EMPTY; // Rule name (set by Parser.rule).
107
+ this.def = {
108
+ open: [], // Open-phase alternates.
109
+ close: [], // Close-phase alternates.
110
+ bo: [], // Before-open actions.
111
+ bc: [], // Before-close actions.
112
+ ao: [], // After-open actions.
113
+ ac: [], // After-close actions.
114
+ tcol: [], // Collated lookahead tins: [stateI][tokenI][tins].
115
+ fnref: {}, // Named function references (@name handlers).
116
+ };
117
+ this.ji = j;
118
+ this.cfg = cfg;
119
+ this.def = Object.assign(this.def, def);
120
+ // Null Alt entries are allowed and ignored as a convenience.
121
+ this.def.open = (this.def.open || []).filter((alt) => null != alt);
122
+ this.def.close = (this.def.close || []).filter((alt) => null != alt);
123
+ for (let alt of this.def.open) {
124
+ normalt(alt, types_1.OPEN, this);
125
+ }
126
+ for (let alt of this.def.close) {
127
+ normalt(alt, types_1.CLOSE, this);
128
+ }
129
+ const anames = ['bo', 'ao', 'bc', 'ac'];
130
+ for (let an of anames) {
131
+ for (let sa of (this.def[an] ?? [])) {
132
+ if ('object' === typeof sa) {
133
+ let sadef = sa;
134
+ this[an](sadef.append, sadef.action);
135
+ }
136
+ }
137
+ }
138
+ }
139
+ // Convenience access to token Tins
140
+ tin(ref) {
141
+ return (0, utility_1.tokenize)(ref, this.cfg);
142
+ }
143
+ fnref(frm) {
144
+ Object.assign(this.def.fnref, frm);
145
+ // Auto-install reserved `@<rulename>-<phase>` handlers as state
146
+ // actions. Dedupe by function identity per phase: registering the
147
+ // same function twice (directly or via a later fnref() call that
148
+ // also passes it) installs only one action, but distinct functions
149
+ // for the same phase all install. This accommodates grammars that
150
+ // layer handlers (e.g. core sets a basic @list-bo, then a richer
151
+ // one replaces/augments it) while preventing the old bug where
152
+ // iterating the accumulated fnref map re-installed every
153
+ // previously-registered handler on every call.
154
+ const rn = this.name;
155
+ const fr = this.def.fnref;
156
+ const installed = (this.def.fnrefInstalled =
157
+ this.def.fnrefInstalled || new Map());
158
+ // Phases an `@<rule>-<phase>/replace` fnref has taken ownership of.
159
+ // Once a phase is replaced, the plain/prepend/append fnrefs for it are
160
+ // ignored so older handlers (still lingering in the accumulated fnref
161
+ // map) are not re-installed on subsequent fnref() calls or re-derivation.
162
+ const replaced = (this.def.fnrefReplaced =
163
+ this.def.fnrefReplaced || new Set());
164
+ const reserved = [`@${rn}-bo`, `@${rn}-ao`, `@${rn}-bc`, `@${rn}-ac`];
165
+ for (let base of reserved) {
166
+ let phaseSet = installed.get(base);
167
+ if (!phaseSet)
168
+ installed.set(base, phaseSet = new WeakSet());
169
+ const aname = base.replace(/^[^-]+-/, '');
170
+ // `/replace` clears all prior actions for this phase (from any
171
+ // plugin) and installs the replacement, then owns the phase.
172
+ const replaceFn = fr[base + '/replace'];
173
+ if (replaceFn) {
174
+ if (!replaced.has(base)) {
175
+ replaced.add(base);
176
+ this.def[aname].length = 0;
177
+ phaseSet = new WeakSet();
178
+ installed.set(base, phaseSet);
179
+ phaseSet.add(replaceFn);
180
+ this[aname](true, replaceFn);
181
+ }
182
+ continue; // phase owned by replace: skip prepend/append/plain
183
+ }
184
+ if (replaced.has(base))
185
+ continue;
186
+ const prependFn = fr[base + '/prepend'];
187
+ const appendFn = fr[base + '/append'] ?? fr[base];
188
+ if (prependFn && !phaseSet.has(prependFn)) {
189
+ phaseSet.add(prependFn);
190
+ this[aname](false, prependFn);
191
+ }
192
+ if (appendFn && !phaseSet.has(appendFn)) {
193
+ phaseSet.add(appendFn);
194
+ this[aname](true, appendFn);
195
+ }
196
+ }
197
+ return this;
198
+ }
199
+ add(rs, a, mods) {
200
+ let inject = mods?.append ? 'push' : 'unshift';
201
+ let aa = ((0, utility_1.isarr)(a) ? a : [a])
202
+ .filter((alt) => null != alt && 'object' === typeof alt)
203
+ .map((a) => normalt(a, rs, this));
204
+ let altState = 'o' === rs ? 'open' : 'close';
205
+ let alts = this.def[altState];
206
+ // `clear` empties the pre-existing alternates (from earlier plugins)
207
+ // before the new ones are injected — a later plugin can thus replace
208
+ // a rule's open/close alternates outright. Done before inject so the
209
+ // new alternates survive.
210
+ if (mods?.clear) {
211
+ alts.length = 0;
212
+ }
213
+ alts[inject](...aa);
214
+ alts = this.def[altState] = (0, utility_1.modlist)(alts, mods);
215
+ (0, utility_1.filterRules)(this, this.cfg);
216
+ this.norm();
217
+ return this;
218
+ }
219
+ open(a, mods) {
220
+ return this.add('o', a, mods);
221
+ }
222
+ close(a, mods) {
223
+ return this.add('c', a, mods);
224
+ }
225
+ action(append, step, state, action) {
226
+ let actions = this.def[step + state];
227
+ if (append) {
228
+ actions.push(action);
229
+ }
230
+ else {
231
+ actions.unshift(action);
232
+ }
233
+ return this;
234
+ }
235
+ bo(append, action) {
236
+ return this.action(action ? !!append : true, types_1.BEFORE, types_1.OPEN, 'string' === typeof append ? this.def.fnref[append] :
237
+ (action ?? append));
238
+ }
239
+ ao(append, action) {
240
+ return this.action(action ? !!append : true, types_1.AFTER, types_1.OPEN, 'string' === typeof append ? this.def.fnref[append] :
241
+ (action ?? append));
242
+ }
243
+ bc(append, action) {
244
+ return this.action(action ? !!append : true, types_1.BEFORE, types_1.CLOSE, 'string' === typeof append ? this.def.fnref[append] :
245
+ (action ?? append));
246
+ }
247
+ ac(append, action) {
248
+ return this.action(action ? !!append : true, types_1.AFTER, types_1.CLOSE, 'string' === typeof append ? this.def.fnref[append] :
249
+ (action ?? append));
250
+ }
251
+ clear() {
252
+ this.def.open.length = 0;
253
+ this.def.close.length = 0;
254
+ this.def.bo.length = 0;
255
+ this.def.ao.length = 0;
256
+ this.def.bc.length = 0;
257
+ this.def.ac.length = 0;
258
+ return this;
259
+ }
260
+ // Remove this rule's open alternates without touching close or the
261
+ // lifecycle actions. A later plugin can call this, then re-add, to
262
+ // replace the open alternates contributed by earlier plugins.
263
+ clearOpen() {
264
+ this.def.open.length = 0;
265
+ return this;
266
+ }
267
+ // Remove this rule's close alternates (see clearOpen).
268
+ clearClose() {
269
+ this.def.close.length = 0;
270
+ return this;
271
+ }
272
+ // Remove the registered lifecycle actions for the named phases (any of
273
+ // 'bo', 'ao', 'bc', 'ac'); with no arguments, clear all four. The
274
+ // fnref dedup/replace bookkeeping for those phases is reset too, so a
275
+ // subsequent fnref() re-installs cleanly. Alternates are untouched.
276
+ clearActions(...phases) {
277
+ const all = (0 < phases.length ? phases : ['bo', 'ao', 'bc', 'ac']);
278
+ const installed = this.def
279
+ .fnrefInstalled;
280
+ const replaced = this.def.fnrefReplaced;
281
+ for (const p of all) {
282
+ ;
283
+ this.def[p].length = 0;
284
+ const base = `@${this.name}-${p}`;
285
+ installed && installed.delete(base);
286
+ replaced && replaced.delete(base);
287
+ }
288
+ return this;
289
+ }
290
+ norm() {
291
+ this.def.open.map((alt) => normalt(alt, types_1.OPEN, this));
292
+ this.def.close.map((alt) => normalt(alt, types_1.CLOSE, this));
293
+ // [stateI is o=0,c=1][tokenI is 0..maxS-1][tins]
294
+ const columns = [];
295
+ // Compute max lookahead depth declared across this rule's alts,
296
+ // per state. Generalizes the previous hard-coded 2-slot collation.
297
+ const maxS = (alts) => alts.reduce((m, a) => Math.max(m, a.sN || 0), 0);
298
+ const maxOpen = maxS(this.def.open);
299
+ const maxClose = maxS(this.def.close);
300
+ for (let tI = 0; tI < maxOpen; tI++) {
301
+ this.def.open.reduce(...collate(0, tI, columns));
302
+ }
303
+ for (let tI = 0; tI < maxClose; tI++) {
304
+ this.def.close.reduce(...collate(1, tI, columns));
305
+ }
306
+ // Ensure tcol[stateI] exists with enough slots so the lexer's
307
+ // tcol gating can always index `tcol[oc][tI]` safely for any tI
308
+ // the parser passes (bounded by this rule's own maxS).
309
+ columns[0] = columns[0] || [];
310
+ columns[1] = columns[1] || [];
311
+ for (let tI = 0; tI < maxOpen; tI++)
312
+ columns[0][tI] = columns[0][tI] || [];
313
+ for (let tI = 0; tI < maxClose; tI++)
314
+ columns[1][tI] = columns[1][tI] || [];
315
+ this.def.tcol = columns;
316
+ function collate(stateI, tokenI, columns) {
317
+ columns[stateI] = columns[stateI] || [];
318
+ let tins = (columns[stateI][tokenI] = columns[stateI][tokenI] || []);
319
+ return [
320
+ function (tins, alt) {
321
+ let resolved = alt.t && alt.t[tokenI];
322
+ if (resolved && 0 < resolved.length) {
323
+ let newtins = [...new Set(tins.concat(resolved))];
324
+ tins.length = 0;
325
+ tins.push(...newtins);
326
+ }
327
+ return tins;
328
+ },
329
+ tins,
330
+ ];
331
+ }
332
+ return this;
333
+ }
334
+ process(rule, ctx, lex, state) {
335
+ ctx.log && ctx.log(utility_1.S.rule, ctx, rule, lex);
336
+ let is_open = state === 'o';
337
+ let next = is_open ? rule : ctx.NORULE;
338
+ let why = is_open ? 'O' : 'C';
339
+ let def = this.def;
340
+ // Match alternates for current state.
341
+ let alts = (is_open ? def.open : def.close);
342
+ // Handle "before" call.
343
+ let befores = is_open ? (rule.bo ? def.bo : null) : rule.bc ? def.bc : null;
344
+ if (befores) {
345
+ let bout = undefined;
346
+ for (let bI = 0; bI < befores.length; bI++) {
347
+ bout = befores[bI].call(this, rule, ctx, next, bout);
348
+ if (bout?.isToken && bout?.err) {
349
+ return this.bad(bout, rule, ctx, { is_open });
350
+ }
351
+ }
352
+ }
353
+ // Attempt to match one of the alts.
354
+ let alt = 0 < alts.length ? parse_alts(is_open, alts, lex, rule, ctx) : EMPTY_ALT;
355
+ // Custom alt handler.
356
+ if (alt.h) {
357
+ alt = alt.h(rule, ctx, alt, next) || alt;
358
+ why += 'H';
359
+ }
360
+ // Unconditional error.
361
+ if (alt.e) {
362
+ return this.bad(alt.e, rule, ctx, { is_open });
363
+ }
364
+ // Update counters.
365
+ if (alt.n) {
366
+ for (let cn in alt.n) {
367
+ rule.n[cn] =
368
+ // 0 reverts counter to 0.
369
+ 0 === alt.n[cn]
370
+ ? 0
371
+ : // First seen, set to 0.
372
+ (null == rule.n[cn]
373
+ ? 0
374
+ : // Increment counter.
375
+ rule.n[cn]) + alt.n[cn];
376
+ }
377
+ }
378
+ // Set custom properties
379
+ if (alt.u) {
380
+ rule.u = Object.assign(rule.u, alt.u);
381
+ }
382
+ if (alt.k) {
383
+ rule.k = Object.assign(rule.k, alt.k);
384
+ }
385
+ // Record consumed tokens (matched minus backtrack) on the v
386
+ // history BEFORE running alt actions, so an action that calls
387
+ // ctx.rewind sees the just-matched tokens on top of the stack.
388
+ // The lookahead-buffer shift itself still happens at the end of
389
+ // process() so non-action paths behave identically.
390
+ //
391
+ // ctx.vAbs is an absolute monotonic counter used as the mark
392
+ // value — it's decoupled from ctx.v.length so the ring-buffer
393
+ // cap can evict old tokens from the front without invalidating
394
+ // outstanding marks (marks older than the retained window will
395
+ // simply fail at rewind time with a clear error).
396
+ const _cons = rule[is_open ? 'oN' : 'cN'] - (alt.b || 0);
397
+ if (0 < _cons) {
398
+ // Move consumed tokens from ctx.t → ctx.v. Clear the tbuf slots
399
+ // so a ctx.rewind call inside the subsequent alt action can
400
+ // distinguish "token already in v" (NOTOKEN here; will be
401
+ // replayed from v) from "pre-lexed lookahead past consumed"
402
+ // (real token in tbuf; needs re-queuing to preserve state).
403
+ const NOTOKEN = ctx.NOTOKEN;
404
+ for (let i = 0; i < _cons; i++) {
405
+ ctx.v.push(ctx.t[i]);
406
+ ctx.t[i] = NOTOKEN;
407
+ }
408
+ ;
409
+ ctx.vAbs += _cons;
410
+ // Amortised-O(1) ring-buffer cap: let v grow to twice the
411
+ // capacity, then splice its front back down. Batch-eviction
412
+ // makes each push O(1) on average even at the cap.
413
+ const cap = ctx.cfg.rewind.history;
414
+ if (cap !== Infinity && ctx.v.length > 2 * cap) {
415
+ ctx.v.splice(0, ctx.v.length - cap);
416
+ }
417
+ }
418
+ // TODO: move after rule.next resolution
419
+ // (breaks Expr! - fix first)
420
+ // Action call.
421
+ if (alt.a) {
422
+ why += 'A';
423
+ let tout = alt.a(rule, ctx, alt);
424
+ if (tout && tout.isToken && tout.err) {
425
+ return this.bad(tout, rule, ctx, { is_open });
426
+ }
427
+ }
428
+ // Push a new rule onto the stack...
429
+ if (alt.p) {
430
+ ctx.rs[ctx.rsI++] = rule;
431
+ let rulespec = ctx.rsm[alt.p];
432
+ if (rulespec) {
433
+ next = rule.child = makeRule(rulespec, ctx, rule.node);
434
+ next.parent = rule;
435
+ next.n = { ...rule.n };
436
+ if (0 < Object.keys(rule.k).length) {
437
+ next.k = { ...rule.k };
438
+ }
439
+ why += 'P`' + alt.p + '`';
440
+ }
441
+ else {
442
+ return this.bad(this.unknownRule(ctx.t0, alt.p), rule, ctx, { is_open });
443
+ }
444
+ }
445
+ // ...or replace with a new rule.
446
+ else if (alt.r) {
447
+ let rulespec = ctx.rsm[alt.r];
448
+ if (rulespec) {
449
+ next = makeRule(rulespec, ctx, rule.node);
450
+ next.parent = rule.parent;
451
+ next.prev = rule;
452
+ next.n = { ...rule.n };
453
+ if (0 < Object.keys(rule.k).length) {
454
+ next.k = { ...rule.k };
455
+ }
456
+ why += 'R`' + alt.r + '`';
457
+ }
458
+ else {
459
+ return this.bad(this.unknownRule(ctx.t0, alt.r), rule, ctx, { is_open });
460
+ }
461
+ }
462
+ // Pop closed rule off stack.
463
+ else if (!is_open) {
464
+ next = ctx.rs[--ctx.rsI] || ctx.NORULE;
465
+ }
466
+ // TODO: move action call here (alt.a)
467
+ // and set r.next = next, so that action has access to next
468
+ rule.next = next;
469
+ // Handle "after" call.
470
+ let afters = is_open ? (rule.ao ? def.ao : null) : rule.ac ? def.ac : null;
471
+ if (afters) {
472
+ let aout = undefined;
473
+ for (let aI = 0; aI < afters.length; aI++) {
474
+ aout = afters[aI](rule, ctx, next, aout);
475
+ if (aout?.isToken && aout?.err) {
476
+ return this.bad(aout, rule, ctx, { is_open });
477
+ }
478
+ }
479
+ }
480
+ next.why = why;
481
+ ctx.log && ctx.log(utility_1.S.node, ctx, rule, lex, next);
482
+ // Must be last as state change is for next process call.
483
+ if (types_1.OPEN === rule.state) {
484
+ rule.state = types_1.CLOSE;
485
+ }
486
+ // Backtrack reduces consumed token count.
487
+ let consumed = rule[is_open ? 'oN' : 'cN'] - (alt.b || 0);
488
+ if (consumed < 0)
489
+ consumed = 0;
490
+ if (0 < consumed) {
491
+ // Shift the lookahead buffer left by `consumed` slots, filling
492
+ // vacated tail positions with NOTOKEN so later alts re-fetch.
493
+ // (The corresponding v-history push ran before alt actions.)
494
+ const L = ctx.t.length;
495
+ for (let i = 0; i < L - consumed; i++)
496
+ ctx.t[i] = ctx.t[i + consumed];
497
+ for (let i = Math.max(0, L - consumed); i < L; i++)
498
+ ctx.t[i] = ctx.NOTOKEN;
499
+ }
500
+ return next;
501
+ }
502
+ bad(tkn, rule, ctx, parse) {
503
+ throw new error_1.TabnasError(tkn.err || utility_1.S.unexpected, {
504
+ ...tkn.use,
505
+ state: parse.is_open ? utility_1.S.open : utility_1.S.close,
506
+ }, tkn, rule, ctx);
507
+ }
508
+ unknownRule(tkn, name) {
509
+ tkn.err = 'unknown_rule';
510
+ tkn.use = tkn.use || {};
511
+ tkn.use.rulename = name;
512
+ return tkn;
513
+ }
514
+ }
515
+ exports.RuleSpec = RuleSpec;
516
+ const makeRuleSpec = (...params) => new RuleSpec(...params);
517
+ exports.makeRuleSpec = makeRuleSpec;
518
+ // First match wins.
519
+ // NOTE: input AltSpecs are used to build the Alt output.
520
+ function parse_alts(is_open, alts, lex, rule, ctx) {
521
+ let out = PALT;
522
+ out.b = 0; // Backtrack n tokens.
523
+ out.p = types_1.EMPTY; // Push named rule onto stack.
524
+ out.r = types_1.EMPTY; // Replace current rule with named rule.
525
+ out.n = undefined; // Increment named counters.
526
+ out.h = undefined; // Custom handler function.
527
+ out.a = undefined; // Rule action.
528
+ out.u = undefined; // Custom rule properties.
529
+ out.k = undefined; // Custom rule properties (propagated).
530
+ out.e = undefined; // Error token.
531
+ let alt = null;
532
+ let altI = 0;
533
+ let t = ctx.cfg.t;
534
+ let cond = true;
535
+ let bitAA = 1 << (t.AA - 1);
536
+ let IGNORE = ctx.cfg.tokenSetTins.IGNORE;
537
+ function next(r, alt, altI, tI) {
538
+ let tkn;
539
+ do {
540
+ tkn = lex.next(r, alt, altI, tI);
541
+ ctx.tC++;
542
+ } while (IGNORE[tkn.tin]);
543
+ return tkn;
544
+ }
545
+ // TODO: replace with lookup map
546
+ let len = alts.length;
547
+ const NOTOKEN = ctx.NOTOKEN;
548
+ const tbuf = ctx.t;
549
+ for (altI = 0; altI < len; altI++) {
550
+ alt = alts[altI];
551
+ // Number of positions that matched in this alt. Tracked so the
552
+ // rule can record exactly which tokens it consumed.
553
+ let matched = 0;
554
+ cond = true;
555
+ const S = alt.S;
556
+ const sN = alt.sN | 0;
557
+ // Iterate alt's lookahead positions. Each position is fetched
558
+ // lazily and only when the previous position matched, preserving
559
+ // the original 2-slot lazy behaviour for any N.
560
+ //
561
+ // A null entry in S[i] means "no Tin constraint at this position"
562
+ // (wildcard) - the token is still fetched and consumed, but the
563
+ // bit-field check is skipped. This matches the `s` docstring
564
+ // ("null if position matches any token") and prevents silently
565
+ // dropping the check at a later required position.
566
+ for (let i = 0; i < sN; i++) {
567
+ let tkn = tbuf[i];
568
+ if (null == tkn || NOTOKEN === tkn) {
569
+ tkn = tbuf[i] = next(rule, alt, altI, i);
570
+ }
571
+ const Si = S ? S[i] : null;
572
+ if (null != Si) {
573
+ const tin = tkn.tin;
574
+ const part = (tin / 31) | 0;
575
+ // bitAA lives in partition 0 (tin=AA=4). ORing it into the
576
+ // match mask for any partition other than 0 lets unrelated
577
+ // tokens in higher partitions collide with alts that merely
578
+ // set bit 3 of their own partition — a false positive. Apply
579
+ // bitAA only when testing a partition-0 token.
580
+ const aaBit = part === 0 ? bitAA : 0;
581
+ if (!(Si[part] & ((1 << ((tin % 31) - 1)) | aaBit))) {
582
+ cond = false;
583
+ break;
584
+ }
585
+ }
586
+ matched = i + 1;
587
+ }
588
+ if (is_open) {
589
+ rule.oN = matched;
590
+ for (let i = 0; i < matched; i++)
591
+ rule.o[i] = tbuf[i];
592
+ // Clear trailing slots so stale matches from earlier alts are
593
+ // not observed via rule.o[i] / rule.o0 / rule.o1 accessors.
594
+ for (let i = matched; i < rule.o.length; i++)
595
+ rule.o[i] = NOTOKEN;
596
+ }
597
+ else {
598
+ rule.cN = matched;
599
+ for (let i = 0; i < matched; i++)
600
+ rule.c[i] = tbuf[i];
601
+ for (let i = matched; i < rule.c.length; i++)
602
+ rule.c[i] = NOTOKEN;
603
+ }
604
+ // Optional custom condition
605
+ if (cond && alt.c) {
606
+ cond = cond && alt.c(rule, ctx, out);
607
+ }
608
+ if (cond) {
609
+ break;
610
+ }
611
+ else {
612
+ alt = null;
613
+ }
614
+ }
615
+ if (!cond) {
616
+ out.e = tbuf[0] ?? NOTOKEN;
617
+ }
618
+ if (alt) {
619
+ out.n = null != alt.n ? alt.n : out.n;
620
+ out.h = null != alt.h ? alt.h : out.h;
621
+ out.a = null != alt.a ? alt.a : out.a;
622
+ out.u = null != alt.u ? alt.u : out.u;
623
+ out.k = null != alt.k ? alt.k : out.k;
624
+ out.g = null != alt.g ? alt.g : out.g;
625
+ out.e = (alt.e && alt.e(rule, ctx, out)) || undefined;
626
+ out.p =
627
+ null != alt.p && false !== alt.p
628
+ ? 'string' === typeof alt.p
629
+ ? alt.p
630
+ : alt.p(rule, ctx, out)
631
+ : out.p;
632
+ out.r =
633
+ null != alt.r && false !== alt.r
634
+ ? 'string' === typeof alt.r
635
+ ? alt.r
636
+ : alt.r(rule, ctx, out)
637
+ : out.r;
638
+ out.b =
639
+ null != alt.b && false !== alt.b
640
+ ? 'number' === typeof alt.b
641
+ ? alt.b
642
+ : alt.b(rule, ctx, out)
643
+ : out.b;
644
+ }
645
+ let match = altI < alts.length;
646
+ ctx.log && ctx.log(utility_1.S.parse, ctx, rule, lex, match, cond, altI, alt, out);
647
+ return out;
648
+ }
649
+ const partify = (tins, part) => tins.filter((tin) => 31 * part <= tin && tin < 31 * (part + 1));
650
+ const bitify = (s, part) => s.reduce((bits, tin) => (1 << (tin - (31 * part + 1))) | bits, 0);
651
+ // Valid group-tag pattern: lowercase letter followed by one or more
652
+ // lowercase letters, digits, or hyphens. Enforced by normalt().
653
+ const GROUP_TAG_RE = /^[a-z][a-z0-9-]+$/;
654
+ // Normalize AltSpec (mutates).
655
+ function normalt(a, rs, r) {
656
+ // Ensure groups are a string[]
657
+ if (types_1.STRING === typeof a.g) {
658
+ a.g = a.g.split(/\s*,\s*/);
659
+ }
660
+ else if (null == a.g) {
661
+ a.g = [];
662
+ }
663
+ // Validate every group tag (reject empty and non-matching tags).
664
+ for (let tag of a.g) {
665
+ if (!GROUP_TAG_RE.test(tag)) {
666
+ throw new Error(`Grammar: invalid group tag "${tag}" ` +
667
+ `in rule ${r.name} (${rs}) — must match ${GROUP_TAG_RE}`);
668
+ }
669
+ }
670
+ a.g = a.g.sort();
671
+ const aa = a;
672
+ if (!a.s || 0 === a.s.length) {
673
+ a.s = null;
674
+ aa.t = [];
675
+ aa.S = null;
676
+ aa.sN = 0;
677
+ }
678
+ else {
679
+ const tinsify = (s) => {
680
+ const tins = s
681
+ .flat()
682
+ .map((n) => 'string' === typeof n ? n.split(/\s* +\s*/) : n)
683
+ .flat()
684
+ .map((n) => 'string' === typeof n ? (r.ji.tokenSet(n) ?? r.ji.token(n)) : n)
685
+ .flat()
686
+ .filter((tin) => 'number' === typeof tin);
687
+ return tins;
688
+ };
689
+ if ('string' === typeof a.s) {
690
+ a.s = a.s.split(/\s* +\s*/);
691
+ }
692
+ // Per-position resolved tins and bit-field match tables.
693
+ // alt.t[i] holds the Tin[] for position i (used by tcol collation);
694
+ // alt.S[i] holds the bit-packed lookup (null if position is empty,
695
+ // which should not normally occur - tinsify filters nulls).
696
+ const sN = a.s.length;
697
+ const t = new Array(sN);
698
+ const S = new Array(sN);
699
+ for (let i = 0; i < sN; i++) {
700
+ const tins = tinsify([a.s[i]]);
701
+ t[i] = tins;
702
+ // `#AA` is the ANY wildcard — a position whose tin list
703
+ // includes it must match every lexed token regardless of
704
+ // partition. Represent that by dropping to the existing
705
+ // `S[i] = null` sentinel ("no constraint"), bypassing the
706
+ // per-partition bitset check in parse_alts. The t[i] entry
707
+ // keeps the raw tin list so tcol collation still reflects
708
+ // what the user wrote.
709
+ const aaTin = r.ji.token('#AA');
710
+ if (aaTin != null && tins.includes(aaTin)) {
711
+ S[i] = null;
712
+ continue;
713
+ }
714
+ S[i] =
715
+ 0 < tins.length
716
+ ? new Array(Math.max(...tins.map((tin) => (1 + tin / 31) | 0)))
717
+ .fill(null)
718
+ .map((_, j) => j)
719
+ .map((part) => bitify(partify(tins, part), part))
720
+ : null;
721
+ }
722
+ aa.t = t;
723
+ aa.S = S;
724
+ aa.sN = sN;
725
+ }
726
+ if (!a.p) {
727
+ a.p = null;
728
+ }
729
+ else {
730
+ resolveFunctionRef('push', rs, r, a, 'p');
731
+ }
732
+ if (!a.r) {
733
+ a.r = null;
734
+ }
735
+ else {
736
+ resolveFunctionRef('replace', rs, r, a, 'r');
737
+ }
738
+ if (!a.b) {
739
+ a.b = null;
740
+ }
741
+ else {
742
+ resolveFunctionRef('back', rs, r, a, 'b');
743
+ }
744
+ if (!a.a) {
745
+ a.a = null;
746
+ }
747
+ else {
748
+ resolveFunctionRef('action', rs, r, a, 'a');
749
+ }
750
+ if (!a.h) {
751
+ a.h = null;
752
+ }
753
+ else {
754
+ resolveFunctionRef('modify', rs, r, a, 'h');
755
+ }
756
+ if (!a.e) {
757
+ a.e = null;
758
+ }
759
+ else {
760
+ resolveFunctionRef('error', rs, r, a, 'e');
761
+ }
762
+ if (!a.c) {
763
+ a.c = null;
764
+ }
765
+ else {
766
+ const ct = typeof a.c;
767
+ if ('string' === ct) {
768
+ resolveFunctionRef('condition', rs, r, a, 'c');
769
+ }
770
+ else if ('function' === ct) {
771
+ if ('c' === a.c.name) {
772
+ (0, utility_1.defprop)(a.c, 'name', { value: 'ruleCond' });
773
+ }
774
+ }
775
+ else if ('object' === ct) {
776
+ const ac = a.c;
777
+ const conds = [];
778
+ const ruleprops = Object.keys(a.c);
779
+ for (let prop of ruleprops) {
780
+ const pspec = ac[prop];
781
+ if (null != pspec) {
782
+ if ('object' === typeof pspec) {
783
+ for (let co of Object.keys(pspec)) {
784
+ if (1 === COND_OPS[co]) {
785
+ conds.push(makeRuleCond(co, prop, pspec[co]));
786
+ }
787
+ }
788
+ }
789
+ else {
790
+ conds.push(makeRuleCond('$eq', prop, pspec));
791
+ }
792
+ }
793
+ }
794
+ if (0 === conds.length) {
795
+ delete a.c;
796
+ }
797
+ else if (1 === conds.length) {
798
+ a.c = conds[0];
799
+ }
800
+ else {
801
+ a.c = function conjunctCond(r, c, a) {
802
+ for (let cond of conds) {
803
+ let pass = cond(r, c, a);
804
+ if (false == pass) {
805
+ return false;
806
+ }
807
+ }
808
+ return true;
809
+ };
810
+ }
811
+ }
812
+ else {
813
+ throw new Error('Grammar: invalid condition: ' + a.c);
814
+ }
815
+ }
816
+ return a;
817
+ }
818
+ function isfnref(v) {
819
+ return 'string' === typeof v && v.startsWith('@');
820
+ }
821
+ function resolveFunctionRef(fkind, rs, r, a, k) {
822
+ const val = a[k];
823
+ // An action (`a`) may be a list of refs/functions, run in order when
824
+ // the alt matches: the matched alt's own action first, then composed
825
+ // user actions. This lets a serialized, function-free grammar carry
826
+ // `a: ['@node$', '@my-user-action']` and resolve each by name. The
827
+ // array is collapsed here to a single function so `process()` keeps
828
+ // invoking one `alt.a`. Gated to `a` only — the other fields are
829
+ // single-valued.
830
+ if ('a' === k && Array.isArray(val)) {
831
+ if (0 === val.length) {
832
+ a[k] = null;
833
+ return;
834
+ }
835
+ const fns = val.map((v) => {
836
+ if (isfnref(v)) {
837
+ const f = r.def.fnref[v];
838
+ if (null == f) {
839
+ throw new Error(`Grammar: unknown ${fkind} function reference: ` + v +
840
+ ` for rule ${r.name} (${rs}) and alt ${a.s} (${a.g})`);
841
+ }
842
+ return f;
843
+ }
844
+ return v;
845
+ });
846
+ a[k] = function composedAction(rule, ctx, alt) {
847
+ let out;
848
+ for (const f of fns) {
849
+ out = f(rule, ctx, alt);
850
+ // Preserve the error-token short-circuit semantics of process().
851
+ if (out && out.isToken && out.err)
852
+ return out;
853
+ }
854
+ return out;
855
+ };
856
+ return;
857
+ }
858
+ if (isfnref(val)) {
859
+ const func = r.def.fnref[val];
860
+ if (null == func) {
861
+ throw new Error(`Grammar: unknown ${fkind} function reference: ` + val +
862
+ ` for rule ${r.name} (${rs}) and alt ${a.s} (${a.g})`);
863
+ }
864
+ a[k] = func;
865
+ }
866
+ }
867
+ const COND_OPS = {
868
+ $eq: 1,
869
+ $ne: 1,
870
+ $lt: 1,
871
+ $lte: 1,
872
+ $gt: 1,
873
+ $gte: 1,
874
+ };
875
+ function makeRuleCond(co, prop, val) {
876
+ const path = prop.split('.');
877
+ if ('$eq' === co) {
878
+ return function ruleCond(r, _c, _a) {
879
+ const rval = (0, utility_1.getpath)(r, path);
880
+ return rval === val;
881
+ };
882
+ }
883
+ else if ('$ne' === co) {
884
+ return function ruleCond(r, _c, _a) {
885
+ const rval = (0, utility_1.getpath)(r, path);
886
+ return rval != val;
887
+ };
888
+ }
889
+ else if ('$lt' === co) {
890
+ return function ruleCond(r, _c, _a) {
891
+ const rval = (0, utility_1.getpath)(r, path);
892
+ return null == rval || rval < val;
893
+ };
894
+ }
895
+ else if ('$lte' === co) {
896
+ return function ruleCond(r, _c, _a) {
897
+ const rval = (0, utility_1.getpath)(r, path);
898
+ return null == rval || rval <= val;
899
+ };
900
+ }
901
+ else if ('$gt' === co) {
902
+ return function ruleCond(r, _c, _a) {
903
+ const rval = (0, utility_1.getpath)(r, path);
904
+ return null == rval || rval > val;
905
+ };
906
+ }
907
+ else if ('$gte' === co) {
908
+ return function ruleCond(r, _c, _a) {
909
+ const rval = (0, utility_1.getpath)(r, path);
910
+ return null == rval || rval >= val;
911
+ };
912
+ }
913
+ else if ('$exist' === co) {
914
+ return function ruleCond(r, _c, _a) {
915
+ const rval = (0, utility_1.getpath)(r, path);
916
+ return true === val ? null != rval : null == rval;
917
+ };
918
+ }
919
+ else {
920
+ throw new Error('Grammer: unknown comparison operator: ' + co);
921
+ }
922
+ }
923
+ //# sourceMappingURL=rules.js.map