WESL Logo

Simple Generics for WESL

Note: this is one of several Generics proposals we’re considering. See also PR #34 and PR #20 for more feature rich generics proposals.

Generic programming is useful for WESL, particularly for libraries. With generics, WESL functions like reduce or prefixSum don’t need to be manually rewritten for each combination of element type and binary operation. I’m hoping we can find a fairly minimal design for generics that is easy for programmers to learn and supportable with modest effort in WESL tools.

To ease implementation effort, I imagine we’ll want to avoid type inference or type constraints on generics. But without type inference, specifying generic types at every call site to a generic function gets verbose and tedious. To avoid that verbosity, let’s allow generic variables on import statements (glslify and wgsl-linker did this too).

I thought we might start by allowing generics only on functions. We’ll want a design that’s extensible to more features (e.g. generic structs) of course, but we can start with a minimal implementation and add features as they prove necessary.

Summary

Examples

Simple Example:

  ./util.wesl:

  @export fn workgroupMin<E>(elems: array<E, 4>) -> E { }  // E is a generic parameter
  ./main.wesl:

  import ./util/workgroupMin<f32> as workMin; // substitutes f32 for E

  fn main() {
    workMin(a1);  // no generic variables required at the call site 
    workMin(a2);
  }

Here’s a more complicated case. reduce is parameterized by an element type (e.g. u32) and a binary operation, e.g. max()

  ./util.wesl:

  export<E, BinOp> 
  fn reduce(elems: array<E, 2>) -> E { 
    return BinOp(elems[0], elems[1]); 
  }
  ./main.wesl:

  import ./ops/binOpMax<f32> as binOpMax;
  import ./util/reduce<f32, binOpMax> as maxF32; 

  fn main() {
    maxF32(a1);
  }

Note that you can import a generic function w/o providing parameters:

  ./util.wesl:

  import binOpMax from ./ops;   // no generic variable specified yet

  export fn reduceMax<E>(elems: array<E, 2>) -> E { 
    return binOpMax<E>(elems[0], elems[1]); // generic value applied at call site
  }

Re-exporting generics is allowed (presuming we allow re-exporting in general, see Visibility):

  ./lib.wesl:
  export reduce from ./util/reduce; // re-export at package root level

  ./util/reduce.wgsl:
  export<E, BinOp> fn reduce(elems: array<E, 2>) -> E { }

Questions and possible extensions