@graffiticode/parser 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +0 -1
- package/src/parser.js +16 -46
- package/src/parser.spec.js +49 -143
- package/src/unparse-l0166.spec.js +364 -0
- package/src/unparse-l0166.spec.js~ +341 -0
- package/src/unparse.js +78 -31
- package/src/unparse.spec.js +113 -54
package/package.json
CHANGED
package/src/index.js
CHANGED
package/src/parser.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import vm from "vm";
|
|
2
|
-
import { getLangAsset } from "../../api/src/lang/index.js";
|
|
3
1
|
import { parse } from "./parse.js";
|
|
4
2
|
import { unparse } from "./unparse.js";
|
|
5
3
|
|
|
@@ -40,58 +38,30 @@ const main = {
|
|
|
40
38
|
}
|
|
41
39
|
};
|
|
42
40
|
|
|
43
|
-
export const buildParser = ({
|
|
44
|
-
log,
|
|
45
|
-
cache,
|
|
46
|
-
getLangAsset,
|
|
47
|
-
main,
|
|
48
|
-
vm
|
|
49
|
-
}) => {
|
|
41
|
+
export const buildParser = ({ main }) => {
|
|
50
42
|
return {
|
|
51
|
-
async parse(lang, src) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
data = data.toString();
|
|
57
|
-
}
|
|
58
|
-
if (typeof (data) !== "string") {
|
|
59
|
-
log(`Failed to get usable lexicon for ${lang}`, typeof (data), data);
|
|
60
|
-
throw new Error("unable to use lexicon");
|
|
61
|
-
}
|
|
62
|
-
const lstr = data.substring(data.indexOf("{"));
|
|
63
|
-
let lexicon;
|
|
64
|
-
try {
|
|
65
|
-
lexicon = JSON.parse(lstr);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
if (err instanceof SyntaxError) {
|
|
68
|
-
log(`failed to parse ${lang} lexicon: ${err.message}`);
|
|
69
|
-
const context = { window: { gcexports: {} } };
|
|
70
|
-
vm.createContext(context);
|
|
71
|
-
vm.runInContext(data, context);
|
|
72
|
-
if (typeof (context.window.gcexports.globalLexicon) === "object") {
|
|
73
|
-
lexicon = context.window.gcexports.globalLexicon;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (!lexicon) {
|
|
77
|
-
throw new Error("Malformed lexicon");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
cache.set(lang, lexicon);
|
|
81
|
-
};
|
|
82
|
-
const lexicon = cache.get(lang);
|
|
43
|
+
async parse(lang, src, lexicon) {
|
|
44
|
+
// Lexicon is now required
|
|
45
|
+
if (!lexicon) {
|
|
46
|
+
throw new Error("Lexicon is required for parsing");
|
|
47
|
+
}
|
|
83
48
|
return await main.parse(src, lexicon);
|
|
84
49
|
}
|
|
85
50
|
};
|
|
86
51
|
};
|
|
87
52
|
|
|
88
53
|
export const parser = buildParser({
|
|
89
|
-
|
|
90
|
-
cache: new Map(),
|
|
91
|
-
getLangAsset,
|
|
92
|
-
main,
|
|
93
|
-
vm
|
|
54
|
+
main
|
|
94
55
|
});
|
|
95
56
|
|
|
96
57
|
// Add unparse as a property of parser
|
|
97
58
|
parser.unparse = unparse;
|
|
59
|
+
|
|
60
|
+
// Add reformat function that parses and unparses code
|
|
61
|
+
parser.reformat = async function(lang, src, lexicon, options = {}) {
|
|
62
|
+
if (!lexicon) {
|
|
63
|
+
throw new Error("Lexicon is required for reformatting");
|
|
64
|
+
}
|
|
65
|
+
const ast = await this.parse(lang, src, lexicon);
|
|
66
|
+
return unparse(ast, lexicon, options);
|
|
67
|
+
};
|
package/src/parser.spec.js
CHANGED
|
@@ -1,153 +1,96 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
2
|
import { buildParser, parser } from "./parser.js";
|
|
3
3
|
import { mockPromiseValue, mockPromiseError } from "./testing/index.js";
|
|
4
|
-
import
|
|
4
|
+
import { lexicon as basisLexicon } from "@graffiticode/basis";
|
|
5
5
|
|
|
6
6
|
describe("lang/parser", () => {
|
|
7
7
|
const log = jest.fn();
|
|
8
|
-
it("should
|
|
8
|
+
it("should use provided lexicon", async () => {
|
|
9
9
|
// Arrange
|
|
10
|
-
const cache = new Map();
|
|
11
|
-
const getLangAsset = mockPromiseValue("{}");
|
|
12
10
|
const main = {
|
|
13
11
|
parse: mockPromiseValue({ root: "0" })
|
|
14
12
|
};
|
|
15
|
-
const parser = buildParser({
|
|
13
|
+
const parser = buildParser({ main });
|
|
16
14
|
const lang = "0";
|
|
17
15
|
const src = "'foo'..";
|
|
16
|
+
const providedLexicon = { test: "lexicon" };
|
|
18
17
|
|
|
19
18
|
// Act
|
|
20
|
-
await expect(parser.parse(lang, src)).resolves.toStrictEqual({ root: "0" });
|
|
19
|
+
await expect(parser.parse(lang, src, providedLexicon)).resolves.toStrictEqual({ root: "0" });
|
|
21
20
|
|
|
22
21
|
// Assert
|
|
23
|
-
expect(
|
|
24
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
25
|
-
expect(cache.has(lang)).toBe(true);
|
|
26
|
-
expect(cache.get(lang)).toStrictEqual({});
|
|
22
|
+
expect(main.parse).toHaveBeenCalledWith(src, providedLexicon);
|
|
27
23
|
});
|
|
28
|
-
|
|
24
|
+
|
|
25
|
+
it("should throw error when lexicon is missing", async () => {
|
|
29
26
|
// Arrange
|
|
30
|
-
const cache = new Map();
|
|
31
27
|
const main = {
|
|
32
28
|
parse: mockPromiseValue({ root: "0" })
|
|
33
29
|
};
|
|
34
|
-
const parser = buildParser({
|
|
35
|
-
cache,
|
|
36
|
-
main
|
|
37
|
-
});
|
|
30
|
+
const parser = buildParser({ main });
|
|
38
31
|
const lang = "0";
|
|
39
32
|
const src = "'foo'..";
|
|
40
|
-
cache.set(lang, {});
|
|
41
|
-
|
|
42
|
-
// Act
|
|
43
|
-
await expect(parser.parse(lang, src)).resolves.toStrictEqual({ root: "0" });
|
|
44
|
-
|
|
45
|
-
// Assert
|
|
46
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
47
|
-
});
|
|
48
|
-
it("should return error if get language asset fails", async () => {
|
|
49
|
-
// Arrange
|
|
50
|
-
const cache = new Map();
|
|
51
|
-
const err = new Error("failed to get lexicon");
|
|
52
|
-
const getLangAsset = mockPromiseError(err);
|
|
53
|
-
const parser = buildParser({
|
|
54
|
-
cache,
|
|
55
|
-
getLangAsset
|
|
56
|
-
});
|
|
57
|
-
const lang = "00";
|
|
58
|
-
const src = "'foo'..";
|
|
59
33
|
|
|
60
|
-
// Act
|
|
61
|
-
await expect(parser.parse(lang, src)).rejects.
|
|
62
|
-
|
|
63
|
-
// Assert
|
|
64
|
-
expect(getLangAsset).toHaveBeenCalledWith(lang, "/lexicon.js");
|
|
34
|
+
// Act & Assert
|
|
35
|
+
await expect(parser.parse(lang, src)).rejects.toThrow("Lexicon is required for parsing");
|
|
65
36
|
});
|
|
66
|
-
it("should
|
|
37
|
+
it("should pass lexicon to main parser", async () => {
|
|
67
38
|
// Arrange
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const main = { parse: mockPromiseError(err) };
|
|
73
|
-
const parser = buildParser({ log, cache, getLangAsset, main });
|
|
39
|
+
const main = {
|
|
40
|
+
parse: mockPromiseValue({ root: "0" })
|
|
41
|
+
};
|
|
42
|
+
const parser = buildParser({ main });
|
|
74
43
|
const lang = "0";
|
|
75
44
|
const src = "'foo'..";
|
|
45
|
+
const lexicon = { someFunc: { name: "SOMEFUNC" } };
|
|
76
46
|
|
|
77
47
|
// Act
|
|
78
|
-
await expect(parser.parse(lang, src)).
|
|
48
|
+
await expect(parser.parse(lang, src, lexicon)).resolves.toStrictEqual({ root: "0" });
|
|
79
49
|
|
|
80
50
|
// Assert
|
|
81
|
-
expect(
|
|
82
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
83
|
-
expect(cache.has(lang)).toBe(true);
|
|
84
|
-
expect(cache.get(lang)).toStrictEqual({});
|
|
51
|
+
expect(main.parse).toHaveBeenCalledWith(src, lexicon);
|
|
85
52
|
});
|
|
86
|
-
it("should return
|
|
53
|
+
it("should return error if main parser fails with lexicon", async () => {
|
|
87
54
|
// Arrange
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const main = { parse: mockPromiseValue(ast) };
|
|
93
|
-
const parser = buildParser({ log, cache, getLangAsset, main });
|
|
94
|
-
const lang = "0";
|
|
55
|
+
const err = new Error("parser failed");
|
|
56
|
+
const main = { parse: mockPromiseError(err) };
|
|
57
|
+
const parser = buildParser({ main });
|
|
58
|
+
const lang = "00";
|
|
95
59
|
const src = "'foo'..";
|
|
60
|
+
const lexicon = {};
|
|
96
61
|
|
|
97
62
|
// Act
|
|
98
|
-
await expect(parser.parse(lang, src)).
|
|
63
|
+
await expect(parser.parse(lang, src, lexicon)).rejects.toBe(err);
|
|
99
64
|
|
|
100
65
|
// Assert
|
|
101
|
-
expect(
|
|
102
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
103
|
-
expect(cache.has(lang)).toBe(true);
|
|
104
|
-
expect(cache.get(lang)).toStrictEqual({});
|
|
66
|
+
expect(main.parse).toHaveBeenCalledWith(src, lexicon);
|
|
105
67
|
});
|
|
106
|
-
it("should
|
|
68
|
+
it("should return error if main parser fails", async () => {
|
|
107
69
|
// Arrange
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
(() => {
|
|
112
|
-
window.gcexports.globalLexicon = {};
|
|
113
|
-
})();
|
|
114
|
-
`;
|
|
115
|
-
const getLangAsset = mockPromiseValue(rawLexicon);
|
|
116
|
-
const ast = { root: "0" };
|
|
117
|
-
const main = { parse: mockPromiseValue(ast) };
|
|
118
|
-
const vm = {
|
|
119
|
-
createContext: jest.fn(),
|
|
120
|
-
runInContext: jest.fn().mockImplementation((data, context) => {
|
|
121
|
-
context.window.gcexports.globalLexicon = {};
|
|
122
|
-
})
|
|
123
|
-
};
|
|
124
|
-
const parser = buildParser({ log, cache, getLangAsset, main, vm });
|
|
70
|
+
const err = new Error("main parser failed");
|
|
71
|
+
const main = { parse: mockPromiseError(err) };
|
|
72
|
+
const parser = buildParser({ main });
|
|
125
73
|
const lang = "0";
|
|
126
74
|
const src = "'foo'..";
|
|
75
|
+
const lexicon = {};
|
|
127
76
|
|
|
128
77
|
// Act
|
|
129
|
-
await expect(parser.parse(lang, src)).
|
|
78
|
+
await expect(parser.parse(lang, src, lexicon)).rejects.toBe(err);
|
|
130
79
|
|
|
131
80
|
// Assert
|
|
132
|
-
expect(
|
|
133
|
-
expect(main.parse).toHaveBeenCalledWith(src, {});
|
|
134
|
-
expect(cache.has(lang)).toBe(true);
|
|
135
|
-
expect(cache.get(lang)).toStrictEqual({});
|
|
136
|
-
expect(vm.createContext).toHaveBeenCalled();
|
|
137
|
-
expect(vm.runInContext).toHaveBeenCalledWith(rawLexicon, expect.anything());
|
|
81
|
+
expect(main.parse).toHaveBeenCalledWith(src, lexicon);
|
|
138
82
|
});
|
|
139
83
|
it("should parse error", async () => {
|
|
140
84
|
// Arrange
|
|
141
|
-
const cache = new Map();
|
|
142
|
-
const getLangAsset = mockPromiseValue("{}");
|
|
143
85
|
const err = new Error("End of program reached.");
|
|
144
86
|
const main = { parse: mockPromiseError(err) };
|
|
145
|
-
const parser = buildParser({
|
|
87
|
+
const parser = buildParser({ main });
|
|
146
88
|
const lang = "0";
|
|
147
89
|
const src = "'hello, world'";
|
|
90
|
+
const lexicon = {};
|
|
148
91
|
|
|
149
92
|
// Act & Assert
|
|
150
|
-
await expect(parser.parse(lang, src)).rejects.toBe(err);
|
|
93
|
+
await expect(parser.parse(lang, src, lexicon)).rejects.toBe(err);
|
|
151
94
|
});
|
|
152
95
|
});
|
|
153
96
|
|
|
@@ -155,7 +98,7 @@ describe("parser integration tests", () => {
|
|
|
155
98
|
// Tests using the actual parser
|
|
156
99
|
it("should parse string literals", async () => {
|
|
157
100
|
// Arrange & Act
|
|
158
|
-
const result = await parser.parse(0, "'hello, world'..");
|
|
101
|
+
const result = await parser.parse(0, "'hello, world'..", basisLexicon);
|
|
159
102
|
|
|
160
103
|
// Assert
|
|
161
104
|
expect(result).toHaveProperty("root");
|
|
@@ -184,7 +127,7 @@ describe("parser integration tests", () => {
|
|
|
184
127
|
|
|
185
128
|
it("should parse numeric literals", async () => {
|
|
186
129
|
// Arrange & Act
|
|
187
|
-
const result = await parser.parse(0, "42..");
|
|
130
|
+
const result = await parser.parse(0, "42..", basisLexicon);
|
|
188
131
|
|
|
189
132
|
// Assert
|
|
190
133
|
expect(result).toHaveProperty("root");
|
|
@@ -208,7 +151,7 @@ describe("parser integration tests", () => {
|
|
|
208
151
|
|
|
209
152
|
it("should have a PROG node at the root", async () => {
|
|
210
153
|
// Let's test the most basic structure that should always work
|
|
211
|
-
const result = await parser.parse(0, "123..");
|
|
154
|
+
const result = await parser.parse(0, "123..", basisLexicon);
|
|
212
155
|
|
|
213
156
|
// Assert
|
|
214
157
|
expect(result).toHaveProperty("root");
|
|
@@ -236,9 +179,8 @@ describe("parser integration tests", () => {
|
|
|
236
179
|
});
|
|
237
180
|
|
|
238
181
|
it("should parse complex program: apply (<a b: add a b>) [10 20]..", async () => {
|
|
239
|
-
// Create
|
|
240
|
-
const
|
|
241
|
-
customLexiconCache.set(0, {
|
|
182
|
+
// Create custom lexicon
|
|
183
|
+
const customLexicon = {
|
|
242
184
|
add: {
|
|
243
185
|
tk: 2,
|
|
244
186
|
name: "add",
|
|
@@ -251,23 +193,10 @@ describe("parser integration tests", () => {
|
|
|
251
193
|
cls: "function",
|
|
252
194
|
length: 2
|
|
253
195
|
}
|
|
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
|
-
});
|
|
196
|
+
};
|
|
268
197
|
|
|
269
198
|
// Act
|
|
270
|
-
const result = await
|
|
199
|
+
const result = await parser.parse(0, "apply (<a b: add a b>) [10 20]..", customLexicon);
|
|
271
200
|
|
|
272
201
|
// Assert
|
|
273
202
|
expect(result).toHaveProperty("root");
|
|
@@ -327,7 +256,7 @@ describe("parser integration tests", () => {
|
|
|
327
256
|
|
|
328
257
|
try {
|
|
329
258
|
// Unclosed string - missing closing quote
|
|
330
|
-
result = await parser.parse(0, "'unclosed string..");
|
|
259
|
+
result = await parser.parse(0, "'unclosed string..", basisLexicon);
|
|
331
260
|
} catch (e) {
|
|
332
261
|
// Check for expected error (we should now have a robust parser that doesn't throw)
|
|
333
262
|
console.error("Unexpected error:", e);
|
|
@@ -367,7 +296,7 @@ describe("parser integration tests", () => {
|
|
|
367
296
|
|
|
368
297
|
try {
|
|
369
298
|
// Missing closing bracket
|
|
370
|
-
result = await parser.parse(0, "[1, 2, 3..");
|
|
299
|
+
result = await parser.parse(0, "[1, 2, 3..", basisLexicon);
|
|
371
300
|
} catch (e) {
|
|
372
301
|
console.error("Unexpected error:", e);
|
|
373
302
|
throw e;
|
|
@@ -406,7 +335,7 @@ describe("parser integration tests", () => {
|
|
|
406
335
|
|
|
407
336
|
try {
|
|
408
337
|
// Invalid sequence of tokens
|
|
409
|
-
result = await parser.parse(0, "if then else..");
|
|
338
|
+
result = await parser.parse(0, "if then else..", basisLexicon);
|
|
410
339
|
} catch (e) {
|
|
411
340
|
console.error("Unexpected error:", e);
|
|
412
341
|
throw e;
|
|
@@ -441,32 +370,9 @@ describe("parser integration tests", () => {
|
|
|
441
370
|
});
|
|
442
371
|
|
|
443
372
|
it("should perform parse-time evaluation for adding two numbers", async () => {
|
|
444
|
-
//
|
|
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
|
-
|
|
373
|
+
// Use basis lexicon which includes add function
|
|
468
374
|
// Act - parse a simple addition expression
|
|
469
|
-
const result = await
|
|
375
|
+
const result = await parser.parse(0, "add 123 456..", basisLexicon);
|
|
470
376
|
console.log(
|
|
471
377
|
"TEST",
|
|
472
378
|
"result=" + JSON.stringify(result, null, 2),
|