ucn 5.4.1 → 5.5.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.
Files changed (87) hide show
  1. package/.claude/skills/ucn/SKILL.md +58 -236
  2. package/.claude/skills/ucn/references/commands.md +144 -68
  3. package/.claude/skills/ucn/references/resolution-rules.md +171 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +27 -33
  5. package/README.md +18 -12
  6. package/cli/index.js +42 -16
  7. package/core/accessors.js +118 -3
  8. package/core/account.js +30 -0
  9. package/core/analysis.js +508 -264
  10. package/core/ast-analysis.js +76 -117
  11. package/core/async-producers.js +1729 -0
  12. package/core/bridge.js +1877 -268
  13. package/core/brief.js +6 -0
  14. package/core/build-worker.js +78 -14
  15. package/core/cache.js +522 -31
  16. package/core/callers.js +12053 -1047
  17. package/core/check.js +30 -38
  18. package/core/class-identity.js +1028 -0
  19. package/core/command-contracts.js +6 -6
  20. package/core/configuration-items.js +86 -0
  21. package/core/contract-membership.js +2262 -0
  22. package/core/cpp-scope.js +532 -0
  23. package/core/deadcode.js +171 -49
  24. package/core/decorator-binding.js +260 -0
  25. package/core/discovery.js +281 -164
  26. package/core/errors.js +52 -0
  27. package/core/execute.js +21 -9
  28. package/core/external-macros.js +326 -0
  29. package/core/graph-build.js +277 -8
  30. package/core/graph.js +35 -2
  31. package/core/ignore-rules.js +472 -0
  32. package/core/imports.js +504 -42
  33. package/core/index-ir.js +94 -11
  34. package/core/inprocess-clients.js +636 -0
  35. package/core/ir.js +87 -8
  36. package/core/macro-expansion.js +1416 -0
  37. package/core/output/analysis-ext.js +6 -6
  38. package/core/output/analysis.js +18 -2
  39. package/core/output/brief.js +3 -0
  40. package/core/output/doctor.js +4 -0
  41. package/core/output/endpoints.js +15 -6
  42. package/core/output/find.js +11 -2
  43. package/core/output/lines.js +8 -2
  44. package/core/output/refactoring.js +121 -14
  45. package/core/output/reporting.js +31 -3
  46. package/core/output/shared.js +14 -0
  47. package/core/output-budget.js +316 -122
  48. package/core/parallel-build.js +124 -47
  49. package/core/parser.js +3 -2
  50. package/core/project.js +457 -60
  51. package/core/provenance-facts.js +156 -12
  52. package/core/provenance-overload.js +30 -3
  53. package/core/provenance.js +104 -30
  54. package/core/pytest-fixtures.js +434 -0
  55. package/core/python-fixture-flow.js +17 -3
  56. package/core/python-modules.js +173 -0
  57. package/core/reflection.js +877 -0
  58. package/core/registry.js +1 -1
  59. package/core/reporting.js +234 -52
  60. package/core/request-helpers.js +561 -0
  61. package/core/route-graph.js +1922 -0
  62. package/core/route-lists.js +2414 -0
  63. package/core/rust-macro-expansion.js +2113 -0
  64. package/core/rust-modules.js +185 -0
  65. package/core/search.js +121 -29
  66. package/core/shared.js +19 -1
  67. package/core/test-name-bindings.js +256 -0
  68. package/core/type-denotation.js +1161 -0
  69. package/core/type-references.js +3229 -0
  70. package/core/verify.js +3360 -672
  71. package/languages/adapter.js +31 -5
  72. package/languages/c-family.js +4061 -212
  73. package/languages/c-macro-invocations.js +380 -0
  74. package/languages/c-preprocessor.js +254 -0
  75. package/languages/csharp.js +1406 -49
  76. package/languages/go.js +1243 -70
  77. package/languages/index.js +728 -6
  78. package/languages/java.js +473 -61
  79. package/languages/javascript.js +1270 -197
  80. package/languages/lexical-scope.js +1280 -0
  81. package/languages/python.js +1557 -72
  82. package/languages/rust-macro-rules.js +1020 -0
  83. package/languages/rust-value-flow.js +416 -0
  84. package/languages/rust.js +2237 -115
  85. package/languages/utils.js +298 -4
  86. package/mcp/server.js +25 -11
  87. package/package.json +1 -1
@@ -0,0 +1,1020 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Rust declarative macros (`macro_rules!`) as a token-tree rewriting engine
5
+ * (fix #374).
6
+ *
7
+ * A `macro_rules!` rule is matcher => transcriber over token trees. The
8
+ * grammar keeps both sides, and every invocation argument, as token trees:
9
+ * tree-sitter never parses what an invocation expands to, so functions,
10
+ * impls and calls a project macro generates are invisible to the AST. This
11
+ * module performs macro-by-example the way rustc's mbe does, over the leaves
12
+ * tree-sitter produced:
13
+ *
14
+ * tokensOf(tokenTree) - token trees of an invocation (leaf tokens +
15
+ * delimited groups; comments dropped)
16
+ * compileMacro(definition) - rules from a `macro_definition` node
17
+ * expandWithRules(...) - first rule whose matcher accepts the input,
18
+ * transcribed (fragments, nested repetitions,
19
+ * separators, `$crate`)
20
+ *
21
+ * Fragment specifiers are recognized by the token extent the Rust grammar
22
+ * gives them (a type stops at a top-level `=>`/`,`/`;`, an expression at
23
+ * `,`/`;`/`=>` outside closure parameters and generic arguments, an item at
24
+ * its body or `;`), which is what rustc's follow-set rules guarantee. The
25
+ * caller parses the transcribed stream with tree-sitter in the invocation's
26
+ * syntactic context; this module never classifies code.
27
+ *
28
+ * Tokens carry their origin: `src: 'arg'` tokens were written in the
29
+ * invocation (their source offset, line and column survive every rewrite),
30
+ * `src: 'tpl'` tokens came from a transcriber (`ctx` identifies the
31
+ * expansion step for hygiene).
32
+ */
33
+
34
+ class MacroBlind extends Error {
35
+ constructor(reason) {
36
+ super(reason);
37
+ this.reason = reason;
38
+ }
39
+ }
40
+
41
+ const LITERAL_TYPES = new Set([
42
+ 'integer_literal', 'float_literal', 'string_literal', 'raw_string_literal',
43
+ 'char_literal', 'boolean_literal',
44
+ ]);
45
+ const COMMENT_TYPES = new Set(['line_comment', 'block_comment']);
46
+ const IDENT_TEXT = /^(?:r#)?[A-Za-z_][A-Za-z0-9_]*$/;
47
+ const CLOSERS = { '(': ')', '[': ']', '{': '}' };
48
+
49
+ function leafKind(node, text) {
50
+ if (LITERAL_TYPES.has(node.type) || text === 'true' || text === 'false') return 'lit';
51
+ if (IDENT_TEXT.test(text)) return 'ident';
52
+ return 'punct';
53
+ }
54
+
55
+ /**
56
+ * Token list of one token_tree / token_tree_pattern's contents (outer
57
+ * delimiters excluded). `origin(node)` returns the origin fields for a leaf.
58
+ * Metavariables and repetitions (definition side) are returned as
59
+ * {k:'meta'} / {k:'rep'} placeholders for the compilers below.
60
+ */
61
+ function tokensOf(tree, origin) {
62
+ const out = [];
63
+ // One batched read of the children (per-index child() calls cross the
64
+ // native boundary once each).
65
+ const children = tree.children || Array.from({ length: tree.childCount }, (_, i) => tree.child(i));
66
+ const count = children.length;
67
+ for (let i = 1; i < count - 1; i++) {
68
+ const child = children[i];
69
+ const type = child.type;
70
+ if (COMMENT_TYPES.has(type)) continue;
71
+ if (type === 'token_tree' || type === 'token_tree_pattern') {
72
+ out.push(groupOf(child, origin));
73
+ continue;
74
+ }
75
+ if (type === 'token_repetition' || type === 'token_repetition_pattern' ||
76
+ type === 'token_binding_pattern') {
77
+ out.push({ k: 'meta', node: child });
78
+ continue;
79
+ }
80
+ if (type === 'metavariable') {
81
+ out.push({ k: 'meta', node: child });
82
+ continue;
83
+ }
84
+ if (type === 'ERROR') throw new MacroBlind('token-error');
85
+ const text = child.text;
86
+ // `'a` is two grammar leaves inside token trees.
87
+ if (text === "'" && i + 1 < count - 1) {
88
+ const next = children[i + 1];
89
+ if (next.type === 'identifier' && next.startIndex === child.endIndex) {
90
+ out.push({ k: 'lifetime', v: `'${next.text}`, ...origin(child, next) });
91
+ i++;
92
+ continue;
93
+ }
94
+ }
95
+ // Any other node (compound literal-like nodes included) is one token.
96
+ out.push({ k: leafKind(child, text), v: text, ...origin(child) });
97
+ }
98
+ return out;
99
+ }
100
+
101
+ function groupOf(node, origin) {
102
+ const open = node.firstChild;
103
+ const close = node.lastChild;
104
+ const d = open ? open.text : '(';
105
+ if (!CLOSERS[d]) throw new MacroBlind('token-error');
106
+ return {
107
+ k: 'group', v: d, d,
108
+ c: tokensOf(node, origin),
109
+ ...origin(open),
110
+ closeOrigin: close ? origin(close) : null,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * tokensOf(tokenTree, origin) for an invocation's argument token tree, read
116
+ * with one tree cursor (fix #375): the same tokens and origins
117
+ * (`src: 'arg'`, source offsets, line and column) without materializing a
118
+ * node object per leaf. `lineStarts` are the source's line start offsets
119
+ * (tree-sitter columns are code-unit offsets within the line).
120
+ */
121
+ function argTokensOf(tokenTree, source, lineStarts) {
122
+ const cursor = tokenTree.walk();
123
+ return argTokensAt(cursor, source, lineStarts);
124
+ }
125
+
126
+ function argOriginAt(s, e, lineStarts) {
127
+ let lo = 0;
128
+ let hi = lineStarts.length - 1;
129
+ while (lo < hi) {
130
+ const mid = (lo + hi + 1) >> 1;
131
+ if (lineStarts[mid] <= s) lo = mid; else hi = mid - 1;
132
+ }
133
+ return { src: 'arg', s, e, line: lo + 1, col: s - lineStarts[lo] };
134
+ }
135
+
136
+ /** Children of the cursor's node, in order (the cursor is left on it). */
137
+ function cursorChildren(cursor, source, lineStarts) {
138
+ const kids = [];
139
+ if (!cursor.gotoFirstChild()) return kids;
140
+ do {
141
+ const type = cursor.nodeType;
142
+ const kid = { type, s: cursor.startIndex, e: cursor.endIndex };
143
+ if (type === 'token_tree' || type === 'token_tree_pattern') {
144
+ kid.group = argGroupAt(cursor, source, lineStarts);
145
+ } else if (type === 'token_repetition' || type === 'token_repetition_pattern' ||
146
+ type === 'token_binding_pattern' || type === 'metavariable') {
147
+ kid.node = cursor.currentNode;
148
+ }
149
+ kids.push(kid);
150
+ } while (cursor.gotoNextSibling());
151
+ cursor.gotoParent();
152
+ return kids;
153
+ }
154
+
155
+ function argGroupAt(cursor, source, lineStarts) {
156
+ const kids = cursorChildren(cursor, source, lineStarts);
157
+ const open = kids[0];
158
+ const close = kids[kids.length - 1];
159
+ const d = open ? source.slice(open.s, open.e) : '(';
160
+ if (!CLOSERS[d]) throw new MacroBlind('token-error');
161
+ return {
162
+ k: 'group', v: d, d,
163
+ c: argTokenList(kids, source, lineStarts),
164
+ ...argOriginAt(open.s, open.e, lineStarts),
165
+ closeOrigin: close ? argOriginAt(close.s, close.e, lineStarts) : null,
166
+ };
167
+ }
168
+
169
+ function argTokensAt(cursor, source, lineStarts) {
170
+ return argTokenList(cursorChildren(cursor, source, lineStarts), source, lineStarts);
171
+ }
172
+
173
+ function argTokenList(kids, source, lineStarts) {
174
+ const out = [];
175
+ const count = kids.length;
176
+ for (let i = 1; i < count - 1; i++) {
177
+ const child = kids[i];
178
+ const type = child.type;
179
+ if (COMMENT_TYPES.has(type)) continue;
180
+ if (child.group) {
181
+ out.push(child.group);
182
+ continue;
183
+ }
184
+ if (child.node) {
185
+ out.push({ k: 'meta', node: child.node });
186
+ continue;
187
+ }
188
+ if (type === 'ERROR') throw new MacroBlind('token-error');
189
+ const text = source.slice(child.s, child.e);
190
+ // `'a` is two grammar leaves inside token trees.
191
+ if (text === "'" && i + 1 < count - 1) {
192
+ const next = kids[i + 1];
193
+ if (next.type === 'identifier' && next.s === child.e) {
194
+ out.push({ k: 'lifetime', v: `'${source.slice(next.s, next.e)}`, ...argOriginAt(child.s, next.e, lineStarts) });
195
+ i++;
196
+ continue;
197
+ }
198
+ }
199
+ out.push({ k: leafKindOf(type, text), v: text, ...argOriginAt(child.s, child.e, lineStarts) });
200
+ }
201
+ return out;
202
+ }
203
+
204
+ function leafKindOf(type, text) {
205
+ if (LITERAL_TYPES.has(type) || text === 'true' || text === 'false') return 'lit';
206
+ if (IDENT_TEXT.test(text)) return 'ident';
207
+ return 'punct';
208
+ }
209
+
210
+ // ── Definition compilation ────────────────────────────────────────────────
211
+
212
+ const FRAGMENTS = new Set([
213
+ 'ident', 'expr', 'expr_2021', 'ty', 'path', 'tt', 'block', 'pat', 'pat_param',
214
+ 'literal', 'lifetime', 'vis', 'item', 'meta', 'stmt',
215
+ ]);
216
+
217
+ /** Children of a repetition node: items between `(`…`)`, separator, op. */
218
+ function repetitionParts(node) {
219
+ let open = -1;
220
+ let close = -1;
221
+ let depth = 0;
222
+ for (let i = 0; i < node.childCount; i++) {
223
+ const t = node.child(i).type;
224
+ if (t === '(' && open < 0) { open = i; depth = 1; continue; }
225
+ if (open >= 0 && close < 0) {
226
+ if (t === '(') depth++;
227
+ else if (t === ')') {
228
+ depth--;
229
+ if (depth === 0) { close = i; break; }
230
+ }
231
+ }
232
+ }
233
+ if (open < 0 || close < 0) throw new MacroBlind('definition-shape');
234
+ const opNode = node.child(node.childCount - 1);
235
+ const op = opNode?.text;
236
+ if (op !== '*' && op !== '+' && op !== '?') throw new MacroBlind('definition-shape');
237
+ // The grammar keeps the separator as a hidden token: read it from the
238
+ // source span between `)` and the operator.
239
+ const text = node.text;
240
+ const base = node.startIndex;
241
+ const sep = text.slice(node.child(close).endIndex - base, opNode.startIndex - base).trim();
242
+ if (/\s/.test(sep)) throw new MacroBlind('definition-shape');
243
+ return { open, close, op, sep: sep.length > 0 ? sep : null };
244
+ }
245
+
246
+ function compileMatcherTokens(tokens) {
247
+ const items = [];
248
+ for (const tok of tokens) {
249
+ if (tok.k === 'group') {
250
+ items.push({ t: 'group', d: tok.d, items: compileMatcherTokens(tok.c) });
251
+ } else if (tok.k === 'meta') {
252
+ const node = tok.node;
253
+ if (node.type === 'token_binding_pattern') {
254
+ const name = node.childForFieldName?.('name')?.text ||
255
+ node.namedChildren.find(c => c.type === 'metavariable')?.text;
256
+ const kind = node.namedChildren.find(c => c.type === 'fragment_specifier')?.text;
257
+ if (!name || !kind || !FRAGMENTS.has(kind)) throw new MacroBlind('fragment-kind');
258
+ items.push({ t: 'frag', name: name.slice(1), kind });
259
+ } else if (node.type === 'token_repetition_pattern') {
260
+ const parts = repetitionParts(node);
261
+ const inner = [];
262
+ for (let i = parts.open + 1; i < parts.close; i++) {
263
+ const child = node.child(i);
264
+ inner.push(child);
265
+ }
266
+ const innerTokens = tokensOfNodes(inner);
267
+ const sub = compileMatcherTokens(innerTokens);
268
+ items.push({ t: 'rep', items: sub, op: parts.op, sep: parts.sep, vars: declaredVars(sub) });
269
+ } else if (node.type === 'metavariable') {
270
+ // `$crate` or a bare `$x` in a matcher: matches that token.
271
+ items.push({ t: 'tok', v: node.text });
272
+ } else {
273
+ throw new MacroBlind('definition-shape');
274
+ }
275
+ } else {
276
+ items.push({ t: 'tok', v: tok.v });
277
+ }
278
+ }
279
+ return items;
280
+ }
281
+
282
+ /** Token list from a list of sibling nodes (repetition bodies). */
283
+ function tokensOfNodes(nodes) {
284
+ return tokensOf({ children: [null, ...nodes, null] }, templateOrigin);
285
+ }
286
+
287
+ function declaredVars(items, out = new Set()) {
288
+ for (const item of items) {
289
+ if (item.t === 'frag') out.add(item.name);
290
+ else if (item.t === 'group' || item.t === 'rep') declaredVars(item.items, out);
291
+ }
292
+ return out;
293
+ }
294
+
295
+ function templateOrigin(node, endNode) {
296
+ return {
297
+ line: node.startPosition.row + 1,
298
+ col: node.startPosition.column,
299
+ ...(endNode && { e: endNode.endIndex }),
300
+ };
301
+ }
302
+
303
+ function compileTranscriberTokens(tokens) {
304
+ const items = [];
305
+ for (const tok of tokens) {
306
+ if (tok.k === 'group') {
307
+ items.push({
308
+ t: 'group', d: tok.d, line: tok.line, col: tok.col,
309
+ items: compileTranscriberTokens(tok.c),
310
+ });
311
+ } else if (tok.k === 'meta') {
312
+ const node = tok.node;
313
+ if (node.type === 'metavariable') {
314
+ const name = node.text.slice(1);
315
+ items.push(name === 'crate' ? { t: 'crate', line: node.startPosition.row + 1 }
316
+ : { t: 'var', name });
317
+ } else if (node.type === 'token_repetition') {
318
+ const parts = repetitionParts(node);
319
+ const inner = [];
320
+ for (let i = parts.open + 1; i < parts.close; i++) inner.push(node.child(i));
321
+ const sub = compileTranscriberTokens(tokensOfNodes(inner));
322
+ items.push({ t: 'rep', items: sub, op: parts.op, sep: parts.sep, vars: usedVars(sub) });
323
+ } else {
324
+ throw new MacroBlind('definition-shape');
325
+ }
326
+ } else {
327
+ items.push({ t: 'tok', tok: { k: tok.k, v: tok.v, line: tok.line, col: tok.col } });
328
+ }
329
+ }
330
+ return items;
331
+ }
332
+
333
+ function usedVars(items, out = new Set()) {
334
+ for (const item of items) {
335
+ if (item.t === 'var') out.add(item.name);
336
+ else if (item.t === 'group' || item.t === 'rep') usedVars(item.items, out);
337
+ }
338
+ return out;
339
+ }
340
+
341
+ /**
342
+ * Rules of a `macro_definition` node. Throws MacroBlind for a definition
343
+ * whose shape the engine does not model.
344
+ */
345
+ function compileMacro(definition) {
346
+ const rules = [];
347
+ for (const rule of definition.namedChildren) {
348
+ if (rule.type !== 'macro_rule') continue;
349
+ const left = rule.childForFieldName('left') ||
350
+ rule.namedChildren.find(c => c.type === 'token_tree_pattern');
351
+ const right = rule.childForFieldName('right') ||
352
+ rule.namedChildren.find(c => c.type === 'token_tree');
353
+ if (!left || !right) throw new MacroBlind('definition-shape');
354
+ const matcher = compileMatcherTokens(tokensOf(left, templateOrigin));
355
+ const transcriber = compileTranscriberTokens(tokensOf(right, templateOrigin));
356
+ const fragments = new Map();
357
+ collectFragmentKinds(matcher, fragments);
358
+ rules.push({ matcher, transcriber, fragments, line: rule.startPosition.row + 1 });
359
+ }
360
+ if (rules.length === 0) throw new MacroBlind('definition-shape');
361
+ const shape = transcriberShape(rules);
362
+ return { rules, passThroughItems: rules.every(isPassThroughItems), ...shape };
363
+ }
364
+
365
+ const ITEM_KEYWORDS = new Set(['fn', 'impl', 'struct', 'enum', 'trait', 'mod', 'union']);
366
+
367
+ /**
368
+ * What a macro's transcribers can change in the code they expand to, beyond
369
+ * the token-tree view the parser already has of an invocation:
370
+ * declares - items (functions, impls, types, modules, consts)
371
+ * bindsArgs - argument fragments bound as locals or parameters
372
+ * (`let $x`, `|$x|`, `fn f($($arg)*)`, `for $x in`), so
373
+ * the argument code is typed in a new context
374
+ * forwardsValue- the value is an argument fragment (`{ ..; $e }`)
375
+ * invokes - macro names the transcribers invoke
376
+ */
377
+ function transcriberShape(rules) {
378
+ let declares = false;
379
+ let bindsArgs = false;
380
+ let callsArgs = false;
381
+ let forwardsValue = false;
382
+ const invokes = new Set();
383
+ const hasVar = items => items.some(item => item.t === 'var' ||
384
+ ((item.t === 'group' || item.t === 'rep') && hasVar(item.items)));
385
+ const flat = items => items.flatMap(item => (item.t === 'rep' ? flat(item.items) : [item]));
386
+ const walk = (items, depth = 0) => {
387
+ const list = flat(items);
388
+ for (let i = 0; i < list.length; i++) {
389
+ const item = list[i];
390
+ if (item.t === 'group') { walk(item.items, depth + 1); continue; }
391
+ // An argument placed where a callee's name goes (`$f(..)`,
392
+ // `x.$m(..)`, `$m!(..)`): the invocation's text never shows it
393
+ // as a call. (A qualifier, `$t::f(..)`, calls the template's `f`,
394
+ // which the definition already shows.)
395
+ if (item.t === 'var') {
396
+ const after = list[i + 1];
397
+ if ((after?.t === 'group' && after.d === '(') ||
398
+ (after?.t === 'tok' && after.tok.v === '!')) callsArgs = true;
399
+ continue;
400
+ }
401
+ if (item.t !== 'tok') continue;
402
+ const v = item.tok.v;
403
+ const next = list[i + 1];
404
+ const after = list[i + 2];
405
+ const namedBy = expect => (next?.t === 'var' || (next?.t === 'tok' && next.tok.k === 'ident')) &&
406
+ after?.t === 'tok' && expect.includes(after.tok.v);
407
+ // (A const/static/type inside a block is local: no symbol.)
408
+ if (ITEM_KEYWORDS.has(v)) declares = true;
409
+ else if (depth === 0 && (v === 'const' || v === 'static') && namedBy([':'])) declares = true;
410
+ else if (depth === 0 && v === 'type' && namedBy(['=', ';', ':'])) declares = true;
411
+ if (v === 'let' || v === 'for') {
412
+ // pattern tokens up to `=` / `in`
413
+ for (let j = i + 1; j < list.length; j++) {
414
+ const t = list[j];
415
+ if (t.t === 'tok' && (t.tok.v === '=' || t.tok.v === 'in' || t.tok.v === ';')) break;
416
+ if (t.t === 'var' || (t.t === 'group' && hasVar(t.items))) { bindsArgs = true; break; }
417
+ }
418
+ }
419
+ if (v === '|') {
420
+ for (let j = i + 1; j < list.length; j++) {
421
+ const t = list[j];
422
+ if (t.t === 'tok' && t.tok.v === '|') break;
423
+ if (t.t === 'var') { bindsArgs = true; break; }
424
+ }
425
+ }
426
+ if (v === '!' && list[i - 1]?.t === 'tok' && list[i - 1].tok.k === 'ident' && next?.t === 'group') {
427
+ invokes.add(list[i - 1].tok.v);
428
+ }
429
+ }
430
+ };
431
+ const usedKinds = (items, kinds, out = new Set()) => {
432
+ for (const item of items) {
433
+ if (item.t === 'var' && kinds.has(item.name)) out.add(kinds.get(item.name));
434
+ else if (item.t === 'group' || item.t === 'rep') usedKinds(item.items, kinds, out);
435
+ }
436
+ return out;
437
+ };
438
+ for (const rule of rules) {
439
+ walk(rule.transcriber);
440
+ // Emitted `item` fragments declare; `stmt` fragments may bind.
441
+ const kinds = usedKinds(rule.transcriber, rule.fragments);
442
+ if (kinds.has('item')) declares = true;
443
+ if (kinds.has('stmt')) bindsArgs = true;
444
+ // Tail value: unwrap `{ .. }` blocks; the last statement is a lone fragment.
445
+ let items = rule.transcriber;
446
+ while (items.length === 1 && items[0].t === 'group' && items[0].d === '{') items = items[0].items;
447
+ let tail = items.length;
448
+ for (let i = items.length - 1; i >= 0; i--) {
449
+ if (items[i].t === 'tok' && items[i].tok.v === ';') { tail = i; break; }
450
+ }
451
+ const last = tail === items.length ? items : items.slice(tail + 1);
452
+ if (last.length === 1 && last[0].t === 'var') forwardsValue = true;
453
+ }
454
+ return { declares, bindsArgs, callsArgs, forwardsValue, invokes: [...invokes] };
455
+ }
456
+
457
+ function collectFragmentKinds(items, out) {
458
+ for (const item of items) {
459
+ if (item.t === 'frag') out.set(item.name, item.kind);
460
+ else if (item.t === 'group' || item.t === 'rep') collectFragmentKinds(item.items, out);
461
+ }
462
+ }
463
+
464
+ /**
465
+ * A rule that re-emits its `item` fragments unchanged, adding only outer
466
+ * attributes (`#[cfg(...)] $item`): the expansion declares exactly the
467
+ * items written in the invocation.
468
+ */
469
+ function isPassThroughItems(rule) {
470
+ const kinds = rule.fragments;
471
+ if (kinds.size === 0 || [...kinds.values()].some(kind => kind !== 'item')) return false;
472
+ const walk = items => {
473
+ for (let i = 0; i < items.length; i++) {
474
+ const item = items[i];
475
+ if (item.t === 'var') continue;
476
+ if (item.t === 'rep') {
477
+ if (!walk(item.items)) return false;
478
+ continue;
479
+ }
480
+ // `#` [`!`] `[...]` attribute
481
+ if (item.t === 'tok' && item.tok.v === '#') {
482
+ let j = i + 1;
483
+ if (items[j]?.t === 'tok' && items[j].tok.v === '!') j++;
484
+ if (items[j]?.t === 'group' && items[j].d === '[') {
485
+ i = j;
486
+ continue;
487
+ }
488
+ }
489
+ return false;
490
+ }
491
+ return true;
492
+ };
493
+ return walk(rule.transcriber);
494
+ }
495
+
496
+ // ── Fragment extents ──────────────────────────────────────────────────────
497
+
498
+ const BINARY_OPS = new Set([
499
+ '+', '-', '*', '/', '%', '^', '&', '|', '&&', '||', '<<', '>>', '==', '!=',
500
+ '<', '>', '<=', '>=', '=', '+=', '-=', '*=', '/=', '%=', '^=', '&=', '|=',
501
+ '<<=', '>>=', '..', '..=', '!', '?', 'as', 'return', 'break', 'in', 'let',
502
+ 'move', 'if', 'else', 'match', 'while', 'for', 'loop', 'unsafe', 'async', 'await',
503
+ ]);
504
+
505
+ const isTok = (tok, v) => tok && tok.k !== 'group' && tok.v === v;
506
+ const isGroup = (tok, d) => tok && tok.k === 'group' && (!d || tok.d === d);
507
+
508
+ /** End index of a balanced `<...>` starting at toks[j] === '<'. */
509
+ function skipAngles(toks, j) {
510
+ let depth = 0;
511
+ for (let i = j; i < toks.length; i++) {
512
+ const t = toks[i];
513
+ if (t.k === 'group') continue;
514
+ if (t.v === '<') depth++;
515
+ else if (t.v === '<<') depth += 2;
516
+ else if (t.v === '>' || t.v === '>=') depth--;
517
+ else if (t.v === '>>' || t.v === '>>=') depth -= 2;
518
+ else if (t.v === ';' || t.v === '{') return -1;
519
+ if (depth <= 0) return i + 1;
520
+ }
521
+ return -1;
522
+ }
523
+
524
+ const PATH_START = tok => tok && tok.k === 'ident' &&
525
+ !['as', 'fn', 'impl', 'dyn', 'for', 'where', 'mut', 'const', 'static', 'let', 'if',
526
+ 'else', 'match', 'while', 'loop', 'in', 'return', 'break', 'continue', 'move',
527
+ 'unsafe', 'async', 'await', 'pub', 'struct', 'enum', 'trait', 'type', 'use', 'mod',
528
+ 'extern', 'ref'].includes(tok.v);
529
+
530
+ function scanPath(toks, j) {
531
+ let i = j;
532
+ if (isTok(toks[i], '::')) i++;
533
+ if (!PATH_START(toks[i])) return -1;
534
+ for (;;) {
535
+ i++;
536
+ if (isTok(toks[i], '<')) {
537
+ const end = skipAngles(toks, i);
538
+ if (end < 0) return -1;
539
+ i = end;
540
+ } else if (isTok(toks[i], '::') && isTok(toks[i + 1], '<')) {
541
+ const end = skipAngles(toks, i + 1);
542
+ if (end < 0) return -1;
543
+ i = end;
544
+ } else if (isGroup(toks[i], '(') && j < i) {
545
+ // Fn(A, B) -> C sugar
546
+ i++;
547
+ if (isTok(toks[i], '->')) {
548
+ const end = scanType(toks, i + 1);
549
+ if (end < 0) return -1;
550
+ i = end;
551
+ }
552
+ }
553
+ if (isTok(toks[i], '::') && PATH_START(toks[i + 1])) {
554
+ i++;
555
+ continue;
556
+ }
557
+ return i;
558
+ }
559
+ }
560
+
561
+ function scanBounds(toks, j) {
562
+ let i = j;
563
+ for (;;) {
564
+ if (isTok(toks[i], '?')) i++;
565
+ if (toks[i]?.k === 'lifetime') i++;
566
+ else if (isGroup(toks[i], '(')) i++;
567
+ else {
568
+ if (isTok(toks[i], 'for') && isTok(toks[i + 1], '<')) {
569
+ const end = skipAngles(toks, i + 1);
570
+ if (end < 0) return -1;
571
+ i = end;
572
+ }
573
+ const end = scanPath(toks, i);
574
+ if (end < 0) return i === j ? -1 : i;
575
+ i = end;
576
+ }
577
+ if (isTok(toks[i], '+')) {
578
+ i++;
579
+ continue;
580
+ }
581
+ return i;
582
+ }
583
+ }
584
+
585
+ function scanType(toks, j) {
586
+ const t = toks[j];
587
+ if (!t) return -1;
588
+ if (t.k === 'group') return t.d === '{' ? -1 : j + 1;
589
+ switch (t.v) {
590
+ case '&': case '&&': {
591
+ let i = j + 1;
592
+ if (toks[i]?.k === 'lifetime') i++;
593
+ if (isTok(toks[i], 'mut')) i++;
594
+ return scanType(toks, i);
595
+ }
596
+ case '*': {
597
+ if (!isTok(toks[j + 1], 'const') && !isTok(toks[j + 1], 'mut')) return -1;
598
+ return scanType(toks, j + 2);
599
+ }
600
+ case '!': case '_':
601
+ return j + 1;
602
+ case 'impl': case 'dyn':
603
+ return scanBounds(toks, j + 1);
604
+ case 'for': {
605
+ if (!isTok(toks[j + 1], '<')) return -1;
606
+ const end = skipAngles(toks, j + 1);
607
+ return end < 0 ? -1 : scanType(toks, end);
608
+ }
609
+ case 'unsafe': case 'extern': case 'fn': {
610
+ let i = j;
611
+ while (i < toks.length && !isTok(toks[i], 'fn')) i++;
612
+ if (!isGroup(toks[i + 1], '(')) return -1;
613
+ i += 2;
614
+ if (isTok(toks[i], '->')) return scanType(toks, i + 1);
615
+ return i;
616
+ }
617
+ case '<': {
618
+ const end = skipAngles(toks, j);
619
+ if (end < 0) return -1;
620
+ if (!isTok(toks[end], '::')) return end;
621
+ return scanPath(toks, end + 1) < 0 ? end : scanPath(toks, end + 1);
622
+ }
623
+ default: {
624
+ const end = scanPath(toks, j);
625
+ if (end < 0) return -1;
626
+ // Bare trait object bounds (`Trait + Send`, edition 2015).
627
+ if (isTok(toks[end], '+') && (toks[end + 1]?.k === 'lifetime' || PATH_START(toks[end + 1]))) {
628
+ return scanBounds(toks, j);
629
+ }
630
+ return end;
631
+ }
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Expression extent: to the first top-level token in `stops` that is not
637
+ * inside closure parameters, generic arguments or an `as` type.
638
+ */
639
+ function scanExpr(toks, j, stops) {
640
+ let i = j;
641
+ let expectOperand = true;
642
+ while (i < toks.length) {
643
+ const t = toks[i];
644
+ if (t.k !== 'group' && stops.has(t.v)) break;
645
+ if (t.k === 'group') {
646
+ expectOperand = false;
647
+ i++;
648
+ continue;
649
+ }
650
+ if (expectOperand && (t.v === '|' || t.v === '||')) {
651
+ if (t.v === '|') {
652
+ let k = i + 1;
653
+ while (k < toks.length && !isTok(toks[k], '|')) k++;
654
+ if (k >= toks.length) return -1;
655
+ i = k + 1;
656
+ } else {
657
+ i++;
658
+ }
659
+ if (isTok(toks[i], '->')) {
660
+ const end = scanType(toks, i + 1);
661
+ if (end < 0) return -1;
662
+ i = end;
663
+ }
664
+ expectOperand = true;
665
+ continue;
666
+ }
667
+ if (t.v === 'as') {
668
+ const end = scanType(toks, i + 1);
669
+ if (end < 0) return -1;
670
+ i = end;
671
+ expectOperand = false;
672
+ continue;
673
+ }
674
+ if (t.v === '::' && isTok(toks[i + 1], '<')) {
675
+ const end = skipAngles(toks, i + 1);
676
+ if (end < 0) return -1;
677
+ i = end;
678
+ expectOperand = false;
679
+ continue;
680
+ }
681
+ expectOperand = t.k === 'punct' ? t.v !== '?' && t.v !== '.' && t.v !== '::'
682
+ : BINARY_OPS.has(t.v);
683
+ if (t.v === '.') expectOperand = false;
684
+ i++;
685
+ }
686
+ return i > j ? i : -1;
687
+ }
688
+
689
+ const EXPR_STOPS = new Set([',', ';', '=>']);
690
+ const PAT_STOPS = new Set(['=>', ',', '=', 'if', 'in']);
691
+ const PAT_PARAM_STOPS = new Set(['=>', ',', '=', 'if', 'in', '|']);
692
+
693
+ function scanPattern(toks, j, stops) {
694
+ let i = j;
695
+ while (i < toks.length) {
696
+ const t = toks[i];
697
+ if (t.k !== 'group' && stops.has(t.v)) break;
698
+ i++;
699
+ }
700
+ return i > j ? i : -1;
701
+ }
702
+
703
+ function scanAttributes(toks, j) {
704
+ let i = j;
705
+ while (isTok(toks[i], '#')) {
706
+ let k = i + 1;
707
+ if (isTok(toks[k], '!')) k++;
708
+ if (!isGroup(toks[k], '[')) break;
709
+ i = k + 1;
710
+ }
711
+ return i;
712
+ }
713
+
714
+ function scanVis(toks, j) {
715
+ if (isTok(toks[j], 'pub')) return isGroup(toks[j + 1], '(') ? j + 2 : j + 1;
716
+ return j;
717
+ }
718
+
719
+ function scanItem(toks, j) {
720
+ let i = scanVis(toks, scanAttributes(toks, j));
721
+ const head = toks[i];
722
+ if (!head) return -1;
723
+ // Macro invocation item: path ! [ident] group [;]
724
+ const macroEnd = (() => {
725
+ const p = scanPath(toks, i);
726
+ if (p < 0 || !isTok(toks[p], '!')) return -1;
727
+ let k = p + 1;
728
+ if (toks[k]?.k === 'ident' && isGroup(toks[k + 1])) k++;
729
+ if (!isGroup(toks[k])) return -1;
730
+ if (toks[k].d !== '{') return isTok(toks[k + 1], ';') ? k + 2 : -1;
731
+ return k + 1;
732
+ })();
733
+ if (macroEnd > 0) return macroEnd;
734
+ const untilSemicolon = ['use', 'type', 'let'].includes(head.v) ||
735
+ (head.v === 'extern' && isTok(toks[i + 1], 'crate')) ||
736
+ ((head.v === 'const' || head.v === 'static') &&
737
+ !isTok(toks[i + 1], 'fn') && !isTok(toks[i + 1], 'unsafe') &&
738
+ !isTok(toks[i + 1], 'async') && !isTok(toks[i + 1], 'extern'));
739
+ for (let k = i; k < toks.length; k++) {
740
+ const t = toks[k];
741
+ if (isTok(t, ';')) return k + 1;
742
+ if (!untilSemicolon && isGroup(t, '{')) return k + 1;
743
+ }
744
+ return -1;
745
+ }
746
+
747
+ function scanMeta(toks, j) {
748
+ const end = scanPath(toks, j);
749
+ if (end < 0) return -1;
750
+ if (isGroup(toks[end])) return end + 1;
751
+ if (isTok(toks[end], '=')) return scanExpr(toks, end + 1, EXPR_STOPS);
752
+ return end;
753
+ }
754
+
755
+ function scanLiteral(toks, j) {
756
+ const t = toks[j];
757
+ if (!t) return -1;
758
+ if (t.k === 'lit') return j + 1;
759
+ if (isTok(t, '-') && toks[j + 1]?.k === 'lit' && /^[0-9]/.test(toks[j + 1].v)) return j + 2;
760
+ return -1;
761
+ }
762
+
763
+ /** End index of the fragment of `kind` starting at toks[j], or -1. */
764
+ function scanFragment(kind, toks, j, nextLiteral = null) {
765
+ const t = toks[j];
766
+ switch (kind) {
767
+ case 'tt': return t ? j + 1 : -1;
768
+ case 'ident': return t && t.k === 'ident' && t.v !== '_' ? j + 1 : -1;
769
+ case 'lifetime': return t && t.k === 'lifetime' ? j + 1 : -1;
770
+ case 'literal': return scanLiteral(toks, j);
771
+ case 'block': return isGroup(t, '{') ? j + 1 : -1;
772
+ case 'vis': return scanVis(toks, j);
773
+ case 'path': return scanPath(toks, j);
774
+ case 'ty': return scanType(toks, j);
775
+ case 'expr': case 'expr_2021': case 'stmt': return scanExpr(toks, j, EXPR_STOPS);
776
+ case 'pat':
777
+ case 'pat_param': {
778
+ const stops = kind === 'pat' ? PAT_STOPS : PAT_PARAM_STOPS;
779
+ return scanPattern(toks, j, nextLiteral && !stops.has(nextLiteral)
780
+ ? new Set([...stops, nextLiteral]) : stops);
781
+ }
782
+ case 'item': return scanItem(toks, j);
783
+ case 'meta': return scanMeta(toks, j);
784
+ default: return -1;
785
+ }
786
+ }
787
+
788
+ // ── Matching ──────────────────────────────────────────────────────────────
789
+
790
+ const MATCH_BUDGET = 200000;
791
+
792
+ function withBinding(binds, name, value) {
793
+ const next = new Map(binds);
794
+ next.set(name, value);
795
+ return next;
796
+ }
797
+
798
+ function matchItems(items, ii, toks, ti, binds, ctx, k) {
799
+ if (--ctx.budget < 0) throw new MacroBlind('match-budget');
800
+ if (ii === items.length) return k(ti, binds);
801
+ const item = items[ii];
802
+ switch (item.t) {
803
+ case 'tok':
804
+ if (isTok(toks[ti], item.v)) return matchItems(items, ii + 1, toks, ti + 1, binds, ctx, k);
805
+ return null;
806
+ case 'group': {
807
+ const tok = toks[ti];
808
+ if (!isGroup(tok, item.d)) return null;
809
+ const inner = matchItems(item.items, 0, tok.c, 0, binds, ctx,
810
+ (end, b) => (end === tok.c.length ? b : null));
811
+ if (!inner) return null;
812
+ return matchItems(items, ii + 1, toks, ti + 1, inner, ctx, k);
813
+ }
814
+ case 'frag': {
815
+ // A pattern ends where the matcher's next literal begins
816
+ // (`|$p:pat|` in 2018-edition macros where `pat` has no
817
+ // top-level alternatives).
818
+ const next = items[ii + 1];
819
+ const end = scanFragment(item.kind, toks, ti,
820
+ next?.t === 'tok' ? next.v : null);
821
+ if (end < 0 || (end === ti && item.kind !== 'vis')) return null;
822
+ return matchItems(items, ii + 1, toks, end,
823
+ withBinding(binds, item.name, { leaf: toks.slice(ti, end), kind: item.kind }), ctx, k);
824
+ }
825
+ case 'rep':
826
+ return matchRepetition(item, items, ii, toks, ti, binds, ctx, k, []);
827
+ default:
828
+ return null;
829
+ }
830
+ }
831
+
832
+ function matchRepetition(rep, items, ii, toks, ti, binds, ctx, k, iterations) {
833
+ const count = iterations.length;
834
+ if (!(rep.op === '?' && count === 1)) {
835
+ let start = ti;
836
+ let ok = true;
837
+ if (count > 0 && rep.sep !== null) {
838
+ if (isTok(toks[ti], rep.sep)) start = ti + 1;
839
+ else ok = false;
840
+ }
841
+ if (ok && start <= toks.length) {
842
+ const more = matchItems(rep.items, 0, toks, start, new Map(), ctx, (end, b) => {
843
+ if (end === ti) return null; // no progress
844
+ return matchRepetition(rep, items, ii, toks, end, binds, ctx, k, [...iterations, b]);
845
+ });
846
+ if (more) return more;
847
+ }
848
+ }
849
+ if (rep.op === '+' && count === 0) return null;
850
+ const next = new Map(binds);
851
+ for (const name of rep.vars) {
852
+ next.set(name, { seq: iterations.map(b => b.get(name)) });
853
+ }
854
+ return matchItems(items, ii + 1, toks, ti, next, ctx, k);
855
+ }
856
+
857
+ /** Bindings of the first rule matching `toks`, or null. */
858
+ function matchRules(compiled, toks) {
859
+ for (const rule of compiled.rules) {
860
+ const ctx = { budget: MATCH_BUDGET };
861
+ let binds;
862
+ try {
863
+ binds = matchItems(rule.matcher, 0, toks, 0, new Map(), ctx,
864
+ (end, b) => (end === toks.length ? b : null));
865
+ } catch (error) {
866
+ if (error instanceof RangeError) throw new MacroBlind('match-depth');
867
+ throw error;
868
+ }
869
+ if (binds) return { rule, binds };
870
+ }
871
+ return null;
872
+ }
873
+
874
+ // ── Transcription ─────────────────────────────────────────────────────────
875
+
876
+ function lookup(binds, name, indices) {
877
+ let value = binds.get(name);
878
+ for (const index of indices) {
879
+ if (value && value.seq) value = value.seq[index];
880
+ }
881
+ return value;
882
+ }
883
+
884
+ function repetitionCount(rep, binds, indices) {
885
+ let count = null;
886
+ for (const name of rep.vars) {
887
+ const value = lookup(binds, name, indices);
888
+ if (!value || !value.seq) continue;
889
+ if (count === null) count = value.seq.length;
890
+ else if (count !== value.seq.length) throw new MacroBlind('repetition-mismatch');
891
+ }
892
+ if (count === null) throw new MacroBlind('repetition-without-variable');
893
+ return count;
894
+ }
895
+
896
+ /** A postfix chain (`a`, `a.b()`, `x::y(..)?`) keeps its precedence unwrapped. */
897
+ function isPostfixChain(tokens) {
898
+ if (tokens.length <= 1) return true;
899
+ const first = tokens[0];
900
+ if (first.k === 'punct' && first.v !== '::') return false;
901
+ if (first.k === 'ident' && BINARY_OPS.has(first.v)) return false;
902
+ for (let i = 1; i < tokens.length; i++) {
903
+ const t = tokens[i];
904
+ if (t.k === 'group' || t.k === 'ident' || t.k === 'lit' || t.k === 'lifetime') {
905
+ if (t.k === 'ident' && BINARY_OPS.has(t.v)) return false;
906
+ continue;
907
+ }
908
+ if (['.', '::', '?', '!'].includes(t.v)) continue;
909
+ if (t.v === '<' || t.v === '>' || t.v === '>>') continue; // turbofish arguments
910
+ return false;
911
+ }
912
+ return true;
913
+ }
914
+
915
+ function cloneTokens(tokens) {
916
+ return tokens.map(tok => (tok.k === 'group' ? { ...tok, c: cloneTokens(tok.c) } : { ...tok }));
917
+ }
918
+
919
+ function transcribeItems(items, binds, indices, out, ctx) {
920
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
921
+ const item = items[itemIndex];
922
+ if (--ctx.budget < 0) throw new MacroBlind('output-budget');
923
+ switch (item.t) {
924
+ case 'tok':
925
+ out.push({ ...item.tok, src: 'tpl', ctx: ctx.hygiene, mac: ctx.macro });
926
+ break;
927
+ case 'group': {
928
+ const children = [];
929
+ transcribeItems(item.items, binds, indices, children, ctx);
930
+ out.push({
931
+ k: 'group', v: item.d, d: item.d, c: children, line: item.line, col: item.col,
932
+ src: 'tpl', ctx: ctx.hygiene, mac: ctx.macro,
933
+ });
934
+ break;
935
+ }
936
+ case 'crate':
937
+ for (const tok of ctx.crateTokens) {
938
+ out.push({ ...tok, line: item.line, src: 'tpl', ctx: ctx.hygiene, mac: ctx.macro, dollarCrate: true });
939
+ }
940
+ break;
941
+ case 'var': {
942
+ const value = lookup(binds, item.name, indices);
943
+ if (!value) {
944
+ if (binds.has(item.name)) throw new MacroBlind('repetition-depth');
945
+ // Not a metavariable of this macro (a nested
946
+ // `macro_rules!` in the transcriber): emitted as written.
947
+ out.push({ k: 'punct', v: '$', src: 'tpl', ctx: ctx.hygiene, mac: ctx.macro },
948
+ { k: 'ident', v: item.name, src: 'tpl', ctx: ctx.hygiene, mac: ctx.macro });
949
+ break;
950
+ }
951
+ if (value.seq) throw new MacroBlind('repetition-depth');
952
+ const tokens = cloneTokens(value.leaf);
953
+ if ((value.kind === 'expr' || value.kind === 'expr_2021') && !isPostfixChain(tokens)) {
954
+ // rustc keeps a substituted expression one operand
955
+ // (an invisible group); parentheses are its source form.
956
+ out.push({
957
+ k: 'group', v: '(', d: '(', c: tokens, src: 'tpl', ctx: ctx.hygiene,
958
+ mac: ctx.macro, line: tokens[0]?.line, synthetic: true,
959
+ });
960
+ } else {
961
+ out.push(...tokens);
962
+ }
963
+ // A `stmt` fragment is a whole statement: rustc never needs
964
+ // the transcriber to supply its terminator.
965
+ if (value.kind === 'stmt' && tokens.length > 0 && !isTok(tokens[tokens.length - 1], ';') &&
966
+ !(items[itemIndex + 1]?.t === 'tok' && items[itemIndex + 1].tok.v === ';')) {
967
+ out.push({ k: 'punct', v: ';', src: 'tpl', ctx: ctx.hygiene, mac: ctx.macro });
968
+ }
969
+ break;
970
+ }
971
+ case 'rep': {
972
+ const count = repetitionCount(item, binds, indices);
973
+ for (let i = 0; i < count; i++) {
974
+ if (i > 0 && item.sep !== null) {
975
+ out.push({ k: 'punct', v: item.sep, src: 'tpl', ctx: ctx.hygiene, mac: ctx.macro });
976
+ }
977
+ transcribeItems(item.items, binds, [...indices, i], out, ctx);
978
+ }
979
+ break;
980
+ }
981
+ default:
982
+ break;
983
+ }
984
+ }
985
+ }
986
+
987
+ const OUTPUT_BUDGET = 60000;
988
+
989
+ /**
990
+ * Expand one invocation's token list with a compiled macro.
991
+ * ctx: { macro, hygiene, crateTokens } -> tokens, or throws MacroBlind.
992
+ */
993
+ function expandWithRules(compiled, toks, ctx) {
994
+ const matched = matchRules(compiled, toks);
995
+ if (!matched) throw new MacroBlind('no-matching-rule');
996
+ const out = [];
997
+ const tctx = { ...ctx, budget: ctx.budget ?? OUTPUT_BUDGET };
998
+ transcribeItems(matched.rule.transcriber, matched.binds, [], out, tctx);
999
+ return { tokens: out, rule: matched.rule };
1000
+ }
1001
+
1002
+ /** Count of leaf tokens (groups count their delimiters). */
1003
+ function tokenCount(tokens) {
1004
+ let n = 0;
1005
+ for (const tok of tokens) n += tok.k === 'group' ? 2 + tokenCount(tok.c) : 1;
1006
+ return n;
1007
+ }
1008
+
1009
+ module.exports = {
1010
+ MacroBlind,
1011
+ tokensOf,
1012
+ argTokensOf,
1013
+ compileMacro,
1014
+ matchRules,
1015
+ expandWithRules,
1016
+ scanFragment,
1017
+ isPostfixChain,
1018
+ tokenCount,
1019
+ CLOSERS,
1020
+ };