diff --git a/wdl-analysis/CHANGELOG.md b/wdl-analysis/CHANGELOG.md index 8415c114..a78f686a 100644 --- a/wdl-analysis/CHANGELOG.md +++ b/wdl-analysis/CHANGELOG.md @@ -15,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* The "required primitive type" constraint has been removed as every place the + constraint was used should allow for optional primitive types as well; + consequently, the AnyPrimitiveTypeConstraint was renamed to simply + `PrimitiveTypeConstraint` ([#257](https://github.com/stjude-rust-labs/wdl/pull/257)). +* The common type calculation now favors the "left-hand side" of the + calculation rather than the right, making it more intuitive to use. For + example, a calculation of `File | String` is now `File` rather than + `String` ([#257](https://github.com/stjude-rust-labs/wdl/pull/257)). * Refactored function call binding information to aid with call evaluation in `wdl-engine` ([#251](https://github.com/stjude-rust-labs/wdl/pull/251)). * Made diagnostic creation functions public ([#249](https://github.com/stjude-rust-labs/wdl/pull/249)). @@ -23,6 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* Common type calculation now supports discovering common types between the + compound types containing Union and None as inner types, e.g. + `Array[String] | Array[None] -> Array[String?]` ([#257](https://github.com/stjude-rust-labs/wdl/pull/257)). * Static analysis of expressions within object literal members now takes place ([#254](https://github.com/stjude-rust-labs/wdl/pull/254)). * Certain standard library functions with an existing constraint on generic parameters that take structs are further constrained to take structs diff --git a/wdl-analysis/src/stdlib.rs b/wdl-analysis/src/stdlib.rs index 64390531..8f4b4ba0 100644 --- a/wdl-analysis/src/stdlib.rs +++ b/wdl-analysis/src/stdlib.rs @@ -2387,7 +2387,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { "prefix", MonomorphicFunction::new( FunctionSignature::builder() - .type_parameter("P", RequiredPrimitiveTypeConstraint) + .type_parameter("P", PrimitiveTypeConstraint) .parameter(PrimitiveTypeKind::String) .parameter(GenericArrayType::new(GenericType::Parameter("P"))) .ret(array_string) @@ -2406,7 +2406,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::One)) - .type_parameter("P", RequiredPrimitiveTypeConstraint) + .type_parameter("P", PrimitiveTypeConstraint) .parameter(PrimitiveTypeKind::String) .parameter(GenericArrayType::new(GenericType::Parameter("P"))) .ret(array_string) @@ -2425,7 +2425,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::One)) - .type_parameter("P", RequiredPrimitiveTypeConstraint) + .type_parameter("P", PrimitiveTypeConstraint) .parameter(GenericArrayType::new(GenericType::Parameter("P"))) .ret(array_string) .build(), @@ -2443,7 +2443,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::One)) - .type_parameter("P", RequiredPrimitiveTypeConstraint) + .type_parameter("P", PrimitiveTypeConstraint) .parameter(GenericArrayType::new(GenericType::Parameter("P"))) .ret(array_string) .build(), @@ -2461,7 +2461,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::One)) - .type_parameter("P", RequiredPrimitiveTypeConstraint) + .type_parameter("P", PrimitiveTypeConstraint) .parameter(PrimitiveTypeKind::String) .parameter(GenericArrayType::new(GenericType::Parameter("P"))) .ret(PrimitiveTypeKind::String) @@ -2586,7 +2586,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::Two)) - .type_parameter("P", AnyPrimitiveTypeConstraint) + .type_parameter("P", PrimitiveTypeConstraint) .parameter(GenericArrayType::new(GenericType::Parameter("P"))) .parameter(GenericType::Parameter("P")) .ret(PrimitiveTypeKind::Boolean) @@ -2685,7 +2685,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::One)) - .type_parameter("K", RequiredPrimitiveTypeConstraint) + .type_parameter("K", PrimitiveTypeConstraint) .any_type_parameter("V") .parameter(GenericMapType::new( GenericType::Parameter("K"), @@ -2710,7 +2710,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::One)) - .type_parameter("K", RequiredPrimitiveTypeConstraint) + .type_parameter("K", PrimitiveTypeConstraint) .any_type_parameter("V") .parameter(GenericArrayType::new(GenericPairType::new( GenericType::Parameter("K"), @@ -2735,7 +2735,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { PolymorphicFunction::new(vec![ FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::One)) - .type_parameter("K", RequiredPrimitiveTypeConstraint) + .type_parameter("K", PrimitiveTypeConstraint) .any_type_parameter("V") .parameter(GenericMapType::new( GenericType::Parameter("K"), @@ -2744,13 +2744,13 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { .ret(GenericArrayType::new(GenericType::Parameter("K"))) .build(), FunctionSignature::builder() - .min_version(SupportedVersion::V1(V1::One)) + .min_version(SupportedVersion::V1(V1::Two)) .type_parameter("S", StructConstraint) .parameter(GenericType::Parameter("S")) .ret(array_string) .build(), FunctionSignature::builder() - .min_version(SupportedVersion::V1(V1::One)) + .min_version(SupportedVersion::V1(V1::Two)) .parameter(Type::Object) .ret(array_string) .build(), @@ -2768,7 +2768,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { PolymorphicFunction::new(vec![ FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::Two)) - .type_parameter("K", RequiredPrimitiveTypeConstraint) + .type_parameter("K", PrimitiveTypeConstraint) .any_type_parameter("V") .parameter(GenericMapType::new( GenericType::Parameter("K"), @@ -2820,7 +2820,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::Two)) - .type_parameter("K", RequiredPrimitiveTypeConstraint) + .type_parameter("K", PrimitiveTypeConstraint) .any_type_parameter("V") .parameter(GenericMapType::new( GenericType::Parameter("K"), @@ -2842,7 +2842,7 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { MonomorphicFunction::new( FunctionSignature::builder() .min_version(SupportedVersion::V1(V1::One)) - .type_parameter("K", RequiredPrimitiveTypeConstraint) + .type_parameter("K", PrimitiveTypeConstraint) .any_type_parameter("V") .parameter(GenericArrayType::new(GenericPairType::new( GenericType::Parameter("K"), @@ -3008,11 +3008,11 @@ mod test { "write_objects(Array[Object]) -> File", "write_objects(Array[S]) -> File where `S`: any structure containing only primitive \ types", - "prefix(String, Array[P]) -> Array[String] where `P`: any required primitive type", - "suffix(String, Array[P]) -> Array[String] where `P`: any required primitive type", - "quote(Array[P]) -> Array[String] where `P`: any required primitive type", - "squote(Array[P]) -> Array[String] where `P`: any required primitive type", - "sep(String, Array[P]) -> String where `P`: any required primitive type", + "prefix(String, Array[P]) -> Array[String] where `P`: any primitive type", + "suffix(String, Array[P]) -> Array[String] where `P`: any primitive type", + "quote(Array[P]) -> Array[String] where `P`: any primitive type", + "squote(Array[P]) -> Array[String] where `P`: any primitive type", + "sep(String, Array[P]) -> String where `P`: any primitive type", "range(Int) -> Array[Int]", "transpose(Array[Array[X]]) -> Array[Array[X]]", "cross(Array[X], Array[Y]) -> Array[Pair[X, Y]]", @@ -3023,19 +3023,18 @@ mod test { "flatten(Array[Array[X]]) -> Array[X]", "select_first(Array[X], ) -> X where `X`: any optional type", "select_all(Array[X]) -> Array[X] where `X`: any optional type", - "as_pairs(Map[K, V]) -> Array[Pair[K, V]] where `K`: any required primitive type", - "as_map(Array[Pair[K, V]]) -> Map[K, V] where `K`: any required primitive type", - "keys(Map[K, V]) -> Array[K] where `K`: any required primitive type", + "as_pairs(Map[K, V]) -> Array[Pair[K, V]] where `K`: any primitive type", + "as_map(Array[Pair[K, V]]) -> Map[K, V] where `K`: any primitive type", + "keys(Map[K, V]) -> Array[K] where `K`: any primitive type", "keys(S) -> Array[String] where `S`: any structure", "keys(Object) -> Array[String]", - "contains_key(Map[K, V], K) -> Boolean where `K`: any required primitive type", + "contains_key(Map[K, V], K) -> Boolean where `K`: any primitive type", "contains_key(Object, String) -> Boolean", "contains_key(Map[String, V], Array[String]) -> Boolean", "contains_key(S, Array[String]) -> Boolean where `S`: any structure", "contains_key(Object, Array[String]) -> Boolean", - "values(Map[K, V]) -> Array[V] where `K`: any required primitive type", - "collect_by_key(Array[Pair[K, V]]) -> Map[K, Array[V]] where `K`: any required \ - primitive type", + "values(Map[K, V]) -> Array[V] where `K`: any primitive type", + "collect_by_key(Array[Pair[K, V]]) -> Map[K, Array[V]] where `K`: any primitive type", "defined(X) -> Boolean where `X`: any optional type", "length(Array[X]) -> Int", "length(Map[K, V]) -> Int", @@ -3135,7 +3134,7 @@ mod test { .expect_err("bind should fail"); assert_eq!(e, FunctionBindError::ArgumentTypeMismatch { index: 0, - expected: "`Map[K, V]` where `K`: any required primitive type".into() + expected: "`Map[K, V]` where `K`: any primitive type".into() }); // Check for Union (i.e. indeterminate) @@ -3178,13 +3177,14 @@ mod test { PrimitiveType::optional(PrimitiveTypeKind::String), PrimitiveTypeKind::Boolean, )); - let e = f + let binding = f .bind(SupportedVersion::V1(V1::Two), &mut types, &[ty]) - .expect_err("bind should fail"); - assert_eq!(e, FunctionBindError::ArgumentTypeMismatch { - index: 0, - expected: "`Map[K, Boolean]` where `K`: any required primitive type".into() - }); + .expect("bind should succeed"); + assert_eq!(binding.index(), 0); + assert_eq!( + binding.return_type().display(&types).to_string(), + "Array[Boolean]" + ); } #[test] diff --git a/wdl-analysis/src/stdlib/constraints.rs b/wdl-analysis/src/stdlib/constraints.rs index dc0f744f..43972aad 100644 --- a/wdl-analysis/src/stdlib/constraints.rs +++ b/wdl-analysis/src/stdlib/constraints.rs @@ -182,37 +182,11 @@ impl Constraint for JsonSerializableConstraint { } } -/// Represents a constraint that ensures the type is a required primitive type. +/// Represents a constraint that ensures the type is a primitive type. #[derive(Debug, Copy, Clone)] -pub struct RequiredPrimitiveTypeConstraint; +pub struct PrimitiveTypeConstraint; -impl Constraint for RequiredPrimitiveTypeConstraint { - fn description(&self) -> &'static str { - "any required primitive type" - } - - fn satisfied(&self, _: &Types, ty: Type) -> bool { - match ty { - Type::Primitive(ty) => !ty.is_optional(), - // Treat unions as primitive as they can only be checked at runtime - Type::Union => true, - Type::Compound(_) - | Type::Object - | Type::OptionalObject - | Type::None - | Type::Task - | Type::Hints - | Type::Input - | Type::Output => false, - } - } -} - -/// Represents a constraint that ensures the type is any primitive type. -#[derive(Debug, Copy, Clone)] -pub struct AnyPrimitiveTypeConstraint; - -impl Constraint for AnyPrimitiveTypeConstraint { +impl Constraint for PrimitiveTypeConstraint { fn description(&self) -> &'static str { "any primitive type" } @@ -542,71 +516,8 @@ mod test { } #[test] - fn test_required_primitive_constraint() { - let constraint = RequiredPrimitiveTypeConstraint; - let mut types = Types::default(); - - assert!(!constraint.satisfied( - &types, - PrimitiveType::optional(PrimitiveTypeKind::Boolean).into() - )); - assert!(!constraint.satisfied( - &types, - PrimitiveType::optional(PrimitiveTypeKind::Integer).into() - )); - assert!(!constraint.satisfied( - &types, - PrimitiveType::optional(PrimitiveTypeKind::Float).into() - )); - assert!(!constraint.satisfied( - &types, - PrimitiveType::optional(PrimitiveTypeKind::String).into() - )); - assert!(!constraint.satisfied( - &types, - PrimitiveType::optional(PrimitiveTypeKind::File).into() - )); - assert!(!constraint.satisfied( - &types, - PrimitiveType::optional(PrimitiveTypeKind::Directory).into() - )); - assert!(constraint.satisfied(&types, PrimitiveTypeKind::Boolean.into())); - assert!(constraint.satisfied(&types, PrimitiveTypeKind::Integer.into())); - assert!(constraint.satisfied(&types, PrimitiveTypeKind::Float.into())); - assert!(constraint.satisfied(&types, PrimitiveTypeKind::String.into())); - assert!(constraint.satisfied(&types, PrimitiveTypeKind::File.into())); - assert!(constraint.satisfied(&types, PrimitiveTypeKind::Directory.into())); - assert!(!constraint.satisfied(&types, Type::Object)); - assert!(!constraint.satisfied(&types, Type::OptionalObject)); - assert!(constraint.satisfied(&types, Type::Union)); - assert!(!constraint.satisfied(&types, Type::None)); - - let ty = types.add_array(ArrayType::non_empty(PrimitiveTypeKind::String)); - assert!(!constraint.satisfied(&types, ty)); - - let ty = types - .add_pair(PairType::new( - PrimitiveTypeKind::String, - PrimitiveTypeKind::String, - )) - .optional(); - assert!(!constraint.satisfied(&types, ty)); - - let ty = types.add_map(MapType::new( - PrimitiveTypeKind::String, - PrimitiveTypeKind::String, - )); - assert!(!constraint.satisfied(&types, ty)); - - let ty = types - .add_struct(StructType::new("Foo", [("foo", PrimitiveTypeKind::String)])) - .optional(); - assert!(!constraint.satisfied(&types, ty)); - } - - #[test] - fn test_any_primitive_constraint() { - let constraint = AnyPrimitiveTypeConstraint; + fn test_primitive_constraint() { + let constraint = PrimitiveTypeConstraint; let mut types = Types::default(); assert!(constraint.satisfied( diff --git a/wdl-analysis/src/types.rs b/wdl-analysis/src/types.rs index 41f32c47..e8fbb344 100644 --- a/wdl-analysis/src/types.rs +++ b/wdl-analysis/src/types.rs @@ -344,27 +344,43 @@ impl Type { /// Calculates a common type between this type and the given type. /// /// Returns `None` if the types have no common type. - pub fn common_type(&self, types: &Types, other: Type) -> Option { - // Check for this type being coercible to the other type - if self.is_coercible_to(types, &other) { + pub fn common_type(&self, types: &mut Types, other: Type) -> Option { + // If the other type is union, then the common type would be this type + if other.is_union() { + return Some(*self); + } + + // If this type is union, then the common type would be the other type + if self.is_union() { return Some(other); } + // If the other type is `None`, then the common type would be an optional this + // type + if other.is_none() { + return Some(self.optional()); + } + + // If this type is `None`, then the common type would be an optional other type + if self.is_none() { + return Some(other.optional()); + } + // Check for the other type being coercible to this type if other.is_coercible_to(types, self) { return Some(*self); } - // Check for `None` for this type; the common type would be an optional other - // type - if self.is_none() { - return Some(other.optional()); + // Check for this type being coercible to the other type + if self.is_coercible_to(types, &other) { + return Some(other); } - // Check for `None` for the other type; the common type would be an optional - // this type - if other.is_none() { - return Some(self.optional()); + // Check for a compound type that might have a common type within it + if let (Some(this), Some(other)) = (self.as_compound(), other.as_compound()) { + if let Some(ty) = this.common_type(types, other) { + return Some(ty); + } } None @@ -587,6 +603,41 @@ impl CompoundType { } } + /// Calculates a common type between two compound types. + /// + /// This method does not attempt coercion; it only attempts to find common + /// inner types for the same outer type. + fn common_type(&self, types: &mut Types, other: Self) -> Option { + // Check to see if the types are both `Array`, `Pair`, or `Map`; if so, attempt + // to find a common type for their inner types + match ( + types.type_definition(self.definition), + types.type_definition(other.definition), + ) { + (CompoundTypeDef::Array(this), CompoundTypeDef::Array(other)) => { + let this = *this; + let other = *other; + let element_type = this.element_type.common_type(types, other.element_type)?; + Some(types.add_array(ArrayType::new(element_type))) + } + (CompoundTypeDef::Pair(this), CompoundTypeDef::Pair(other)) => { + let this = *this; + let other = *other; + let left_type = this.left_type.common_type(types, other.left_type)?; + let right_type = this.right_type.common_type(types, other.right_type)?; + Some(types.add_pair(PairType::new(left_type, right_type))) + } + (CompoundTypeDef::Map(this), CompoundTypeDef::Map(other)) => { + let this = *this; + let other = *other; + let key_type = this.key_type.common_type(types, other.key_type)?; + let value_type = this.value_type.common_type(types, other.value_type)?; + Some(types.add_map(MapType::new(key_type, value_type))) + } + _ => None, + } + } + /// Asserts that the type is valid. fn assert_valid(&self, types: &Types) { types.type_definition(self.definition).assert_valid(types); diff --git a/wdl-analysis/src/types/v1.rs b/wdl-analysis/src/types/v1.rs index 7bc526d1..1f16b0f4 100644 --- a/wdl-analysis/src/types/v1.rs +++ b/wdl-analysis/src/types/v1.rs @@ -669,7 +669,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { // Ensure the remaining element types share a common type for expr in elements { if let Some(actual) = self.evaluate_expr(&expr) { - if let Some(ty) = actual.common_type(self.context.types(), expected) { + if let Some(ty) = expected.common_type(self.context.types_mut(), actual) { expected = ty; expected_span = expr.span(); } else { @@ -747,7 +747,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { if let Some(actual_key) = self.evaluate_expr(&key) { if let Some(actual_value) = self.evaluate_expr(&value) { if let Some(ty) = - actual_key.common_type(self.context.types(), expected_key) + expected_key.common_type(self.context.types_mut(), actual_key) { expected_key = ty; expected_key_span = key.span(); @@ -762,7 +762,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { } if let Some(ty) = - actual_value.common_type(self.context.types(), expected_value) + expected_value.common_type(self.context.types_mut(), actual_value) { expected_value = ty; expected_value_span = value.span(); @@ -1135,7 +1135,7 @@ impl<'a, C: EvaluationContext> ExprTypeEvaluator<'a, C> { (Type::Union, _) => Some(false_ty), (_, Type::Union) => Some(true_ty), _ => { - if let Some(ty) = true_ty.common_type(self.context.types(), false_ty) { + if let Some(ty) = true_ty.common_type(self.context.types_mut(), false_ty) { Some(ty) } else { self.diagnostics.push(type_mismatch( diff --git a/wdl-analysis/tests/analysis/common-types/source.wdl b/wdl-analysis/tests/analysis/common-types/source.wdl index 30997c8b..f16c7e2c 100644 --- a/wdl-analysis/tests/analysis/common-types/source.wdl +++ b/wdl-analysis/tests/analysis/common-types/source.wdl @@ -28,4 +28,14 @@ workflow test { String? n = if (true) then "foo" else file String? o = if (false) then None else file String? p = if (true) then file else None + + # Tests for compound types + Array[String?] q = ["foo", None, "baz"] + Array[String?] r = ["foo", "bar", "baz"] + Array[Pair[String?, Float]] s = [("foo", 1.0), (None, 2), ("baz", 3)] + Map[String?, Float] t = { "foo": 1, None: 1.0 } + Map[String?, String] u = { None: "bar", "foo": "baz" } + Map[String?, Pair[Array[String?]?, Int]] v = { None: (["foo", None], 1), "foo": (None, 2) } + Array[File]? w = ["foo"] + Array[String] x = select_first([w, ["foo"]]) } diff --git a/wdl-engine/CHANGELOG.md b/wdl-engine/CHANGELOG.md index 36ed6b14..5092c9d7 100644 --- a/wdl-engine/CHANGELOG.md +++ b/wdl-engine/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Fixed `Map` values not accepting `None` for keys ([#257](https://github.com/stjude-rust-labs/wdl/pull/257)). +* Implement the generic map functions from the WDL standard library ([#257](https://github.com/stjude-rust-labs/wdl/pull/257)). * Implement the generic array functions from the WDL standard library ([#256](https://github.com/stjude-rust-labs/wdl/pull/256)). * Implement the string array functions from the WDL standard library ([#255](https://github.com/stjude-rust-labs/wdl/pull/255)). * Replaced the `Value::from_json` method with `Value::deserialize` which allows diff --git a/wdl-engine/src/eval/v1.rs b/wdl-engine/src/eval/v1.rs index ce7a69df..1ff4fa94 100644 --- a/wdl-engine/src/eval/v1.rs +++ b/wdl-engine/src/eval/v1.rs @@ -397,7 +397,7 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { let value = self.evaluate_expr(&expr)?; let actual = value.ty(); - if let Some(ty) = actual.common_type(self.context.types(), expected) { + if let Some(ty) = expected.common_type(self.context.types_mut(), actual) { expected = ty; expected_span = expr.span(); } else { @@ -458,10 +458,9 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { let mut expected_value_span = value.span(); // The key type must be primitive - match expected_key { - Value::Primitive(key) => { - elements.push((key, expected_value)); - } + let key = match expected_key { + Value::None => None, + Value::Primitive(key) => Some(key), _ => { return Err(map_key_not_primitive( self.context.types(), @@ -469,7 +468,9 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { expected_key.ty(), )); } - } + }; + + elements.push((key, expected_value)); // Ensure the remaining items types share common types for item in items { @@ -480,7 +481,7 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { let actual_value_ty = actual_value.ty(); if let Some(ty) = - actual_key_ty.common_type(self.context.types(), expected_key_ty) + expected_key_ty.common_type(self.context.types_mut(), actual_key_ty) { expected_key_ty = ty; expected_key_span = key.span(); @@ -496,7 +497,7 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { } if let Some(ty) = - actual_value_ty.common_type(self.context.types(), expected_value_ty) + expected_value_ty.common_type(self.context.types_mut(), actual_value_ty) { expected_value_ty = ty; expected_value_span = value.span(); @@ -511,12 +512,13 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { )); } - match actual_key { - Value::Primitive(key) => { - elements.push((key, actual_value)); - } + let key = match actual_key { + Value::None => None, + Value::Primitive(key) => Some(key), _ => panic!("the key type is not primitive, but had a common type"), - } + }; + + elements.push((key, actual_value)); } (expected_key_ty, expected_value_ty, elements) @@ -718,8 +720,8 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { // Determine the common type of the true and false expressions // The value must be coerced to that type - let ty = false_ty - .common_type(self.context.types(), true_ty) + let ty = true_ty + .common_type(self.context.types_mut(), false_ty) .ok_or_else(|| { no_common_type( self.context.types(), @@ -1141,22 +1143,31 @@ impl<'a, C: EvaluationContext> ExprEvaluator<'a, C> { _ => panic!("expected a map type"), }; - match self.evaluate_expr(&index)? { + let i = match self.evaluate_expr(&index)? { + Value::None + if Type::None.is_coercible_to(self.context.types(), &key_type.into()) => + { + None + } Value::Primitive(i) if i.ty() .is_coercible_to(self.context.types(), &key_type.into()) => { - match map.elements().get(&i) { - Some(value) => Ok(value.clone()), - None => Err(map_key_not_found(index.span())), - } + Some(i) } - value => Err(index_type_mismatch( - self.context.types(), - key_type.into(), - value.ty(), - index.span(), - )), + value => { + return Err(index_type_mismatch( + self.context.types(), + key_type.into(), + value.ty(), + index.span(), + )); + } + }; + + match map.elements().get(&i) { + Some(value) => Ok(value.clone()), + None => Err(map_key_not_found(index.span())), } } value => Err(cannot_index( diff --git a/wdl-engine/src/stdlib.rs b/wdl-engine/src/stdlib.rs index 0da63666..8f05b2ca 100644 --- a/wdl-engine/src/stdlib.rs +++ b/wdl-engine/src/stdlib.rs @@ -15,16 +15,21 @@ use crate::Coercible; use crate::EvaluationContext; use crate::Value; +mod as_map; +mod as_pairs; mod basename; mod ceil; mod chunk; +mod collect_by_key; mod contains; +mod contains_key; mod cross; mod find; mod flatten; mod floor; mod glob; mod join_paths; +mod keys; mod matches; mod max; mod min; @@ -53,6 +58,7 @@ mod sub; mod suffix; mod transpose; mod unzip; +mod values; mod write_json; mod write_lines; mod write_map; @@ -285,6 +291,12 @@ pub static STDLIB: LazyLock = LazyLock::new(|| { func!(flatten), func!(select_first), func!(select_all), + func!(as_pairs), + func!(as_map), + func!(keys), + func!(contains_key), + func!(values), + func!(collect_by_key), ]), } }); diff --git a/wdl-engine/src/stdlib/as_map.rs b/wdl-engine/src/stdlib/as_map.rs new file mode 100644 index 00000000..cd499275 --- /dev/null +++ b/wdl-engine/src/stdlib/as_map.rs @@ -0,0 +1,202 @@ +//! Implements the `as_map` function from the WDL standard library. + +use std::fmt; +use std::sync::Arc; + +use indexmap::IndexMap; +use wdl_ast::Diagnostic; + +use super::CallContext; +use super::Function; +use super::Signature; +use crate::Map; +use crate::PrimitiveValue; +use crate::Value; +use crate::diagnostics::function_call_failed; + +/// Used for displaying duplicate key errors. +struct DuplicateKeyError(Option); + +impl fmt::Display for DuplicateKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + Some(v) => write!( + f, + "array contains a duplicate entry for map key `{v}`", + v = v.raw() + ), + None => write!(f, "array contains a duplicate entry for map key `None`"), + } + } +} + +/// Converts an Array of Pairs into a Map in which the left elements of the +/// Pairs are the keys and the right elements the values. +/// +/// All the keys must be unique, or an error is raised. +/// +/// The order of the key/value pairs in the output Map is the same as the order +/// of the Pairs in the Array. +/// +/// https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#as_map +fn as_map(context: CallContext<'_>) -> Result { + debug_assert_eq!(context.arguments.len(), 1); + debug_assert!( + context + .types() + .type_definition( + context + .return_type + .as_compound() + .expect("type should be compound") + .definition() + ) + .as_map() + .is_some(), + "return type should be a map" + ); + + let array = context.arguments[0] + .value + .as_array() + .expect("argument should be an array"); + + let mut elements = IndexMap::with_capacity(array.len()); + for e in array.elements() { + let pair = e.as_pair().expect("element should be a pair"); + let key = match pair.left() { + Value::None => None, + Value::Primitive(v) => Some(v.clone()), + _ => unreachable!("expected a primitive type for the left value"), + }; + + if elements.insert(key.clone(), pair.right().clone()).is_some() { + return Err(function_call_failed( + "as_map", + DuplicateKeyError(key), + context.arguments[0].span, + )); + } + } + + Ok(Map::new_unchecked(context.return_type, Arc::new(elements)).into()) +} + +/// Gets the function describing `as_map`. +pub const fn descriptor() -> Function { + Function::new( + const { + &[Signature::new( + "(Array[Pair[K, V]]) -> Map[K, V] where `K`: any primitive type", + as_map, + )] + }, + ) +} + +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + use wdl_ast::version::V1; + + use crate::v1::test::TestEnv; + use crate::v1::test::eval_v1_expr; + + #[test] + fn as_map() { + let mut env = TestEnv::default(); + + let value = eval_v1_expr(&mut env, V1::One, "as_map([])").unwrap(); + assert_eq!(value.unwrap_map().len(), 0); + + let value = eval_v1_expr( + &mut env, + V1::One, + "as_map([('foo', 'bar'), ('bar', 'baz')])", + ) + .unwrap(); + let elements: Vec<_> = value + .as_map() + .unwrap() + .elements() + .iter() + .map(|(k, v)| { + ( + k.as_ref().and_then(|k| k.as_string()).unwrap().as_str(), + v.as_string().unwrap().as_str(), + ) + }) + .collect(); + assert_eq!(elements, [("foo", "bar"), ("bar", "baz")]); + + let value = + eval_v1_expr(&mut env, V1::One, "as_map([('a', 1), ('c', 3), ('b', 2)])").unwrap(); + let elements: Vec<_> = value + .as_map() + .unwrap() + .elements() + .iter() + .map(|(k, v)| { + ( + k.as_ref().and_then(|k| k.as_string()).unwrap().as_str(), + v.as_integer().unwrap(), + ) + }) + .collect(); + assert_eq!(elements, [("a", 1), ("c", 3), ("b", 2)]); + + let value = + eval_v1_expr(&mut env, V1::One, "as_map([('a', 1), (None, 3), ('b', 2)])").unwrap(); + let elements: Vec<_> = value + .as_map() + .unwrap() + .elements() + .iter() + .map(|(k, v)| { + ( + k.as_ref().map(|k| k.as_string().unwrap().as_str()), + v.as_integer().unwrap(), + ) + }) + .collect(); + assert_eq!(elements, [(Some("a"), 1), (None, 3), (Some("b"), 2)]); + + let value = eval_v1_expr( + &mut env, + V1::One, + "as_map(as_pairs({'a': 1, 'c': 3, 'b': 2}))", + ) + .unwrap(); + let elements: Vec<_> = value + .as_map() + .unwrap() + .elements() + .iter() + .map(|(k, v)| { + ( + k.as_ref().and_then(|k| k.as_string()).unwrap().as_str(), + v.as_integer().unwrap(), + ) + }) + .collect(); + assert_eq!(elements, [("a", 1), ("c", 3), ("b", 2)]); + + let diagnostic = + eval_v1_expr(&mut env, V1::One, "as_map([('a', 1), ('c', 3), ('a', 2)])").unwrap_err(); + assert_eq!( + diagnostic.message(), + "call to function `as_map` failed: array contains a duplicate entry for map key `a`" + ); + + let diagnostic = eval_v1_expr( + &mut env, + V1::One, + "as_map([(None, 1), ('c', 3), (None, 2)])", + ) + .unwrap_err(); + assert_eq!( + diagnostic.message(), + "call to function `as_map` failed: array contains a duplicate entry for map key `None`" + ); + } +} diff --git a/wdl-engine/src/stdlib/as_pairs.rs b/wdl-engine/src/stdlib/as_pairs.rs new file mode 100644 index 00000000..bf598322 --- /dev/null +++ b/wdl-engine/src/stdlib/as_pairs.rs @@ -0,0 +1,138 @@ +//! Implements the `as_pairs` function from the WDL standard library. + +use std::sync::Arc; + +use wdl_ast::Diagnostic; + +use super::CallContext; +use super::Function; +use super::Signature; +use crate::Array; +use crate::Pair; +use crate::Value; + +/// Converts a Map into an Array of Pairs. +/// +/// Since Maps are ordered, the output array will always have elements in the +/// same order they were added to the Map. +/// +/// https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#as_pairs +fn as_pairs(context: CallContext<'_>) -> Result { + debug_assert_eq!(context.arguments.len(), 1); + + let map = context.arguments[0] + .value + .as_map() + .expect("argument should be a map"); + + let element_ty = context + .types() + .type_definition( + context + .return_type + .as_compound() + .expect("type should be compound") + .definition(), + ) + .as_array() + .expect("type should be an array") + .element_type(); + + let elements = map + .elements() + .iter() + .map(|(k, v)| { + Pair::new_unchecked(element_ty, Arc::new(k.clone().into()), Arc::new(v.clone())).into() + }) + .collect(); + + Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) +} + +/// Gets the function describing `as_pairs`. +pub const fn descriptor() -> Function { + Function::new( + const { + &[Signature::new( + "(Map[K, V]) -> Array[Pair[K, V]] where `K`: any primitive type", + as_pairs, + )] + }, + ) +} + +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + use wdl_ast::version::V1; + + use crate::PrimitiveValue; + use crate::Value; + use crate::v1::test::TestEnv; + use crate::v1::test::eval_v1_expr; + + #[test] + fn as_pairs() { + let mut env = TestEnv::default(); + + let value = eval_v1_expr(&mut env, V1::One, "as_pairs({})").unwrap(); + assert_eq!(value.unwrap_array().len(), 0); + + let value = eval_v1_expr( + &mut env, + V1::One, + "as_pairs({ 'foo': 'bar', 'bar': 'baz' })", + ) + .unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| { + let pair = v.as_pair().unwrap(); + ( + pair.left().as_string().unwrap().as_str(), + pair.right().as_string().unwrap().as_str(), + ) + }) + .collect(); + assert_eq!(elements, [("foo", "bar"), ("bar", "baz")]); + + let value = eval_v1_expr(&mut env, V1::One, "as_pairs({'a': 1, 'c': 3, 'b': 2})").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| { + let pair = v.as_pair().unwrap(); + ( + pair.left().as_string().unwrap().as_str(), + pair.right().as_integer().unwrap(), + ) + }) + .collect(); + assert_eq!(elements, [("a", 1), ("c", 3), ("b", 2)]); + + let value = eval_v1_expr(&mut env, V1::One, "as_pairs({'a': 1, None: 3, 'b': 2})").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| { + let pair = v.as_pair().unwrap(); + ( + match pair.left() { + Value::None => None, + Value::Primitive(PrimitiveValue::String(s)) => Some(s.as_str()), + _ => panic!("expected a String?"), + }, + pair.right().as_integer().unwrap(), + ) + }) + .collect(); + assert_eq!(elements, [(Some("a"), 1), (None, 3), (Some("b"), 2)]); + } +} diff --git a/wdl-engine/src/stdlib/collect_by_key.rs b/wdl-engine/src/stdlib/collect_by_key.rs new file mode 100644 index 00000000..da0f49ec --- /dev/null +++ b/wdl-engine/src/stdlib/collect_by_key.rs @@ -0,0 +1,138 @@ +//! Implements the `collect_by_key` function from the WDL standard library. + +use std::sync::Arc; + +use indexmap::IndexMap; +use wdl_ast::Diagnostic; + +use super::CallContext; +use super::Function; +use super::Signature; +use crate::Array; +use crate::Map; +use crate::Value; + +/// Given an Array of Pairs, creates a Map in which the right elements of the +/// Pairs are grouped by the left elements. +/// +/// In other words, the input Array may have multiple Pairs with the same key. +/// +/// Rather than causing an error (as would happen with as_map), all the values +/// with the same key are grouped together into an Array. +/// +/// The order of the keys in the output Map is the same as the order of their +/// first occurrence in the input Array. +/// +/// The order of the elements in the Map values is the same as their order of +/// occurrence in the input Array. +/// +/// https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#collect_by_key +fn collect_by_key(context: CallContext<'_>) -> Result { + debug_assert_eq!(context.arguments.len(), 1); + + let array = context.arguments[0] + .value + .as_array() + .expect("value should be an array"); + + let map_ty = context + .types() + .type_definition( + context + .return_type + .as_compound() + .expect("type should be compound") + .definition(), + ) + .as_map() + .expect("return type should be a map"); + debug_assert!( + context + .types() + .type_definition( + map_ty + .value_type() + .as_compound() + .expect("type should be compound") + .definition(), + ) + .as_array() + .is_some(), + "return type's value type should be an array" + ); + + // Start by collecting duplicate keys into a `Vec` + let mut map: IndexMap<_, Vec<_>> = IndexMap::new(); + for v in array.elements() { + let pair = v.as_pair().expect("value should be a pair"); + map.entry(match pair.left() { + Value::None => None, + Value::Primitive(v) => Some(v.clone()), + _ => unreachable!("value should be primitive"), + }) + .or_default() + .push(pair.right().clone()); + } + + // Transform each `Vec` into an array value + let elements = map + .into_iter() + .map(|(k, v)| { + ( + k, + Array::new_unchecked(map_ty.value_type(), Arc::new(v)).into(), + ) + }) + .collect(); + + Ok(Map::new_unchecked(context.return_type, Arc::new(elements)).into()) +} + +/// Gets the function describing `collect_by_key`. +pub const fn descriptor() -> Function { + Function::new( + const { + &[Signature::new( + "(Array[Pair[K, V]]) -> Map[K, Array[V]] where `K`: any primitive type", + collect_by_key, + )] + }, + ) +} + +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + use wdl_ast::version::V1; + + use crate::v1::test::TestEnv; + use crate::v1::test::eval_v1_expr; + + #[test] + fn collect_by_key() { + let mut env = TestEnv::default(); + + let value = eval_v1_expr(&mut env, V1::Two, "collect_by_key([])").unwrap(); + assert_eq!(value.unwrap_map().len(), 0); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "collect_by_key([('a', 1), ('b', 2), ('a', 3)])", + ) + .unwrap(); + assert_eq!(value.to_string(), r#"{"a": [1, 3], "b": [2]}"#); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "collect_by_key([('a', 1), (None, 2), ('a', 3), (None, 4), ('b', 5), ('c', 6), ('b', \ + 7)])", + ) + .unwrap(); + assert_eq!( + value.to_string(), + r#"{"a": [1, 3], None: [2, 4], "b": [5, 7], "c": [6]}"# + ); + } +} diff --git a/wdl-engine/src/stdlib/contains_key.rs b/wdl-engine/src/stdlib/contains_key.rs new file mode 100644 index 00000000..3cc639e8 --- /dev/null +++ b/wdl-engine/src/stdlib/contains_key.rs @@ -0,0 +1,354 @@ +//! Implements the `contains_key` function from the WDL standard library. + +use std::sync::Arc; + +use wdl_analysis::stdlib::STDLIB as ANALYSIS_STDLIB; +use wdl_analysis::types::PrimitiveTypeKind; +use wdl_analysis::types::Type; +use wdl_ast::Diagnostic; + +use super::CallContext; +use super::Function; +use super::Signature; +use crate::CompoundValue; +use crate::Object; +use crate::PrimitiveValue; +use crate::Struct; +use crate::Value; + +/// Given a Map and a key, tests whether the collection contains an entry with +/// the given key. +/// +/// `Boolean contains_key(Map[P, Y], P)`: Tests whether the Map has an entry +/// with the given key. If P is an optional type (e.g., String?), then the +/// second argument may be None. +/// +/// https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#-contains_key +fn contains_key_map(context: CallContext<'_>) -> Result { + debug_assert_eq!(context.arguments.len(), 2); + debug_assert!(context.return_type_eq(PrimitiveTypeKind::Boolean)); + + let map = context.arguments[0] + .value + .as_map() + .expect("first argument should be a map"); + + let key = match &context.arguments[1].value { + Value::None => None, + Value::Primitive(v) => Some(v.clone()), + _ => unreachable!("expected a primitive value for second argument"), + }; + + Ok(map.elements().contains_key(&key).into()) +} + +/// Given an object and a key, tests whether the object contains an entry with +/// the given key. +/// +/// `Boolean contains_key(Object, String)`: Tests whether the Object has an +/// entry with the given name.` +/// +/// https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#-contains_key +fn contains_key_object(context: CallContext<'_>) -> Result { + debug_assert_eq!(context.arguments.len(), 2); + debug_assert!(context.return_type_eq(PrimitiveTypeKind::Boolean)); + + // As `Map[String, X]` coerces to `Object`, dispatch to the map overload if + // passed a map + if context.arguments[0].value.as_map().is_some() { + return contains_key_map(context); + } + + let object = context.coerce_argument(0, Type::Object).unwrap_object(); + let key = context.coerce_argument(1, PrimitiveTypeKind::String); + Ok(object + .members() + .contains_key(key.unwrap_string().as_str()) + .into()) +} + +/// Given a key-value type collection (Map, Struct, or Object) and a key, tests +/// whether the collection contains an entry with the given key. +/// +/// `Boolean contains_key(Map[String, Y]|Struct|Object, Array[String])`: Tests +/// recursively for the presence of a compound key within a nested collection. +/// +/// https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#-contains_key +fn contains_key_recursive(context: CallContext<'_>) -> Result { + debug_assert_eq!(context.arguments.len(), 2); + debug_assert!(context.return_type_eq(PrimitiveTypeKind::Boolean)); + + /// Helper for looking up a value in a map, object, or struct by the given + /// key. + fn get(value: &Value, key: &Arc) -> Option { + match value { + Value::Compound(CompoundValue::Map(map)) => map + .elements() + .get(&Some(PrimitiveValue::String(key.clone()))) + .cloned(), + Value::Compound(CompoundValue::Object(Object { members, .. })) + | Value::Compound(CompoundValue::Struct(Struct { members, .. })) => { + members.get(key.as_str()).cloned() + } + _ => None, + } + } + + let mut value = context.arguments[0].value.clone(); + let keys = context + .coerce_argument(1, ANALYSIS_STDLIB.array_string_type()) + .unwrap_array(); + + for key in keys + .elements() + .iter() + .map(|v| v.as_string().expect("element should be a string")) + { + match get(&value, key) { + Some(v) => value = v, + None => return Ok(false.into()), + } + } + + Ok(true.into()) +} + +/// Gets the function describing `contains_key`. +pub const fn descriptor() -> Function { + Function::new( + const { + &[ + Signature::new( + "(Map[K, V], K) -> Boolean where `K`: any primitive type", + contains_key_map, + ), + Signature::new("(Object, String) -> Boolean", contains_key_object), + Signature::new( + "(Map[String, V], Array[String]) -> Boolean", + contains_key_recursive, + ), + Signature::new( + "(S, Array[String]) -> Boolean where `S`: any structure", + contains_key_recursive, + ), + Signature::new("(Object, Array[String]) -> Boolean", contains_key_recursive), + ] + }, + ) +} + +#[cfg(test)] +mod test { + use wdl_analysis::types::PrimitiveTypeKind; + use wdl_analysis::types::StructType; + use wdl_ast::version::V1; + + use crate::v1::test::TestEnv; + use crate::v1::test::eval_v1_expr; + + #[test] + fn contains_key() { + let mut env = TestEnv::default(); + + let bar_ty = env + .types_mut() + .add_struct(StructType::new("Bar", [("baz", PrimitiveTypeKind::String)])); + + env.insert_struct("Bar", bar_ty); + + let foo_ty = env + .types_mut() + .add_struct(StructType::new("Foo", [("bar", bar_ty)])); + + env.insert_struct("Foo", foo_ty); + + let value = eval_v1_expr(&mut env, V1::Two, "contains_key({}, 1)").unwrap(); + assert!(!value.unwrap_boolean()); + + let value = + eval_v1_expr(&mut env, V1::Two, "contains_key({ 1: 2, None: 3}, None)").unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr(&mut env, V1::Two, "contains_key({ 1: 2 }, 1)").unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key({ 'foo': 1, 'bar': 2, 'baz': 3 }, 'qux')", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key({ 'foo': 1, 'bar': 2, 'baz': 3 }, 'baz')", + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(object { foo: 1, bar: 2, baz: 3 }, 'qux')", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(object { foo: 1, bar: 2, baz: 3 }, 'baz')", + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key({ 'foo': 1, 'bar': 2, 'baz': 3 }, ['qux'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key({ 'foo': 1, 'bar': 2, 'baz': 3 }, ['baz'])", + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(object { foo: 1, bar: 2, baz: 3 }, ['qux'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(object { foo: 1, bar: 2, baz: 3 }, ['baz'])", + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(Foo { bar: Bar { baz: 'qux' } }, ['qux'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(Foo { bar: Bar { baz: 'qux' } }, ['bar'])", + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key({ 'foo': 1, 'bar': 2, 'baz': 3 }, ['qux', 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key({ 'foo': 1, 'bar': 2, 'baz': 3 }, ['baz', 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(object { foo: 1, bar: 2, baz: 3 }, ['qux', 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(object { foo: 1, bar: 2, baz: 3 }, ['baz', 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(Foo { bar: Bar { baz: 'qux' } }, ['qux', 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(Foo { bar: Bar { baz: 'qux' } }, ['bar', 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key({ 'foo': { 'qux': 1 }, 'bar': { 'qux': 2 }, 'baz': { 'qux': 3 } }, \ + ['baz', 'qux'])", + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key({ 'foo': { 'qux': 1 }, 'bar': { 'qux': 2 }, 'baz': { 'qux': 3 } }, \ + ['baz', 'qux', 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(object { foo: 1, bar: 2, baz: object { qux: 3 } }, ['baz', 'qux'])", + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(object { foo: 1, bar: 2, baz: object { qux: 3 } }, ['baz', 'qux', \ + 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(Foo { bar: Bar { baz: 'qux' } }, ['bar', 'baz'])", + ) + .unwrap(); + assert!(value.unwrap_boolean()); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "contains_key(Foo { bar: Bar { baz: 'qux' } }, ['bar', 'baz', 'nope'])", + ) + .unwrap(); + assert!(!value.unwrap_boolean()); + } +} diff --git a/wdl-engine/src/stdlib/keys.rs b/wdl-engine/src/stdlib/keys.rs new file mode 100644 index 00000000..f2f2ecd2 --- /dev/null +++ b/wdl-engine/src/stdlib/keys.rs @@ -0,0 +1,150 @@ +//! Implements the `keys` function from the WDL standard library. + +use std::sync::Arc; + +use wdl_ast::Diagnostic; + +use super::CallContext; +use super::Function; +use super::Signature; +use crate::Array; +use crate::CompoundValue; +use crate::Object; +use crate::PrimitiveValue; +use crate::Struct; +use crate::Value; + +/// Given a key-value type collection (Map, Struct, or Object), returns an Array +/// of the keys from the input collection, in the same order as the elements in +/// the collection. +/// +/// When the argument is a Struct, the returned array will contain the keys in +/// the same order they appear in the struct definition. +/// +/// When the argument is an Object, the returned array has no guaranteed order. +/// +/// When the input Map or Object is empty, an empty array is returned. +/// +/// https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#keys +fn keys(context: CallContext<'_>) -> Result { + debug_assert_eq!(context.arguments.len(), 1); + debug_assert!( + context + .types() + .type_definition( + context + .return_type + .as_compound() + .expect("type should be compound") + .definition() + ) + .as_array() + .is_some(), + "return type should be an array" + ); + + let elements = match &context.arguments[0].value { + Value::Compound(CompoundValue::Map(map)) => { + map.elements().keys().map(|k| k.clone().into()).collect() + } + Value::Compound(CompoundValue::Object(Object { members, .. })) + | Value::Compound(CompoundValue::Struct(Struct { members, .. })) => members + .keys() + .map(|k| PrimitiveValue::new_string(k).into()) + .collect(), + _ => unreachable!("expected a map, object, or struct"), + }; + + Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) +} + +/// Gets the function describing `keys`. +pub const fn descriptor() -> Function { + Function::new( + const { + &[ + Signature::new( + "(Map[K, V]) -> Array[K] where `K`: any primitive type", + keys, + ), + Signature::new("(S) -> Array[String] where `S`: any structure", keys), + Signature::new("(Object) -> Array[String]", keys), + ] + }, + ) +} + +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + use wdl_analysis::types::PrimitiveTypeKind; + use wdl_analysis::types::StructType; + use wdl_ast::version::V1; + + use crate::Value; + use crate::v1::test::TestEnv; + use crate::v1::test::eval_v1_expr; + + #[test] + fn keys() { + let mut env = TestEnv::default(); + + let ty = env.types_mut().add_struct(StructType::new("Foo", [ + ("foo", PrimitiveTypeKind::Float), + ("bar", PrimitiveTypeKind::String), + ("baz", PrimitiveTypeKind::Integer), + ])); + + env.insert_struct("Foo", ty); + + let value = eval_v1_expr(&mut env, V1::One, "keys({})").unwrap(); + assert_eq!(value.unwrap_array().len(), 0); + + let value = + eval_v1_expr(&mut env, V1::One, "keys({'foo': 1, 'bar': 2, 'baz': 3})").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| v.as_string().unwrap().as_str()) + .collect(); + assert_eq!(elements, ["foo", "bar", "baz"]); + + let value = eval_v1_expr(&mut env, V1::One, "keys({'foo': 1, None: 2, 'baz': 3})").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| match v { + Value::None => None, + Value::Primitive(v) => Some(v.as_string().unwrap().as_str()), + _ => unreachable!("expected an optional primitive value"), + }) + .collect(); + assert_eq!(elements, [Some("foo"), None, Some("baz")]); + + let value = + eval_v1_expr(&mut env, V1::Two, "keys(object { foo: 1, bar: 2, baz: 3})").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| v.as_string().unwrap().as_str()) + .collect(); + assert_eq!(elements, ["foo", "bar", "baz"]); + + let value = + eval_v1_expr(&mut env, V1::Two, "keys(Foo { foo: 1.0, bar: '2', baz: 3})").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| v.as_string().unwrap().as_str()) + .collect(); + assert_eq!(elements, ["foo", "bar", "baz"]); + } +} diff --git a/wdl-engine/src/stdlib/prefix.rs b/wdl-engine/src/stdlib/prefix.rs index 6430538e..cde7c985 100644 --- a/wdl-engine/src/stdlib/prefix.rs +++ b/wdl-engine/src/stdlib/prefix.rs @@ -36,6 +36,7 @@ fn prefix(context: CallContext<'_>) -> Result { .elements() .iter() .map(|v| match v { + Value::None => PrimitiveValue::String(prefix.clone()).into(), Value::Primitive(v) => { PrimitiveValue::new_string(format!("{prefix}{v}", v = v.raw())).into() } @@ -51,7 +52,7 @@ pub const fn descriptor() -> Function { Function::new( const { &[Signature::new( - "(String, Array[P]) -> Array[String] where `P`: any required primitive type", + "(String, Array[P]) -> Array[String] where `P`: any primitive type", prefix, )] }, @@ -100,6 +101,16 @@ mod test { .collect(); assert_eq!(elements, ["foobar", "foobaz", "fooqux"]); + let value = eval_v1_expr(&mut env, V1::Zero, "prefix('foo', [1, None, 3])").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| v.as_string().unwrap().as_str()) + .collect(); + assert_eq!(elements, ["foo1", "foo", "foo3"]); + let value = eval_v1_expr(&mut env, V1::One, "prefix('foo', [])").unwrap(); assert!(value.unwrap_array().is_empty()); } diff --git a/wdl-engine/src/stdlib/quote.rs b/wdl-engine/src/stdlib/quote.rs index 2febd5f1..c9ffd16f 100644 --- a/wdl-engine/src/stdlib/quote.rs +++ b/wdl-engine/src/stdlib/quote.rs @@ -31,6 +31,7 @@ fn quote(context: CallContext<'_>) -> Result { .elements() .iter() .map(|v| match v { + Value::None => PrimitiveValue::new_string("\"\"").into(), Value::Primitive(v) => { PrimitiveValue::new_string(format!("\"{v}\"", v = v.raw())).into() } @@ -46,7 +47,7 @@ pub const fn descriptor() -> Function { Function::new( const { &[Signature::new( - "(Array[P]) -> Array[String] where `P`: any required primitive type", + "(Array[P]) -> Array[String] where `P`: any primitive type", quote, )] }, @@ -94,6 +95,16 @@ mod test { .collect(); assert_eq!(elements, [r#""bar""#, r#""baz""#, r#""qux""#]); + let value = eval_v1_expr(&mut env, V1::One, "quote(['bar', None, 'qux'])").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| v.as_string().unwrap().as_str()) + .collect(); + assert_eq!(elements, [r#""bar""#, r#""""#, r#""qux""#]); + let value = eval_v1_expr(&mut env, V1::One, "quote([])").unwrap(); assert!(value.unwrap_array().is_empty()); } diff --git a/wdl-engine/src/stdlib/read_map.rs b/wdl-engine/src/stdlib/read_map.rs index 52386c06..b48b140c 100644 --- a/wdl-engine/src/stdlib/read_map.rs +++ b/wdl-engine/src/stdlib/read_map.rs @@ -47,7 +47,7 @@ fn read_map(context: CallContext<'_>) -> Result { .with_context(|| format!("failed to open file `{path}`", path = path.display())) .map_err(|e| function_call_failed("read_map", format!("{e:?}"), context.call_site))?; - let mut map: IndexMap = IndexMap::new(); + let mut map: IndexMap, Value> = IndexMap::new(); for (i, line) in BufReader::new(file).lines().enumerate() { let line = line .with_context(|| format!("failed to read file `{path}`", path = path.display())) @@ -70,7 +70,7 @@ fn read_map(context: CallContext<'_>) -> Result { if map .insert( - PrimitiveValue::new_string(key), + Some(PrimitiveValue::new_string(key)), PrimitiveValue::new_string(value).into(), ) .is_some() diff --git a/wdl-engine/src/stdlib/sep.rs b/wdl-engine/src/stdlib/sep.rs index 8b6cbbad..8dadac11 100644 --- a/wdl-engine/src/stdlib/sep.rs +++ b/wdl-engine/src/stdlib/sep.rs @@ -45,6 +45,7 @@ fn sep(context: CallContext<'_>) -> Result { } match v { + Value::None => {} Value::Primitive(v) => { write!(&mut s, "{v}", v = v.raw()).expect("failed to write to a string") } @@ -62,7 +63,7 @@ pub const fn descriptor() -> Function { Function::new( const { &[Signature::new( - "(String, Array[P]) -> String where `P`: any required primitive type", + "(String, Array[P]) -> String where `P`: any primitive type", sep, )] }, @@ -94,6 +95,9 @@ mod test { let value = eval_v1_expr(&mut env, V1::One, "sep(' ', ['a', 'b', 'c'])").unwrap(); assert_eq!(value.unwrap_string().as_str(), "a b c"); + let value = eval_v1_expr(&mut env, V1::One, "sep(' ', ['a', None, 'c'])").unwrap(); + assert_eq!(value.unwrap_string().as_str(), "a c"); + let value = eval_v1_expr(&mut env, V1::One, "sep(',', [1])").unwrap(); assert_eq!(value.unwrap_string().as_str(), "1"); diff --git a/wdl-engine/src/stdlib/size.rs b/wdl-engine/src/stdlib/size.rs index 23f31ef1..3be75862 100644 --- a/wdl-engine/src/stdlib/size.rs +++ b/wdl-engine/src/stdlib/size.rs @@ -116,7 +116,12 @@ fn compound_disk_size(value: &CompoundValue, unit: StorageUnit, cwd: &Path) -> R anyhow::Ok(t + calculate_disk_size(e, unit, cwd)?) })?), CompoundValue::Map(map) => Ok(map.elements().iter().try_fold(0.0, |t, (k, v)| { - anyhow::Ok(t + primitive_disk_size(k, unit, cwd)? + calculate_disk_size(v, unit, cwd)?) + anyhow::Ok( + t + match k { + Some(k) => primitive_disk_size(k, unit, cwd)?, + None => 0.0, + } + calculate_disk_size(v, unit, cwd)?, + ) })?), CompoundValue::Object(object) => { Ok(object.members().iter().try_fold(0.0, |t, (_, v)| { diff --git a/wdl-engine/src/stdlib/squote.rs b/wdl-engine/src/stdlib/squote.rs index 246ff0b5..07c8fa74 100644 --- a/wdl-engine/src/stdlib/squote.rs +++ b/wdl-engine/src/stdlib/squote.rs @@ -31,6 +31,7 @@ fn squote(context: CallContext<'_>) -> Result { .elements() .iter() .map(|v| match v { + Value::None => PrimitiveValue::new_string("''").into(), Value::Primitive(v) => PrimitiveValue::new_string(format!("'{v}'", v = v.raw())).into(), _ => panic!("expected an array of primitive values"), }) @@ -44,7 +45,7 @@ pub const fn descriptor() -> Function { Function::new( const { &[Signature::new( - "(Array[P]) -> Array[String] where `P`: any required primitive type", + "(Array[P]) -> Array[String] where `P`: any primitive type", squote, )] }, @@ -92,6 +93,16 @@ mod test { .collect(); assert_eq!(elements, ["'bar'", "'baz'", "'qux'"]); + let value = eval_v1_expr(&mut env, V1::One, "squote(['bar', None, 'qux'])").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| v.as_string().unwrap().as_str()) + .collect(); + assert_eq!(elements, ["'bar'", "''", "'qux'"]); + let value = eval_v1_expr(&mut env, V1::One, "squote([])").unwrap(); assert!(value.unwrap_array().is_empty()); } diff --git a/wdl-engine/src/stdlib/suffix.rs b/wdl-engine/src/stdlib/suffix.rs index 8e55beb9..46b0ee54 100644 --- a/wdl-engine/src/stdlib/suffix.rs +++ b/wdl-engine/src/stdlib/suffix.rs @@ -36,6 +36,7 @@ fn suffix(context: CallContext<'_>) -> Result { .elements() .iter() .map(|v| match v { + Value::None => PrimitiveValue::String(suffix.clone()).into(), Value::Primitive(v) => { PrimitiveValue::new_string(format!("{v}{suffix}", v = v.raw())).into() } @@ -51,7 +52,7 @@ pub const fn descriptor() -> Function { Function::new( const { &[Signature::new( - "(String, Array[P]) -> Array[String] where `P`: any required primitive type", + "(String, Array[P]) -> Array[String] where `P`: any primitive type", suffix, )] }, @@ -100,6 +101,16 @@ mod test { .collect(); assert_eq!(elements, ["barfoo", "bazfoo", "quxfoo"]); + let value = eval_v1_expr(&mut env, V1::One, "suffix('foo', ['bar', None, 'qux'])").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| v.as_string().unwrap().as_str()) + .collect(); + assert_eq!(elements, ["barfoo", "foo", "quxfoo"]); + let value = eval_v1_expr(&mut env, V1::One, "suffix('foo', [])").unwrap(); assert!(value.unwrap_array().is_empty()); } diff --git a/wdl-engine/src/stdlib/values.rs b/wdl-engine/src/stdlib/values.rs new file mode 100644 index 00000000..e18958ea --- /dev/null +++ b/wdl-engine/src/stdlib/values.rs @@ -0,0 +1,115 @@ +//! Implements the `values` function from the WDL standard library. + +use std::sync::Arc; + +use wdl_ast::Diagnostic; + +use super::CallContext; +use super::Function; +use super::Signature; +use crate::Array; +use crate::Value; + +/// Returns an Array of the values from the input Map, in the same order as the +/// elements in the map. +/// +/// If the map is empty, an empty array is returned. +/// +/// https://github.com/openwdl/wdl/blob/wdl-1.2/SPEC.md#-values +fn values(context: CallContext<'_>) -> Result { + debug_assert_eq!(context.arguments.len(), 1); + debug_assert!( + context + .types() + .type_definition( + context + .return_type + .as_compound() + .expect("type should be compound") + .definition() + ) + .as_array() + .is_some(), + "return type should be an array" + ); + + let elements = context.arguments[0] + .value + .as_map() + .expect("value should be a map") + .elements() + .values() + .cloned() + .collect(); + Ok(Array::new_unchecked(context.return_type, Arc::new(elements)).into()) +} + +/// Gets the function describing `values`. +pub const fn descriptor() -> Function { + Function::new( + const { + &[Signature::new( + "(Map[K, V]) -> Array[V] where `K`: any primitive type", + values, + )] + }, + ) +} + +#[cfg(test)] +mod test { + use pretty_assertions::assert_eq; + use wdl_analysis::types::PrimitiveTypeKind; + use wdl_analysis::types::StructType; + use wdl_ast::version::V1; + + use crate::Value; + use crate::v1::test::TestEnv; + use crate::v1::test::eval_v1_expr; + + #[test] + fn values() { + let mut env = TestEnv::default(); + + let ty = env.types_mut().add_struct(StructType::new("Foo", [ + ("foo", PrimitiveTypeKind::Float), + ("bar", PrimitiveTypeKind::String), + ("baz", PrimitiveTypeKind::Integer), + ])); + + env.insert_struct("Foo", ty); + + let value = eval_v1_expr(&mut env, V1::Two, "values({})").unwrap(); + assert_eq!(value.unwrap_array().len(), 0); + + let value = + eval_v1_expr(&mut env, V1::Two, "values({'foo': 1, 'bar': 2, 'baz': 3})").unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| v.as_integer().unwrap()) + .collect(); + assert_eq!(elements, [1, 2, 3]); + + let value = eval_v1_expr( + &mut env, + V1::Two, + "values({'foo': 1, 'bar': None, 'baz': 3})", + ) + .unwrap(); + let elements: Vec<_> = value + .as_array() + .unwrap() + .elements() + .iter() + .map(|v| match v { + Value::None => None, + Value::Primitive(v) => Some(v.as_integer().unwrap()), + _ => unreachable!("expected an optional primitive value"), + }) + .collect(); + assert_eq!(elements, [Some(1), None, Some(3)]); + } +} diff --git a/wdl-engine/src/stdlib/write_map.rs b/wdl-engine/src/stdlib/write_map.rs index 4edbb7ef..d9aa70ae 100644 --- a/wdl-engine/src/stdlib/write_map.rs +++ b/wdl-engine/src/stdlib/write_map.rs @@ -59,7 +59,11 @@ fn write_map(context: CallContext<'_>) -> Result { writeln!( &mut writer, "{key}\t{value}", - key = key.as_string().unwrap(), + key = key + .as_ref() + .expect("key should not be optional") + .as_string() + .unwrap(), value = value.as_string().unwrap() ) .map_err(write_error)?; diff --git a/wdl-engine/src/stdlib/write_object.rs b/wdl-engine/src/stdlib/write_object.rs index 810300f7..95f31f17 100644 --- a/wdl-engine/src/stdlib/write_object.rs +++ b/wdl-engine/src/stdlib/write_object.rs @@ -72,6 +72,7 @@ fn write_object(context: CallContext<'_>) -> Result { } match value { + Value::None => {} Value::Primitive(v) => { if !write_tsv_value(&mut writer, v).map_err(write_error)? { return Err(function_call_failed( @@ -213,6 +214,25 @@ mod test { "foo\tbar\tbaz\n1\tfoo\ttrue\n", ); + let value = eval_v1_expr( + &mut env, + V1::Two, + "write_object(object { foo: 1, bar: None, baz: true })", + ) + .unwrap(); + assert!( + value + .as_file() + .expect("should be file") + .as_str() + .starts_with(env.tmp().to_str().expect("should be UTF-8")), + "file should be in temp directory" + ); + assert_eq!( + fs::read_to_string(value.unwrap_file().as_str()).expect("failed to read file"), + "foo\tbar\tbaz\n1\t\ttrue\n", + ); + let diagnostic = eval_v1_expr(&mut env, V1::Two, "write_object(object { foo: [] })").unwrap_err(); assert_eq!( diff --git a/wdl-engine/src/stdlib/write_objects.rs b/wdl-engine/src/stdlib/write_objects.rs index d0d5042b..c4b0eb73 100644 --- a/wdl-engine/src/stdlib/write_objects.rs +++ b/wdl-engine/src/stdlib/write_objects.rs @@ -137,6 +137,7 @@ fn write_objects(context: CallContext<'_>) -> Result { } match v { + Value::None => {} Value::Primitive(v) => { if !write_tsv_value(&mut writer, v).map_err(write_error)? { return Err(function_call_failed( @@ -261,6 +262,26 @@ mod test { "foo\tbar\tbaz\nbar\t1\t3.5\nfoo\t101\t1234\n", ); + let value = eval_v1_expr( + &mut env, + V1::Two, + "write_objects([object { foo: 'bar', bar: 1, baz: 3.5 }, object { foo: 'foo', bar: \ + None, baz: 1234 }, ])", + ) + .unwrap(); + assert!( + value + .as_file() + .expect("should be file") + .as_str() + .starts_with(env.tmp().to_str().expect("should be UTF-8")), + "file should be in temp directory" + ); + assert_eq!( + fs::read_to_string(value.unwrap_file().as_str()).expect("failed to read file"), + "foo\tbar\tbaz\nbar\t1\t3.5\nfoo\t\t1234\n", + ); + let value = eval_v1_expr( &mut env, V1::Two, diff --git a/wdl-engine/src/stdlib/write_tsv.rs b/wdl-engine/src/stdlib/write_tsv.rs index 1a7b4b30..1f469847 100644 --- a/wdl-engine/src/stdlib/write_tsv.rs +++ b/wdl-engine/src/stdlib/write_tsv.rs @@ -334,6 +334,7 @@ fn write_tsv_struct(context: CallContext<'_>) -> Result { } match column { + Value::None => {} Value::Primitive(v) => { if !write_tsv_value(&mut writer, v).map_err(write_error)? { return Err(function_call_failed( @@ -403,6 +404,8 @@ mod test { use std::fs; use pretty_assertions::assert_eq; + use wdl_analysis::types::Optional; + use wdl_analysis::types::PrimitiveType; use wdl_analysis::types::PrimitiveTypeKind; use wdl_analysis::types::StructType; use wdl_ast::version::V1; @@ -415,9 +418,12 @@ mod test { let mut env = TestEnv::default(); let ty = env.types_mut().add_struct(StructType::new("Foo", [ - ("foo", PrimitiveTypeKind::Integer), - ("bar", PrimitiveTypeKind::String), - ("baz", PrimitiveTypeKind::Boolean), + ("foo", PrimitiveType::from(PrimitiveTypeKind::Integer)), + ("bar", PrimitiveType::from(PrimitiveTypeKind::String)), + ( + "baz", + PrimitiveType::from(PrimitiveTypeKind::Boolean).optional(), + ), ])); env.insert_struct("Foo", ty); @@ -530,8 +536,8 @@ mod test { let value = eval_v1_expr( &mut env, V1::Two, - "write_tsv([Foo { foo: 1, bar: 'hi', baz: true }, Foo { foo: 1234, bar: 'there', baz: \ - false }], false)", + "write_tsv([Foo { foo: 1, bar: 'hi', baz: false }, Foo { foo: 1234, bar: 'there' }], \ + false)", ) .unwrap(); assert!( @@ -544,7 +550,7 @@ mod test { ); assert_eq!( fs::read_to_string(value.unwrap_file().as_str()).expect("failed to read file"), - "1\thi\ttrue\n1234\tthere\tfalse\n", + "1\thi\tfalse\n1234\tthere\t\n", ); let value = eval_v1_expr( @@ -570,8 +576,8 @@ mod test { let value = eval_v1_expr( &mut env, V1::Two, - "write_tsv([Foo { foo: 1, bar: 'hi', baz: true }, Foo { foo: 1234, bar: 'there', baz: \ - false }], true, ['qux', 'jam', 'cakes'])", + "write_tsv([Foo { foo: 1, bar: 'hi' }, Foo { foo: 1234, bar: 'there', baz: false }], \ + true, ['qux', 'jam', 'cakes'])", ) .unwrap(); assert!( @@ -584,7 +590,7 @@ mod test { ); assert_eq!( fs::read_to_string(value.unwrap_file().as_str()).expect("failed to read file"), - "qux\tjam\tcakes\n1\thi\ttrue\n1234\tthere\tfalse\n", + "qux\tjam\tcakes\n1\thi\t\n1234\tthere\tfalse\n", ); let diagnostic = eval_v1_expr( diff --git a/wdl-engine/src/value.rs b/wdl-engine/src/value.rs index d9cab6a4..180c64e1 100644 --- a/wdl-engine/src/value.rs +++ b/wdl-engine/src/value.rs @@ -513,7 +513,7 @@ impl fmt::Display for Value { impl Coercible for Value { fn coerce(&self, types: &Types, target: Type) -> Result { - if self.ty().type_eq(types, &target) { + if target.is_union() || target.is_none() || self.ty().type_eq(types, &target) { return Ok(self.clone()); } @@ -558,6 +558,15 @@ impl From for Value { } } +impl From> for Value { + fn from(value: Option) -> Self { + match value { + Some(v) => v.into(), + None => Self::None, + } + } +} + impl From for Value { fn from(value: Pair) -> Self { Self::Compound(value.into()) @@ -895,7 +904,7 @@ impl From for PrimitiveValue { impl Coercible for PrimitiveValue { fn coerce(&self, types: &Types, target: Type) -> Result { - if self.ty().type_eq(types, &target) { + if target.is_union() || target.is_none() || self.ty().type_eq(types, &target) { return Ok(self.clone()); } @@ -1194,13 +1203,13 @@ pub struct Map { /// The type of the map value. ty: Type, /// The elements of the map value. - elements: Arc>, + elements: Arc, Value>>, } impl Map { /// Creates a new `Map` value. /// - /// Returns an error if an key or value did not coerce to the map's key or + /// Returns an error if a key or value did not coerce to the map's key or /// value type, respectively. /// /// # Panics @@ -1213,7 +1222,7 @@ impl Map { elements: impl IntoIterator, ) -> Result where - K: Into, + K: Into, V: Into, { if let Type::Compound(compound_ty) = ty { @@ -1231,9 +1240,21 @@ impl Map { let k = k.into(); let v = v.into(); Ok(( - k.coerce(types, key_type).with_context(|| { - format!("failed to coerce map key for element at index {i}") - })?, + if k.is_none() { + None + } else { + match k.coerce(types, key_type).with_context(|| { + format!( + "failed to coerce map key for element at index {i}" + ) + })? { + Value::None => None, + Value::Primitive(v) => Some(v), + Value::Compound(_) => { + bail!("not all key values are primitive") + } + } + }, v.coerce(types, value_type).with_context(|| { format!( "failed to coerce map value for element at index {i}" @@ -1252,7 +1273,10 @@ impl Map { /// Constructs a new map without checking the given elements conform to the /// given type. - pub(crate) fn new_unchecked(ty: Type, elements: Arc>) -> Self { + pub(crate) fn new_unchecked( + ty: Type, + elements: Arc, Value>>, + ) -> Self { Self { ty, elements } } @@ -1262,9 +1286,19 @@ impl Map { } /// Gets the elements of the `Map` value. - pub fn elements(&self) -> &IndexMap { + pub fn elements(&self) -> &IndexMap, Value> { &self.elements } + + /// Returns the number of elements in the map. + pub fn len(&self) -> usize { + self.elements.len() + } + + /// Returns `true` if the map has no elements. + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } } impl fmt::Display for Map { @@ -1276,7 +1310,10 @@ impl fmt::Display for Map { write!(f, ", ")?; } - write!(f, "{k}: {v}")?; + match k { + Some(k) => write!(f, "{k}: {v}")?, + None => write!(f, "None: {v}")?, + } } write!(f, "}}") @@ -1733,7 +1770,7 @@ impl fmt::Display for CompoundValue { impl Coercible for CompoundValue { fn coerce(&self, types: &Types, target: Type) -> Result { - if self.ty().type_eq(types, &target) { + if target.is_union() || target.is_none() || self.ty().type_eq(types, &target) { return Ok(self.clone()); } @@ -1761,7 +1798,9 @@ impl Coercible for CompoundValue { return Ok(Self::Map(Map::new( types, target, - v.elements.iter().map(|(k, v)| (k.clone(), v.clone())), + v.elements.iter().map(|(k, v)| { + (k.clone().map(Into::into).unwrap_or(Value::None), v.clone()) + }), )?)); } // Pair[W, Y] -> Pair[X, Z] where W -> X and Y -> Z @@ -1796,7 +1835,8 @@ impl Coercible for CompoundValue { .iter() .map(|(k, v)| { let k: String = k - .as_string() + .as_ref() + .and_then(|k| k.as_string()) .ok_or_else(|| { anyhow!( "cannot coerce a map with a non-string key type \ @@ -1847,7 +1887,7 @@ impl Coercible for CompoundValue { let v = v.coerce(types, value_ty).with_context(|| { format!("failed to coerce member `{n}`") })?; - Ok((PrimitiveValue::new_string(n), v)) + Ok((PrimitiveValue::new_string(n).into(), v)) }) .collect::>()?, ), @@ -1949,7 +1989,8 @@ impl Coercible for CompoundValue { .iter() .map(|(k, v)| { let k = k - .as_string() + .as_ref() + .and_then(|k| k.as_string()) .ok_or_else(|| { anyhow!( "cannot coerce a map with a non-string key type \