spex-parser 0.1.0 → 0.1.2

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.
@@ -18,7 +18,7 @@ jobs:
18
18
  - name: Setup Node.js
19
19
  uses: actions/setup-node@v4
20
20
  with:
21
- node-version: '20'
21
+ node-version: '22'
22
22
  cache: 'npm'
23
23
 
24
24
  - name: Install dependencies
@@ -4,6 +4,10 @@ on:
4
4
  release:
5
5
  types: [published]
6
6
 
7
+ permissions:
8
+ contents: read
9
+ id-token: write
10
+
7
11
  jobs:
8
12
  publish:
9
13
  name: Publish to npm
@@ -16,7 +20,7 @@ jobs:
16
20
  - name: Setup Node.js
17
21
  uses: actions/setup-node@v4
18
22
  with:
19
- node-version: '20'
23
+ node-version: '22'
20
24
  registry-url: 'https://registry.npmjs.org'
21
25
  cache: 'npm'
22
26
 
@@ -30,6 +34,4 @@ jobs:
30
34
  run: npm test
31
35
 
32
36
  - name: Publish to npm
33
- run: npm stage publish
34
- env:
35
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
37
+ run: npm publish --access public
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spex-parser",
3
3
  "repository": "github:Freelansys/spex-parser",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "description": "A parser for Spex, a language designed for programming with LLMs",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -40,6 +40,6 @@
40
40
  "vitest": "^4.0.18"
41
41
  },
42
42
  "dependencies": {
43
- "chevrotain": "^11.1.2"
43
+ "chevrotain": "^12.0.0"
44
44
  }
45
45
  }
package/src/ast.ts CHANGED
@@ -55,10 +55,27 @@ export type ExponentialObject = {
55
55
  exponent: ObjectExpression
56
56
  }
57
57
 
58
+ export type ConstraintReference = {
59
+ kind: 'ConstraintReference'
60
+ name: string
61
+ }
62
+
63
+ export type ConstraintText = {
64
+ kind: 'ConstraintText'
65
+ text: string
66
+ }
67
+
68
+ export type ConstraintPart = ConstraintReference | ConstraintText
69
+
70
+ export type Constraint = {
71
+ raw: string
72
+ parts: ConstraintPart[]
73
+ }
74
+
58
75
  export type SubObject = {
59
76
  kind: 'SubObject'
60
77
  base: ObjectExpression
61
- constraint: string
78
+ constraint: Constraint
62
79
  }
63
80
 
