@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiticode/parser",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
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
- Ast.string(ctx, lexeme, getCoord(ctx)); // strip quotes;
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
- Ast.string(ctx, lexeme, getCoord(ctx)); // strip quotes;
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
- Ast.string(ctx, lexeme, getCoord(ctx)); // strip quotes;
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
- Ast.string(ctx, lexeme, getCoord(ctx)); // strip quotes;
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
- lexeme += String.fromCharCode(c);
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
- lexeme += String.fromCharCode(c);
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
- lexeme += String.fromCharCode(c);
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
- lexeme += String.fromCharCode(c);
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;
@@ -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
- 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 "..";
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
- 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
- }
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
- // For single expression, return as is
65
- if (node.elts.length === 1) {
66
- return unparseNode(node.elts[0], lexicon, indent, opts);
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
- // For multiple expressions, put each on its own line
70
- return node.elts.map(elt => unparseNode(elt, lexicon, indent, opts)).join("\n");
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
- case "NUM":
73
- return node.elts[0];
72
+ case "NUM":
73
+ return node.elts[0];
74
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
- }
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
- case "BOOL":
83
- return node.elts[0] ? "true" : "false";
82
+ case "BOOL":
83
+ return node.elts[0] ? "true" : "false";
84
84
 
85
- case "NULL":
86
- return "null";
85
+ case "NULL":
86
+ return "null";
87
87
 
88
- case "IDENT":
89
- return node.elts[0];
88
+ case "IDENT":
89
+ return node.elts[0];
90
90
 
91
- case "LIST": {
92
- // Array literal [a, b, c]
93
- if (!node.elts || node.elts.length === 0) {
94
- return "[]";
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
- 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
- }
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
- case "RECORD": {
113
- // Object literal {a: 1, b: 2}
114
- if (!node.elts || node.elts.length === 0) {
115
- return "{}";
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
- 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
- }
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
- 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}`;
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
- return "";
144
+ const value = unparseNode(node.elts[1], lexicon, indent, opts);
145
+ return `${key}: ${value}`;
147
146
  }
147
+ return "";
148
+ }
148
149
 
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 "";
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
- 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];
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
- // Extract parameter names
172
- let paramStr = "";
173
- if (params && params.elts) {
174
- paramStr = params.elts.map(p => unparseNode(p, lexicon, indent, opts)).join(" ");
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
- // Unparse body
178
- const bodyStr = unparseNode(body, lexicon, indent, opts);
178
+ // Unparse body
179
+ const bodyStr = unparseNode(body, lexicon, indent, opts);
179
180
 
180
- if (paramStr) {
181
- return `\\${paramStr} . ${bodyStr}`;
182
- } else {
183
- return `\\. ${bodyStr}`;
184
- }
181
+ if (paramStr) {
182
+ return `\\${paramStr} . ${bodyStr}`;
183
+ } else {
184
+ return `\\. ${bodyStr}`;
185
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
- }
186
+ }
187
+ return "";
205
188
 
206
- const bodyStr = unparseNode(body, lexicon, indent, opts);
207
- return `let ${bindingStr} in ${bodyStr}`;
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
- 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);
207
+ const bodyStr = unparseNode(body, lexicon, indent, opts);
208
+ return `let ${bindingStr} in ${bodyStr}`;
209
+ }
210
+ return "";
216
211
 
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 "";
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
- 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(" | ")}`;
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
- return "";
224
+ }
225
+ return "";
234
226
 
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 "";
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
- 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 "";
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
- 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} */`;
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 "/* 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}`;
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
- // 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;
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
- // Fallback for unknown nodes
294
- console.warn(`Unknown node tag: ${node.tag}`);
295
- return `/* ${node.tag} */`;
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
- 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;
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;