@graffiticode/parser 1.1.0 → 1.2.1

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.1.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/unparse.js CHANGED
@@ -28,272 +28,296 @@ 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
+ // Array literal [a, b, c]
93
+ if (!node.elts || node.elts.length === 0) {
94
+ return "[]";
95
+ }
96
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
- }
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) + "]";
110
109
  }
110
+ }
111
111
 
112
- case "RECORD": {
113
- // Object literal {a: 1, b: 2}
114
- if (!node.elts || node.elts.length === 0) {
115
- return "{}";
116
- }
112
+ case "RECORD": {
113
+ // Object literal {a: 1, b: 2}
114
+ if (!node.elts || node.elts.length === 0) {
115
+ return "{}";
116
+ }
117
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
- }
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) + "}";
131
130
  }
131
+ }
132
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}`;
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);
145
142
  }
146
- return "";
143
+ const value = unparseNode(node.elts[1], lexicon, indent, opts);
144
+ return `${key}: ${value}`;
147
145
  }
146
+ return "";
147
+ }
148
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 "";
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
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];
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
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
- }
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
176
 
177
- // Unparse body
178
- const bodyStr = unparseNode(body, lexicon, indent, opts);
177
+ // Unparse body
178
+ const bodyStr = unparseNode(body, lexicon, indent, opts);
179
179
 
180
- if (paramStr) {
181
- return `\\${paramStr} . ${bodyStr}`;
182
- } else {
183
- return `\\. ${bodyStr}`;
184
- }
180
+ if (paramStr) {
181
+ return `\\${paramStr} . ${bodyStr}`;
182
+ } else {
183
+ return `\\. ${bodyStr}`;
185
184
  }
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
- }
185
+ }
186
+ return "";
205
187
 
206
- const bodyStr = unparseNode(body, lexicon, indent, opts);
207
- return `let ${bindingStr} in ${bodyStr}`;
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(", ");
208
204
  }
209
- return "";
210
205
 
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);
206
+ const bodyStr = unparseNode(body, lexicon, indent, opts);
207
+ return `let ${bindingStr} in ${bodyStr}`;
208
+ }
209
+ return "";
216
210
 
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 "";
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 innerIndent = indent + opts.indentSize;
216
+ const indentStr = " ".repeat(innerIndent);
217
+ const thenExpr = unparseNode(node.elts[1], lexicon, innerIndent, opts);
225
218
 
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(" | ")}`;
219
+ if (node.elts.length >= 3) {
220
+ const elseExpr = unparseNode(node.elts[2], lexicon, innerIndent, opts);
221
+ return `if ${cond} then\n${indentStr}${thenExpr}\nelse\n${indentStr}${elseExpr}`;
222
+ } else {
223
+ return `if ${cond} then\n${indentStr}${thenExpr}`;
232
224
  }