64
81
  export type ArrayObject = {
package/src/index.ts CHANGED
@@ -10,4 +10,8 @@ export type {
10
10
  ProductObject,
11
11
  ExponentialObject,
12
12
  SubObject,
13
+ Constraint,
14
+ ConstraintPart,
15
+ ConstraintReference,
16
+ ConstraintText,
13
17
  } from './ast.js'
package/src/visitor.ts CHANGED
@@ -11,6 +11,9 @@ import type {
11
11
  ProductObject,
12
12
  SubObject,
13
13
  ArrayObject,
14
+ Constraint,
15
+ ConstraintPart,
16
+ ConstraintReference,
14
17
  } from './ast.js'
15
18
  import { SpexLexer } from './lexer.js'
16
19
  import { SpexParser } from './parser.js'
@@ -18,6 +21,28 @@ import { SpexParser } from './parser.js'
18
21
  const parserInstance = new SpexParser()
19
22
  const BaseSpexVisitor = parserInstance.getBaseCstVisitorConstructor()
20
23
 
24
+ export const REFERENCE_PATTERN = /@([a-zA-Z_][\w]*(?:\.[a-zA-Z_][\w]*)*)/g
25
+
26
+ export function parseConstraint(raw: string): Constraint {
27
+ const parts: ConstraintPart[] = []
28
+ let lastIndex = 0
29
+ let match: RegExpExecArray | null
30
+
31
+ while ((match = REFERENCE_PATTERN.exec(raw)) !== null) {
32
+ if (match.index > lastIndex) {
33
+ parts.push({ kind: 'ConstraintText', text: raw.slice(lastIndex, match.index) })
34
+ }
35
+ parts.push({ kind: 'ConstraintReference', name: match[1]! })
36
+ lastIndex = match.index + match[0].length
37
+ }
38
+
39
+ if (lastIndex < raw.length) {
40
+ parts.push({ kind: 'ConstraintText', text: raw.slice(lastIndex) })
41
+ }
42
+
43
+ return { raw, parts }
44
+ }
45
+
21
46
  export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<any, any> {
22
47
  constructor() {
23
48
  super()
@@ -112,11 +137,11 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor<an
112
137
 
113
138
  subObject(ctx: any): SubObject {
114
139
  const rawText: string = ctx.SelectBlock[0].image
115
- const constraint = rawText.slice(1, -1).trim()
140
+ const rawConstraint = rawText.slice(1, -1).trim()
116
141
  return {
117
142
  kind: 'SubObject',
118
143
  base: this.visit(ctx.base),
119
- constraint,
144
+ constraint: parseConstraint(rawConstraint),
120
145
  }
121
146
  }
122
147
 
@@ -1,10 +1,11 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { parseToAst } from '../src/visitor.js'
2
+ import { parseToAst, parseConstraint } from '../src/visitor.js'
3
3
  import type {
4
4
  ObjectDeclaration,
5
5
  ImportDeclaration,
6
6
  ExportDeclaration,
7
7
  GenerateDeclaration,
8
+ Constraint,
8
9
  } from '../src/ast.js'
9
10
 
10
11
  describe('SpexParserVisitor', () => {
@@ -92,7 +93,10 @@ describe('SpexParserVisitor', () => {
92
93
  p: {
93
94
  kind: 'SubObject',
94
95
  base: { kind: 'NamedObject', name: 'Number' },
95
- constraint: 'value is positive',
96
+ constraint: {
97
+ raw: 'value is positive',
98
+ parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
99
+ },
96
100
  },
97
101
  n: { kind: 'NamedObject', name: 'Number' },
98
102
  },
@@ -178,12 +182,18 @@ describe('SpexParserVisitor', () => {
178
182
  exponent: {
179
183
  kind: 'SubObject',
180
184
  base: { kind: 'NamedObject', name: 'Number' },
181
- constraint: 'value is positive',
185
+ constraint: {
186
+ raw: 'value is positive',
187
+ parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
188
+ },
182
189
  },
183
190
  base: {
184
191
  kind: 'SubObject',
185
192
  base: { kind: 'NamedObject', name: 'Number' },
186
- constraint: 'value is positive',
193
+ constraint: {
194
+ raw: 'value is positive',
195
+ parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
196
+ },
187
197
  },
188
198
  },
189
199
  })
@@ -199,7 +209,10 @@ describe('SpexParserVisitor', () => {
199
209
  object: {
200
210
  kind: 'SubObject',
201
211
  base: { kind: 'NamedObject', name: 'Number' },
202
- constraint: 'isPositive',
212
+ constraint: {
213
+ raw: 'isPositive',
214
+ parts: [{ kind: 'ConstraintText', text: 'isPositive' }],
215
+ },
203
216
  },
204
217
  })
205
218
  })
@@ -214,7 +227,10 @@ describe('SpexParserVisitor', () => {
214
227
  object: {
215
228
  kind: 'SubObject',
216
229
  base: { kind: 'NamedObject', name: 'Number' },
217
- constraint: 'the number is positive',
230
+ constraint: {
231
+ raw: 'the number is positive',
232
+ parts: [{ kind: 'ConstraintText', text: 'the number is positive' }],
233
+ },
218
234
  },
219
235
  })
220
236
  })
@@ -236,7 +252,13 @@ describe('SpexParserVisitor', () => {
236
252
  s: { kind: 'NamedObject', name: 'String' },
237
253
  },
238
254
  },
239
- constraint: '@n is positive',
255
+ constraint: {
256
+ raw: '@n is positive',
257
+ parts: [
258
+ { kind: 'ConstraintReference', name: 'n' },
259
+ { kind: 'ConstraintText', text: ' is positive' },
260
+ ],
261
+ },
240
262
  },
241
263
  })
242
264
  })
@@ -262,7 +284,10 @@ describe('SpexParserVisitor', () => {
262
284
  },
263
285
  base: { kind: 'NamedObject', name: 'Bool' },
264
286
  },
265
- constraint: 'logs the given input',
287
+ constraint: {
288
+ raw: 'logs the given input',
289
+ parts: [{ kind: 'ConstraintText', text: 'logs the given input' }],
290
+ },
266
291
  },
267
292
  })
268
293
  })
@@ -280,9 +305,15 @@ describe('SpexParserVisitor', () => {
280
305
  base: {
281
306
  kind: 'SubObject',
282
307
  base: { kind: 'NamedObject', name: 'Number' },
283
- constraint: 'value is positive',
308
+ constraint: {
309
+ raw: 'value is positive',
310
+ parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
311
+ },
312
+ },
313
+ constraint: {
314
+ raw: 'value is odd',
315
+ parts: [{ kind: 'ConstraintText', text: 'value is odd' }],
284
316
  },
285
- constraint: 'value is odd',
286
317
  },
287
318
  })
