Types

Types and data

TinyFX uses static, nominal types with local inference. An annotation supplies an expected type; otherwise an initializer, return expression, or call context can infer one. The compiler records the resulting type and every coercion once for all backends. For function calls and overload selection, read functions; for exact builtin and method signatures, use the generated operation reference.

Scalars, vectors, and matrices

The scalar types are int (signed 32-bit), uint (unsigned 32-bit), float (32-bit), and bool. Numeric vector types are int2 through int4, uint2 through uint4, and float2 through float4. Matrix types are square, column-major float2x2, float3x3, and float4x4 values.

let pixels: int2 = (1920, 1080);
let tint: float4 = (1.0, 0.6, 0.2, 1.0);
let transform = float4x4(
  (1.0, 0.0, 0.0, 0.0),
  (0.0, 1.0, 0.0, 0.0),
  (0.0, 0.0, 1.0, 0.0),
  (0.0, 0.0, 0.0, 1.0),
);

Vectors support component access and swizzles such as .x, .rgb, .xy, and .zyx. Matrix construction accepts columns, a source matrix, or the required number of scalar components. Matrix and vector constructors are positional; they are not named-argument APIs.

The only implicit numeric conversions are int to float, uint to float, and int to uint, including element-wise conversions for equal-width vectors. Use an explicit constructor such as float(value), int(value), or float3(value) for every other conversion. This is especially important at resource and enum boundaries.

Strings and CPU collections

string, dynamic arrays (T[]), maps (map<K, V>), Json, function values, trait objects, meshes, models, fonts, camera input, microphone input, and video handles are CPU-only values. They cannot become shader values, GPU uniform captures, or fixed GPU data. Assets and media describes the opaque host-backed handles.

let names: string[] = ["Ada", "Grace"];
let scores = map<string, int>();
scores.set("Ada", 10);
 
let saved = json::parse("{\"enabled\":true}");

An uncontextualized array literal is dynamic; provide a fixed-array expected type when the value must be GPU-compatible:

let dynamic = [1.0, 2.0, 3.0];       // float[]
let taps: float[4] = [0.1, 0.2, 0.3, 0.4];
let filled = array<float>(4, 0.0);   // float[4]

Dynamic arrays and maps are mutable reference values. Their complete method sets and error behavior belong in the operation reference, not this narrative page. JSON supports both immutable dynamic JSON values and context-typed conversion; use that reference for conversion constraints and the exact accessor surface.

String operations are catalogued rather than duplicated here. Two details are worth calling out because they are easy to misread:

  • text.trim() removes whitespace; text.trim(characters) accepts its character set positionally or as characters:.
  • text.substr(start, end) uses an exclusive end index, not a character count.

Structs and methods

Struct declarations define nominal aggregate types. Fields default to private; mark a field pub to read or write it outside the struct's own methods. A field can have an explicit type, a default expression that supplies its type, or both. A defaulted field may be omitted from a struct literal.

pub struct Particle {
  pub position: float3,
  pub velocity: float3 = (0.0, 0.0, 0.0),
  mass: float = 1.0,
}
 
let particle = Particle { position: (1.0, 2.0, 3.0) };
particle.position += particle.velocity;

On the CPU, structs are reference values: assigning one struct to another binding aliases its fields. The standard clone() operation makes a shallow copy of the outer structure, so nested structs and collections remain shared; a user-defined clone() method can deliberately replace that behavior. Do not use CPU aliasing to infer GPU capture behavior.

Methods live in impl blocks. An instance method has self as its first parameter and is called with .; a method without self is static and is called with ::. Methods are private unless declared pub.

impl Particle {
  pub fn advance(self, dt: float) {
    self.position += self.velocity * dt;
  }
 
  pub fn stationary(position: float3) -> Particle {
    return Particle { position: position };
  }
}

Static fields are declared with static in a struct. Every static field requires an initializer, is read-only after initialization, is accessed through the type, and follows the same member visibility rules. Empty structs with public static methods are the standard-library namespace pattern, for example noise::fbm2 and matrix::lookAt.

Enums

Enums are nominal, signed-int-backed value types. An enum member has an implicit increasing discriminant beginning at zero, or an explicit compile-time int expression. Both names and discriminants must be unique and fit the signed 32-bit range.

pub enum Quality {
  low,
  medium = 4,
  high,
}
 
fn exposure(value: Quality) -> float {
  if (value == Quality.high) { return 1.0; }
  return 0.5;
}

Use qualified members such as Quality.high. Equality and inequality only work with the same enum type. Enums do not implicitly convert to an int or to a different enum, and arithmetic, ordering, and bitwise operations on them are rejected. int(value) explicitly extracts a discriminant.

On the CPU, Quality(rawInt) validates a dynamic int before constructing an enum; statically invalid values are rejected by the compiler. Enums are legal GPU data and lower to i32 in WGSL while retaining nominal checking in TinyFX source. Inherent impl Quality { ... } methods are supported. Trait implementations for enums are not currently supported.

The standard library uses enums for closed renderer/host options, including Blend, DepthCompare, DepthConvention, TextureFormat, and PaintComposite; see standard-library options.

Traits

Traits define a CPU-only dynamic-dispatch interface. A struct can implement a trait and then flow to a parameter, field, or local typed as that trait.

trait Area {
  fn area(self) -> float;
}
 
struct Circle { pub radius: float }
 
impl Area for Circle {
  pub fn area(self) -> float {
    return 3.14159265 * self.radius * self.radius;
  }
}
 
fn printArea(shape: Area) {
  print(shape.area());
}

Calls through a trait value dispatch at runtime. Trait objects, dynamic dispatch, and trait implementations are CPU-only; a trait cannot participate in shader or compute code. Only structs currently implement traits—use an inherent enum implementation for enum behavior.

GPU-safe data and resources

The GPU-safe data types are scalars, numeric vectors, matrices, enums, fixed arrays of GPU-safe elements, and non-recursive structs whose fields are all GPU-safe. A GPU-visible struct cannot contain string, a dynamic array, a map, a trait, a function value, or an opaque CPU handle.

buffer<T>, texture2d<T>, and texture3d<T> are resources rather than ordinary copied data. shader is a CPU-created shader value. Resource identity, texture element restrictions, capture rules, and writable compute semantics are covered by GPU programming. A particular GPU use can be stricter than the basic type rule: uniform captures cannot contain bool, for example, because WGSL uniform buffers cannot represent it.

Custom mesh attribute channels are currently supplied as arrays of float, float2, float3, or float4 only. The matching shader attribute and mesh layout rules are in shader declarations and attributes.

More detail

  • Functions — function types, arrow closures, overloads, and stage annotations.
  • GPU programming — captures, resource parameters, shader data, and compute restrictions.
  • Standard-library reference — embedded modules and the option enums.
  • Operation reference — generated signatures, overloads, parameter labels, CPU/GPU stages, and execution-target evidence.

Advanced storage types

workspace<T>, atomics<int|uint>, appendList<T>, counter<T>, and slot<T> are the structured advanced-compute resource/capability types. Their element and aliasing rules are documented in Structured advanced compute. gpuCount is an opaque producer-backed view inferred from .count; it cannot be named, constructed, or converted from an integer or atomic buffer.