spex-parser 0.1.4 → 0.6.1

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 (44) hide show
  1. package/README.md +312 -1
  2. package/dist/ast.d.ts +70 -2
  3. package/dist/ast.d.ts.map +1 -1
  4. package/dist/constants.d.ts +20 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +32 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/index.d.ts +4 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/lexer.d.ts +19 -1
  13. package/dist/lexer.d.ts.map +1 -1
  14. package/dist/lexer.js +172 -5
  15. package/dist/lexer.js.map +1 -1
  16. package/dist/parser.d.ts +9 -0
  17. package/dist/parser.d.ts.map +1 -1
  18. package/dist/parser.js +103 -7
  19. package/dist/parser.js.map +1 -1
  20. package/dist/visitor.d.ts +11 -2
  21. package/dist/visitor.d.ts.map +1 -1
  22. package/dist/visitor.js +181 -8
  23. package/dist/visitor.js.map +1 -1
  24. package/package.json +1 -1
  25. package/src/ast.ts +93 -0
  26. package/src/constants.ts +40 -0
  27. package/src/index.ts +20 -3
  28. package/src/lexer.ts +164 -6
  29. package/src/parser.ts +128 -7
  30. package/src/visitor.ts +195 -11
  31. package/tests/constants.test.ts +102 -0
  32. package/tests/e2e.test.ts +15 -3
  33. package/tests/lexer.test.ts +291 -9
  34. package/tests/parser.test.ts +489 -0
  35. package/tests/props/express_todo_web.spex +108 -0
  36. package/tests/props/express_web_env.spex +16 -0
  37. package/tests/props/flask_todo_web.spex +111 -0
  38. package/tests/props/flask_web_env.spex +16 -0
  39. package/tests/props/python_cli_env.spex +16 -0
  40. package/tests/props/python_todo_cli.spex +70 -0
  41. package/tests/props/todo.spex +100 -60
  42. package/tests/props/typescript_cli_env.spex +16 -0
  43. package/tests/props/typescript_todo_cli.spex +70 -0
  44. package/tests/visitor.test.ts +892 -0
