From 28286a9490a234ca37ed6c826950b9a7ef539972 Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Sun, 26 Jan 2025 08:42:16 -0600 Subject: [PATCH 1/4] feat: adds `else if` and `else` conditional clauses --- SPEC.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/SPEC.md b/SPEC.md index ff90b7e5..1c29f8e5 100644 --- a/SPEC.md +++ b/SPEC.md @@ -7146,11 +7146,11 @@ Example output: ### Conditional Statement -A conditional statement consists of the `if` keyword, followed by a `Boolean` expression and a body of (potentially nested) statements. The conditional body is only evaluated if the conditional expression evaluates to `true`. +A conditional statement consists of one or more conditional clauses. Each conditional clause is comprised of an expression that evaluates to a `Boolean` and an associated clause body. When a conditional statement is executed, each of the conditional clauses is evaluated sequentially: (a) the expression for that clause is evaluated and, if the returned value is `true`, the body of that clause is executed and the entire conditional statement suspends further execution. The simplest conditional statement contains a single "if" clause, which is the `if` keyword, followed by the clause's boolean expression, and, finally, the clause body of (potentially nested) statements wrapped in curly brackets. After evaluation of the conditional has completed, each declaration or call output in the conditional body is exposed in the enclosing context as an optional declaration. In other words, for a declaration or call output `T ` within a conditional body, a declaration `T? ` is implicitly available outside of the conditional body. If the expression evaluated to `true`, and thus the body of the conditional was evaluated, then the value of each exposed declaration is the same as its original value inside the conditional body. If the expression evaluated to `false` and thus the body of the conditional was not evaluated, then the value of each exposed declaration is `None`. -The scoping rules for conditionals are similar to those for scatters - declarations or call outputs inside a conditional body are accessible within that conditional and any nested statements. +The scoping rules for conditionals are similar to those for scatters—declarations or call outputs inside a conditional body are accessible within that conditional and any nested statements. In the example below, `Int j` is accessible anywhere in the conditional body, and `Int? j` is an optional that is accessible outside of the conditional anywhere in `workflow test_conditional`. @@ -7231,7 +7231,9 @@ Example output:

