Abstract Numerics
In statically-typed languages, types are typically fixed and rigid. However, WGSL introduces a unique and extremely powerful mechanism for compile-time arithmetic: Abstract Numerics.
An abstract-numeric type is a compile-time "meta-type" used to represent numbers with extremely high precision before they are committed to the GPU's fixed-precision hardware registers.
There are two abstract-numeric types in WGSL:
* abstract-float: Represented as a 64-bit floating-point type during compile-time.
* abstract-int: Represented as a 64-bit signed integer type during compile-time.
The Literal-Type Connection
Syntax Reference: Numeric Literals
For full details on concrete literal suffixes (like i, u, f, h) and hexadecimal formats, refer to the Numeric Literals guide.
Abstract types are closely tied to Numeric Literals. Whenever you write a numeric literal in your shader without a suffix (such as i, u, f, or h), its type is automatically abstract:
3.14159or1e3are of typeabstract-float.42or0xFFare of typeabstract-int.
No Explicit Spelling
You cannot write or spell abstract-int or abstract-float in your actual WGSL code (e.g., let x: abstract-float = 1.0 is a compile error). They only exist implicitly as the type of unsuffixed literals and constant-expressions.
1. Compile-Time "Infinite" Precision
When the WGSL compiler processes your shader, it performs all constant arithmetic using abstract types on your CPU, using at least 64-bit precision (double-precision floats and 64-bit signed integers).
This allows you to compute complex mathematical constant-expressions without any accumulation of rounding errors or premature overflows:
const pi = 3.141592653589793; // Handled with full 64-bit float precision
const radius = 2.0;
const area = pi * radius * radius; // Executed on CPU with 64-bit precision
Only when these constant-expressions are assigned to a concrete variable (like f32 or i32) does the compiler downcast the final pre-calculated result to the target GPU precision.
2. Implicit Type Conversions
In WGSL, concrete types (like f32, i32, u32) strictly prohibit implicit conversions (no implicit coercion). For example, 1.0 + 2 is a compile-time type mismatch error if 1.0 is concrete.
However, abstract-numerics are a deliberate exception to this rule. They support implicit type conversion to make writing mathematical expressions intuitive and clean:
| Abstract Type | Can Implicitly Convert To | Example |
|---|---|---|
abstract-int |
i32, u32, f32, f16, abstract-float |
let x: f32 = 42; (maps abstract-int to f32) |
abstract-float |
f32, f16 |
let y: f32 = 1.5; (maps abstract-float to f32) |
Mixing Abstract Types
When you combine an abstract-int and an abstract-float in an arithmetic operation, the compiler implicitly promotes the abstract-int to abstract-float, performing the calculation using floating-point math:
Default Resolution Rules
If you initialize a variable without specifying an explicit type, the compiler must resolve the abstract expression to a concrete GPU register representation. In this case, standard default conversion rules apply:
- An
abstract-intvalue defaults toi32. - An
abstract-floatvalue defaults tof32.
let count = 42; // No type specified -> count resolves to i32
let scale = 1.25; // No type specified -> scale resolves to f32
3. Strict Compile-Time Constraints
Because abstract-numerics are resolved entirely during the compilation phase, they must adhere to strict mathematical validity constraints:
- Finite Expressions: Any compile-time arithmetic operation on abstract-numerics must produce a finite, valid number.
- No Compilation Loop-holes: Compilation will immediately fail if a constant abstract-numeric expression:
- Overflows or underflows the 64-bit bounds.
- Divides by zero.
- Produces a positive infinity, negative infinity, or a NaN (Not-a-Number).
const fail_1 = 1.0 / 0.0; // COMPILE ERROR: Division by zero
const fail_2 = 1e400; // COMPILE ERROR: abstract-float overflow
This ensures that any constant expressions embedded in your final compiled shader binaries are guaranteed to be finite, predictable, and fully initialized before they ever reach the GPU.
Interactive Visualizer
In the interactive simulation on the right, you can see these exact type resolutions executing live.
Look at the Results panel to see how abstract types like pi and two are evaluated on the CPU at compile time, and how they implicitly downcast to concrete f32, i32, or u32 types to feed your GPU buffers!
/*
* Copyright ©2026 Michael R. Bernstein. All new modifications licensed under Apache 2.0.
* Upstream lineage ©2023 governed by original BSD 3-Clause. See README.md.
*/
const pi = 3.14159265359; // 'pi' is of type abstract-float
const two = 2; // 'two' is of type abstract-int
// 'two_pi' is an abstract-float
// 'two' was implicitly converted from an abstract-int to abstract-float for the
// multiplication, which is performed with 64-bit floating point precision.
const two_pi = pi * two;
// Abstract-ints can implicitly convert to i32, u32, f32
const implicitly_convert_abstract_int_to_i32 : i32 = 100;
const implicitly_convert_abstract_int_to_u32 : u32 = 100;
const implicitly_convert_abstract_int_to_f32 : f32 = 100;
// Abstract-float can implicitly convert to f32
const implicitly_convert_abstract_float_to_f32 : f32 = 100.0 + 1e2;
fn f() {
// If you use an abstract-numeric to initialize a var, let or override
// without an explicit type, then the abstract-numeric will implicitly
// convert to its default concrete type.
var default_concrete_type_for_abstract_int = 12; // abstract-int will default to i32
var default_concrete_type_for_abstract_float = 12.3; // abstract-float will default to f32
var check_is_i32 : i32 = default_concrete_type_for_abstract_int;
var check_is_f32 : f32 = default_concrete_type_for_abstract_float;
}