prettier-plugin-wolfram 0.7.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 (45) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +290 -0
  3. package/bin/prettier-wolfram.js +55 -0
  4. package/package.json +58 -0
  5. package/src/index.js +80 -0
  6. package/src/options.js +206 -0
  7. package/src/parser/adapter.js +690 -0
  8. package/src/parser/cstEqual.js +18 -0
  9. package/src/parser/index.js +29 -0
  10. package/src/parser/operators.js +35 -0
  11. package/src/parser/position.js +62 -0
  12. package/src/parser/tree-sitter-wolfram.wasm +0 -0
  13. package/src/range.js +98 -0
  14. package/src/rules/index.js +57 -0
  15. package/src/rules/line-width.js +129 -0
  16. package/src/rules/newlines-between-definitions.js +103 -0
  17. package/src/rules/no-bare-symbol-set.js +19 -0
  18. package/src/rules/no-dynamic-module-leak.js +74 -0
  19. package/src/rules/no-general-infix-function.js +52 -0
  20. package/src/rules/no-shadowed-pattern.js +71 -0
  21. package/src/rules/no-unused-module-var.js +84 -0
  22. package/src/rules/prefer-rule-delayed.js +59 -0
  23. package/src/rules/spacing-commas.js +64 -0
  24. package/src/rules/spacing-operators.js +87 -0
  25. package/src/translator/commentSpacing.js +51 -0
  26. package/src/translator/docComments.js +89 -0
  27. package/src/translator/index.js +98 -0
  28. package/src/translator/nodes/binary.js +205 -0
  29. package/src/translator/nodes/call.js +254 -0
  30. package/src/translator/nodes/compound.js +117 -0
  31. package/src/translator/nodes/container.js +194 -0
  32. package/src/translator/nodes/group.js +159 -0
  33. package/src/translator/nodes/infix.js +408 -0
  34. package/src/translator/nodes/leaf.js +605 -0
  35. package/src/translator/nodes/postfix.js +29 -0
  36. package/src/translator/nodes/prefix.js +27 -0
  37. package/src/translator/nodes/ternary.js +82 -0
  38. package/src/translator/ruleAlignment.js +133 -0
  39. package/src/translator/sourceLines.js +49 -0
  40. package/src/translator/sourcePreservation.js +22 -0
  41. package/src/translator/specialForms.js +665 -0
  42. package/src/utils/codeSpacing.js +420 -0
  43. package/src/utils/cstErrors.js +36 -0
  44. package/src/utils/offsets.js +132 -0
  45. package/src/utils/operatorSpacing.js +49 -0
