Symbolic Variables in Circom

A symbolic variable in Circom is a variable that has been assigned values from a signal.

When a signal is assigned to a variable (thereby turning it into a symbolic variable), the variable becomes a container for that signal and for any arithmetic operations applied to it. A symbolic variable is declared using the var keyword, just like other variables.

For example, the following two circuits are equivalent, i.e. they produce the same underlying R1CS:

template ExampleA() {
    signal input a;
    signal input b;
    signal input c;

    a * b === c;
}

template ExampleB() {
    signal input a;
    signal input b;
    signal input c;

    // symbolic variable v "contains" a * b
    var v = a * b;

    // a * b === c under the hood
    v === c;
}

In ExampleB, the symbolic variable v is simply a placeholder for the expression a * b. Both ExampleA and ExampleB are compiled using the exact same R1CS, and there is zero functional difference between them.

Use cases of symbolic variables

Checking That $\sum\texttt{in}[i]=\texttt{sum}$

Symbolic variables are extremely handy if we want to sum up an array of signals in a loop. In fact, summing signals in a loop is their most common use case:

// assert sum of in === sum
template Sum(n) {
    signal input in[n];
    signal input sum;

    var accumulator;
    for (var i = 0; i < n; i++) {
        accumulator += in[i];
    }

    // in[0] + in[1] + in[2] + ... + in[n - 1] === sum
    accumulator === sum;
}

Checking That in Is a Valid Binary Representation of k

A more interesting example is proving that in[n] is the binary representation of k for a templated value of n. In the circuit below, we check that:

$$ \texttt{in[0]}+2\cdot\texttt{in[1]}+4\cdot\texttt{in[2]} +\dots2^{n-1}\cdot\texttt{in[n-1]} == k $$

If all the signals in in are constrained to be $\set{0,1}$, then that implies in[] is the binary representation of k:

template IsBinaryRepresentation(n) {

    signal input in[n];
    signal input k;

    // in is binary only
    for (var i = 0; i < n; i++) {
        in[i] * (in[i] - 1) === 0;
    }

    // in is the binary representation of k
    var acc; // symbolic variable
    var powersOf2 = 1; // regular variable
    for (var i = 0; i < n; i++) {
        acc += powersOf2 * in[i];
        powersOf2 *= 2;
    }

    acc === k;
}

Why symbolic variables are helpful

Consider the earlier example of proving that $\sum\texttt{in}[i]=\texttt{sum}$. Without symbolic variables, it’s very clumsy to express

sum === in[0] + in[1] + in[2] + ... + in[n-1];

if we don’t know what n is in advance. Even if n was fixed, say at 32, actually typing out 32 variables by hand would be annoying. Thus, symbolic variables enable us to incrementally construct in[0] + in[1] + in[2] + ... without explicitly writing out the signals.

Non quadratic Constraints With Symbolic Variables

Because symbolic variables can “contain” a multiplication between two signals, they can lead to embedding two multiplications into one constraint if we aren’t careful. Consider the following example, that will not compile:

template QViolation() {
    signal input a;
    signal input b;
    signal input c;
    signal input d;

    // v "contains" a * b
    var v = a * b;

    // error: there are two
    // multiplications
    // in this constraint
    v === c * d;
}

In the code above, the symbolic variable v has one multiplication in it and we declared v == a*b. So the constraint v === c * d; is equivalent to a * b = c * d;. Hence, the above code will not compile.

Arbitrary operators are allowed with non-symbolic variables

Doing operations like computing the modulo or bitshifting are allowed with (non-symbolic) variables. However, this means that the variable can no longer be used as part of a constraint:

// this has no constraints
// but it will compile
template Ok() {
    signal input a;
    signal input b;

    var v = a % b;
}

The above example will compile because v is not used in a constraint. However, if we use v in a constraint, the code will not compile. An example of this is shown below:

template NotOk() {
    signal input a;
    signal input b;
    signal input c;

    var v = a % b;

    // non-quadratic constraint
    c === v;
}

Symbolic variables cannot be used to determine the boundary of a loop or the condition

Similarly, only regular variables can be used to determine the boundary of a loop or the condition of an if statement. If a symbolic variable is used, then the code will not compile:

template NotOk() {
    signal input a;
    signal input b;
    signal input c;

    var v = a * b;

    // v is a symbolic variable
    // used in an if statement
    if (v == 0) {
        c === 0;
    } else {
        c === 1;
    }
}

Summary

Symbolic variables are variables that were assigned a value from a signal. They are most frequently used for adding a parameterizable number of signals together, as the sum can be accumulated in a for loop. They are effectively a “container” or “bucket” that holds either a single signal or a collection of signals that are added or multiplied together. If a variable is never assigned a value from a signal, then it is not a symbolic variable.

Since symbolic variables contain signals, care must be taken to avoid quadratic constraint violations when using them.

Circle FFT — Part 1: Building the Circle Domain

Circle FFT — Part 1: Building the Circle Domain Circle STARKs is a new zk-STARK scheme that has been implemented in Stwo and Plonky3, and it has been adopted by several zkVM projects. Its key innovation lies in enabling the use of small 32-bit fields (Mersenne prime $2^{31}-1$) while still maintaining the mathematical properties needed for efficient FFT operations. […]

Multiplicative Subgroups and Primitive Elements

Multiplicative Subgroups and Primitive Elements Introduction This chapter continues our study of group theory by exploring subgroups and generators. The concept of a primitive element will be introduced at the end. We assume you are already familiar with the definition of a group. If you need a refresher, check out this article. To build intuition, […]

Square and Multiply Algorithm

Square and Multiply Algorithm The square and multiply algorithm computes integer exponents in $\mathcal{O}(\log n)$ (logarithmic time). The naive way to compute an exponent $x^n$ is to multiply $x$ by itself, $n$ times, which would require $\mathcal{O}(n)$ time to compute. Suppose we want to compute $x^{20}$. Instead of multiplying $x$ with itself 20 times, we […]

Computing the Current Tick Given sqrtPriceX96

Computing the Current Tick Given sqrtPriceX96 In the previous chapters, we saw that the protocol stores the square root of the price instead of the price itself. Therefore, it is necessary to relate ticks to the square root of the price in the fixed-point Q64.96 format. In this chapter, we will explore how to convert […]

Opportunities

Looking for an audit?

Leverage our extensive network of top security specialists.

Get A Quote
Security Engineer

Asymmetric Research is looking for someone to research and secure Solana/Rust-based smart contracts, build security tools, and lead audits.

Apply Now
Senior Rust Engineer

Irys is looking for a Senior Rust Engineer to lead protocol development, optimise core blockchain systems, and drive technical excellence.

Apply Now
Intermediate Rust Engineer

ZK Email is looking for a Rust engineer to build zero-knowledge tools, implement core features, and contribute to high-impact projects like zk-regex, zk-email, and partner integrations.

Apply Now