Operators

An expression is a compound statement that consists of literal values, identifiers (references to declarations or call outputs), built-in operators (e.g., + or >=), and calls to standard library functions.

A "literal" expression is one that consists only of a literal value. For example, "foo" is a literal String expression and [1, 2, 3] is a literal Array[Int] expression.

A "simple" expression is one that can be evaluated unambiguously without any knowledge of the runtime context. Literal expressions, operations on literals (e.g., 1 + 2), and function calls with literal arguments (excluding any functions that read or create Files) are all simple expressions. A simple expression cannot refer to any declarations (i.e., it cannot contain identifiers). An execution engine may choose to replace a simple expression with its literal value during static analysis.

Example: expressions_task.wdl

version 1.3

task expressions {
  input {
    Int x
  }

  command <<<
  printf "hello" > hello.txt
  >>>

  output {
    # simple expressions
    Float f = 1 + 2.2
    Boolean b = if 1 > 2 then true else false
    Map[String, Int] m = as_map(zip(["a", "b", "c"], [1, 2, 3]))

    # non-simple expressions
    Int i = x + 3  # requires knowing the value of x
    # requires reading a file that might only exist at runtime
    String s = read_string("hello.txt")
  }
}

Example input:

{
  "expressions.x": 5
}

Example output:

{
  "expressions.f": 3.2,
  "expressions.b": false,
  "expressions.m": {
    "a": 1,
    "b": 2,
    "c": 3
  },
  "expressions.i": 8,
  "expressions.s": "hello"
}

ยงBuilt-in Operators

WDL provides the standard unary and binary mathematical and logical operators. The following tables list the valid operand and result type combinations for each operator. Using an operator with unsupported types results in an error.

In operations on mismatched numeric types (e.g., Int + Float), the Int is first is coerced to Float, and the result type is Float. This may result in loss of precision, for example if the Int is too large to be represented exactly by a Float. A Float can be converted to Int with the ceil, round, or floor functions.

ยงUnary Operators

OperatorRHS TypeResult
-FloatFloat
-IntInt
!BooleanBoolean

ยงBinary Operators on Primitive Types

LHS TypeOperatorRHS TypeResultSemantics
Boolean==BooleanBoolean
Boolean!=BooleanBoolean
Boolean||BooleanBoolean
Boolean&&BooleanBoolean
๐Ÿ—‘ Boolean>BooleanBooleantrue is greater than false
๐Ÿ—‘ Boolean>=BooleanBooleantrue is greater than false
๐Ÿ—‘ Boolean<BooleanBooleantrue is greater than false
๐Ÿ—‘ Boolean<=BooleanBooleantrue is greater than false
Int+IntInt
Int-IntInt
Int*IntInt
Int/IntIntInteger division
Int**IntIntInteger exponentiation
Int%IntIntInteger division, return remainder
Int==IntBoolean
Int!=IntBoolean
Int>IntBoolean
Int>=IntBoolean
Int<IntBoolean
Int<=IntBoolean
๐Ÿ—‘ Int+StringString
Int+FloatFloat
Int-FloatFloat
Int*FloatFloat
Int/FloatFloat
Int**FloatFloat
Int==FloatBoolean
Int!=FloatBoolean
Int>FloatBoolean
Int>=FloatBoolean
Int<FloatBoolean
Int<=FloatBoolean
Float+FloatFloat
Float-FloatFloat
Float*FloatFloat
Float/FloatFloat
Float**FloatFloat
Float%FloatFloat
Float==FloatBoolean
Float!=FloatBoolean
Float>FloatBoolean
Float>=FloatBoolean
Float<FloatBoolean
Float<=FloatBoolean
๐Ÿ—‘ Float+StringString
Float+IntFloat
Float-IntFloat
Float*IntFloat
Float/IntFloat
Float**IntFloat
Float%IntFloat
Float==IntBoolean
Float!=IntBoolean
Float>IntBoolean
Float>=IntBoolean
Float<IntBoolean
Float<=IntBoolean
String+StringStringConcatenation
String+FileFile
String==StringBooleanUnicode comparison
String!=StringBooleanUnicode comparison
String>StringBooleanUnicode comparison
String>=StringBooleanUnicode comparison
String<StringBooleanUnicode comparison
String<=StringBooleanUnicode comparison
๐Ÿ—‘ String+IntString
๐Ÿ—‘ String+FloatString
File==FileBoolean
File!=FileBoolean
File==StringBoolean
File!=StringBoolean
๐Ÿ—‘ File+FileFileappend file paths - error if second path is not relative
๐Ÿ—‘ File+StringFileappend file paths - error if second path is not relative
Directory==DirectoryBoolean
Directory!=DirectoryBoolean
Directory==StringBoolean
Directory!=StringBoolean

Boolean operator evaluation is minimal (or "short-circuiting"), meaning that:

  1. For A && B, if A evalutes to false then B is not evaluated
  2. For A || B, if A evaluates to true then B is not evaluated.

WDL Strings are compared by the unicode values of their corresponding characters. Character a is less than character b if it has a lower unicode value.

File and Directory values are canonicalized when the value is created. Two File or Directory values that refer to the same underlying resource are considered equal, even if they were initialized from different string representations. For example, /home/user/file.txt and /home/user/../user/file.txt refer to the same file and compare as equal. Similarly, for Directory values, trailing slashes are ignored when determining equality (e.g., /home/user/dir and /home/user/dir/ are equal). Equality relationships between File and Directory values are preserved throughout workflow execution, including before and after task input localization.