@@ -5,7 +5,7 @@ describe('SpexLexer', () => {
5
5
  describe('tokenization', () => {
6
6
  it('should tokenize keywords', () => {
7
7
  const result = SpexLexer.tokenize(
8
- 'create as from select generate import export package executable module'
8
+ 'create as from select generate import export package executable module enum realize in'
9
9
  )
10
10
  expect(result.errors).toHaveLength(0)
11
11
  expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
@@ -19,12 +19,15 @@ describe('SpexLexer', () => {
19
19
  'PackageTok',
20
20
  'ExecutableTok',
21
21
  'ModuleTok',
22
+ 'EnumTok',
23
+ 'RealizeTok',
24
+ 'InTok',
22
25
  ])
23
26
  })
24
27
 
25
28
  it('should tokenize keywords case-insensitively', () => {
26
29
  const result = SpexLexer.tokenize(
27
- 'CREATE AS FROM SELECT GENERATE IMPORT EXPORT PACKAGE EXECUTABLE MODULE'
30
+ 'CREATE AS FROM SELECT GENERATE IMPORT EXPORT PACKAGE EXECUTABLE MODULE ENUM UNION INTERSECT EXCEPT REALIZE IN'
28
31
  )
29
32
  expect(result.errors).toHaveLength(0)
30
33
  expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
@@ -38,16 +41,37 @@ describe('SpexLexer', () => {
38
41
  'PackageTok',
39
42
  'ExecutableTok',
40
43
  'ModuleTok',
44
+ 'EnumTok',
45
+ 'UnionTok',
46
+ 'IntersectTok',
47
+ 'ExceptTok',
48
+ 'RealizeTok',
49
+ 'InTok',
50
+ ])
51
+ })
52
+
53
+ it('should tokenize enum object declarations', () => {
54
+ const result = SpexLexer.tokenize("create myEnum as enum ('v1', 'v2');")
55
+ expect(result.errors).toHaveLength(0)
56
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
57
+ 'CreateTok',
58
+ 'Identifier',
59
+ 'AsTok',
60
+ 'EnumTok',
61
+ 'LParen',
62
+ 'StringLiteral',
63
+ 'Comma',
64
+ 'StringLiteral',
65
+ 'RParen',
66
+ 'Semicolon',
41
67
  ])
42
68
  })
43
69
 
44
70
  it('should tokenize symbols', () => {
45
- const result = SpexLexer.tokenize('->{}[]():;,.')
71
+ const result = SpexLexer.tokenize('->[]():;,.|')
46
72
  expect(result.errors).toHaveLength(0)
47
73
  expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
48
74
  'ArrowTok',
49
- 'LCurly',
50
- 'RCurly',
51
75
  'LBracket',
52
76
  'RBracket',
53
77
  'LParen',
@@ -56,9 +80,16 @@ describe('SpexLexer', () => {
56
80
  'Semicolon',
57
81
  'Comma',
58
82
  'Dot',
83
+ 'PipeTok',
59
84
  ])
60
85
  })
61
86
 
87
+ it('should tokenize braces as separate symbols when not forming a block', () => {
88
+ const result = SpexLexer.tokenize('}{')
89
+ expect(result.errors).toHaveLength(0)
90
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['RCurly', 'LCurly'])
91
+ })
92
+
62
93
  it('should tokenize identifiers', () => {
63
94
  const result = SpexLexer.tokenize('foo bar _test _123 ABC')
64
95
  expect(result.errors).toHaveLength(0)
@@ -89,10 +120,19 @@ describe('SpexLexer', () => {
89
120
 
90
121
  it('should handle keywords with word boundary', () => {
91
122
  const result = SpexLexer.tokenize(
92
- 'createfoo foocreate asfoo fooas fooselect selectfoo foofrom fromfoo generatefoo foogenerate importfoo fooimport exportfoo fooexport packagefoo fopackage executablefoo foexecutable modulefoo fomodule'
123
+ 'createfoo foocreate asfoo fooas fooselect selectfoo foofrom fromfoo generatefoo foogenerate importfoo fooimport exportfoo fooexport packagefoo fopackage executablefoo foexecutable modulefoo fomodule conceptfoo fooconcept environmentfoo fooenvironment realizefoo foorealize info fooin'
93
124
  )
94
125
  expect(result.errors).toHaveLength(0)
95
- expect(result.tokens.map((t) => t.tokenType.name)).toEqual(Array(20).fill('Identifier'))
126
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(Array(28).fill('Identifier'))
127
+ })
128
+
129
+ it('should tokenize the concept and environment base types', () => {
130
+ const result = SpexLexer.tokenize('concept environment')
131
+ expect(result.errors).toHaveLength(0)
132
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
133
+ 'ConceptTok',
134
+ 'EnvironmentTok',
135
+ ])
96
136
  })
97
137
 
98
138
  it('should tokenize the text between braces', () => {
@@ -103,14 +143,79 @@ describe('SpexLexer', () => {
103
143
  expect(result.tokens[0]?.image).toBe('{hello\nworld}')
104
144
  })
105
145
 
106
- it('should tokenize path literals', () => {
146
+ it('should tokenize double quoted string literals', () => {
107
147
  const result = SpexLexer.tokenize('"types.spex"')
108
148
  expect(result.errors).toHaveLength(0)
109
149
  expect(result.tokens).toHaveLength(1)
110
- expect(result.tokens[0]?.tokenType.name).toBe('PathLiteral')
150
+ expect(result.tokens[0]?.tokenType.name).toBe('StringLiteral')
111
151
  expect(result.tokens[0]?.image).toBe('"types.spex"')
112
152
  })
113
153
 
154
+ it('should tokenize single quoted string literals', () => {
155
+ const result = SpexLexer.tokenize("'types.spex'")
156
+ expect(result.errors).toHaveLength(0)
157
+ expect(result.tokens).toHaveLength(1)
158
+ expect(result.tokens[0]?.tokenType.name).toBe('StringLiteral')
159
+ expect(result.tokens[0]?.image).toBe("'types.spex'")
160
+ })
161
+
162
+ it('should tokenize string literals with escaped quotes', () => {
163
+ const result = SpexLexer.tokenize("'it\\'s a \"quote\"'")
164
+ expect(result.errors).toHaveLength(0)
165
+ expect(result.tokens).toHaveLength(1)
166
+ expect(result.tokens[0]?.tokenType.name).toBe('StringLiteral')
167
+ expect(result.tokens[0]?.image).toBe("'it\\'s a \"quote\"'")
168
+ })
169
+
170
+ it('should tokenize string literals with escaped backslashes', () => {
171
+ const result = SpexLexer.tokenize('"a\\\\b"')
172
+ expect(result.errors).toHaveLength(0)
173
+ expect(result.tokens).toHaveLength(1)
174
+ expect(result.tokens[0]?.tokenType.name).toBe('StringLiteral')
175
+ expect(result.tokens[0]?.image).toBe('"a\\\\b"')
176
+ })
177
+
178
+ it('should tokenize number literals', () => {
179
+ const result = SpexLexer.tokenize('42 3.14')
180
+ expect(result.errors).toHaveLength(0)
181
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
182
+ 'NumberLiteral',
183
+ 'NumberLiteral',
184
+ ])
185
+ expect(result.tokens.map((t) => t.image)).toEqual(['42', '3.14'])
186
+ })
187
+
188
+ it('should tokenize bool literals', () => {
189
+ const result = SpexLexer.tokenize('true false TRUE FALSE')
190
+ expect(result.errors).toHaveLength(0)
191
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
192
+ 'TrueTok',
193
+ 'FalseTok',
194
+ 'TrueTok',
195
+ 'FalseTok',
196
+ ])
197
+ })
198
+
199
+ it('should tokenize literal object declarations', () => {
200
+ const result = SpexLexer.tokenize("create Foo as (name: \"John\", age: 42);")
201
+ expect(result.errors).toHaveLength(0)
202
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
203
+ 'CreateTok',
204
+ 'Identifier',
205
+ 'AsTok',
206
+ 'LParen',
207
+ 'Identifier',
208
+ 'Colon',
209
+ 'StringLiteral',
210
+ 'Comma',
211
+ 'Identifier',
212
+ 'Colon',
213
+ 'NumberLiteral',
214
+ 'RParen',
215
+ 'Semicolon',
216
+ ])
217
+ })
218
+
114
219
  it('should tokenize array brackets', () => {
115
220
  const result = SpexLexer.tokenize('string[]')
116
221
  expect(result.errors).toHaveLength(0)
@@ -123,6 +228,183 @@ describe('SpexLexer', () => {
123
228
  })
124
229
  })
125
230
 
231
+ describe('select block escaping', () => {
232
+ it('should tokenize an empty select block', () => {
233
+ const result = SpexLexer.tokenize('{}')
234
+ expect(result.errors).toHaveLength(0)
235
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
236
+ expect(result.tokens[0]?.image).toBe('{}')
237
+ })
238
+
239
+ it('should tokenize a select block with an escaped close brace', () => {
240
+ const result = SpexLexer.tokenize('{end with \\} }')
241
+ expect(result.errors).toHaveLength(0)
242
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
243
+ expect(result.tokens[0]?.image).toBe('{end with \\} }')
244
+ })
245
+
246
+ it('should tokenize a select block with escaped open and close braces', () => {
247
+ const result = SpexLexer.tokenize('{match \\{a\\} }')
248
+ expect(result.errors).toHaveLength(0)
249
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
250
+ expect(result.tokens[0]?.image).toBe('{match \\{a\\} }')
251
+ })
252
+
253
+ it('should tokenize a select block with escaped backslashes', () => {
254
+ const result = SpexLexer.tokenize('{a \\\\ b}')
255
+ expect(result.errors).toHaveLength(0)
256
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
257
+ expect(result.tokens[0]?.image).toBe('{a \\\\ b}')
258
+ })
259
+
260
+ it('should keep backslashes before ordinary characters', () => {
261
+ const result = SpexLexer.tokenize('{\\d matches}')
262
+ expect(result.errors).toHaveLength(0)
263
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['SelectBlock'])
264
+ expect(result.tokens[0]?.image).toBe('{\\d matches}')
265
+ })
266
+
267
+ it('should fall back to individual symbols when a select block is unterminated', () => {
268
+ const result = SpexLexer.tokenize('{foo')
269
+ expect(result.errors).toHaveLength(0)
270
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['LCurly', 'Identifier'])
271
+ })
272
+ })
273
+
274
+ describe('comments', () => {
275
+ it('should skip single-line comments', () => {
276
+ const result = SpexLexer.tokenize('create Foo as string; -- a comment')
277
+ expect(result.errors).toHaveLength(0)
278
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
279
+ 'CreateTok',
280
+ 'Identifier',
281
+ 'AsTok',
282
+ 'StringTok',
283
+ 'Semicolon',
284
+ ])
285
+ })
286
+
287
+ it('should skip single-line comment with no trailing newline', () => {
288
+ const result = SpexLexer.tokenize('-- a comment')
289
+ expect(result.errors).toHaveLength(0)
290
+ expect(result.tokens).toHaveLength(0)
291
+ })
292
+
293
+ it('should skip inline block comments', () => {
294
+ const result = SpexLexer.tokenize('create /* inline */ Foo as string;')
295
+ expect(result.errors).toHaveLength(0)
296
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
297
+ 'CreateTok',
298
+ 'Identifier',
299
+ 'AsTok',
300
+ 'StringTok',
301
+ 'Semicolon',
302
+ ])
303
+ })
304
+
305
+ it('should skip multi-line block comments', () => {
306
+ const result = SpexLexer.tokenize('/* line 1\nline 2 */ create Foo as string;')
307
+ expect(result.errors).toHaveLength(0)
308
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
309
+ 'CreateTok',
310
+ 'Identifier',
311
+ 'AsTok',
312
+ 'StringTok',
313
+ 'Semicolon',
314
+ ])
315
+ })
316
+
317
+ it('should skip block comments containing token-like content', () => {
318
+ const result = SpexLexer.tokenize('/* create Bar as Number; */ create Foo as string;')
319
+ expect(result.errors).toHaveLength(0)
320
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
321
+ 'CreateTok',
322
+ 'Identifier',
323
+ 'AsTok',
324
+ 'StringTok',
325
+ 'Semicolon',
326
+ ])
327
+ })
328
+
329
+ it('should produce an error for an unclosed block comment', () => {
330
+ const result = SpexLexer.tokenize('create Foo as string; /* unclosed')
331
+ expect(result.errors).not.toHaveLength(0)
332
+ })
333
+
334
+ it('should not strip comment markers inside a select block', () => {
335
+ const result = SpexLexer.tokenize('select { are valid -- like emails };')
336
+ expect(result.errors).toHaveLength(0)
337
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
338
+ 'SelectTok',
339
+ 'SelectBlock',
340
+ 'Semicolon',
341
+ ])
342
+ expect(result.tokens[1]?.image).toBe('{ are valid -- like emails }')
343
+ })
344
+
345
+ it('should preserve block comment markers inside a select block', () => {
346
+ const result = SpexLexer.tokenize('select { match /* strict */ pattern };')
347
+ expect(result.errors).toHaveLength(0)
348
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
349
+ 'SelectTok',
350
+ 'SelectBlock',
351
+ 'Semicolon',
352
+ ])
353
+ expect(result.tokens[1]?.image).toBe('{ match /* strict */ pattern }')
354
+ })
355
+ })
356
+
357
+ describe('pattern literal', () => {
358
+ it('should tokenize a pattern literal with flags', () => {
359
+ const result = SpexLexer.tokenize('/create\\b/i')
360
+ expect(result.errors).toHaveLength(0)
361
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['PatternLiteral'])
362
+ expect(result.tokens[0]?.image).toBe('/create\\b/i')
363
+ })
364
+
365
+ it('should tokenize a pattern literal with an escaped slash', () => {
366
+ const result = SpexLexer.tokenize('/\\/\\*[\\s\\S]*?\\*\\//')
367
+ expect(result.errors).toHaveLength(0)
368
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['PatternLiteral'])
369
+ expect(result.tokens[0]?.image).toBe('/\\/\\*[\\s\\S]*?\\*\\//')
370
+ })
371
+
372
+ it('should tokenize a pattern literal with quotes in char classes', () => {
373
+ const result = SpexLexer.tokenize("/'([^'\\\\]|\\\\.)*'|\"([^\"\\\\]|\\\\.)*\"/")
374
+ expect(result.errors).toHaveLength(0)
375
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['PatternLiteral'])
376
+ expect(result.tokens[0]?.image).toBe("/'([^'\\\\]|\\\\.)*'|\"([^\"\\\\]|\\\\.)*\"/")
377
+ })
378
+
379
+ it('should tokenize an empty pattern', () => {
380
+ const result = SpexLexer.tokenize('//')
381
+ expect(result.errors).toHaveLength(0)
382
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual(['PatternLiteral'])
383
+ expect(result.tokens[0]?.image).toBe('//')
384
+ })
385
+
386
+ it('should tokenize a pattern before following tokens', () => {
387
+ const result = SpexLexer.tokenize('/\\d+/ foo;')
388
+ expect(result.errors).toHaveLength(0)
389
+ expect(result.tokens.map((t) => t.tokenType.name)).toEqual([
390
+ 'PatternLiteral',
391
+ 'Identifier',
392
+ 'Semicolon',
393
+ ])
394
+ })
395
+
396
+ it('should not tokenize an unterminated pattern', () => {
397
+ const result = SpexLexer.tokenize('/foo')
398
+ expect(result.errors).not.toHaveLength(0)
399
+ })
400
+
401
+ it('should prefer a block comment over a pattern literal', () => {
402
+ const result = SpexLexer.tokenize('/* comment */')
403
+ expect(result.errors).toHaveLength(0)
404
+ expect(result.tokens).toHaveLength(0)
405
+ })
406
+ })
407
+
126
408
  describe('error handling', () => {
127
409
  it('should return empty tokens for empty input', () => {
128
410
  const result = SpexLexer.tokenize('')