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;
}
while (accumulator < 12) {
sum += 0.5;
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, while, 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);
}
Eval locals can be scalars or vectors (float2–float4, int2–int4, uint2–uint4). A vector eval local can be reassigned from a constructor and used in the runtime statements the block emits, where it is substituted as a constant:
eval {
eval float3 tint = float3(0.2, 0.4, 0.6);
output.color = float4(tint * resources.exposure, 1.0);
}
Compile-time arithmetic and swizzles on vector eval locals are not supported yet.
An assignment to an eval local must keep its declared type. An integer value can be assigned to a float local, but other scalar kinds, vector component types and vector widths must match exactly.
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();
}
}
Compile-time while — repeats while a compile-time condition remains true:
eval {
eval int i = 0;
while (i < taps) {
sum += sampleTap(i);
i = i + 1;
}
}
The condition is checked before every iteration and must resolve to a compile-time bool, int, uint, or float.
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;
}
}
Eval Functions
An eval function declared at pipeline scope runs at compile time. It can be called from eval code and from ordinary runtime expressions with compile-time arguments, and the call folds to its result:
pipeline Shapes {
const float PI = 3.14159265359;
eval circleArea :: (float radius) -> float {
return PI * radius * radius;
}
eval fib :: (int n) -> int {
if (n < 2) { return n; }
return fib(n - 1) + fib(n - 2); // Recursion is allowed here
}
pass "Main" {
// ...
fragment {
const float area = circleArea(2.0); // 12.566...
float steps = float(fib(10)); // 55.0
// ...
}
}
}
Eval function bodies can use locals, loops, if and early return. Unlike runtime functions, they may recurse, up to a depth of 128 calls. Pipeline- and module-scope constant initializers cannot call them; call them from a stage, a function or another eval context.
Pattern Arms
The same arm syntax used by enum methods also appears in free functions:
shapeDistance :: (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 for,eval while, andeval loop - Language Overview — BWSL syntax and concepts