When comparing a File or Directory to a String, the String is first coerced to File or Directory (and thus canonicalized) before the comparison is performed.

Example: file_directory_equality.wdl

version 1.3

task check_equality {
  input {
    File file_a
    File file_b
    Directory dir_a
    Directory dir_b
  }

  command <<<
  # The execution engine localizes equal files once
  # so file_a and file_b will have the same path
  if [ "~{file_a}" = "~{file_b}" ]; then
    echo "true" > files_equal.txt
  else
    echo "false" > files_equal.txt
  fi

  if [ "~{dir_a}" = "~{dir_b}" ]; then
    echo "true" > dirs_equal.txt
  else
    echo "false" > dirs_equal.txt
  fi
  >>>

  output {
    Boolean task_files_equal = read_boolean("files_equal.txt")
    Boolean task_dirs_equal = read_boolean("dirs_equal.txt")
  }
}

workflow file_directory_equality {
  input {
    File file_a
    File file_b
    Directory dir_a
    Directory dir_b
  }

  # After canonicalization, these compare as equal
  Boolean files_eq = file_a == file_b
  Boolean dirs_eq = dir_a == dir_b

  call check_equality {
    file_a = file_a,
    file_b = file_b,
    dir_a = dir_a,
    dir_b = dir_b
  }

  output {
    Boolean workflow_files_equal = files_eq
    Boolean workflow_dirs_equal = dirs_eq
    Boolean task_files_equal = check_equality.task_files_equal
    Boolean task_dirs_equal = check_equality.task_dirs_equal
  }
}

Example input:

{
  "file_directory_equality.file_a": "data/hello.txt",
  "file_directory_equality.file_b": "data/../data/hello.txt",
  "file_directory_equality.dir_a": "data/testdir/",
  "file_directory_equality.dir_b": "data/testdir"
}

Example output:

{
  "file_directory_equality.workflow_files_equal": true,
  "file_directory_equality.workflow_dirs_equal": true,
  "file_directory_equality.task_files_equal": true,
  "file_directory_equality.task_dirs_equal": true
}

In this example, file_a and file_b use different string representations (tests/data/hello.txt vs tests/data/../data/hello.txt) but both canonicalize to the same path and compare as equal at workflow scope. When passed to the task, the execution engine localizes the file once, and both file_a and file_b in the task reference the same localized path. Similarly, dir_a includes a trailing slash while dir_b does not, but they canonicalize to the same directory and are localized once.

Except for String + File, all concatenations between String and non-String types are deprecated and will be removed in WDL 2.0. The same effect can be achieved using string interpolation.

ยงEquality of Compound Types

LHS TypeOperatorRHS TypeResult
Array==ArrayBoolean
Array!=ArrayBoolean
Map==MapBoolean
Map!=MapBoolean
Pair==PairBoolean
Pair!=PairBoolean
Struct==StructBoolean
Struct!=StructBoolean
Object==ObjectBoolean
Object!=ObjectBoolean

In general, two compound values are equal if-and-only-if all of the following are true:

  1. They are of the same type.
  2. They are the same length.
  3. All of their contained elements are equal.

Since Arrays and Maps are ordered, the order of their elements are also compared. For example:

Example: array_map_equality.wdl

version 1.3

workflow array_map_equality {
  output {
    # arrays and maps with the same elements in the same order are equal
    Boolean is_true1 = [1, 2, 3] == [1, 2, 3]
    Boolean is_true2 = {"a": 1, "b": 2} == {"a": 1, "b": 2}

    # arrays and maps with the same elements in different orders are not equal
    Boolean is_false1 = [1, 2, 3] == [2, 1, 3]
    Boolean is_false2 = {"a": 1, "b": 2} == {"b": 2, "a": 1}
  }
}

Example input:

{}

Example output:

{
  "array_map_equality.is_true1": true,
  "array_map_equality.is_true2": true,
  "array_map_equality.is_false1": false,
  "array_map_equality.is_false2": false
}

Type coercion can be employed to compare values of different but compatible types.

Example: compare_coerced.wdl

version 1.3

workflow compare_coerced {
  Array[Int] i = [1, 2, 3]
  Array[Float] f1 = i
  Array[Float] f2 = [1.0, 2.0, 3.0]

  output {
    # Ints are automatically coerced to Floats for comparison
    Boolean is_true = f1 == f2
  }
}

Example input:

{}

Example output:

{
  "compare_coerced.is_true": true
}

ยงEquality and Inequality Comparison of Optional Types

The equality and inequality operators are exceptions to the general rules on coercion of optional types. Either or both operands of an equality or inequality comparison can be optional, considering that None is equal to itself but no other value.

Example: compare_optionals.wdl

version 1.3

workflow compare_optionals {
  Int i = 1
  Int? j = 1
  Int? k = None

  output {
    # equal values of the same type are equal even if one is optional
    Boolean is_true1 = i == j
    # k is undefined (None), and so is only equal to None
    Boolean is_true2 = k == None
    # these comparisons are valid and evaluate to false
    Boolean is_false1 = i == k
    Boolean is_false2 = j == k
  }
}

Example input:

{}

Example output:

{
  "compare_optionals.is_true1": true,
  "compare_optionals.is_true2": true,
  "compare_optionals.is_false1": false,
  "compare_optionals.is_false2": false
}