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.
- package/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +42 -16
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis-ext.js +6 -6
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +11 -2
- package/core/output/lines.js +8 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +316 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +25 -11
- 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
|
+
};
|