Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2f57021
Add ztest example and discovery demo
karandeol-26 Jul 14, 2026
dd8b3f7
Update .gitignore
karandeol-26 Jul 14, 2026
332040c
Merge branch 'zok_unit_test_runner' of https://github.com/sschefflab/…
karandeol-26 Jul 14, 2026
27b3b86
Add test runner and eval for @test inputs
karandeol-26 Jul 16, 2026
d396732
Add end-to-end test runner and array support
karandeol-26 Jul 17, 2026
e84472f
Merge branch 'master' into zok_unit_test_runner
karandeol-26 Jul 17, 2026
6c83f30
Merge branch 'zok_unit_test_runner' of https://github.com/sschefflab/…
karandeol-26 Jul 17, 2026
5e3f639
Update coverage_test.zok
karandeol-26 Jul 17, 2026
3ba60f8
Remove duplicate assertion in test case
karandeol-26 Jul 17, 2026
08236ec
Refactor comments to use Rust doc comments format
karandeol-26 Jul 17, 2026
49ca19b
Clarify documentation on value-only array handling
karandeol-26 Jul 21, 2026
ae9cbc2
Fix formatting in documentation comment
karandeol-26 Jul 21, 2026
020f58f
Update comments for zero-length array check
karandeol-26 Jul 21, 2026
0ab8cbf
Refactor comments and update outcome descriptions
karandeol-26 Jul 21, 2026
9cd54fe
Update comment on generic test functions rejection
karandeol-26 Jul 21, 2026
bf16d69
Remove performance notes from ztest.rs
karandeol-26 Jul 21, 2026
d2bcd53
Add TODO for error handling refactor in catch function
karandeol-26 Jul 21, 2026
f66c1d9
Refactor comments in walk_function_definition
karandeol-26 Jul 22, 2026
9700b47
Revise flatten function documentation
karandeol-26 Jul 22, 2026
85cd72e
Remove return type restriction in run_test function
karandeol-26 Jul 22, 2026
8029862
Refactor TestCase struct and improve documentation
karandeol-26 Jul 22, 2026
51d9b3a
Reject return-typed test functions during discovery
karandeol-26 Jul 22, 2026
1e0fe83
Refine documentation for public inputs and tests
karandeol-26 Jul 22, 2026
88aa167
Refactor comments in test runner module
karandeol-26 Jul 22, 2026
4995d41
Refine documentation for proof-mode optimization
karandeol-26 Jul 22, 2026
2f56252
Refine comments in ztest_e2e.rs for clarity
karandeol-26 Jul 22, 2026
97c586e
Support tuple test inputs
karandeol-26 Jul 23, 2026
0e83e78
Support per-test backend selection (mirage/groth16)
karandeol-26 Jul 23, 2026
883ff16
Remove singleton-tuple special-case and update tests
karandeol-26 Jul 27, 2026
a80c190
Add Mirage and Groth16 challenge e2e tests
karandeol-26 Jul 28, 2026
93b84cc
Refactor challenge tests for Mirage backend
karandeol-26 Jul 29, 2026
c8bcfc7
Clarify assertion checks before optimization
karandeol-26 Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,17 @@ required-features = ["lp", "aby"]
name = "r1cs_inspect"
required-features = ["r1cs"]

[[example]]
name = "ztest"
required-features = ["smt", "zokc", "bellman"]

[[test]]
name = "zok_test_inputs"
required-features = ["smt", "zokc"]

[[test]]
name = "ztest_e2e"
required-features = ["smt", "zokc", "bellman"]

[profile.release]
debug = true
99 changes: 99 additions & 0 deletions examples/ZoKratesCurly/pf/coverage_test.zok
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Simple @test coverage: backend selection, scalar, array, and tuple inputs,
// and private/public visibility. All tests pass. Run:
// cargo run --example ztest --features zokc -- examples/ZoKratesCurly/pf/coverage_test.zok

// ---- scalar inputs ----

@test x = 5;
def test_field(private field x) {
assert(x == 5);
}

@test(backend = mirage) a = 4, b = 6;
def test_add(private field a, private field b) {
assert(a + b == 10);
}

@test b = true;
def test_bool(private bool b) {
assert(b);
}

@test x = 7u32;
def test_u32(private u32 x) {
assert(x * 2 == 14u32);
}

// ---- scalar visibility corners ----

@test a = 3, b = 4;
def test_all_public(public field a, public field b) {
assert(a + b == 7);
}

@test a = 3, b = 4;
def test_mixed_visibility(private field a, public field b) {
assert(a + b == 7);
}

// ---- array inputs ----

@test xs = [1, 2, 3];
def test_array(private field[3] xs) {
assert(xs[0] + xs[1] == xs[2]);
}

@test A = [[1, 2], [3, 4]];
def test_nested_array(private field[2][2] A) {
assert(A[0][0] == 1);
assert(A[1][1] == 4);
}

