Skip to content

Commit 5b0a122

Browse files
committed
Merge branch 'main' into linda/shape-validation
2 parents 8524673 + 4bd1ea7 commit 5b0a122

28 files changed

+1108
-423
lines changed

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* [Introduction](introduction.md)
55
* [Construction](construction.md)
66
* [Circuit building](circuit_building.md)
7+
* [Trace generation](trace_generation.md)
78
* [Handling arbitrary programs](extensions.md)
89
* [Debugging](debugging.md)
910
* [Benchmarks](benchmark.md)

book/src/trace_generation.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Trace Generation Pipeline
2+
3+
After building a circuit, the next step is execution: running the program with concrete inputs and generating the execution traces needed for proving. This section describes the complete flow from a static `Circuit` specification to the final `Traces` structure.
4+
5+
## Overview
6+
7+
The trace generation pipeline consists of three distinct phases:
8+
9+
1. **Circuit compilation** — Transform high-level circuit expressions into a fixed intermediate representation.
10+
2. **Circuit execution** — Populate the witness table by evaluating operations with concrete input values.
11+
3. **Trace extraction** — Generate operation-specific traces from the populated witness table.
12+
13+
Each phase has a clear responsibility and produces well-defined outputs that feed into the next stage.
14+
15+
## Phase 1: Circuit Compilation
16+
17+
Circuit compilation happens when calling `builder.build()`. This phase translates circuit expressions into a deterministic sequence of primitive and non-primitive operations.
18+
19+
The compilation process is described in detail in the [Circuit Building](./circuit_building.md#building-pipeline) section. In summary, it performs three successive passes to lower expressions to primitives, handle non-primitive operations, and optimize the resulting graph.
20+
21+
The output is a `Circuit<F>` containing the primitive operations in topological order, non-primitive operation specifications, and witness allocation metadata. This circuit is a static, serializable specification that can be executed multiple times with different inputs.
22+
23+
## Phase 2: Circuit Execution
24+
25+
Circuit execution happens when calling `runner.run()`. This phase populates the witness table by evaluating each operation with concrete field element values.
26+
27+
The runner is initialized with a `Circuit<F>` and receives:
28+
- Public input values via `runner.set_public_inputs()`
29+
- Private data for non-primitive operations via `runner.set_non_primitive_op_private_data()`
30+
31+
The runner iterates through primitive operations in topological order, executing each one to populate witness slots. Operations can run in **forward mode** (computing outputs from inputs) or **backward mode** (inferring missing inputs from outputs), allowing bidirectional constraint solving.
32+
33+
The output is a fully populated witness table where every slot contains a concrete field element. Any unset witness triggers a `WitnessNotSet` error.
34+
35+
## Phase 3: Trace Extraction
36+
37+
Trace extraction happens internally within `runner.run()` after execution completes. This phase delegates to specialized trace builders that transform the populated witness table into operation-specific trace tables.
38+
39+
Each primitive operation has a dedicated builder that extracts its operations from the IR and produces trace columns:
40+
41+
- **WitnessTraceBuilder** — Generates the central [witness table](./construction.md#witness-table) with sequential `(index, value)` pairs
42+
- **ConstTraceBuilder** — Extracts constants (both columns preprocessed)
43+
- **PublicTraceBuilder** — Extracts public inputs (index preprocessed, values at runtime)
44+
- **AddTraceBuilder** — Extracts additions with six columns: `(lhs_index, lhs_value, rhs_index, rhs_value, result_index, result_value)`
45+
- **MulTraceBuilder** — Extracts multiplications with the same six-column structure
46+
47+
Non-primitive operations require custom trace builders. For example, **MmcsTraceBuilder** validates and extracts MMCS path verification traces. Custom trace builders follow the same pattern, operating independently in a single pass to produce isolated trace tables. All index columns are preprocessed since the IR is fixed and known to the verifier.
48+
49+
The output is a `Traces<F>` structure containing all execution traces needed by the prover to generate STARK proofs for each [operation-specific chip](./construction.md#operation-specific-stark-chips).
50+
51+
## Example: Fibonacci Circuit
52+
53+
Consider a simple Fibonacci circuit computing `F(5)`:
54+
55+
```rust,ignore
56+
let mut builder = CircuitBuilder::new();
57+
58+
let expected = builder.add_public_input();
59+
let mut a = builder.add_const(F::ZERO);
60+
let mut b = builder.add_const(F::ONE);
61+
62+
for _ in 2..=5 {
63+
let next = builder.add(a, b);
64+
a = b;
65+
b = next;
66+
}
67+
68+
builder.connect(b, expected);
69+
let circuit = builder.build()?;
70+
```
71+
72+
**Phase 1: Compilation** produces:
73+
```text
74+
primitive_ops: [
75+
Const { out: w0, val: 0 },
76+
Const { out: w1, val: 1 },
77+
Public { out: w2, public_pos: 0 },
78+
Add { a: w0, b: w1, out: w3 }, // F(2) = 0 + 1
79+
Add { a: w1, b: w3, out: w4 }, // F(3) = 1 + 1
80+
Add { a: w3, b: w4, out: w5 }, // F(4) = 1 + 2
81+
Add { a: w4, b: w5, out: w2 }, // F(5) = 2 + 3 (connects to expected)
82+
]
83+
witness_count: 6
84+
```
85+
86+
**Phase 2: Execution** with `runner.set_public_inputs(&[F::from(5)])`:
87+
```text
88+
witness[0] = Some(0)
89+
witness[1] = Some(1)
90+
witness[2] = Some(5)
91+
witness[3] = Some(1)
92+
witness[4] = Some(2)
93+
witness[5] = Some(3)
94+
```
95+
96+
**Phase 3: Trace Extraction** produces:
97+
```text
98+
const_trace: [(w0, 0), (w1, 1)]
99+
public_trace: [(w2, 5)]
100+
add_trace: [
101+
(w0, 0, w1, 1, w3, 1),
102+
(w1, 1, w3, 1, w4, 2),
103+
(w3, 1, w4, 2, w5, 3),
104+
(w4, 2, w5, 3, w2, 5),
105+
]
106+
mul_trace: []
107+
```
108+
109+
The witness table acts as the central bus, with each operation table containing [lookups](./construction.md#lookups) into it. The aggregated lookup argument enforces that all these lookups are consistent.
110+
111+
## Key Properties
112+
113+
**Determinism** — Given the same circuit and inputs, trace generation is completely deterministic, ensuring reproducible proofs.
114+
115+
**Separation of concerns** — Each phase has a single responsibility: compilation handles expression lowering, execution populates concrete values, and trace builders format data for proving.
116+
117+
**Builder pattern efficiency** — Trace builders operate in a single pass using only the data they need. No builder depends on another's output, enabling future parallelization.
118+
119+
**Preprocessed columns** — All index columns in operation traces are preprocessed. Since the IR is fixed, the verifier can reconstruct these columns without online commitments, significantly reducing proof size.

circuit/src/alloc_entry.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub enum AllocationType {
2121
Mul,
2222
Div,
2323
NonPrimitiveOp(NonPrimitiveOpType),
24+
WitnessHint,
2425
}
2526

2627
/// Detailed allocation entry for debugging
@@ -92,6 +93,7 @@ fn dump_internal_log(allocation_log: &[AllocationEntry]) {
9293
let mut muls = Vec::new();
9394
let mut divs = Vec::new();
9495
let mut non_primitives = Vec::new();
96+
let mut witness_hints = Vec::new();
9597

9698
fn display_label(label: &str) -> String {
9799
if label.is_empty() {
@@ -110,6 +112,7 @@ fn dump_internal_log(allocation_log: &[AllocationEntry]) {
110112
AllocationType::Mul => muls.push(entry),
111113
AllocationType::Div => divs.push(entry),
112114
AllocationType::NonPrimitiveOp(_) => non_primitives.push(entry),
115+
AllocationType::WitnessHint => witness_hints.push(entry),
113116
}
114117
}
115118

@@ -255,6 +258,18 @@ fn dump_internal_log(allocation_log: &[AllocationEntry]) {
255258
}
256259
tracing::debug!("");
257260
}
261+
262+
if !witness_hints.is_empty() {
263+
tracing::debug!("--- Witness Hints ({}) ---", witness_hints.len());
264+
for entry in witness_hints {
265+
tracing::debug!(
266+
" expr_{} (WitnessHint){}",
267+
entry.expr_id.0,
268+
display_label(entry.label)
269+
);
270+
}
271+
tracing::debug!("");
272+
}
258273
}
259274

260275
/// List all unique scopes present in the allocation log.

circuit/src/builder/circuit_builder.rs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use alloc::vec::Vec;
2+
use core::hash::Hash;
23

34
use hashbrown::HashMap;
4-
use p3_field::PrimeCharacteristicRing;
5+
use p3_field::{Field, PrimeCharacteristicRing};
56

67
use super::compiler::{ExpressionLowerer, NonPrimitiveLowerer, Optimizer};
78
use super::{BuilderConfig, ExpressionBuilder, PublicInputTracker};
@@ -115,6 +116,18 @@ where
115116
self.public_tracker.count()
116117
}
117118

119+
/// Allocates a witness hint (uninitialized witness slot set during non-primitive execution).
120+
#[must_use]
121+
pub fn alloc_witness_hint(&mut self, label: &'static str) -> ExprId {
122+
self.expr_builder.add_witness_hint(label)
123+
}
124+
125+
/// Allocates multiple witness hints.
126+
#[must_use]
127+
pub fn alloc_witness_hints(&mut self, count: usize, label: &'static str) -> Vec<ExprId> {
128+
self.expr_builder.add_witness_hints(count, label)
129+
}
130+
118131
/// Adds a constant to the circuit (deduplicated).
119132
///
120133
/// If this value was previously added, returns the original ExprId.
@@ -292,7 +305,7 @@ where
292305

293306
impl<F> CircuitBuilder<F>
294307
where
295-
F: Clone + PrimeCharacteristicRing + PartialEq + Eq + core::hash::Hash,
308+
F: Field + Clone + PrimeCharacteristicRing + PartialEq + Eq + Hash,
296309
{
297310
/// Builds the circuit into a Circuit with separate lowering and IR transformation stages.
298311
/// Returns an error if lowering fails due to an internal inconsistency.
@@ -326,7 +339,7 @@ where
326339
let primitive_ops = optimizer.optimize(primitive_ops);
327340

328341
// Stage 4: Generate final circuit
329-
let mut circuit = Circuit::new(witness_count);
342+
let mut circuit = Circuit::new(witness_count, expr_to_widx);
330343
circuit.primitive_ops = primitive_ops;
331344
circuit.non_primitive_ops = lowered_non_primitive_ops;
332345
circuit.public_rows = public_rows;
@@ -471,7 +484,7 @@ mod tests {
471484
assert!(circuit.enabled_ops.is_empty());
472485

473486
match &circuit.primitive_ops[0] {
474-
crate::op::Prim::Const { out, val } => {
487+
crate::op::Op::Const { out, val } => {
475488
assert_eq!(*out, WitnessId(0));
476489
assert_eq!(*val, BabyBear::ZERO);
477490
}
@@ -494,23 +507,23 @@ mod tests {
494507
assert_eq!(circuit.primitive_ops.len(), 3);
495508

496509
match &circuit.primitive_ops[0] {
497-
crate::op::Prim::Const { out, val } => {
510+
crate::op::Op::Const { out, val } => {
498511
assert_eq!(*out, WitnessId(0));
499512
assert_eq!(*val, BabyBear::ZERO);
500513
}
501514
_ => panic!("Expected Const at index 0"),
502515
}
503516

504517
match &circuit.primitive_ops[1] {
505-
crate::op::Prim::Public { out, public_pos } => {
518+
crate::op::Op::Public { out, public_pos } => {
506519
assert_eq!(*out, WitnessId(1));
507520
assert_eq!(*public_pos, 0);
508521
}
509522
_ => panic!("Expected Public at index 1"),
510523
}
511524

512525
match &circuit.primitive_ops[2] {
513-
crate::op::Prim::Public { out, public_pos } => {
526+
crate::op::Op::Public { out, public_pos } => {
514527
assert_eq!(*out, WitnessId(2));
515528
assert_eq!(*public_pos, 1);
516529
}
@@ -536,23 +549,23 @@ mod tests {
536549
assert_eq!(circuit.primitive_ops.len(), 3);
537550

538551
match &circuit.primitive_ops[0] {
539-
crate::op::Prim::Const { out, val } => {
552+
crate::op::Op::Const { out, val } => {
540553
assert_eq!(*out, WitnessId(0));
541554
assert_eq!(*val, BabyBear::ZERO);
542555
}
543556
_ => panic!("Expected Const at index 0"),
544557
}
545558

546559
match &circuit.primitive_ops[1] {
547-
crate::op::Prim::Const { out, val } => {
560+
crate::op::Op::Const { out, val } => {
548561
assert_eq!(*out, WitnessId(1));
549562
assert_eq!(*val, BabyBear::from_u64(1));
550563
}
551564
_ => panic!("Expected Const at index 1"),
552565
}
553566

554567
match &circuit.primitive_ops[2] {
555-
crate::op::Prim::Const { out, val } => {
568+
crate::op::Op::Const { out, val } => {
556569
assert_eq!(*out, WitnessId(2));
557570
assert_eq!(*val, BabyBear::from_u64(2));
558571
}
@@ -574,7 +587,7 @@ mod tests {
574587
assert_eq!(circuit.primitive_ops.len(), 4);
575588

576589
match &circuit.primitive_ops[3] {
577-
crate::op::Prim::Add { out, a, b } => {
590+
crate::op::Op::Add { out, a, b } => {
578591
assert_eq!(*out, WitnessId(3));
579592
assert_eq!(*a, WitnessId(1));
580593
assert_eq!(*b, WitnessId(2));

0 commit comments

Comments
 (0)