Skip to content

Commit 9d18658

Browse files
author
Dmytro Gordon
committed
Add save command to the examples
1 parent f244472 commit 9d18658

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

crates/examples/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,48 @@ cargo run --release --example my_circuit -- --help
274274
RUST_LOG=info cargo run --release --example my_circuit
275275
```
276276

277+
## CLI subcommands
278+
279+
All example binaries share a common CLI with these subcommands:
280+
281+
- prove (default): build the circuit, generate witness, create and verify a proof
282+
- stat: print circuit statistics
283+
- composition: output circuit composition as JSON
284+
- check-snapshot: compare current stats with the stored snapshot
285+
- bless-snapshot: update the stored snapshot with current stats
286+
- save: save artifacts to files (only those explicitly requested)
287+
288+
### Save artifacts
289+
290+
Use the save subcommand to write selected artifacts to disk. Nothing is written unless a corresponding path is provided.
291+
292+
Flags:
293+
- --cs-path PATH: write the constraint system binary
294+
- --pub-witness-path PATH: write the public values (ValuesData) binary
295+
- --non-pub-data-path PATH: write the non-public values (ValuesData) binary
296+
297+
Examples:
298+
299+
```bash
300+
# Save only the constraint system
301+
cargo run --release --example my_circuit -- save --cs-path out/cs.bin
302+
303+
# Save public values and non-public values
304+
cargo run --release --example my_circuit -- save \
305+
--pub-witness-path out/public.bin \
306+
--non-pub-data-path out/non_public.bin
307+
308+
# Save all three
309+
cargo run --release --example my_circuit -- save \
310+
--cs-path out/cs.bin \
311+
--pub-witness-path out/public.bin \
312+
--non-pub-data-path out/non_public.bin
313+
```
314+
315+
Notes:
316+
- Public and non-public outputs are serialized using the versioned ValuesData format from core.
317+
- Parent directories are created automatically if they don’t exist.
318+
277319
## Adding to Cargo.toml
278320

279321
Add your example to `crates/examples/Cargo.toml`:

crates/examples/src/cli.rs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1+
use std::{fs, path::Path};
2+
13
use anyhow::Result;
4+
use binius_core::constraint_system::{ValueVec, ValuesData};
25
use binius_frontend::{compiler::CircuitBuilder, stat::CircuitStat};
6+
use binius_utils::serialization::SerializeBytes;
37
use clap::{Arg, Args, Command, FromArgMatches, Subcommand};
48

59
use crate::{ExampleCircuit, prove_verify, setup};
610

11+
/// Serialize a value implementing `SerializeBytes` and write it to the given path.
12+
fn write_serialized<T: SerializeBytes>(value: &T, path: &str) -> Result<()> {
13+
if let Some(parent) = Path::new(path).parent()
14+
&& !parent.as_os_str().is_empty()
15+
{
16+
fs::create_dir_all(parent)?;
17+
}
18+
let mut buf: Vec<u8> = Vec::new();
19+
value.serialize(&mut buf)?;
20+
fs::write(path, &buf)?;
21+
Ok(())
22+
}
23+
724
/// A CLI builder for circuit examples that handles all command-line parsing and execution.
825
///
926
/// This provides a clean API for circuit examples where developers only need to:
@@ -74,6 +91,27 @@ enum Commands {
7491
#[command(flatten)]
7592
params: CommandArgs,
7693
},
94+
95+
/// Save constraint system, public witness, and non-public data to files if paths are provided
96+
Save {
97+
/// Output path for the constraint system binary
98+
#[arg(long = "cs-path")]
99+
cs_path: Option<String>,
100+
101+
/// Output path for the public witness binary
102+
#[arg(long = "pub-witness-path")]
103+
pub_witness_path: Option<String>,
104+
105+
/// Output path for the non-public data (witness + internal) binary
106+
#[arg(long = "non-pub-data-path")]
107+
non_pub_data_path: Option<String>,
108+
109+
#[command(flatten)]
110+
params: CommandArgs,
111+
112+
#[command(flatten)]
113+
instance: CommandArgs,
114+
},
77115
}
78116

79117
/// Wrapper for dynamic command arguments
@@ -102,13 +140,15 @@ where
102140
let composition_cmd = Self::build_composition_subcommand();
103141
let check_snapshot_cmd = Self::build_check_snapshot_subcommand();
104142
let bless_snapshot_cmd = Self::build_bless_snapshot_subcommand();
143+
let save_cmd = Self::build_save_subcommand();
105144

106145
let command = command
107146
.subcommand(prove_cmd)
108147
.subcommand(stat_cmd)
109148
.subcommand(composition_cmd)
110149
.subcommand(check_snapshot_cmd)
111-
.subcommand(bless_snapshot_cmd);
150+
.subcommand(bless_snapshot_cmd)
151+
.subcommand(save_cmd);
112152

113153
// Also add top-level args for default prove behavior
114154
let command = command.arg(
@@ -171,6 +211,34 @@ where
171211
E::Params::augment_args(cmd)
172212
}
173213

214+
fn build_save_subcommand() -> Command {
215+
let mut cmd = Command::new("save").about(
216+
"Save constraint system, public witness, and non-public data to files if paths are provided",
217+
);
218+
cmd = cmd
219+
.arg(
220+
Arg::new("cs_path")
221+
.long("cs-path")
222+
.value_name("PATH")
223+
.help("Output path for the constraint system binary"),
224+
)
225+
.arg(
226+
Arg::new("pub_witness_path")
227+
.long("pub-witness-path")
228+
.value_name("PATH")
229+
.help("Output path for the public witness binary"),
230+
)
231+
.arg(
232+
Arg::new("non_pub_data_path")
233+
.long("non-pub-data-path")
234+
.value_name("PATH")
235+
.help("Output path for the non-public data (witness + internal) binary"),
236+
);
237+
cmd = E::Params::augment_args(cmd);
238+
cmd = E::Instance::augment_args(cmd);
239+
cmd
240+
}
241+
174242
/// Set the about/description text for the command.
175243
///
176244
/// This appears in the help output.
@@ -212,6 +280,7 @@ where
212280
Some(("bless-snapshot", sub_matches)) => {
213281
Self::run_bless_snapshot_impl(sub_matches.clone(), circuit_name)
214282
}
283+
Some(("save", sub_matches)) => Self::run_save(sub_matches.clone()),
215284
Some((cmd, _)) => anyhow::bail!("Unknown subcommand: {}", cmd),
216285
None => {
217286
// No subcommand - default to prove behavior for backward compatibility
@@ -319,6 +388,51 @@ where
319388
Ok(())
320389
}
321390

391+
fn run_save(matches: clap::ArgMatches) -> Result<()> {
392+
// Extract optional output paths
393+
let cs_path = matches.get_one::<String>("cs_path").cloned();
394+
let pub_witness_path = matches.get_one::<String>("pub_witness_path").cloned();
395+
let non_pub_data_path = matches.get_one::<String>("non_pub_data_path").cloned();
396+
397+
// If nothing to save, exit early
398+
if cs_path.is_none() && pub_witness_path.is_none() && non_pub_data_path.is_none() {
399+
tracing::info!("No output paths provided; nothing to save");
400+
return Ok(());
401+
}
402+
403+
// Parse Params and Instance
404+
let params = E::Params::from_arg_matches(&matches)?;
405+
let instance = E::Instance::from_arg_matches(&matches)?;
406+
407+
// Build circuit
408+
let mut builder = CircuitBuilder::new();
409+
let example = E::build(params, &mut builder)?;
410+
let circuit = builder.build();
411+
412+
// Generate witness
413+
let mut filler = circuit.new_witness_filler();
414+
example.populate_witness(instance, &mut filler)?;
415+
circuit.populate_wire_witness(&mut filler)?;
416+
let witness: ValueVec = filler.into_value_vec();
417+
418+
// Conditionally write artifacts
419+
if let Some(path) = cs_path.as_deref() {
420+
write_serialized(circuit.constraint_system(), path)?;
421+
}
422+
423+
if let Some(path) = pub_witness_path.as_deref() {
424+
let data = ValuesData::from(witness.public());
425+
write_serialized(&data, path)?;
426+
}
427+
428+
if let Some(path) = non_pub_data_path.as_deref() {
429+
let data = ValuesData::from(witness.non_public());
430+
write_serialized(&data, path)?;
431+
}
432+
433+
Ok(())
434+
}
435+
322436
/// Parse arguments and run the circuit example.
323437
///
324438
/// This orchestrates the entire flow:

0 commit comments

Comments
 (0)