Var<T> vs Expr<T> in the Luisa Compute DSL: Storage and Expression Types Explained

Var<T> declares mutable storage locations in kernel IR while Expr<T> represents read-only values and computation results, with Var implicitly converting to Expr but never the reverse.

In the luisagroup/luisacompute shading language, distinguishing between mutable storage and read-only expressions is fundamental to writing correct GPU kernels. The DSL provides two primary template types—Var<T> and Expr<T>—that separate variable declaration from value computation within the abstract syntax tree. Understanding when to use each type ensures you allocate device memory only when necessary while maintaining type-safe expression trees.

Core Concepts of Var and Expr

What is Var?

In include/luisa/dsl/var.h, the Var<T> template class creates a named storage location in the generated kernel intermediate representation (IR). Each Var instance generates a declaration statement and supports assignment operators like operator= and operator+=. This type is essential when you need values to persist across statements or require mutation within loops and conditionals.

What is Expr?

Defined primarily in include/luisa/dsl/struct.h, Expr<T> wraps existing values without allocating new storage. It represents literals, variable references, or temporary computation results. The DSL treats Expr as an immutable view that composes freely into expression trees but cannot serve as an assignment target.

When to Use Var in the Luisa Compute DSL

Use Var<T> when you require:

  • Named local variables inside kernels that persist across multiple statements, such as loop accumulators or temporary buffers
  • Assignment capabilities including mutation operators like +=, -=, or direct reassignment
  • Kernel parameters passed by reference where the underlying storage must be extracted and potentially modified
  • Mutable struct fields when defining DSL structs, as each field stores data as Var<member_type>
device.compile<1>([](Var<float3> pos, Var<float> time) noexcept {
    Var<float3> shifted = pos + make_float3(0.f, 1.f, 0.f);
    Var<float> intensity = dot(shifted, make_float3(1.f, 0.f, 0.f));
    shifted = make_float3(0.f, 0.f, 0.f);  // Reassignment allowed
});

When to Use Expr in the Luisa Compute DSL

Use Expr<T> when you need:

  • Read-only values sourced from variables, literals, or function returns without declaring new storage
  • Pure expressions inside return statements, function arguments, or conditional checks where mutation is unnecessary
  • Temporary computations passed to callables without explicit variable declaration overhead
  • Struct field accessors that expose read-only views of underlying Var storage members
device.compile<1>([](Var<float3> pos) noexcept {
    Expr<float3> direction = normalize(pos);  // Read-only view
    Expr<float> length = dot(pos, direction);  // Computation result
    return length * 0.5f;  // Used directly in expressions
});

Relationship Between Var and Expr

The types maintain a strict one-way conversion relationship enforced by the DSL type system.

Implicit Conversion from Var to Expr

In include/luisa/dsl/var.h, the Var class defines operator Expr<T>() const noexcept, allowing seamless use of variables within expressions:

Var<int> counter = def<int>(0);
Expr<int> expr_view = counter;  // Implicit conversion

No Reverse Conversion

Converting Expr<T> to Var<T> is prohibited because expressions may represent temporary values without unique storage locations. Only Var can serve as the assignment target.

Practical Implementation Examples

Accumulating Values Inside Kernels

When reducing buffer data, declare the accumulator as Var while reading buffer elements as Expr:

auto sum_kernel = device.compile<1>([](BufferVar<int> data, Var<int> out) noexcept {
    Var<int> sum = def<int>(0);  // Mutable storage
    ForRange(i, 0, data.size()) {
        sum += data.read(i);     // data.read() returns Expr<int>
    }
    out = sum;  // Assignment to Var parameter
});

Read-Only Computations in Callables

For pure calculations without side effects, chain Expr values through function calls:

auto bright_pass = device.compile<1>([](Var<float3> color) noexcept {
    Expr<float> lum = 0.2126f * color.x + 0.7152f * color.y + 0.0722f * color.z;
    return color * (lum + 0.1f);  // Returns Expr<float3>
});

DSL Structs with Automatic Type Handling

The struct macro in include/luisa/dsl/struct.h generates Var members for storage and Expr accessors for read-only field access:

struct Light {
    Var<float3> position;   // Storage
    Var<float>  intensity;  // Storage
};

device.compile<1>([](Var<Light> l) noexcept {
    Expr<float3> pos = l.position;   // Read-only accessor
    Expr<float>  i   = l.intensity;
    Expr<float3> dir = normalize(pos - make_float3(0.f));
    return i * max(dot(dir, make_float3(0.f, 1.f, 0.f)), 0.f);
});

Summary

  • Var<T> creates mutable storage locations in the kernel IR and supports assignment; defined in include/luisa/dsl/var.h
  • Expr<T> provides read-only views of values and computation results without allocating storage; defined in include/luisa/dsl/struct.h
  • Var converts to Expr implicitly via operator Expr<T>() const noexcept, but Expr cannot convert to Var
  • Use Var for local variables, accumulators, and mutable struct fields
  • Use Expr for literals, temporary calculations, and read-only struct accessors

Frequently Asked Questions

Can I assign a value to an Expr?

No. Expr<T> is strictly read-only and lacks assignment operators. If you need to modify a value, declare it as Var<T> instead. The expression type represents values that may not have unique storage backing them, making assignment semantically invalid in the DSL.

How do I convert a Var to Expr?

Conversion happens automatically through the implicit conversion operator defined in include/luisa/dsl/var.h. Simply assign a Var to an Expr variable or pass it to functions expecting Expr parameters: Expr<float> e = my_var;.

Why does the DSL struct macro use both Var and Expr types?

The macro generates Var<member_type> for actual data storage within the struct layout, ensuring each field occupies device memory. It simultaneously exposes Expr<member_type> accessors to prevent accidental mutation of struct fields when accessed through instances, maintaining the DSL's distinction between storage and expression contexts.

Where are Var and Expr defined in the luisacompute source?

Var<T> is defined in include/luisa/dsl/var.h with declaration helpers and assignment operators. Expr<T> is defined in include/luisa/dsl/struct.h alongside the struct definition machinery. Additional statement builders that consume these types reside in include/luisa/dsl/stmt.h.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →