@graffiticode/parser 0.2.0 → 0.4.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/CLAUDE.md ADDED
@@ -0,0 +1,72 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Development Commands
6
+
7
+ ### Testing
8
+ ```bash
9
+ # Run all tests with experimental VM modules
10
+ npm test
11
+
12
+ # Run specific test files
13
+ NODE_OPTIONS=--experimental-vm-modules jest src/parser.spec.js
14
+ ```
15
+
16
+ ### Linting
17
+ ```bash
18
+ # Lint code
19
+ npm run lint
20
+
21
+ # Lint and automatically fix issues
22
+ npm run lint:fix
23
+ ```
24
+
25
+ ## Architecture Overview
26
+
27
+ This is the Graffiticode parser package - a core component that parses Graffiticode language syntax into ASTs (Abstract Syntax Trees).
28
+
29
+ ### Package Structure
30
+
31
+ The parser is a workspace package within the Graffiticode monorepo. It's an ES module package (`"type": "module"`) that exports parsing functionality used by the API and language compilers.
32
+
33
+ ### Core Components
34
+
35
+ 1. **Parser Entry Point** (`src/parser.js`):
36
+ - `buildParser()` - Factory function that creates a parser instance with dependencies
37
+ - Integrates with language lexicons loaded from the API
38
+ - Uses Node.js VM module for sandboxed execution
39
+
40
+ 2. **Core Parser** (`src/parse.js`):
41
+ - Implements the main parsing logic with a state machine approach
42
+ - Handles tokenization and AST construction
43
+ - Includes error tracking and position coordinates
44
+ - Supports keywords, operators, and language-specific lexicons
45
+
46
+ 3. **AST Module** (`src/ast.js`):
47
+ - Manages AST node creation and manipulation
48
+ - Node pooling for memory efficiency
49
+ - Error node generation
50
+
51
+ 4. **Environment** (`src/env.js`):
52
+ - Manages parsing environment and scopes
53
+ - Handles lexicon lookups
54
+
55
+ 5. **Folder** (`src/folder.js`):
56
+ - AST transformation and folding operations
57
+
58
+ ## Testing Strategy
59
+
60
+ - Uses Jest with experimental VM modules support
61
+ - Test files follow `*.spec.js` pattern
62
+ - Main test file: `src/parser.spec.js` contains comprehensive parsing tests
63
+
64
+ ## Monorepo Context
65
+
66
+ This parser package is part of the Graffiticode monorepo:
67
+ - Parent monorepo runs Firebase emulators for integration testing
68
+ - API package (`../api`) depends on this parser
69
+ - Auth packages (`../auth`, `../auth-client`) handle authentication
70
+ - Common package (`../common`) contains shared utilities
71
+
72
+ When working with the parser, be aware that it integrates tightly with the API's language loading mechanism (`../../api/src/lang/index.js`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiticode/parser",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -20,5 +20,8 @@
20
20
  "keywords": [],
21
21
  "author": "",
22
22
  "license": "MIT",
23
- "description": ""
23
+ "description": "",
24
+ "dependencies": {
25
+ "@graffiticode/basis": "^1.6.2"
26
+ }
24
27
  }
package/src/parser.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import vm from "vm";
2
2
  import { getLangAsset } from "../../api/src/lang/index.js";
3
3
  import { parse } from "./parse.js";
4
+ import { unparse } from "./unparse.js";
4
5
 
5
6
  // commonjs export
6
7
  const main = {
@@ -47,7 +48,13 @@ export const buildParser = ({
47
48
  vm
48
49
  }) => {
49
50
  return {
50
- async parse(lang, src) {
51
+ async parse(lang, src, lexicon = null) {
52
+ // If lexicon is provided, use it directly
53
+ if (lexicon) {
54
+ return await main.parse(src, lexicon);
55
+ }
56
+
57
+ // Otherwise, load from cache or remote
51
58
  if (!cache.has(lang)) {
52
59
  let data = await getLangAsset(lang, "/lexicon.js");
53
60
  // TODO Make lexicon JSON.
@@ -59,9 +66,9 @@ export const buildParser = ({
59
66
  throw new Error("unable to use lexicon");
60
67
  }
61
68
  const lstr = data.substring(data.indexOf("{"));
62
- let lexicon;
69
+ let loadedLexicon;
63
70
  try {
64
- lexicon = JSON.parse(lstr);
71
+ loadedLexicon = JSON.parse(lstr);
65
72
  } catch (err) {
66
73
  if (err instanceof SyntaxError) {
67
74
  log(`failed to parse ${lang} lexicon: ${err.message}`);
@@ -69,17 +76,17 @@ export const buildParser = ({
69
76
  vm.createContext(context);
70
77
  vm.runInContext(data, context);
71
78
  if (typeof (context.window.gcexports.globalLexicon) === "object") {
72
- lexicon = context.window.gcexports.globalLexicon;
79
+ loadedLexicon = context.window.gcexports.globalLexicon;
73
80
  }
74
81
  }
75
- if (!lexicon) {
82
+ if (!loadedLexicon) {
76
83
  throw new Error("Malformed lexicon");
77
84
  }
78
85
  }
79
- cache.set(lang, lexicon);
86
+ cache.set(lang, loadedLexicon);
80
87
  };
81
- const lexicon = cache.get(lang);
82
- return await main.parse(src, lexicon);
88
+ const cachedLexicon = cache.get(lang);
89
+ return await main.parse(src, cachedLexicon);
83
90
  }
84
91
  };
85
92
  };
@@ -91,3 +98,12 @@ export const parser = buildParser({
91
98
  main,
92
99
  vm
93
100
  });
101
+
102
+ // Add unparse as a property of parser
103
+ parser.unparse = unparse;
104
+
105
+ // Add reformat function that parses and unparses code
106
+ parser.reformat = async function(lang, src, lexicon, options = {}) {
107
+ const ast = await this.parse(lang, src, lexicon);
108
+ return unparse(ast, lexicon, options);
109
+ };
@@ -5,6 +5,27 @@ import vm from "vm";
5
5
 
6
6
  describe("lang/parser", () => {
7
7
  const log = jest.fn();
8
+ it("should use provided lexicon directly", async () => {
9
+ // Arrange
10
+ const cache = new Map();
11
+ const getLangAsset = jest.fn(); // Should not be called
12
+ const main = {
13
+ parse: mockPromiseValue({ root: "0" })
14
+ };
15
+ const parser = buildParser({ log, cache, getLangAsset, main });
16
+ const lang = "0";
17
+ const src = "'foo'..";
18
+ const providedLexicon = { test: "lexicon" };
19
+
20
+ // Act
21
+ await expect(parser.parse(lang, src, providedLexicon)).resolves.toStrictEqual({ root: "0" });
22
+
23
+ // Assert
24
+ expect(getLangAsset).not.toHaveBeenCalled(); // Should not fetch when lexicon is provided
25
+ expect(main.parse).toHaveBeenCalledWith(src, providedLexicon);
26
+ expect(cache.has(lang)).toBe(false); // Should not cache when lexicon is provided
27
+ });
28
+
8
29
  it("should call main parser language lexicon", async () => {
9
30
  // Arrange
10
31
  const cache = new Map();
@@ -0,0 +1,361 @@
1
+ import { parser } from "./parser.js";
2
+ import { unparse } from "./unparse.js";
3
+
4
+ describe("unparse with L0166 lexicon", () => {
5
+ // L0166 lexicon for spreadsheet operations (from l0166/packages/api/src/lexicon.js)
6
+ const l0166Lexicon = {
7
+ "title": {
8
+ "tk": 1,
9
+ "name": "TITLE",
10
+ "cls": "function",
11
+ "length": 2,
12
+ "arity": 2,
13
+ },
14
+ "instructions": {
15
+ "tk": 1,
16
+ "name": "INSTRUCTIONS",
17
+ "cls": "function",
18
+ "length": 2,
19
+ "arity": 2,
20
+ },
21
+ "params": {
22
+ "tk": 1,
23
+ "name": "PARAMS",
24
+ "cls": "function",
25
+ "length": 2,
26
+ "arity": 2,
27
+ },
28
+ "cell": {
29
+ "tk": 1,
30
+ "name": "CELL",
31
+ "cls": "function",
32
+ "length": 2,
33
+ "arity": 2,
34
+ },
35
+ "text": {
36
+ "tk": 1,
37
+ "name": "TEXT",
38
+ "cls": "function",
39
+ "length": 2,
40
+ "arity": 2,
41
+ },
42
+ "assess": {
43
+ "tk": 1,
44
+ "name": "ASSESS",
45
+ "cls": "function",
46
+ "length": 2,
47
+ "arity": 2,
48
+ },
49
+ "method": {
50
+ "tk": 1,
51
+ "name": "METHOD",
52
+ "cls": "function",
53
+ "length": 1,
54
+ "arity": 1,
55
+ },
56
+ "expected": {
57
+ "tk": 1,
58
+ "name": "EXPECTED",
59
+ "cls": "function",
60
+ "length": 1,
61
+ "arity": 1,
62
+ },
63
+ "width": {
64
+ "tk": 1,
65
+ "name": "WIDTH",
66
+ "cls": "function",
67
+ "length": 2,
68
+ "arity": 2,
69
+ },
70
+ "align": {
71
+ "tk": 1,
72
+ "name": "ALIGN",
73
+ "cls": "function",
74
+ "length": 2,
75
+ "arity": 2,
76
+ },
77
+ "background-color": {
78
+ "tk": 1,
79
+ "name": "BACKGROUND_COLOR",
80
+ "cls": "function",
81
+ "length": 2,
82
+ "arity": 2,
83
+ },
84
+ "font-weight": {
85
+ "tk": 1,
86
+ "name": "FONT_WEIGHT",
87
+ "cls": "function",
88
+ "length": 2,
89
+ "arity": 2,
90
+ },
91
+ "format": {
92
+ "tk": 1,
93
+ "name": "FORMAT",
94
+ "cls": "function",
95
+ "length": 2,
96
+ "arity": 2,
97
+ },
98
+ "protected": {
99
+ "tk": 1,
100
+ "name": "PROTECTED",
101
+ "cls": "function",
102
+ "length": 2,
103
+ "arity": 2,
104
+ },
105
+ "cells": {
106
+ "tk": 1,
107
+ "name": "CELLS",
108
+ "cls": "function",
109
+ "length": 2,
110
+ "arity": 2,
111
+ },
112
+ "rows": {
113
+ "tk": 1,
114
+ "name": "ROWS",
115
+ "cls": "function",
116
+ "length": 2,
117
+ "arity": 2,
118
+ },
119
+ "column": {
120
+ "tk": 1,
121
+ "name": "COLUMN",
122
+ "cls": "function",
123
+ "length": 2,
124
+ "arity": 2,
125
+ },
126
+ "columns": {
127
+ "tk": 1,
128
+ "name": "COLUMNS",
129
+ "cls": "function",
130
+ "length": 2,
131
+ "arity": 2,
132
+ }
133
+ };
134
+
135
+ it("should unparse L0166 spreadsheet code", async () => {
136
+ const source = `columns [
137
+ column A width 100 align "center" protected true {}
138
+ ]
139
+ rows [
140
+ row 1 background-color "#eee" protected true {}
141
+ ]
142
+ cells [
143
+ cell A1 text "A1" protected true {}
144
+ ]
145
+ {
146
+ v: "0.0.1"
147
+ }..`;
148
+
149
+ // Note: The parser may transform this code, so we test that unparse
150
+ // produces valid code that can be parsed again
151
+ // Pass the lexicon directly to avoid fetching
152
+
153
+ // For complex L0166 code, we'll just parse with language 0
154
+ // since the specific L0166 syntax may require special handling
155
+ const ast = await parser.parse(0, source);
156
+
157
+ // Log the AST pool
158
+ console.log("AST Pool:", JSON.stringify(ast, null, 2));
159
+
160
+ const unparsed = unparse(ast, l0166Lexicon);
161
+
162
+ // The unparsed code should be valid and parseable
163
+ expect(unparsed).toBeDefined();
164
+ expect(unparsed.endsWith("..")).toBe(true);
165
+
166
+ // Check that key elements appear in the output
167
+ // (the exact format may differ due to how the parser handles the syntax)
168
+ console.log("Original source:", source);
169
+ console.log("Unparsed:", unparsed);
170
+ });
171
+
172
+ it("should handle individual L0166 constructs", async () => {
173
+ const tests = [
174
+ {
175
+ source: '{v: "0.0.1"}..',
176
+ description: "version record"
177
+ },
178
+ {
179
+ source: '[]..',
180
+ description: "empty list"
181
+ },
182
+ {
183
+ source: '{}..',
184
+ description: "empty record"
185
+ },
186
+ {
187
+ source: '"A1"..',
188
+ description: "string literal"
189
+ },
190
+ {
191
+ source: '100..',
192
+ description: "number literal"
193
+ },
194
+ {
195
+ source: 'true..',
196
+ description: "boolean literal"
197
+ }
198
+ ];
199
+
200
+ for (const { source, description } of tests) {
201
+ const ast = await parser.parse(166, source, l0166Lexicon);
202
+ const unparsed = unparse(ast, l0166Lexicon);
203
+
204
+ // Check that unparse produces output
205
+ expect(unparsed).toBeDefined();
206
+ expect(unparsed).not.toBe("");
207
+
208
+ // The output should end with ..
209
+ if (!unparsed.endsWith("..")) {
210
+ console.log(`${description}: "${source}" -> "${unparsed}"`);
211
+ }
212
+ expect(unparsed.endsWith("..")).toBe(true);
213
+ }
214
+ });
215
+
216
+ it("should preserve simple L0166 expressions", async () => {
217
+ // Test simpler L0166 expressions that should parse correctly
218
+ const tests = [
219
+ 'column A {}..',
220
+ 'row 1 {}..',
221
+ 'cell A1 {}..',
222
+ ];
223
+
224
+ for (const source of tests) {
225
+ const ast = await parser.parse(0, source);
226
+ const unparsed = unparse(ast, l0166Lexicon);
227
+
228
+ // Should produce valid output
229
+ expect(unparsed).toBeDefined();
230
+ expect(unparsed.endsWith("..")).toBe(true);
231
+
232
+ console.log(`Simple L0166: "${source}" -> "${unparsed}"`);
233
+ }
234
+ });
235
+
236
+ it("should handle complex L0166 budget assessment code", async () => {
237
+ const source = `title "Home Budget Assessment"
238
+ instructions \`
239
+ - Calculate your monthly budget based on income percentages
240
+ - Fill in the empty cells with the correct formulas
241
+ - Ensure all expenses and savings are properly allocated
242
+ \`
243
+ columns [
244
+ column A width 150 align "left" {}
245
+ column B width 100 format "($#,##0)" {}
246
+ column C width 250 align "left" {}
247
+ ]
248
+ cells [
249
+ cell A1 text "CATEGORY" font-weight "bold" {}
250
+ cell B1 text "AMOUNT" font-weight "bold" {}
251
+ cell C1 text "DETAILS" font-weight "bold" {}
252
+
253
+ cell A2 text "Income" {}
254
+ cell B2 text "4000" {}
255
+ cell C2 text "Total monthly income" {}
256
+
257
+ cell A3 text "Rent" {}
258
+ cell B3
259
+ text "",
260
+ assess [
261
+ method "value"
262
+ expected "1400"
263
+ ] {}
264
+ cell C3 text "35% of your total income" {}
265
+
266
+ cell A4 text "Utilities" {}
267
+ cell B4 text "200" {}
268
+ cell C4 text "Fixed expense" {}
269
+
270
+ cell A5 text "Food" {}
271
+ cell B5
272
+ text "",
273
+ assess [
274
+ method "value"
275
+ expected "600"
276
+ ] {}
277
+ cell C5 text "15% of your total income" {}
278
+
279
+ cell A6 text "Transportation" {}
280
+ cell B6
281
+ text "",
282
+ assess [
283
+ method "value"
284
+ expected "400"
285
+ ] {}
286
+ cell C6 text "10% of your total income" {}
287
+
288
+ cell A7 text "Entertainment" {}
289
+ cell B7 text "150" {}
290
+ cell C7 text "Fixed expense" {}
291
+
292
+ cell A8 text "Savings" {}
293
+ cell B8
294
+ text "",
295
+ assess [
296
+ method "value"
297
+ expected "800"
298
+ ] {}
299
+ cell C8 text "20% of your total income" {}
300
+
301
+ cell A9 text "Miscellaneous" {}
302
+ cell B9
303
+ text "",
304
+ assess [
305
+ method "value"
306
+ expected "450"
307
+ ] {}
308
+ cell C9 text "Remaining income after all other expenses" {}
309
+ ]
310
+ {
311
+ v: "0.0.1"
312
+ }..`;
313
+
314
+ // Parse with L0166 lexicon
315
+ const ast = await parser.parse("0166", source, l0166Lexicon);
316
+
317
+ console.log("Complex L0166 AST nodes:", Object.keys(ast).length);
318
+
319
+ const unparsed = unparse(ast, l0166Lexicon);
320
+
321
+ // The unparsed code should be valid and parseable
322
+ expect(unparsed).toBeDefined();
323
+ expect(unparsed.endsWith("..")).toBe(true);
324
+
325
+ // Check that key elements appear in the output
326
+ expect(unparsed).toContain("title");
327
+ expect(unparsed).toContain("columns");
328
+ expect(unparsed).toContain("cells");
329
+ expect(unparsed).toContain("column A");
330
+ expect(unparsed).toContain("column B");
331
+ expect(unparsed).toContain("column C");
332
+
333
+ // Log a portion of the output to see the pretty printing
334
+ const lines = unparsed.split("\n");
335
+ console.log("First 20 lines of unparsed output:");
336
+ console.log(lines.slice(0, 20).join("\n"));
337
+ console.log("...");
338
+ console.log("Last 10 lines of unparsed output:");
339
+ console.log(lines.slice(-10).join("\n"));
340
+ console.log(unparsed);
341
+ });
342
+
343
+ it("should reformat L0166 code using parser.reformat", async () => {
344
+ const source = `columns [column A width 100 {}] rows [row 1 {}] cells [cell A1 text "Hello" {}] {v: "0.0.1"}..`;
345
+
346
+ // Reformat with L0166 lexicon
347
+ const reformatted = await parser.reformat("0166", source, l0166Lexicon);
348
+
349
+ // Check that it produces valid output
350
+ expect(reformatted).toBeDefined();
351
+ expect(reformatted.endsWith("..")).toBe(true);
352
+
353
+ // Check for pretty printing
354
+ expect(reformatted).toContain("columns [\n");
355
+ expect(reformatted).toContain("rows [\n");
356
+ expect(reformatted).toContain("cells [\n");
357
+
358
+ console.log("Reformatted L0166 code:");
359
+ console.log(reformatted);
360
+ });
361
+ });