how circom works
This note tries to document how the circom code is compiled to constraints and witness calculation executables.
related doc
A paper explain in details on the features of the circom lang: https://www.computer.org/csdl/journal/tq/2023/06/10002421/1Jv6BEAupcA
AST parser
It uses lalrpop parser framework: https://github.com/iden3/circom/blob/736c554551ac8a29a28d9eac53d1700f47c07f89/parser/src/lang.lalrpop#L211
1. this grammar definition file could be used as a clue to find the internal code based on the syntax naming
2. book: https://lalrpop.github.io/lalrpop/
ast nodes
Below are some of the key AST nodes in circom:
assign op
pub enum AssignOp {
AssignVar, // =
AssignSignal, // <--
AssignConstraintSignal, // <==
}
statement
pub enum Statement {
IfThenElse {
...
},
While {
...
},
Return {
...
},
InitializationBlock {
...
},
Declaration {
...
},
Substitution { // this is assignment, such as =
...
},
...
ConstraintEquality { // ===
...
},
LogCall {
...
},
Block { // a block statement can contain a list of this statement structure
...
},
Assert {
...
},
}
In the a Substitution statement, when there is a AssignOp::AssignConstraintSignal, it will add constraints for that statement. Syntactically, this is <==.
In a ConstraintEquality, it will add constraints. Syntactically, this is === .
block type
enum BlockType {
Known,
Unknown,
}
- Not sure what is the logistic behind this structure. But based on the code, when it is
BlockType::Unknown, the block is seen as non-deterministic.
arithmetic expression
Each statement can contain a set of expressions. These expressions can be folded if the involved arithmetic expressions are deterministic.
pub enum ArithmeticExpression<C>
where
C: Hash + Eq,
{
Number {
value: BigInt,
},
Signal {
symbol: C,
},
Linear {
// Represents the expression: c1*s1 + .. + cn*sn + C
// where c1..cn are integers modulo a prime and
// s1..sn are signals. C is a constant value
coefficients: HashMap<C, BigInt>,
},
Quadratic {
// Is a quadratic expression of the form:
// a*b + c
// Where a,b and c are linear expression
a: HashMap<C, BigInt>,
b: HashMap<C, BigInt>,
c: HashMap<C, BigInt>,
},
NonQuadratic,
}
During the folding process, when it encounters an non-deterministic arithmetic such as divide operation, the resulted folded expression will be NonQuadratic.
The side effect of folding these expressions when walking through the statements is to allocate memory slices, which might be similar to witness vector in noname. The funciton perform_assign might provide the clues on how the memory slice works.
/*
Represents the value stored in a element of a circom program.
The attribute route stores the dimensions of the slice, used to navigate through them.
The length of values is equal to multiplying all the values in route.
*/
pub struct MemorySlice<C> {
route: Vec<SliceCapacity>,
values: Vec<C>,
number_inserts: usize,
}
overall compilation pipeline
fn start() -> Result<(), ()> {
// parse ast
let mut program_archive = parser_user::parse_project(&user_input)?;
// type check
type_analysis_user::analyse_project(&mut program_archive)?;
// generate circuit constraints
let circuit = execution_user::execute_project(program_archive, config)?;
// generate witness calculation executable
compilation_user::compile(compilation_config)?;
}
type checker
pub fn check_types(){
// Structural analyses
program_level_analyses(program_archive, &mut errors);
template_level_analyses(program_archive, &mut errors);
function_level_analyses(program_archive, &mut errors);
// Decorators
template_level_decorators(program_archive, &mut errors);
function_level_decorators(program_archive, &mut errors);
// Type analysis
let typing_result = type_check(program_archive);
// Semantics analyses
semantic_analyses(program_archive, &mut errors, &mut warnings);
}
- It also has a type checker, and it looks complicated.
- how does it actually work though.
- maybe it is low level language so it is not at a good position to support more useful type checking.
generic
build_template_instances: maybe this is the same concept as function instantiation for generic in noname: https://github.com/iden3/circom/blob/5991d900d64df43f69e4a9ca2d01fe663820b542/compiler/src/circuit_design/build.rs#L23
circuit / constraint generation
the workflow is like
- walk blocks
- walk statements
- walk expressions
- fold the arithmetic expressions
- when it encounters an arithmetic expression that can't be expressed in quadratic form, it is marked as non quadratic, take div for example:
- stores the expressions as memory slice, which is kind of like a symbolic / witness value vector
- if it is === constraint equality statement
- returns error if it is non quadratic value
- otherwise transform the expression as a constraint
- if it is <==
- returns error if it is non quadratic value
- otherwise transform the expression as a constraint
- walk expressions
The way it manage the memory slice is bit tricky to understand. I am yet to full understand it. It seems to be related to how to allocate the cells for constraints, as well as to wire with the variables to be calculated in the generated witness calculator.
So how are signal / witness vector might be built?
- during constraint generation phase, witness vector will be created for symbolic values including for the non quadratic expressions.
- these symbolic values are recorded as memory slice which maps to the signals.
- during witness generation, I guess it generate the code for the calculator executables with the references to the memory slice for correctly putting the calculated value to the witness vector.
witness generation
Below is the generated c++ code for the Num2Bits circuit.
circom code:

generated c++:

signalValuesmight be corresponding to the witness vector in noname.signalStartsets the start of the witness range in the vector.- the results of non deterministic calculations are assigned to slots of
signalValues, so this seems to be equivalent to the mini VM represented byValuein noname.
How does it generate this witness calculator from circom code?
- I guess it relies a mapping between the memory slice created during the circuit generation and the AST nodes.
- Then it just translates the ast nodes to the target language for the witness calculator, as long as it can assign calculated values to variables mapping correctly to the witness vector, or
signalValues. - in noname, the symbolic values in the witness vector are calculated via the folded
Value, which is like a mini VM that can calculate the instructions.
what is next?
- verify the findings above using a small demo
- how are the arithmetic expressions are folded
- how are the symbolic value vector is created
- how are the memory slice mapping to the expressions?
- what is access information?
- how are the arithmetic expressions transformed to constraints
- how is the witness calculators generated?
conclusion so far
- circom mix non deterministic functions in the code without delimiting the boundary.
- this might exist some tricky assumptions on how to deal with the non deterministic functions in the circom language.
- it would require noname to support hint in full DSL just like circom, in order to transpile as it is.
- so it might not be that straightforward to transpile circom, considering noname would has its own strategy to gauge security setup.