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.
- package/README.md +312 -1
- package/dist/ast.d.ts +70 -2
- package/dist/ast.d.ts.map +1 -1
- package/dist/constants.d.ts +20 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +32 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts +19 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +172 -5
- package/dist/lexer.js.map +1 -1
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +103 -7
- package/dist/parser.js.map +1 -1
- package/dist/visitor.d.ts +11 -2
- package/dist/visitor.d.ts.map +1 -1
- package/dist/visitor.js +181 -8
- package/dist/visitor.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +93 -0
- package/src/constants.ts +40 -0
- package/src/index.ts +20 -3
- package/src/lexer.ts +164 -6
- package/src/parser.ts +128 -7
- package/src/visitor.ts +195 -11
- package/tests/constants.test.ts +102 -0
- package/tests/e2e.test.ts +15 -3
- package/tests/lexer.test.ts +291 -9
- package/tests/parser.test.ts +489 -0
- package/tests/props/express_todo_web.spex +108 -0
- package/tests/props/express_web_env.spex +16 -0
- package/tests/props/flask_todo_web.spex +111 -0
- package/tests/props/flask_web_env.spex +16 -0
- package/tests/props/python_cli_env.spex +16 -0
- package/tests/props/python_todo_cli.spex +70 -0
- package/tests/props/todo.spex +100 -60
- package/tests/props/typescript_cli_env.spex +16 -0
- package/tests/props/typescript_todo_cli.spex +70 -0
- package/tests/visitor.test.ts +892 -0
package/tests/visitor.test.ts
CHANGED
|
@@ -6,10 +6,30 @@ import type {
|
|
|
6
6
|
ExportDeclaration,
|
|
7
7
|
GenerateDeclaration,
|
|
8
8
|
PackageDeclaration,
|
|
9
|
+
RealizeDeclaration,
|
|
10
|
+
IncludeDeclaration,
|
|
11
|
+
ExponentialPattern,
|
|
12
|
+
EnumObject,
|
|
9
13
|
Constraint,
|
|
10
14
|
} from '../src/ast.js'
|
|
11
15
|
|
|
12
16
|
describe('SpexParserVisitor', () => {
|
|
17
|
+
describe('lexing errors', () => {
|
|
18
|
+
it('should throw on unexpected characters', () => {
|
|
19
|
+
expect(() => parseToAst('package module spex-parser as Main;')).toThrow(
|
|
20
|
+
'Lexing errors: unexpected character: ->-<'
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should throw on unclosed block comments', () => {
|
|
25
|
+
expect(() => parseToAst('create Foo as string; /* unclosed')).toThrow(/Lexing errors:/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should not throw when the input lexes cleanly', () => {
|
|
29
|
+
expect(() => parseToAst('create Foo as string;')).not.toThrow()
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
13
33
|
describe('object declaration', () => {
|
|
14
34
|
it('should convert named object declaration to AST', () => {
|
|
15
35
|
const testCase = 'create MyObject as Number;'
|
|
@@ -218,6 +238,29 @@ describe('SpexParserVisitor', () => {
|
|
|
218
238
|
})
|
|
219
239
|
})
|
|
220
240
|
|
|
241
|
+
it('should convert subobject declaration with a set operation base to AST', () => {
|
|
242
|
+
const testCase =
|
|
243
|
+
'create ExpressWebEnv as from Web intersect TypeScript select { is an express app };'
|
|
244
|
+
const ast = parseToAst(testCase)
|
|
245
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
246
|
+
expect(decl).toEqual({
|
|
247
|
+
kind: 'ObjectDeclaration',
|
|
248
|
+
name: 'ExpressWebEnv',
|
|
249
|
+
object: {
|
|
250
|
+
kind: 'SubObject',
|
|
251
|
+
base: {
|
|
252
|
+
kind: 'SetIntersectionObject',
|
|
253
|
+
left: { kind: 'NamedObject', name: 'Web' },
|
|
254
|
+
right: { kind: 'NamedObject', name: 'TypeScript' },
|
|
255
|
+
},
|
|
256
|
+
constraint: {
|
|
257
|
+
raw: 'is an express app',
|
|
258
|
+
parts: [{ kind: 'ConstraintText', text: 'is an express app' }],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
221
264
|
it('should convert subobject declaration with text constraint to AST', () => {
|
|
222
265
|
const testCase = 'create PositiveNumber as from Number select { the number is positive };'
|
|
223
266
|
const ast = parseToAst(testCase)
|
|
@@ -375,6 +418,34 @@ describe('SpexParserVisitor', () => {
|
|
|
375
418
|
})
|
|
376
419
|
})
|
|
377
420
|
|
|
421
|
+
it('should convert basic object concept to AST', () => {
|
|
422
|
+
const testCase = 'create MyObject as concept;'
|
|
423
|
+
const ast = parseToAst(testCase)
|
|
424
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
425
|
+
expect(decl).toEqual({
|
|
426
|
+
kind: 'ObjectDeclaration',
|
|
427
|
+
name: 'MyObject',
|
|
428
|
+
object: {
|
|
429
|
+
kind: 'NamedObject',
|
|
430
|
+
name: 'concept',
|
|
431
|
+
},
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('should convert basic object environment to AST', () => {
|
|
436
|
+
const testCase = 'create MyObject as environment;'
|
|
437
|
+
const ast = parseToAst(testCase)
|
|
438
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
439
|
+
expect(decl).toEqual({
|
|
440
|
+
kind: 'ObjectDeclaration',
|
|
441
|
+
name: 'MyObject',
|
|
442
|
+
object: {
|
|
443
|
+
kind: 'NamedObject',
|
|
444
|
+
name: 'environment',
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
|
|
378
449
|
it('should convert array type declaration to AST', () => {
|
|
379
450
|
const testCase = 'create MyArray as string[];'
|
|
380
451
|
const ast = parseToAst(testCase)
|
|
@@ -467,6 +538,25 @@ describe('SpexParserVisitor', () => {
|
|
|
467
538
|
alias: 'types',
|
|
468
539
|
})
|
|
469
540
|
})
|
|
541
|
+
|
|
542
|
+
it('should convert named import with single quoted source to AST', () => {
|
|
543
|
+
const testCase = "import EmailAddress from 'types.spex' as Username;"
|
|
544
|
+
const ast = parseToAst(testCase)
|
|
545
|
+
const decl = ast.declarations[0] as ImportDeclaration
|
|
546
|
+
expect(decl).toEqual({
|
|
547
|
+
kind: 'ImportDeclaration',
|
|
548
|
+
name: 'EmailAddress',
|
|
549
|
+
source: 'types.spex',
|
|
550
|
+
alias: 'Username',
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('should unescape escaped characters in import sources', () => {
|
|
555
|
+
const testCase = "import EmailAddress from 'dir\\\\types.spex';"
|
|
556
|
+
const ast = parseToAst(testCase)
|
|
557
|
+
const decl = ast.declarations[0] as ImportDeclaration
|
|
558
|
+
expect(decl.source).toBe('dir\\types.spex')
|
|
559
|
+
})
|
|
470
560
|
})
|
|
471
561
|
|
|
472
562
|
describe('export declaration', () => {
|
|
@@ -493,6 +583,491 @@ describe('SpexParserVisitor', () => {
|
|
|
493
583
|
})
|
|
494
584
|
})
|
|
495
585
|
|
|
586
|
+
describe('enum object', () => {
|
|
587
|
+
it('should convert enum object declaration to AST', () => {
|
|
588
|
+
const testCase = "create myEnum as enum ('v1', 'v2');"
|
|
589
|
+
const ast = parseToAst(testCase)
|
|
590
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
591
|
+
expect(decl).toEqual({
|
|
592
|
+
kind: 'ObjectDeclaration',
|
|
593
|
+
name: 'myEnum',
|
|
594
|
+
object: {
|
|
595
|
+
kind: 'EnumObject',
|
|
596
|
+
values: ['v1', 'v2'],
|
|
597
|
+
},
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('should convert single-value enum object to AST', () => {
|
|
602
|
+
const testCase = "create Status as enum ('ACTIVE');"
|
|
603
|
+
const ast = parseToAst(testCase)
|
|
604
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
605
|
+
expect(decl).toEqual({
|
|
606
|
+
kind: 'ObjectDeclaration',
|
|
607
|
+
name: 'Status',
|
|
608
|
+
object: {
|
|
609
|
+
kind: 'EnumObject',
|
|
610
|
+
values: ['ACTIVE'],
|
|
611
|
+
},
|
|
612
|
+
})
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('should convert enum object with mixed quote values to AST', () => {
|
|
616
|
+
const testCase = "create myEnum as enum (\"v1\", 'v2');"
|
|
617
|
+
const ast = parseToAst(testCase)
|
|
618
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
619
|
+
expect(decl).toEqual({
|
|
620
|
+
kind: 'ObjectDeclaration',
|
|
621
|
+
name: 'myEnum',
|
|
622
|
+
object: {
|
|
623
|
+
kind: 'EnumObject',
|
|
624
|
+
values: ['v1', 'v2'],
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it('should convert enum object with escaped values to AST', () => {
|
|
630
|
+
const testCase = "create myEnum as enum ('it\\'s', \"a \\\"b\\\"\", 'a\\\\b');"
|
|
631
|
+
const ast = parseToAst(testCase)
|
|
632
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
633
|
+
expect(decl).toEqual({
|
|
634
|
+
kind: 'ObjectDeclaration',
|
|
635
|
+
name: 'myEnum',
|
|
636
|
+
object: {
|
|
637
|
+
kind: 'EnumObject',
|
|
638
|
+
values: ["it's", 'a "b"', 'a\\b'],
|
|
639
|
+
},
|
|
640
|
+
})
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it('should convert enum object inside a product object to AST', () => {
|
|
644
|
+
const testCase = "create Config as (kind: enum ('a', 'b'));"
|
|
645
|
+
const ast = parseToAst(testCase)
|
|
646
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
647
|
+
expect(decl.object).toEqual({
|
|
648
|
+
kind: 'ProductObject',
|
|
649
|
+
fields: {
|
|
650
|
+
kind: { kind: 'EnumObject', values: ['a', 'b'] },
|
|
651
|
+
},
|
|
652
|
+
})
|
|
653
|
+
})
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
describe('literal object', () => {
|
|
657
|
+
it('should convert string literal object to AST', () => {
|
|
658
|
+
const testCase = 'create Foo as "SpexFile";'
|
|
659
|
+
const ast = parseToAst(testCase)
|
|
660
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
661
|
+
expect(decl).toEqual({
|
|
662
|
+
kind: 'ObjectDeclaration',
|
|
663
|
+
name: 'Foo',
|
|
664
|
+
object: {
|
|
665
|
+
kind: 'StringLiteralObject',
|
|
666
|
+
value: 'SpexFile',
|
|
667
|
+
},
|
|
668
|
+
})
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('should convert number literal object to AST', () => {
|
|
672
|
+
const testCase = 'create Foo as 42;'
|
|
673
|
+
const ast = parseToAst(testCase)
|
|
674
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
675
|
+
expect(decl.object).toEqual({
|
|
676
|
+
kind: 'NumberLiteralObject',
|
|
677
|
+
value: '42',
|
|
678
|
+
})
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
it('should convert bool literal object to AST', () => {
|
|
682
|
+
const ast = parseToAst('create Foo as true;')
|
|
683
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
684
|
+
expect(decl.object).toEqual({
|
|
685
|
+
kind: 'BoolLiteralObject',
|
|
686
|
+
value: true,
|
|
687
|
+
})
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
it('should convert product of literal objects to AST', () => {
|
|
691
|
+
const testCase = "create Foo as (name: \"John\", age: 42);"
|
|
692
|
+
const ast = parseToAst(testCase)
|
|
693
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
694
|
+
expect(decl.object).toEqual({
|
|
695
|
+
kind: 'ProductObject',
|
|
696
|
+
fields: {
|
|
697
|
+
name: { kind: 'StringLiteralObject', value: 'John' },
|
|
698
|
+
age: { kind: 'NumberLiteralObject', value: '42' },
|
|
699
|
+
},
|
|
700
|
+
})
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
it('should unescape string literal object values', () => {
|
|
704
|
+
const testCase = "create Foo as 'it\\'s';"
|
|
705
|
+
const ast = parseToAst(testCase)
|
|
706
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
707
|
+
expect(decl.object).toEqual({
|
|
708
|
+
kind: 'StringLiteralObject',
|
|
709
|
+
value: "it's",
|
|
710
|
+
})
|
|
711
|
+
})
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
describe('set object', () => {
|
|
715
|
+
it('should convert union object to AST', () => {
|
|
716
|
+
const testCase = 'create X as A UNION B;'
|
|
717
|
+
const ast = parseToAst(testCase)
|
|
718
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
719
|
+
expect(decl.object).toEqual({
|
|
720
|
+
kind: 'SetUnionObject',
|
|
721
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
722
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
723
|
+
})
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
it('should convert intersect object to AST', () => {
|
|
727
|
+
const testCase = 'create X as A INTERSECT B;'
|
|
728
|
+
const ast = parseToAst(testCase)
|
|
729
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
730
|
+
expect(decl.object).toEqual({
|
|
731
|
+
kind: 'SetIntersectionObject',
|
|
732
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
733
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
734
|
+
})
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
it('should convert except object to AST', () => {
|
|
738
|
+
const testCase = 'create X as A EXCEPT B;'
|
|
739
|
+
const ast = parseToAst(testCase)
|
|
740
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
741
|
+
expect(decl.object).toEqual({
|
|
742
|
+
kind: 'SetDifferenceObject',
|
|
743
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
744
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
745
|
+
})
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
it('should convert chained set operations left-associatively', () => {
|
|
749
|
+
const testCase = 'create X as A UNION B EXCEPT C;'
|
|
750
|
+
const ast = parseToAst(testCase)
|
|
751
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
752
|
+
expect(decl.object).toEqual({
|
|
753
|
+
kind: 'SetDifferenceObject',
|
|
754
|
+
left: {
|
|
755
|
+
kind: 'SetUnionObject',
|
|
756
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
757
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
758
|
+
},
|
|
759
|
+
right: { kind: 'NamedObject', name: 'C' },
|
|
760
|
+
})
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
it('should preserve mixed operation order', () => {
|
|
764
|
+
const testCase = 'create X as A EXCEPT B UNION C;'
|
|
765
|
+
const ast = parseToAst(testCase)
|
|
766
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
767
|
+
expect(decl.object).toEqual({
|
|
768
|
+
kind: 'SetUnionObject',
|
|
769
|
+
left: {
|
|
770
|
+
kind: 'SetDifferenceObject',
|
|
771
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
772
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
773
|
+
},
|
|
774
|
+
right: { kind: 'NamedObject', name: 'C' },
|
|
775
|
+
})
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
it('should convert set operations with literals to AST', () => {
|
|
779
|
+
const testCase = 'create X as string EXCEPT "root";'
|
|
780
|
+
const ast = parseToAst(testCase)
|
|
781
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
782
|
+
expect(decl.object).toEqual({
|
|
783
|
+
kind: 'SetDifferenceObject',
|
|
784
|
+
left: { kind: 'NamedObject', name: 'string' },
|
|
785
|
+
right: { kind: 'StringLiteralObject', value: 'root' },
|
|
786
|
+
})
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it('should convert set operations in product fields to AST', () => {
|
|
790
|
+
const testCase = 'create X as (a: A UNION B);'
|
|
791
|
+
const ast = parseToAst(testCase)
|
|
792
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
793
|
+
expect(decl.object).toEqual({
|
|
794
|
+
kind: 'ProductObject',
|
|
795
|
+
fields: {
|
|
796
|
+
a: {
|
|
797
|
+
kind: 'SetUnionObject',
|
|
798
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
799
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
})
|
|
803
|
+
})
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
describe('coproduct object', () => {
|
|
807
|
+
it('should convert pipe object to AST', () => {
|
|
808
|
+
const testCase = 'create Shape as Point | Circle;'
|
|
809
|
+
const ast = parseToAst(testCase)
|
|
810
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
811
|
+
expect(decl.object).toEqual({
|
|
812
|
+
kind: 'CoproductObject',
|
|
813
|
+
left: { kind: 'NamedObject', name: 'Point' },
|
|
814
|
+
right: { kind: 'NamedObject', name: 'Circle' },
|
|
815
|
+
})
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('should convert chained pipes left-associatively', () => {
|
|
819
|
+
const testCase = 'create X as A | B | C;'
|
|
820
|
+
const ast = parseToAst(testCase)
|
|
821
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
822
|
+
expect(decl.object).toEqual({
|
|
823
|
+
kind: 'CoproductObject',
|
|
824
|
+
left: {
|
|
825
|
+
kind: 'CoproductObject',
|
|
826
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
827
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
828
|
+
},
|
|
829
|
+
right: { kind: 'NamedObject', name: 'C' },
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
it('should bind arrow tighter than pipe', () => {
|
|
834
|
+
const testCase = 'create X as A -> B | C;'
|
|
835
|
+
const ast = parseToAst(testCase)
|
|
836
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
837
|
+
expect(decl.object).toEqual({
|
|
838
|
+
kind: 'CoproductObject',
|
|
839
|
+
left: {
|
|
840
|
+
kind: 'ExponentialObject',
|
|
841
|
+
base: { kind: 'NamedObject', name: 'B' },
|
|
842
|
+
exponent: { kind: 'NamedObject', name: 'A' },
|
|
843
|
+
},
|
|
844
|
+
right: { kind: 'NamedObject', name: 'C' },
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
it('should bind pipe tighter than set operations', () => {
|
|
849
|
+
const testCase = 'create X as A UNION B | C;'
|
|
850
|
+
const ast = parseToAst(testCase)
|
|
851
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
852
|
+
expect(decl.object).toEqual({
|
|
853
|
+
kind: 'SetUnionObject',
|
|
854
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
855
|
+
right: {
|
|
856
|
+
kind: 'CoproductObject',
|
|
857
|
+
left: { kind: 'NamedObject', name: 'B' },
|
|
858
|
+
right: { kind: 'NamedObject', name: 'C' },
|
|
859
|
+
},
|
|
860
|
+
})
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
it('should bind arrow tighter than set operations', () => {
|
|
864
|
+
const testCase = 'create X as A -> B UNION C;'
|
|
865
|
+
const ast = parseToAst(testCase)
|
|
866
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
867
|
+
expect(decl.object).toEqual({
|
|
868
|
+
kind: 'SetUnionObject',
|
|
869
|
+
left: {
|
|
870
|
+
kind: 'ExponentialObject',
|
|
871
|
+
base: { kind: 'NamedObject', name: 'B' },
|
|
872
|
+
exponent: { kind: 'NamedObject', name: 'A' },
|
|
873
|
+
},
|
|
874
|
+
right: { kind: 'NamedObject', name: 'C' },
|
|
875
|
+
})
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it('should convert pipes in product fields to AST', () => {
|
|
879
|
+
const testCase = 'create X as (a: A | B, b: C);'
|
|
880
|
+
const ast = parseToAst(testCase)
|
|
881
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
882
|
+
expect(decl.object).toEqual({
|
|
883
|
+
kind: 'ProductObject',
|
|
884
|
+
fields: {
|
|
885
|
+
a: {
|
|
886
|
+
kind: 'CoproductObject',
|
|
887
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
888
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
889
|
+
},
|
|
890
|
+
b: { kind: 'NamedObject', name: 'C' },
|
|
891
|
+
},
|
|
892
|
+
})
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
it('should convert pipes with literals to AST', () => {
|
|
896
|
+
const testCase = 'create X as string | "number";'
|
|
897
|
+
const ast = parseToAst(testCase)
|
|
898
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
899
|
+
expect(decl.object).toEqual({
|
|
900
|
+
kind: 'CoproductObject',
|
|
901
|
+
left: { kind: 'NamedObject', name: 'string' },
|
|
902
|
+
right: { kind: 'StringLiteralObject', value: 'number' },
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
describe('parenthesized object', () => {
|
|
908
|
+
it('should strip grouping parentheses from the AST', () => {
|
|
909
|
+
const testCase = 'create X as (A | B);'
|
|
910
|
+
const ast = parseToAst(testCase)
|
|
911
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
912
|
+
expect(decl.object).toEqual({
|
|
913
|
+
kind: 'CoproductObject',
|
|
914
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
915
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
916
|
+
})
|
|
917
|
+
})
|
|
918
|
+
|
|
919
|
+
it('should override arrow precedence with parentheses', () => {
|
|
920
|
+
const testCase = 'create X as A -> (B | C);'
|
|
921
|
+
const ast = parseToAst(testCase)
|
|
922
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
923
|
+
expect(decl.object).toEqual({
|
|
924
|
+
kind: 'ExponentialObject',
|
|
925
|
+
base: {
|
|
926
|
+
kind: 'CoproductObject',
|
|
927
|
+
left: { kind: 'NamedObject', name: 'B' },
|
|
928
|
+
right: { kind: 'NamedObject', name: 'C' },
|
|
929
|
+
},
|
|
930
|
+
exponent: { kind: 'NamedObject', name: 'A' },
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
it('should group set operations with parentheses', () => {
|
|
935
|
+
const testCase = 'create X as (A UNION B) EXCEPT C;'
|
|
936
|
+
const ast = parseToAst(testCase)
|
|
937
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
938
|
+
expect(decl.object).toEqual({
|
|
939
|
+
kind: 'SetDifferenceObject',
|
|
940
|
+
left: {
|
|
941
|
+
kind: 'SetUnionObject',
|
|
942
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
943
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
944
|
+
},
|
|
945
|
+
right: { kind: 'NamedObject', name: 'C' },
|
|
946
|
+
})
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
it('should apply array brackets to the whole group', () => {
|
|
950
|
+
const testCase = 'create X as (A | B)[];'
|
|
951
|
+
const ast = parseToAst(testCase)
|
|
952
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
953
|
+
expect(decl.object).toEqual({
|
|
954
|
+
kind: 'ArrayObject',
|
|
955
|
+
base: {
|
|
956
|
+
kind: 'CoproductObject',
|
|
957
|
+
left: { kind: 'NamedObject', name: 'A' },
|
|
958
|
+
right: { kind: 'NamedObject', name: 'B' },
|
|
959
|
+
},
|
|
960
|
+
})
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
it('should convert the empty product to the unit object', () => {
|
|
964
|
+
const testCase = 'create X as ();'
|
|
965
|
+
const ast = parseToAst(testCase)
|
|
966
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
967
|
+
expect(decl.object).toEqual({ kind: 'NamedObject', name: 'unit' })
|
|
968
|
+
expect(parseToAst('create X as unit;').declarations[0]).toEqual(decl)
|
|
969
|
+
})
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
describe('unit elision', () => {
|
|
973
|
+
it('should drop unit fields from a product', () => {
|
|
974
|
+
const testCase = 'create X as (id: string, foo: unit);'
|
|
975
|
+
const ast = parseToAst(testCase)
|
|
976
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
977
|
+
expect(decl.object).toEqual({
|
|
978
|
+
kind: 'ProductObject',
|
|
979
|
+
fields: {
|
|
980
|
+
id: { kind: 'NamedObject', name: 'string' },
|
|
981
|
+
},
|
|
982
|
+
})
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
it('should collapse a product of only unit fields to the unit object', () => {
|
|
986
|
+
const testCase = 'create X as (foo: unit, bar: unit);'
|
|
987
|
+
const ast = parseToAst(testCase)
|
|
988
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
989
|
+
expect(decl.object).toEqual({ kind: 'NamedObject', name: 'unit' })
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
it('should drop fields whose value is an empty product', () => {
|
|
993
|
+
const testCase = 'create X as (id: string, foo: ());'
|
|
994
|
+
const ast = parseToAst(testCase)
|
|
995
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
996
|
+
expect(decl.object).toEqual({
|
|
997
|
+
kind: 'ProductObject',
|
|
998
|
+
fields: {
|
|
999
|
+
id: { kind: 'NamedObject', name: 'string' },
|
|
1000
|
+
},
|
|
1001
|
+
})
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
it('should keep fields that are not the unit object', () => {
|
|
1005
|
+
const testCase = 'create X as (id: string, nothing: (a: unit, b: bool));'
|
|
1006
|
+
const ast = parseToAst(testCase)
|
|
1007
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1008
|
+
expect(decl.object).toEqual({
|
|
1009
|
+
kind: 'ProductObject',
|
|
1010
|
+
fields: {
|
|
1011
|
+
id: { kind: 'NamedObject', name: 'string' },
|
|
1012
|
+
nothing: {
|
|
1013
|
+
kind: 'ProductObject',
|
|
1014
|
+
fields: {
|
|
1015
|
+
b: { kind: 'NamedObject', name: 'bool' },
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
})
|
|
1020
|
+
})
|
|
1021
|
+
})
|
|
1022
|
+
|
|
1023
|
+
describe('pattern object', () => {
|
|
1024
|
+
it('should convert a pattern object to AST', () => {
|
|
1025
|
+
const testCase = 'create X as /\\d+/i;'
|
|
1026
|
+
const ast = parseToAst(testCase)
|
|
1027
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1028
|
+
expect(decl.object).toEqual({
|
|
1029
|
+
kind: 'PatternLiteralObject',
|
|
1030
|
+
source: '\\d+',
|
|
1031
|
+
flags: 'i',
|
|
1032
|
+
})
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
it('should keep the pattern source verbatim', () => {
|
|
1036
|
+
const testCase = 'create X as /\\/\\*[\\s\\S]*?\\*\\//;'
|
|
1037
|
+
const ast = parseToAst(testCase)
|
|
1038
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1039
|
+
expect(decl.object).toEqual({
|
|
1040
|
+
kind: 'PatternLiteralObject',
|
|
1041
|
+
source: '\\/\\*[\\s\\S]*?\\*\\/',
|
|
1042
|
+
flags: '',
|
|
1043
|
+
})
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
it('should record empty flags', () => {
|
|
1047
|
+
const testCase = 'create X as /[a-zA-Z_][a-zA-Z0-9_]*/;'
|
|
1048
|
+
const ast = parseToAst(testCase)
|
|
1049
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1050
|
+
expect(decl.object).toEqual({
|
|
1051
|
+
kind: 'PatternLiteralObject',
|
|
1052
|
+
source: '[a-zA-Z_][a-zA-Z0-9_]*',
|
|
1053
|
+
flags: '',
|
|
1054
|
+
})
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
it('should convert patterns in product fields to AST', () => {
|
|
1058
|
+
const testCase = 'create X as (name: string, pattern: /[a-z]+/i);'
|
|
1059
|
+
const ast = parseToAst(testCase)
|
|
1060
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1061
|
+
expect(decl.object).toEqual({
|
|
1062
|
+
kind: 'ProductObject',
|
|
1063
|
+
fields: {
|
|
1064
|
+
name: { kind: 'NamedObject', name: 'string' },
|
|
1065
|
+
pattern: { kind: 'PatternLiteralObject', source: '[a-z]+', flags: 'i' },
|
|
1066
|
+
},
|
|
1067
|
+
})
|
|
1068
|
+
})
|
|
1069
|
+
})
|
|
1070
|
+
|
|
496
1071
|
describe('parseConstraint', () => {
|
|
497
1072
|
it('should parse plain text constraint with no references', () => {
|
|
498
1073
|
const result = parseConstraint('value is positive')
|
|
@@ -596,6 +1171,101 @@ describe('SpexParserVisitor', () => {
|
|
|
596
1171
|
})
|
|
597
1172
|
})
|
|
598
1173
|
|
|
1174
|
+
describe('comments inside select constraints', () => {
|
|
1175
|
+
it('should keep comment markers in the constraint raw text', () => {
|
|
1176
|
+
const testCase = 'create Foo as from string select { are valid -- like emails };'
|
|
1177
|
+
const ast = parseToAst(testCase)
|
|
1178
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1179
|
+
expect(decl.object).toEqual({
|
|
1180
|
+
kind: 'SubObject',
|
|
1181
|
+
base: { kind: 'NamedObject', name: 'string' },
|
|
1182
|
+
constraint: {
|
|
1183
|
+
raw: 'are valid -- like emails',
|
|
1184
|
+
parts: [{ kind: 'ConstraintText', text: 'are valid -- like emails' }],
|
|
1185
|
+
},
|
|
1186
|
+
})
|
|
1187
|
+
})
|
|
1188
|
+
|
|
1189
|
+
it('should extract references alongside comment markers', () => {
|
|
1190
|
+
const testCase = 'create Foo as from string select { match /* strict */ @pattern };'
|
|
1191
|
+
const ast = parseToAst(testCase)
|
|
1192
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1193
|
+
expect(decl.object).toEqual({
|
|
1194
|
+
kind: 'SubObject',
|
|
1195
|
+
base: { kind: 'NamedObject', name: 'string' },
|
|
1196
|
+
constraint: {
|
|
1197
|
+
raw: 'match /* strict */ @pattern',
|
|
1198
|
+
parts: [
|
|
1199
|
+
{ kind: 'ConstraintText', text: 'match /* strict */ ' },
|
|
1200
|
+
{ kind: 'ConstraintReference', name: 'pattern' },
|
|
1201
|
+
],
|
|
1202
|
+
},
|
|
1203
|
+
})
|
|
1204
|
+
})
|
|
1205
|
+
})
|
|
1206
|
+
|
|
1207
|
+
describe('escaped braces in select constraints', () => {
|
|
1208
|
+
it('should unescape an escaped close brace', () => {
|
|
1209
|
+
const testCase = 'create Foo as from string select { end with \\} };'
|
|
1210
|
+
const ast = parseToAst(testCase)
|
|
1211
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1212
|
+
expect(decl.object).toEqual({
|
|
1213
|
+
kind: 'SubObject',
|
|
1214
|
+
base: { kind: 'NamedObject', name: 'string' },
|
|
1215
|
+
constraint: {
|
|
1216
|
+
raw: 'end with }',
|
|
1217
|
+
parts: [{ kind: 'ConstraintText', text: 'end with }' }],
|
|
1218
|
+
},
|
|
1219
|
+
})
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
it('should unescape escaped open and close braces', () => {
|
|
1223
|
+
const testCase = 'create Foo as from string select { match \\{a\\} };'
|
|
1224
|
+
const ast = parseToAst(testCase)
|
|
1225
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1226
|
+
expect(decl.object).toEqual({
|
|
1227
|
+
kind: 'SubObject',
|
|
1228
|
+
base: { kind: 'NamedObject', name: 'string' },
|
|
1229
|
+
constraint: {
|
|
1230
|
+
raw: 'match {a}',
|
|
1231
|
+
parts: [{ kind: 'ConstraintText', text: 'match {a}' }],
|
|
1232
|
+
},
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
it('should unescape escaped backslashes', () => {
|
|
1237
|
+
const testCase = 'create Foo as from string select { paths use \\\\ };'
|
|
1238
|
+
const ast = parseToAst(testCase)
|
|
1239
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1240
|
+
expect(decl.object).toEqual({
|
|
1241
|
+
kind: 'SubObject',
|
|
1242
|
+
base: { kind: 'NamedObject', name: 'string' },
|
|
1243
|
+
constraint: {
|
|
1244
|
+
raw: 'paths use \\',
|
|
1245
|
+
parts: [{ kind: 'ConstraintText', text: 'paths use \\' }],
|
|
1246
|
+
},
|
|
1247
|
+
})
|
|
1248
|
+
})
|
|
1249
|
+
|
|
1250
|
+
it('should extract references next to escaped braces', () => {
|
|
1251
|
+
const testCase = 'create Foo as from string select { call @foo with \\} };'
|
|
1252
|
+
const ast = parseToAst(testCase)
|
|
1253
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1254
|
+
expect(decl.object).toEqual({
|
|
1255
|
+
kind: 'SubObject',
|
|
1256
|
+
base: { kind: 'NamedObject', name: 'string' },
|
|
1257
|
+
constraint: {
|
|
1258
|
+
raw: 'call @foo with }',
|
|
1259
|
+
parts: [
|
|
1260
|
+
{ kind: 'ConstraintText', text: 'call ' },
|
|
1261
|
+
{ kind: 'ConstraintReference', name: 'foo' },
|
|
1262
|
+
{ kind: 'ConstraintText', text: ' with }' },
|
|
1263
|
+
],
|
|
1264
|
+
},
|
|
1265
|
+
})
|
|
1266
|
+
})
|
|
1267
|
+
})
|
|
1268
|
+
|
|
599
1269
|
describe('package declaration', () => {
|
|
600
1270
|
it('should convert package executable declaration to AST', () => {
|
|
601
1271
|
const testCase = 'package executable myapp as Main;'
|
|
@@ -683,4 +1353,226 @@ describe('SpexParserVisitor', () => {
|
|
|
683
1353
|
})
|
|
684
1354
|
})
|
|
685
1355
|
})
|
|
1356
|
+
|
|
1357
|
+
describe('realize declaration', () => {
|
|
1358
|
+
it('should convert a realize declaration to AST', () => {
|
|
1359
|
+
const testCase = 'realize Shape as Circle in environment;'
|
|
1360
|
+
const ast = parseToAst(testCase)
|
|
1361
|
+
const decl = ast.declarations[0] as RealizeDeclaration
|
|
1362
|
+
expect(decl).toEqual({
|
|
1363
|
+
kind: 'RealizeDeclaration',
|
|
1364
|
+
object: { kind: 'NamedObject', name: 'Shape' },
|
|
1365
|
+
target: { kind: 'NamedObject', name: 'Circle' },
|
|
1366
|
+
environment: { kind: 'NamedObject', name: 'environment' },
|
|
1367
|
+
})
|
|
1368
|
+
})
|
|
1369
|
+
|
|
1370
|
+
it('should convert a realize declaration with complex objects to AST', () => {
|
|
1371
|
+
const testCase = 'realize string -> number as (y: string) in MyEnv;'
|
|
1372
|
+
const ast = parseToAst(testCase)
|
|
1373
|
+
const decl = ast.declarations[0] as RealizeDeclaration
|
|
1374
|
+
expect(decl).toEqual({
|
|
1375
|
+
kind: 'RealizeDeclaration',
|
|
1376
|
+
object: {
|
|
1377
|
+
kind: 'ExponentialObject',
|
|
1378
|
+
exponent: { kind: 'NamedObject', name: 'string' },
|
|
1379
|
+
base: { kind: 'NamedObject', name: 'number' },
|
|
1380
|
+
},
|
|
1381
|
+
target: {
|
|
1382
|
+
kind: 'ProductObject',
|
|
1383
|
+
fields: { y: { kind: 'NamedObject', name: 'string' } },
|
|
1384
|
+
},
|
|
1385
|
+
environment: { kind: 'NamedObject', name: 'MyEnv' },
|
|
1386
|
+
})
|
|
1387
|
+
})
|
|
1388
|
+
|
|
1389
|
+
it('should fall back to the base environment when omitted', () => {
|
|
1390
|
+
const testCase = 'realize A as B;'
|
|
1391
|
+
const ast = parseToAst(testCase)
|
|
1392
|
+
const decl = ast.declarations[0] as RealizeDeclaration
|
|
1393
|
+
expect(decl).toEqual({
|
|
1394
|
+
kind: 'RealizeDeclaration',
|
|
1395
|
+
object: { kind: 'NamedObject', name: 'A' },
|
|
1396
|
+
target: { kind: 'NamedObject', name: 'B' },
|
|
1397
|
+
environment: { kind: 'NamedObject', name: 'environment' },
|
|
1398
|
+
})
|
|
1399
|
+
})
|
|
1400
|
+
|
|
1401
|
+
it('should convert mixed declarations with realize to AST', () => {
|
|
1402
|
+
const testCase = 'create A as string;\nrealize A as B in MyEnv;'
|
|
1403
|
+
const ast = parseToAst(testCase)
|
|
1404
|
+
expect(ast.declarations).toHaveLength(2)
|
|
1405
|
+
const realizeDecl = ast.declarations[1] as RealizeDeclaration
|
|
1406
|
+
expect(realizeDecl).toEqual({
|
|
1407
|
+
kind: 'RealizeDeclaration',
|
|
1408
|
+
object: { kind: 'NamedObject', name: 'A' },
|
|
1409
|
+
target: { kind: 'NamedObject', name: 'B' },
|
|
1410
|
+
environment: { kind: 'NamedObject', name: 'MyEnv' },
|
|
1411
|
+
})
|
|
1412
|
+
})
|
|
1413
|
+
})
|
|
1414
|
+
|
|
1415
|
+
describe('include declaration', () => {
|
|
1416
|
+
it('should convert include declaration to AST', () => {
|
|
1417
|
+
const testCase = 'include "config.json" as config;'
|
|
1418
|
+
const ast = parseToAst(testCase)
|
|
1419
|
+
const decl = ast.declarations[0] as IncludeDeclaration
|
|
1420
|
+
expect(decl).toEqual({
|
|
1421
|
+
kind: 'IncludeDeclaration',
|
|
1422
|
+
name: 'config',
|
|
1423
|
+
address: 'config.json',
|
|
1424
|
+
})
|
|
1425
|
+
})
|
|
1426
|
+
|
|
1427
|
+
it('should convert include declaration case-insensitively to AST', () => {
|
|
1428
|
+
const testCase = 'INCLUDE "config.json" AS config;'
|
|
1429
|
+
const ast = parseToAst(testCase)
|
|
1430
|
+
const decl = ast.declarations[0] as IncludeDeclaration
|
|
1431
|
+
expect(decl).toEqual({
|
|
1432
|
+
kind: 'IncludeDeclaration',
|
|
1433
|
+
name: 'config',
|
|
1434
|
+
address: 'config.json',
|
|
1435
|
+
})
|
|
1436
|
+
})
|
|
1437
|
+
|
|
1438
|
+
it('should convert include with single-quoted address to AST', () => {
|
|
1439
|
+
const testCase = "include 'data/file.txt' as myfile;"
|
|
1440
|
+
const ast = parseToAst(testCase)
|
|
1441
|
+
const decl = ast.declarations[0] as IncludeDeclaration
|
|
1442
|
+
expect(decl).toEqual({
|
|
1443
|
+
kind: 'IncludeDeclaration',
|
|
1444
|
+
name: 'myfile',
|
|
1445
|
+
address: 'data/file.txt',
|
|
1446
|
+
})
|
|
1447
|
+
})
|
|
1448
|
+
|
|
1449
|
+
it('should convert include with folder address to AST', () => {
|
|
1450
|
+
const testCase = 'include "images/" as assets;'
|
|
1451
|
+
const ast = parseToAst(testCase)
|
|
1452
|
+
const decl = ast.declarations[0] as IncludeDeclaration
|
|
1453
|
+
expect(decl).toEqual({
|
|
1454
|
+
kind: 'IncludeDeclaration',
|
|
1455
|
+
name: 'assets',
|
|
1456
|
+
address: 'images/',
|
|
1457
|
+
})
|
|
1458
|
+
})
|
|
1459
|
+
|
|
1460
|
+
it('should convert mixed declarations with include to AST', () => {
|
|
1461
|
+
const testCase = 'include "config.json" as config;\ncreate Todo as (id: string);'
|
|
1462
|
+
const ast = parseToAst(testCase)
|
|
1463
|
+
expect(ast.declarations).toHaveLength(2)
|
|
1464
|
+
const includeDecl = ast.declarations[0] as IncludeDeclaration
|
|
1465
|
+
expect(includeDecl).toEqual({
|
|
1466
|
+
kind: 'IncludeDeclaration',
|
|
1467
|
+
name: 'config',
|
|
1468
|
+
address: 'config.json',
|
|
1469
|
+
})
|
|
1470
|
+
const createDecl = ast.declarations[1] as ObjectDeclaration
|
|
1471
|
+
expect(createDecl.kind).toBe('ObjectDeclaration')
|
|
1472
|
+
})
|
|
1473
|
+
})
|
|
1474
|
+
|
|
1475
|
+
describe('lambda object', () => {
|
|
1476
|
+
it('should convert lambda with product base to AST', () => {
|
|
1477
|
+
const testCase = 'create sum as lambda (a: number, b: number) -> number ```python\nreturn a + b\n```;'
|
|
1478
|
+
const ast = parseToAst(testCase)
|
|
1479
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1480
|
+
const lambda = decl.object as ExponentialPattern
|
|
1481
|
+
expect(lambda).toEqual({
|
|
1482
|
+
kind: 'ExponentialPattern',
|
|
1483
|
+
base: { kind: 'NamedObject', name: 'number' },
|
|
1484
|
+
exponent: {
|
|
1485
|
+
kind: 'ProductObject',
|
|
1486
|
+
fields: {
|
|
1487
|
+
a: { kind: 'NamedObject', name: 'number' },
|
|
1488
|
+
b: { kind: 'NamedObject', name: 'number' },
|
|
1489
|
+
},
|
|
1490
|
+
},
|
|
1491
|
+
language: 'python',
|
|
1492
|
+
body: 'return a + b',
|
|
1493
|
+
patterns: [],
|
|
1494
|
+
})
|
|
1495
|
+
})
|
|
1496
|
+
|
|
1497
|
+
it('should convert lambda with named base to AST', () => {
|
|
1498
|
+
const testCase = 'create double as lambda number -> number ```python\nreturn @n * 2\n```;'
|
|
1499
|
+
const ast = parseToAst(testCase)
|
|
1500
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1501
|
+
const lambda = decl.object as ExponentialPattern
|
|
1502
|
+
expect(lambda).toEqual({
|
|
1503
|
+
kind: 'ExponentialPattern',
|
|
1504
|
+
base: { kind: 'NamedObject', name: 'number' },
|
|
1505
|
+
exponent: { kind: 'NamedObject', name: 'number' },
|
|
1506
|
+
language: 'python',
|
|
1507
|
+
body: 'return @n * 2',
|
|
1508
|
+
patterns: [],
|
|
1509
|
+
})
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
it('should extract pattern blocks from lambda body', () => {
|
|
1513
|
+
const testCase =
|
|
1514
|
+
'create transform as lambda (x: number) -> number ```python\nif @x > 0:\n @{return sin(@x)}\nelse:\n @{return cos(@x)}\n```;'
|
|
1515
|
+
const ast = parseToAst(testCase)
|
|
1516
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1517
|
+
const lambda = decl.object as ExponentialPattern
|
|
1518
|
+
expect(lambda.patterns).toHaveLength(2)
|
|
1519
|
+
expect(lambda.patterns[0]).toEqual({
|
|
1520
|
+
raw: 'return sin(@x)',
|
|
1521
|
+
parts: [
|
|
1522
|
+
{ kind: 'ConstraintText', text: 'return sin(' },
|
|
1523
|
+
{ kind: 'ConstraintReference', name: 'x' },
|
|
1524
|
+
{ kind: 'ConstraintText', text: ')' },
|
|
1525
|
+
],
|
|
1526
|
+
start: expect.any(Number),
|
|
1527
|
+
end: expect.any(Number),
|
|
1528
|
+
})
|
|
1529
|
+
expect(lambda.patterns[1]).toEqual({
|
|
1530
|
+
raw: 'return cos(@x)',
|
|
1531
|
+
parts: [
|
|
1532
|
+
{ kind: 'ConstraintText', text: 'return cos(' },
|
|
1533
|
+
{ kind: 'ConstraintReference', name: 'x' },
|
|
1534
|
+
{ kind: 'ConstraintText', text: ')' },
|
|
1535
|
+
],
|
|
1536
|
+
start: expect.any(Number),
|
|
1537
|
+
end: expect.any(Number),
|
|
1538
|
+
})
|
|
1539
|
+
})
|
|
1540
|
+
|
|
1541
|
+
it('should track correct positions for multiple pattern blocks', () => {
|
|
1542
|
+
const testCase =
|
|
1543
|
+
'create transform as lambda (x: number) -> number ```python\nif @x > 0:\n @{return sin(@x)}\nelse:\n @{return cos(@x)}\n```;'
|
|
1544
|
+
const ast = parseToAst(testCase)
|
|
1545
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1546
|
+
const lambda = decl.object as ExponentialPattern
|
|
1547
|
+
expect(lambda.patterns[0].start).toBeLessThan(lambda.patterns[0].end)
|
|
1548
|
+
expect(lambda.patterns[1].start).toBeGreaterThan(lambda.patterns[0].end)
|
|
1549
|
+
expect(lambda.patterns[1].start).toBeLessThan(lambda.patterns[1].end)
|
|
1550
|
+
})
|
|
1551
|
+
|
|
1552
|
+
it('should convert lambda in product field to AST', () => {
|
|
1553
|
+
const testCase =
|
|
1554
|
+
'create Config as (handler: lambda (x: string) -> string ```typescript\nreturn x.toUpperCase();\n```, port: number);'
|
|
1555
|
+
const ast = parseToAst(testCase)
|
|
1556
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1557
|
+
expect(decl.object).toEqual({
|
|
1558
|
+
kind: 'ProductObject',
|
|
1559
|
+
fields: {
|
|
1560
|
+
handler: {
|
|
1561
|
+
kind: 'ExponentialPattern',
|
|
1562
|
+
base: { kind: 'NamedObject', name: 'string' },
|
|
1563
|
+
exponent: {
|
|
1564
|
+
kind: 'ProductObject',
|
|
1565
|
+
fields: {
|
|
1566
|
+
x: { kind: 'NamedObject', name: 'string' },
|
|
1567
|
+
},
|
|
1568
|
+
},
|
|
1569
|
+
language: 'typescript',
|
|
1570
|
+
body: 'return x.toUpperCase();',
|
|
1571
|
+
patterns: [],
|
|
1572
|
+
},
|
|
1573
|
+
port: { kind: 'NamedObject', name: 'number' },
|
|
1574
|
+
},
|
|
1575
|
+
})
|
|
1576
|
+
})
|
|
1577
|
+
})
|
|
686
1578
|
})
|