@graffiticode/parser 0.1.1 → 0.1.3
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 +6 -7
- package/src/ast.js +42 -26
- package/src/env.js +8 -4
- package/src/folder.js +12 -4
- package/src/folder.spec.js +42 -0
- package/src/parse.js +16 -21
- package/src/parse.spec.js~ +119 -0
- package/src/parser.js +0 -4
- package/src/parser.spec.js +260 -33
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.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -8,18 +8,17 @@
|
|
|
8
8
|
"main": "./src/index.js",
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"eslint": "^8.38.0",
|
|
11
|
-
"jest": "^29.
|
|
11
|
+
"jest": "^29.7.0",
|
|
12
12
|
"supertest": "^6.3.3"
|
|
13
13
|
},
|
|
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": "",
|
|
21
|
-
"license": "
|
|
22
|
-
"description": ""
|
|
23
|
-
"dependencies": {
|
|
24
|
-
}
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"description": ""
|
|
25
24
|
}
|
package/src/ast.js
CHANGED
|
@@ -20,23 +20,23 @@ function nodeToJSON(n) {
|
|
|
20
20
|
let obj;
|
|
21
21
|
if (typeof n === "object") {
|
|
22
22
|
switch (n.tag) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
case "num":
|
|
24
|
+
obj = n.elts[0];
|
|
25
|
+
break;
|
|
26
|
+
case "str":
|
|
27
|
+
obj = n.elts[0];
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
obj = {};
|
|
31
|
+
obj.tag = n.tag;
|
|
32
|
+
obj.elts = [];
|
|
33
|
+
if (n.coord) {
|
|
34
|
+
obj.coord = n.coord;
|
|
35
|
+
}
|
|
36
|
+
for (let i = 0; i < n.elts.length; i++) {
|
|
37
|
+
obj.elts[i] = nodeToJSON(n.elts[i]);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
40
|
}
|
|
41
41
|
} else if (typeof n === "string") {
|
|
42
42
|
obj = n;
|
|
@@ -148,13 +148,27 @@ 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
|
+
};
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
static dump(n) {
|
|
@@ -666,7 +680,7 @@ export class Ast {
|
|
|
666
680
|
});
|
|
667
681
|
nids.push(word.nid || 0);
|
|
668
682
|
}
|
|
669
|
-
const pattern = env.pattern;
|
|
683
|
+
// const pattern = env.pattern;
|
|
670
684
|
Ast.push(ctx, {
|
|
671
685
|
tag: "LAMBDA",
|
|
672
686
|
elts: [{
|
|
@@ -674,7 +688,9 @@ export class Ast {
|
|
|
674
688
|
elts: names
|
|
675
689
|
}, nid, {
|
|
676
690
|
tag: "LIST",
|
|
677
|
-
elts:
|
|
691
|
+
elts: [
|
|
692
|
+
// pattern // FIXME
|
|
693
|
+
],
|
|
678
694
|
}, {
|
|
679
695
|
tag: "LIST",
|
|
680
696
|
elts: nids
|
package/src/env.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// env
|
|
2
2
|
|
|
3
|
+
function topEnv(ctx) {
|
|
4
|
+
return ctx.state.env[ctx.state.env.length - 1];
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
export class Env {
|
|
4
8
|
static findWord(ctx, lexeme) {
|
|
5
9
|
const env = ctx.state.env;
|
|
@@ -13,12 +17,12 @@ export class Env {
|
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
static addWord(ctx, lexeme, entry) {
|
|
16
|
-
|
|
20
|
+
topEnv(ctx).lexicon[lexeme] = entry;
|
|
17
21
|
return null;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
static addPattern(ctx, pattern) {
|
|
21
|
-
|
|
25
|
+
topEnv(ctx).pattern.push(pattern);
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
static enterEnv(ctx, name) {
|
|
@@ -31,7 +35,7 @@ export class Env {
|
|
|
31
35
|
// return; // just stop recursing
|
|
32
36
|
throw new Error("runaway recursion");
|
|
33
37
|
}
|
|
34
|
-
|
|
38
|
+
topEnv(ctx).paramc = ctx.state.paramc;
|
|
35
39
|
ctx.state.env.push({
|
|
36
40
|
name,
|
|
37
41
|
lexicon: {},
|
|
@@ -41,6 +45,6 @@ export class Env {
|
|
|
41
45
|
|
|
42
46
|
static exitEnv(ctx) {
|
|
43
47
|
ctx.state.env.pop();
|
|
44
|
-
ctx.state.paramc =
|
|
48
|
+
ctx.state.paramc = topEnv(ctx).paramc;
|
|
45
49
|
}
|
|
46
50
|
}
|
package/src/folder.js
CHANGED
|
@@ -203,11 +203,16 @@ export class Folder {
|
|
|
203
203
|
const elts = [];
|
|
204
204
|
for (let i = 0; i < word.length; i++) {
|
|
205
205
|
const elt = Ast.pop(ctx);
|
|
206
|
-
assertErr(
|
|
206
|
+
assertErr(
|
|
207
|
+
ctx,
|
|
208
|
+
elt,
|
|
209
|
+
`Too few arguments for ${word.name}. Expected ${word.length}.`,
|
|
210
|
+
node.coord
|
|
211
|
+
);
|
|
207
212
|
elts.push(elt);
|
|
208
213
|
}
|
|
209
214
|
if (word.nid) {
|
|
210
|
-
Ast.
|
|
215
|
+
Ast.foldApply(ctx, word, elts);
|
|
211
216
|
} else {
|
|
212
217
|
Ast.push(ctx, {
|
|
213
218
|
tag: word.name,
|
|
@@ -220,8 +225,11 @@ export class Folder {
|
|
|
220
225
|
assert(false);
|
|
221
226
|
}
|
|
222
227
|
} else {
|
|
223
|
-
//
|
|
224
|
-
Ast.push(ctx,
|
|
228
|
+
// Tag value.
|
|
229
|
+
Ast.push(ctx, {
|
|
230
|
+
tag: name,
|
|
231
|
+
elts: [],
|
|
232
|
+
});
|
|
225
233
|
}
|
|
226
234
|
}
|
|
227
235
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Folder } from "./folder.js";
|
|
2
|
+
import { Ast } from "./ast.js";
|
|
3
|
+
|
|
4
|
+
describe("folder", () => {
|
|
5
|
+
it("should fold 'add 2 3' to 5", () => {
|
|
6
|
+
// Arrange
|
|
7
|
+
const ctx = {
|
|
8
|
+
state: {
|
|
9
|
+
nodePool: ["unused"],
|
|
10
|
+
nodeStack: [],
|
|
11
|
+
nodeStackStack: [],
|
|
12
|
+
nodeMap: {},
|
|
13
|
+
env: [{ name: "global", lexicon: {} }]
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Create nodes for "add 2 3"
|
|
18
|
+
const n1Id = Ast.intern(ctx, {
|
|
19
|
+
tag: "NUM",
|
|
20
|
+
elts: ["2"]
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const n2Id = Ast.intern(ctx, {
|
|
24
|
+
tag: "NUM",
|
|
25
|
+
elts: ["3"]
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const addNodeId = Ast.intern(ctx, {
|
|
29
|
+
tag: "ADD",
|
|
30
|
+
elts: [n1Id, n2Id]
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Act
|
|
34
|
+
Folder.fold(ctx, addNodeId);
|
|
35
|
+
const resultId = ctx.state.nodeStack.pop();
|
|
36
|
+
const resultNode = ctx.state.nodePool[resultId];
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(resultNode.tag).toBe("NUM");
|
|
40
|
+
expect(resultNode.elts[0]).toBe("5");
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/parse.js
CHANGED
|
@@ -239,7 +239,7 @@ export const parse = (function () {
|
|
|
239
239
|
scanTime += (t1 - t0);
|
|
240
240
|
ctx.state.nextToken = tk;
|
|
241
241
|
const to = getPos(ctx);
|
|
242
|
-
ctx.state.nextTokenCoord = {from, to};
|
|
242
|
+
ctx.state.nextTokenCoord = { from, to };
|
|
243
243
|
} else {
|
|
244
244
|
tk = nextToken;
|
|
245
245
|
}
|
|
@@ -266,7 +266,7 @@ export const parse = (function () {
|
|
|
266
266
|
function number(ctx, cc) {
|
|
267
267
|
eat(ctx, TK_NUM);
|
|
268
268
|
cc.cls = "number";
|
|
269
|
-
Ast.number(ctx, lexeme
|
|
269
|
+
Ast.number(ctx, lexeme);
|
|
270
270
|
return cc;
|
|
271
271
|
}
|
|
272
272
|
|
|
@@ -281,28 +281,24 @@ export const parse = (function () {
|
|
|
281
281
|
*/
|
|
282
282
|
|
|
283
283
|
function str(ctx, cc) {
|
|
284
|
-
let coord;
|
|
285
284
|
if (match(ctx, TK_STR)) {
|
|
286
285
|
eat(ctx, TK_STR);
|
|
287
|
-
|
|
288
|
-
Ast.string(ctx, lexeme, coord); // strip quotes;
|
|
286
|
+
Ast.string(ctx, lexeme); // strip quotes;
|
|
289
287
|
cc.cls = "string";
|
|
290
288
|
return cc;
|
|
291
289
|
} else if (match(ctx, TK_STRPREFIX)) {
|
|
292
290
|
ctx.state.inStr++;
|
|
293
291
|
eat(ctx, TK_STRPREFIX);
|
|
294
292
|
startCounter(ctx);
|
|
295
|
-
|
|
296
|
-
Ast.string(ctx, lexeme, coord); // strip quotes;
|
|
293
|
+
Ast.string(ctx, lexeme); // strip quotes;
|
|
297
294
|
countCounter(ctx);
|
|
298
295
|
const ret = function (ctx) {
|
|
299
296
|
return strSuffix(ctx, function (ctx) {
|
|
300
297
|
ctx.state.inStr--;
|
|
301
298
|
eat(ctx, TK_STRSUFFIX);
|
|
302
|
-
|
|
303
|
-
Ast.string(ctx, lexeme, coord); // strip quotes;
|
|
299
|
+
Ast.string(ctx, lexeme); // strip quotes;
|
|
304
300
|
countCounter(ctx);
|
|
305
|
-
Ast.list(ctx, ctx.state.exprc
|
|
301
|
+
Ast.list(ctx, ctx.state.exprc);
|
|
306
302
|
stopCounter(ctx);
|
|
307
303
|
Ast.concat(ctx);
|
|
308
304
|
cc.cls = "string";
|
|
@@ -324,8 +320,7 @@ export const parse = (function () {
|
|
|
324
320
|
if (match(ctx, TK_STRMIDDLE)) {
|
|
325
321
|
// Not done yet.
|
|
326
322
|
eat(ctx, TK_STRMIDDLE);
|
|
327
|
-
|
|
328
|
-
Ast.string(ctx, lexeme, coord); // strip quotes;
|
|
323
|
+
Ast.string(ctx, lexeme); // strip quotes;
|
|
329
324
|
countCounter(ctx);
|
|
330
325
|
ret = function (ctx) {
|
|
331
326
|
return strSuffix(ctx, resume);
|
|
@@ -355,7 +350,7 @@ export const parse = (function () {
|
|
|
355
350
|
function bindingName(ctx, cc) {
|
|
356
351
|
if (match(ctx, TK_IDENT)) {
|
|
357
352
|
eat(ctx, TK_IDENT);
|
|
358
|
-
Ast.string(ctx, lexeme
|
|
353
|
+
Ast.string(ctx, lexeme);
|
|
359
354
|
cc.cls = "variable";
|
|
360
355
|
return cc;
|
|
361
356
|
}
|
|
@@ -389,7 +384,7 @@ export const parse = (function () {
|
|
|
389
384
|
offset: ctx.state.paramc,
|
|
390
385
|
nid: 0
|
|
391
386
|
});
|
|
392
|
-
Ast.name(ctx, lexeme
|
|
387
|
+
Ast.name(ctx, lexeme);
|
|
393
388
|
cc.cls = "val";
|
|
394
389
|
return cc;
|
|
395
390
|
}
|
|
@@ -414,8 +409,10 @@ export const parse = (function () {
|
|
|
414
409
|
}
|
|
415
410
|
}
|
|
416
411
|
} else {
|
|
417
|
-
cc.cls = "error";
|
|
418
|
-
Ast.error(ctx, "Name '" + lexeme + "' not found.", coord);
|
|
412
|
+
// cc.cls = "error";
|
|
413
|
+
// Ast.error(ctx, "Name '" + lexeme + "' not found.", coord);
|
|
414
|
+
// Create a tag value.
|
|
415
|
+
Ast.name(ctx, lexeme, coord);
|
|
419
416
|
}
|
|
420
417
|
// assert(cc, "name");
|
|
421
418
|
return cc;
|
|
@@ -505,14 +502,12 @@ export const parse = (function () {
|
|
|
505
502
|
return ret;
|
|
506
503
|
}
|
|
507
504
|
function list(ctx, cc) {
|
|
508
|
-
const coord = getCoord(ctx);
|
|
509
505
|
eat(ctx, TK_LEFTBRACKET);
|
|
510
506
|
startCounter(ctx);
|
|
511
507
|
const ret = function (ctx) {
|
|
512
508
|
return elements(ctx, function (ctx) {
|
|
513
509
|
eat(ctx, TK_RIGHTBRACKET);
|
|
514
|
-
|
|
515
|
-
Ast.list(ctx, ctx.state.exprc, coord);
|
|
510
|
+
Ast.list(ctx, ctx.state.exprc);
|
|
516
511
|
stopCounter(ctx);
|
|
517
512
|
cc.cls = "punc";
|
|
518
513
|
return cc;
|
|
@@ -984,7 +979,7 @@ export const parse = (function () {
|
|
|
984
979
|
stream.next();
|
|
985
980
|
}
|
|
986
981
|
if (cc && !stream.peek()) {
|
|
987
|
-
assertErr(ctx, false, "
|
|
982
|
+
assertErr(ctx, false, "Missing program terminator.", getCoord(ctx));
|
|
988
983
|
}
|
|
989
984
|
} catch (x) {
|
|
990
985
|
// console.log("catch() x=" + x);
|
|
@@ -1166,7 +1161,7 @@ export const parse = (function () {
|
|
|
1166
1161
|
c = nextCC();
|
|
1167
1162
|
}
|
|
1168
1163
|
}
|
|
1169
|
-
const coord = {from: getPos(ctx) - lexeme.length, to: getPos(ctx)};
|
|
1164
|
+
const coord = { from: getPos(ctx) - lexeme.length, to: getPos(ctx) };
|
|
1170
1165
|
assertErr(ctx, c !== 0, `Unterminated string: ${lexeme}`, coord);
|
|
1171
1166
|
if (quoteChar === CC_BACKTICK && c === CC_DOLLAR &&
|
|
1172
1167
|
peekCC() === CC_LEFTBRACE) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { parser, buildParser } from "./parser.js";
|
|
3
|
+
import { mockPromiseValue, mockPromiseError } from "./testing/index.js";
|
|
4
|
+
|
|
5
|
+
describe("parse operations", () => {
|
|
6
|
+
const log = jest.fn();
|
|
7
|
+
|
|
8
|
+
it("should parse 'apply (<a b: add a b>) [10 20]'", async () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
const getLangAsset = mockPromiseValue("{}");
|
|
12
|
+
const main = {
|
|
13
|
+
parse: mockPromiseValue({
|
|
14
|
+
1: {
|
|
15
|
+
elts: ["a"],
|
|
16
|
+
tag: "IDENT"
|
|
17
|
+
},
|
|
18
|
+
2: {
|
|
19
|
+
elts: ["b"],
|
|
20
|
+
tag: "IDENT"
|
|
21
|
+
},
|
|
22
|
+
3: {
|
|
23
|
+
elts: [1, 2],
|
|
24
|
+
tag: "ADD"
|
|
25
|
+
},
|
|
26
|
+
4: {
|
|
27
|
+
elts: [3],
|
|
28
|
+
tag: "EXPRS"
|
|
29
|
+
},
|
|
30
|
+
5: {
|
|
31
|
+
elts: [1, 2],
|
|
32
|
+
tag: "LIST"
|
|
33
|
+
},
|
|
34
|
+
6: {
|
|
35
|
+
elts: [5, 4],
|
|
36
|
+
tag: "LAMBDA"
|
|
37
|
+
},
|
|
38
|
+
7: {
|
|
39
|
+
elts: ["10"],
|
|
40
|
+
tag: "NUM"
|
|
41
|
+
},
|
|
42
|
+
8: {
|
|
43
|
+
elts: ["20"],
|
|
44
|
+
tag: "NUM"
|
|
45
|
+
},
|
|
46
|
+
9: {
|
|
47
|
+
elts: [7, 8],
|
|
48
|
+
tag: "LIST"
|
|
49
|
+
},
|
|
50
|
+
10: {
|
|
51
|
+
elts: [6, 9],
|
|
52
|
+
tag: "APPLY"
|
|
53
|
+
},
|
|
54
|
+
11: {
|
|
55
|
+
elts: [10],
|
|
56
|
+
tag: "EXPRS"
|
|
57
|
+
},
|
|
58
|
+
12: {
|
|
59
|
+
elts: [11],
|
|
60
|
+
tag: "PROG"
|
|
61
|
+
},
|
|
62
|
+
root: 12
|
|
63
|
+
})
|
|
64
|
+
};
|
|
65
|
+
const parser = buildParser({ log, cache, getLangAsset, main });
|
|
66
|
+
const lang = "0";
|
|
67
|
+
const src = "apply (<a b: add a b>) [10 20]..";
|
|
68
|
+
|
|
69
|
+
// Act
|
|
70
|
+
const result = await parser.parse(lang, src);
|
|
71
|
+
|
|
72
|
+
// Assert
|
|
73
|
+
expect(result).toHaveProperty("root", 12);
|
|
74
|
+
expect(result[10].tag).toBe("APPLY");
|
|
75
|
+
expect(result[10].elts).toEqual([6, 9]);
|
|
76
|
+
expect(result[6].tag).toBe("LAMBDA");
|
|
77
|
+
expect(result[9].tag).toBe("LIST");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should parse 'hello, world!'", async () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
const cache = new Map();
|
|
83
|
+
const getLangAsset = mockPromiseValue("{}");
|
|
84
|
+
const main = {
|
|
85
|
+
parse: mockPromiseValue({
|
|
86
|
+
1: {
|
|
87
|
+
elts: [
|
|
88
|
+
"hello, world"
|
|
89
|
+
],
|
|
90
|
+
tag: "STR"
|
|
91
|
+
},
|
|
92
|
+
2: {
|
|
93
|
+
elts: [
|
|
94
|
+
1
|
|
95
|
+
],
|
|
96
|
+
tag: "EXPRS"
|
|
97
|
+
},
|
|
98
|
+
3: {
|
|
99
|
+
elts: [
|
|
100
|
+
2
|
|
101
|
+
],
|
|
102
|
+
tag: "PROG"
|
|
103
|
+
},
|
|
104
|
+
root: 3
|
|
105
|
+
})
|
|
106
|
+
};
|
|
107
|
+
const parser = buildParser({ log, cache, getLangAsset, main });
|
|
108
|
+
const lang = "0";
|
|
109
|
+
const src = "'hello, world'..";
|
|
110
|
+
|
|
111
|
+
// Act
|
|
112
|
+
const result = await parser.parse(lang, src);
|
|
113
|
+
|
|
114
|
+
// Assert
|
|
115
|
+
expect(result).toHaveProperty("root", 3);
|
|
116
|
+
expect(result[1].tag).toBe("STR");
|
|
117
|
+
expect(result[1].elts).toEqual(["hello, world"]);
|
|
118
|
+
});
|
|
119
|
+
});
|
package/src/parser.js
CHANGED
package/src/parser.spec.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
|
-
import {
|
|
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();
|
|
@@ -135,41 +136,267 @@ describe("lang/parser", () => {
|
|
|
135
136
|
expect(vm.createContext).toHaveBeenCalled();
|
|
136
137
|
expect(vm.runInContext).toHaveBeenCalledWith(rawLexicon, expect.anything());
|
|
137
138
|
});
|
|
138
|
-
it
|
|
139
|
+
it("should parse error", async () => {
|
|
140
|
+
// Arrange
|
|
141
|
+
const cache = new Map();
|
|
142
|
+
const getLangAsset = mockPromiseValue("{}");
|
|
143
|
+
const err = new Error("End of program reached.");
|
|
144
|
+
const main = { parse: mockPromiseError(err) };
|
|
145
|
+
const parser = buildParser({ log, cache, getLangAsset, main });
|
|
139
146
|
const lang = "0";
|
|
140
|
-
const src = "'hello, world'
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
const src = "'hello, world'";
|
|
148
|
+
|
|
149
|
+
// Act & Assert
|
|
150
|
+
await expect(parser.parse(lang, src)).rejects.toBe(err);
|
|
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
|
|
153
247
|
},
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
265
|
},
|
|
160
|
-
|
|
161
|
-
};
|
|
162
|
-
|
|
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();
|
|
163
321
|
});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
322
|
+
|
|
323
|
+
it("should perform parse-time evaluation for adding two numbers", async () => {
|
|
324
|
+
// Create parser with custom lexicon that defines 'add' function
|
|
325
|
+
const customLexiconCache = new Map();
|
|
326
|
+
customLexiconCache.set(0, {
|
|
327
|
+
add: {
|
|
328
|
+
tk: 2,
|
|
329
|
+
name: "add",
|
|
330
|
+
cls: "function",
|
|
331
|
+
length: 2
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Use the parser with our custom cache
|
|
336
|
+
const customParser = buildParser({
|
|
337
|
+
log: console.log,
|
|
338
|
+
cache: customLexiconCache,
|
|
339
|
+
getLangAsset: async () => ({}),
|
|
340
|
+
main: {
|
|
341
|
+
parse: (src, lexicon) => {
|
|
342
|
+
return Promise.resolve(parser.parse(0, src));
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
vm
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Act - parse a simple addition expression
|
|
349
|
+
const result = await customParser.parse(0, "add 123 456..");
|
|
350
|
+
console.log(
|
|
351
|
+
"TEST",
|
|
352
|
+
"result=" + JSON.stringify(result, null, 2),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Assert
|
|
356
|
+
expect(result).toHaveProperty("root");
|
|
357
|
+
|
|
358
|
+
// Verify basic structure
|
|
359
|
+
const rootId = result.root;
|
|
360
|
+
const rootNode = result[rootId];
|
|
361
|
+
expect(rootNode.tag).toBe("PROG");
|
|
362
|
+
|
|
363
|
+
// Find the result node - expecting a single NUM node with the sum
|
|
364
|
+
let resultNode = null;
|
|
365
|
+
|
|
366
|
+
// Check all nodes for the result of evaluation (123 + 456 = 579)
|
|
367
|
+
for (const key in result) {
|
|
368
|
+
if (key !== "root") {
|
|
369
|
+
const node = result[key];
|
|
370
|
+
if (node.tag === "NUM" && node.elts[0] === "579") {
|
|
371
|
+
resultNode = node;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// We should find a node with the computed value (579)
|
|
378
|
+
expect(resultNode).not.toBeNull();
|
|
379
|
+
expect(resultNode.tag).toBe("NUM");
|
|
380
|
+
expect(resultNode.elts[0]).toBe("579");
|
|
381
|
+
|
|
382
|
+
// The original numbers should not be in the final AST
|
|
383
|
+
// if parse-time evaluation is working correctly
|
|
384
|
+
let found123 = false;
|
|
385
|
+
let found456 = false;
|
|
386
|
+
|
|
387
|
+
for (const key in result) {
|
|
388
|
+
if (key !== "root") {
|
|
389
|
+
const node = result[key];
|
|
390
|
+
if (node.tag === "NUM") {
|
|
391
|
+
if (node.elts[0] === "123") found123 = true;
|
|
392
|
+
if (node.elts[0] === "456") found456 = true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// The original operands should not be in the final AST
|
|
398
|
+
// if they were properly evaluated at parse time
|
|
399
|
+
expect(found123).toBe(false);
|
|
400
|
+
expect(found456).toBe(false);
|
|
168
401
|
});
|
|
169
|
-
// it("should parse error", async () => {
|
|
170
|
-
// const lang = "0";
|
|
171
|
-
// const src = "'hello, world..";
|
|
172
|
-
// const ast = {};
|
|
173
|
-
// await expect(parser.parse(lang, src)).rejects.toThrow("End of program reached");
|
|
174
|
-
// });
|
|
175
402
|
});
|