@test xs = [1, 2];
def test_public_array(public field[2] xs) {
assert(xs[0] + xs[1] == 3);
}

// ---- array + scalar together ----

@test k = 10, xs = [1, 2];
def test_array_and_scalar(private field k, public field[2] xs) {
assert(k + xs[0] == 11);
assert(k + xs[1] == 12);
}

// ---- tuple inputs ----

@test t = (4, true);
def test_tuple(private (field, bool) t) {
assert(t.0 == 4);
assert(t.1);
}

// A singleton tuple needs the trailing comma: (5,) is a 1-tuple, (5) is
// just a parenthesized expression.
@test t = (5,);
def test_singleton_tuple(private (field,) t) {
assert(t.0 == 5);
}

@test t = ([1, 2], (true, 3));
def test_nested_tuple(private (field[2], (bool, u32)) t) {
assert(t.0[0] + t.0[1] == 3);
assert(t.1.0);
assert(t.1.1 == 3u32);
}

@test pairs = [(1, 2), (3, 4)];
def test_array_of_tuples(private (field, u32)[2] pairs) {
assert(pairs[0].0 == 1);
assert(pairs[0].1 == 2u32);
assert(pairs[1].0 == 3);
assert(pairs[1].1 == 4u32);
}

@test t = (7, 9);
def test_public_tuple(public (field, u32) t) {
assert(t.0 == 7);
assert(t.1 == 9u32);
}
16 changes: 16 additions & 0 deletions examples/ZoKratesCurly/pf/discovery_demo.zok
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Demo file for @test discovery (examples/ztest.rs).
// Two functions are marked @test; `helper` is not and should be skipped.

@test a = 4, b = 5;
def test_mul(private field a, private field b) -> field {
return a * b;
}

@test x = 2, y = 3 * 3;
def test_add(private field x, private field y) -> field {
return x + y;
}

def helper(field n) -> field {
return n + 1;
}
44 changes: 10 additions & 34 deletions examples/circ.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,37 +326,7 @@ fn main() {
// vec![Opt::Sha, Opt::ConstantFold, Opt::Mem, Opt::ConstantFold],
)
}
Mode::Proof | Mode::ProofOfHighValue(_) => {
let mut opts = Vec::new();

opts.push(Opt::ConstantFold(Box::new([])));
opts.push(Opt::DeskolemizeWitnesses);
opts.push(Opt::ScalarizeVars);
opts.push(Opt::Flatten);
opts.push(Opt::Sha);
opts.push(Opt::ConstantFold(Box::new([])));
opts.push(Opt::ParseCondStores);
// Tuples must be eliminated before oblivious array elim
opts.push(Opt::ConstantFold(Box::new([])));
opts.push(Opt::Obliv);
// The obliv elim pass produces more tuples, that must be eliminated
opts.push(Opt::SetMembership);
opts.push(Opt::PersistentRam);
opts.push(Opt::VolatileRam);
if options.circ.ir.fits_in_bits_ip {
opts.push(Opt::FitsInBitsIp);
}
opts.push(Opt::SkolemizeChallenges);
opts.push(Opt::ScalarizeVars);
opts.push(Opt::ConstantFold(Box::new([])));
opts.push(Opt::Obliv);
opts.push(Opt::LinearScan);
// The linear scan pass produces more tuples, that must be eliminated
opts.push(Opt::Tuple);
opts.push(Opt::Flatten);
opts.push(Opt::ConstantFold(Box::new([])));
opt(cs, opts)
}
Mode::Proof | Mode::ProofOfHighValue(_) => circ::compile::opt_for_proof(cs, cfg()),
};
println!("Running backend");
match options.backend {
Expand Down Expand Up @@ -427,9 +397,13 @@ fn main() {
)
.unwrap(),
#[cfg(not(feature = "spartan"))]
ProofImpl::Spartan | ProofImpl::Dorian => panic!("Missing feature: spartan"),
ProofImpl::Spartan | ProofImpl::Dorian => {
panic!("Missing feature: spartan")
}
#[cfg(not(feature = "bellman"))]
ProofImpl::Groth16 | ProofImpl::Mirage => panic!("Missing feature: bellman"),
ProofImpl::Groth16 | ProofImpl::Mirage => {
panic!("Missing feature: bellman")
}
};
}
#[cfg(feature = "bellman")]
Expand All @@ -444,7 +418,9 @@ fn main() {
verifier_key,
)
.unwrap(),
ProofImpl::Spartan | ProofImpl::Dorian => panic!("Spartan/Dorian is not CP"),
ProofImpl::Spartan | ProofImpl::Dorian => {
panic!("Spartan/Dorian is not CP")
}
};
}
#[cfg(not(feature = "bellman"))]
Expand Down
167 changes: 167 additions & 0 deletions examples/ztest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! Unit-test runner for ZoKratesCurly programs.
//! Reads a .zok file, finds every function marked with a @test annotation,
//! evaluates its annotation inputs to concrete values, then runs each test
//! through the full in-memory proof pipeline:
//! compile (test fn as entry point) -> assert check -> setup -> prove -> verify
//! and prints ok / FAILED per test. A test passes when its assertions hold for
//! the supplied inputs and its proof verifies. Assertions are evaluated
//! directly on the unoptimized IR before proof generation. Tests use
//! Groth16 by default and may select Mirage in the annotation.
//!
//! The per-test execution logic lives in [`circ::test_runner`]; this file is
//! the CLI wrapper.
use bls12_381::Bls12;
use circ::cfg::{clap, CircOpt};
use circ::front::zsharpcurly::{TestBackend, ZSharpCurlyFE};
use circ::front::Mode;
use circ::ir::term::Value;
use circ::target::r1cs::{bellman::Bellman, mirage::Mirage};
use circ::test_runner::{catch, run_test, Outcome};
use clap::Parser;
use std::path::PathBuf;

