Eval
Compile-time evaluation, code folding, and compile-time expansion blocks in BWSL.
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 EditThe eval keyword enables compile-time evaluation in BWSL. It can mark functions and methods for constant folding, and it can define eval { ... } blocks inside shader bodies for compile-time statement expansion.
Eval Blocks
An eval { ... } block inside a shader body runs at compile time and expands into ordinary runtime statements. This lets you use compile-time control flow, loops, and local variables to generate shader code.
vertex {
float sum = 0.0;
eval {
const int taps = 4;
eval int accumulator = 0;
if (taps > 2) {
sum += 1.0;
}
for (i in 0..taps) {
accumulator = accumulator + i;
sum += float(i);
}
loop (2) {
accumulator = accumulator + 1;
}
sum += float(accumulator);
}
output.position = float4(sum, 0.0, 0.0, 1.0);
}
The block above expands before IR lowering. The compiler evaluates if, for, and loop at compile time, then emits the resulting runtime statements (like sum += 1.0; and sum += float(i);) with all compile-time values substituted.
Compile-Time Locals
Inside an eval block, there are two kinds of compile-time variables:
| Declaration | Mutability | Usage |
|---|---|---|
const int x = 4; | Immutable | Compile-time constants |
eval int x = 0; | Mutable | Compile-time accumulators, counters |
const locals work the same as elsewhere — they're fixed at declaration. eval locals can be reassigned within the block:
eval {
eval int count = 0;
for (i in 0..8) {
count = count + 1;
}
// count is now 8 at compile time
output.totalIterations = float(count);
}
Compile-Time Control Flow
All control flow inside an eval block is evaluated at compile time:
Compile-time if — branches are resolved at compile time. Only the taken branch emits runtime code:
eval {
const int quality = 3;
if (quality > 2) {
// This branch is taken — these statements are emitted
color = highQualityLighting(N, V, L);
} else {
// This branch is eliminated entirely
color = simpleLighting(N, L);
}
}
Compile-time for — range-based iteration unrolls at compile time:
eval {
for (i in 0..4) {
float weight = float(i) / 4.0;
sum += sample(resources.tex, resources.texSampler, uv + float2(weight, 0.0)).r;
}
}
This expands to four separate sum += statements with i substituted as 0, 1, 2, 3.
Compile-time loop — repeats a body a fixed number of times:
eval {
loop (3) {
sum += sampleNext();
}
}
eval { ... } expands into ordinary AST nodes before IR lowering. The rest of the compiler pipeline sees standard statements — there is no runtime overhead from the eval machinery.
Runtime Statements Inside Eval
Any statement inside an eval block that isn't a compile-time construct is emitted as a runtime statement, with compile-time values substituted:
eval {
for (i in 0..3) {
// This is a runtime statement, emitted 3 times with i = 0, 1, 2
float w = weights[i];
color += sample(resources.tex, resources.texSampler, uv + offsets[i]) * w;
}
}
Runtime declarations inside an eval block should not shadow a visible compile-time variable. Keep compile-time and runtime names distinct to avoid confusion.
Eval Methods
The eval keyword also marks enum methods for compile-time evaluation. Method bodies pattern match on the enum instance directly, and self is available inside the body without being declared as a parameter.
enum Light {
Point(float3 position, float radius),
Directional(float3 direction),
Spot(float3 position, float3 direction, float angle),
eval getDirection :: (float3 surfacePos) -> float3 {
Point(pos, _): normalize(pos - surfacePos)
Directional(dir): -dir
Spot(pos, _, _): normalize(pos - surfacePos)
}
}
When the variant is known at compile time, the compiler folds the selected arm directly into the generated shader.
Eval If (Standalone)
Outside of eval { } blocks, eval if can be used as a standalone statement for compile-time conditional evaluation:
enum Channels : uint {
Red = 0b0001,
Green = 0b0010,
Blue = 0b0100,
Alpha = 0b1000,
eval componentCount :: () -> int {
int count = 0;
eval if ((self & Red) != 0) count++;
eval if ((self & Green) != 0) count++;
eval if ((self & Blue) != 0) count++;
eval if ((self & Alpha) != 0) count++;
return count;
}
}
Code Folding
Small helpers can be folded into constants:
const float PI = 3.14159265359;
eval circleArea :: (float radius) -> float {
return PI * radius * radius;
}
const float area = circleArea(2.0);
Pattern Arms
The same arm syntax used by enum methods also appears in free functions:
distance :: (SDFShape shape, float3 p) -> float {
Sphere(r): length(p) - r
Box(size): boxSDF(p, size)
Torus(major, minor): torusSDF(p, major, minor)
}
Best Practices
Do use eval for:
eval { }blocks to generate repetitive code (blur kernels, tap loops, feature toggles)- Enum dispatch methods
- Compile-time configuration branching
- Constant folding opportunities
- Small helper logic with compile-time inputs
Don't use eval for:
- Logic that depends on runtime values — everything inside
eval { }control flow must be compile-time known - Large deeply-nested expansions that produce hard-to-debug generated code
- Public examples based on unsupported syntax such as explicit
selfparameters
See Also
- Enums — Algebraic data types with eval methods
- Loops — Iteration constructs including
eval forandeval loop - Language Overview — BWSL syntax and concepts