@graffiticode/parser 1.0.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/parse.js +104 -16
- package/src/parser.spec.js +145 -0
- package/src/unparse-l0166.spec.js +7 -0
- package/src/unparse.js +252 -251
package/package.json
CHANGED
package/src/parse.js
CHANGED
|
@@ -174,6 +174,49 @@ export const parse = (function () {
|
|
|
174
174
|
const TK_STRSUFFIX = 0xB4;
|
|
175
175
|
const TK_DOTDOT = 0xB5;
|
|
176
176
|
|
|
177
|
+
// Process escape sequences in a string lexeme
|
|
178
|
+
function processEscapeSequences(str) {
|
|
179
|
+
// The string still has backslash escape sequences
|
|
180
|
+
// Process them to get the actual string value
|
|
181
|
+
let result = "";
|
|
182
|
+
let i = 0;
|
|
183
|
+
while (i < str.length) {
|
|
184
|
+
if (str[i] === '\\' && i + 1 < str.length) {
|
|
185
|
+
// Handle escape sequence
|
|
186
|
+
const nextChar = str[i + 1];
|
|
187
|
+
switch (nextChar) {
|
|
188
|
+
case '\\':
|
|
189
|
+
case '"':
|
|
190
|
+
case "'":
|
|
191
|
+
case '`':
|
|
192
|
+
result += nextChar;
|
|
193
|
+
break;
|
|
194
|
+
case 'n':
|
|
195
|
+
result += '\n';
|
|
196
|
+
break;
|
|
197
|
+
case 't':
|
|
198
|
+
result += '\t';
|
|
199
|
+
break;
|
|
200
|
+
case 'r':
|
|
201
|
+
result += '\r';
|
|
202
|
+
break;
|
|
203
|
+
case '$':
|
|
204
|
+
result += '$';
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
// Unknown escape, keep the backslash and character
|
|
208
|
+
result += '\\' + nextChar;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
i += 2;
|
|
212
|
+
} else {
|
|
213
|
+
result += str[i];
|
|
214
|
+
i++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
177
220
|
function tokenToLexeme(tk) {
|
|
178
221
|
switch (tk) {
|
|
179
222
|
case TK_EQUAL: return "a '=' symbol";
|
|
@@ -300,20 +343,24 @@ export const parse = (function () {
|
|
|
300
343
|
function str(ctx, cc) {
|
|
301
344
|
if (match(ctx, TK_STR)) {
|
|
302
345
|
eat(ctx, TK_STR);
|
|
303
|
-
|
|
346
|
+
// Process escape sequences in the lexeme
|
|
347
|
+
const processedStr = processEscapeSequences(lexeme);
|
|
348
|
+
Ast.string(ctx, processedStr, getCoord(ctx)); // strip quotes;
|
|
304
349
|
cc.cls = "string";
|
|
305
350
|
return cc;
|
|
306
351
|
} else if (match(ctx, TK_STRPREFIX)) {
|
|
307
352
|
ctx.state.inStr++;
|
|
308
353
|
eat(ctx, TK_STRPREFIX);
|
|
309
354
|
startCounter(ctx);
|
|
310
|
-
|
|
355
|
+
const processedPrefix = processEscapeSequences(lexeme);
|
|
356
|
+
Ast.string(ctx, processedPrefix, getCoord(ctx)); // strip quotes;
|
|
311
357
|
countCounter(ctx);
|
|
312
358
|
const ret = function (ctx) {
|
|
313
359
|
return strSuffix(ctx, function (ctx) {
|
|
314
360
|
ctx.state.inStr--;
|
|
315
361
|
eat(ctx, TK_STRSUFFIX);
|
|
316
|
-
|
|
362
|
+
const processedSuffix = processEscapeSequences(lexeme);
|
|
363
|
+
Ast.string(ctx, processedSuffix, getCoord(ctx)); // strip quotes;
|
|
317
364
|
countCounter(ctx);
|
|
318
365
|
Ast.list(ctx, ctx.state.exprc);
|
|
319
366
|
stopCounter(ctx);
|
|
@@ -337,7 +384,8 @@ export const parse = (function () {
|
|
|
337
384
|
if (match(ctx, TK_STRMIDDLE)) {
|
|
338
385
|
// Not done yet.
|
|
339
386
|
eat(ctx, TK_STRMIDDLE);
|
|
340
|
-
|
|
387
|
+
const processedMiddle = processEscapeSequences(lexeme);
|
|
388
|
+
Ast.string(ctx, processedMiddle, getCoord(ctx)); // strip quotes;
|
|
341
389
|
countCounter(ctx);
|
|
342
390
|
ret = function (ctx) {
|
|
343
391
|
return strSuffix(ctx, resume);
|
|
@@ -1199,24 +1247,44 @@ export const parse = (function () {
|
|
|
1199
1247
|
lexeme += String.fromCharCode(c);
|
|
1200
1248
|
c = nextCC();
|
|
1201
1249
|
const inTemplateLiteral = quoteChar === CC_BACKTICK;
|
|
1250
|
+
let escaped = false;
|
|
1251
|
+
|
|
1202
1252
|
if (inTemplateLiteral) {
|
|
1203
1253
|
while (
|
|
1204
|
-
c !== quoteChar &&
|
|
1254
|
+
(c !== quoteChar || escaped) &&
|
|
1205
1255
|
c !== 0 &&
|
|
1206
|
-
!(c === CC_DOLLAR && peekCC() === CC_LEFTBRACE)) {
|
|
1207
|
-
|
|
1256
|
+
!(c === CC_DOLLAR && peekCC() === CC_LEFTBRACE && !escaped)) {
|
|
1257
|
+
if (escaped) {
|
|
1258
|
+
// Handle escaped characters
|
|
1259
|
+
lexeme += String.fromCharCode(c);
|
|
1260
|
+
escaped = false;
|
|
1261
|
+
} else if (c === 92) { // backslash
|
|
1262
|
+
lexeme += String.fromCharCode(c);
|
|
1263
|
+
escaped = true;
|
|
1264
|
+
} else {
|
|
1265
|
+
lexeme += String.fromCharCode(c);
|
|
1266
|
+
}
|
|
1208
1267
|
c = nextCC();
|
|
1209
1268
|
}
|
|
1210
1269
|
} else {
|
|
1211
|
-
while (c !== quoteChar && c !== 0) {
|
|
1212
|
-
|
|
1270
|
+
while ((c !== quoteChar || escaped) && c !== 0) {
|
|
1271
|
+
if (escaped) {
|
|
1272
|
+
// Handle escaped characters
|
|
1273
|
+
lexeme += String.fromCharCode(c);
|
|
1274
|
+
escaped = false;
|
|
1275
|
+
} else if (c === 92) { // backslash
|
|
1276
|
+
lexeme += String.fromCharCode(c);
|
|
1277
|
+
escaped = true;
|
|
1278
|
+
} else {
|
|
1279
|
+
lexeme += String.fromCharCode(c);
|
|
1280
|
+
}
|
|
1213
1281
|
c = nextCC();
|
|
1214
1282
|
}
|
|
1215
1283
|
}
|
|
1216
1284
|
const coord = { from: getPos(ctx) - lexeme.length, to: getPos(ctx) };
|
|
1217
1285
|
assertErr(ctx, c !== 0, `Unterminated string: ${lexeme}`, coord);
|
|
1218
1286
|
if (quoteChar === CC_BACKTICK && c === CC_DOLLAR &&
|
|
1219
|
-
peekCC() === CC_LEFTBRACE) {
|
|
1287
|
+
peekCC() === CC_LEFTBRACE && !escaped) {
|
|
1220
1288
|
nextCC(); // Eat CC_LEFTBRACE
|
|
1221
1289
|
lexeme = lexeme.substring(1); // Strip off punct.
|
|
1222
1290
|
return TK_STRPREFIX;
|
|
@@ -1234,21 +1302,41 @@ export const parse = (function () {
|
|
|
1234
1302
|
const quoteChar = quoteCharStack[quoteCharStack.length - 1];
|
|
1235
1303
|
c = nextCC();
|
|
1236
1304
|
const inTemplateLiteral = quoteChar === CC_BACKTICK;
|
|
1305
|
+
let escaped = false;
|
|
1306
|
+
|
|
1237
1307
|
if (inTemplateLiteral) {
|
|
1238
|
-
while (c !== quoteChar && c !== 0 &&
|
|
1308
|
+
while ((c !== quoteChar || escaped) && c !== 0 &&
|
|
1239
1309
|
!(c === CC_DOLLAR &&
|
|
1240
|
-
peekCC() === CC_LEFTBRACE)) {
|
|
1241
|
-
|
|
1310
|
+
peekCC() === CC_LEFTBRACE && !escaped)) {
|
|
1311
|
+
if (escaped) {
|
|
1312
|
+
// Handle escaped characters
|
|
1313
|
+
lexeme += String.fromCharCode(c);
|
|
1314
|
+
escaped = false;
|
|
1315
|
+
} else if (c === 92) { // backslash
|
|
1316
|
+
lexeme += String.fromCharCode(c);
|
|
1317
|
+
escaped = true;
|
|
1318
|
+
} else {
|
|
1319
|
+
lexeme += String.fromCharCode(c);
|
|
1320
|
+
}
|
|
1242
1321
|
c = nextCC();
|
|
1243
1322
|
}
|
|
1244
1323
|
} else {
|
|
1245
|
-
while (c !== quoteChar && c !== 0) {
|
|
1246
|
-
|
|
1324
|
+
while ((c !== quoteChar || escaped) && c !== 0) {
|
|
1325
|
+
if (escaped) {
|
|
1326
|
+
// Handle escaped characters
|
|
1327
|
+
lexeme += String.fromCharCode(c);
|
|
1328
|
+
escaped = false;
|
|
1329
|
+
} else if (c === 92) { // backslash
|
|
1330
|
+
lexeme += String.fromCharCode(c);
|
|
1331
|
+
escaped = true;
|
|
1332
|
+
} else {
|
|
1333
|
+
lexeme += String.fromCharCode(c);
|
|
1334
|
+
}
|
|
1247
1335
|
c = nextCC();
|
|
1248
1336
|
}
|
|
1249
1337
|
}
|
|
1250
1338
|
if (quoteChar === CC_BACKTICK && c === CC_DOLLAR &&
|
|
1251
|
-
peekCC() === CC_LEFTBRACE) {
|
|
1339
|
+
peekCC() === CC_LEFTBRACE && !escaped) {
|
|
1252
1340
|
nextCC(); // Eat brace.
|
|
1253
1341
|
lexeme = lexeme.substring(1); // Strip off leading brace and trailing brace.
|
|
1254
1342
|
return TK_STRMIDDLE;
|
package/src/parser.spec.js
CHANGED
|
@@ -425,4 +425,149 @@ describe("parser integration tests", () => {
|
|
|
425
425
|
expect(found123).toBe(false);
|
|
426
426
|
expect(found456).toBe(false);
|
|
427
427
|
});
|
|
428
|
+
|
|
429
|
+
// Tests for escaped quotes
|
|
430
|
+
it("should parse strings with escaped double quotes", async () => {
|
|
431
|
+
// Arrange & Act
|
|
432
|
+
const result = await parser.parse(0, '"He said \\"Hello\\""..', basisLexicon);
|
|
433
|
+
|
|
434
|
+
// Assert
|
|
435
|
+
expect(result).toHaveProperty("root");
|
|
436
|
+
|
|
437
|
+
// Find the STR node
|
|
438
|
+
let strNode = null;
|
|
439
|
+
for (const key in result) {
|
|
440
|
+
if (key !== "root") {
|
|
441
|
+
const node = result[key];
|
|
442
|
+
if (node.tag === "STR" && node.elts[0] === 'He said "Hello"') {
|
|
443
|
+
strNode = node;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
expect(strNode).not.toBeNull();
|
|
450
|
+
expect(strNode.tag).toBe("STR");
|
|
451
|
+
expect(strNode.elts[0]).toBe('He said "Hello"');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("should parse strings with escaped single quotes", async () => {
|
|
455
|
+
// Arrange & Act
|
|
456
|
+
const result = await parser.parse(0, "'It\\'s working!'..", basisLexicon);
|
|
457
|
+
|
|
458
|
+
// Assert
|
|
459
|
+
expect(result).toHaveProperty("root");
|
|
460
|
+
|
|
461
|
+
// Find the STR node
|
|
462
|
+
let strNode = null;
|
|
463
|
+
for (const key in result) {
|
|
464
|
+
if (key !== "root") {
|
|
465
|
+
const node = result[key];
|
|
466
|
+
if (node.tag === "STR" && node.elts[0] === "It's working!") {
|
|
467
|
+
strNode = node;
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
expect(strNode).not.toBeNull();
|
|
474
|
+
expect(strNode.tag).toBe("STR");
|
|
475
|
+
expect(strNode.elts[0]).toBe("It's working!");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("should parse strings with escaped backticks", async () => {
|
|
479
|
+
// Arrange & Act
|
|
480
|
+
const result = await parser.parse(0, "`This has a \\` backtick`..", basisLexicon);
|
|
481
|
+
|
|
482
|
+
// Assert
|
|
483
|
+
expect(result).toHaveProperty("root");
|
|
484
|
+
|
|
485
|
+
// Find the STR node
|
|
486
|
+
let strNode = null;
|
|
487
|
+
for (const key in result) {
|
|
488
|
+
if (key !== "root") {
|
|
489
|
+
const node = result[key];
|
|
490
|
+
if (node.tag === "STR" && node.elts[0] === "This has a ` backtick") {
|
|
491
|
+
strNode = node;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
expect(strNode).not.toBeNull();
|
|
498
|
+
expect(strNode.tag).toBe("STR");
|
|
499
|
+
expect(strNode.elts[0]).toBe("This has a ` backtick");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("should parse strings with escaped backslashes", async () => {
|
|
503
|
+
// Arrange & Act
|
|
504
|
+
const result = await parser.parse(0, '"Path: C:\\\\Users\\\\Test"..', basisLexicon);
|
|
505
|
+
|
|
506
|
+
// Assert
|
|
507
|
+
expect(result).toHaveProperty("root");
|
|
508
|
+
|
|
509
|
+
// Find the STR node
|
|
510
|
+
let strNode = null;
|
|
511
|
+
for (const key in result) {
|
|
512
|
+
if (key !== "root") {
|
|
513
|
+
const node = result[key];
|
|
514
|
+
if (node.tag === "STR" && node.elts[0] === "Path: C:\\Users\\Test") {
|
|
515
|
+
strNode = node;
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
expect(strNode).not.toBeNull();
|
|
522
|
+
expect(strNode.tag).toBe("STR");
|
|
523
|
+
expect(strNode.elts[0]).toBe("Path: C:\\Users\\Test");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("should parse template literals with escaped interpolation", async () => {
|
|
527
|
+
// Arrange & Act
|
|
528
|
+
const result = await parser.parse(0, "`Price: \\${amount}`..", basisLexicon);
|
|
529
|
+
|
|
530
|
+
// Assert
|
|
531
|
+
expect(result).toHaveProperty("root");
|
|
532
|
+
|
|
533
|
+
// Find the STR node
|
|
534
|
+
let strNode = null;
|
|
535
|
+
for (const key in result) {
|
|
536
|
+
if (key !== "root") {
|
|
537
|
+
const node = result[key];
|
|
538
|
+
if (node.tag === "STR" && node.elts[0] === "Price: ${amount}") {
|
|
539
|
+
strNode = node;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
expect(strNode).not.toBeNull();
|
|
546
|
+
expect(strNode.tag).toBe("STR");
|
|
547
|
+
expect(strNode.elts[0]).toBe("Price: ${amount}");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("should parse strings with mixed escape sequences", async () => {
|
|
551
|
+
// Arrange & Act
|
|
552
|
+
const result = await parser.parse(0, '"Line 1\\nTab\\t\\"Quote\\""..', basisLexicon);
|
|
553
|
+
|
|
554
|
+
// Assert
|
|
555
|
+
expect(result).toHaveProperty("root");
|
|
556
|
+
|
|
557
|
+
// Find the STR node
|
|
558
|
+
let strNode = null;
|
|
559
|
+
for (const key in result) {
|
|
560
|
+
if (key !== "root") {
|
|
561
|
+
const node = result[key];
|
|
562
|
+
if (node.tag === "STR" && node.elts[0] === 'Line 1\nTab\t"Quote"') {
|
|
563
|
+
strNode = node;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
expect(strNode).not.toBeNull();
|
|
570
|
+
expect(strNode.tag).toBe("STR");
|
|
571
|
+
expect(strNode.elts[0]).toBe('Line 1\nTab\t"Quote"');
|
|
572
|
+
});
|
|
428
573
|
});
|
|
@@ -117,6 +117,13 @@ describe("unparse with L0166 lexicon", () => {
|
|
|
117
117
|
"length": 2,
|
|
118
118
|
"arity": 2,
|
|
119
119
|
},
|
|
120
|
+
"row": {
|
|
121
|
+
"tk": 1,
|
|
122
|
+
"name": "ROW",
|
|
123
|
+
"cls": "function",
|
|
124
|
+
"length": 2,
|
|
125
|
+
"arity": 2,
|
|
126
|
+
},
|
|
120
127
|
"column": {
|
|
121
128
|
"tk": 1,
|
|
122
129
|
"name": "COLUMN",
|
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;
|