#[derive(Debug, Parser)]
#[command(
name = "ztest",
about = "Run @test functions in a ZoKratesCurly program"
)]
struct Options {
/// Input file
#[arg(name = "PATH")]
path: PathBuf,

#[command(flatten)]
circ: CircOpt,
}

/// Render a concrete Value as a plain number/bool, without the field
/// modulus that Value's own Display appends (e.g. `9` instead of `#f9m524...`).
fn pretty_value(v: &Value) -> String {
match v {
Value::Field(f) => f.i().to_string(),
Value::BitVector(b) => b.uint().to_string(),
Value::Bool(b) => b.to_string(),
Value::Array(a) => format!(
"[{}]",
a.values()
.iter()
.map(pretty_value)
.collect::<Vec<_>>()
.join(", ")
),
// Singletons print as `(5,)` — the trailing comma matches how they
// must be written in source.
Value::Tuple(vs) if vs.len() == 1 => format!("({},)", pretty_value(&vs[0])),
Value::Tuple(vs) => format!(
"({})",
vs.iter().map(pretty_value).collect::<Vec<_>>().join(", ")
),
// Anything else (structs, ...): fall back to the default form.
other => other.to_string(),
}
}

fn main() {
env_logger::Builder::from_default_env()
.format_level(false)
.format_timestamp(None)
.init();

let options = Options::parse();
circ::cfg::set(&options.circ);

// The frontend panics on a missing file with an unhelpful unwrap
// message; check up front so the user gets a plain answer.
if !options.path.exists() {
eprintln!("Error: file not found: {}", options.path.display());
std::process::exit(1);
}

// Find every @test function and evaluate its annotation inputs to
// concrete values. Parse errors surface as panics, evaluation errors
// through the Result.
let tests = match catch(|| ZSharpCurlyFE::eval_test_inputs(options.path.clone(), Mode::Proof)) {
Ok(Ok(tests)) => tests,
Ok(Err(e)) | Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
};

println!(
"running {} test{} from {}",
tests.len(),
if tests.len() == 1 { "" } else { "s" },
options.path.display()
);

let (mut passed, mut failed, mut errored) = (0, 0, 0);
for t in &tests {
// Show each input as `name = <source> = <value>`, collapsing to
// `name = <value>` when the source is already just the value.
let inputs: Vec<String> = t
.inputs()
.iter()
.map(|input| {
let value = pretty_value(input.value());
if input.source() == value {
format!("{} = {}", input.name(), value)
} else {
format!("{} = {} = {}", input.name(), input.source(), value)
}
})
.collect();
print!(
"test {} [{}] ({}) ... ",
t.name(),
t.settings().backend(),
inputs.join(", ")
);
// Flush so the prover's own stderr diagnostics (printed when a
// constraint fails) appear under this test's line, not before it.
use std::io::Write;
let _ = std::io::stdout().flush();

let indent = |msg: String| {
// The prover's message spans several lines; indent them all.
for line in msg.lines() {
println!(" {}", line);
}
};
let outcome = match t.settings().backend() {
TestBackend::Groth16 => run_test::<Bellman<Bls12>>(t),
TestBackend::Mirage => run_test::<Mirage<Bls12>>(t),
};
match outcome {
Outcome::Pass => {
passed += 1;
println!("ok");
}
Outcome::AssertionFailed(msg) => {
failed += 1;
println!("FAILED");
indent(msg);
}
Outcome::CompileError(msg) | Outcome::BackendError(msg) => {
errored += 1;
println!("error");
indent(msg);
}
}
}

println!(
"\ntest result: {}. {} passed; {} failed; {} errored",
if failed == 0 && errored == 0 {
"ok"
} else {
"FAILED"
},
passed,
failed,
errored
);
if failed > 0 || errored > 0 {
std::process::exit(1);
}
}
Loading