Language4 min read

Functions

Function declaration, shader entry points, generics, and scoping in BWSL.

Reading Time
4 min
Word Count
574
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 functions use the :: declaration form and support overloading, pass-local helpers, module-qualified calls, stage-returning helpers, and pass-block-returning helpers.

Function Declaration

Functions are declared with typed parameters and an explicit return type after ->:

bwsl
add :: (int a, int b) -> int {
return a + b;
}
calculateNormal :: (float3 p0, float3 p1, float3 p2) -> float3 {
float3 edge1 = p1 - p0;
float3 edge2 = p2 - p0;
return normalize(cross(edge1, edge2));
}

The general form is:

bwsl
functionName :: (Type arg1, Type arg2, ...) -> ReturnType {
// body
}

Function Names

A function or method cannot share its name with a built-in intrinsic. length :: (float3 v) -> float is rejected with Function name 'length' is reserved for a built-in intrinsic, even when the function is never called or lives in a module. Pick a distinct name such as signedLength or signed_distance.

Parameters Are Passed by Value

Every parameter is a local copy. Assigning to a parameter, or to a field or element of one, never changes the caller's variable. Structs and fixed-size arrays are copied as well:

bwsl
scaled :: (float3 v, float k) -> float3 {
v *= k; // Changes only the local copy
return v;
}
float3 p = float3(1.0);
float3 q = scaled(p, 2.0); // p is still (1, 1, 1)

Pointer-typed parameters (float^ out) are not supported. To produce several results, return a struct, or use a mutating struct method, which can update its receiver.

Returning

A function with a return type must return a value on every path that can reach the end of its body. A missing return after an if or a switch without default is a compile error. void functions exit early with return; or return if (cond);. Functions with a return type can use return expr if (cond); (see Control Flow).

Recursion

Shaders run without a call stack, so a function that calls itself, directly or through other functions, is a compile error. Rewrite such algorithms as loops. eval functions are the exception: they run at compile time and may recurse up to 128 levels deep (see Eval).

Function Calls

Call functions with positional arguments:

bwsl
int result = add(5, 3);
float3 normal = calculateNormal(v0, v1, v2);

The current parser surface uses positional calls only.

Overloading

Multiple functions can share a name as long as their parameter lists differ:

bwsl
square :: (float x) -> float {
return x * x;
}
square :: (float3 x) -> float3 {
return x * x;
}

Shader Entry Points

BWSL can package shader stages into helper functions that return vertex_function, fragment_function, or compute_function.

Vertex and Fragment Helpers

bwsl
vertexMain :: () -> vertex_function {
vertex {
output.position = float4(attributes.position, 1.0);
output.uv = attributes.texcoord;
}
}
fragmentMain :: () -> fragment_function {
fragment {
output.color = float4(input.uv, 0.0, 1.0);
}
}

Use them in a pass with stage assignment:

bwsl
pass "Main" {
use attributes { position, texcoord }
vertex = vertexMain()
fragment = fragmentMain()
}

Compute Helpers

Provisional

compute_function return types are parsed, but compute stages are normally declared directly in a pass with compute "Name" [x, y, z] { ... }. Treat this form as provisional.

bwsl
clearPass :: () -> compute_function {
compute "Main" [64, 1, 1] {
uint idx = input.global_id.x;
if (idx >= 1024u) {
return;
}
// resources.output[idx] = 0.0;
}
}

Pass Block Helpers

Functions can also return pass_block to package a whole pass body:

bwsl
makePass :: () -> pass_block {
pass {
vertex {
output.position = float4(0.0, 0.0, 0.0, 1.0);
}
fragment {
output.color = float4(1.0);
}
}
}
pass "Generated" = makePass();

Module pass blocks can map their local attributes, resources, and variants to the caller pipeline. See Pass Blocks for the complete rules.

Function Scope

Functions can live at pipeline scope, pass scope, or module scope.

Pipeline Scope

Pipeline-scoped helpers are visible to every pass in the pipeline:

bwsl
pipeline MyPipeline {
luma :: (float3 color) -> float {
return dot(color, float3(0.299, 0.587, 0.114));
}
pass "Forward" {
fragment {
float v = luma(float3(1.0, 0.8, 0.2));
output.color = float4(v, v, v, 1.0);
}
}
}

Pass Scope

Helpers declared inside a pass stay inside that pass:

bwsl
pipeline MyPipeline {
pass "SpecialEffect" {
wobble :: (float2 uv, float t) -> float2 {
return uv + sin(uv.y * 10.0 + t) * 0.01;
}
fragment {
float2 distorted = wobble(input.uv, 1.0);
output.color = float4(distorted, 0.0, 1.0);
}
}
}

Module Scope

Functions defined in modules are accessed using the :: namespace operator:

bwsl
module Math {
square :: (float x) -> float {
return x * x;
}
}
pipeline MyPipeline {
import Math
pass "Main" {
fragment {
float v = Math::square(0.5);
output.color = float4(v, v, v, 1.0);
}
}
}

Imported modules can be aliased, and using ModuleName can opt an imported module into unqualified lookup:

bwsl
pipeline MyPipeline {
import Math as M
using M
pass "Main" {
fragment {
float v = square(0.5) + M::PI;
output.color = float4(v, v, v, 1.0);
}
}
}

Constraint-Based Functions

BWSL's active generic system is constraint-based:

bwsl
constraint FloatVectors = float2 | float3 | float4;
scale :: (FloatVectors v, float s) -> FloatVectors {
return v * s;
}

Constrained functions can dispatch on the resolved concrete type:

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

Angle-bracket syntax such as foo<T> and where clauses are not part of the current supported surface. See Generics for the constraint-based form.

Function Summary

FeatureSyntax
Declarationname :: (params) -> ReturnType { }
Positional callfunc(a, b, c)
Overloadname :: (float x) -> ... and name :: (float3 x) -> ...
Vertex helpername :: () -> vertex_function { vertex { } }
Fragment helpername :: () -> fragment_function { fragment { } }
Compute helpername :: () -> compute_function { compute "Main" [X, Y, Z] { } }
Pass-block helpername :: () -> pass_block { pass { } }
Type constraintconstraint Name = Type1 | Type2;
Constrained parameter(ConstraintName param)
Module functionmoduleName::functionName()
Imported module aliasimport Module as Alias
Unqualified module lookupusing Alias
Type aliasusing Name = ExistingType

See Also