Language2 min read

Generics

Generic functions with type constraints and compile-time type pattern matching in BWSL.

Reading Time
2 min
Word Count
358
Sections
18
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 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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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:

bwsl
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

FeatureSyntax
Define constraintconstraint Name = Type1 | Type2 | ...;
Constrained parameterfunc :: (ConstraintName param) -> ReturnType
Generic returnfunc :: (ConstraintName a) -> ConstraintName
Type pattern (expression)float2: expression
Type pattern (block)float3: { statements; return value; }
Default patterndefault: expression

See Also

  • Functions - Function declaration and scopes
  • Types - Core data types in BWSL
  • Eval - Compile-time evaluation