-WDL has no `else` keyword. To mimic an `if-else` statement, you would simply use two conditionals with inverted boolean expressions. A common idiom is to use `select_first` to select a value from either the `if` or the `if not` body, whichever one is defined. +After the initial `if` clause, conditional statements may have any number of `else if` clauses and a single, final `else` clause. As described in the paragraph above, `else if` clauses are only evaluated if all prior clauses in the statement have evaluated to `false`. If present, the final `else` clause executes only if all prior clauses evaluated to `false`. + +When gathering results from conditionals, a common idiom is to use `select_first` to select the defined value from the `if`, `else if`, or `else` body that was evaluated.
@@ -7246,7 +7248,7 @@ task greet { } command <<< - printf "Good ~{time} buddy!" + printf "Good ~{time} buddy!" >>> output { @@ -7259,13 +7261,11 @@ workflow if_else { Boolean is_morning = false } - # the body *is not* evaluated since 'b' is false if (is_morning) { + # The body *is not* evaluated since `is_morning` is `false`. call greet as morning { time = "morning" } - } - - # the body *is* evaluated since !b is true - if (!is_morning) { + } else { + # The body *is* evaluated since the clause above did not trigger. call greet as afternoon { time = "afternoon" } } From fe767c4c085ec36b278ef60570320c1a4db12340 Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Sun, 5 Oct 2025 12:44:50 -0500 Subject: [PATCH 2/4] revise: updates based on implementation --- SPEC.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index 1c29f8e5..70a8fdc3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -7146,9 +7146,53 @@ Example output: ### Conditional Statement -A conditional statement consists of one or more conditional clauses. Each conditional clause is comprised of an expression that evaluates to a `Boolean` and an associated clause body. When a conditional statement is executed, each of the conditional clauses is evaluated sequentially: (a) the expression for that clause is evaluated and, if the returned value is `true`, the body of that clause is executed and the entire conditional statement suspends further execution. The simplest conditional statement contains a single "if" clause, which is the `if` keyword, followed by the clause's boolean expression, and, finally, the clause body of (potentially nested) statements wrapped in curly brackets. +A conditional statement consists of one or more conditional clauses, each with an associated body. The types of conditional statement clauses are: -After evaluation of the conditional has completed, each declaration or call output in the conditional body is exposed in the enclosing context as an optional declaration. In other words, for a declaration or call output `T ` within a conditional body, a declaration `T? ` is implicitly available outside of the conditional body. If the expression evaluated to `true`, and thus the body of the conditional was evaluated, then the value of each exposed declaration is the same as its original value inside the conditional body. If the expression evaluated to `false` and thus the body of the conditional was not evaluated, then the value of each exposed declaration is `None`. +* A required `if` clause with an associated expression that evaluates to a + `Boolean`. The `if` clause must be first in the conditional expression. +* Zero or more `else if` clauses, each with an associated expression that + evaluates to a `Boolean`. If present, `else if` clauses must follow the `if` + clause and be before the optional `else` clause. +* At most, one `else` clause with no associated expression. The `else` clause + must be last in the conditional expression. + +When a conditional statement is evaluated, each conditional clause is evaluated sequentially; for each `if` and `else if` clause, the expression is evaluated—if the result of the evaluation is `true`, the body of that clause is evaluated and the entire conditional statement suspends further evaluation. If none of the `if` or `else if` clauses execute and we reach the final `else` clause, then the `else` clause is executed and the conditional suspends further evaluation. + +The declarations promoted to the parent scope depend on a union of the scopes for each conditional statement clause using the following algorithm: + +* Traverse all clauses in the conditional statement, gathering the declarations in the scope into a mapping of declaration names to types. For each clause, + * Reconcile the declaration names and their associated types in the map. + * If the name _isn't_ already in the map, insert the name into the map and assign the type seen. + * If the name _is_ already in the map, update the mapped type to a common type between the current declaration's type and the type stored in the map. If there is no common type, emit an error. + * For each name in the map that was _not_ seen in the current scope, mark the type in the map as optional. +* If there is no `else` clause, mark every type in the map as optional. + +The result is a set of declaration available in the parent scope that concretely represent the union of all scopes of the conditional statement. Any declaration that does not execute but is available in the union of the conditional statement clause scopes should be set to `None`. Further, when finding common types across scopes, the type declared in the earliest conditional statement clause is used as the base type. If a declaration that _would_ be promoted to a parent scope conflicts with an existing name in the parent scope, an error should be returned. + +For example, + +```wdl +if (...) { + String a = "foo" + String b = "foo" + String always_available = "foo" + String bad = "foo" +} else if (...) { + # If this clause executes, the both `a` and `b` will be `None`. + String? b = None + String always_available = "bar" + Int bad = 1 +} else { + String a = "baz" + String b = "baz + String always_available = "baz" + String bad = "baz" +} + +# Both `a` and `b` can be `None` or unevaluated, so they both promote as a `String?`. +# `always_available` is always available, so it will be promoted as a `String`. +# `bad` will return an error, as there is no common type between a `String` and an `Int`. +``` The scoping rules for conditionals are similar to those for scatters—declarations or call outputs inside a conditional body are accessible within that conditional and any nested statements. From a7abb03454126843d92df5fe71bf3a26926979d6 Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Sun, 5 Oct 2025 12:49:25 -0500 Subject: [PATCH 3/4] revise: fix mistakes in grammar and otherwise --- SPEC.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SPEC.md b/SPEC.md index 70a8fdc3..1d5abfee 100644 --- a/SPEC.md +++ b/SPEC.md @@ -7146,7 +7146,7 @@ Example output: ### Conditional Statement -A conditional statement consists of one or more conditional clauses, each with an associated body. The types of conditional statement clauses are: +A conditional statement consists of one or more conditional clauses, each having an associated body. The types of conditional statement clauses are: * A required `if` clause with an associated expression that evaluates to a `Boolean`. The `if` clause must be first in the conditional expression. @@ -7156,18 +7156,18 @@ A conditional statement consists of one or more conditional clauses, each with a * At most, one `else` clause with no associated expression. The `else` clause must be last in the conditional expression. -When a conditional statement is evaluated, each conditional clause is evaluated sequentially; for each `if` and `else if` clause, the expression is evaluated—if the result of the evaluation is `true`, the body of that clause is evaluated and the entire conditional statement suspends further evaluation. If none of the `if` or `else if` clauses execute and we reach the final `else` clause, then the `else` clause is executed and the conditional suspends further evaluation. +When a conditional statement is evaluated, each conditional clause is evaluated sequentially; for each `if` and `else if` clause, the expression is evaluated—if the result of the evaluation is `true`, the body of that clause is evaluated and the entire conditional statement suspends further evaluation. If none of the `if` or `else if` clauses execute and we reach the final `else` clause, the `else` clause is executed and the conditional suspends further evaluation. The declarations promoted to the parent scope depend on a union of the scopes for each conditional statement clause using the following algorithm: -* Traverse all clauses in the conditional statement, gathering the declarations in the scope into a mapping of declaration names to types. For each clause, +* Traverse all clauses in the conditional statement, gathering the declarations in the scope into a mapping of declaration names to types. For each clause: * Reconcile the declaration names and their associated types in the map. * If the name _isn't_ already in the map, insert the name into the map and assign the type seen. * If the name _is_ already in the map, update the mapped type to a common type between the current declaration's type and the type stored in the map. If there is no common type, emit an error. * For each name in the map that was _not_ seen in the current scope, mark the type in the map as optional. * If there is no `else` clause, mark every type in the map as optional. -The result is a set of declaration available in the parent scope that concretely represent the union of all scopes of the conditional statement. Any declaration that does not execute but is available in the union of the conditional statement clause scopes should be set to `None`. Further, when finding common types across scopes, the type declared in the earliest conditional statement clause is used as the base type. If a declaration that _would_ be promoted to a parent scope conflicts with an existing name in the parent scope, an error should be returned. +The result is a set of declarations available in the parent scope that concretely represent the union of all scopes of the conditional statement. Any declaration that does not execute but is available in the union of the conditional statement clause scopes should be set to `None`. Further, when finding common types across scopes, the type declared in the earliest conditional statement clause is used as the base type. If a declaration that _would_ be promoted to a parent scope conflicts with an existing name in the parent scope, an error should be returned. For example, @@ -7178,13 +7178,13 @@ if (...) { String always_available = "foo" String bad = "foo" } else if (...) { - # If this clause executes, the both `a` and `b` will be `None`. + # If this clause executes, both `a` and `b` will be `None`. String? b = None String always_available = "bar" Int bad = 1 } else { String a = "baz" - String b = "baz + String b = "baz" String always_available = "baz" String bad = "baz" } From dbfc08c3091eb33e39909a960a15020fee822bac Mon Sep 17 00:00:00 2001 From: Clay McLeod Date: Sun, 5 Oct 2025 13:07:33 -0500 Subject: [PATCH 4/4] revise: further revisions to algorithm --- SPEC.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/SPEC.md b/SPEC.md index 1d5abfee..76ee5907 100644 --- a/SPEC.md +++ b/SPEC.md @@ -7160,14 +7160,14 @@ When a conditional statement is evaluated, each conditional clause is evaluated The declarations promoted to the parent scope depend on a union of the scopes for each conditional statement clause using the following algorithm: -* Traverse all clauses in the conditional statement, gathering the declarations in the scope into a mapping of declaration names to types. For each clause: +1. Create a new map of declaration names to types. Traverse all clauses in the conditional statement, gathering the declarations in the scope into a mapping of declaration names to types. For each clause: * Reconcile the declaration names and their associated types in the map. * If the name _isn't_ already in the map, insert the name into the map and assign the type seen. * If the name _is_ already in the map, update the mapped type to a common type between the current declaration's type and the type stored in the map. If there is no common type, emit an error. - * For each name in the map that was _not_ seen in the current scope, mark the type in the map as optional. -* If there is no `else` clause, mark every type in the map as optional. +2. Perform a second pass through each clause in the conditional statement. For each name in the map created in step 1, if that name is _not_ seen in the current clause's scope, mark that type as optional. +3. If there is no `else` clause, mark every type in the map as optional. -The result is a set of declarations available in the parent scope that concretely represent the union of all scopes of the conditional statement. Any declaration that does not execute but is available in the union of the conditional statement clause scopes should be set to `None`. Further, when finding common types across scopes, the type declared in the earliest conditional statement clause is used as the base type. If a declaration that _would_ be promoted to a parent scope conflicts with an existing name in the parent scope, an error should be returned. +The result is a set of declarations available in the parent scope that concretely represent the union of all scopes of the conditional statement. Any declaration in the union map that does not evaluate in a conditional statement clause's body is set to `None`. Further, when finding common types across scopes, the type declared in the earliest conditional statement clause is used as the base type. If a declaration that _would_ be promoted to a parent scope conflicts with an existing name in the parent scope, an error should be returned. For example, @@ -7180,16 +7180,19 @@ if (...) { } else if (...) { # If this clause executes, both `a` and `b` will be `None`. String? b = None + String c = "bar" String always_available = "bar" Int bad = 1 } else { String a = "baz" String b = "baz" + String c = "baz" String always_available = "baz" String bad = "baz" } # Both `a` and `b` can be `None` or unevaluated, so they both promote as a `String?`. +# `c` is missing from the first scope, so it must also be marked as `String?`. # `always_available` is always available, so it will be promoted as a `String`. # `bad` will return an error, as there is no common type between a `String` and an `Int`. ```