@graffiticode/parser 0.1.2 → 0.1.4
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/jest.config.js +5 -0
- package/jest.config.js~ +6 -0
- package/package.json +3 -2
- package/src/ast.js +26 -12
- package/src/folder.js +8 -4
- package/src/parse.js +60 -26
- package/src/parser.js +1 -5
- package/src/parser.spec.js +372 -1
- package/src/parse.spec.js +0 -118
package/jest.config.js
ADDED
package/jest.config.js~
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graffiticode/parser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"lint": "eslint src/",
|
|
16
16
|
"lint:fix": "eslint --fix src/",
|
|
17
|
-
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
|
|
17
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
18
|
+
"console-test": "node console-output-test.js"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [],
|
|
20
21
|
"author": "",
|
package/src/ast.js
CHANGED
|
@@ -148,13 +148,31 @@ export class Ast {
|
|
|
148
148
|
|
|
149
149
|
static poolToJSON(ctx) {
|
|
150
150
|
const nodePool = ctx.state.nodePool;
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
const rootId = nodePool.length - 1;
|
|
152
|
+
// Find all nodes transitively referenced from root
|
|
153
|
+
const referencedNodes = new Set();
|
|
154
|
+
const collectReferences = (nodeId) => {
|
|
155
|
+
if (!nodeId || referencedNodes.has(nodeId)) return;
|
|
156
|
+
referencedNodes.add(nodeId);
|
|
157
|
+
|
|
158
|
+
const node = nodePool[nodeId];
|
|
159
|
+
if (node && node.elts) {
|
|
160
|
+
node.elts.forEach(elt => {
|
|
161
|
+
if (typeof elt === "number") collectReferences(elt);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
collectReferences(rootId);
|
|
166
|
+
return {
|
|
167
|
+
...Object.fromEntries(
|
|
168
|
+
Array.from(referencedNodes).map(id => [id, nodeToJSON(nodePool[id])])
|
|
169
|
+
),
|
|
170
|
+
root: rootId
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static hasSyntaxError(ctx) {
|
|
175
|
+
return ctx.state.errors && ctx.state.errors.length > 0;
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
static dump(n) {
|
|
@@ -666,11 +684,7 @@ export class Ast {
|
|
|
666
684
|
});
|
|
667
685
|
nids.push(word.nid || 0);
|
|
668
686
|
}
|
|
669
|
-
const pattern = env.pattern;
|
|
670
|
-
console.log(
|
|
671
|
-
"lambda()",
|
|
672
|
-
"pattern=" + JSON.stringify(pattern, null, 2),
|
|
673
|
-
);
|
|
687
|
+
// const pattern = env.pattern;
|
|
674
688
|
Ast.push(ctx, {
|
|
675
689
|
tag: "LAMBDA",
|
|
676
690
|
elts: [{
|
package/src/folder.js
CHANGED
|
@@ -184,7 +184,7 @@ export class Folder {
|
|
|
184
184
|
if (word) {
|
|
185
185
|
if (word.cls === "val") {
|
|
186
186
|
if (word.val) {
|
|
187
|
-
Ast.string(ctx, word.val); // strip quotes;
|
|
187
|
+
Ast.string(ctx, word.val, node.coord); // strip quotes;
|
|
188
188
|
} else if (word.nid) {
|
|
189
189
|
let wrd;
|
|
190
190
|
if ((wrd = Ast.node(ctx, word.nid)).tag === "LAMBDA") {
|
|
@@ -225,8 +225,12 @@ export class Folder {
|
|
|
225
225
|
assert(false);
|
|
226
226
|
}
|
|
227
227
|
} else {
|
|
228
|
-
//
|
|
229
|
-
Ast.push(ctx,
|
|
228
|
+
// Tag value.
|
|
229
|
+
Ast.push(ctx, {
|
|
230
|
+
tag: name,
|
|
231
|
+
elts: [],
|
|
232
|
+
coord: node.coord,
|
|
233
|
+
});
|
|
230
234
|
}
|
|
231
235
|
}
|
|
232
236
|
|
|
@@ -235,7 +239,7 @@ export class Folder {
|
|
|
235
239
|
}
|
|
236
240
|
|
|
237
241
|
static str(node) {
|
|
238
|
-
Ast.string(Folder.#ctx, node.elts[0]);
|
|
242
|
+
Ast.string(Folder.#ctx, node.elts[0], node.coord);
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
static bool(node) {
|
package/src/parse.js
CHANGED
|
@@ -71,6 +71,16 @@ export function assertErr(ctx, b, str, coord) {
|
|
|
71
71
|
"assertErr()",
|
|
72
72
|
"str=" + str,
|
|
73
73
|
);
|
|
74
|
+
// Push error into state errors collection
|
|
75
|
+
if (!ctx.state.errors) {
|
|
76
|
+
ctx.state.errors = [];
|
|
77
|
+
}
|
|
78
|
+
ctx.state.errors.push({
|
|
79
|
+
message: str,
|
|
80
|
+
coord
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Create error node in AST
|
|
74
84
|
Ast.error(ctx, str, coord);
|
|
75
85
|
throw new Error(str);
|
|
76
86
|
}
|
|
@@ -79,7 +89,9 @@ export function assertErr(ctx, b, str, coord) {
|
|
|
79
89
|
export const parse = (function () {
|
|
80
90
|
function assert(b, str) {
|
|
81
91
|
if (!b) {
|
|
82
|
-
throw
|
|
92
|
+
// Just throw a simple error with the message "Syntax Error" if none provided
|
|
93
|
+
// This will be caught by our catch-all handler
|
|
94
|
+
throw new Error(str || "Syntax Error");
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
|
|
@@ -160,6 +172,7 @@ export const parse = (function () {
|
|
|
160
172
|
const TK_STRPREFIX = 0xB2;
|
|
161
173
|
const TK_STRMIDDLE = 0xB3;
|
|
162
174
|
const TK_STRSUFFIX = 0xB4;
|
|
175
|
+
const TK_DOTDOT = 0xB5;
|
|
163
176
|
|
|
164
177
|
function tokenToLexeme(tk) {
|
|
165
178
|
switch (tk) {
|
|
@@ -191,6 +204,7 @@ export const parse = (function () {
|
|
|
191
204
|
case TK_PLUS: return "a '+'";
|
|
192
205
|
case TK_MINUS: return "a '-'";
|
|
193
206
|
case TK_DOT: return "a '.'";
|
|
207
|
+
case TK_DOTDOT: return "a '..'";
|
|
194
208
|
case TK_COLON: return "a ':'";
|
|
195
209
|
case TK_COMMA: return "a ','";
|
|
196
210
|
case TK_BACKQUOTE: return "a '`'";
|
|
@@ -205,11 +219,14 @@ export const parse = (function () {
|
|
|
205
219
|
if (nextToken !== tk) {
|
|
206
220
|
const to = getPos(ctx);
|
|
207
221
|
const from = to - lexeme.length;
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
const errorMessage = "Expecting " + tokenToLexeme(tk) +
|
|
223
|
+
", found " + tokenToLexeme(nextToken) + ".";
|
|
224
|
+
|
|
225
|
+
// Use assertErr to ensure error is tracked consistently
|
|
226
|
+
assertErr(ctx, false, errorMessage, { from, to });
|
|
227
|
+
|
|
210
228
|
next(ctx); // Advance past error.
|
|
211
|
-
throw new Error(
|
|
212
|
-
", found " + tokenToLexeme(nextToken) + ".");
|
|
229
|
+
throw new Error(errorMessage);
|
|
213
230
|
}
|
|
214
231
|
}
|
|
215
232
|
|
|
@@ -266,7 +283,7 @@ export const parse = (function () {
|
|
|
266
283
|
function number(ctx, cc) {
|
|
267
284
|
eat(ctx, TK_NUM);
|
|
268
285
|
cc.cls = "number";
|
|
269
|
-
Ast.number(ctx, lexeme
|
|
286
|
+
Ast.number(ctx, lexeme);
|
|
270
287
|
return cc;
|
|
271
288
|
}
|
|
272
289
|
|
|
@@ -281,28 +298,24 @@ export const parse = (function () {
|
|
|
281
298
|
*/
|
|
282
299
|
|
|
283
300
|
function str(ctx, cc) {
|
|
284
|
-
let coord;
|
|
285
301
|
if (match(ctx, TK_STR)) {
|
|
286
302
|
eat(ctx, TK_STR);
|
|
287
|
-
|
|
288
|
-
Ast.string(ctx, lexeme, coord); // strip quotes;
|
|
303
|
+
Ast.string(ctx, lexeme, getCoord(ctx)); // strip quotes;
|
|
289
304
|
cc.cls = "string";
|
|
290
305
|
return cc;
|
|
291
306
|
} else if (match(ctx, TK_STRPREFIX)) {
|
|
292
307
|
ctx.state.inStr++;
|
|
293
308
|
eat(ctx, TK_STRPREFIX);
|
|
294
309
|
startCounter(ctx);
|
|
295
|
-
|
|
296
|
-
Ast.string(ctx, lexeme, coord); // strip quotes;
|
|
310
|
+
Ast.string(ctx, lexeme, getCoord(ctx)); // strip quotes;
|
|
297
311
|
countCounter(ctx);
|
|
298
312
|
const ret = function (ctx) {
|
|
299
313
|
return strSuffix(ctx, function (ctx) {
|
|
300
314
|
ctx.state.inStr--;
|
|
301
315
|
eat(ctx, TK_STRSUFFIX);
|
|
302
|
-
|
|
303
|
-
Ast.string(ctx, lexeme, coord); // strip quotes;
|
|
316
|
+
Ast.string(ctx, lexeme, getCoord(ctx)); // strip quotes;
|
|
304
317
|
countCounter(ctx);
|
|
305
|
-
Ast.list(ctx, ctx.state.exprc
|
|
318
|
+
Ast.list(ctx, ctx.state.exprc);
|
|
306
319
|
stopCounter(ctx);
|
|
307
320
|
Ast.concat(ctx);
|
|
308
321
|
cc.cls = "string";
|
|
@@ -324,8 +337,7 @@ export const parse = (function () {
|
|
|
324
337
|
if (match(ctx, TK_STRMIDDLE)) {
|
|
325
338
|
// Not done yet.
|
|
326
339
|
eat(ctx, TK_STRMIDDLE);
|
|
327
|
-
|
|
328
|
-
Ast.string(ctx, lexeme, coord); // strip quotes;
|
|
340
|
+
Ast.string(ctx, lexeme, getCoord(ctx)); // strip quotes;
|
|
329
341
|
countCounter(ctx);
|
|
330
342
|
ret = function (ctx) {
|
|
331
343
|
return strSuffix(ctx, resume);
|
|
@@ -389,7 +401,7 @@ export const parse = (function () {
|
|
|
389
401
|
offset: ctx.state.paramc,
|
|
390
402
|
nid: 0
|
|
391
403
|
});
|
|
392
|
-
Ast.name(ctx, lexeme
|
|
404
|
+
Ast.name(ctx, lexeme);
|
|
393
405
|
cc.cls = "val";
|
|
394
406
|
return cc;
|
|
395
407
|
}
|
|
@@ -414,8 +426,8 @@ export const parse = (function () {
|
|
|
414
426
|
}
|
|
415
427
|
}
|
|
416
428
|
} else {
|
|
417
|
-
|
|
418
|
-
Ast.
|
|
429
|
+
// Create a tag value.
|
|
430
|
+
Ast.name(ctx, lexeme, coord);
|
|
419
431
|
}
|
|
420
432
|
// assert(cc, "name");
|
|
421
433
|
return cc;
|
|
@@ -505,14 +517,12 @@ export const parse = (function () {
|
|
|
505
517
|
return ret;
|
|
506
518
|
}
|
|
507
519
|
function list(ctx, cc) {
|
|
508
|
-
const coord = getCoord(ctx);
|
|
509
520
|
eat(ctx, TK_LEFTBRACKET);
|
|
510
521
|
startCounter(ctx);
|
|
511
522
|
const ret = function (ctx) {
|
|
512
523
|
return elements(ctx, function (ctx) {
|
|
513
524
|
eat(ctx, TK_RIGHTBRACKET);
|
|
514
|
-
|
|
515
|
-
Ast.list(ctx, ctx.state.exprc, coord);
|
|
525
|
+
Ast.list(ctx, ctx.state.exprc);
|
|
516
526
|
stopCounter(ctx);
|
|
517
527
|
cc.cls = "punc";
|
|
518
528
|
return cc;
|
|
@@ -858,9 +868,17 @@ export const parse = (function () {
|
|
|
858
868
|
return exprsStart(ctx, TK_DOT, function (ctx) {
|
|
859
869
|
let nid;
|
|
860
870
|
while (Ast.peek(ctx) !== nid) {
|
|
871
|
+
console.log(
|
|
872
|
+
"program()",
|
|
873
|
+
"peek()=" + Ast.peek(ctx),
|
|
874
|
+
);
|
|
861
875
|
nid = Ast.pop(ctx);
|
|
862
876
|
folder.fold(ctx, nid); // Fold the exprs on top
|
|
863
877
|
}
|
|
878
|
+
console.log(
|
|
879
|
+
"program()",
|
|
880
|
+
"nodeStack.length=" + ctx.state.nodeStack.length,
|
|
881
|
+
);
|
|
864
882
|
Ast.exprs(ctx, ctx.state.nodeStack.length, true);
|
|
865
883
|
Ast.program(ctx);
|
|
866
884
|
assert(cc === null, "internal error, expecting null continuation");
|
|
@@ -984,7 +1002,7 @@ export const parse = (function () {
|
|
|
984
1002
|
stream.next();
|
|
985
1003
|
}
|
|
986
1004
|
if (cc && !stream.peek()) {
|
|
987
|
-
assertErr(ctx, false, "
|
|
1005
|
+
assertErr(ctx, false, "Missing program terminator.", getCoord(ctx));
|
|
988
1006
|
}
|
|
989
1007
|
} catch (x) {
|
|
990
1008
|
// console.log("catch() x=" + x);
|
|
@@ -993,17 +1011,28 @@ export const parse = (function () {
|
|
|
993
1011
|
cls = x;
|
|
994
1012
|
} else {
|
|
995
1013
|
console.log("catch() x=" + x.stack);
|
|
996
|
-
|
|
1014
|
+
|
|
1015
|
+
// Add generic "Syntax Error" message if not already handled
|
|
1016
|
+
if (!Ast.hasSyntaxError(ctx)) {
|
|
1017
|
+
// Create a generic syntax error
|
|
1018
|
+
const coord = getCoord(ctx) || { from: 0, to: stream.pos || 0 };
|
|
1019
|
+
Ast.error(ctx, "Syntax Error", coord);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
997
1022
|
state.cc = null; // done for now.
|
|
998
1023
|
return Ast.poolToJSON(ctx);
|
|
999
|
-
// cls = "error";
|
|
1000
|
-
// throw new Error(JSON.stringify(window.gcexports.errors, null, 2));
|
|
1001
1024
|
}
|
|
1002
1025
|
} else {
|
|
1003
1026
|
// throw x
|
|
1004
1027
|
next(ctx);
|
|
1005
1028
|
cls = "error";
|
|
1006
1029
|
console.log(x.stack);
|
|
1030
|
+
|
|
1031
|
+
// Add generic "Syntax Error" message for non-Error exceptions too
|
|
1032
|
+
if (!Ast.hasSyntaxError(ctx)) {
|
|
1033
|
+
const coord = getCoord(ctx) || { from: 0, to: stream.pos || 0 };
|
|
1034
|
+
Ast.error(ctx, "Syntax Error", coord);
|
|
1035
|
+
}
|
|
1007
1036
|
}
|
|
1008
1037
|
}
|
|
1009
1038
|
const t1 = new Date();
|
|
@@ -1048,6 +1077,11 @@ export const parse = (function () {
|
|
|
1048
1077
|
case 46: // dot
|
|
1049
1078
|
if (isNumeric(stream.peek())) {
|
|
1050
1079
|
return number(c);
|
|
1080
|
+
// } else if (stream.peek() === 46) {
|
|
1081
|
+
// TODO
|
|
1082
|
+
// stream.next();
|
|
1083
|
+
// lexeme += String.fromCharCode(c);
|
|
1084
|
+
// return TK_DOTDOT;
|
|
1051
1085
|
}
|
|
1052
1086
|
lexeme += String.fromCharCode(c);
|
|
1053
1087
|
return TK_DOT;
|
package/src/parser.js
CHANGED
|
@@ -7,7 +7,7 @@ const main = {
|
|
|
7
7
|
parse(src, lexicon) {
|
|
8
8
|
const stream = new parse.StringStream(src);
|
|
9
9
|
const state = {
|
|
10
|
-
cc: parse.program, // top level
|
|
10
|
+
cc: parse.program, // top level parsig function
|
|
11
11
|
argc: 0,
|
|
12
12
|
argcStack: [0],
|
|
13
13
|
paramc: 0,
|
|
@@ -35,10 +35,6 @@ const main = {
|
|
|
35
35
|
if (state.cc) {
|
|
36
36
|
throw new Error("End of program reached.");
|
|
37
37
|
}
|
|
38
|
-
console.log(
|
|
39
|
-
"parse()",
|
|
40
|
-
"ast=" + JSON.stringify(ast, null, 2),
|
|
41
|
-
);
|
|
42
38
|
return ast;
|
|
43
39
|
}
|
|
44
40
|
};
|
package/src/parser.spec.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
|
-
import { buildParser } from "./parser.js";
|
|
2
|
+
import { buildParser, parser } from "./parser.js";
|
|
3
3
|
import { mockPromiseValue, mockPromiseError } from "./testing/index.js";
|
|
4
|
+
import vm from "vm";
|
|
4
5
|
|
|
5
6
|
describe("lang/parser", () => {
|
|
6
7
|
const log = jest.fn();
|
|
@@ -149,3 +150,373 @@ describe("lang/parser", () => {
|
|
|
149
150
|
await expect(parser.parse(lang, src)).rejects.toBe(err);
|
|
150
151
|
});
|
|
151
152
|
});
|
|
153
|
+
|
|
154
|
+
describe("parser integration tests", () => {
|
|
155
|
+
// Tests using the actual parser
|
|
156
|
+
it("should parse string literals", async () => {
|
|
157
|
+
// Arrange & Act
|
|
158
|
+
const result = await parser.parse(0, "'hello, world'..");
|
|
159
|
+
|
|
160
|
+
// Assert
|
|
161
|
+
expect(result).toHaveProperty("root");
|
|
162
|
+
|
|
163
|
+
// Find the STR node
|
|
164
|
+
let strNode = null;
|
|
165
|
+
for (const key in result) {
|
|
166
|
+
if (key !== "root") {
|
|
167
|
+
const node = result[key];
|
|
168
|
+
if (node.tag === "STR" && node.elts[0] === "hello, world") {
|
|
169
|
+
strNode = node;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
expect(strNode).not.toBeNull();
|
|
176
|
+
expect(strNode.tag).toBe("STR");
|
|
177
|
+
expect(strNode.elts).toEqual(["hello, world"]);
|
|
178
|
+
|
|
179
|
+
// Program structure verification
|
|
180
|
+
const rootId = result.root;
|
|
181
|
+
const rootNode = result[rootId];
|
|
182
|
+
expect(rootNode.tag).toBe("PROG");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should parse numeric literals", async () => {
|
|
186
|
+
// Arrange & Act
|
|
187
|
+
const result = await parser.parse(0, "42..");
|
|
188
|
+
|
|
189
|
+
// Assert
|
|
190
|
+
expect(result).toHaveProperty("root");
|
|
191
|
+
|
|
192
|
+
// Find the NUM node
|
|
193
|
+
let numNode = null;
|
|
194
|
+
for (const key in result) {
|
|
195
|
+
if (key !== "root") {
|
|
196
|
+
const node = result[key];
|
|
197
|
+
if (node.tag === "NUM" && node.elts[0] === "42") {
|
|
198
|
+
numNode = node;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
expect(numNode).not.toBeNull();
|
|
205
|
+
expect(numNode.tag).toBe("NUM");
|
|
206
|
+
expect(numNode.elts).toEqual(["42"]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should have a PROG node at the root", async () => {
|
|
210
|
+
// Let's test the most basic structure that should always work
|
|
211
|
+
const result = await parser.parse(0, "123..");
|
|
212
|
+
|
|
213
|
+
// Assert
|
|
214
|
+
expect(result).toHaveProperty("root");
|
|
215
|
+
|
|
216
|
+
// Verify the structure: we need to have a PROG node at the root
|
|
217
|
+
const rootId = result.root;
|
|
218
|
+
const rootNode = result[rootId];
|
|
219
|
+
expect(rootNode.tag).toBe("PROG");
|
|
220
|
+
|
|
221
|
+
// Find the NUM node for 123
|
|
222
|
+
let numNode = null;
|
|
223
|
+
for (const key in result) {
|
|
224
|
+
if (key !== "root") {
|
|
225
|
+
const node = result[key];
|
|
226
|
+
if (node.tag === "NUM" && node.elts[0] === "123") {
|
|
227
|
+
numNode = node;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
expect(numNode).not.toBeNull();
|
|
234
|
+
expect(numNode.tag).toBe("NUM");
|
|
235
|
+
expect(numNode.elts[0]).toBe("123");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should parse complex program: apply (<a b: add a b>) [10 20]..", async () => {
|
|
239
|
+
// Create parser with custom lexicon
|
|
240
|
+
const customLexiconCache = new Map();
|
|
241
|
+
customLexiconCache.set(0, {
|
|
242
|
+
add: {
|
|
243
|
+
tk: 2,
|
|
244
|
+
name: "add",
|
|
245
|
+
cls: "function",
|
|
246
|
+
length: 2
|
|
247
|
+
},
|
|
248
|
+
apply: {
|
|
249
|
+
tk: 40,
|
|
250
|
+
name: "apply",
|
|
251
|
+
cls: "function",
|
|
252
|
+
length: 2
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Use the parser with our custom cache
|
|
257
|
+
const customParser = buildParser({
|
|
258
|
+
log: console.log,
|
|
259
|
+
cache: customLexiconCache,
|
|
260
|
+
getLangAsset: async () => ({}),
|
|
261
|
+
main: {
|
|
262
|
+
parse: (src, lexicon) => {
|
|
263
|
+
return Promise.resolve(parser.parse(0, src));
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
vm
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Act
|
|
270
|
+
const result = await customParser.parse(0, "apply (<a b: add a b>) [10 20]..");
|
|
271
|
+
|
|
272
|
+
// Assert
|
|
273
|
+
expect(result).toHaveProperty("root");
|
|
274
|
+
|
|
275
|
+
// Verify basic structure
|
|
276
|
+
const rootId = result.root;
|
|
277
|
+
const rootNode = result[rootId];
|
|
278
|
+
expect(rootNode.tag).toBe("PROG");
|
|
279
|
+
|
|
280
|
+
// Find NUM nodes with values 10 and 20
|
|
281
|
+
let num10Node = null;
|
|
282
|
+
let num20Node = null;
|
|
283
|
+
|
|
284
|
+
for (const key in result) {
|
|
285
|
+
if (key !== "root") {
|
|
286
|
+
const node = result[key];
|
|
287
|
+
if (node.tag === "NUM") {
|
|
288
|
+
if (node.elts[0] === "10") {
|
|
289
|
+
num10Node = node;
|
|
290
|
+
} else if (node.elts[0] === "20") {
|
|
291
|
+
num20Node = node;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// At minimum, we should be able to find the number values
|
|
298
|
+
expect(num10Node).not.toBeNull();
|
|
299
|
+
expect(num20Node).not.toBeNull();
|
|
300
|
+
|
|
301
|
+
// Find IDENT nodes with names 'a' and 'b'
|
|
302
|
+
let identNodeA = null;
|
|
303
|
+
let identNodeB = null;
|
|
304
|
+
|
|
305
|
+
for (const key in result) {
|
|
306
|
+
if (key !== "root") {
|
|
307
|
+
const node = result[key];
|
|
308
|
+
if (node.tag === "IDENT") {
|
|
309
|
+
if (node.elts[0] === "a") {
|
|
310
|
+
identNodeA = node;
|
|
311
|
+
} else if (node.elts[0] === "b") {
|
|
312
|
+
identNodeB = node;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check that we found the identifiers
|
|
319
|
+
expect(identNodeA).not.toBeNull();
|
|
320
|
+
expect(identNodeB).not.toBeNull();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("should handle syntax errors with generic error message", async () => {
|
|
324
|
+
// Test various syntax errors and confirm they're caught properly
|
|
325
|
+
let errorNode = null;
|
|
326
|
+
let result = null;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
// Unclosed string - missing closing quote
|
|
330
|
+
result = await parser.parse(0, "'unclosed string..");
|
|
331
|
+
} catch (e) {
|
|
332
|
+
// Check for expected error (we should now have a robust parser that doesn't throw)
|
|
333
|
+
console.error("Unexpected error:", e);
|
|
334
|
+
throw e;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Should get a result even with syntax error
|
|
338
|
+
expect(result).toHaveProperty("root");
|
|
339
|
+
|
|
340
|
+
// Find the ERROR node
|
|
341
|
+
errorNode = null;
|
|
342
|
+
for (const key in result) {
|
|
343
|
+
if (key !== "root") {
|
|
344
|
+
const node = result[key];
|
|
345
|
+
if (node.tag === "ERROR") {
|
|
346
|
+
errorNode = node;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Should have an ERROR node
|
|
353
|
+
expect(errorNode).not.toBeNull();
|
|
354
|
+
expect(errorNode.tag).toBe("ERROR");
|
|
355
|
+
|
|
356
|
+
// The error message should be either the specific syntax error or the generic "Syntax Error"
|
|
357
|
+
// Verify that we have an ERROR node with the proper structure
|
|
358
|
+
expect(errorNode.elts.length).toBeGreaterThan(0);
|
|
359
|
+
|
|
360
|
+
// The error structure might be different based on implementation details
|
|
361
|
+
// We just want to ensure there's an error node in the result
|
|
362
|
+
expect(errorNode.tag).toBe("ERROR");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("should handle mismatched brackets with syntax error", async () => {
|
|
366
|
+
let result = null;
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
// Missing closing bracket
|
|
370
|
+
result = await parser.parse(0, "[1, 2, 3..");
|
|
371
|
+
} catch (e) {
|
|
372
|
+
console.error("Unexpected error:", e);
|
|
373
|
+
throw e;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Should get a result even with syntax error
|
|
377
|
+
expect(result).toHaveProperty("root");
|
|
378
|
+
|
|
379
|
+
// Find the ERROR node
|
|
380
|
+
let errorNode = null;
|
|
381
|
+
for (const key in result) {
|
|
382
|
+
if (key !== "root") {
|
|
383
|
+
const node = result[key];
|
|
384
|
+
if (node.tag === "ERROR") {
|
|
385
|
+
errorNode = node;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Should have an ERROR node
|
|
392
|
+
expect(errorNode).not.toBeNull();
|
|
393
|
+
expect(errorNode.tag).toBe("ERROR");
|
|
394
|
+
|
|
395
|
+
// The error message should indicate the syntax error
|
|
396
|
+
// Verify that we have an ERROR node with the proper structure
|
|
397
|
+
expect(errorNode.elts.length).toBeGreaterThan(0);
|
|
398
|
+
|
|
399
|
+
// The error structure might be different based on implementation details
|
|
400
|
+
// We just want to ensure there's an error node in the result
|
|
401
|
+
expect(errorNode.tag).toBe("ERROR");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should handle invalid token sequences with syntax error", async () => {
|
|
405
|
+
let result = null;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
// Invalid sequence of tokens
|
|
409
|
+
result = await parser.parse(0, "if then else..");
|
|
410
|
+
} catch (e) {
|
|
411
|
+
console.error("Unexpected error:", e);
|
|
412
|
+
throw e;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Should get a result even with syntax error
|
|
416
|
+
expect(result).toHaveProperty("root");
|
|
417
|
+
|
|
418
|
+
// Find the ERROR node
|
|
419
|
+
let errorNode = null;
|
|
420
|
+
for (const key in result) {
|
|
421
|
+
if (key !== "root") {
|
|
422
|
+
const node = result[key];
|
|
423
|
+
if (node.tag === "ERROR") {
|
|
424
|
+
errorNode = node;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Should have an ERROR node
|
|
431
|
+
expect(errorNode).not.toBeNull();
|
|
432
|
+
expect(errorNode.tag).toBe("ERROR");
|
|
433
|
+
|
|
434
|
+
// The error message should indicate the syntax error
|
|
435
|
+
// Verify that we have an ERROR node with the proper structure
|
|
436
|
+
expect(errorNode.elts.length).toBeGreaterThan(0);
|
|
437
|
+
|
|
438
|
+
// The error structure might be different based on implementation details
|
|
439
|
+
// We just want to ensure there's an error node in the result
|
|
440
|
+
expect(errorNode.tag).toBe("ERROR");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("should perform parse-time evaluation for adding two numbers", async () => {
|
|
444
|
+
// Create parser with custom lexicon that defines 'add' function
|
|
445
|
+
const customLexiconCache = new Map();
|
|
446
|
+
customLexiconCache.set(0, {
|
|
447
|
+
add: {
|
|
448
|
+
tk: 2,
|
|
449
|
+
name: "add",
|
|
450
|
+
cls: "function",
|
|
451
|
+
length: 2
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Use the parser with our custom cache
|
|
456
|
+
const customParser = buildParser({
|
|
457
|
+
log: console.log,
|
|
458
|
+
cache: customLexiconCache,
|
|
459
|
+
getLangAsset: async () => ({}),
|
|
460
|
+
main: {
|
|
461
|
+
parse: (src, lexicon) => {
|
|
462
|
+
return Promise.resolve(parser.parse(0, src));
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
vm
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Act - parse a simple addition expression
|
|
469
|
+
const result = await customParser.parse(0, "add 123 456..");
|
|
470
|
+
console.log(
|
|
471
|
+
"TEST",
|
|
472
|
+
"result=" + JSON.stringify(result, null, 2),
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Assert
|
|
476
|
+
expect(result).toHaveProperty("root");
|
|
477
|
+
|
|
478
|
+
// Verify basic structure
|
|
479
|
+
const rootId = result.root;
|
|
480
|
+
const rootNode = result[rootId];
|
|
481
|
+
expect(rootNode.tag).toBe("PROG");
|
|
482
|
+
|
|
483
|
+
// Find the result node - expecting a single NUM node with the sum
|
|
484
|
+
let resultNode = null;
|
|
485
|
+
|
|
486
|
+
// Check all nodes for the result of evaluation (123 + 456 = 579)
|
|
487
|
+
for (const key in result) {
|
|
488
|
+
if (key !== "root") {
|
|
489
|
+
const node = result[key];
|
|
490
|
+
if (node.tag === "NUM" && node.elts[0] === "579") {
|
|
491
|
+
resultNode = node;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// We should find a node with the computed value (579)
|
|
498
|
+
expect(resultNode).not.toBeNull();
|
|
499
|
+
expect(resultNode.tag).toBe("NUM");
|
|
500
|
+
expect(resultNode.elts[0]).toBe("579");
|
|
501
|
+
|
|
502
|
+
// The original numbers should not be in the final AST
|
|
503
|
+
// if parse-time evaluation is working correctly
|
|
504
|
+
let found123 = false;
|
|
505
|
+
let found456 = false;
|
|
506
|
+
|
|
507
|
+
for (const key in result) {
|
|
508
|
+
if (key !== "root") {
|
|
509
|
+
const node = result[key];
|
|
510
|
+
if (node.tag === "NUM") {
|
|
511
|
+
if (node.elts[0] === "123") found123 = true;
|
|
512
|
+
if (node.elts[0] === "456") found456 = true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// The original operands should not be in the final AST
|
|
518
|
+
// if they were properly evaluated at parse time
|
|
519
|
+
expect(found123).toBe(false);
|
|
520
|
+
expect(found456).toBe(false);
|
|
521
|
+
});
|
|
522
|
+
});
|
package/src/parse.spec.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { buildParser } from "./parser.js";
|
|
2
|
-
import { mockPromiseValue } from "./testing/index.js";
|
|
3
|
-
|
|
4
|
-
describe("parse operations", () => {
|
|
5
|
-
const log = jest.fn();
|
|
6
|
-
|
|
7
|
-
it("should parse 'apply (<a b: add a b>) [10 20]'", async () => {
|
|
8
|
-
// Arrange
|
|
9
|
-
const cache = new Map();
|
|
10
|
-
const getLangAsset = mockPromiseValue("{}");
|
|
11
|
-
const main = {
|
|
12
|
-
parse: mockPromiseValue({
|
|
13
|
-
1: {
|
|
14
|
-
elts: ["a"],
|
|
15
|
-
tag: "IDENT"
|
|
16
|
-
},
|
|
17
|
-
2: {
|
|
18
|
-
elts: ["b"],
|
|
19
|
-
tag: "IDENT"
|
|
20
|
-
},
|
|
21
|
-
3: {
|
|
22
|
-
elts: [1, 2],
|
|
23
|
-
tag: "ADD"
|
|
24
|
-
},
|
|
25
|
-
4: {
|
|
26
|
-
elts: [3],
|
|
27
|
-
tag: "EXPRS"
|
|
28
|
-
},
|
|
29
|
-
5: {
|
|
30
|
-
elts: [1, 2],
|
|
31
|
-
tag: "LIST"
|
|
32
|
-
},
|
|
33
|
-
6: {
|
|
34
|
-
elts: [5, 4],
|
|
35
|
-
tag: "LAMBDA"
|
|
36
|
-
},
|
|
37
|
-
7: {
|
|
38
|
-
elts: ["10"],
|
|
39
|
-
tag: "NUM"
|
|
40
|
-
},
|
|
41
|
-
8: {
|
|
42
|
-
elts: ["20"],
|
|
43
|
-
tag: "NUM"
|
|
44
|
-
},
|
|
45
|
-
9: {
|
|
46
|
-
elts: [7, 8],
|
|
47
|
-
tag: "LIST"
|
|
48
|
-
},
|
|
49
|
-
10: {
|
|
50
|
-
elts: [6, 9],
|
|
51
|
-
tag: "APPLY"
|
|
52
|
-
},
|
|
53
|
-
11: {
|
|
54
|
-
elts: [10],
|
|
55
|
-
tag: "EXPRS"
|
|
56
|
-
},
|
|
57
|
-
12: {
|
|
58
|
-
elts: [11],
|
|
59
|
-
tag: "PROG"
|
|
60
|
-
},
|
|
61
|
-
root: 12
|
|
62
|
-
})
|
|
63
|
-
};
|
|
64
|
-
const parser = buildParser({ log, cache, getLangAsset, main });
|
|
65
|
-
const lang = "0002";
|
|
66
|
-
const src = "apply (<a b: add a b>) [10 20]..";
|
|
67
|
-
|
|
68
|
-
// Act
|
|
69
|
-
const result = await parser.parse(lang, src);
|
|
70
|
-
|
|
71
|
-
// Assert
|
|
72
|
-
expect(result).toHaveProperty("root", 12);
|
|
73
|
-
expect(result[10].tag).toBe("APPLY");
|
|
74
|
-
expect(result[10].elts).toEqual([6, 9]);
|
|
75
|
-
expect(result[6].tag).toBe("LAMBDA");
|
|
76
|
-
expect(result[9].tag).toBe("LIST");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("should parse 'hello, world!'", async () => {
|
|
80
|
-
// Arrange
|
|
81
|
-
const cache = new Map();
|
|
82
|
-
const getLangAsset = mockPromiseValue("{}");
|
|
83
|
-
const main = {
|
|
84
|
-
parse: mockPromiseValue({
|
|
85
|
-
1: {
|
|
86
|
-
elts: [
|
|
87
|
-
"hello, world"
|
|
88
|
-
],
|
|
89
|
-
tag: "STR"
|
|
90
|
-
},
|
|
91
|
-
2: {
|
|
92
|
-
elts: [
|
|
93
|
-
1
|
|
94
|
-
],
|
|
95
|
-
tag: "EXPRS"
|
|
96
|
-
},
|
|
97
|
-
3: {
|
|
98
|
-
elts: [
|
|
99
|
-
2
|
|
100
|
-
],
|
|
101
|
-
tag: "PROG"
|
|
102
|
-
},
|
|
103
|
-
root: 3
|
|
104
|
-
})
|
|
105
|
-
};
|
|
106
|
-
const parser = buildParser({ log, cache, getLangAsset, main });
|
|
107
|
-
const lang = "0002";
|
|
108
|
-
const src = "'hello, world'..";
|
|
109
|
-
|
|
110
|
-
// Act
|
|
111
|
-
const result = await parser.parse(lang, src);
|
|
112
|
-
|
|
113
|
-
// Assert
|
|
114
|
-
expect(result).toHaveProperty("root", 3);
|
|
115
|
-
expect(result[1].tag).toBe("STR");
|
|
116
|
-
expect(result[1].elts).toEqual(["hello, world"]);
|
|
117
|
-
});
|
|
118
|
-
});
|