@graffiticode/parser 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +72 -0
- package/package.json +5 -2
- package/src/parser.js +24 -8
- package/src/parser.spec.js +21 -0
- package/src/unparse-l0166.spec.js +361 -0
- package/src/unparse-l0166.spec.js~ +341 -0
- package/src/unparse.js +377 -0
- package/src/unparse.spec.js +347 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { parser } from "./parser.js";
|
|
2
|
+
import { unparse } from "./unparse.js";
|
|
3
|
+
|
|
4
|
+
describe("unparse with L0166 lexicon", () => {
|
|
5
|
+
// L0166 lexicon for spreadsheet operations (from l0166/packages/api/src/lexicon.js)
|
|
6
|
+
const l0166Lexicon = {
|
|
7
|
+
"title": {
|
|
8
|
+
"tk": 1,
|
|
9
|
+
"name": "TITLE",
|
|
10
|
+
"cls": "function",
|
|
11
|
+
"length": 2,
|
|
12
|
+
"arity": 2,
|
|
13
|
+
},
|
|
14
|
+
"instructions": {
|
|
15
|
+
"tk": 1,
|
|
16
|
+
"name": "INSTRUCTIONS",
|
|
17
|
+
"cls": "function",
|
|
18
|
+
"length": 2,
|
|
19
|
+
"arity": 2,
|
|
20
|
+
},
|
|
21
|
+
"params": {
|
|
22
|
+
"tk": 1,
|
|
23
|
+
"name": "PARAMS",
|
|
24
|
+
"cls": "function",
|
|
25
|
+
"length": 2,
|
|
26
|
+
"arity": 2,
|
|
27
|
+
},
|
|
28
|
+
"cell": {
|
|
29
|
+
"tk": 1,
|
|
30
|
+
"name": "CELL",
|
|
31
|
+
"cls": "function",
|
|
32
|
+
"length": 2,
|
|
33
|
+
"arity": 2,
|
|
34
|
+
},
|
|
35
|
+
"text": {
|
|
36
|
+
"tk": 1,
|
|
37
|
+
"name": "TEXT",
|
|
38
|
+
"cls": "function",
|
|
39
|
+
"length": 2,
|
|
40
|
+
"arity": 2,
|
|
41
|
+
},
|
|
42
|
+
"assess": {
|
|
43
|
+
"tk": 1,
|
|
44
|
+
"name": "ASSESS",
|
|
45
|
+
"cls": "function",
|
|
46
|
+
"length": 2,
|
|
47
|
+
"arity": 2,
|
|
48
|
+
},
|
|
49
|
+
"method": {
|
|
50
|
+
"tk": 1,
|
|
51
|
+
"name": "METHOD",
|
|
52
|
+
"cls": "function",
|
|
53
|
+
"length": 1,
|
|
54
|
+
"arity": 1,
|
|
55
|
+
},
|
|
56
|
+
"expected": {
|
|
57
|
+
"tk": 1,
|
|
58
|
+
"name": "EXPECTED",
|
|
59
|
+
"cls": "function",
|
|
60
|
+
"length": 1,
|
|
61
|
+
"arity": 1,
|
|
62
|
+
},
|
|
63
|
+
"width": {
|
|
64
|
+
"tk": 1,
|
|
65
|
+
"name": "WIDTH",
|
|
66
|
+
"cls": "function",
|
|
67
|
+
"length": 2,
|
|
68
|
+
"arity": 2,
|
|
69
|
+
},
|
|
70
|
+
"align": {
|
|
71
|
+
"tk": 1,
|
|
72
|
+
"name": "ALIGN",
|
|
73
|
+
"cls": "function",
|
|
74
|
+
"length": 2,
|
|
75
|
+
"arity": 2,
|
|
76
|
+
},
|
|
77
|
+
"background-color": {
|
|
78
|
+
"tk": 1,
|
|
79
|
+
"name": "BACKGROUND_COLOR",
|
|
80
|
+
"cls": "function",
|
|
81
|
+
"length": 2,
|
|
82
|
+
"arity": 2,
|
|
83
|
+
},
|
|
84
|
+
"font-weight": {
|
|
85
|
+
"tk": 1,
|
|
86
|
+
"name": "FONT_WEIGHT",
|
|
87
|
+
"cls": "function",
|
|
88
|
+
"length": 2,
|
|
89
|
+
"arity": 2,
|
|
90
|
+
},
|
|
91
|
+
"format": {
|
|
92
|
+
"tk": 1,
|
|
93
|
+
"name": "FORMAT",
|
|
94
|
+
"cls": "function",
|
|
95
|
+
"length": 2,
|
|
96
|
+
"arity": 2,
|
|
97
|
+
},
|
|
98
|
+
"protected": {
|
|
99
|
+
"tk": 1,
|
|
100
|
+
"name": "PROTECTED",
|
|
101
|
+
"cls": "function",
|
|
102
|
+
"length": 2,
|
|
103
|
+
"arity": 2,
|
|
104
|
+
},
|
|
105
|
+
"cells": {
|
|
106
|
+
"tk": 1,
|
|
107
|
+
"name": "CELLS",
|
|
108
|
+
"cls": "function",
|
|
109
|
+
"length": 2,
|
|
110
|
+
"arity": 2,
|
|
111
|
+
},
|
|
112
|
+
"rows": {
|
|
113
|
+
"tk": 1,
|
|
114
|
+
"name": "ROWS",
|
|
115
|
+
"cls": "function",
|
|
116
|
+
"length": 2,
|
|
117
|
+
"arity": 2,
|
|
118
|
+
},
|
|
119
|
+
"column": {
|
|
120
|
+
"tk": 1,
|
|
121
|
+
"name": "COLUMN",
|
|
122
|
+
"cls": "function",
|
|
123
|
+
"length": 2,
|
|
124
|
+
"arity": 2,
|
|
125
|
+
},
|
|
126
|
+
"columns": {
|
|
127
|
+
"tk": 1,
|
|
128
|
+
"name": "COLUMNS",
|
|
129
|
+
"cls": "function",
|
|
130
|
+
"length": 2,
|
|
131
|
+
"arity": 2,
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
it("should unparse L0166 spreadsheet code", async () => {
|
|
136
|
+
const source = `columns [
|
|
137
|
+
column A width 100 align "center" protected true {}
|
|
138
|
+
]
|
|
139
|
+
rows [
|
|
140
|
+
row 1 background-color "#eee" protected true {}
|
|
141
|
+
]
|
|
142
|
+
cells [
|
|
143
|
+
cell A1 text "A1" protected true {}
|
|
144
|
+
]
|
|
145
|
+
{
|
|
146
|
+
v: "0.0.1"
|
|
147
|
+
}..`;
|
|
148
|
+
|
|
149
|
+
// Note: The parser may transform this code, so we test that unparse
|
|
150
|
+
// produces valid code that can be parsed again
|
|
151
|
+
// Pass the lexicon directly to avoid fetching
|
|
152
|
+
|
|
153
|
+
// For complex L0166 code, we'll just parse with language 0
|
|
154
|
+
// since the specific L0166 syntax may require special handling
|
|
155
|
+
const ast = await parser.parse(0, source);
|
|
156
|
+
|
|
157
|
+
// Log the AST pool
|
|
158
|
+
console.log("AST Pool:", JSON.stringify(ast, null, 2));
|
|
159
|
+
|
|
160
|
+
const unparsed = unparse(ast, l0166Lexicon);
|
|
161
|
+
|
|
162
|
+
// The unparsed code should be valid and parseable
|
|
163
|
+
expect(unparsed).toBeDefined();
|
|
164
|
+
expect(unparsed.endsWith("..")).toBe(true);
|
|
165
|
+
|
|
166
|
+
// Check that key elements appear in the output
|
|
167
|
+
// (the exact format may differ due to how the parser handles the syntax)
|
|
168
|
+
console.log("Original source:", source);
|
|
169
|
+
console.log("Unparsed:", unparsed);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should handle individual L0166 constructs", async () => {
|
|
173
|
+
const tests = [
|
|
174
|
+
{
|
|
175
|
+
source: '{v: "0.0.1"}..',
|
|
176
|
+
description: "version record"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
source: '[]..',
|
|
180
|
+
description: "empty list"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
source: '{}..',
|
|
184
|
+
description: "empty record"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
source: '"A1"..',
|
|
188
|
+
description: "string literal"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
source: '100..',
|
|
192
|
+
description: "number literal"
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
source: 'true..',
|
|
196
|
+
description: "boolean literal"
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
for (const { source, description } of tests) {
|
|
201
|
+
const ast = await parser.parse(166, source, l0166Lexicon);
|
|
202
|
+
const unparsed = unparse(ast, l0166Lexicon);
|
|
203
|
+
|
|
204
|
+
// Check that unparse produces output
|
|
205
|
+
expect(unparsed).toBeDefined();
|
|
206
|
+
expect(unparsed).not.toBe("");
|
|
207
|
+
|
|
208
|
+
// The output should end with ..
|
|
209
|
+
if (!unparsed.endsWith("..")) {
|
|
210
|
+
console.log(`${description}: "${source}" -> "${unparsed}"`);
|
|
211
|
+
}
|
|
212
|
+
expect(unparsed.endsWith("..")).toBe(true);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should preserve simple L0166 expressions", async () => {
|
|
217
|
+
// Test simpler L0166 expressions that should parse correctly
|
|
218
|
+
const tests = [
|
|
219
|
+
'column A {}..',
|
|
220
|
+
'row 1 {}..',
|
|
221
|
+
'cell A1 {}..',
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
for (const source of tests) {
|
|
225
|
+
const ast = await parser.parse(0, source);
|
|
226
|
+
const unparsed = unparse(ast, l0166Lexicon);
|
|
227
|
+
|
|
228
|
+
// Should produce valid output
|
|
229
|
+
expect(unparsed).toBeDefined();
|
|
230
|
+
expect(unparsed.endsWith("..")).toBe(true);
|
|
231
|
+
|
|
232
|
+
console.log(`Simple L0166: "${source}" -> "${unparsed}"`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should handle complex L0166 budget assessment code", async () => {
|
|
237
|
+
const source = `title "Home Budget Assessment"
|
|
238
|
+
instructions \`
|
|
239
|
+
- Calculate your monthly budget based on income percentages
|
|
240
|
+
- Fill in the empty cells with the correct formulas
|
|
241
|
+
- Ensure all expenses and savings are properly allocated
|
|
242
|
+
\`
|
|
243
|
+
columns [
|
|
244
|
+
column A width 150 align "left" {}
|
|
245
|
+
column B width 100 format "($#,##0)" {}
|
|
246
|
+
column C width 250 align "left" {}
|
|
247
|
+
]
|
|
248
|
+
cells [
|
|
249
|
+
cell A1 text "CATEGORY" font-weight "bold" {}
|
|
250
|
+
cell B1 text "AMOUNT" font-weight "bold" {}
|
|
251
|
+
cell C1 text "DETAILS" font-weight "bold" {}
|
|
252
|
+
|
|
253
|
+
cell A2 text "Income" {}
|
|
254
|
+
cell B2 text "4000" {}
|
|
255
|
+
cell C2 text "Total monthly income" {}
|
|
256
|
+
|
|
257
|
+
cell A3 text "Rent" {}
|
|
258
|
+
cell B3
|
|
259
|
+
text "",
|
|
260
|
+
assess [
|
|
261
|
+
method "value"
|
|
262
|
+
expected "1400"
|
|
263
|
+
] {}
|
|
264
|
+
cell C3 text "35% of your total income" {}
|
|
265
|
+
|
|
266
|
+
cell A4 text "Utilities" {}
|
|
267
|
+
cell B4 text "200" {}
|
|
268
|
+
cell C4 text "Fixed expense" {}
|
|
269
|
+
|
|
270
|
+
cell A5 text "Food" {}
|
|
271
|
+
cell B5
|
|
272
|
+
text "",
|
|
273
|
+
assess [
|
|
274
|
+
method "value"
|
|
275
|
+
expected "600"
|
|
276
|
+
] {}
|
|
277
|
+
cell C5 text "15% of your total income" {}
|
|
278
|
+
|
|
279
|
+
cell A6 text "Transportation" {}
|
|
280
|
+
cell B6
|
|
281
|
+
text "",
|
|
282
|
+
assess [
|
|
283
|
+
method "value"
|
|
284
|
+
expected "400"
|
|
285
|
+
] {}
|
|
286
|
+
cell C6 text "10% of your total income" {}
|
|
287
|
+
|
|
288
|
+
cell A7 text "Entertainment" {}
|
|
289
|
+
cell B7 text "150" {}
|
|
290
|
+
cell C7 text "Fixed expense" {}
|
|
291
|
+
|
|
292
|
+
cell A8 text "Savings" {}
|
|
293
|
+
cell B8
|
|
294
|
+
text "",
|
|
295
|
+
assess [
|
|
296
|
+
method "value"
|
|
297
|
+
expected "800"
|
|
298
|
+
] {}
|
|
299
|
+
cell C8 text "20% of your total income" {}
|
|
300
|
+
|
|
301
|
+
cell A9 text "Miscellaneous" {}
|
|
302
|
+
cell B9
|
|
303
|
+
text "",
|
|
304
|
+
assess [
|
|
305
|
+
method "value"
|
|
306
|
+
expected "450"
|
|
307
|
+
] {}
|
|
308
|
+
cell C9 text "Remaining income after all other expenses" {}
|
|
309
|
+
]
|
|
310
|
+
{
|
|
311
|
+
v: "0.0.1"
|
|
312
|
+
}..`;
|
|
313
|
+
|
|
314
|
+
// Parse with L0166 lexicon
|
|
315
|
+
const ast = await parser.parse("0166", source, l0166Lexicon);
|
|
316
|
+
|
|
317
|
+
console.log("Complex L0166 AST nodes:", Object.keys(ast).length);
|
|
318
|
+
|
|
319
|
+
const unparsed = unparse(ast, l0166Lexicon);
|
|
320
|
+
|
|
321
|
+
// The unparsed code should be valid and parseable
|
|
322
|
+
expect(unparsed).toBeDefined();
|
|
323
|
+
expect(unparsed.endsWith("..")).toBe(true);
|
|
324
|
+
|
|
325
|
+
// Check that key elements appear in the output
|
|
326
|
+
expect(unparsed).toContain("title");
|
|
327
|
+
expect(unparsed).toContain("columns");
|
|
328
|
+
expect(unparsed).toContain("cells");
|
|
329
|
+
expect(unparsed).toContain("column A");
|
|
330
|
+
expect(unparsed).toContain("column B");
|
|
331
|
+
expect(unparsed).toContain("column C");
|
|
332
|
+
|
|
333
|
+
// Log a portion of the output to see the pretty printing
|
|
334
|
+
const lines = unparsed.split("\n");
|
|
335
|
+
console.log("First 20 lines of unparsed output:");
|
|
336
|
+
console.log(lines.slice(0, 20).join("\n"));
|
|
337
|
+
console.log("...");
|
|
338
|
+
console.log("Last 10 lines of unparsed output:");
|
|
339
|
+
console.log(lines.slice(-10).join("\n"));
|
|
340
|
+
});
|
|
341
|
+
});
|
package/src/unparse.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Pretty printer that converts an AST back to source code
|
|
2
|
+
import { lexicon as basisLexicon } from "@graffiticode/basis";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Unparse an AST node to source code
|
|
6
|
+
* @param {object} node - The AST node to unparse
|
|
7
|
+
* @param {object} lexicon - The lexicon containing operator and keyword definitions
|
|
8
|
+
* @param {number} indent - The current indentation level (default 0)
|
|
9
|
+
* @param {object} options - Options for unparsing (e.g., indentSize, compact)
|
|
10
|
+
* @returns {string} The unparsed source code
|
|
11
|
+
*/
|
|
12
|
+
function unparseNode(node, lexicon, indent = 0, options = {}) {
|
|
13
|
+
// Default options
|
|
14
|
+
const opts = {
|
|
15
|
+
indentSize: 2,
|
|
16
|
+
compact: false,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (!node) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle primitive values
|
|
25
|
+
if (typeof node === "string" || typeof node === "number" || typeof node === "boolean") {
|
|
26
|
+
return String(node);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle AST nodes
|
|
30
|
+
switch (node.tag) {
|
|
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
|
+
|
|
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}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// For single expression, return as is
|
|
65
|
+
if (node.elts.length === 1) {
|
|
66
|
+
return unparseNode(node.elts[0], lexicon, indent, opts);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// For multiple expressions, put each on its own line
|
|
70
|
+
return node.elts.map(elt => unparseNode(elt, lexicon, indent, opts)).join("\n");
|
|
71
|
+
|
|
72
|
+
case "NUM":
|
|
73
|
+
return node.elts[0];
|
|
74
|
+
|
|
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
|
+
|
|
82
|
+
case "BOOL":
|
|
83
|
+
return node.elts[0] ? "true" : "false";
|
|
84
|
+
|
|
85
|
+
case "NULL":
|
|
86
|
+
return "null";
|
|
87
|
+
|
|
88
|
+
case "IDENT":
|
|
89
|
+
return node.elts[0];
|
|
90
|
+
|
|
91
|
+
case "LIST": {
|
|
92
|
+
// Array literal [a, b, c]
|
|
93
|
+
if (!node.elts || node.elts.length === 0) {
|
|
94
|
+
return "[]";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (opts.compact) {
|
|
98
|
+
// Compact mode: inline list
|
|
99
|
+
const items = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts));
|
|
100
|
+
return "[" + items.join(", ") + "]";
|
|
101
|
+
} else {
|
|
102
|
+
// Pretty print with each element on a new line
|
|
103
|
+
const innerIndent = indent + opts.indentSize;
|
|
104
|
+
const indentStr = " ".repeat(innerIndent);
|
|
105
|
+
const items = node.elts.map(elt =>
|
|
106
|
+
indentStr + unparseNode(elt, lexicon, innerIndent, opts)
|
|
107
|
+
);
|
|
108
|
+
return "[\n" + items.join("\n") + "\n" + " ".repeat(indent) + "]";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "RECORD": {
|
|
113
|
+
// Object literal {a: 1, b: 2}
|
|
114
|
+
if (!node.elts || node.elts.length === 0) {
|
|
115
|
+
return "{}";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (opts.compact) {
|
|
119
|
+
// Compact mode: inline record
|
|
120
|
+
const bindings = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts));
|
|
121
|
+
return "{" + bindings.join(", ") + "}";
|
|
122
|
+
} else {
|
|
123
|
+
// Pretty print with each binding on a new line
|
|
124
|
+
const innerIndent = indent + opts.indentSize;
|
|
125
|
+
const indentStr = " ".repeat(innerIndent);
|
|
126
|
+
const bindings = node.elts.map(elt =>
|
|
127
|
+
indentStr + unparseNode(elt, lexicon, innerIndent, opts)
|
|
128
|
+
);
|
|
129
|
+
return "{\n" + bindings.join("\n") + "\n" + " ".repeat(indent) + "}";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case "BINDING": {
|
|
134
|
+
// Key-value pair in a record
|
|
135
|
+
if (node.elts && node.elts.length >= 2) {
|
|
136
|
+
// If the key is a string node, unparse it without quotes for object keys
|
|
137
|
+
let key;
|
|
138
|
+
if (node.elts[0] && node.elts[0].tag === "STR") {
|
|
139
|
+
key = node.elts[0].elts[0]; // Get the raw string without quotes
|
|
140
|
+
} else {
|
|
141
|
+
key = unparseNode(node.elts[0], lexicon, indent);
|
|
142
|
+
}
|
|
143
|
+
const value = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
144
|
+
return `${key}: ${value}`;
|
|
145
|
+
}
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case "PAREN":
|
|
150
|
+
// Parenthesized expression
|
|
151
|
+
if (node.elts && node.elts.length > 0) {
|
|
152
|
+
return "(" + unparseNode(node.elts[0], lexicon, indent, opts) + ")";
|
|
153
|
+
}
|
|
154
|
+
return "()";
|
|
155
|
+
|
|
156
|
+
case "APPLY":
|
|
157
|
+
// Function application
|
|
158
|
+
if (node.elts && node.elts.length >= 2) {
|
|
159
|
+
const func = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
160
|
+
const args = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
161
|
+
return func + " " + args;
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
|
|
165
|
+
case "LAMBDA":
|
|
166
|
+
// Lambda function
|
|
167
|
+
if (node.elts && node.elts.length >= 3) {
|
|
168
|
+
const params = node.elts[1];
|
|
169
|
+
const body = node.elts[2];
|
|
170
|
+
|
|
171
|
+
// Extract parameter names
|
|
172
|
+
let paramStr = "";
|
|
173
|
+
if (params && params.elts) {
|
|
174
|
+
paramStr = params.elts.map(p => unparseNode(p, lexicon, indent, opts)).join(" ");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Unparse body
|
|
178
|
+
const bodyStr = unparseNode(body, lexicon, indent, opts);
|
|
179
|
+
|
|
180
|
+
if (paramStr) {
|
|
181
|
+
return `\\${paramStr} . ${bodyStr}`;
|
|
182
|
+
} else {
|
|
183
|
+
return `\\. ${bodyStr}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return "";
|
|
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
|
+
}
|
|
205
|
+
|
|
206
|
+
const bodyStr = unparseNode(body, lexicon, indent, opts);
|
|
207
|
+
return `let ${bindingStr} in ${bodyStr}`;
|
|
208
|
+
}
|
|
209
|
+
return "";
|
|
210
|
+
|
|
211
|
+
case "IF":
|
|
212
|
+
// If-then-else
|
|
213
|
+
if (node.elts && node.elts.length >= 2) {
|
|
214
|
+
const cond = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
215
|
+
const thenExpr = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
216
|
+
|
|
217
|
+
if (node.elts.length >= 3) {
|
|
218
|
+
const elseExpr = unparseNode(node.elts[2], lexicon, indent, opts);
|
|
219
|
+
return `if ${cond} then ${thenExpr} else ${elseExpr}`;
|
|
220
|
+
} else {
|
|
221
|
+
return `if ${cond} then ${thenExpr}`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return "";
|
|
225
|
+
|
|
226
|
+
case "CASE":
|
|
227
|
+
// Case expression
|
|
228
|
+
if (node.elts && node.elts.length > 0) {
|
|
229
|
+
const expr = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
230
|
+
const cases = node.elts.slice(1).map(c => unparseNode(c, lexicon, indent, opts));
|
|
231
|
+
return `case ${expr} of ${cases.join(" | ")}`;
|
|
232
|
+
}
|
|
233
|
+
return "";
|
|
234
|
+
|
|
235
|
+
case "OF":
|
|
236
|
+
// Case branch
|
|
237
|
+
if (node.elts && node.elts.length >= 2) {
|
|
238
|
+
const pattern = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
239
|
+
const expr = unparseNode(node.elts[1], lexicon, indent, opts);
|
|
240
|
+
return `${pattern} => ${expr}`;
|
|
241
|
+
}
|
|
242
|
+
return "";
|
|
243
|
+
|
|
244
|
+
// Unary operator - negative
|
|
245
|
+
case "NEG":
|
|
246
|
+
if (node.elts && node.elts.length >= 1) {
|
|
247
|
+
const expr = unparseNode(node.elts[0], lexicon, indent, opts);
|
|
248
|
+
return `-${expr}`;
|
|
249
|
+
}
|
|
250
|
+
return "";
|
|
251
|
+
|
|
252
|
+
case "ERROR":
|
|
253
|
+
// Error nodes - include as comments
|
|
254
|
+
if (node.elts && node.elts.length > 0) {
|
|
255
|
+
// The first element might be a node reference or a string
|
|
256
|
+
const firstElt = node.elts[0];
|
|
257
|
+
if (typeof firstElt === "object" && firstElt.elts) {
|
|
258
|
+
// It's a node, unparse it
|
|
259
|
+
return `/* ERROR: ${unparseNode(firstElt, lexicon, indent, opts)} */`;
|
|
260
|
+
}
|
|
261
|
+
return `/* ERROR: ${firstElt} */`;
|
|
262
|
+
}
|
|
263
|
+
return "/* ERROR */";
|
|
264
|
+
|
|
265
|
+
default: {
|
|
266
|
+
// Check if this is a lexicon-defined function
|
|
267
|
+
// First, find the source name for this tag in the lexicon
|
|
268
|
+
let sourceName = null;
|
|
269
|
+
if (lexicon) {
|
|
270
|
+
for (const [key, value] of Object.entries(lexicon)) {
|
|
271
|
+
if (value && value.name === node.tag) {
|
|
272
|
+
sourceName = key;
|
|
273
|
+
break;
|
|
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}`;
|
|
283
|
+
}
|
|
284
|
+
return sourceName;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Handle identifiers that aren't in the lexicon (like lowercase "sub")
|
|
288
|
+
if (node.elts && node.elts.length === 0) {
|
|
289
|
+
// This is likely an identifier
|
|
290
|
+
return node.tag;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Fallback for unknown nodes
|
|
294
|
+
console.warn(`Unknown node tag: ${node.tag}`);
|
|
295
|
+
return `/* ${node.tag} */`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Unparse an AST pool (as returned by the parser) to source code
|
|
302
|
+
* @param {object} ast - The AST pool with a root property
|
|
303
|
+
* @param {object} dialectLexicon - The dialect-specific lexicon (optional)
|
|
304
|
+
* @param {object} options - Options for unparsing (e.g., indentSize, compact)
|
|
305
|
+
* @returns {string} The unparsed source code
|
|
306
|
+
*/
|
|
307
|
+
export function unparse(ast, dialectLexicon = {}, options = {}) {
|
|
308
|
+
if (!ast || !ast.root) {
|
|
309
|
+
return "";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Merge basis lexicon with dialect lexicon (dialect takes precedence)
|
|
313
|
+
const mergedLexicon = { ...basisLexicon, ...dialectLexicon };
|
|
314
|
+
|
|
315
|
+
// The AST is in pool format - reconstruct the tree from the root
|
|
316
|
+
const rootId = ast.root;
|
|
317
|
+
const rootNode = reconstructNode(ast, rootId);
|
|
318
|
+
|
|
319
|
+
return unparseNode(rootNode, mergedLexicon, 0, options);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Reconstruct a node from the AST pool format
|
|
324
|
+
* @param {object} pool - The AST pool
|
|
325
|
+
* @param {string|number} nodeId - The node ID to reconstruct
|
|
326
|
+
* @returns {object} The reconstructed node
|
|
327
|
+
*/
|
|
328
|
+
function reconstructNode(pool, nodeId) {
|
|
329
|
+
if (!nodeId || nodeId === "0" || nodeId === 0) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const node = pool[nodeId];
|
|
334
|
+
if (!node) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Create a new node with the same structure
|
|
339
|
+
const result = {
|
|
340
|
+
tag: node.tag,
|
|
341
|
+
elts: []
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Handle different node types
|
|
345
|
+
switch (node.tag) {
|
|
346
|
+
case "NUM":
|
|
347
|
+
case "STR":
|
|
348
|
+
case "IDENT":
|
|
349
|
+
case "BOOL":
|
|
350
|
+
// These nodes have primitive values in elts[0]
|
|
351
|
+
result.elts = [node.elts[0]];
|
|
352
|
+
break;
|
|
353
|
+
|
|
354
|
+
case "NULL":
|
|
355
|
+
// NULL nodes have no elements
|
|
356
|
+
result.elts = [];
|
|
357
|
+
break;
|
|
358
|
+
|
|
359
|
+
default:
|
|
360
|
+
// For all other nodes, recursively reconstruct child nodes
|
|
361
|
+
if (node.elts && Array.isArray(node.elts)) {
|
|
362
|
+
result.elts = node.elts.map(eltId => {
|
|
363
|
+
// Check if this is a node ID (number or string number)
|
|
364
|
+
if (typeof eltId === "number" || (typeof eltId === "string" && /^\d+$/.test(eltId))) {
|
|
365
|
+
// This is a reference to another node in the pool
|
|
366
|
+
return reconstructNode(pool, eltId);
|
|
367
|
+
} else {
|
|
368
|
+
// This is a primitive value
|
|
369
|
+
return eltId;
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return result;
|
|
377
|
+
}
|