Language4 min read

Control Flow

if/else, switch, early returns, discard, and how break and skip behave inside a switch.

Reading Time
4 min
Word Count
631
Sections
11
Try It Live

Pressure-test the syntax

Take the concept from this page into the playground and deliberately break a pass, binding, or type signature to see how the compiler responds.

Try a Live Edit

BWSL has C-like control flow with a few differences worth learning up front. The biggest one: switch cases never fall through.

Loops (for, while, loop, range loops, break, skip) have their own page: Loops.

If and Else

if takes a parenthesized bool condition. Bodies can be blocks or single statements, and else if chains as usual:

bwsl
float3 color;
if (ndotl > 0.5) {
color = litColor;
} else if (ndotl > 0.0) {
color = lerp(shadowColor, litColor, ndotl * 2.0);
} else {
color = shadowColor;
}
if (alpha < 0.01) discard;

The condition is a scalar. A numeric condition counts as true when it is non-zero, but an explicit comparison such as if (count != 0) reads better.

Ternary

cond ? a : b picks one of two values. Only the selected arm is evaluated, and both arms must have the same type:

bwsl
float weight = isBackFace ? 0.5 : 1.0;
float3 tint = isSelected ? float3(1.0, 0.8, 0.2) : float3(1.0);
// Error: arms have different types (float3 and float2)
// float3 bad = isSelected ? float3(1.0) : float2(1.0);

The arms can be scalars, vectors, matrices, structs or arrays. When both arms are short and have no side effects, the compiler emits a branch-free select instead of an if.

Switch

switch selects one case by an integer value:

bwsl
float3 debugColor;
switch (resources.debugMode) {
case 0:
debugColor = albedo;
case 1:
debugColor = normal * 0.5 + 0.5;
case 2, 3:
debugColor = float3(roughness, metallic, 0.0);
default:
debugColor = float3(1.0, 0.0, 1.0);
}

No Fallthrough

Each case runs only its own statements and then leaves the switch. There is no fallthrough into the next case and no break is needed to stop it.

To share one body between several values, list them in a single case, separated by commas:

bwsl
switch (materialId) {
case 1, 2: // Both 1 and 2 run this body
roughness = 0.2;
default:
roughness = 0.8;
}

Coming from C, HLSL or GLSL

Stacked labels do not share a body. In case 1: case 2: roughness = 0.2;, value 1 runs the empty first case and does nothing. Write case 1, 2: instead.

Selectors and Labels

  • The selector must be an integer: int, uint or an enum value. A float selector is rejected.
  • Case labels must be compile-time values: integer literals (including negative, hex and binary literals), named constants, or enum members such as case Side::Left:. A runtime variable as a label is an error.
  • Each value can appear in only one case, and a switch can have at most one default. Duplicates are compile errors.
  • default is optional. Without it, a value that matches no case skips the switch.
  • A case body can be a list of statements or a { ... } block. Each case is its own scope, so two cases can declare locals with the same name.

Break and Skip Inside a Switch

A case never needs break. When one appears inside a switch, it applies to the enclosing loop:

  • Inside a loop, break in a case exits the loop, not just the switch. skip in a case moves on to the loop's next iteration.
  • Outside any loop, break in a case does nothing, because the case already ends there.
bwsl
float total = 0.0;
for (int i = 0; i < 8; i++) {
switch (i) {
case 1:
skip; // Next iteration of the for loop
case 5:
break; // Leaves the for loop
default:
total += float(i);
}
total += 10.0;
}
// i = 0, 2, 3, 4 each add i + 10, so total is 49.0

Switching on Variants

A switch on a shader variant with a compile-time value is resolved during compilation: only the matching case is kept in each specialized shader. When the selector itself is a constant, the switch also compiles to just the matching case.

Returning Early

return leaves the current function or stage. BWSL also has conditional forms that keep guard clauses on one line:

bwsl
shade :: (float3 n, float3 l) -> float {
float ndotl = dot(n, l);
return 0.0 if (ndotl <= 0.0); // Return a value only when the condition holds
return ndotl * ndotl;
}
compute "Main" [64, 1, 1] {
uint id = input.global_id.x;
return if (id >= resources.count); // Bounds guard for the extra invocations
resources.values[id] *= 2.0;
}
FormMeaning
return;Leave a void function or a stage
return expr;Return a value
return if (cond);Leave only when cond is true (void functions and stages)
return expr if (cond);Return expr only when cond is true

A function with a return type must return a value on every path that can reach the end of the function. Otherwise it is a compile error. A switch without a default counts as a path that can fall out of the switch.

Discard

In a fragment stage, discard; drops the current fragment: nothing is written to any color target or to depth.

bwsl
float4 texel = sample(resources.albedo, input.uv);
if (texel.a < 0.5) {
discard;
}

discard is a statement, so it takes no parentheses.

Summary

StatementNotes
if (c) { } else if (c) { } else { }c is a scalar condition
c ? a : bOnly the chosen arm runs; both arms have the same type
switch (x) { case 1, 2: ... default: ... }Integer or enum selector; no fallthrough
break / skip in a caseApply to the enclosing loop
return expr if (c);Conditional return
discard;Fragment stages only

See Also

  • Loops - for, while, loop, range loops, break if and skip if
  • Shader Variants - Compile-time switch on variants
  • Eval - eval if and compile-time loops