@@ -0,0 +1,690 @@
1
+ import { makeLineIndex, nodeSource, offsetToLineCol } from "./position.js";
2
+ import { INFIX_OPS, BINARY_OPS, PREFIX_OPS, POSTFIX_OPS, opName } from "./operators.js";
3
+
4
+ const LEAF_KIND = { symbol: "Symbol", integer: "Integer", real: "Real", string: "String", comment: "Token`Comment" };
5
+
6
+ const GROUP_KIND = { "{": "List", "(": "GroupParen", "[": "Group", "<|": "Association" };
7
+ const GROUP_OPEN_LEAF = { "{": "Token`OpenCurly", "(": "Token`OpenParen", "[": "Token`OpenSquare", "<|": "Token`LessBar" };
8
+ const GROUP_CLOSE_LEAF = { "}": "Token`CloseCurly", ")": "Token`CloseParen", "]": "Token`CloseSquare", "|>": "Token`BarGreater" };
9
+
10
+ export function adapt(tree, source) {
11
+ const lineIndex = makeLineIndex(source);
12
+ const ctx = { source, lineIndex };
13
+ const root = tree.rootNode;
14
+ if (subtreeHasError(root)) {
15
+ const src = nodeSource(root, lineIndex);
16
+ return { type: "ContainerNode", kind: "String", children: [{ type: "Unknown", kind: "SyntaxErrorNode[]", source: src }], source: src };
17
+ }
18
+ // Hoist top-level semicolon chains that end with a trailing ";" (MISSING rhs) into separate
19
+ // ContainerNode children, matching the CodeParser output structure.
20
+ // Only hoist if the outer infix has a trailing MISSING (i.e. ends with ";").
21
+ const children = [];
22
+ for (const c of namedChildren(root)) {
23
+ if (c.type === "infix" && operatorLiteral(c, ctx) === ";" && hasTrailingSemicolon(c)) {
24
+ hoistSemicolonChildren(c, ctx, children);
25
+ } else {
26
+ children.push(adaptNode(c, ctx));
27
+ }
28
+ }
29
+ return {
30
+ type: "ContainerNode",
31
+ kind: "String",
32
+ children,
33
+ source: nodeSource(root, lineIndex),
34
+ };
35
+ }
36
+
37
+ // Returns true if the infix node ends with a MISSING node (i.e. has a trailing ";").
38
+ function hasTrailingSemicolon(node) {
39
+ const last = node.child(node.childCount - 1);
40
+ return last !== null && last.isMissing;
41
+ }
42
+
43
+ // Collect all leaf statements from a semicolon infix chain (possibly left-recursive),
44
+ // flattening the left-associative structure into a linear list of segments.
45
+ // Each segment is { nodes: TSNode[], semiToken: TSNode|null } where semiToken is the ";" that follows.
46
+ function collectSemicolonSegments(node, ctx, segments) {
47
+ // A semicolon infix has children: [lhs, ";", rhs] or with comments interspersed.
48
+ // The lhs may itself be a semicolon infix (left-assoc chaining).
49
+ let lhs = null, semiToken = null, rhs = null;
50
+ const pendingComments = [];
51
+
52
+ for (let i = 0; i < node.childCount; i++) {
53
+ const c = node.child(i);
54
+ if (!c.isNamed) {
55
+ // ";" operator token
56
+ semiToken = c;
57
+ } else if (c.isMissing) {
58
+ // trailing implicit null — rhs is nothing (trailing semicolon)
59
+ } else if (c.type === "comment") {
60
+ pendingComments.push(c);
61
+ } else if (lhs === null) {
62
+ lhs = c;
63
+ } else {
64
+ rhs = c;
65
+ }
66
+ }
67
+
68
+ if (lhs !== null && lhs.type === "infix" && operatorLiteral(lhs, ctx) === ";") {
69
+ // Flatten the left subtree
70
+ collectSemicolonSegments(lhs, ctx, segments);
71
+ // The last segment from lhs should get the semiToken we found
72
+ if (segments.length > 0 && semiToken !== null) {
73
+ segments[segments.length - 1].semiToken = semiToken;
74
+ }
75
+ // Add any comments from between lhs and rhs to the last segment's pending
76
+ for (const c of pendingComments) {
77
+ if (segments.length > 0) segments[segments.length - 1].trailingComments.push(c);
78
+ }
79
+ } else {
80
+ // Push lhs as a new segment
81
+ const nodes = [];
82
+ if (lhs !== null) nodes.push(lhs);
83
+ segments.push({ nodes, semiToken, trailingComments: [] });
84
+ for (const c of pendingComments) segments[segments.length - 1].trailingComments.push(c);
85
+ }
86
+
87
+ // Push rhs as the next segment (no semiToken yet — will be set by caller if needed)
88
+ if (rhs !== null) {
89
+ segments.push({ nodes: [rhs], semiToken: null, trailingComments: [] });
90
+ }
91
+ }
92
+
93
+ // Hoist top-level semicolon-separated statements into separate ContainerNode children.
94
+ // For `a; b; c;`, produces [CompoundExpr(a;Null), CompoundExpr(b;Null), CompoundExpr(c;Null)].
95
+ // For `a; b`, produces [CompoundExpr(a;Null), b].
96
+ function hoistSemicolonChildren(node, ctx, out) {
97
+ const segments = [];
98
+ collectSemicolonSegments(node, ctx, segments);
99
+
100
+ for (const seg of segments) {
101
+ if (seg.nodes.length === 0) continue;
102
+
103
+ const adaptedNodes = seg.nodes.map(n => adaptNode(n, ctx));
104
+ const src = nodeSource(seg.nodes[0], ctx.lineIndex);
105
+
106
+ if (seg.semiToken !== null) {
107
+ // This statement had a trailing ";" — wrap in CompoundExpression
108
+ const semiLeaf = { type: "LeafNode", kind: "Token`Semi", value: ";", source: nodeSource(seg.semiToken, ctx.lineIndex) };
109
+ const implicitNullLeaf = { type: "LeafNode", kind: "Token`Fake`ImplicitNull", value: "", source: nodeSource(seg.semiToken, ctx.lineIndex) };
110
+ const trailingCommentLeaves = seg.trailingComments.map(c => leaf(c, ctx));
111
+ out.push({ type: "InfixNode", op: "CompoundExpression", children: [...adaptedNodes, semiLeaf, ...trailingCommentLeaves, implicitNullLeaf], source: src });
112
+ } else {
113
+ // No trailing semicolon — emit the node directly
114
+ out.push(...adaptedNodes);
115
+ }
116
+ }
117
+ }
118
+
119
+ // A MISSING node that is the last child of a ";" infix represents trailing-semicolon implicit null —
120
+ // a valid Wolfram Language construct. Allow it rather than treating it as a parse error.
121
+ // Note: web-tree-sitter returns new JS wrappers each time, so use .id for identity comparison.
122
+ function isTrailingSemicolonImplicitNull(node) {
123
+ if (!node.isMissing) return false;
124
+ const parent = node.parent;
125
+ if (!parent || parent.type !== "infix") return false;
126
+ // The last child must be this MISSING node (compare by id)
127
+ const lastIdx = parent.childCount - 1;
128
+ if (parent.child(lastIdx).id !== node.id) return false;
129
+ // Find the last unnamed child (the operator) and check it's ";"
130
+ for (let i = lastIdx - 1; i >= 0; i--) {
131
+ const c = parent.child(i);
132
+ if (!c.isNamed) return c.type === ";";
133
+ }
134
+ return false;
135
+ }
136
+
137
+ function subtreeHasError(node) {
138
+ if (node.type === "ERROR") return true;
139
+ if (node.isMissing && !isTrailingSemicolonImplicitNull(node)) return true;
140
+ for (let i = 0; i < node.childCount; i++) if (subtreeHasError(node.child(i))) return true;
141
+ return false;
142
+ }
143
+
144
+ // tree-sitter named children, including comment extras, in source order.
145
+ function namedChildren(node) {
146
+ const out = [];
147
+ for (let i = 0; i < node.childCount; i++) {
148
+ const c = node.child(i);
149
+ if (c.isNamed) out.push(c);
150
+ }
151
+ return out;
152
+ }
153
+
154
+ function leaf(node, ctx, kind = LEAF_KIND[node.type]) {
155
+ return { type: "LeafNode", kind, value: ctx.source.slice(node.startIndex, node.endIndex), source: nodeSource(node, ctx.lineIndex) };
156
+ }
157
+
158
+ function adaptNode(node, ctx) {
159
+ switch (node.type) {
160
+ case "symbol": case "integer": case "real": case "string": case "comment":
161
+ return leaf(node, ctx);
162
+ case "group": return adaptGroup(node, ctx);
163
+ case "call": return adaptCall(node, ctx, "Token`OpenSquare", "[", "Token`CloseSquare", "]");
164
+ case "part": return adaptPart(node, ctx);
165
+ case "infix": return adaptInfix(node, ctx);
166
+ case "binary": return adaptBinary(node, ctx);
167
+ case "prefix": return adaptPrefix(node, ctx);
168
+ case "postfix": return adaptPostfix(node, ctx);
169
+ case "pattern": return adaptPattern(node, ctx);
170
+ case "blank": case "blank_sequence": case "blank_null_sequence":
171
+ return adaptBlank(node, ctx);
172
+ case "slot": return adaptSlot(node, ctx);
173
+ case "slot_sequence": return adaptSlotSequence(node, ctx);
174
+ case "out": return adaptOut(node, ctx);
175
+ case "message_name": return adaptMessageName(node, ctx);
176
+ case "get": return adaptGet(node, ctx);
177
+ case "put": return adaptPut(node, ctx);
178
+ case "tilde_infix": return adaptTildeInfix(node, ctx);
179
+ case "span": return adaptSpan(node, ctx);
180
+ case "ERROR":
181
+ return { type: "Unknown", kind: "SyntaxErrorNode[]", source: nodeSource(node, ctx.lineIndex) };
182
+ default:
183
+ // MISSING nodes that passed subtreeHasError (i.e. trailing semicolon implicit null)
184
+ if (node.isMissing) return { type: "LeafNode", kind: "Token`Fake`ImplicitNull", value: "", source: nodeSource(node, ctx.lineIndex) };
185
+ return { type: "Unknown", kind: "SyntaxErrorNode[]", source: nodeSource(node, ctx.lineIndex) };
186
+ }
187
+ }
188
+
189
+ function adaptGroup(node, ctx) {
190
+ const open = node.child(0);
191
+ const openText = ctx.source.slice(open.startIndex, open.endIndex);
192
+ const close = node.child(node.childCount - 1);
193
+ const closeText = ctx.source.slice(close.startIndex, close.endIndex);
194
+ const children = [delimLeaf(open, GROUP_OPEN_LEAF[openText], openText, ctx)];
195
+ // Collect all named children in source order, injecting whitespace trivia between them.
196
+ let lastIdx = open.endIndex;
197
+ for (const c of namedChildren(node)) {
198
+ for (const t of triviaLeaves(lastIdx, c.startIndex, ctx)) children.push(t);
199
+ if (c.type === "comment") children.push(leaf(c, ctx));
200
+ else children.push(adaptArguments(c, ctx));
201
+ lastIdx = c.endIndex;
202
+ }
203
+ for (const t of triviaLeaves(lastIdx, close.startIndex, ctx)) children.push(t);
204
+ children.push(delimLeaf(close, GROUP_CLOSE_LEAF[closeText], closeText, ctx));
205
+ return { type: "GroupNode", kind: GROUP_KIND[openText], children, source: nodeSource(node, ctx.lineIndex) };
206
+ }
207
+
208
+ function adaptCall(node, ctx, openKind, openText, closeKind, closeText) {
209
+ const headNode = node.childForFieldName("head");
210
+ const argsNode = node.childForFieldName("arguments");
211
+ const open = firstAnon(node, openText, ctx);
212
+ const close = firstAnon(node, closeText, ctx);
213
+ const children = [delimLeaf(open, openKind, openText, ctx)];
214
+ const openEnd = open.endIndex;
215
+ const closeStart = close.startIndex;
216
+ let lastIdx = openEnd;
217
+ if (argsNode) {
218
+ // Collect all named non-head children between delimiters (args + any comment extras)
219
+ for (const c of namedChildren(node)) {
220
+ if (c === headNode) continue;
221
+ if (c.startIndex < openEnd || c.endIndex > closeStart) continue;
222
+ for (const t of triviaLeaves(lastIdx, c.startIndex, ctx)) children.push(t);
223
+ if (c.type === "comment") children.push(leaf(c, ctx));
224
+ else children.push(adaptArguments(c, ctx));
225
+ lastIdx = c.endIndex;
226
+ }
227
+ } else {
228
+ // No arguments field: only comments (or truly empty) between delimiters
229
+ for (const c of namedChildren(node)) {
230
+ if (c === headNode) continue;
231
+ if (c.type === "comment" && c.startIndex >= openEnd && c.endIndex <= closeStart) {
232
+ for (const t of triviaLeaves(lastIdx, c.startIndex, ctx)) children.push(t);
233
+ children.push(leaf(c, ctx));
234
+ lastIdx = c.endIndex;
235
+ }
236
+ }
237
+ }
238
+ for (const t of triviaLeaves(lastIdx, closeStart, ctx)) children.push(t);
239
+ children.push(delimLeaf(close, closeKind, closeText, ctx));
240
+ return { type: "CallNode", head: adaptNode(headNode, ctx), children, source: nodeSource(node, ctx.lineIndex) };
241
+ }
242
+
243
+ // adaptPart: produces the CodeParser shape for list[[...]] which is
244
+ // CallNode(head, [Token`OpenSquare, GroupNode(GroupSquare, [Token`OpenSquare, content, Token`CloseSquare]), Token`CloseSquare])
245
+ // The tree-sitter "part" node uses "[[" and "]]" tokens; we split each into two "["/"]" leaves.
246
+ function adaptPart(node, ctx) {
247
+ const headNode = node.childForFieldName("head");
248
+ const argsNode = node.childForFieldName("arguments");
249
+ // Find the "[[" and "]]" anonymous tokens
250
+ let openDoubleToken = null, closeDoubleToken = null;
251
+ for (let i = 0; i < node.childCount; i++) {
252
+ const c = node.child(i);
253
+ if (!c.isNamed) {
254
+ const t = ctx.source.slice(c.startIndex, c.endIndex);
255
+ if (t === "[[") openDoubleToken = c;
256
+ else if (t === "]]") closeDoubleToken = c;
257
+ }
258
+ }
259
+ // Split "[[" into outer "[" (first char) and inner "[" (second char)
260
+ const outerOpenSrc = [offsetToLineCol(ctx.lineIndex, openDoubleToken.startIndex), offsetToLineCol(ctx.lineIndex, openDoubleToken.startIndex + 1)];
261
+ const innerOpenSrc = [offsetToLineCol(ctx.lineIndex, openDoubleToken.startIndex + 1), offsetToLineCol(ctx.lineIndex, openDoubleToken.endIndex)];
262
+ const outerOpenLeaf = { type: "LeafNode", kind: "Token`OpenSquare", value: "[", source: outerOpenSrc };
263
+ const innerOpenLeaf = { type: "LeafNode", kind: "Token`OpenSquare", value: "[", source: innerOpenSrc };
264
+ // Split "]]" into inner "]" (first char) and outer "]" (second char)
265
+ const innerCloseSrc = [offsetToLineCol(ctx.lineIndex, closeDoubleToken.startIndex), offsetToLineCol(ctx.lineIndex, closeDoubleToken.startIndex + 1)];
266
+ const outerCloseSrc = [offsetToLineCol(ctx.lineIndex, closeDoubleToken.startIndex + 1), offsetToLineCol(ctx.lineIndex, closeDoubleToken.endIndex)];
267
+ const innerCloseLeaf = { type: "LeafNode", kind: "Token`CloseSquare", value: "]", source: innerCloseSrc };
268
+ const outerCloseLeaf = { type: "LeafNode", kind: "Token`CloseSquare", value: "]", source: outerCloseSrc };
269
+ // Build GroupNode(GroupSquare) wrapping the content
270
+ const groupChildren = [innerOpenLeaf];
271
+ if (argsNode) groupChildren.push(adaptArguments(argsNode, ctx));
272
+ groupChildren.push(innerCloseLeaf);
273
+ const groupSrc = [offsetToLineCol(ctx.lineIndex, openDoubleToken.startIndex + 1), offsetToLineCol(ctx.lineIndex, closeDoubleToken.startIndex + 1)];
274
+ const groupNode = { type: "GroupNode", kind: "GroupSquare", children: groupChildren, source: groupSrc };
275
+ // Build CallNode
276
+ const callChildren = [outerOpenLeaf, groupNode, outerCloseLeaf];
277
+ return { type: "CallNode", head: adaptNode(headNode, ctx), children: callChildren, source: nodeSource(node, ctx.lineIndex) };
278
+ }
279
+
280
+ function firstAnon(node, text, ctx) {
281
+ for (let i = 0; i < node.childCount; i++) {
282
+ const c = node.child(i);
283
+ if (!c.isNamed && ctx.source.slice(c.startIndex, c.endIndex) === text) return c;
284
+ }
285
+ return node; // defensive
286
+ }
287
+
288
+ function delimLeaf(node, kind, value, ctx) {
289
+ return { type: "LeafNode", kind, value, source: nodeSource(node, ctx.lineIndex) };
290
+ }
291
+
292
+ // Produce trivia (whitespace/newline) LeafNodes for the gap between two character offsets.
293
+ // Splits on newlines: each run of spaces produces Token`Whitespace, each "\n" produces Token`Newline.
294
+ function triviaLeaves(fromIdx, toIdx, ctx) {
295
+ if (fromIdx >= toIdx) return [];
296
+ const gap = ctx.source.slice(fromIdx, toIdx);
297
+ if (!/[\s]/.test(gap)) return [];
298
+ const leaves = [];
299
+ let i = 0;
300
+ while (i < gap.length) {
301
+ const ch = gap[i];
302
+ if (ch === "\n") {
303
+ const endChar = fromIdx + i + 1;
304
+ const src = [offsetToLineCol(ctx.lineIndex, fromIdx + i), offsetToLineCol(ctx.lineIndex, endChar)];
305
+ leaves.push({ type: "LeafNode", kind: "Token`Newline", value: "\n", source: src });
306
+ i++;
307
+ } else if (ch === "\r") {
308
+ const nl = gap[i + 1] === "\n" ? "\r\n" : "\r";
309
+ const endChar = fromIdx + i + nl.length;
310
+ const src = [offsetToLineCol(ctx.lineIndex, fromIdx + i), offsetToLineCol(ctx.lineIndex, endChar)];
311
+ leaves.push({ type: "LeafNode", kind: "Token`Newline", value: nl, source: src });
312
+ i += nl.length;
313
+ } else {
314
+ // Collect run of whitespace (non-newline)
315
+ let j = i;
316
+ while (j < gap.length && gap[j] !== "\n" && gap[j] !== "\r") j++;
317
+ const ws = gap.slice(i, j);
318
+ const startChar = fromIdx + i;
319
+ const endChar = fromIdx + j;
320
+ const src = [offsetToLineCol(ctx.lineIndex, startChar), offsetToLineCol(ctx.lineIndex, endChar)];
321
+ leaves.push({ type: "LeafNode", kind: "Token`Whitespace", value: ws, source: src });
322
+ i = j;
323
+ }
324
+ }
325
+ return leaves;
326
+ }
327
+
328
+ // Return the first unnamed child's text — this is the operator literal for an infix node.
329
+ function operatorLiteral(node, ctx) {
330
+ for (let i = 0; i < node.childCount; i++) {
331
+ const c = node.child(i);
332
+ if (!c.isNamed) return ctx.source.slice(c.startIndex, c.endIndex);
333
+ }
334
+ return null;
335
+ }
336
+
337
+ // Map operator literal to the CodeParser token kind suffix.
338
+ const TOKEN_KIND_NAME = {
339
+ ",": "Comma", "+": "Plus", "-": "Minus", "*": "Star", ";": "Semi", ".": "Dot",
340
+ "=": "Equal", "&": "Amp", ":=": "ColonEqual", "^=": "CaretEqual", "^:=": "CaretColonEqual",
341
+ "->": "MinusGreater", ":>": "ColonGreater", "<->": "LessMinusGreater", "|->": "BarMinusGreater",
342
+ "/;": "SlashSemi", "/.": "SlashDot", "//.": "SlashSlashDot",
343
+ "/:": "SlashColon", "//": "SlashSlash", "//=": "SlashSlashEqual",
344
+ "+=": "PlusEqual", "-=": "MinusEqual", "*=": "StarEqual", "/=": "SlashEqual",
345
+ "^": "Caret", "@": "At", "@@": "AtAt", "@@@": "AtAtAt",
346
+ "/@": "SlashAt", "//@": "SlashSlashAt", "?": "Question", ":": "Colon",
347
+ "!": "Bang", "!!": "BangBang", "++": "PlusPlus", "--": "MinusMinus",
348
+ "..": "DotDot", "...": "DotDotDot", "'": "SingleQuote", "=.": "EqualDot",
349
+ // comparison operators
350
+ ">": "Greater", "<": "Less", ">=": "GreaterEqual", "<=": "LessEqual",
351
+ "==": "EqualEqual", "!=": "BangEqual", "===": "TripleEqual", "=!=": "EqualBangEqual",
352
+ // division
353
+ "/": "Slash",
354
+ // blank tokens
355
+ "_": "Under", "__": "UnderUnder", "___": "UnderUnderUnder",
356
+ // tier-1 gap constructs
357
+ "::": "ColonColon", "<<": "LessLess", ">>": "GreaterGreater", ">>>": "GreaterGreaterGreater",
358
+ "~": "Tilde", ";;": "SemiSemi",
359
+ // string operators
360
+ "<>": "LessGreater", "~~": "TildeTilde",
361
+ // additional infix operators missing from original table
362
+ "**": "StarStar", "|": "Bar", "||": "BarBar", "&&": "AmpAmp",
363
+ "@*": "AtStar", "/*": "SlashStar",
364
+ };
365
+
366
+ const INEQUALITY_OPS = new Set(["<", "<=", ">", ">=", "==", "!=", "===", "=!="]);
367
+ const RIGHT_ASSOC_BINARY = new Set(["=", ":=", "^=", "^:="]);
368
+
369
+ const BLANK_OP = { "_": "Blank", "__": "BlankSequence", "___": "BlankNullSequence" };
370
+ const PATTERN_OP = { "_": "PatternBlank", "__": "PatternBlankSequence", "___": "PatternBlankNullSequence" };
371
+ function tokenKindName(literal) {
372
+ return TOKEN_KIND_NAME[literal] ?? "Operator";
373
+ }
374
+
375
+ // Recursively collapse a left-assoc chain of the same infix operator into a flat children array.
376
+ // Appends to out.children: [operand, opLeaf, ..., operand, opLeaf, operand]
377
+ // Comments (tree-sitter extras) are threaded through in source order.
378
+ // Whitespace/newlines between children are injected as trivia nodes.
379
+ // Returns the endIndex of the last tree-sitter node processed (for trivia tracking).
380
+ function flattenInfix(node, literal, ctx, out) {
381
+ let lastEndIndex = node.startIndex; // track end of last emitted node for trivia
382
+ let operandCount = 0;
383
+ for (let i = 0; i < node.childCount; i++) {
384
+ const c = node.child(i);
385
+ // Inject trivia for the gap before this child
386
+ for (const t of triviaLeaves(lastEndIndex, c.startIndex, ctx)) out.children.push(t);
387
+ if (!c.isNamed) {
388
+ // Operator token
389
+ const text = ctx.source.slice(c.startIndex, c.endIndex);
390
+ out.children.push({ type: "LeafNode", kind: `Token\`${tokenKindName(text)}`, value: text, source: nodeSource(c, ctx.lineIndex) });
391
+ lastEndIndex = c.endIndex;
392
+ } else if (c.type === "comment") {
393
+ out.children.push(leaf(c, ctx));
394
+ lastEndIndex = c.endIndex;
395
+ } else {
396
+ // Operand: if it's the first and is a same-op infix, flatten recursively
397
+ if (operandCount === 0 && c.type === "infix" && operatorLiteral(c, ctx) === literal) {
398
+ flattenInfix(c, literal, ctx, out);
399
+ lastEndIndex = c.endIndex; // sub-infix ends at its endIndex
400
+ } else {
401
+ out.children.push(adaptNode(c, ctx));
402
+ lastEndIndex = c.endIndex;
403
+ }
404
+ operandCount++;
405
+ }
406
+ }
407
+ }
408
+
409
+ function adaptInfix(node, ctx) {
410
+ const literal = operatorLiteral(node, ctx);
411
+ const op = INEQUALITY_OPS.has(literal) ? "InfixInequality" : opName(INFIX_OPS, literal);
412
+ const out = { type: "InfixNode", op, children: [], source: nodeSource(node, ctx.lineIndex) };
413
+ flattenInfix(node, literal, ctx, out);
414
+ return out;
415
+ }
416
+
417
+ // A comma-separated argument list parses as an infix(",") chain; map to flat Comma InfixNode.
418
+ // Otherwise, adapt the single argument normally.
419
+ function adaptArguments(node, ctx) {
420
+ if (node.type === "infix" && operatorLiteral(node, ctx) === ",") return adaptInfix(node, ctx);
421
+ return adaptNode(node, ctx);
422
+ }
423
+
424
+ // Produce a LeafNode for an operator token.
425
+ function opLeaf(tokenNode, ctx) {
426
+ const v = ctx.source.slice(tokenNode.startIndex, tokenNode.endIndex);
427
+ return { type: "LeafNode", kind: `Token\`${tokenKindName(v)}`, value: v, source: nodeSource(tokenNode, ctx.lineIndex) };
428
+ }
429
+
430
+ // Separate a node's children into non-comment named operands and unnamed operator tokens,
431
+ // preserving source order. Also returns all children in source order for ordering-aware callers.
432
+ function parts(node) {
433
+ const named = [], tokens = [], comments_ = [];
434
+ for (let i = 0; i < node.childCount; i++) {
435
+ const c = node.child(i);
436
+ if (!c.isNamed) tokens.push(c);
437
+ else if (c.type === "comment") comments_.push(c);
438
+ else named.push(c);
439
+ }
440
+ return { named, tokens, comments: comments_ };
441
+ }
442
+
443
+ // Collect all children of a node in source order as adapted CST nodes.
444
+ // Named non-comment children are adapted via adaptNode; comments become LeafNode(Token`Comment);
445
+ // unnamed tokens become operator LeafNodes. Whitespace between nodes is injected as trivia.
446
+ function adaptChildrenInOrder(node, ctx) {
447
+ const out = [];
448
+ let lastEndIndex = node.startIndex;
449
+ for (let i = 0; i < node.childCount; i++) {
450
+ const c = node.child(i);
451
+ for (const t of triviaLeaves(lastEndIndex, c.startIndex, ctx)) out.push(t);
452
+ if (!c.isNamed) {
453
+ out.push(opLeaf(c, ctx));
454
+ } else if (c.type === "comment") {
455
+ out.push(leaf(c, ctx));
456
+ } else {
457
+ out.push(adaptNode(c, ctx));
458
+ }
459
+ lastEndIndex = c.endIndex;
460
+ }
461
+ return out;
462
+ }
463
+
464
+ function collectBinaryChain(node, literal, ctx, operands, opTokens) {
465
+ if (node.type !== "binary") { operands.push(node); return; }
466
+ const { named, tokens } = parts(node);
467
+ const lit = ctx.source.slice(tokens[0].startIndex, tokens[0].endIndex);
468
+ if (lit !== literal) { operands.push(node); return; }
469
+ collectBinaryChain(named[0], literal, ctx, operands, opTokens);
470
+ opTokens.push(tokens[0]);
471
+ operands.push(named[1]);
472
+ }
473
+
474
+ function adaptBinaryRight(node, literal, op, ctx) {
475
+ const operands = [], opTokens = [];
476
+ collectBinaryChain(node, literal, ctx, operands, opTokens);
477
+ const last = operands[operands.length - 1];
478
+ let rhs = adaptNode(last, ctx);
479
+ for (let i = operands.length - 2; i >= 0; i--) {
480
+ const lhsNode = operands[i];
481
+ const opToken = opTokens[i];
482
+ const lhs = adaptNode(lhsNode, ctx);
483
+ const src = [offsetToLineCol(ctx.lineIndex, lhsNode.startIndex), offsetToLineCol(ctx.lineIndex, last.endIndex)];
484
+ // Inject whitespace between lhs and operator, and between operator and rhs
485
+ const children = [lhs,
486
+ ...triviaLeaves(lhsNode.endIndex, opToken.startIndex, ctx),
487
+ opLeaf(opToken, ctx),
488
+ ...triviaLeaves(opToken.endIndex, operands[i + 1].startIndex, ctx),
489
+ rhs,
490
+ ];
491
+ rhs = { type: "BinaryNode", op, children, source: src };
492
+ }
493
+ return rhs;
494
+ }
495
+
496
+ function adaptBinary(node, ctx) {
497
+ const { tokens } = parts(node);
498
+ const literal = ctx.source.slice(tokens[0].startIndex, tokens[0].endIndex);
499
+ const op = opName(BINARY_OPS, literal);
500
+ if (RIGHT_ASSOC_BINARY.has(literal)) return adaptBinaryRight(node, literal, op, ctx);
501
+ // Iterate children in source order so comments between operands are preserved
502
+ const children = adaptChildrenInOrder(node, ctx);
503
+ return { type: "BinaryNode", op, children, source: nodeSource(node, ctx.lineIndex) };
504
+ }
505
+
506
+ function adaptPrefix(node, ctx) {
507
+ const { tokens } = parts(node);
508
+ const literal = ctx.source.slice(tokens[0].startIndex, tokens[0].endIndex);
509
+ // Iterate in source order to preserve any comments between operator and operand
510
+ const children = adaptChildrenInOrder(node, ctx);
511
+ return { type: "PrefixNode", op: opName(PREFIX_OPS, literal), children, source: nodeSource(node, ctx.lineIndex) };
512
+ }
513
+
514
+ function adaptPostfix(node, ctx) {
515
+ const { tokens } = parts(node);
516
+ const literal = ctx.source.slice(tokens[0].startIndex, tokens[0].endIndex);
517
+ // Iterate in source order to preserve any comments between operand and operator
518
+ const children = adaptChildrenInOrder(node, ctx);
519
+ return { type: "PostfixNode", op: opName(POSTFIX_OPS, literal), children, source: nodeSource(node, ctx.lineIndex) };
520
+ }
521
+
522
+ // blank/blank_sequence/blank_null_sequence: "_", "__", "___" tokens, optionally followed by a head type.
523
+ // Returns LeafNode(Token`Under) when no head, CompoundNode(Blank/BlankSeq/BlankNullSeq, [...]) when there is one.
524
+ function adaptBlank(node, ctx) {
525
+ const underToken = node.child(0);
526
+ const underText = ctx.source.slice(underToken.startIndex, underToken.endIndex);
527
+ const underLeaf = { type: "LeafNode", kind: `Token\`${tokenKindName(underText)}`, value: underText, source: nodeSource(underToken, ctx.lineIndex) };
528
+ const headText = ctx.source.slice(underToken.endIndex, node.endIndex);
529
+ if (!headText) return underLeaf;
530
+ const headSource = [offsetToLineCol(ctx.lineIndex, underToken.endIndex), offsetToLineCol(ctx.lineIndex, node.endIndex)];
531
+ const headLeaf = { type: "LeafNode", kind: "Symbol", value: headText, source: headSource };
532
+ return { type: "CompoundNode", op: BLANK_OP[underText], children: [underLeaf, headLeaf], source: nodeSource(node, ctx.lineIndex) };
533
+ }
534
+
535
+ // pattern: symbol followed by blank/blank_sequence/blank_null_sequence.
536
+ function adaptPattern(node, ctx) {
537
+ const nc = namedChildren(node);
538
+ const symLeaf = leaf(nc[0], ctx, "Symbol");
539
+ const blankNode = nc[1];
540
+ const underToken = blankNode.child(0);
541
+ const underText = ctx.source.slice(underToken.startIndex, underToken.endIndex);
542
+ const underLeaf = { type: "LeafNode", kind: `Token\`${tokenKindName(underText)}`, value: underText, source: nodeSource(underToken, ctx.lineIndex) };
543
+ const headText = ctx.source.slice(underToken.endIndex, blankNode.endIndex);
544
+ const patternOp = PATTERN_OP[underText];
545
+ if (!headText) {
546
+ return { type: "CompoundNode", op: patternOp, children: [symLeaf, underLeaf], source: nodeSource(node, ctx.lineIndex) };
547
+ }
548
+ const headSource = [offsetToLineCol(ctx.lineIndex, underToken.endIndex), offsetToLineCol(ctx.lineIndex, blankNode.endIndex)];
549
+ const headLeaf = { type: "LeafNode", kind: "Symbol", value: headText, source: headSource };
550
+ const blankCompound = { type: "CompoundNode", op: BLANK_OP[underText], children: [underLeaf, headLeaf], source: nodeSource(blankNode, ctx.lineIndex) };
551
+ return { type: "CompoundNode", op: patternOp, children: [symLeaf, blankCompound], source: nodeSource(node, ctx.lineIndex) };
552
+ }
553
+
554
+ // slot: "#" optionally followed by integer or symbol name
555
+ function adaptSlot(node, ctx) {
556
+ const hashToken = node.child(0); // the "#" anonymous token
557
+ const hashLeaf = { type: "LeafNode", kind: "Token`Hash", value: "#", source: nodeSource(hashToken, ctx.lineIndex) };
558
+ const suffix = ctx.source.slice(node.startIndex + 1, node.endIndex);
559
+ if (!suffix) return hashLeaf;
560
+ const suffixSource = [offsetToLineCol(ctx.lineIndex, node.startIndex + 1), offsetToLineCol(ctx.lineIndex, node.endIndex)];
561
+ if (/^[0-9]+$/.test(suffix)) {
562
+ const intLeaf = { type: "LeafNode", kind: "Integer", value: suffix, source: suffixSource };
563
+ return { type: "CompoundNode", op: "Slot", children: [hashLeaf, intLeaf], source: nodeSource(node, ctx.lineIndex) };
564
+ }
565
+ const symLeaf = { type: "LeafNode", kind: "Symbol", value: suffix, source: suffixSource };
566
+ return { type: "CompoundNode", op: "Slot", children: [hashLeaf, symLeaf], source: nodeSource(node, ctx.lineIndex) };
567
+ }
568
+
569
+ // slot_sequence: "##" optionally followed by integer
570
+ function adaptSlotSequence(node, ctx) {
571
+ const hashHashToken = node.child(0); // the "##" anonymous token
572
+ const hashHashLeaf = { type: "LeafNode", kind: "Token`HashHash", value: "##", source: nodeSource(hashHashToken, ctx.lineIndex) };
573
+ const suffix = ctx.source.slice(node.startIndex + 2, node.endIndex);
574
+ if (!suffix) return hashHashLeaf;
575
+ const suffixSource = [offsetToLineCol(ctx.lineIndex, node.startIndex + 2), offsetToLineCol(ctx.lineIndex, node.endIndex)];
576
+ const intLeaf = { type: "LeafNode", kind: "Integer", value: suffix, source: suffixSource };
577
+ return { type: "CompoundNode", op: "SlotSequence", children: [hashHashLeaf, intLeaf], source: nodeSource(node, ctx.lineIndex) };
578
+ }
579
+
580
+ // out: whole-node token: "%" → Token`Percent, "%%" or "%%..." → Token`PercentPercent, "%n" → CompoundNode(Out)
581
+ function adaptOut(node, ctx) {
582
+ const text = ctx.source.slice(node.startIndex, node.endIndex);
583
+ if (text === "%") return { type: "LeafNode", kind: "Token`Percent", value: "%", source: nodeSource(node, ctx.lineIndex) };
584
+ if (/^%%+$/.test(text)) return { type: "LeafNode", kind: "Token`PercentPercent", value: text, source: nodeSource(node, ctx.lineIndex) };
585
+ // %n form
586
+ const percentSource = [offsetToLineCol(ctx.lineIndex, node.startIndex), offsetToLineCol(ctx.lineIndex, node.startIndex + 1)];
587
+ const nSource = [offsetToLineCol(ctx.lineIndex, node.startIndex + 1), offsetToLineCol(ctx.lineIndex, node.endIndex)];
588
+ const percentLeaf = { type: "LeafNode", kind: "Token`Percent", value: "%", source: percentSource };
589
+ const nLeaf = { type: "LeafNode", kind: "Integer", value: text.slice(1), source: nSource };
590
+ return { type: "CompoundNode", op: "Out", children: [percentLeaf, nLeaf], source: nodeSource(node, ctx.lineIndex) };
591
+ }
592
+
593
+ // message_name: lhs :: tag — emit InfixNode(MessageName, [lhs, Token`ColonColon, LeafNode(String, tag)])
594
+ // Note: token.immediate() in the grammar makes the tag text part of the node range but NOT a separate child.
595
+ // We extract the tag text by slicing from the end of the "::" token to the end of the node.
596
+ function adaptMessageName(node, ctx) {
597
+ const named = [], unnamed = [];
598
+ for (let i = 0; i < node.childCount; i++) {
599
+ const c = node.child(i);
600
+ (c.isNamed ? named : unnamed).push(c);
601
+ }
602
+ // named[0] = LHS expression; unnamed[0] = "::" token
603
+ const lhs = adaptNode(named[0], ctx);
604
+ const colonColonToken = unnamed[0]; // the "::" token
605
+ const colonColonLeaf = { type: "LeafNode", kind: "Token`ColonColon", value: "::", source: nodeSource(colonColonToken, ctx.lineIndex) };
606
+ // The tag name text sits between end of "::" and end of message_name node
607
+ const tagStart = colonColonToken.endIndex;
608
+ const tagEnd = node.endIndex;
609
+ const tagText = ctx.source.slice(tagStart, tagEnd);
610
+ const tagSource = [offsetToLineCol(ctx.lineIndex, tagStart), offsetToLineCol(ctx.lineIndex, tagEnd)];
611
+ const tagLeaf = { type: "LeafNode", kind: "String", value: tagText, source: tagSource };
612
+ return { type: "InfixNode", op: "MessageName", children: [lhs, colonColonLeaf, tagLeaf], source: nodeSource(node, ctx.lineIndex) };
613
+ }
614
+
615
+ // get: << expr — emit PrefixNode(Get, [Token`LessLess, expr])
616
+ function adaptGet(node, ctx) {
617
+ const { named, tokens } = parts(node);
618
+ const opToken = tokens[0];
619
+ const opLeafNode = { type: "LeafNode", kind: "Token`LessLess", value: "<<", source: nodeSource(opToken, ctx.lineIndex) };
620
+ return { type: "PrefixNode", op: "Get", children: [opLeafNode, adaptNode(named[0], ctx)], source: nodeSource(node, ctx.lineIndex) };
621
+ }
622
+
623
+ // put: lhs >> rhs or lhs >>> rhs — emit BinaryNode(Put/PutAppend, [lhs, op, rhs])
624
+ function adaptPut(node, ctx) {
625
+ const { named, tokens } = parts(node);
626
+ const opToken = tokens[0];
627
+ const opText = ctx.source.slice(opToken.startIndex, opToken.endIndex);
628
+ const op = opText === ">>>" ? "PutAppend" : "Put";
629
+ const kind = opText === ">>>" ? "Token`GreaterGreaterGreater" : "Token`GreaterGreater";
630
+ const opLeafNode = { type: "LeafNode", kind, value: opText, source: nodeSource(opToken, ctx.lineIndex) };
631
+ return {
632
+ type: "BinaryNode",
633
+ op,
634
+ children: [adaptNode(named[0], ctx), opLeafNode, adaptNode(named[1], ctx)],
635
+ source: nodeSource(node, ctx.lineIndex),
636
+ };
637
+ }
638
+
639
+ // tilde_infix: a ~ f ~ b — emit TernaryNode(TernaryTilde, [a, Token`Tilde, f, Token`Tilde, b])
640
+ // Comments between operands and tilde tokens are preserved in source order.
641
+ function adaptTildeInfix(node, ctx) {
642
+ const children = adaptChildrenInOrder(node, ctx);
643
+ return { type: "TernaryNode", op: "TernaryTilde", children, source: nodeSource(node, ctx.lineIndex) };
644
+ }
645
+
646
+ // span: ;; with optional LHS and RHS
647
+ // Forms: a ;; b, a ;;, ;; b, ;; (bare)
648
+ function adaptSpan(node, ctx) {
649
+ const named = [], semiSemiTokens = [];
650
+ for (let i = 0; i < node.childCount; i++) {
651
+ const c = node.child(i);
652
+ if (c.isNamed) named.push(c);
653
+ else if (ctx.source.slice(c.startIndex, c.endIndex) === ";;") semiSemiTokens.push(c);
654
+ }
655
+ const semiSemiToken = semiSemiTokens[0];
656
+ const semiSemiLeaf = { type: "LeafNode", kind: "Token`SemiSemi", value: ";;", source: nodeSource(semiSemiToken, ctx.lineIndex) };
657
+
658
+ // Determine LHS and RHS based on what's present
659
+ // We look at whether the ";;" comes after or before named children by comparing indices
660
+ let lhsNode = null, rhsNode = null;
661
+ if (named.length === 2) {
662
+ // a ;; b
663
+ lhsNode = named[0];
664
+ rhsNode = named[1];
665
+ } else if (named.length === 1) {
666
+ if (named[0].startIndex < semiSemiToken.startIndex) {
667
+ // a ;;
668
+ lhsNode = named[0];
669
+ } else {
670
+ // ;; b
671
+ rhsNode = named[0];
672
+ }
673
+ }
674
+ // else named.length === 0: bare ;;
675
+
676
+ const lhs = lhsNode
677
+ ? adaptNode(lhsNode, ctx)
678
+ : { type: "LeafNode", kind: "Integer", value: "1", source: nodeSource(semiSemiToken, ctx.lineIndex) };
679
+ const rhs = rhsNode
680
+ ? adaptNode(rhsNode, ctx)
681
+ : { type: "LeafNode", kind: "Symbol", value: "All", source: nodeSource(semiSemiToken, ctx.lineIndex) };
682
+ return {
683
+ type: "BinaryNode",
684
+ op: "Span",
685
+ children: [lhs, semiSemiLeaf, rhs],
686
+ source: nodeSource(node, ctx.lineIndex),
687
+ };
688
+ }
689
+
690
+ export { adaptNode, namedChildren, leaf };