diff --git a/WDL/Type.py b/WDL/Type.py index db8c0170..12739989 100644 --- a/WDL/Type.py +++ b/WDL/Type.py @@ -483,7 +483,7 @@ def coerces(self, rhs: Base, check_quant: bool = True) -> bool: return False -def unify(types: List[Base], check_quant: bool = True, force_string: bool = False,) -> Base: +def unify(types: List[Base], check_quant: bool = True, force_string: bool = False) -> Base: """ Given a list of types, compute a type to which they're all coercible, or :class:`WDL.Type.Any` if no more-specific inference is possible. @@ -496,20 +496,38 @@ def unify(types: List[Base], check_quant: bool = True, force_string: bool = Fals # begin with first type; or if --no-quant-check, the first array type (as we can try to promote # other T to Array[T]) - t = types[0] + t = next((t for t in types if not isinstance(t, Any)), types[0]) if not check_quant: - t = next((a for a in types if isinstance(a, Array)), t) + t = next((a for a in types if isinstance(a, Array) and not isinstance(a.item_type, Any)), t) + t = t.copy() # pyre-ignore # potentially promote/generalize t to other types seen optional = False all_nonempty = True all_stringifiable = True for t2 in types: + # recurse on parameters of compound types + t_was_array_any = isinstance(t, Array) and isinstance(t.item_type, Any) + if isinstance(t, Array) and isinstance(t2, Array) and not isinstance(t2.item_type, Any): + t.item_type = unify([t.item_type, t2.item_type], check_quant, force_string) + if isinstance(t, Pair) and isinstance(t2, Pair): + t.left_type = unify([t.left_type, t2.left_type], check_quant, force_string) + t.right_type = unify([t.right_type, t2.right_type], check_quant, force_string) + if isinstance(t, Map) and isinstance(t2, Map): + t.item_type = ( # pyre-ignore + unify([t.item_type[0], t2.item_type[0]], check_quant, force_string), # pyre-ignore + unify([t.item_type[1], t2.item_type[1]], check_quant, force_string), # pyre-ignore + ) + if not t_was_array_any and next((pt for pt in t.parameters if isinstance(pt, Any)), False): + return Any() + + # Int/Float, String/File if isinstance(t, Int) and isinstance(t2, Float): t = Float() if isinstance(t, String) and isinstance(t2, File): t = File() + # String if ( isinstance(t2, String) and not isinstance(t2, File) @@ -520,9 +538,10 @@ def unify(types: List[Base], check_quant: bool = True, force_string: bool = Fals if not t2.coerces(String(optional=True), check_quant=check_quant): all_stringifiable = False - if t2.optional: + # optional/nonempty + if t.optional or t2.optional: optional = True - if isinstance(t2, Array) and not t2.nonempty: + if isinstance(t, Array) and not t.nonempty or isinstance(t2, Array) and not t2.nonempty: all_nonempty = False if isinstance(t, Array): diff --git a/WDL/Value.py b/WDL/Value.py index b47f07bd..9f5a285a 100644 --- a/WDL/Value.py +++ b/WDL/Value.py @@ -415,6 +415,12 @@ def from_json(type: Type.Base, value: Any) -> Base: return String(value) if isinstance(type, Type.Array) and isinstance(value, list): return Array(type, [from_json(type.item_type, item) for item in value]) + if isinstance(type, Type.Pair) and isinstance(value, list) and len(value) == 2: + return Pair( + type.left_type, + type.right_type, + (from_json(type.left_type, value[0]), from_json(type.right_type, value[1])), + ) if ( isinstance(type, Type.Map) and type.item_type[0] == Type.String() diff --git a/tests/test_0eval.py b/tests/test_0eval.py index 84fe4114..b40a0fef 100644 --- a/tests/test_0eval.py +++ b/tests/test_0eval.py @@ -448,6 +448,7 @@ def test_json(self): (WDL.Type.Map((WDL.Type.String(), WDL.Type.Int())), {"cats": 42, "dogs": 99}), (pty, {"name": "Alyssa", "age": 42, "pets": None}), (pty, {"name": "Alyssa", "age": 42, "pets": {"cats": 42, "dogs": 99}}), + (WDL.Type.Array(WDL.Type.Pair(WDL.Type.String(), WDL.Type.Int())), [["a",0],["b",1]]), (WDL.Type.Boolean(), 42, WDL.Error.InputError), (WDL.Type.Float(), "your president", WDL.Error.InputError), diff --git a/tests/test_1doc.py b/tests/test_1doc.py index 7e20cbaf..de05c00a 100644 --- a/tests/test_1doc.py +++ b/tests/test_1doc.py @@ -490,9 +490,14 @@ def test_unify(self): workflow unify { String s File? f2 + Array[Int] a1 = [1] + Array[Int?]? a2 = [] + Array[Pair[String,String]] ap = [(0,1),(2,3)] + Array[Map[String,String]] am = [{ "a": 0, "b": 1 }, { "a": "x", "b": "y" }, { 1: 2, 3: 4 }] output { Array[File?] a = [s, f2] + Array[Array[Int?]?] a3 = [a1, a2] Map[String, File?] m = { "foo": s, "bar": f2 } Map[Float, File?] m2 = { 1: s, 2.0: f2 } } @@ -500,6 +505,14 @@ def test_unify(self): """) doc.typecheck() + with self.assertRaises(WDL.Error.ValidationError): + doc = WDL.parse_document(""" + workflow unify { + Array[Pair[String,String]] bogus = [("a","b"), ("c",("d","e"))] + } + """) + doc.typecheck() + class TestDoc(unittest.TestCase): def test_count_foo(self): doc = r"""#foo diff --git a/tests/test_3corpi.py b/tests/test_3corpi.py index 270c5a9b..64227fe4 100644 --- a/tests/test_3corpi.py +++ b/tests/test_3corpi.py @@ -300,7 +300,7 @@ class ViralNGS(unittest.TestCase): @wdl_corpus( ["test_corpi/ENCODE-DCC/chip-seq-pipeline2/**"], expected_lint={ - "StringCoercion": 192, + "StringCoercion": 208, "FileCoercion": 154, "NameCollision": 16, "OptionalCoercion": 64,