288
319
  })
@@ -460,4 +491,107 @@ describe('SpexParserVisitor', () => {
460
491
  })
461
492
  })
462
493
  })
494
+
495
+ describe('parseConstraint', () => {
496
+ it('should parse plain text constraint with no references', () => {
497
+ const result = parseConstraint('value is positive')
498
+ expect(result).toEqual<Constraint>({
499
+ raw: 'value is positive',
500
+ parts: [{ kind: 'ConstraintText', text: 'value is positive' }],
501
+ })
502
+ })
503
+
504
+ it('should parse constraint with single reference', () => {
505
+ const result = parseConstraint('@n is positive')
506
+ expect(result).toEqual<Constraint>({
507
+ raw: '@n is positive',
508
+ parts: [
509
+ { kind: 'ConstraintReference', name: 'n' },
510
+ { kind: 'ConstraintText', text: ' is positive' },
511
+ ],
512
+ })
513
+ })
514
+
515
+ it('should parse constraint with reference in the middle', () => {
516
+ const result = parseConstraint('use @path for storage')
517
+ expect(result).toEqual<Constraint>({
518
+ raw: 'use @path for storage',
519
+ parts: [
520
+ { kind: 'ConstraintText', text: 'use ' },
521
+ { kind: 'ConstraintReference', name: 'path' },
522
+ { kind: 'ConstraintText', text: ' for storage' },
523
+ ],
524
+ })
525
+ })
526
+
527
+ it('should parse constraint with dotted references', () => {
528
+ const result = parseConstraint('return @z.real^2 + @z.imag^2')
529
+ expect(result).toEqual<Constraint>({
530
+ raw: 'return @z.real^2 + @z.imag^2',
531
+ parts: [
532
+ { kind: 'ConstraintText', text: 'return ' },
533
+ { kind: 'ConstraintReference', name: 'z.real' },
534
+ { kind: 'ConstraintText', text: '^2 + ' },
535
+ { kind: 'ConstraintReference', name: 'z.imag' },
536
+ { kind: 'ConstraintText', text: '^2' },
537
+ ],
538
+ })
539
+ })
540
+
541
+ it('should parse constraint with multiple references', () => {
542
+ const result = parseConstraint('call @LoadTodos using @path')
543
+ expect(result).toEqual<Constraint>({
544
+ raw: 'call @LoadTodos using @path',
545
+ parts: [
546
+ { kind: 'ConstraintText', text: 'call ' },
547
+ { kind: 'ConstraintReference', name: 'LoadTodos' },
548
+ { kind: 'ConstraintText', text: ' using ' },
549
+ { kind: 'ConstraintReference', name: 'path' },
550
+ ],
551
+ })
552
+ })
553
+
554
+ it('should parse constraint with reference at start', () => {
555
+ const result = parseConstraint('@validate the input')
556
+ expect(result).toEqual<Constraint>({
557
+ raw: '@validate the input',
558
+ parts: [
559
+ { kind: 'ConstraintReference', name: 'validate' },
560
+ { kind: 'ConstraintText', text: ' the input' },
561
+ ],
562
+ })
563
+ })
564
+
565
+ it('should parse constraint with no @ symbols', () => {
566
+ const result = parseConstraint('the user is authenticated')
567
+ expect(result).toEqual<Constraint>({
568
+ raw: 'the user is authenticated',
569
+ parts: [{ kind: 'ConstraintText', text: 'the user is authenticated' }],
570
+ })
571
+ })
572
+
573
+ it('should parse constraint with underscore in reference', () => {
574
+ const result = parseConstraint('@todo_item is valid')
575
+ expect(result).toEqual<Constraint>({
576
+ raw: '@todo_item is valid',
577
+ parts: [
578
+ { kind: 'ConstraintReference', name: 'todo_item' },
579
+ { kind: 'ConstraintText', text: ' is valid' },
580
+ ],
581
+ })
582
+ })
583
+
584
+ it('should parse empty constraint', () => {
585
+ const result = parseConstraint('')
586
+ expect(result).toEqual<Constraint>({
587
+ raw: '',
588
+ parts: [],
589
+ })
590
+ })
591
+
592
+ it('should preserve raw text exactly', () => {
593
+ const result = parseConstraint(' @foo ')
594
+ expect(result.raw).toBe(' @foo ')
595
+ })
596
+ })
463
597
  })