spex-parser 0.1.5 → 0.6.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.
- package/README.md +320 -9
- package/dist/ast.d.ts +71 -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 +17 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +159 -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 +105 -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 +182 -8
- package/dist/visitor.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +94 -0
- package/src/constants.ts +40 -0
- package/src/index.ts +20 -3
- package/src/lexer.ts +150 -6
- package/src/parser.ts +130 -7
- package/src/visitor.ts +196 -11
- package/tests/constants.test.ts +102 -0
- package/tests/e2e.test.ts +15 -3
- package/tests/lexer.test.ts +230 -9
- package/tests/parser.test.ts +455 -10
- 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 +99 -68
- package/tests/props/typescript_cli_env.spex +16 -0
- package/tests/props/typescript_todo_cli.spex +70 -0
- package/tests/visitor.test.ts +905 -6
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,9 +1171,104 @@ 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
|
-
const testCase = 'package executable myapp as Main;'
|
|
1271
|
+
const testCase = 'package executable myapp as Main in Python;'
|
|
602
1272
|
const ast = parseToAst(testCase)
|
|
603
1273
|
const decl = ast.declarations[0] as PackageDeclaration
|
|
604
1274
|
expect(decl).toEqual({
|
|
@@ -606,11 +1276,12 @@ describe('SpexParserVisitor', () => {
|
|
|
606
1276
|
packageType: 'EXECUTABLE',
|
|
607
1277
|
name: 'myapp',
|
|
608
1278
|
objectName: { kind: 'NamedObject', name: 'Main' },
|
|
1279
|
+
environment: { kind: 'NamedObject', name: 'Python' },
|
|
609
1280
|
})
|
|
610
1281
|
})
|
|
611
1282
|
|
|
612
1283
|
it('should convert package module declaration to AST', () => {
|
|
613
|
-
const testCase = 'package module mylib as utils;'
|
|
1284
|
+
const testCase = 'package module mylib as utils in Node;'
|
|
614
1285
|
const ast = parseToAst(testCase)
|
|
615
1286
|
const decl = ast.declarations[0] as PackageDeclaration
|
|
616
1287
|
expect(decl).toEqual({
|
|
@@ -618,11 +1289,12 @@ describe('SpexParserVisitor', () => {
|
|
|
618
1289
|
packageType: 'MODULE',
|
|
619
1290
|
name: 'mylib',
|
|
620
1291
|
objectName: { kind: 'NamedObject', name: 'utils' },
|
|
1292
|
+
environment: { kind: 'NamedObject', name: 'Node' },
|
|
621
1293
|
})
|
|
622
1294
|
})
|
|
623
1295
|
|
|
624
1296
|
it('should convert package executable with complex object to AST', () => {
|
|
625
|
-
const testCase = 'package executable cli as (path: string) -> unit;'
|
|
1297
|
+
const testCase = 'package executable cli as (path: string) -> unit in Python;'
|
|
626
1298
|
const ast = parseToAst(testCase)
|
|
627
1299
|
const decl = ast.declarations[0] as PackageDeclaration
|
|
628
1300
|
expect(decl).toEqual({
|
|
@@ -637,11 +1309,12 @@ describe('SpexParserVisitor', () => {
|
|
|
637
1309
|
},
|
|
638
1310
|
base: { kind: 'NamedObject', name: 'unit' },
|
|
639
1311
|
},
|
|
1312
|
+
environment: { kind: 'NamedObject', name: 'Python' },
|
|
640
1313
|
})
|
|
641
1314
|
})
|
|
642
1315
|
|
|
643
1316
|
it('should convert package executable with dotted name to AST', () => {
|
|
644
|
-
const testCase = 'package executable myapp as app.Main;'
|
|
1317
|
+
const testCase = 'package executable myapp as app.Main in Python;'
|
|
645
1318
|
const ast = parseToAst(testCase)
|
|
646
1319
|
const decl = ast.declarations[0] as PackageDeclaration
|
|
647
1320
|
expect(decl).toEqual({
|
|
@@ -649,11 +1322,12 @@ describe('SpexParserVisitor', () => {
|
|
|
649
1322
|
packageType: 'EXECUTABLE',
|
|
650
1323
|
name: 'myapp',
|
|
651
1324
|
objectName: { kind: 'NamedObject', name: 'app.Main' },
|
|
1325
|
+
environment: { kind: 'NamedObject', name: 'Python' },
|
|
652
1326
|
})
|
|
653
1327
|
})
|
|
654
1328
|
|
|
655
1329
|
it('should convert package executable with array type to AST', () => {
|
|
656
|
-
const testCase = 'package module mylib as string[];'
|
|
1330
|
+
const testCase = 'package module mylib as string[] in Python;'
|
|
657
1331
|
const ast = parseToAst(testCase)
|
|
658
1332
|
const decl = ast.declarations[0] as PackageDeclaration
|
|
659
1333
|
expect(decl).toEqual({
|
|
@@ -661,11 +1335,13 @@ describe('SpexParserVisitor', () => {
|
|
|
661
1335
|
packageType: 'MODULE',
|
|
662
1336
|
name: 'mylib',
|
|
663
1337
|
objectName: { kind: 'ArrayObject', base: { kind: 'NamedObject', name: 'string' } },
|
|
1338
|
+
environment: { kind: 'NamedObject', name: 'Python' },
|
|
664
1339
|
})
|
|
665
1340
|
})
|
|
666
1341
|
|
|
667
1342
|
it('should convert mixed declarations with package to AST', () => {
|
|
668
|
-
const testCase =
|
|
1343
|
+
const testCase =
|
|
1344
|
+
'create Main as Number;\npackage executable myapp as Main in Python;'
|
|
669
1345
|
const ast = parseToAst(testCase)
|
|
670
1346
|
expect(ast.declarations).toHaveLength(2)
|
|
671
1347
|
const createDecl = ast.declarations[0] as ObjectDeclaration
|
|
@@ -680,6 +1356,229 @@ describe('SpexParserVisitor', () => {
|
|
|
680
1356
|
packageType: 'EXECUTABLE',
|
|
681
1357
|
name: 'myapp',
|
|
682
1358
|
objectName: { kind: 'NamedObject', name: 'Main' },
|
|
1359
|
+
environment: { kind: 'NamedObject', name: 'Python' },
|
|
1360
|
+
})
|
|
1361
|
+
})
|
|
1362
|
+
})
|
|
1363
|
+
|
|
1364
|
+
describe('realize declaration', () => {
|
|
1365
|
+
it('should convert a realize declaration to AST', () => {
|
|
1366
|
+
const testCase = 'realize Shape as Circle in environment;'
|
|
1367
|
+
const ast = parseToAst(testCase)
|
|
1368
|
+
const decl = ast.declarations[0] as RealizeDeclaration
|
|
1369
|
+
expect(decl).toEqual({
|
|
1370
|
+
kind: 'RealizeDeclaration',
|
|
1371
|
+
object: { kind: 'NamedObject', name: 'Shape' },
|
|
1372
|
+
target: { kind: 'NamedObject', name: 'Circle' },
|
|
1373
|
+
environment: { kind: 'NamedObject', name: 'environment' },
|
|
1374
|
+
})
|
|
1375
|
+
})
|
|
1376
|
+
|
|
1377
|
+
it('should convert a realize declaration with complex objects to AST', () => {
|
|
1378
|
+
const testCase = 'realize string -> number as (y: string) in MyEnv;'
|
|
1379
|
+
const ast = parseToAst(testCase)
|
|
1380
|
+
const decl = ast.declarations[0] as RealizeDeclaration
|
|
1381
|
+
expect(decl).toEqual({
|
|
1382
|
+
kind: 'RealizeDeclaration',
|
|
1383
|
+
object: {
|
|
1384
|
+
kind: 'ExponentialObject',
|
|
1385
|
+
exponent: { kind: 'NamedObject', name: 'string' },
|
|
1386
|
+
base: { kind: 'NamedObject', name: 'number' },
|
|
1387
|
+
},
|
|
1388
|
+
target: {
|
|
1389
|
+
kind: 'ProductObject',
|
|
1390
|
+
fields: { y: { kind: 'NamedObject', name: 'string' } },
|
|
1391
|
+
},
|
|
1392
|
+
environment: { kind: 'NamedObject', name: 'MyEnv' },
|
|
1393
|
+
})
|
|
1394
|
+
})
|
|
1395
|
+
|
|
1396
|
+
it('should fall back to the base environment when omitted', () => {
|
|
1397
|
+
const testCase = 'realize A as B;'
|
|
1398
|
+
const ast = parseToAst(testCase)
|
|
1399
|
+
const decl = ast.declarations[0] as RealizeDeclaration
|
|
1400
|
+
expect(decl).toEqual({
|
|
1401
|
+
kind: 'RealizeDeclaration',
|
|
1402
|
+
object: { kind: 'NamedObject', name: 'A' },
|
|
1403
|
+
target: { kind: 'NamedObject', name: 'B' },
|
|
1404
|
+
environment: { kind: 'NamedObject', name: 'environment' },
|
|
1405
|
+
})
|
|
1406
|
+
})
|
|
1407
|
+
|
|
1408
|
+
it('should convert mixed declarations with realize to AST', () => {
|
|
1409
|
+
const testCase = 'create A as string;\nrealize A as B in MyEnv;'
|
|
1410
|
+
const ast = parseToAst(testCase)
|
|
1411
|
+
expect(ast.declarations).toHaveLength(2)
|
|
1412
|
+
const realizeDecl = ast.declarations[1] as RealizeDeclaration
|
|
1413
|
+
expect(realizeDecl).toEqual({
|
|
1414
|
+
kind: 'RealizeDeclaration',
|
|
1415
|
+
object: { kind: 'NamedObject', name: 'A' },
|
|
1416
|
+
target: { kind: 'NamedObject', name: 'B' },
|
|
1417
|
+
environment: { kind: 'NamedObject', name: 'MyEnv' },
|
|
1418
|
+
})
|
|
1419
|
+
})
|
|
1420
|
+
})
|
|
1421
|
+
|
|
1422
|
+
describe('include declaration', () => {
|
|
1423
|
+
it('should convert include declaration to AST', () => {
|
|
1424
|
+
const testCase = 'include "config.json" as config;'
|
|
1425
|
+
const ast = parseToAst(testCase)
|
|
1426
|
+
const decl = ast.declarations[0] as IncludeDeclaration
|
|
1427
|
+
expect(decl).toEqual({
|
|
1428
|
+
kind: 'IncludeDeclaration',
|
|
1429
|
+
name: 'config',
|
|
1430
|
+
address: 'config.json',
|
|
1431
|
+
})
|
|
1432
|
+
})
|
|
1433
|
+
|
|
1434
|
+
it('should convert include declaration case-insensitively to AST', () => {
|
|
1435
|
+
const testCase = 'INCLUDE "config.json" AS config;'
|
|
1436
|
+
const ast = parseToAst(testCase)
|
|
1437
|
+
const decl = ast.declarations[0] as IncludeDeclaration
|
|
1438
|
+
expect(decl).toEqual({
|
|
1439
|
+
kind: 'IncludeDeclaration',
|
|
1440
|
+
name: 'config',
|
|
1441
|
+
address: 'config.json',
|
|
1442
|
+
})
|
|
1443
|
+
})
|
|
1444
|
+
|
|
1445
|
+
it('should convert include with single-quoted address to AST', () => {
|
|
1446
|
+
const testCase = "include 'data/file.txt' as myfile;"
|
|
1447
|
+
const ast = parseToAst(testCase)
|
|
1448
|
+
const decl = ast.declarations[0] as IncludeDeclaration
|
|
1449
|
+
expect(decl).toEqual({
|
|
1450
|
+
kind: 'IncludeDeclaration',
|
|
1451
|
+
name: 'myfile',
|
|
1452
|
+
address: 'data/file.txt',
|
|
1453
|
+
})
|
|
1454
|
+
})
|
|
1455
|
+
|
|
1456
|
+
it('should convert include with folder address to AST', () => {
|
|
1457
|
+
const testCase = 'include "images/" as assets;'
|
|
1458
|
+
const ast = parseToAst(testCase)
|
|
1459
|
+
const decl = ast.declarations[0] as IncludeDeclaration
|
|
1460
|
+
expect(decl).toEqual({
|
|
1461
|
+
kind: 'IncludeDeclaration',
|
|
1462
|
+
name: 'assets',
|
|
1463
|
+
address: 'images/',
|
|
1464
|
+
})
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
it('should convert mixed declarations with include to AST', () => {
|
|
1468
|
+
const testCase = 'include "config.json" as config;\ncreate Todo as (id: string);'
|
|
1469
|
+
const ast = parseToAst(testCase)
|
|
1470
|
+
expect(ast.declarations).toHaveLength(2)
|
|
1471
|
+
const includeDecl = ast.declarations[0] as IncludeDeclaration
|
|
1472
|
+
expect(includeDecl).toEqual({
|
|
1473
|
+
kind: 'IncludeDeclaration',
|
|
1474
|
+
name: 'config',
|
|
1475
|
+
address: 'config.json',
|
|
1476
|
+
})
|
|
1477
|
+
const createDecl = ast.declarations[1] as ObjectDeclaration
|
|
1478
|
+
expect(createDecl.kind).toBe('ObjectDeclaration')
|
|
1479
|
+
})
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
describe('lambda object', () => {
|
|
1483
|
+
it('should convert lambda with product base to AST', () => {
|
|
1484
|
+
const testCase = 'create sum as lambda (a: number, b: number) -> number ```python\nreturn a + b\n```;'
|
|
1485
|
+
const ast = parseToAst(testCase)
|
|
1486
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1487
|
+
const lambda = decl.object as ExponentialPattern
|
|
1488
|
+
expect(lambda).toEqual({
|
|
1489
|
+
kind: 'ExponentialPattern',
|
|
1490
|
+
base: { kind: 'NamedObject', name: 'number' },
|
|
1491
|
+
exponent: {
|
|
1492
|
+
kind: 'ProductObject',
|
|
1493
|
+
fields: {
|
|
1494
|
+
a: { kind: 'NamedObject', name: 'number' },
|
|
1495
|
+
b: { kind: 'NamedObject', name: 'number' },
|
|
1496
|
+
},
|
|
1497
|
+
},
|
|
1498
|
+
language: 'python',
|
|
1499
|
+
body: 'return a + b',
|
|
1500
|
+
patterns: [],
|
|
1501
|
+
})
|
|
1502
|
+
})
|
|
1503
|
+
|
|
1504
|
+
it('should convert lambda with named base to AST', () => {
|
|
1505
|
+
const testCase = 'create double as lambda number -> number ```python\nreturn @n * 2\n```;'
|
|
1506
|
+
const ast = parseToAst(testCase)
|
|
1507
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1508
|
+
const lambda = decl.object as ExponentialPattern
|
|
1509
|
+
expect(lambda).toEqual({
|
|
1510
|
+
kind: 'ExponentialPattern',
|
|
1511
|
+
base: { kind: 'NamedObject', name: 'number' },
|
|
1512
|
+
exponent: { kind: 'NamedObject', name: 'number' },
|
|
1513
|
+
language: 'python',
|
|
1514
|
+
body: 'return @n * 2',
|
|
1515
|
+
patterns: [],
|
|
1516
|
+
})
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
it('should extract pattern blocks from lambda body', () => {
|
|
1520
|
+
const testCase =
|
|
1521
|
+
'create transform as lambda (x: number) -> number ```python\nif @x > 0:\n @{return sin(@x)}\nelse:\n @{return cos(@x)}\n```;'
|
|
1522
|
+
const ast = parseToAst(testCase)
|
|
1523
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1524
|
+
const lambda = decl.object as ExponentialPattern
|
|
1525
|
+
expect(lambda.patterns).toHaveLength(2)
|
|
1526
|
+
expect(lambda.patterns[0]).toEqual({
|
|
1527
|
+
raw: 'return sin(@x)',
|
|
1528
|
+
parts: [
|
|
1529
|
+
{ kind: 'ConstraintText', text: 'return sin(' },
|
|
1530
|
+
{ kind: 'ConstraintReference', name: 'x' },
|
|
1531
|
+
{ kind: 'ConstraintText', text: ')' },
|
|
1532
|
+
],
|
|
1533
|
+
start: expect.any(Number),
|
|
1534
|
+
end: expect.any(Number),
|
|
1535
|
+
})
|
|
1536
|
+
expect(lambda.patterns[1]).toEqual({
|
|
1537
|
+
raw: 'return cos(@x)',
|
|
1538
|
+
parts: [
|
|
1539
|
+
{ kind: 'ConstraintText', text: 'return cos(' },
|
|
1540
|
+
{ kind: 'ConstraintReference', name: 'x' },
|
|
1541
|
+
{ kind: 'ConstraintText', text: ')' },
|
|
1542
|
+
],
|
|
1543
|
+
start: expect.any(Number),
|
|
1544
|
+
end: expect.any(Number),
|
|
1545
|
+
})
|
|
1546
|
+
})
|
|
1547
|
+
|
|
1548
|
+
it('should track correct positions for multiple pattern blocks', () => {
|
|
1549
|
+
const testCase =
|
|
1550
|
+
'create transform as lambda (x: number) -> number ```python\nif @x > 0:\n @{return sin(@x)}\nelse:\n @{return cos(@x)}\n```;'
|
|
1551
|
+
const ast = parseToAst(testCase)
|
|
1552
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1553
|
+
const lambda = decl.object as ExponentialPattern
|
|
1554
|
+
expect(lambda.patterns[0].start).toBeLessThan(lambda.patterns[0].end)
|
|
1555
|
+
expect(lambda.patterns[1].start).toBeGreaterThan(lambda.patterns[0].end)
|
|
1556
|
+
expect(lambda.patterns[1].start).toBeLessThan(lambda.patterns[1].end)
|
|
1557
|
+
})
|
|
1558
|
+
|
|
1559
|
+
it('should convert lambda in product field to AST', () => {
|
|
1560
|
+
const testCase =
|
|
1561
|
+
'create Config as (handler: lambda (x: string) -> string ```typescript\nreturn x.toUpperCase();\n```, port: number);'
|
|
1562
|
+
const ast = parseToAst(testCase)
|
|
1563
|
+
const decl = ast.declarations[0] as ObjectDeclaration
|
|
1564
|
+
expect(decl.object).toEqual({
|
|
1565
|
+
kind: 'ProductObject',
|
|
1566
|
+
fields: {
|
|
1567
|
+
handler: {
|
|
1568
|
+
kind: 'ExponentialPattern',
|
|
1569
|
+
base: { kind: 'NamedObject', name: 'string' },
|
|
1570
|
+
exponent: {
|
|
1571
|
+
kind: 'ProductObject',
|
|
1572
|
+
fields: {
|
|
1573
|
+
x: { kind: 'NamedObject', name: 'string' },
|
|
1574
|
+
},
|
|
1575
|
+
},
|
|
1576
|
+
language: 'typescript',
|
|
1577
|
+
body: 'return x.toUpperCase();',
|
|
1578
|
+
patterns: [],
|
|
1579
|
+
},
|
|
1580
|
+
port: { kind: 'NamedObject', name: 'number' },
|
|
1581
|
+
},
|
|
683
1582
|
})
|
|
684
1583
|
})
|
|
685
1584
|
})
|