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
package/src/lexer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createToken, Lexer } from 'chevrotain'
1
+ import { createToken, Lexer, type CustomPatternMatcherReturn } from 'chevrotain'
2
2
 
3
3
  export const WhiteSpace = createToken({
4
4
  name: 'WhiteSpace',
@@ -6,6 +6,18 @@ export const WhiteSpace = createToken({
6
6
  group: Lexer.SKIPPED,
7
7
  })
8
8
 
9
+ // Comments (SQL-style)
10
+ export const LineComment = createToken({
11
+ name: 'LineComment',
12
+ pattern: /--[^\r\n]*/,
13
+ group: Lexer.SKIPPED,
14
+ })
15
+ export const BlockComment = createToken({
16
+ name: 'BlockComment',
17
+ pattern: /\/\*[\s\S]*?\*\//,
18
+ group: Lexer.SKIPPED,
19
+ })
20
+
9
21
  // Keywords (case-insensitive)
10
22
  export const CreateTok = createToken({
11
23
  name: 'CreateTok',
@@ -47,9 +59,42 @@ export const ModuleTok = createToken({
47
59
  name: 'ModuleTok',
48
60
  pattern: /module\b/i,
49
61
  })
62
+ export const EnumTok = createToken({
63
+ name: 'EnumTok',
64
+ pattern: /enum\b/i,
65
+ })
66
+ export const UnionTok = createToken({
67
+ name: 'UnionTok',
68
+ pattern: /union\b/i,
69
+ })
70
+ export const IntersectTok = createToken({
71
+ name: 'IntersectTok',
72
+ pattern: /intersect\b/i,
73
+ })
74
+ export const ExceptTok = createToken({
75
+ name: 'ExceptTok',
76
+ pattern: /except\b/i,
77
+ })
78
+ export const RealizeTok = createToken({
79
+ name: 'RealizeTok',
80
+ pattern: /realize\b/i,
81
+ })
82
+ export const InTok = createToken({
83
+ name: 'InTok',
84
+ pattern: /in\b/i,
85
+ })
86
+ export const IncludeTok = createToken({
87
+ name: 'IncludeTok',
88
+ pattern: /include\b/i,
89
+ })
90
+ export const LambdaTok = createToken({
91
+ name: 'LambdaTok',
92
+ pattern: /lambda\b/i,
93
+ })
50
94
 
51
95
  // Symbols
52
96
  export const ArrowTok = createToken({ name: 'ArrowTok', pattern: /->/ })
97
+ export const PipeTok = createToken({ name: 'PipeTok', pattern: /\|/ })
53
98
  export const LCurly = createToken({ name: 'LCurly', pattern: /{/ })
54
99
  export const RCurly = createToken({ name: 'RCurly', pattern: /}/ })
55
100
  export const LBracket = createToken({ name: 'LBracket', pattern: /\[/ })
@@ -64,13 +109,100 @@ export const Dot = createToken({ name: 'Dot', pattern: /\./ })
64
109
  // Brace text block (for SELECT { ... })
65
110
  export const SelectBlock = createToken({
66
111
  name: 'SelectBlock',
67
- pattern: /\{[^}]+\}/,
112
+ line_breaks: true,
113
+ pattern: (text: string, startOffset: number): CustomPatternMatcherReturn | null => {
114
+ let i = startOffset
115
+ if (text[i] !== '{') return null
116
+ for (i++; i < text.length; i++) {
117
+ if (text[i] === '\\') {
118
+ i++
119
+ continue
120
+ }
121
+ if (text[i] === '}') {
122
+ return [text.slice(startOffset, i + 1)]
123
+ }
124
+ }
125
+ return null
126
+ },
127
+ })
128
+
129
+ // Regex literal (JS-style): /pattern/flags
130
+ export const PatternLiteral = createToken({
131
+ name: 'PatternLiteral',
132
+ line_breaks: true,
133
+ pattern: (text: string, startOffset: number): CustomPatternMatcherReturn | null => {
134
+ if (text[startOffset] !== '/') return null
135
+ let inClass = false
136
+ let i = startOffset + 1
137
+ while (i < text.length) {
138
+ const ch = text[i]
139
+ if (ch === '\\') {
140
+ i += 2
141
+ continue
142
+ }
143
+ if (inClass) {
144
+ if (ch === ']') inClass = false
145
+ } else if (ch === '[') {
146
+ inClass = true
147
+ } else if (ch === '/') {
148
+ let j = i + 1
149
+ while (j < text.length && /[a-z]/.test(text[j] ?? '')) j++
150
+ return [text.slice(startOffset, j)]
151
+ }
152
+ i++
153
+ }
154
+ return null
155
+ },
156
+ })
157
+
158
+ // Code block: ```language\n...content...```
159
+ export const CodeBlock = createToken({
160
+ name: 'CodeBlock',
161
+ line_breaks: true,
162
+ pattern: (text: string, startOffset: number): CustomPatternMatcherReturn | null => {
163
+ if (text[startOffset] !== '`' || text[startOffset + 1] !== '`' || text[startOffset + 2] !== '`')
164
+ return null
165
+ let i = startOffset + 3
166
+ // require at least one language identifier character
167
+ if (i >= text.length || !/[a-zA-Z0-9_]/.test(text[i]!)) return null
168
+ const langStart = i
169
+ while (i < text.length && text[i] !== '\n' && text[i] !== '\r') i++
170
+ if (i === langStart) return null // no language identifier
171
+ if (i >= text.length) return null
172
+ // skip line ending
173
+ if (text[i] === '\r' && text[i + 1] === '\n') i += 2
174
+ else i++
175
+ // find closing ```
176
+ while (i < text.length - 2) {
177
+ if (text[i] === '\\') {
178
+ i += 2
179
+ continue
180
+ }
181
+ if (text[i] === '`' && text[i + 1] === '`' && text[i + 2] === '`') {
182
+ return [text.slice(startOffset, i + 3)]
183
+ }
184
+ i++
185
+ }
186
+ return null
187
+ },
68
188
  })
69
189
 
70
190
  // Literals
71
- export const PathLiteral = createToken({
72
- name: 'PathLiteral',
73
- pattern: /"([^"\\]|\\.)*"/,
191
+ export const StringLiteral = createToken({
192
+ name: 'StringLiteral',
193
+ pattern: /'([^'\\]|\\.)*'|"([^"\\]|\\.)*"/,
194
+ })
195
+ export const NumberLiteral = createToken({
196
+ name: 'NumberLiteral',
197
+ pattern: /\d+(\.\d+)?/,
198
+ })
199
+ export const TrueTok = createToken({
200
+ name: 'TrueTok',
201
+ pattern: /true\b/i,
202
+ })
203
+ export const FalseTok = createToken({
204
+ name: 'FalseTok',
205
+ pattern: /false\b/i,
74
206
  })
75
207
 
76
208
  // Basic objects (native types)
@@ -90,6 +222,14 @@ export const UnitTok = createToken({
90
222
  name: 'UnitTok',
91
223
  pattern: /unit\b/i,
92
224
  })
225
+ export const ConceptTok = createToken({
226
+ name: 'ConceptTok',
227
+ pattern: /concept\b/i,
228
+ })
229
+ export const EnvironmentTok = createToken({
230
+ name: 'EnvironmentTok',
231
+ pattern: /environment\b/i,
232
+ })
93
233
 
94
234
  // Identifiers
95
235
  export const Identifier = createToken({
@@ -99,6 +239,8 @@ export const Identifier = createToken({
99
239
 
100
240
  export const allTokens = [
101
241
  WhiteSpace,
242
+ LineComment,
243
+ BlockComment,
102
244
 
103
245
  CreateTok,
104
246
  AsTok,
@@ -110,8 +252,17 @@ export const allTokens = [
110
252
  PackageTok,
111
253
  ExecutableTok,
112
254
  ModuleTok,
255
+ EnumTok,
256
+ UnionTok,
257
+ IntersectTok,
258
+ ExceptTok,
259
+ RealizeTok,
260
+ InTok,
261
+ IncludeTok,
262
+ LambdaTok,
113
263
 
114
264
  ArrowTok,
265
+ PipeTok,
115
266
  SelectBlock,
116
267
  LCurly,
117
268
  RCurly,
@@ -123,12 +274,19 @@ export const allTokens = [
123
274
  Comma,
124
275
  Semicolon,
125
276
  Dot,
126
- PathLiteral,
277
+ StringLiteral,
278
+ NumberLiteral,
279
+ TrueTok,
280
+ FalseTok,
281
+ PatternLiteral,
282
+ CodeBlock,
127
283
 
128
284
  StringTok,
129
285
  NumberTok,
130
286
  BoolTok,
131
287
  UnitTok,
288
+ ConceptTok,
289
+ EnvironmentTok,
132
290
 
133
291
  Identifier,
134
292
  ]
package/src/parser.ts CHANGED
@@ -11,8 +11,18 @@ import {
11
11
  PackageTok,
12
12
  ExecutableTok,
13
13
  ModuleTok,
14
+ EnumTok,
15
+ UnionTok,
16
+ IntersectTok,
17
+ ExceptTok,
18
+ RealizeTok,
19
+ InTok,
20
+ IncludeTok,
21
+ LambdaTok,
14
22
  ArrowTok,
23
+ PipeTok,
15
24
  SelectBlock,
25
+ CodeBlock,
16
26
  LBracket,
17
27
  RBracket,
18
28
  LParen,
@@ -22,11 +32,17 @@ import {
22
32
  Semicolon,
23
33
  Dot,
24
34
  Identifier,
25
- PathLiteral,
35
+ StringLiteral,
36
+ NumberLiteral,
37
+ TrueTok,
38
+ FalseTok,
39
+ PatternLiteral,
26
40
  StringTok,
27
41
  NumberTok,
28
42
  BoolTok,
29
43
  UnitTok,
44
+ ConceptTok,
45
+ EnvironmentTok,
30
46
  } from './lexer.js'
31
47
 
32
48
  export class SpexParser extends CstParser {
@@ -43,6 +59,10 @@ export class SpexParser extends CstParser {
43
59
 
44
60
  private declaration = this.RULE('declaration', () => {
45
61
  this.OR([
62
+ {
63
+ GATE: this.BACKTRACK(this.realizeDeclaration),
64
+ ALT: () => this.SUBRULE(this.realizeDeclaration),
65
+ },
46
66
  {
47
67
  GATE: this.BACKTRACK(this.packageDeclaration),
48
68
  ALT: () => this.SUBRULE(this.packageDeclaration),
@@ -59,6 +79,10 @@ export class SpexParser extends CstParser {
59
79
  GATE: this.BACKTRACK(this.exportDeclaration),
60
80
  ALT: () => this.SUBRULE(this.exportDeclaration),
61
81
  },
82
+ {
83
+ GATE: this.BACKTRACK(this.includeDeclaration),
84
+ ALT: () => this.SUBRULE(this.includeDeclaration),
85
+ },
62
86
  {
63
87
  ALT: () => this.SUBRULE(this.generateDeclaration),
64
88
  },
@@ -69,10 +93,50 @@ export class SpexParser extends CstParser {
69
93
  this.CONSUME(CreateTok)
70
94
  this.CONSUME(Identifier)
71
95
  this.CONSUME(AsTok)
72
- this.SUBRULE(this.objectExpression)
96
+ this.SUBRULE(this.setObject)
73
97
  this.CONSUME(Semicolon)
74
98
  })
75
99
 
100
+ private setObject = this.RULE('setObject', () => {
101
+ this.SUBRULE(this.coproductObject)
102
+ this.MANY(() => {
103
+ this.OR([
104
+ { ALT: () => this.CONSUME(UnionTok, { LABEL: 'op' }) },
105
+ { ALT: () => this.CONSUME(IntersectTok, { LABEL: 'op' }) },
106
+ { ALT: () => this.CONSUME(ExceptTok, { LABEL: 'op' }) },
107
+ ])
108
+ this.SUBRULE2(this.coproductObject)
109
+ })
110
+ })
111
+
112
+ private coproductObject = this.RULE('coproductObject', () => {
113
+ this.SUBRULE(this.objectExpression)
114
+ this.MANY(() => {
115
+ this.CONSUME(PipeTok, { LABEL: 'op' })
116
+ this.SUBRULE2(this.objectExpression)
117
+ })
118
+ })
119
+
120
+ private enumObject = this.RULE('enumObject', () => {
121
+ this.CONSUME(EnumTok)
122
+ this.CONSUME(LParen)
123
+ this.CONSUME(StringLiteral)
124
+ this.MANY(() => {
125
+ this.CONSUME(Comma)
126
+ this.CONSUME2(StringLiteral)
127
+ })
128
+ this.CONSUME(RParen)
129
+ })
130
+
131
+ private literalObject = this.RULE('literalObject', () => {
132
+ this.OR([
133
+ { ALT: () => this.CONSUME(StringLiteral) },
134
+ { ALT: () => this.CONSUME(NumberLiteral) },
135
+ { ALT: () => this.CONSUME(TrueTok) },
136
+ { ALT: () => this.CONSUME(FalseTok) },
137
+ ])
138
+ })
139
+
76
140
  private objectExpression = this.RULE('objectExpression', () => {
77
141
  this.SUBRULE(this.objectOperand, { LABEL: 'base' })
78
142
  this.OPTION(() => {
@@ -83,10 +147,27 @@ export class SpexParser extends CstParser {
83
147
 
84
148
  private objectOperand = this.RULE('objectOperand', () => {
85
149
  this.OR([
150
+ {
151
+ ALT: () => this.SUBRULE(this.literalObject),
152
+ },
153
+ {
154
+ ALT: () => this.SUBRULE(this.enumObject),
155
+ },
156
+ {
157
+ ALT: () => this.SUBRULE(this.patternObject),
158
+ },
159
+ {
160
+ GATE: this.BACKTRACK(this.lambdaObject),
161
+ ALT: () => this.SUBRULE(this.lambdaObject),
162
+ },
86
163
  {
87
164
  GATE: this.BACKTRACK(this.subObject),
88
165
  ALT: () => this.SUBRULE(this.subObject),
89
166
  },
167
+ {
168
+ GATE: this.BACKTRACK(this.parenthesizedObject),
169
+ ALT: () => this.SUBRULE(this.parenthesizedObject),
170
+ },
90
171
  {
91
172
  GATE: this.BACKTRACK(this.productObject),
92
173
  ALT: () => this.SUBRULE(this.productObject),
@@ -101,6 +182,16 @@ export class SpexParser extends CstParser {
101
182
  })
102
183
  })
103
184
 
185
+ private parenthesizedObject = this.RULE('parenthesizedObject', () => {
186
+ this.CONSUME(LParen)
187
+ this.SUBRULE(this.setObject)
188
+ this.CONSUME(RParen)
189
+ })
190
+
191
+ private patternObject = this.RULE('patternObject', () => {
192
+ this.CONSUME(PatternLiteral)
193
+ })
194
+
104
195
  private namedObject = this.RULE('namedObject', () => {
105
196
  this.OR([
106
197
  { ALT: () => this.CONSUME(Identifier) },
@@ -108,6 +199,8 @@ export class SpexParser extends CstParser {
108
199
  { ALT: () => this.CONSUME(NumberTok) },
109
200
  { ALT: () => this.CONSUME(BoolTok) },
110
201
  { ALT: () => this.CONSUME(UnitTok) },
202
+ { ALT: () => this.CONSUME(ConceptTok) },
203
+ { ALT: () => this.CONSUME(EnvironmentTok) },
111
204
  ])
112
205
  this.MANY(() => {
113
206
  this.CONSUME(Dot)
@@ -120,7 +213,7 @@ export class SpexParser extends CstParser {
120
213
  this.MANY(() => {
121
214
  this.CONSUME(Identifier)
122
215
  this.CONSUME(Colon)
123
- this.SUBRULE(this.objectExpression)
216
+ this.SUBRULE(this.setObject)
124
217
  this.OPTION(() => this.CONSUME(Comma))
125
218
  })
126
219
  this.CONSUME(RParen)
@@ -128,7 +221,7 @@ export class SpexParser extends CstParser {
128
221
 
129
222
  private subObject = this.RULE('subObject', () => {
130
223
  this.CONSUME(FromTok)
131
- this.SUBRULE(this.objectExpression, { LABEL: 'base' })
224
+ this.SUBRULE(this.setObject, { LABEL: 'base' })
132
225
  this.CONSUME(SelectTok)
133
226
  this.CONSUME(SelectBlock)
134
227
  })
@@ -150,7 +243,7 @@ export class SpexParser extends CstParser {
150
243
  private namedImport = this.RULE('namedImport', () => {
151
244
  this.CONSUME(Identifier)
152
245
  this.CONSUME(FromTok)
153
- this.CONSUME(PathLiteral)
246
+ this.CONSUME(StringLiteral)
154
247
  this.OPTION(() => {
155
248
  this.CONSUME(AsTok)
156
249
  this.CONSUME2(Identifier)
@@ -158,7 +251,7 @@ export class SpexParser extends CstParser {
158
251
  })
159
252
 
160
253
  private moduleImport = this.RULE('moduleImport', () => {
161
- this.CONSUME(PathLiteral)
254
+ this.CONSUME(StringLiteral)
162
255
  this.CONSUME(AsTok)
163
256
  this.CONSUME(Identifier)
164
257
  })
@@ -180,7 +273,35 @@ export class SpexParser extends CstParser {
180
273
  this.OR([{ ALT: () => this.CONSUME(ExecutableTok) }, { ALT: () => this.CONSUME(ModuleTok) }])
181
274
  this.CONSUME(Identifier)
182
275
  this.CONSUME(AsTok)
183
- this.SUBRULE(this.objectExpression)
276
+ this.SUBRULE(this.setObject)
184
277
  this.CONSUME(Semicolon)
185
278
  })
279
+
280
+ private realizeDeclaration = this.RULE('realizeDeclaration', () => {
281
+ this.CONSUME(RealizeTok)
282
+ this.SUBRULE(this.setObject, { LABEL: 'object' })
283
+ this.CONSUME(AsTok)
284
+ this.SUBRULE2(this.setObject, { LABEL: 'target' })
285
+ this.OPTION(() => {
286
+ this.CONSUME(InTok)
287
+ this.SUBRULE3(this.setObject, { LABEL: 'environment' })
288
+ })
289
+ this.CONSUME(Semicolon)
290
+ })
291
+
292
+ private includeDeclaration = this.RULE('includeDeclaration', () => {
293
+ this.CONSUME(IncludeTok)
294
+ this.CONSUME(StringLiteral)
295
+ this.CONSUME(AsTok)
296
+ this.CONSUME(Identifier)
297
+ this.CONSUME(Semicolon)
298
+ })
299
+
300
+ private lambdaObject = this.RULE('lambdaObject', () => {
301
+ this.CONSUME(LambdaTok)
302
+ this.SUBRULE(this.objectOperand, { LABEL: 'base' })
303
+ this.CONSUME(ArrowTok)
304
+ this.SUBRULE(this.setObject, { LABEL: 'exponent' })
305
+ this.CONSUME(CodeBlock)
306
+ })
186
307
  }