Generics
Generic functions with type constraints and compile-time type pattern matching 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 EditBWSL supports generic functions that work with multiple types through a constraint-based system. This allows you to write reusable code while maintaining full type safety and enabling compile-time specialization.
Type Constraints
Type constraints define a set of allowed types that can be used with generic functions. Define constraints at the pipeline level using the constraint keyword:
pipeline MyPipeline {
// Define type constraints
constraint FloatVectors = float2 | float3 | float4;
constraint AllFloats = float | float2 | float3 | float4;
constraint Scalars = float | int;
constraint Vec3Or4 = float3 | float4;
}
Constraints can reference other constraints to build more complex type sets:
constraint IntVectors = int2 | int3 | int4;
constraint AllVectors = FloatVectors | IntVectors;
constraint Numeric = float | int | uint | AllVectors;
Generic Functions
Generic functions use constraint names as parameter types. When called, the compiler resolves the concrete type based on the arguments provided.
Basic Generic Functions
Use a constraint name as the parameter type:
constraint FloatVectors = float2 | float3 | float4;
// Constrained parameter, concrete return type
lengthSquared :: (FloatVectors v) -> float {
return dot(v, v);
}
// Usage - type is inferred from argument
float len2d = lengthSquared(float2(1.0, 2.0)); // Works with float2
float len3d = lengthSquared(float3(1.0, 2.0, 3.0)); // Works with float3
float len4d = lengthSquared(float4(1.0, 2.0, 3.0, 4.0)); // Works with float4
Generic Return Types
When the return type uses the same constraint as a parameter, the return type matches the input type:
constraint FloatVectors = float2 | float3 | float4;
// Return type matches input type
genericAdd :: (FloatVectors a, FloatVectors b) -> FloatVectors {
return a + b;
}
// Usage
float2 sum2 = genericAdd(float2(1.0), float2(2.0)); // Returns float2
float3 sum3 = genericAdd(float3(1.0), float3(2.0)); // Returns float3
float4 sum4 = genericAdd(float4(1.0), float4(2.0)); // Returns float4
Mixed Parameter Types
Generic functions can combine constrained and concrete types:
constraint FloatVectors = float2 | float3 | float4;
// Generic vector with concrete scalar
scale :: (FloatVectors v, float s) -> FloatVectors {
return v * s;
}
// Usage
float2 scaled2 = scale(uv, 2.0);
float3 scaled3 = scale(position, 0.5);
Type Pattern Matching
For operations that need different implementations per type, use type pattern matching. This allows compile-time dispatch to type-specific code paths.
Expression Syntax
For simple single-expression implementations, use the concise syntax:
constraint FloatVectors = float2 | float3 | float4;
vecProcess :: (FloatVectors v) -> FloatVectors {
float2: v * 2.0
float3: cross(v, float3(0.0, 1.0, 0.0))
float4: v.wzyx
}
Each arm consists of a type followed by a colon and an expression. The appropriate arm is selected at compile time based on the argument type.
Block Syntax
For more complex implementations requiring multiple statements, use block syntax:
constraint FloatVectors = float2 | float3 | float4;
vecNormalize :: (FloatVectors v) -> FloatVectors {
float2: {
float len = length(v);
if (len < 0.0001) {
return float2(0.0, 1.0);
}
return v / len;
}
float3: normalize(v)
float4: normalize(v)
}
You can combine expression and block syntax within the same function.
Default Arms
Use default to handle multiple types with the same implementation:
constraint FloatVectors = float2 | float3 | float4;
vecScale :: (FloatVectors v, float s) -> FloatVectors {
float4: v * s * 0.5 // Special case for float4
default: v * s // Handles float2 and float3
}
The default arm matches any type not explicitly handled by other arms.
Practical Examples
Safe Normalize
A normalize function that handles zero-length vectors:
constraint Vec3Or4 = float3 | float4;
safeNormalize :: (Vec3Or4 v) -> Vec3Or4 {
float len = length(v);
if (len < 0.0001) {
return v;
}
return v / len;
}
Generic Blend (Lerp)
A user-defined linear interpolation:
constraint FloatVectors = float2 | float3 | float4;
genericBlend :: (FloatVectors a, FloatVectors b, float t) -> FloatVectors {
return a + (b - a) * t;
}
// Usage
float3 blended = genericBlend(colorA, colorB, 0.5);
Generic Clamp
A wrapper around min/max for clamping vectors:
constraint FloatVectors = float2 | float3 | float4;
genericClampVec :: (FloatVectors v, FloatVectors minVal, FloatVectors maxVal) -> FloatVectors {
return max(min(v, maxVal), minVal);
}
Component Sum with Type Patterns
Different summation logic based on vector size:
constraint FloatVectors = float2 | float3 | float4;
componentSum :: (FloatVectors v) -> float {
float2: v.x + v.y
float3: v.x + v.y + v.z
float4: v.x + v.y + v.z + v.w
}
Nested Generic Calls
Generic functions can call other generic functions:
constraint FloatVectors = float2 | float3 | float4;
genericBlend :: (FloatVectors a, FloatVectors b, float t) -> FloatVectors {
return a + (b - a) * t;
}
genericClampVec :: (FloatVectors v, FloatVectors minVal, FloatVectors maxVal) -> FloatVectors {
return max(min(v, maxVal), minVal);
}
// Function that uses other generic functions
customSmoother :: (FloatVectors edge0, FloatVectors edge1, FloatVectors x) -> FloatVectors {
FloatVectors t = genericClampVec((x - edge0) / (edge1 - edge0),
edge0 * 0.0,
edge0 * 0.0 + 1.0);
return t * t * (3.0 - 2.0 * t);
}
Complete Example
Here's a full pipeline demonstrating generics:
pipeline GenericLighting {
attributes {
position: float3
normal: float3
texcoord: float2
}
// Type constraints
constraint FloatVectors = float2 | float3 | float4;
constraint Vec3Or4 = float3 | float4;
// Generic utilities
safeNormalize :: (Vec3Or4 v) -> Vec3Or4 {
float len = length(v);
if (len < 0.0001) {
return v;
}
return v / len;
}
genericBlend :: (FloatVectors a, FloatVectors b, float t) -> FloatVectors {
return a + (b - a) * t;
}
doubleValue :: (FloatVectors x) -> FloatVectors {
return x + x;
}
pass "Main" {
use attributes { position, normal, texcoord }
vertex {
float3 worldPos = position;
float3 worldNormal = safeNormalize(normal);
output.position = float4(worldPos, 1.0);
output.normal = worldNormal;
output.texcoord = texcoord;
}
fragment {
float3 N = safeNormalize(input.normal);
float3 L = safeNormalize(float3(1.0, 1.0, 1.0));
float NdotL = max(dot(N, L), 0.0);
float3 ambient = float3(0.1, 0.1, 0.1);
float3 diffuse = float3(1.0, 1.0, 1.0);
float3 lighting = genericBlend(ambient, diffuse, NdotL);
output.color = float4(lighting, 1.0);
}
}
}
Summary
| Feature | Syntax |
|---|---|
| Define constraint | constraint Name = Type1 | Type2 | ...; |
| Constrained parameter | func :: (ConstraintName param) -> ReturnType |
| Generic return | func :: (ConstraintName a) -> ConstraintName |
| Type pattern (expression) | float2: expression |
| Type pattern (block) | float3: { statements; return value; } |
| Default pattern | default: expression |