@graffiticode/parser 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/unparse.js +252 -251
package/package.json
CHANGED
package/src/unparse.js
CHANGED
|
@@ -28,272 +28,273 @@ function unparseNode(node, lexicon, indent = 0, options = {}) {
|
|
|
28
28
|
|
|
29
29
|
// Handle AST nodes
|
|
30
30
|
switch (node.tag) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
case "PROG":
|
|
32
|
+
// Program is a list of expressions ending with ".."
|
|
33
|
+
if (node.elts && node.elts.length > 0) {
|
|
34
|
+
const exprs = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
35
|
+
return exprs + "..";
|
|
36
|
+
}
|
|
37
|
+
return "..";
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
39
|
+
case "EXPRS":
|
|
40
|
+
// Multiple expressions
|
|
41
|
+
if (!node.elts || node.elts.length === 0) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
// Check if this looks like a function application that wasn't folded
|
|
45
|
+
// e.g., sub followed by arguments as separate expressions
|
|
46
|
+
if (node.elts.length >= 3) {
|
|
47
|
+
const first = node.elts[0];
|
|
48
|
+
// Check if first element is an identifier that could be a function
|
|
49
|
+
if (first && first.tag && first.elts && first.elts.length === 0) {
|
|
50
|
+
// This might be a function name followed by arguments
|
|
51
|
+
const funcName = first.tag;
|
|
52
|
+
// Check if this matches a lexicon function
|
|
53
|
+
if (lexicon && lexicon[funcName]) {
|
|
54
|
+
const arity = lexicon[funcName].arity || 0;
|
|
55
|
+
if (arity > 0 && node.elts.length === arity + 1) {
|
|
56
|
+
// Treat this as a function application
|
|
57
|
+
const args = node.elts.slice(1).map(elt => unparseNode(elt, lexicon, indent, opts)).join(" ");
|
|
58
|
+
return `${funcName} ${args}`;
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
}
|
|
62
|
+
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// For single expression, return as is
|
|
65
|
+
if (node.elts.length === 1) {
|
|
66
|
+
return unparseNode(node.elts[0], lexicon, indent, opts);
|
|
67
|
+
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// For multiple expressions, put each on its own line
|
|
70
|
+
return node.elts.map(elt => unparseNode(elt, lexicon, indent, opts)).join("\n");
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
case "NUM":
|
|
73
|
+
return node.elts[0];
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
case "STR": {
|
|
76
|
+
// Escape quotes and backslashes in the string
|
|
77
|
+
const str = node.elts[0];
|
|
78
|
+
const escaped = str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
79
|
+
return `"${escaped}"`;
|
|
80
|
+
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
case "BOOL":
|
|
83
|
+
return node.elts[0] ? "true" : "false";
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
case "NULL":
|
|
86
|
+
return "null";
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
case "IDENT":
|
|
89
|
+
return node.elts[0];
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
case "LIST": {
|
|
92
|
+
console.log("LIST");
|
|
93
|
+
// Array literal [a, b, c]
|
|
94
|
+
if (!node.elts || node.elts.length === 0) {
|
|
95
|
+
return "[]";
|
|
96
|
+
}
|
|
96
97
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
98
|
+
if (opts.compact) {
|
|
99
|
+
// Compact mode: inline list
|
|
100
|
+
const items = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts));
|
|
101
|
+
return "[" + items.join(", ") + "]";
|
|
102
|
+
} else {
|
|
103
|
+
// Pretty print with each element on a new line
|
|
104
|
+
const innerIndent = indent + opts.indentSize;
|
|
105
|
+
const indentStr = " ".repeat(innerIndent);
|
|
106
|
+
const items = node.elts.map(elt =>
|
|
107
|
+
indentStr + unparseNode(elt, lexicon, innerIndent, opts)
|
|
108
|
+
);
|
|
109
|
+
return "[\n" + items.join("\n") + "\n" + " ".repeat(indent) + "]";
|
|
110
110
|
}
|
|
111
|
+
}
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
case "RECORD": {
|
|
114
|
+
// Object literal {a: 1, b: 2}
|
|
115
|
+
if (!node.elts || node.elts.length === 0) {
|
|
116
|
+
return "{}";
|
|
117
|
+
}
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
119
|
+
if (opts.compact) {
|
|
120
|
+
// Compact mode: inline record
|
|
121
|
+
const bindings = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts));
|
|
122
|
+
return "{" + bindings.join(", ") + "}";
|
|
123
|
+
} else {
|
|
124
|
+
// Pretty print with each binding on a new line
|
|
125
|
+
const innerIndent = indent + opts.indentSize;
|
|
126
|
+
const indentStr = " ".repeat(innerIndent);
|
|
127
|
+
const bindings = node.elts.map(elt =>
|
|
128
|
+
indentStr + unparseNode(elt, lexicon, innerIndent, opts)
|
|
129
|
+
);
|
|
130
|
+
return "{\n" + bindings.join("\n") + "\n" + " ".repeat(indent) + "}";
|
|
131
131
|
}
|
|
132
|
+
}
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
const value = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
144
|
-
return `${key}: ${value}`;
|
|
134
|
+
case "BINDING": {
|
|
135
|
+
// Key-value pair in a record
|
|
136
|
+
if (node.elts && node.elts.length >= 2) {
|
|
137
|
+
// If the key is a string node, unparse it without quotes for object keys
|
|
138
|
+
let key;
|
|
139
|
+
if (node.elts[0] && node.elts[0].tag === "STR") {
|
|
140
|
+
key = node.elts[0].elts[0]; // Get the raw string without quotes
|
|
141
|
+
} else {
|
|
142
|
+
key = unparseNode(node.elts[0], lexicon, indent);
|
|
145
143
|
}
|
|
146
|
-
|
|
144
|
+
const value = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
145
|
+
return `${key}: ${value}`;
|
|
147
146
|
}
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
case "PAREN":
|
|
151
|
+
// Parenthesized expression
|
|
152
|
+
if (node.elts && node.elts.length > 0) {
|
|
153
|
+
return "(" + unparseNode(node.elts[0], lexicon, indent, opts) + ")";
|
|
154
|
+
}
|
|
155
|
+
return "()";
|
|
156
|
+
|
|
157
|
+
case "APPLY":
|
|
158
|
+
// Function application
|
|
159
|
+
if (node.elts && node.elts.length >= 2) {
|
|
160
|
+
const func = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
161
|
+
const args = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
162
|
+
return func + " " + args;
|
|
163
|
+
}
|
|
164
|
+
return "";
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
case "LAMBDA":
|
|
167
|
+
// Lambda function
|
|
168
|
+
if (node.elts && node.elts.length >= 3) {
|
|
169
|
+
const params = node.elts[1];
|
|
170
|
+
const body = node.elts[2];
|
|
170
171
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
// Extract parameter names
|
|
173
|
+
let paramStr = "";
|
|
174
|
+
if (params && params.elts) {
|
|
175
|
+
paramStr = params.elts.map(p => unparseNode(p, lexicon, indent, opts)).join(" ");
|
|
176
|
+
}
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
// Unparse body
|
|
179
|
+
const bodyStr = unparseNode(body, lexicon, indent, opts);
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
181
|
+
if (paramStr) {
|
|
182
|
+
return `\\${paramStr} . ${bodyStr}`;
|
|
183
|
+
} else {
|
|
184
|
+
return `\\. ${bodyStr}`;
|
|
185
185
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
case "LET":
|
|
189
|
-
// Let binding
|
|
190
|
-
if (node.elts && node.elts.length >= 2) {
|
|
191
|
-
const bindings = node.elts[0];
|
|
192
|
-
const body = node.elts[1];
|
|
193
|
-
|
|
194
|
-
let bindingStr = "";
|
|
195
|
-
if (bindings && bindings.elts) {
|
|
196
|
-
bindingStr = bindings.elts.map(b => {
|
|
197
|
-
if (b.elts && b.elts.length >= 2) {
|
|
198
|
-
const name = unparseNode(b.elts[0], lexicon, indent, opts);
|
|
199
|
-
const value = unparseNode(b.elts[1], lexicon, indent, opts);
|
|
200
|
-
return `${name} = ${value}`;
|
|
201
|
-
}
|
|
202
|
-
return "";
|
|
203
|
-
}).filter(s => s).join(", ");
|
|
204
|
-
}
|
|
186
|
+
}
|
|
187
|
+
return "";
|
|
205
188
|
|
|
206
|
-
|
|
207
|
-
|
|
189
|
+
case "LET":
|
|
190
|
+
// Let binding
|
|
191
|
+
if (node.elts && node.elts.length >= 2) {
|
|
192
|
+
const bindings = node.elts[0];
|
|
193
|
+
const body = node.elts[1];
|
|
194
|
+
|
|
195
|
+
let bindingStr = "";
|
|
196
|
+
if (bindings && bindings.elts) {
|
|
197
|
+
bindingStr = bindings.elts.map(b => {
|
|
198
|
+
if (b.elts && b.elts.length >= 2) {
|
|
199
|
+
const name = unparseNode(b.elts[0], lexicon, indent, opts);
|
|
200
|
+
const value = unparseNode(b.elts[1], lexicon, indent, opts);
|
|
201
|
+
return `${name} = ${value}`;
|
|
202
|
+
}
|
|
203
|
+
return "";
|
|
204
|
+
}).filter(s => s).join(", ");
|
|
208
205
|
}
|
|
209
|
-
return "";
|
|
210
206
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const thenExpr = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
207
|
+
const bodyStr = unparseNode(body, lexicon, indent, opts);
|
|
208
|
+
return `let ${bindingStr} in ${bodyStr}`;
|
|
209
|
+
}
|
|
210
|
+
return "";
|
|
216
211
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return "";
|
|
212
|
+
case "IF":
|
|
213
|
+
// If-then-else
|
|
214
|
+
if (node.elts && node.elts.length >= 2) {
|
|
215
|
+
const cond = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
216
|
+
const thenExpr = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
225
217
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return `case ${expr} of ${cases.join(" | ")}`;
|
|
218
|
+
if (node.elts.length >= 3) {
|
|
219
|
+
const elseExpr = unparseNode(node.elts[2], lexicon, indent, opts);
|
|
220
|
+
return `if ${cond} then ${thenExpr} else ${elseExpr}`;
|
|
221
|
+
} else {
|
|
222
|
+
return `if ${cond} then ${thenExpr}`;
|
|
232
223
|
}
|
|
233
|
-
|
|
224
|
+
}
|
|
225
|
+
return "";
|
|
234
226
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
227
|
+
case "CASE":
|
|
228
|
+
// Case expression
|
|
229
|
+
if (node.elts && node.elts.length > 0) {
|
|
230
|
+
const expr = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
231
|
+
const cases = node.elts.slice(1).map(c => unparseNode(c, lexicon, indent, opts));
|
|
232
|
+
return `case ${expr} of ${cases.join(" | ")}`;
|
|
233
|
+
}
|
|
234
|
+
return "";
|
|
235
|
+
|
|
236
|
+
case "OF":
|
|
237
|
+
// Case branch
|
|
238
|
+
if (node.elts && node.elts.length >= 2) {
|
|
239
|
+
const pattern = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
240
|
+
const expr = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
241
|
+
return `${pattern} => ${expr}`;
|
|
242
|
+
}
|
|
243
|
+
return "";
|
|
243
244
|
|
|
244
245
|
// Unary operator - negative
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
246
|
+
case "NEG":
|
|
247
|
+
if (node.elts && node.elts.length >= 1) {
|
|
248
|
+
const expr = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
249
|
+
return `-${expr}`;
|
|
250
|
+
}
|
|
251
|
+
return "";
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
return `/* ERROR: ${firstElt} */`;
|
|
253
|
+
case "ERROR":
|
|
254
|
+
// Error nodes - include as comments
|
|
255
|
+
if (node.elts && node.elts.length > 0) {
|
|
256
|
+
// The first element might be a node reference or a string
|
|
257
|
+
const firstElt = node.elts[0];
|
|
258
|
+
if (typeof firstElt === "object" && firstElt.elts) {
|
|
259
|
+
// It's a node, unparse it
|
|
260
|
+
return `/* ERROR: ${unparseNode(firstElt, lexicon, indent, opts)} */`;
|
|
262
261
|
}
|
|
263
|
-
return
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (sourceName) {
|
|
279
|
-
// This is a known lexicon function - unparse in prefix notation
|
|
280
|
-
if (node.elts && node.elts.length > 0) {
|
|
281
|
-
const args = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts)).join(" ");
|
|
282
|
-
return `${sourceName} ${args}`;
|
|
262
|
+
return `/* ERROR: ${firstElt} */`;
|
|
263
|
+
}
|
|
264
|
+
return "/* ERROR */";
|
|
265
|
+
|
|
266
|
+
default: {
|
|
267
|
+
// Check if this is a lexicon-defined function
|
|
268
|
+
// First, find the source name for this tag in the lexicon
|
|
269
|
+
let sourceName = null;
|
|
270
|
+
if (lexicon) {
|
|
271
|
+
for (const [key, value] of Object.entries(lexicon)) {
|
|
272
|
+
if (value && value.name === node.tag) {
|
|
273
|
+
sourceName = key;
|
|
274
|
+
break;
|
|
283
275
|
}
|
|
284
|
-
return sourceName;
|
|
285
276
|
}
|
|
277
|
+
}
|
|
286
278
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
279
|
+
if (sourceName) {
|
|
280
|
+
// This is a known lexicon function - unparse in prefix notation
|
|
281
|
+
if (node.elts && node.elts.length > 0) {
|
|
282
|
+
const args = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts)).join(" ");
|
|
283
|
+
return `${sourceName} ${args}`;
|
|
291
284
|
}
|
|
285
|
+
return sourceName;
|
|
286
|
+
}
|
|
292
287
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
288
|
+
// Handle identifiers that aren't in the lexicon (like lowercase "sub")
|
|
289
|
+
if (node.elts && node.elts.length === 0) {
|
|
290
|
+
// This is likely an identifier
|
|
291
|
+
return node.tag;
|
|
296
292
|
}
|
|
293
|
+
|
|
294
|
+
// Fallback for unknown nodes
|
|
295
|
+
console.warn(`Unknown node tag: ${node.tag}`);
|
|
296
|
+
return `/* ${node.tag} */`;
|
|
297
|
+
}
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -343,34 +344,34 @@ function reconstructNode(pool, nodeId) {
|
|
|
343
344
|
|
|
344
345
|
// Handle different node types
|
|
345
346
|
switch (node.tag) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
347
|
+
case "NUM":
|
|
348
|
+
case "STR":
|
|
349
|
+
case "IDENT":
|
|
350
|
+
case "BOOL":
|
|
351
|
+
// These nodes have primitive values in elts[0]
|
|
352
|
+
result.elts = [node.elts[0]];
|
|
353
|
+
break;
|
|
354
|
+
|
|
355
|
+
case "NULL":
|
|
356
|
+
// NULL nodes have no elements
|
|
357
|
+
result.elts = [];
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
default:
|
|
361
|
+
// For all other nodes, recursively reconstruct child nodes
|
|
362
|
+
if (node.elts && Array.isArray(node.elts)) {
|
|
363
|
+
result.elts = node.elts.map(eltId => {
|
|
364
|
+
// Check if this is a node ID (number or string number)
|
|
365
|
+
if (typeof eltId === "number" || (typeof eltId === "string" && /^\d+$/.test(eltId))) {
|
|
366
|
+
// This is a reference to another node in the pool
|
|
367
|
+
return reconstructNode(pool, eltId);
|
|
368
|
+
} else {
|
|
369
|
+
// This is a primitive value
|
|
370
|
+
return eltId;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
374
375
|
}
|
|
375
376
|
|
|
376
377
|
return result;
|