@graffiticode/parser 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/unparse.js +252 -251
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiticode/parser",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
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;