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.
- package/LICENSE +7 -0
- package/README.md +290 -0
- package/bin/prettier-wolfram.js +55 -0
- package/package.json +58 -0
- package/src/index.js +80 -0
- package/src/options.js +206 -0
- package/src/parser/adapter.js +690 -0
- package/src/parser/cstEqual.js +18 -0
- package/src/parser/index.js +29 -0
- package/src/parser/operators.js +35 -0
- package/src/parser/position.js +62 -0
- package/src/parser/tree-sitter-wolfram.wasm +0 -0
- package/src/range.js +98 -0
- package/src/rules/index.js +57 -0
- package/src/rules/line-width.js +129 -0
- package/src/rules/newlines-between-definitions.js +103 -0
- package/src/rules/no-bare-symbol-set.js +19 -0
- package/src/rules/no-dynamic-module-leak.js +74 -0
- package/src/rules/no-general-infix-function.js +52 -0
- package/src/rules/no-shadowed-pattern.js +71 -0
- package/src/rules/no-unused-module-var.js +84 -0
- package/src/rules/prefer-rule-delayed.js +59 -0
- package/src/rules/spacing-commas.js +64 -0
- package/src/rules/spacing-operators.js +87 -0
- package/src/translator/commentSpacing.js +51 -0
- package/src/translator/docComments.js +89 -0
- package/src/translator/index.js +98 -0
- package/src/translator/nodes/binary.js +205 -0
- package/src/translator/nodes/call.js +254 -0
- package/src/translator/nodes/compound.js +117 -0
- package/src/translator/nodes/container.js +194 -0
- package/src/translator/nodes/group.js +159 -0
- package/src/translator/nodes/infix.js +408 -0
- package/src/translator/nodes/leaf.js +605 -0
- package/src/translator/nodes/postfix.js +29 -0
- package/src/translator/nodes/prefix.js +27 -0
- package/src/translator/nodes/ternary.js +82 -0
- package/src/translator/ruleAlignment.js +133 -0
- package/src/translator/sourceLines.js +49 -0
- package/src/translator/sourcePreservation.js +22 -0
- package/src/translator/specialForms.js +665 -0
- package/src/utils/codeSpacing.js +420 -0
- package/src/utils/cstErrors.js +36 -0
- package/src/utils/offsets.js +132 -0
- 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 };
|