Skip to content

Commit 62edd20

Browse files
committed
hax_compoly_horner_evaluation_PoC
1 parent 27274ff commit 62edd20

17 files changed

Lines changed: 990 additions & 0 deletions

File tree

README_CompPoly_Horner.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Horner algorithm verification (WIP) using CompPoly
2+
3+
This folder documents an ongoing experiment toward an end-to-end verification workflow:
4+
5+
**Rust implementation → hax extraction → CompPoly specification → Lean proofs**
6+
7+
This repository follows a “one repo, multiple PoCs” approach. The Horner PoC currently lives in:
8+
- rust/horner_eval_rs/ — Rust implementation to be extracted with hax
9+
- lean-compoly/ — Lean project intended to host CompPoly specs and (eventually) proofs for the extracted code
10+
11+
Status note: this work is **in progress**. At the moment, the hax-extracted Horner code does not compile in the same Lean project as CompPoly due to Lean toolchain version pinning across libraries. The primary success so far is that we raised issues and coordinated with maintainers, who agreed on a unified Lean version. Once the upgrades land, the pipeline should become feasible without splitting projects.
12+
13+
14+
## Horner Evaluation
15+
16+
Many zk proof systems (including Plonky3-based stacks) rely heavily on polynomial arithmetic. A minimal but representative component is **univariate polynomial evaluation**.
17+
18+
Given coefficients [c0, c1, c2, …], Horner’s method computes:
19+
p(x)=c0+x(c1+x(c2+… ))
20+
21+
We aim to verify that the Rust implementation of Horner evaluation corresponds to a mathematical specification expressed using **CompPoly**.
22+
23+
**Reference (algorithmic origin)**
24+
25+
We use Plonky3-recursion as the [Plonky3-recursion](https://github.com/Plonky3/Plonky3-recursion/blob/d3ccaf73d5f707b4a2018ac65ece55d329fee934/recursion/src/pcs/fri/verifier.rs#L98).
26+
27+
28+
```Rust
29+
fn evaluate_polynomial<EF: Field>(
30+
builder: &mut CircuitBuilder<EF>,
31+
coefficients: &[Target],
32+
point: Target,
33+
) -> Target {
34+
let mut result = coefficients[coefficients.len() - 1];
35+
for &coeff in coefficients.iter().rev().skip(1) {
36+
result = builder.mul(result, point);
37+
result = builder.add(result, coeff);
38+
}
39+
result
40+
}
41+
```
42+
43+
This is Horner evaluation, but implemented over a circuit builder abstraction. For a first PoC we extract a simpler arithmetic-only version.
44+
45+
## Rust side (implementation + extraction target)
46+
47+
**Create an extraction-friendly Rust crate**
48+
49+
```bash
50+
cd zkvm-merkle-lean-verified/rust
51+
cargo new --lib horner_eval_rs
52+
cd horner_eval_rs
53+
cargo add --target 'cfg(hax)' --git https://github.com/hacspec/hax hax-lib
54+
55+
```
56+
57+
**Extraction-friendly Horner implementation**
58+
59+
Because the hax Lean backend is still evolving (and iterator support is limited), we avoid Rust iterators and write the loop using while. This keeps the algorithm identical but reduces extraction complexity.
60+
61+
```Rust
62+
pub fn horner_eval_i64(coeffs: &[i64], x: i64) -> i64 {
63+
// p(x) = a0 + a1*x + ... + an*x^n
64+
// Horner: (((an*x + a_{n-1})*x + ...)*x + a0)
65+
if coeffs.is_empty() {
66+
return 0;
67+
}
68+
69+
let mut i = coeffs.len();
70+
let mut acc = coeffs[i - 1];
71+
72+
while i > 1 {
73+
i -= 1;
74+
acc = acc * x + coeffs[i - 1];
75+
}
76+
acc
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use super::*;
82+
83+
#[test]
84+
fn horner_basic() {
85+
// p(x) = 3 + 2x + x^2, x=10 => 3 + 20 + 100 = 123
86+
let coeffs = [3i64, 2i64, 1i64];
87+
assert_eq!(horner_eval_i64(&coeffs, 10), 123);
88+
}
89+
90+
#[test]
91+
fn empty_is_zero() {
92+
let coeffs: [i64; 0] = [];
93+
assert_eq!(horner_eval_i64(&coeffs, 7), 0);
94+
}
95+
}
96+
```
97+
98+
**Run hax extraction**
99+
100+
We extract to:
101+
- lean-compoly/CompPolyEval/Extracted/Horner_eval_rs.lean
102+
103+
For convenience we added a script:
104+
- scripts/extract_horner_to_compoly.sh
105+
106+
## Lean side (CompPoly specification)
107+
108+
We created a separate Lean project:
109+
110+
- lean-compoly/
111+
112+
and added CompPoly as a dependency.
113+
114+
**Specification module**
115+
116+
We added:
117+
118+
- lean-compoly/CompPolyEval/Spec.lean
119+
120+
This defines a univariate polynomial model (one variable) over ℤ using CompPoly:
121+
122+
- P := CPoly.CMvPolynomial 1 ℤ
123+
- X0 as the variable X in one dimension
124+
- polyOfCoeffs : List ℤ → P building the polynomial from coefficients
125+
- evalAt : ℤ → P → ℤ using CPoly.CMvPolynomial.eval
126+
127+
This enables a clean mathematical statement of Horner correctness in Lean once the extracted code can be compiled in the same environment.
128+
129+
## Proof plan (WIP)
130+
131+
A direct equivalence proof between the extracted Rust function and the CompPoly spec over ℤ will require:
132+
133+
- Panic/exception freedom (or bounds assumptions) for the extracted i64 code, since the hax Lean model uses RustM and overflow-aware operators.
134+
- A semantic bridge from machine integers (i64) to mathematical integers (ℤ), typically under bounds that rule out overflow.
135+
- The main functional correctness theorem: extracted Horner evaluation equals evalAt of polyOfCoeffs.
136+
137+
## Current bottleneck: Lean toolchain version mismatch
138+
139+
At the time of writing, the blocker for a single-project end-to-end pipeline is Lean version pinning across libraries:
140+
141+
- hax Lean prelude was pinned to a single Lean version and is sensitive to Lean changes.
142+
- CompPoly and ArkLib were pinned to different Lean versions.
143+
144+
This prevented compiling the hax-extracted Horner module in the same Lean project as CompPoly.
145+
146+
### Coordination outcome (major progress)
147+
148+
As a result of issues raised during this work, maintainers of hax, CompPoly, and ArkLib agreed to converge on a unified Lean toolchain version:
149+
150+
**Lean 4.26.0**
151+
152+
Once these upgrades land, we expect the end-to-end pipeline:
153+
154+
**Rust → hax → CompPoly spec → proofs**
155+
156+
to become feasible without splitting across incompatible Lean versions.
157+
158+
Reference discussion: hax issue [#1873](https://github.com/cryspen/hax/issues/1873)
159+
160+
## Artifacts in this repository
161+
162+
- rust/horner_eval_rs/ — Rust Horner implementation (tests pass)
163+
- scripts/extract_horner_to_compoly.sh — extraction helper
164+
- lean-compoly/CompPolyEval/Spec.lean — CompPoly-based specification (typechecks)
165+
- lean-compoly/CompPolyEval/Extracted/Horner_eval_rs.lean — hax-extracted Lean code (generated; compilation depends on toolchain alignment)
166+
167+
## Next steps
168+
169+
- Update dependencies once hax/CompPoly/ArkLib are on Lean 4.26.0.
170+
- Compile extracted Horner code and write the first proof obligations:
171+
- panic-freedom / bounds,
172+
- correctness w.r.t. CompPoly eval.
173+
- Add additional normalization lemmas/tactics (simp/grind sets) as needed to support proof automation for Horner and future polynomial components.

lean-compoly/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/.lake

lean-compoly/CompPolyEval.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- This module serves as the root of the `CompPolyEval` library.
2+
-- Import modules here that should be built as part of the library.
3+
import CompPolyEval.Basic
4+
import CompPolyEval.HornerProof
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import CompPoly.Multivariate.CMvPolynomial
2+
import CompPoly.Multivariate.MvPolyEquiv
3+
import CompPoly.Multivariate.Lawful
4+
5+
namespace CompPolyEval
6+
7+
abbrev P := CPoly.CMvPolynomial 1
8+
9+
-- constant polynomial 3 in 1 variable ring
10+
def pConst : P := (CPoly.Lawful.C (n := 1) (R := ℤ) 3)
11+
12+
-- X_0 lives in 1-variable ring, and Lawful.X 0 has type Lawful 1 ℤ
13+
def X0 : P := (CPoly.Lawful.X 0)
14+
15+
def p : P := pConst + (CPoly.Lawful.C (n := 1) (R := ℤ) 2) * X0
16+
17+
#check p
18+
19+
end CompPolyEval
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
-- Experimental lean backend for Hax
3+
-- The Hax prelude library can be found in hax/proof-libs/lean
4+
import Hax
5+
import Std.Tactic.Do
6+
import Std.Do.Triple
7+
import Std.Tactic.Do.Syntax
8+
open Std.Do
9+
open Std.Tactic
10+
11+
set_option mvcgen.warning false
12+
set_option linter.unusedVariables false
13+
14+
def Horner_eval_rs.horner_eval_i64
15+
(coeffs : (RustSlice i64))
16+
(x : i64)
17+
: RustM i64
18+
:= do
19+
if (← (Core.Slice.Impl.is_empty i64 coeffs)) then
20+
(pure (0 : i64))
21+
else
22+
let i : usize ← (Core.Slice.Impl.len i64 coeffs);
23+
let acc : i64 ← coeffs[(← (i -? (1 : usize)))]_?;
24+
let ⟨acc, i⟩ ←
25+
(Rust_primitives.Hax.while_loop
26+
(fun ⟨acc, i⟩ => (do (pure true) : RustM Bool))
27+
(fun ⟨acc, i⟩ => (do
28+
(Rust_primitives.Hax.Machine_int.gt i (1 : usize)) : RustM Bool))
29+
(fun ⟨acc, i⟩ => (do
30+
(Rust_primitives.Hax.Int.from_machine (0 : u32)) : RustM
31+
Hax_lib.Int.Int))
32+
(Rust_primitives.Hax.Tuple2.mk acc i)
33+
(fun ⟨acc, i⟩ => (do
34+
let i : usize ← (i -? (1 : usize));
35+
let acc : i64 ←
36+
((← (acc *? x)) +? (← coeffs[(← (i -? (1 : usize)))]_?));
37+
(pure (Rust_primitives.Hax.Tuple2.mk acc i)) : RustM
38+
(Rust_primitives.Hax.Tuple2 i64 usize))));
39+
(pure acc)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import CompPolyEval.Extracted.Horner_eval_rs
2+
3+
-- Sanity: check the extracted name exists
4+
#check Horner_eval_rs.horner_eval_i64
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import CompPoly.Multivariate.CMvPolynomial
2+
import CompPoly.Multivariate.Lawful
3+
4+
namespace CompPolyEval
5+
6+
abbrev P := CPoly.CMvPolynomial 1
7+
8+
def x0 : Fin 1 := ⟨0, by decide⟩
9+
def X0 : P := CPoly.Lawful.X 0
10+
11+
def evalAt (x : ℤ) (p : P) : ℤ :=
12+
CPoly.CMvPolynomial.eval (R := ℤ) (n := 1) (fun (_ : Fin 1) => x) p
13+
14+
def polyOfCoeffs : List ℤ → P
15+
| [] => (CPoly.Lawful.C (n := 1) (R := ℤ) 0)
16+
| c :: cs => (CPoly.Lawful.C (n := 1) (R := ℤ) c) + X0 * polyOfCoeffs cs
17+
18+
end CompPolyEval

lean-compoly/Main.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import CompPolyEval.Basic
2+
3+
def main : IO Unit :=
4+
IO.println "CompPolyEval build ok"

0 commit comments

Comments
 (0)