From cdefab50ef958f2c44a7a5b457f6a1f89931f588 Mon Sep 17 00:00:00 2001 From: Mike Lin Date: Sat, 27 Oct 2018 22:53:04 -0700 Subject: [PATCH 1/5] Map, Pair wip --- WDL/Expr.py | 27 ++++++++++++++++++--- WDL/StdLib.py | 33 ++++++++++++++++++++++--- WDL/Type.py | 54 ++++++++++++++++++++++++++++++++++------- WDL/Value.py | 20 ++++++++++++--- WDL/__init__.py | 20 +++++++++++++++ WDL/_parser.py | 8 ++++-- tests/test_HCAskylab.py | 2 +- tests/test_doc.py | 3 +++ tests/test_eval.py | 4 ++- 9 files changed, 149 insertions(+), 22 deletions(-) diff --git a/WDL/Expr.py b/WDL/Expr.py index 882609d5..2ea3bd27 100644 --- a/WDL/Expr.py +++ b/WDL/Expr.py @@ -125,8 +125,8 @@ def _infer_type(self, type_env : Env.Types) -> T.Base: if isinstance(self.expr.type, T.Array): if 'sep' not in self.options: raise Error.StaticTypeMismatch(self, T.Array(None), self.expr.type, "array command placeholder must have 'sep'") - if sum(1 for t in [T.Int, T.Float, T.Boolean, T.String, T.File] if isinstance(self.expr.type.item_type, t)) == 0: - raise Error.StaticTypeMismatch(self, T.Array(None), self.expr.type, "cannot use array of complex types for command placeholder") + #if sum(1 for t in [T.Int, T.Float, T.Boolean, T.String, T.File] if isinstance(self.expr.type.item_type, t)) == 0: + # raise Error.StaticTypeMismatch(self, T.Array(None), self.expr.type, "cannot use array of complex types for command placeholder") elif 'sep' in self.options: raise Error.StaticTypeMismatch(self, T.Array(None), self.expr.type, "command placeholder has 'sep' option for non-Array expression") if ('true' in self.options or 'false' in self.options): @@ -144,7 +144,7 @@ def eval(self, env : Env.Values) -> V.String: if isinstance(v, V.String): return v if isinstance(v, V.Array): - return V.String(self.options['sep'].join(str(item.value) for item in v.value)) # pyre-ignore + return V.String(self.options['sep'].join(str(item.value) for item in v.value)) if v == V.Boolean(True) and 'true' in self.options: return V.String(self.options['true']) if v == V.Boolean(False) and 'false' in self.options: @@ -321,6 +321,18 @@ def __init__(self, pos : SourcePosition, parts : List[str]) -> None: self.namespace = parts[:-1] def _infer_type(self, type_env : Env.Types) -> T.Base: + if len(self.namespace) > 0 and (self.name in ['left', 'right']): + # TODO: this only works for an identifier that resolves to a pair, + # not any syntactic pair. .left and .right should be treated as + # postfix function applications. + pair_name = self.namespace[-1] + pair_namespace = self.namespace[:-1] + try: + ans : T.Base = Env.resolve(type_env, pair_namespace, pair_name) + except KeyError: + pass + if isinstance(ans, T.Pair): + return ans.left_type if self.name == 'left' else ans.right_type try: ans : T.Base = Env.resolve(type_env, self.namespace, self.name) return ans @@ -328,6 +340,15 @@ def _infer_type(self, type_env : Env.Types) -> T.Base: raise Error.UnknownIdentifier(self) from None def eval(self, env : Env.Values) -> V.Base: + if len(self.namespace) > 0 and (self.name in ['left', 'right']): + pair_name = self.namespace[-1] + pair_namespace = self.namespace[:-1] + try: + + ans : V.Base = Env.resolve(env, pair_namespace, pair_name) + return ans + except KeyError: + pass try: ans : V.Base = Env.resolve(env, self.namespace, self.name) return ans diff --git a/WDL/StdLib.py b/WDL/StdLib.py index c3cb0a46..7506944f 100644 --- a/WDL/StdLib.py +++ b/WDL/StdLib.py @@ -73,7 +73,15 @@ def __call__(self, expr : E.Apply, env : E.Env) -> V.Base: ("ceil", [T.Float()], T.Int(), lambda x: exec('raise NotImplementedError()')), ("glob", [T.String()], T.Array(T.File()), lambda pattern: exec('raise NotImplementedError()')), ("read_int", [T.String()], T.Int(), lambda pattern: exec('raise NotImplementedError()')), - ("range", [T.Int()], T.Array(T.Int()), lambda high: exec('raise NotImplementedError()')) + ("read_boolean", [T.String()], T.Boolean(), lambda pattern: exec('raise NotImplementedError()')), + ("read_string", [T.String()], T.String(), lambda pattern: exec('raise NotImplementedError()')), + ("read_float", [T.String()], T.Float(), lambda pattern: exec('raise NotImplementedError()')), + ("read_array", [T.String()], T.Array(None), lambda pattern: exec('raise NotImplementedError()')), + ("read_map", [T.String()], T.Map(None), lambda pattern: exec('raise NotImplementedError()')), + ("read_lines", [T.String()], T.Array(None), lambda pattern: exec('raise NotImplementedError()')), + ("read_tsv", [T.String()], T.Array(T.Array(T.String())), lambda pattern: exec('raise NotImplementedError()')), + ("write_map", [T.Map(None)], T.String(), lambda pattern: exec('raise NotImplementedError()')), + ("range", [T.Int()], T.Array(T.Int()), lambda high: exec('raise NotImplementedError()')), ] for name, argument_types, return_type, F in _static_functions: E._stdlib[name] = _StaticFunction(name, argument_types, return_type, F) @@ -212,9 +220,9 @@ def infer_type(self, expr : E.Apply) -> T.Base: if len(expr.arguments) != 1: raise Error.WrongArity(expr, 1) if not isinstance(expr.arguments[0].type, T.Array): - raise Error.StaticTypeMismatch(expr, T.Array(None), expr.arguments[0].type) + raise Error.StaticTypeMismatch(expr.arguments[0], T.Array(None), expr.arguments[0].type) if expr.arguments[0].type.item_type is None: - raise Error.EmptyArray(expr.arguments[0]) + raise Error.EmptyArray(expr.arguments[0]) # TODO: error for 'indeterminate type' ty = copy.copy(expr.arguments[0].type.item_type) assert isinstance(ty, T.Base) ty.optional = False @@ -223,3 +231,22 @@ def infer_type(self, expr : E.Apply) -> T.Base: def __call__(self, expr : E.Apply, env : Env.Values) -> V.Base: raise NotImplementedError() E._stdlib["select_first"] = _SelectFirst() + +class _Zip(E._Function): + # 'a array -> 'b array -> ('a,'b) array + def infer_type(self, expr : E.Apply) -> T.Base: + if len(expr.arguments) != 2: + raise Error.WrongArity(expr, 2) + if not isinstance(expr.arguments[0].type, T.Array): + raise Error.StaticTypeMismatch(expr.arguments[0], T.Array(None), expr.arguments[0].type) + if expr.arguments[0].type.item_type is None: + raise Error.EmptyArray(expr.arguments[0]) # TODO: error for 'indeterminate type' + if not isinstance(expr.arguments[1].type, T.Array): + raise Error.StaticTypeMismatch(expr.arguments[1], T.Array(None), expr.arguments[0].type) + if expr.arguments[1].type.item_type is None: + raise Error.EmptyArray(expr.arguments[1]) # TODO: error for 'indeterminate type' + return T.Array(T.Pair(expr.arguments[0].type.item_type, expr.arguments[1].type.item_type)) + + def __call__(self, expr : E.Apply, env : Env.Values) -> V.Base: + raise NotImplementedError() +E._stdlib["zip"] = _Zip() diff --git a/WDL/Type.py b/WDL/Type.py index c2b4ff9f..241af717 100644 --- a/WDL/Type.py +++ b/WDL/Type.py @@ -23,7 +23,7 @@ ``WDL.Type.Array(WDL.Type.String()) != WDL.Type.Array(WDL.Type.Float())``. """ from abc import ABC, abstractmethod -from typing import Optional, TypeVar +from typing import Optional, TypeVar, Tuple TVBase = TypeVar("TVBase", bound="Base") class Base(ABC): @@ -91,10 +91,10 @@ class Array(Base): """ Array type, parameterized by the type of the constituent items. - ``item_type`` may be None to represent the type of the literal empty array - ``[]``, which is considered compatible with any array type (lacking the - nonempty quantifier). This special case should be considered explicitly - when comparing array types. + ``item_type`` may be None to represent an array whose item type isn't + known statically, such as a literal empty array ``[]``, or the result + of the ``read_array()`` standard library function. This is considered + statically coercible to any array type (but may fail at runtime) """ item_type : Optional[Base] nonempty : bool @@ -109,11 +109,47 @@ def __str__(self) -> str: ans = "Array[" + (str(self.item_type) if self.item_type is not None else "") + "]" return ans def coerces(self, rhs : Base) -> bool: - if self.item_type is None and isinstance(rhs, Array) and not rhs.nonempty: - return True if isinstance(rhs, Array): - if self.item_type is None: - return (not rhs.nonempty) + if self.item_type is None or rhs.item_type is None: + return True else: return self.item_type.coerces(rhs.item_type) return super().coerces(rhs) + +class Map(Base): + """ + Map type, parameterized by the (key,value) item type. + + ``item_type`` may be None to represent a map whose type isn't known + statically, such as a literal empty may ``{}``, or the result of the + ``read_map()`` standard library function. This is considered statically + coercible to any map type (but may fail at runtime) + """ + item_type : Optional[Tuple[Base,Base]] + + def __init__(self, item_type : Optional[Tuple[Base,Base]], optional : bool = False) -> None: + self.optional = optional + self.item_type = item_type + def __str__(self) -> str: + return "Map[" + (str(self.item_type[0]) + "," + str(self.item_type[1]) if self.item_type is not None else "") + "]" # pyre-fixme + def coerces(self, rhs : Base) -> bool: + if isinstance(rhs, Map): + if self.item_type is None or rhs.item_type is None: + return True + else: + return self.item_type[0].coerces(rhs.item_type[0]) and self.item_type[1].coerces(rhs.item_type[1]) # pyre-fixme + return super().coerces(rhs) + +class Pair(Base): + """ + Pair type, parameterized by the left and right item types. + """ + left_type : Base + right_type : Base + + def __init__(self, left_type : Base, right_type : Base, optional : bool = False) -> None: + self.optional = optional + self.left_type = left_type + self.right_type = right_type + def __str__(self) -> str: + return "Pair[" + (str(self.left_type) + "," + str(self.right_type)) + "]" diff --git a/WDL/Value.py b/WDL/Value.py index c7d2948d..b735afa6 100644 --- a/WDL/Value.py +++ b/WDL/Value.py @@ -6,7 +6,7 @@ ``WDL.Value.Base``. """ from abc import ABC, abstractmethod -from typing import Any, List, Optional, TypeVar +from typing import Any, List, Optional, TypeVar, Tuple import WDL.Type as T import json @@ -75,12 +75,26 @@ def __str__(self) -> str: class Array(Base): """``value`` is a Python ``list`` of other ``WDL.Value`` instances""" - value : List[Any] = [] - def __init__(self, type : T.Array, value : List[Any]) -> None: + value : List[Base] = [] + def __init__(self, type : T.Array, value : List[Base]) -> None: super().__init__(type, value) def __str__(self) -> str: return "[" + ", ".join([str(item) for item in self.value]) + "]" +class Map(Base): + value : List[Tuple[Base,Base]] = [] + def __init__(self, type : T.Map, value : List[Tuple[Base,Base]]) -> None: + super().__init__(type, value) + def __str__(self) -> str: + raise NotImplementedError() # TODO + +class Pair(Base): + value : Optional[Tuple[Base,Base]] = None + def __init__(self, type : T.Map, value : Tuple[Base,Base]) -> None: + super().__init__(type, value) + def __str__(self) -> str: + raise NotImplementedError() # TODO + class Null(Base): """Represents the missing value which optional inputs may take. ``type`` and ``value`` are both None.""" type : Optional[Any] # pyre-ignore diff --git a/WDL/__init__.py b/WDL/__init__.py index 9f1fe12f..1c2c63fa 100644 --- a/WDL/__init__.py +++ b/WDL/__init__.py @@ -124,6 +124,24 @@ def array_type(self, items, meta): if items[1].value == "+": nonempty = True return T.Array(items[0], optional, nonempty) + def map_type(self, items, meta): + assert len(items) >= 2 + assert isinstance(items[0], WDL.Type.Base) + assert isinstance(items[1], WDL.Type.Base) + optional = False + if len(items) > 2: + if items[2].value == "?": + optional = True + return T.Map((items[0], items[1]), optional) + def pair_type(self, items, meta): + assert len(items) >= 2 + assert isinstance(items[0], WDL.Type.Base) + assert isinstance(items[1], WDL.Type.Base) + optional = False + if len(items) > 2: + if items[2].value == "?": + optional = True + return T.Pair(items[0], items[1], optional) class _DocTransformer(_ExprTransformer, _TypeTransformer): def __init__(self, file : str) -> None: @@ -303,6 +321,8 @@ def parse_document(txt : str, uri : str = '') -> D.Document: return _DocTransformer(uri).transform(WDL._parser.parse(txt, "document")) except lark.exceptions.UnexpectedCharacters as exn: raise Err.ParserError(uri if uri != '' else '(in buffer)') from exn + except lark.exceptions.UnexpectedToken as exn: + raise Err.ParserError(uri if uri != '' else '(in buffer)') from exn def load(uri : str, path : List[str] = []) -> D.Document: """ diff --git a/WDL/_parser.py b/WDL/_parser.py index 8e4bf9c7..601d1d1e 100644 --- a/WDL/_parser.py +++ b/WDL/_parser.py @@ -55,13 +55,13 @@ | SIGNED_FLOAT -> float // string (single-quoted) -STRING1_CHAR: "\\'" | /[^'$]/ | /\$[^{]/ +STRING1_CHAR: "\\'" | /[^'$]/ | /\$[^{']/ STRING1_END: STRING1_CHAR* "$"? "'" STRING1_FRAGMENT: STRING1_CHAR* "${" string1: /'/ [(STRING1_FRAGMENT expr "}")*] STRING1_END -> string // string (double-quoted) -STRING2_CHAR: "\\\"" | /[^"$]/ | /\$[^{]/ +STRING2_CHAR: "\\\"" | /[^"$]/ | /\$[^{"]/ STRING2_END: STRING2_CHAR* "$"? /"/ STRING2_FRAGMENT: STRING2_CHAR* "${" string2: /"/ [(STRING2_FRAGMENT expr "}")*] STRING2_END -> string @@ -81,12 +81,16 @@ | _STRING QUANT? -> string_type | _FILE QUANT? -> file_type | _ARRAY "[" type "]" ARRAY_QUANT? -> array_type + | _MAP "[" type "," type "]" QUANT? -> map_type + | _PAIR "[" type "," type "]" QUANT? -> pair_type _INT.2: "Int" // .2 ensures higher priority than CNAME _FLOAT.2: "Float" _BOOLEAN.2: "Boolean" _STRING.2: "String" _FILE.2: "File" _ARRAY.2: "Array" +_MAP.2: "Map" +_PAIR.2: "Pair" QUANT: "?" ARRAY_QUANT: "?" | "+" diff --git a/tests/test_HCAskylab.py b/tests/test_HCAskylab.py index 58d912c9..0659e74d 100644 --- a/tests/test_HCAskylab.py +++ b/tests/test_HCAskylab.py @@ -21,7 +21,7 @@ def t(self, fn=fn): for fn in workflow_files: name = os.path.split(fn)[1] name = name[:-4] - if name not in ['count','make_fastq']: + if name not in []: name = 'test_HCAskylab_workflow_' + name.replace('.', '_') def t(self, fn=fn): WDL.load(fn, path=[glob.glob(os.path.join(tdn, 'skylab-*', 'library', 'tasks'))[0]]) diff --git a/tests/test_doc.py b/tests/test_doc.py index 3393bb24..13065034 100644 --- a/tests/test_doc.py +++ b/tests/test_doc.py @@ -224,6 +224,9 @@ def test_meta(self): Boolean? b Array[Int]+ n } + String dollar = "$" + String lbrace = "{" + String rbrace = "}" parameter_meta { b: { help: "it's a boolean" } n: 'x' diff --git a/tests/test_eval.py b/tests/test_eval.py index e92fe54d..d2ec700a 100644 --- a/tests/test_eval.py +++ b/tests/test_eval.py @@ -121,7 +121,9 @@ def test_str(self): ('"foo" + "bar"', '"foobar"'), ('"foo" + 1', '"foo1"'), ('2.0 + "bar"', '"2.0bar"'), - (""" 'foo' + "bar" """, '"foobar"')) + (""" 'foo' + "bar" """, '"foobar"'), + ('"{"', '"{"', WDL.Type.String()), + ('"$" + "$"', '"$$"', WDL.Type.String())) self._test_tuples( (r'''"CNN is working frantically to find their \"source.\""''', r'''"CNN is working frantically to find their \"source.\""'''), From 82b10105c29f334728e2613714a3eaefc82d3364 Mon Sep 17 00:00:00 2001 From: Mike Lin Date: Sat, 27 Oct 2018 23:22:04 -0700 Subject: [PATCH 2/5] Pair get path --- WDL/Error.py | 4 ++++ WDL/Expr.py | 6 +++--- WDL/StdLib.py | 20 ++++++++++++++++++++ WDL/__init__.py | 5 +++++ WDL/_parser.py | 5 +++++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/WDL/Error.py b/WDL/Error.py index 847f6b9f..5bb2fc4f 100644 --- a/WDL/Error.py +++ b/WDL/Error.py @@ -41,6 +41,10 @@ class NotAnArray(Base): def __init__(self, node : SourceNode) -> None: super().__init__(node, "Not an array") +class NotAPair(Base): + def __init__(self, node : SourceNode) -> None: + super().__init__(node, "Not a pair (taking left or right)") + class StaticTypeMismatch(Base): def __init__(self, node : SourceNode, expected : T.Base, actual : T.Base, message : Optional[str] = None) -> None: msg = "Expected {} instead of {}".format(str(expected), str(actual)) diff --git a/WDL/Expr.py b/WDL/Expr.py index 2ea3bd27..a9798d1c 100644 --- a/WDL/Expr.py +++ b/WDL/Expr.py @@ -322,9 +322,9 @@ def __init__(self, pos : SourcePosition, parts : List[str]) -> None: def _infer_type(self, type_env : Env.Types) -> T.Base: if len(self.namespace) > 0 and (self.name in ['left', 'right']): - # TODO: this only works for an identifier that resolves to a pair, - # not any syntactic pair. .left and .right should be treated as - # postfix function applications. + # Handle pair access IDENT.left or IDENT.right + # Pair access through non-identifier expressions goes a different + # path, through the get_left and get_right terminals. pair_name = self.namespace[-1] pair_namespace = self.namespace[:-1] try: diff --git a/WDL/StdLib.py b/WDL/StdLib.py index 7506944f..06df364d 100644 --- a/WDL/StdLib.py +++ b/WDL/StdLib.py @@ -33,6 +33,26 @@ def __call__(self, expr : E.Apply, env : E.Env) -> V.Base: return arr.value[idx] # pyre-ignore E._stdlib["_get"] = _ArrayGet() +# Pair get (EXPR.left/EXPR.right) +# The special case where EXPR is an identifier goes a different path, through +# Expr.Ident. +class _PairGet(E._Function): + left : bool + def __init__(self, left : bool) -> None: + self.left = left + def infer_type(self, expr : E.Apply) -> T.Base: + assert len(expr.arguments) == 1 + if not isinstance(expr.arguments[0].type, T.Pair): + raise Error.NotAPair(expr.arguments[0]) + return expr.arguments[0].type.left_type if self.left else expr.arguments[0].type.right_type + def __call__(self, expr : E.Apply, env : E.Env) -> V.Base: + assert len(expr.arguments) == 1 + pair = expr.arguments[0].eval(env) + assert isinstance(pair.type, T.Pair) + assert isinstance(pair.value, tuple) + return pair.value[0] if self.left else pair.value[1] +E._stdlib["_get_left"] = _PairGet(True) +E._stdlib["_get_right"] = _PairGet(False) # _Function helper for simple functions with fixed argument and return types class _StaticFunction(E._Function): diff --git a/WDL/__init__.py b/WDL/__init__.py index 1c2c63fa..696eddde 100644 --- a/WDL/__init__.py +++ b/WDL/__init__.py @@ -69,6 +69,11 @@ def negate(self, items, meta) -> E.Base: def get(self, items, meta) -> E.Base: return E.Apply(sp(self.filename, meta), "_get", items) + def get_left(self, items, meta) -> E.Base: + return E.Apply(sp(self.filename, meta), "_get_left", items) + def get_right(self, items, meta) -> E.Base: + return E.Apply(sp(self.filename, meta), "_get_right", items) + def ifthenelse(self, items, meta) -> E.Base: assert len(items) == 3 return E.IfThenElse(sp(self.filename, meta), *items) diff --git a/WDL/_parser.py b/WDL/_parser.py index 601d1d1e..3f596a63 100644 --- a/WDL/_parser.py +++ b/WDL/_parser.py @@ -42,6 +42,9 @@ | "[" [expr ("," expr)*] "]" -> array | expr_core "[" expr "]" -> get + | expr_core "." _LEFT -> get_left + | expr_core "." _RIGHT -> get_right + | "if" expr "then" expr "else" expr -> ifthenelse | ident @@ -72,6 +75,8 @@ ESCAPED_STRING1: "'" STRING_INNER1* "'" string_literal: ESCAPED_STRING | ESCAPED_STRING1 +_LEFT.2: "left" +_RIGHT.2: "right" ident: [CNAME ("." CNAME)*] // WDL types and declarations From cad2ff713270c89e9d986fcfcb05a8a14a1bf8fe Mon Sep 17 00:00:00 2001 From: Mike Lin Date: Sun, 28 Oct 2018 22:38:10 -0700 Subject: [PATCH 3/5] cleanup --- WDL/Expr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WDL/Expr.py b/WDL/Expr.py index a9798d1c..d4be4a2c 100644 --- a/WDL/Expr.py +++ b/WDL/Expr.py @@ -322,9 +322,11 @@ def __init__(self, pos : SourcePosition, parts : List[str]) -> None: def _infer_type(self, type_env : Env.Types) -> T.Base: if len(self.namespace) > 0 and (self.name in ['left', 'right']): - # Handle pair access IDENT.left or IDENT.right + # Special case for pair access, IDENT.left or IDENT.right # Pair access through non-identifier expressions goes a different # path, through the get_left and get_right terminals. + # TODO: avoid having two paths by ensuring .left and .right can't + # parse as Ident pair_name = self.namespace[-1] pair_namespace = self.namespace[:-1] try: @@ -344,7 +346,6 @@ def eval(self, env : Env.Values) -> V.Base: pair_name = self.namespace[-1] pair_namespace = self.namespace[:-1] try: - ans : V.Base = Env.resolve(env, pair_namespace, pair_name) return ans except KeyError: From dc6f8646c412022d5fbf354b14e42200fb92c689 Mon Sep 17 00:00:00 2001 From: Mike Lin Date: Mon, 29 Oct 2018 09:50:42 -0700 Subject: [PATCH 4/5] cleanup --- tests/test_HCAskylab.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/test_HCAskylab.py b/tests/test_HCAskylab.py index 0659e74d..e7384da1 100644 --- a/tests/test_HCAskylab.py +++ b/tests/test_HCAskylab.py @@ -21,8 +21,7 @@ def t(self, fn=fn): for fn in workflow_files: name = os.path.split(fn)[1] name = name[:-4] - if name not in []: - name = 'test_HCAskylab_workflow_' + name.replace('.', '_') - def t(self, fn=fn): - WDL.load(fn, path=[glob.glob(os.path.join(tdn, 'skylab-*', 'library', 'tasks'))[0]]) - setattr(TestHCAskylab, name, t) + name = 'test_HCAskylab_workflow_' + name.replace('.', '_') + def t(self, fn=fn): + WDL.load(fn, path=[glob.glob(os.path.join(tdn, 'skylab-*', 'library', 'tasks'))[0]]) + setattr(TestHCAskylab, name, t) From 291696d91ae789f6b54904c5efd780c0b4a062c9 Mon Sep 17 00:00:00 2001 From: Mike Lin Date: Tue, 30 Oct 2018 10:36:36 -0700 Subject: [PATCH 5/5] Pair literals and tests --- WDL/Expr.py | 26 +++++++++++++++++++++++++- WDL/Value.py | 5 +++-- WDL/__init__.py | 3 +++ WDL/_parser.py | 1 + tests/test_eval.py | 16 +++++++++++++++- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/WDL/Expr.py b/WDL/Expr.py index d4be4a2c..e43dfdc8 100644 --- a/WDL/Expr.py +++ b/WDL/Expr.py @@ -347,7 +347,9 @@ def eval(self, env : Env.Values) -> V.Base: pair_namespace = self.namespace[:-1] try: ans : V.Base = Env.resolve(env, pair_namespace, pair_name) - return ans + if isinstance(ans, V.Pair): + assert ans.value is not None + return ans.value[0] if self.name == 'left' else ans.value[1] except KeyError: pass try: @@ -355,3 +357,25 @@ def eval(self, env : Env.Values) -> V.Base: return ans except KeyError: raise Error.UnknownIdentifier(self) from None + +# Pair literal + +class Pair(Base): + left : Base + right : Base + + def __init__(self, pos : SourcePosition, left : Base, right : Base) -> None: + super().__init__(pos) + self.left = left + self.right = right + + def _infer_type(self, type_env : Env.Types) -> T.Base: + self.left.infer_type(type_env) + self.right.infer_type(type_env) + return T.Pair(self.left.type, self.right.type) + + def eval(self, env : Env.Values) -> V.Base: + assert isinstance(self.type, T.Pair) + lv = self.left.eval(env) + rv = self.right.eval(env) + return V.Pair(self.type, (lv,rv)) diff --git a/WDL/Value.py b/WDL/Value.py index b735afa6..3d10ac45 100644 --- a/WDL/Value.py +++ b/WDL/Value.py @@ -90,10 +90,11 @@ def __str__(self) -> str: class Pair(Base): value : Optional[Tuple[Base,Base]] = None - def __init__(self, type : T.Map, value : Tuple[Base,Base]) -> None: + def __init__(self, type : T.Pair, value : Tuple[Base,Base]) -> None: super().__init__(type, value) def __str__(self) -> str: - raise NotImplementedError() # TODO + assert isinstance(self.value, tuple) + return "(" + str(self.value[0]) + "," + str(self.value[1]) + ")" # pyre-fixme class Null(Base): """Represents the missing value which optional inputs may take. ``type`` and ``value`` are both None.""" diff --git a/WDL/__init__.py b/WDL/__init__.py index 696eddde..ed10426e 100644 --- a/WDL/__init__.py +++ b/WDL/__init__.py @@ -69,6 +69,9 @@ def negate(self, items, meta) -> E.Base: def get(self, items, meta) -> E.Base: return E.Apply(sp(self.filename, meta), "_get", items) + def pair(self, items, meta) -> E.Base: + assert len(items) == 2 + return E.Pair(sp(self.filename, meta), items[0], items[1]) def get_left(self, items, meta) -> E.Base: return E.Apply(sp(self.filename, meta), "_get_left", items) def get_right(self, items, meta) -> E.Base: diff --git a/WDL/_parser.py b/WDL/_parser.py index 3f596a63..55449518 100644 --- a/WDL/_parser.py +++ b/WDL/_parser.py @@ -42,6 +42,7 @@ | "[" [expr ("," expr)*] "]" -> array | expr_core "[" expr "]" -> get + | "(" expr "," expr ")" -> pair | expr_core "." _LEFT -> get_left | expr_core "." _RIGHT -> get_right diff --git a/tests/test_eval.py b/tests/test_eval.py index d2ec700a..49592616 100644 --- a/tests/test_eval.py +++ b/tests/test_eval.py @@ -193,7 +193,7 @@ def test_ident(self): ("bogus", "(Ln 1, Col 1) Unknown identifier", WDL.Error.UnknownIdentifier, env), ("pi+e", "5.85987", env), ("t||f", "true", WDL.Type.Boolean(), env), - ("if t then pi else e", "3.14159", env), + ("if t then pi else e", "3.14159", env) ) @@ -214,6 +214,20 @@ def test_interpolation(self): ("'The U.$. is re$pected again!'",'"The U.$. is re$pected again!"') ) + def test_pair(self): + env = cons_env(("p", WDL.Value.Pair(WDL.Type.Pair(WDL.Type.Float(), WDL.Type.Float()), + (WDL.Value.Float(3.14159), WDL.Value.Float(2.71828))))) + self._test_tuples( + ("(1,2)", "(1,2)", WDL.Type.Pair(WDL.Type.Int(), WDL.Type.Int())), + ("(1,2).left", "1"), + ("(1,false).right", "false"), + ("(false,[1,2]).right[1]", "2"), + ("[1,2].left", "", WDL.Error.NotAPair), + ("false.right", "", WDL.Error.NotAPair), + ("p.left", "3.14159", env), + ("p.right", "2.71828", env) + ) + def test_errors(self): self._test_tuples( ("1 + bogus(2)", "(Ln 1, Col 5) No such function: bogus", WDL.Error.NoSuchFunction)