233
- return "";
225
+ }
226
+ return "";
234
227
 
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}`;
228
+ case "CASE":
229
+ // Case expression
230
+ if (node.elts && node.elts.length > 0) {
231
+ const expr = unparseNode(node.elts[0], lexicon, indent, opts);
232
+ const innerIndent = indent + opts.indentSize;
233
+ const indentStr = " ".repeat(innerIndent);
234
+ const endIndentStr = " ".repeat(indent);
235
+
236
+ // Process each case branch with proper indentation
237
+ const cases = node.elts.slice(1).map(c => {
238
+ // Pass the inner indent to OF nodes but don't add extra indentation here
239
+ // The OF node will handle its own formatting
240
+ const caseStr = unparseNode(c, lexicon, indent, opts);
241
+ return caseStr;
242
+ });
243
+
244
+ return `case ${expr} of\n${cases.join("\n")}\n${endIndentStr}end`;
245
+ }
246
+ return "";
247
+
248
+ case "OF":
249
+ // Case branch
250
+ if (node.elts && node.elts.length >= 2) {
251
+ const indentStr = " ".repeat(indent + opts.indentSize);
252
+ const pattern = unparseNode(node.elts[0], lexicon, indent, opts);
253
+
254
+ // Check if the expression is a CASE node for proper nested formatting
255
+ const exprNode = node.elts[1];
256
+ let expr;
257
+ if (exprNode && exprNode.tag === "CASE") {
258
+ // For nested case, don't add extra indent to the case keyword itself
259
+ expr = unparseNode(exprNode, lexicon, indent + opts.indentSize, opts);
260
+ } else {
261
+ expr = unparseNode(exprNode, lexicon, indent, opts);
241
262
  }
242
- return "";
263
+
264
+ return `${indentStr}${pattern}: ${expr}`;
265
+ }
266
+ return "";
243
267
 
244
268
  // 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 "";
269
+ case "NEG":
270
+ if (node.elts && node.elts.length >= 1) {
271
+ const expr = unparseNode(node.elts[0], lexicon, indent, opts);
272
+ return `-${expr}`;
273
+ }
274
+ return "";
251
275
 
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} */`;
276
+ case "ERROR":
277
+ // Error nodes - include as comments
278
+ if (node.elts && node.elts.length > 0) {
279
+ // The first element might be a node reference or a string
280
+ const firstElt = node.elts[0];
281
+ if (typeof firstElt === "object" && firstElt.elts) {
282
+ // It's a node, unparse it
283
+ return `/* ERROR: ${unparseNode(firstElt, lexicon, indent, opts)} */`;
262
284
  }
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}`;
285
+ return `/* ERROR: ${firstElt} */`;
286
+ }
287
+ return "/* ERROR */";
288
+
289
+ default: {
290
+ // Check if this is a lexicon-defined function
291
+ // First, find the source name for this tag in the lexicon
292
+ let sourceName = null;
293
+ if (lexicon) {
294
+ for (const [key, value] of Object.entries(lexicon)) {
295
+ if (value && value.name === node.tag) {
296
+ sourceName = key;
297
+ break;
283
298
  }
284
- return sourceName;
285
299
  }
300
+ }
286
301
 
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;
302
+ if (sourceName) {
303
+ // This is a known lexicon function - unparse in prefix notation
304
+ if (node.elts && node.elts.length > 0) {
305
+ const args = node.elts.map(elt => unparseNode(elt, lexicon, indent, opts)).join(" ");
306
+ return `${sourceName} ${args}`;
291
307
  }
308
+ return sourceName;
309
+ }
292
310
 
293
- // Fallback for unknown nodes
294
- console.warn(`Unknown node tag: ${node.tag}`);
295
- return `/* ${node.tag} */`;
311
+ // Handle identifiers that aren't in the lexicon (like lowercase "sub")
312
+ if (node.elts && node.elts.length === 0) {
313
+ // This is likely an identifier
314
+ return node.tag;
296
315
  }
316
+
317
+ // Fallback for unknown nodes
318
+ console.warn(`Unknown node tag: ${node.tag}`);
319
+ return `/* ${node.tag} */`;
320
+ }
297
321
  }
298
322
  }
299
323
 
@@ -343,34 +367,34 @@ function reconstructNode(pool, nodeId) {
343
367
 
344
368
  // Handle different node types
345
369
  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;
370
+ case "NUM":
371
+ case "STR":
372
+ case "IDENT":
373
+ case "BOOL":
374
+ // These nodes have primitive values in elts[0]
375
+ result.elts = [node.elts[0]];
376
+ break;
377
+
378
+ case "NULL":
379
+ // NULL nodes have no elements
380
+ result.elts = [];
381
+ break;
382
+
383
+ default:
384
+ // For all other nodes, recursively reconstruct child nodes
385
+ if (node.elts && Array.isArray(node.elts)) {
386
+ result.elts = node.elts.map(eltId => {
387
+ // Check if this is a node ID (number or string number)
388
+ if (typeof eltId === "number" || (typeof eltId === "string" && /^\d+$/.test(eltId))) {
389
+ // This is a reference to another node in the pool
390
+ return reconstructNode(pool, eltId);
391
+ } else {
392
+ // This is a primitive value
393
+ return eltId;
394
+ }
395
+ });
396
+ }
397
+ break;
374
398
  }
375
399
 
376
400
  return result;
@@ -16,7 +16,7 @@ describe("unparse", () => {
16
16
  it("should unparse string literals", async () => {
17
17
  const source = "'hello, world'..";
18
18
  const unparsed = await testRoundTrip(source);
19
- expect(unparsed).toBe("'hello, world'..");
19
+ expect(unparsed).toBe('"hello, world"..');
20
20
  });
21
21
 
22
22
  it.skip("should unparse string literals with escaped quotes", async () => {
@@ -218,14 +218,14 @@ describe("unparse", () => {
218
218
 
219
219
  it("should unparse if-then-else expression", async () => {
220
220
  const source = "if true then 1 else 2..";
221
- const unparsed = await testRoundTrip(source);
222
- expect(unparsed).toBe("if true then 1 else 2..");
221
+ const unparsed = await testRoundTrip(source, {}, { compact: false });
222
+ expect(unparsed).toBe("if true then\n 1\nelse\n 2..");
223
223
  });
224
224
 
225
225
  it("should unparse nested if expressions", async () => {
226
226
  const source = "if true then (if false then 1 else 2) else 3..";
227
- const unparsed = await testRoundTrip(source);
228
- expect(unparsed).toBe("if true then (if false then 1 else 2) else 3..");
227
+ const unparsed = await testRoundTrip(source, {}, { compact: false });
228
+ expect(unparsed).toBe("if true then\n (if false then\n 1\nelse\n 2)\nelse\n 3..");
229
229
  });
230
230
  });
231
231
 
@@ -328,7 +328,7 @@ describe("unparse", () => {
328
328
  it("should reformat multiple expressions", async () => {
329
329
  const source = "'hello'.[1, 2].{x: 10}..";
330
330
  const reformatted = await parser.reformat(0, source, basisLexicon);
331
- expect(reformatted).toContain("'hello'");
331
+ expect(reformatted).toContain('"hello"');
332
332
  expect(reformatted).toContain("[\n 1");
333
333
  expect(reformatted).toContain("{\n x: 10");
334
334
  expect(reformatted).toContain("..");
@@ -346,5 +346,11 @@ describe("unparse", () => {
346
346
  expect(reformatted).toContain(" 1"); // 4 spaces
347
347
  expect(reformatted).toContain(" 2"); // 4 spaces
348
348
  });
349
+
350
+ it("should preserve escaped quotes in strings", async () => {
351
+ const source = '"\\\"hello\\\""..'
352
+ const reformatted = await parser.reformat(0, source, basisLexicon, { compact: true });
353
+ expect(reformatted).toBe('"\\\"hello\\\""..'); // Should produce identical program
354
+ });
349
355
  });
350
356
  });