-
Notifications
You must be signed in to change notification settings - Fork 52
/
Copy pathExpr.py
1068 lines (898 loc) · 36.3 KB
/
Expr.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
WDL expressions composing literal values, arithmetic, comparison, conditionals,
string interpolation, arrays & maps, and function applications. These appear on
the right-hand side of value declarations and in task command substitutions,
task runtime sections, and workflow scatter and conditional sections.
The abstract syntax tree (AST) for any expression is represented by an instance
of a Python class deriving from ``WDL.Expr.Base``. Any such node may have other
nodes attached "beneath" it. An expression can be evaluated to a ``Value``
given a suitable ``WDL.Env.Bindings[Value.Base]``.
.. inheritance-diagram:: WDL.Expr
"""
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Tuple, Union, Iterable
from .Error import SourcePosition, SourceNode
from . import Type, Value, Env, Error, StdLib
class Base(SourceNode, ABC):
"""Superclass of all expression AST nodes"""
_type: Optional[Type.Base] = None
_check_quant: bool = True
_stdlib: "Optional[StdLib.Base]" = None
@property
def type(self) -> Type.Base:
"""
:type: WDL.Type.Base
WDL type of this expression. Undefined on construction; populated by one
invocation of ``infer_type``.
"""
# Failure of this assertion indicates use of an Expr object without
# first calling _infer_type
assert self._type is not None
return self._type
@abstractmethod
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
# Abstract protected method called by infer_type(): return the inferred
# type with no side-effects, obeying self._check_quant.
pass
def infer_type(
self,
type_env: Env.Bindings[Type.Base],
stdlib: "Optional[StdLib.Base]" = None,
check_quant: bool = True,
) -> "Base":
"""infer_type(self, type_env : Env.Bindings[Type.Base]) -> WDL.Expr.Base
Infer the expression's type within the given type environment. Must be
invoked exactly once prior to use of other methods.
:param stdlib: a context-specific standard function library for typechecking
:param check_quant: when ``False``, disables static validation of the optional (?) type quantifier when `typecheck()` is called on this expression, so for example type ``T?`` can satisfy an expected type ``T``. Applies recursively to the type inference and checking of any sub-expressions.
:raise WDL.Error.StaticTypeMismatch: when the expression fails to type-check
:return: `self`
"""
# Failure of this assertion indicates multiple invocations of
# infer_type
assert self._type is None
# recursive descent into child expressions
with Error.multi_context() as errors:
for child in self.children:
assert isinstance(child, Base)
errors.try1(lambda child=child: child.infer_type(type_env, stdlib, check_quant))
# invoke derived-class logic. we pass check_quant and stdlib hackily
# through instance variables since only some subclasses use them.
self._check_quant = check_quant
self._stdlib = stdlib
self._type = self._infer_type(type_env)
self._stdlib = None
assert self._type and isinstance(self.type, Type.Base)
return self
def typecheck(self, expected: Type.Base) -> "Base":
"""typecheck(self, expected : Type.Base) -> WDL.Expr.Base
Check that this expression's type is, or can be coerced to,
``expected``.
:raise WDL.Error.StaticTypeMismatch:
:return: `self`
"""
if not self.type.coerces(expected, self._check_quant):
raise Error.StaticTypeMismatch(self, expected, self.type)
return self
@abstractmethod
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
# to be overridden by subclasses. eval() calls this and deals with any
# exceptions raised
pass
def eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
"""
Evaluate the expression in the given environment
:param stdlib: a context-specific standard function library implementation
"""
try:
ans = self._eval(env, stdlib)
ans.expr = self
return ans
except Error.RuntimeError:
raise
except Exception as exn:
raise Error.EvalError(self, str(exn)) from exn
class Boolean(Base):
"""
Boolean literal
"""
value: bool
"""
:type: bool
Literal value
"""
def __init__(self, pos: SourcePosition, literal: bool) -> None:
super().__init__(pos)
self.value = literal
def __str__(self):
return str(self.value).lower()
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
return Type.Boolean()
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Boolean:
""
return Value.Boolean(self.value)
class Int(Base):
"""
Integer literal
"""
value: int
"""
:type: int
Literal value
"""
def __init__(self, pos: SourcePosition, literal: int) -> None:
super().__init__(pos)
self.value = literal
def __str__(self):
return str(self.value)
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
return Type.Int()
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Int:
""
return Value.Int(self.value)
# Float literal
class Float(Base):
"""
Numeric literal
"""
value: float
"""
:type: float
Literal value
"""
def __init__(self, pos: SourcePosition, literal: float) -> None:
super().__init__(pos)
self.value = literal
def __str__(self):
return str(self.value)
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
return Type.Float()
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Float:
""
return Value.Float(self.value)
class Placeholder(Base):
"""Holds an expression interpolated within a string or command"""
options: Dict[str, str]
"""
:type: Dict[str,str]
Placeholder options (sep, true, false, default)"""
expr: Base
"""
:type: WDL.Expr.Base
Expression to be evaluated and substituted
"""
def __init__(self, pos: SourcePosition, options: Dict[str, str], expr: Base) -> None:
super().__init__(pos)
self.options = options
self.expr = expr
def __str__(self):
options = []
for option in self.options:
options.append('{}="{}"'.format(option, self.options[option]))
options.append(str(self.expr))
return "~{{{}}}".format(" ".join(options))
@property
def children(self) -> Iterable[SourceNode]:
yield self.expr
def infer_type(
self,
type_env: Env.Bindings[Type.Base],
stdlib: "Optional[StdLib.Base]" = None,
check_quant: bool = True,
) -> Base:
# override the + operator with the within-interpolation version which accepts String?
# operands and produces a String? result
stdlib = stdlib or StdLib.Base()
setattr(stdlib, "_add", StdLib.InterpolationAddOperator())
return super().infer_type(type_env, stdlib, check_quant)
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
if isinstance(self.expr.type, Type.Array):
if "sep" not in self.options:
raise Error.StaticTypeMismatch(
self,
Type.Array(Type.Any()),
self.expr.type,
"array command placeholder must have 'sep'",
)
# if sum(1 for t in [Type.Int, Type.Float, Type.Boolean, Type.String, Type.File] if isinstance(self.expr.type.item_type, t)) == 0:
# raise Error.StaticTypeMismatch(self, Type.Array(Type.Any()), self.expr.type, "cannot use array of complex types for command placeholder")
elif "sep" in self.options:
raise Error.StaticTypeMismatch(
self,
Type.Array(Type.Any()),
self.expr.type,
"command placeholder has 'sep' option for non-Array expression",
)
if "true" in self.options or "false" in self.options:
if not isinstance(self.expr.type, Type.Boolean):
raise Error.StaticTypeMismatch(
self,
Type.Boolean(),
self.expr.type,
"command placeholder 'true' and 'false' options used with non-Boolean expression",
)
if not ("true" in self.options and "false" in self.options):
raise Error.StaticTypeMismatch(
self,
Type.Boolean(),
self.expr.type,
"command placeholder with only one of 'true' and 'false' options",
)
return Type.String()
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.String:
""
# override the + operator with the within-interpolation version which evaluates to None
# if either operand is None
stdlib = stdlib or StdLib.Base()
setattr(stdlib, "_add", StdLib.InterpolationAddOperator())
v = self.expr.eval(env, stdlib)
if isinstance(v, Value.Null):
if "default" in self.options:
return Value.String(self.options["default"])
return Value.String("")
if isinstance(v, Value.String):
return v
if isinstance(v, Value.Array):
return Value.String(self.options["sep"].join(str(item.value) for item in v.value))
if v == Value.Boolean(True) and "true" in self.options:
return Value.String(self.options["true"])
if v == Value.Boolean(False) and "false" in self.options:
return Value.String(self.options["false"])
return Value.String(str(v))
class String(Base):
"""String literal, possibly interleaved with expression placeholders for interpolation"""
parts: List[Union[str, Placeholder]]
"""
:type: List[Union[str,WDL.Expr.Placeholder]]
The parts list begins and ends with matching single- or double- quote
marks. Between these is a sequence of literal strings and/or
interleaved placeholder expressions. Escape sequences in the literals
have NOT been decoded.
"""
command: bool
"""
:type: bool
True if this expression is a task command template, as opposed to a string expression anywhere
else. Controls whether backslash escape sequences are evaluated or (for commands) passed
through for shell interpretation.
"""
def __init__(
self, pos: SourcePosition, parts: List[Union[str, Placeholder]], command: bool = False
) -> None:
super().__init__(pos)
self.parts = parts
self.command = command
def __str__(self):
parts = []
for part in self.parts:
if isinstance(part, Placeholder):
parts.append(str(part))
else:
parts.append(part)
return "".join(parts)
@property
def children(self) -> Iterable[SourceNode]:
for p in self.parts:
if isinstance(p, Base):
yield p
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
return Type.String()
def typecheck(self, expected: Optional[Type.Base]) -> Base:
""
return super().typecheck(expected) # pyre-ignore
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.String:
""
ans = []
for part in self.parts:
if isinstance(part, Placeholder):
# evaluate interpolated expression & stringify
ans.append(part.eval(env, stdlib).value)
elif isinstance(part, str):
if self.command:
ans.append(part)
else:
# use python builtins to decode escape sequences
ans.append(str.encode(part).decode("unicode_escape"))
else:
assert False
# concatenate the stringified parts and trim the surrounding quotes
return Value.String("".join(ans)[1:-1])
class Array(Base):
"""
Array literal
"""
items: List[Base]
"""
:type: List[WDL.Expr.Base]
Expression for each item in the array literal
"""
def __init__(self, pos: SourcePosition, items: List[Base]) -> None:
super(Array, self).__init__(pos)
self.items = items
def __str__(self):
items = []
for item in self.items:
items.append(str(item))
return "[{}]".format(", ".join(items))
@property
def children(self) -> Iterable[SourceNode]:
for it in self.items:
yield it
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
if not self.items:
return Type.Array(Type.Any())
# Start by assuming the type of the first item is the item type
item_type: Type.Base = self.items[0].type
# Allow a mixture of Int and Float to construct Array[Float]
if isinstance(item_type, Type.Int):
for item in self.items:
if isinstance(item.type, Type.Float):
item_type = Type.Float()
# If any item is String, assume item type is String
# If any item has optional quantifier, assume item type is optional
# If all items have nonempty quantifier, assume item type is nonempty
all_nonempty = len(self.items) > 0
all_stringifiable = True
for item in self.items:
if isinstance(item.type, Type.String):
item_type = Type.String(optional=item_type.optional)
if item.type.optional:
item_type = item_type.copy(optional=True)
if isinstance(item.type, Type.Array) and not item.type.nonempty:
all_nonempty = False
if not item.type.coerces(Type.String(optional=True)):
all_stringifiable = False
if isinstance(item_type, Type.Array):
item_type = item_type.copy(nonempty=all_nonempty)
# Check all items are coercible to item_type
for item in self.items:
try:
item.typecheck(item_type)
except Error.StaticTypeMismatch:
if all_stringifiable:
# Last resort: coerce all to strings if possible
return Type.Array(
Type.String(optional=item_type.optional), optional=False, nonempty=True
)
self._type = Type.Array(item_type, optional=False, nonempty=True)
raise Error.StaticTypeMismatch(
self, item_type, item.type, "(inconsistent types within array)"
) from None
return Type.Array(item_type, optional=False, nonempty=True)
def typecheck(self, expected: Optional[Type.Base]) -> Base:
""
if not self.items and isinstance(expected, Type.Array):
# the literal empty array satisfies any array type
return self
return super().typecheck(expected) # pyre-ignore
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Array:
""
assert isinstance(self.type, Type.Array)
return Value.Array(
self.type.item_type,
[item.eval(env, stdlib).coerce(self.type.item_type) for item in self.items],
)
class Pair(Base):
"""
Pair literal
"""
left: Base
"""
:type: WDL.Expr.Base
Left-hand expression in the pair literal
"""
right: Base
"""
:type: WDL.Expr.Base
Right-hand expression in the pair literal
"""
def __init__(self, pos: SourcePosition, left: Base, right: Base) -> None:
super().__init__(pos)
self.left = left
self.right = right
def __str__(self):
return "({}, {})".format(str(self.left), str(self.right))
@property
def children(self) -> Iterable[SourceNode]:
yield self.left
yield self.right
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
return Type.Pair(self.left.type, self.right.type)
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
""
assert isinstance(self.type, Type.Pair)
lv = self.left.eval(env, stdlib)
rv = self.right.eval(env, stdlib)
return Value.Pair(self.left.type, self.right.type, (lv, rv))
class Map(Base):
"""
Map literal
"""
items: List[Tuple[Base, Base]]
"""
:type: List[Tuple[WDL.Expr.Base,WDL.Expr.Base]]
Expressions for the map literal keys and values
"""
def __init__(self, pos: SourcePosition, items: List[Tuple[Base, Base]]) -> None:
super().__init__(pos)
self.items = items
def __str__(self):
items = []
for item in self.items:
items.append("{}: {}".format(str(item[0]), str(item[1])))
return "{{{}}}".format(", ".join(items))
@property
def children(self) -> Iterable[SourceNode]:
for k, v in self.items:
yield k
yield v
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
kty = None
vty = None
for k, v in self.items:
if kty is None:
kty = k.type
else:
k.typecheck(kty)
if (
vty is None
or vty == Type.Array(Type.Any())
or vty == Type.Map((Type.Any(), Type.Any()))
):
vty = v.type
else:
v.typecheck(vty)
if kty is None:
return Type.Map((Type.Any(), Type.Any()), literal_keys=set())
assert vty is not None
literal_keys = None
if kty == Type.String():
# If the keys are string constants, record them in the Type object
# for potential later use in struct coercion. (Normally the Type
# encodes the common type of the keys, but not the keys themselves)
literal_keys = set()
for k, _ in self.items:
if (
literal_keys is not None
and isinstance(k, String)
and len(k.parts) == 3
and isinstance(k.parts[1], str)
):
literal_keys.add(k.parts[1])
else:
literal_keys = None
return Type.Map((kty, vty), literal_keys=literal_keys)
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
""
assert isinstance(self.type, Type.Map)
eitems = []
for k, v in self.items:
eitems.append((k.eval(env, stdlib), v.eval(env, stdlib)))
# TODO: complain of duplicate keys
return Value.Map(self.type.item_type, eitems)
class Struct(Base):
"""
Struct literal
"""
members: Dict[str, Base]
"""
:type: Dict[str,WDL.Expr.Base]
The struct literal is modelled initially as a bag of keys and values, which
can be coerced to a specific struct type during typechecking.
"""
def __init__(self, pos: SourcePosition, members: List[Tuple[str, Base]]):
super().__init__(pos)
self.members = {}
for (k, v) in members:
if k in self.members:
raise Error.MultipleDefinitions(self.pos, "duplicate keys " + k)
self.members[k] = v
def __str__(self):
members = []
for member in self.members:
members.append('"{}": {}'.format(member, str(self.members[member])))
# Returns a Map literal instead of a struct literal as these are version dependant
return "{{{}}}".format(", ".join(members))
@property
def children(self) -> Iterable[SourceNode]:
return self.members.values()
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
member_types = {}
for k, v in self.members.items():
member_types[k] = v.type
return Type.Object(member_types)
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
ans = {}
for k, v in self.members.items():
ans[k] = v.eval(env, stdlib)
assert isinstance(self.type, Type.Object)
return Value.Struct(self.type, ans)
class IfThenElse(Base):
"""
Ternary conditional expression
"""
condition: Base
"""
:type: WDL.Expr.Base
A Boolean expression for the condition
"""
consequent: Base
"""
:type: WDL.Expr.Base
Expression evaluated when the condition is true
"""
alternative: Base
"""
:type: WDL.Expr.Base
Expression evaluated when the condition is false
"""
def __init__(
self, pos: SourcePosition, condition: Base, consequent: Base, alternative: Base
) -> None:
super().__init__(pos)
self.condition = condition
self.consequent = consequent
self.alternative = alternative
def __str__(self):
return "if {} then {} else {}".format(
str(self.condition), str(self.consequent), str(self.alternative)
)
@property
def children(self) -> Iterable[SourceNode]:
yield self.condition
yield self.consequent
yield self.alternative
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
# check for Boolean condition
if self.condition.type != Type.Boolean():
raise Error.StaticTypeMismatch(
self, Type.Boolean(), self.condition.type, "in if condition"
)
# Unify consequent & alternative types. Subtleties:
# 1. If either is optional, unify to optional
# 2. If one is Int and the other is Float, unify to Float
# 3. If one is a nonempty array and the other is a possibly empty
# array, unify to possibly empty array
self_type = self.consequent.type
assert isinstance(self_type, Type.Base)
if isinstance(self_type, Type.Int) and isinstance(self.alternative.type, Type.Float):
self_type = Type.Float(optional=self_type.optional)
if self.alternative.type.optional:
self_type = self_type.copy(optional=True)
if (
isinstance(self_type, Type.Array)
and isinstance(self.consequent.type, Type.Array)
and isinstance(self.alternative.type, Type.Array)
):
self_type = self_type.copy(
nonempty=(self.consequent.type.nonempty and self.alternative.type.nonempty)
)
try:
self.consequent.typecheck(self_type)
self.alternative.typecheck(self_type)
except Error.StaticTypeMismatch:
raise Error.StaticTypeMismatch(
self,
self.consequent.type,
self.alternative.type,
" (if consequent & alternative must have the same type)",
) from None
return self_type
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
""
if self.condition.eval(env, stdlib).expect(Type.Boolean()).value:
ans = self.consequent.eval(env, stdlib)
else:
ans = self.alternative.eval(env, stdlib)
return ans
class Ident(Base):
"""
An identifier referencing a named value or call output.
``Ident`` nodes are wrapped in ``Get`` nodes, as discussed below.
"""
name: str
""":type: str
Name, possibly including a dot-separated namespace
"""
referee: "Union[None, WDL.Tree.Decl, WDL.Tree.Call, WDL.Tree.Scatter, WDL.Tree.Gather]"
"""
After typechecking within a task or workflow, stores the AST node to which the identifier
refers: a ``WDL.Tree.Decl`` for value references; a ``WDL.Tree.Call`` for call outputs; a
``WDL.Tree.Scatter`` for scatter variables; or a ``WDL.Tree.Gather`` object representing a
value or call output that resides within a scatter or conditional section.
"""
def __init__(self, pos: SourcePosition, name: str) -> None:
super().__init__(pos)
assert name and not name.endswith(".") and not name.startswith(".") and ".." not in name
self.name = name
self.referee = None
def __str__(self):
return self.name
@property
def children(self) -> Iterable[SourceNode]:
return []
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
# The following Env.resolve will never fail, as Get._infer_type does
# the heavy lifting for us.
b = type_env.resolve_binding(self.name)
ans = b.value
# referee comes from the type environment's info value
referee = b.info
if referee:
assert referee.__class__.__name__ in [
"Decl",
"Call",
"Scatter",
"Gather",
], referee.__class__.__name__
self.referee = referee
return ans
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
""
return env[self.name]
@property
def _ident(self) -> str:
return self.name
class _LeftName(Base):
# This AST node is a placeholder involved in disambiguating dot-separated
# identifiers (e.g. "leftname.midname.rightname") as elaborated in the Get
# docstring below. The parser, lacking the context to resolve this syntax,
# creates this node simply to represent the leftmost (sometimes only) name,
# as the innard of a Get node, potentially (not necessarily) with a
# member name. Later during typechecking, Get._infer_type folds _LeftName
# into an `Ident` expression; the library user should never have to work
# with _LeftName.
name: str
def __init__(self, pos: SourcePosition, name: str) -> None:
super().__init__(pos)
assert name
self.name = name
def __str__(self):
return self.name
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
raise NotImplementedError()
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
raise NotImplementedError()
@property
def _ident(self) -> str:
return self.name
class Get(Base):
"""
AST node representing access to a value by identifier (including namespaced
ones), or accessing a member of a pair or struct as ``.member``.
The entaglement of these two cases is inherent in WDL. Consider the syntax
``leftname.midname.rightname``. One interpretation is that ``leftname`` is
an identifier for a struct value, and ``.midname.rightname`` represents a
chain of struct member accesses. But another possibility is that
``leftname`` is a call, ``midname`` is a struct output of that call, and
``rightname`` is a member of that struct. These cases can't be
distinguished by the syntax parser alone, but must be resolved during
typechecking with reference to the calls and identifiers available in the
environment.
The typechecker does conveniently resolve such cases, and to minimize the
extent to which it has to restructure the AST in doing so, all identifiers
(with or without a namespace) are represented as a ``Get`` node wrapping an
``Ident`` node. The ``Get`` node may specify a member name to access, but
may not if the identifier is to be accessed directly. On the other hand,
the expression inside a ``Get`` node need not be a simple identifier, e.g.
``arr[1].memb.left`` is be represented as:
``Get(Get(Apply("_at", Get(Ident("arr")), 1),"memb"),"left")``
"""
expr: Base
"""
:type: WDL.Expr.Base
The expression whose value is accessed
"""
member: Optional[str]
"""
:type: Optional[str]
If the expression is accessing a pair/struct member, then ``expr.type`` is
``WDL.Type.Pair`` or ``WDL.Type.StructInstance`` and this field gives the
desired member name (``left`` or ``right`` for pairs).
Otherwise the expression accesses ``expr`` directly, and ``member`` is
``None``.
"""
def __init__(self, pos: SourcePosition, expr: Base, member: Optional[str]) -> None:
super().__init__(pos)
assert expr
self.expr = expr
self.member = member
def __str__(self):
if self.member is not None:
return "{}.{}".format(str(self.expr), self.member)
return str(self.expr)
@property
def children(self) -> Iterable[SourceNode]:
if self._type:
# suppress children until resolution/typechecking is complete
yield self.expr
def _infer_type(self, type_env: Env.Bindings[Type.Base]) -> Type.Base:
if isinstance(self.expr, _LeftName):
# expr is a lone "name" -- try to resolve it as an identifier,
# and if that works, transform it to Ident("name")
if self.expr.name in type_env:
self.expr = Ident(self.expr.pos, self.expr.name)
elif not self.member:
raise Error.UnknownIdentifier(self)
# attempt to typecheck expr, disambiguating whether it's an
# intermediate value, a resolvable identifier, or neither
try:
self.expr.infer_type(type_env, self._stdlib, self._check_quant)
except Error.UnknownIdentifier:
# Fail...there's one case we may be able to rescue, where expr is a
# _LeftName inside zero or more Gets representing an incomplete
# namespaced identifier, and our member completes the path to an
# available named value.
if not (isinstance(self.expr, (_LeftName, Get)) and self.expr._ident and self.member):
raise
# attempt to resolve "expr.member" and if that works, transform
# expr to Ident("expr.member")
if self.expr._ident + "." + self.member not in type_env:
raise Error.UnknownIdentifier(self) from None
self.expr = Ident(self.pos, self._ident)
self.expr.infer_type(type_env, self._stdlib, self._check_quant)
self.member = None
# now we've typechecked expr
ety = self.expr.type
assert ety
if not self.member:
# no member to access; just propagate expr type
assert isinstance(self.expr, Ident)
return ety
# now we expect expr to be a pair or struct, whose member we're
# accessing
if not isinstance(ety, (Type.Pair, Type.StructInstance)):
raise Error.NoSuchMember(self, self.member)
if self._check_quant and ety.optional:
raise Error.StaticTypeMismatch(self.expr, ety.copy(optional=False), ety)
if self.member in ["left", "right"]:
if isinstance(ety, Type.Pair):
return ety.left_type if self.member == "left" else ety.right_type
raise Error.NoSuchMember(self, self.member)
if isinstance(ety, Type.StructInstance):
try:
assert ety.members is not None
return ety.members[self.member]
except KeyError:
pass
raise Error.NoSuchMember(self, self.member)
def _eval(
self, env: Env.Bindings[Value.Base], stdlib: "Optional[StdLib.Base]" = None
) -> Value.Base:
innard_value = self.expr.eval(env, stdlib)
if not self.member:
return innard_value
if isinstance(innard_value, Value.Pair):
assert self.member in ["left", "right"]
return innard_value.value[0 if self.member == "left" else 1]
if isinstance(innard_value, Value.Struct):
return innard_value.value[self.member]
raise NotImplementedError()
@property
def _ident(self) -> str:
# helper for the resolution logic above -- get the partial identifier
# recursing into nested Gets, if there's a _LeftName at the bottom.
if isinstance(self.expr, (_LeftName, Get)) and self.expr._ident:
return self.expr._ident + (("." + self.member) if self.member else "")
return ""
_base_stdlib = None # memorized instance of the default WDL.StdLib.Base()
def _add_parentheses(arguments, parent_operator):
"""
Add parentheses around arguments if necessary.
Adds parentheses around if-then-else clauses if on the left side of the
parent operator (otherwise it is ambiguous whether 'if true
then 1 else 100 + 1' should return 1 or 2).
Adds parentheses around expression with a lower precedence than the parent operator
"""
arguments_out = []
precedence = {
"_mul": 7,
"_div": 7,
"_rem": 7,
"_add": 6,
"_sub": 6,
"_lt": 5,
"_lte": 5,
"_gt": 5,
"_gte": 5,
"_eqeq": 4,
"_neq": 4,
"_land": 3,
"_lor": 3,
}
for i, argument in enumerate(arguments):
if isinstance(argument, IfThenElse) and (parent_operator in precedence and i == 0):
arguments_out.append("({})".format(str(argument)))
elif isinstance(argument, Apply):
if precedence.get(parent_operator, 100) > precedence.get(argument.function_name, 100):
arguments_out.append("({})".format(str(argument)))
else:
arguments_out.append(str(argument))
else:
arguments_out.append(str(argument))
return arguments_out