#![allow(dead_code, unused_imports)]
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[path = "../parser/mod.rs"]
mod parser;
#[path = "../translator/mod.rs"]
mod translator;
mod cli;
#[derive(Parser)]
#[command(
name = "cargo-spec-lock",
version,
about = "BLVM Spec Lock verification tool",
long_about = None
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
fn resolve_crate_path(crate_path: Option<PathBuf>) -> PathBuf {
crate_path
.or_else(|| {
std::env::var("SPEC_LOCK_CRATE_PATH")
.ok()
.map(PathBuf::from)
})
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
}
fn resolve_spec_paths(spec_paths: Vec<PathBuf>) -> Vec<PathBuf> {
let paths = if !spec_paths.is_empty() {
spec_paths
} else if let Ok(env_val) = std::env::var("SPEC_LOCK_SPEC_PATH") {
env_val
.split([',', ':'])
.map(|s| PathBuf::from(s.trim()))
.filter(|p| !p.as_os_str().is_empty())
.collect()
} else {
return Vec::new();
};
for p in &paths {
if p.is_dir() {
eprintln!(
"error: --spec-path argument `{}` is a directory, not a Markdown spec file.\n\
Hint: pass the crate/workspace root via --crate-path instead:\n\
\n cargo spec-lock verify --spec-path PROTOCOL.md --crate-path {}\n",
p.display(),
p.display(),
);
std::process::exit(2);
}
}
paths
}
fn warn_formula_dep_diagnostics(parser: &parser::orange_paper::SpecParser, context_label: &str) {
let uf = parser.unresolved_formula_dependencies();
let uc = parser.unresolved_constant_dependencies();
if !uf.is_empty() {
eprintln!(
"{context_label}: warning: {} **`F_*`** **Depends on** reference(s) missing from merged formula registry:",
uf.len(),
);
for (formula_id, dep) in &uf {
eprintln!(" {formula_id} -> {dep}");
}
}
if !uc.is_empty() {
eprintln!(
"{context_label}: warning: {} **`C_*`** **Depends on** reference(s) missing from merged consensus-constant set (§**4** excerpts in `--spec-path` merge):",
uc.len(),
);
for (formula_id, dep) in &uc {
eprintln!(" {formula_id} -> {dep}");
}
}
let cycles = cli::formula_dep_graph::find_formula_id_cycles(parser.formulas());
if !cycles.is_empty() {
eprintln!(
"{context_label}: warning: cyclic **`F_*`→`F_*`** **Depends on** among defined formulas (informational):",
);
for c in &cycles {
eprintln!(" {}", c.join(" -> "));
}
}
}
#[derive(Subcommand)]
enum Commands {
Verify {
#[arg(long)]
crate_path: Option<PathBuf>,
#[arg(long)]
subsystem: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long, action = clap::ArgAction::Append)]
section: Vec<String>,
#[arg(long, default_value = "human")]
format: OutputFormat,
#[arg(short, long, default_value = "1")]
jobs: usize,
#[arg(long, default_value = "10")]
timeout: u64,
#[arg(short, long)]
verbose: bool,
#[arg(long)]
strict: bool,
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(long, value_name = "PATH")]
json_out: Option<PathBuf>,
files: Vec<String>,
},
Coverage {
#[arg(long)]
crate_path: Option<PathBuf>,
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(long, value_name = "PATH")]
rollup_from_verify_json: Option<PathBuf>,
#[arg(long, default_value = "human")]
format: OutputFormat,
},
#[command(name = "verify-formulas")]
VerifyFormulas {
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(long, default_value = "10")]
timeout: u64,
#[arg(long)]
skip_z3: bool,
#[arg(long, default_value = "human")]
format: OutputFormat,
#[arg(long, value_name = "PATH")]
json_out: Option<PathBuf>,
},
CheckFormulas {
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(long)]
z3_sat: bool,
#[arg(long, default_value = "5")]
timeout: u64,
},
List {
#[arg(long)]
crate_path: Option<PathBuf>,
#[arg(long)]
subsystem: Option<String>,
#[arg(long)]
section: Option<String>,
},
Summary {
#[arg(long)]
crate_path: Option<PathBuf>,
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(long, default_value = "human")]
format: String,
},
ListFormulas {
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
},
CheckDrift {
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(long)]
crate_path: Option<PathBuf>,
#[arg(long)]
scoped_unparseables: bool,
#[arg(long)]
scoped_formulas: bool,
#[arg(long, default_value = "human")]
format: OutputFormat,
},
ExtractConstants {
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(short, long)]
output: Option<PathBuf>,
},
ExtractFormulas {
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(short, long)]
output: Option<PathBuf>,
},
ExtractPropertyTests {
#[arg(long, num_args = 1.., value_delimiter = ',')]
spec_path: Vec<PathBuf>,
#[arg(long)]
bindings_path: Option<PathBuf>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
crate_path: Option<PathBuf>,
},
}
#[derive(Clone, Debug)]
enum OutputFormat {
Human,
Json,
Junit,
Markdown,
}
struct VerifyArgs {
crate_path: PathBuf,
files: Vec<String>,
subsystem: Option<String>,
name: Option<String>,
sections: Vec<String>,
format: OutputFormat,
strict: bool,
spec_paths: Vec<PathBuf>,
timeout_secs: u64,
json_out: Option<PathBuf>,
}
impl std::str::FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"human" => Ok(OutputFormat::Human),
"json" => Ok(OutputFormat::Json),
"junit" => Ok(OutputFormat::Junit),
"markdown" => Ok(OutputFormat::Markdown),
_ => Err(format!(
"Unknown format: {s}. Expected: human, json, junit, markdown"
)),
}
}
}
fn main() {
let cli = Cli::parse();
let exit_code = match cli.command {
Commands::Verify {
crate_path,
subsystem,
name,
section,
format,
jobs: _,
timeout,
verbose: _,
strict,
spec_path,
files,
json_out,
} => handle_verify(VerifyArgs {
crate_path: resolve_crate_path(crate_path),
files,
subsystem,
name,
sections: section,
format,
strict,
spec_paths: resolve_spec_paths(spec_path),
timeout_secs: timeout,
json_out,
}),
Commands::Coverage {
crate_path,
spec_path,
rollup_from_verify_json,
format,
} => handle_coverage(
resolve_crate_path(crate_path),
resolve_spec_paths(spec_path),
rollup_from_verify_json,
format,
),
Commands::List {
crate_path,
subsystem,
section,
} => handle_list(resolve_crate_path(crate_path), subsystem, section),
Commands::Summary {
crate_path,
spec_path,
format,
} => handle_summary(
resolve_crate_path(crate_path),
resolve_spec_paths(spec_path),
format,
),
Commands::ListFormulas { spec_path } => handle_list_formulas(resolve_spec_paths(spec_path)),
Commands::CheckDrift {
spec_path,
crate_path,
scoped_unparseables,
scoped_formulas,
format,
} => handle_check_drift(
resolve_spec_paths(spec_path),
resolve_crate_path(crate_path),
scoped_unparseables,
scoped_formulas,
format,
),
Commands::VerifyFormulas {
spec_path,
timeout,
skip_z3,
format,
json_out,
} => handle_verify_formulas(
resolve_spec_paths(spec_path),
timeout,
skip_z3,
format,
json_out,
),
Commands::CheckFormulas {
spec_path,
z3_sat,
timeout,
} => handle_check_formulas(resolve_spec_paths(spec_path), z3_sat, timeout),
Commands::ExtractConstants { spec_path, output } => {
handle_extract_constants(resolve_spec_paths(spec_path), output)
}
Commands::ExtractFormulas { spec_path, output } => {
handle_extract_formulas(resolve_spec_paths(spec_path), output)
}
Commands::ExtractPropertyTests {
spec_path,
bindings_path,
output,
crate_path: _,
} => handle_extract_property_tests(resolve_spec_paths(spec_path), bindings_path, output),
};
std::process::exit(exit_code);
}
fn handle_list_formulas(spec_paths: Vec<PathBuf>) -> i32 {
if spec_paths.is_empty() {
eprintln!("Error: --spec-path or SPEC_LOCK_SPEC_PATH required for list-formulas");
return 1;
}
match parser::orange_paper::SpecParser::from_paths(&spec_paths) {
Ok(parser) => {
warn_formula_dep_diagnostics(&parser, "list-formulas");
if parser.formulas().is_empty() {
println!("(no F_* Formula blocks in merged spec)");
} else {
let ids: std::collections::HashSet<&str> =
parser.formulas().keys().map(|s| s.as_str()).collect();
let c_stable = parser.merged_consensus_constant_ids();
let mut formulas: Vec<_> = parser.formulas().values().collect();
formulas.sort_by(|a, b| a.id.cmp(&b.id));
for f in formulas {
let condensed: String = f
.latex_body
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
let deps = f.depends_on.join(",");
let gate =
if crate::cli::drift::formula_latex_parseable_for_verify(&f.latex_body) {
"ok"
} else {
"fail"
};
let missing_f_refs: String = f
.depends_on
.iter()
.filter(|d| d.starts_with("F_") && !ids.contains(d.as_str()))
.cloned()
.collect::<Vec<_>>()
.join(",");
let missing_c_refs: String = f
.depends_on
.iter()
.filter(|d| d.starts_with("C_") && !c_stable.contains(d.as_str()))
.cloned()
.collect::<Vec<_>>()
.join(",");
println!(
"{}\t{}\t{gate}\t{deps}\t{missing_f_refs}\t{missing_c_refs}\t{condensed}",
f.id, f.section,
);
}
}
0
}
Err(e) => {
eprintln!("{e}");
1
}
}
}
fn handle_check_drift(
spec_paths: Vec<PathBuf>,
crate_path: PathBuf,
scoped_unparseables: bool,
scoped_formulas: bool,
format: OutputFormat,
) -> i32 {
if spec_paths.is_empty() {
eprintln!("Error: --spec-path or SPEC_LOCK_SPEC_PATH required for check-drift");
return 1;
}
if let Ok(sp) = parser::orange_paper::SpecParser::from_paths(&spec_paths) {
warn_formula_dep_diagnostics(&sp, "check-drift");
}
let result = match cli::drift::detect_drift(
&crate_path,
Some(&spec_paths),
scoped_unparseables,
scoped_formulas,
) {
Ok(r) => r,
Err(e) => {
eprintln!("Error detecting drift: {e}");
return 1;
}
};
let output = match format {
OutputFormat::Human => cli::drift::format_drift_human(&result),
OutputFormat::Json => cli::drift::format_drift_json(&result),
OutputFormat::Markdown => {
eprintln!("Markdown format not yet implemented for drift detection");
return 1;
}
OutputFormat::Junit => {
eprintln!("JUnit format not yet implemented for drift detection");
return 1;
}
};
print!("{output}");
if !result.mismatched_contracts.is_empty()
|| !result.missing_from_spec.is_empty()
|| !result.missing_implementations.is_empty()
{
1
} else {
0
}
}
fn handle_coverage(
crate_path: PathBuf,
spec_paths: Vec<PathBuf>,
rollup_from_verify_json: Option<PathBuf>,
format: OutputFormat,
) -> i32 {
let stats = match cli::coverage::generate_coverage(
&crate_path,
if spec_paths.is_empty() {
None
} else {
Some(spec_paths.as_slice())
},
rollup_from_verify_json.as_deref(),
) {
Ok(s) => s,
Err(e) => {
eprintln!("Error generating coverage: {e}");
return 1;
}
};
if !spec_paths.is_empty() {
if let Ok(sp) = parser::orange_paper::SpecParser::from_paths(&spec_paths) {
warn_formula_dep_diagnostics(&sp, "coverage");
}
}
let output = if !spec_paths.is_empty() {
match cli::coverage::generate_spec_coverage_report(&crate_path, &spec_paths, &stats) {
Ok(report) => match format {
OutputFormat::Human => cli::coverage::format_spec_coverage_human(&report),
OutputFormat::Json => cli::coverage::format_spec_coverage_json(&report),
OutputFormat::Markdown => cli::coverage::format_spec_coverage_markdown(&report),
OutputFormat::Junit => {
eprintln!("JUnit format not yet implemented for spec coverage");
return 1;
}
},
Err(e) => {
eprintln!("Error generating spec coverage: {e}");
return 1;
}
}
} else {
match format {
OutputFormat::Human => cli::coverage::format_coverage_human(&stats),
OutputFormat::Json => cli::coverage::format_coverage_json(&stats),
OutputFormat::Markdown => cli::coverage::format_coverage_markdown(&stats),
OutputFormat::Junit => {
eprintln!("JUnit format not yet implemented for coverage");
return 1;
}
}
};
print!("{output}");
0
}
fn handle_list(crate_path: PathBuf, subsystem: Option<String>, section: Option<String>) -> i32 {
let all_functions = match cli::verify::discover_functions(&crate_path) {
Ok(funcs) => funcs,
Err(e) => {
eprintln!("Error discovering functions: {e}");
return 1;
}
};
let sections: Vec<String> = section.into_iter().collect();
let filtered =
cli::filters::filter_functions(all_functions, subsystem.as_deref(), None, §ions);
if filtered.is_empty() {
eprintln!("No spec-locked functions found");
return 0;
}
let mut sorted: Vec<_> = filtered.into_iter().collect();
sorted.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.function_name.cmp(&b.function_name))
});
for f in &sorted {
let section_str = f.section.as_deref().unwrap_or("(no section)");
println!(
"{}\t{}\t{}",
f.function_name,
f.file_path.display(),
section_str
);
}
eprintln!("{} spec-locked function(s)", sorted.len());
0
}
fn handle_summary(crate_path: PathBuf, spec_paths: Vec<PathBuf>, format: String) -> i32 {
let all_functions = match cli::verify::discover_functions(&crate_path) {
Ok(funcs) => funcs,
Err(e) => {
eprintln!("Error discovering functions: {e}");
return 1;
}
};
if all_functions.is_empty() {
if format == "badge" {
println!(
"[](#)"
);
} else {
eprintln!("No spec-locked functions found in {}", crate_path.display());
}
return 0;
}
let mut functions = all_functions.clone();
let mut enriched_count = 0;
if !spec_paths.is_empty() {
match cli::spec_enrich::enrich_functions_with_spec(&mut functions, &spec_paths) {
Ok(n) => enriched_count = n,
Err(e) => eprintln!("Warning: Could not parse spec: {e}"),
}
}
if !spec_paths.is_empty() {
if let Ok(sp) = parser::orange_paper::SpecParser::from_paths(&spec_paths) {
warn_formula_dep_diagnostics(&sp, "summary");
}
}
if format == "badge" {
let n = functions.len();
let color = if n > 0 { "brightgreen" } else { "lightgrey" };
println!("[](#)");
return 0;
}
let mut by_section: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for f in &functions {
let section = f.section.as_deref().unwrap_or("(no section)");
by_section
.entry(section.to_string())
.or_default()
.push(f.function_name.clone());
}
let mut sections: Vec<_> = by_section.keys().collect();
sections.sort();
println!("Lock status: {}", crate_path.display());
println!(" Functions: {}", functions.len());
println!(" Sections: {}", sections.len());
if !spec_paths.is_empty() {
println!(" Enriched with spec: {enriched_count} (contracts from Orange Paper)");
} else {
println!(" Enriched: (use --spec-path for spec-derived contracts)");
}
println!();
println!("Sections:");
for section in sections {
let funcs = by_section.get(section).unwrap();
println!(" {} {} function(s)", section, funcs.len());
}
0
}
fn handle_verify(args: VerifyArgs) -> i32 {
let VerifyArgs {
crate_path,
files: _files,
subsystem,
name,
sections,
format,
strict: strict_cli,
spec_paths,
timeout_secs,
json_out,
} = args;
let strict = strict_cli
|| matches!(
std::env::var("SPEC_LOCK_STRICT").as_deref(),
Ok("1") | Ok("true") | Ok("yes")
);
let timeout_secs = std::env::var("SPEC_LOCK_Z3_TIMEOUT_SECS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
.filter(|&t| t > 0)
.unwrap_or(timeout_secs);
let mut all_functions = match cli::verify::discover_functions(&crate_path) {
Ok(funcs) => funcs,
Err(e) => {
eprintln!("Error discovering functions: {e}");
return 1;
}
};
let callee_postconds: Vec<cli::verify::CalleePostcond> = {
use cli::verify::ContractType;
all_functions
.iter()
.flat_map(|f| {
f.contracts.iter().filter_map(move |c| {
if (c.contract_type == ContractType::Ensures
|| c.contract_type == ContractType::Axiom)
&& !c.is_spec_derived
&& c.expr.is_some()
&& !c.condition.is_empty()
{
Some(cli::verify::CalleePostcond {
function_name: f.function_name.clone(),
condition: c.condition.clone(),
})
} else {
None
}
})
})
.collect()
};
if !spec_paths.is_empty() {
match cli::spec_enrich::enrich_functions_with_spec(&mut all_functions, &spec_paths) {
Ok(enriched) => {
if enriched > 0 {
eprintln!("📋 Enriched {enriched} functions with spec-derived contracts");
}
}
Err(e) => {
eprintln!("error: Could not parse spec for contract extraction: {e}");
eprintln!(" Fix the spec files passed via --spec-path and retry.");
eprintln!(" Common causes: duplicate section IDs across merged spec files.");
return 1;
}
}
} else {
eprintln!(
"Note: --spec-path not set. Use --spec-path <ORANGE_PAPER.md> for spec-derived contracts."
);
eprintln!(" Without it, only manual #[requires]/#[ensures] are used.");
}
let filtered = cli::filters::filter_functions(
all_functions,
subsystem.as_deref(),
name.as_deref(),
§ions,
);
if filtered.is_empty() {
eprintln!("No functions found matching criteria");
return 1;
}
let mut sorted: Vec<_> = filtered.into_iter().collect();
sorted.sort_by(|a, b| {
a.file_path
.cmp(&b.file_path)
.then_with(|| a.function_name.cmp(&b.function_name))
});
use crate::cli::formula_checks::{
FormulaAnalyzeConfig, FormulaRegistryAnalysis, analyze_formula_registry,
registry_has_blocking_static_failure, registry_has_blocking_z3_outcome,
};
use crate::cli::output::{
FormulaVerifyJsonFlags, format_formula_verify_human, format_verify_json_report,
};
let mut formula_registry_for_json: Option<(FormulaRegistryAnalysis, FormulaVerifyJsonFlags)> =
None;
if !spec_paths.is_empty() {
if let Ok(parser) = parser::orange_paper::SpecParser::from_paths(&spec_paths) {
warn_formula_dep_diagnostics(&parser, "verify");
let skip_z3 = matches!(
std::env::var("SPEC_LOCK_VERIFY_FORMULAS_SKIP_Z3").as_deref(),
Ok("1") | Ok("true") | Ok("yes")
);
let z3_requested = !skip_z3;
let want_z3 = z3_requested && cfg!(feature = "z3");
let analysis = analyze_formula_registry(
&parser,
FormulaAnalyzeConfig {
request_z3_sat: want_z3,
timeout_ms: timeout_secs.saturating_mul(1000).max(1),
},
);
let formulas_len = parser.formulas().len();
let flags = FormulaVerifyJsonFlags {
z3_sat_requested: z3_requested,
cargo_built_with_z3: cfg!(feature = "z3"),
};
let fail_static = registry_has_blocking_static_failure(&analysis);
let fail_z3 = want_z3 && registry_has_blocking_z3_outcome(&analysis);
let cannot_run_requested_z3 =
z3_requested && !cfg!(feature = "z3") && formulas_len > 0 && !fail_static;
if cannot_run_requested_z3 {
eprintln!(
"verify: Z3 SAT smoke for the merged **`F_*`** registry was requested — rebuild **`cargo-spec-lock`** with **`--features z3`** (or set **`SPEC_LOCK_VERIFY_FORMULAS_SKIP_Z3=1`** for static-only)."
);
}
let formula_blocking = fail_static || fail_z3 || cannot_run_requested_z3;
if formula_blocking {
match format {
OutputFormat::Human | OutputFormat::Junit | OutputFormat::Markdown => {
println!("{}", format_formula_verify_human(&analysis, z3_requested));
}
OutputFormat::Json => {
println!(
"{}",
format_verify_json_report(&[], Some((&analysis, flags)))
);
}
}
if let Some(ref path) = json_out {
let json = format_verify_json_report(&[], Some((&analysis, flags)));
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!(
"Error creating parent directories for --json-out {}: {}",
path.display(),
e
);
return 1;
}
}
}
if let Err(e) = std::fs::write(path, json.as_bytes()) {
eprintln!("Error writing --json-out {}: {}", path.display(), e);
return 1;
}
}
return 1;
}
formula_registry_for_json = Some((analysis, flags));
} else {
eprintln!(
"error: could not parse merged spec for **`F_*`** registry (file not found or parse error)"
);
return 1;
}
}
let mut results = Vec::new();
for func in &sorted {
let result = cli::verify::verify_function(func, timeout_secs, &callee_postconds);
results.push((func.clone(), result));
}
let format_str = match format {
OutputFormat::Human => "human",
OutputFormat::Json => "json",
OutputFormat::Junit => "junit",
OutputFormat::Markdown => "markdown",
};
let printed = match format {
OutputFormat::Json => cli::output::format_verify_json_report(
&results,
formula_registry_for_json.as_ref().map(|(a, f)| (a, *f)),
),
_ => cli::output::format_results(&results, format_str),
};
print!("{printed}");
if let Some(ref path) = json_out {
let json = cli::output::format_verify_json_report(
&results,
formula_registry_for_json.as_ref().map(|(a, f)| (a, *f)),
);
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!(
"Error creating parent directories for --json-out {}: {}",
path.display(),
e
);
return 1;
}
}
}
if let Err(e) = std::fs::write(path, json.as_bytes()) {
eprintln!("Error writing --json-out {}: {}", path.display(), e);
return 1;
}
}
let _ = strict; let has_failures = results
.iter()
.any(|(_, r)| matches!(r, cli::verify::VerificationResult::Failed { .. }));
let has_no_contracts = results
.iter()
.any(|(_, r)| matches!(r, cli::verify::VerificationResult::NoContracts { .. }));
if has_failures || has_no_contracts {
1
} else {
0
}
}
fn handle_verify_formulas(
spec_paths: Vec<PathBuf>,
timeout_secs: u64,
skip_z3: bool,
format: OutputFormat,
json_out: Option<PathBuf>,
) -> i32 {
use crate::cli::formula_checks::{
FormulaAnalyzeConfig, analyze_formula_registry, registry_has_blocking_static_failure,
registry_has_blocking_z3_outcome,
};
use crate::cli::output::{
FormulaVerifyJsonFlags, format_formula_verify_human, format_formula_verify_json_report,
};
use crate::parser::orange_paper::SpecParser;
if spec_paths.is_empty() {
eprintln!("Error: --spec-path or SPEC_LOCK_SPEC_PATH required for verify-formulas");
return 1;
}
let parser = match SpecParser::from_paths(&spec_paths) {
Ok(p) => p,
Err(e) => {
eprintln!("Error parsing merged spec: {e}");
return 1;
}
};
warn_formula_dep_diagnostics(&parser, "verify-formulas");
let formulas_len = parser.formulas().len();
let z3_requested = !skip_z3;
let want_z3 = z3_requested && cfg!(feature = "z3");
let analysis = analyze_formula_registry(
&parser,
FormulaAnalyzeConfig {
request_z3_sat: want_z3,
timeout_ms: timeout_secs.saturating_mul(1000).max(1),
},
);
let flags = FormulaVerifyJsonFlags {
z3_sat_requested: z3_requested,
cargo_built_with_z3: cfg!(feature = "z3"),
};
let json_doc = format_formula_verify_json_report(&analysis, flags);
let format_str = match format {
OutputFormat::Human => "human",
OutputFormat::Json => "json",
OutputFormat::Junit | OutputFormat::Markdown => {
eprintln!(
"verify-formulas: `{}` unsupported for this subcommand — using human stdout",
match format {
OutputFormat::Junit => "junit",
OutputFormat::Markdown => "markdown",
_ => unreachable!(),
}
);
"human"
}
};
match format_str {
"json" => println!("{json_doc}"),
_ => print!("{}", format_formula_verify_human(&analysis, z3_requested)),
}
if let Some(ref path) = json_out {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!(
"Error creating parent directories for verify-formulas --json-out {}: {}",
path.display(),
e
);
return 1;
}
}
}
if let Err(e) = std::fs::write(path, json_doc.as_bytes()) {
eprintln!(
"Error writing verify-formulas --json-out {}: {}",
path.display(),
e
);
return 1;
}
}
let fail_static = registry_has_blocking_static_failure(&analysis);
let fail_z3 = want_z3 && registry_has_blocking_z3_outcome(&analysis);
let cannot_run_requested_z3 =
z3_requested && !cfg!(feature = "z3") && formulas_len > 0 && !fail_static;
if cannot_run_requested_z3 {
eprintln!(
"verify-formulas: Z3 SAT smoke was requested — rebuild **`cargo-spec-lock`** with **`--features z3`** (or pass **`--skip-z3`** for static-only)."
);
}
if fail_static || fail_z3 || cannot_run_requested_z3 {
1
} else {
0
}
}
fn handle_check_formulas(spec_paths: Vec<PathBuf>, z3_sat: bool, timeout_secs: u64) -> i32 {
use crate::cli::formula_checks::{
FormulaAnalyzeConfig, FormulaStaticOutcome, Z3FormulaPhase, analyze_formula_registry,
registry_has_blocking_static_failure, registry_has_blocking_z3_outcome,
};
use crate::parser::orange_paper::SpecParser;
if spec_paths.is_empty() {
eprintln!("Error: --spec-path or SPEC_LOCK_SPEC_PATH required for check-formulas");
return 1;
}
let parser = match SpecParser::from_paths(&spec_paths) {
Ok(p) => p,
Err(e) => {
eprintln!("Error parsing merged spec: {e}");
return 1;
}
};
warn_formula_dep_diagnostics(&parser, "check-formulas");
let analysis = analyze_formula_registry(
&parser,
FormulaAnalyzeConfig {
request_z3_sat: z3_sat && cfg!(feature = "z3"),
timeout_ms: timeout_secs.saturating_mul(1000).max(1),
},
);
let formulas_len = parser.formulas().len();
if registry_has_blocking_static_failure(&analysis) {
let bad: Vec<String> = analysis
.rows
.iter()
.filter_map(|r| match &r.static_gate {
FormulaStaticOutcome::Failed { message } => {
Some(format!("{}: {message}", r.formula_id))
}
_ => None,
})
.collect();
eprintln!(
"check-formulas: {} failure(s) (static gate) among {} **`F_*`** formula(s):\n{}",
bad.len(),
formulas_len,
bad.join("\n")
);
return 1;
}
if !z3_sat {
println!(
"check-formulas: {formulas_len} **`F_*`** body(ies) OK (static enrich/verify gate)."
);
return 0;
}
#[cfg(not(feature = "z3"))]
{
eprintln!(
"check-formulas: **`--z3-sat`** requires building **`cargo-spec-lock`** with **`--features z3`**."
);
return 1;
}
#[cfg(feature = "z3")]
{
if registry_has_blocking_z3_outcome(&analysis) {
let mut z3_bad = Vec::new();
for r in &analysis.rows {
match &r.z3_phase {
Z3FormulaPhase::SatSmokeOk => {}
Z3FormulaPhase::NotRequested | Z3FormulaPhase::SkippedDueToStatic => {}
Z3FormulaPhase::SkippedNoZ3Feature => z3_bad.push(format!(
"{}: Z3 SAT smoke requested — rebuild **`cargo-spec-lock`** with **`--features z3`**",
r.formula_id
)),
Z3FormulaPhase::UnsatContradiction => z3_bad.push(format!(
"{}: Z3 reports UNSAT (formula is a contradiction under current translation)",
r.formula_id
)),
Z3FormulaPhase::Unknown { reason } => {
z3_bad.push(format!("{}: Z3 unknown — {reason}", r.formula_id));
}
Z3FormulaPhase::Error { message } => {
z3_bad.push(format!("{}: {message}", r.formula_id));
}
}
}
eprintln!(
"check-formulas: {} Z3 satisfiability issue(s) among {} formula(s) (static gate passed):\n{}",
z3_bad.len(),
formulas_len,
z3_bad.join("\n")
);
return 1;
}
println!(
"check-formulas: {formulas_len} **`F_*`** static OK; **`--z3-sat`**: all {formulas_len} satisfiability smoke checks passed."
);
0
}
}
fn handle_extract_constants(spec_paths: Vec<PathBuf>, output_path: Option<PathBuf>) -> i32 {
if spec_paths.is_empty() {
eprintln!("Error: --spec-path or SPEC_LOCK_SPEC_PATH required for extract-constants");
return 1;
}
let output_path =
output_path.or_else(|| std::env::var("SPEC_LOCK_OUTPUT").ok().map(PathBuf::from));
let output_path = match output_path {
Some(p) => p,
None => {
eprintln!("Error: --output or SPEC_LOCK_OUTPUT required for extract-constants");
return 1;
}
};
let parser = match parser::orange_paper::SpecParser::from_paths(&spec_paths) {
Ok(p) => p,
Err(e) => {
eprintln!("Error parsing Orange Paper: {e}");
return 1;
}
};
warn_formula_dep_diagnostics(&parser, "extract-constants");
let constants = parser.extract_constants();
if constants.is_empty() {
eprintln!("No constants found in Orange Paper Section 4");
return 1;
}
let rust_code = generate_constants_module(&constants);
if let Err(e) = std::fs::write(&output_path, rust_code) {
eprintln!(
"Error writing constants module to {}: {}",
output_path.display(),
e
);
return 1;
}
eprintln!(
"✅ Generated {} constants in {}",
constants.len(),
output_path.display()
);
0
}
fn generate_constants_module(constants: &[&parser::orange_paper::ExtractedConstant]) -> String {
let mut code =
String::from("//! Constants extracted from Orange Paper Section 4 (Consensus Constants)\n");
code.push_str("//!\n");
code.push_str("//! This file is AUTO-GENERATED from blvm-spec/THE_ORANGE_PAPER.md\n");
code.push_str("//! DO NOT EDIT MANUALLY - changes should be made to Orange Paper\n");
code.push_str("//!\n");
code.push_str("//! To regenerate: cargo spec-lock extract-constants\n");
code.push_str("//!\n");
code.push_str("//! These constants are always available for use in property tests and code.\n");
code.push_str(
"//! Each constant is linked to its Orange Paper section via documentation comments.\n\n",
);
for constant in constants {
code.push_str(&format!("/// {}\n", constant.description));
code.push_str("/// \n");
code.push_str(&format!(
"/// Source: Orange Paper Section {}\n",
constant.section
));
code.push_str(&format!(
"/// Formula: ${} = {}$\n",
constant.name, constant.value
));
let rust_expr = if constant.rust_expr.contains("* C") && constant.rust_type == "i64" {
format!("({}) as i64", constant.rust_expr)
} else {
constant.rust_expr.clone()
};
code.push_str(&format!(
"pub const {}: {} = {};\n\n",
constant.name, constant.rust_type, rust_expr
));
}
code
}
fn handle_extract_formulas(spec_paths: Vec<PathBuf>, output_path: Option<PathBuf>) -> i32 {
if spec_paths.is_empty() {
eprintln!("Error: --spec-path or SPEC_LOCK_SPEC_PATH required for extract-formulas");
return 1;
}
let output_path =
output_path.or_else(|| std::env::var("SPEC_LOCK_OUTPUT").ok().map(PathBuf::from));
let output_path = match output_path {
Some(p) => p,
None => {
eprintln!("Error: --output or SPEC_LOCK_OUTPUT required for extract-formulas");
return 1;
}
};
let parser = match parser::orange_paper::SpecParser::from_paths(&spec_paths) {
Ok(p) => p,
Err(e) => {
eprintln!("Error parsing Orange Paper: {e}");
return 1;
}
};
warn_formula_dep_diagnostics(&parser, "extract-formulas");
let functions = parser.extract_functions_with_formulas();
if functions.is_empty() {
eprintln!("No functions with formulas found in Orange Paper");
return 1;
}
let rust_code = generate_property_helpers(&functions);
if let Err(e) = std::fs::write(&output_path, rust_code) {
eprintln!(
"Error writing property helpers to {}: {}",
output_path.display(),
e
);
return 1;
}
eprintln!(
"✅ Generated property test helpers for {} functions in {}",
functions.len(),
output_path.display()
);
0
}
fn generate_property_helpers(functions: &[&parser::orange_paper::FunctionSpec]) -> String {
let mut code = String::from("//! Property test helpers generated from Orange Paper formulas\n");
code.push_str("//!\n");
code.push_str("//! This file is AUTO-GENERATED from blvm-spec/THE_ORANGE_PAPER.md\n");
code.push_str("//! DO NOT EDIT MANUALLY - changes should be made to Orange Paper\n");
code.push_str("//!\n");
code.push_str("//! To regenerate: cargo spec-lock extract-formulas\n");
code.push_str("//!\n");
code.push_str("//! These helpers allow property tests to compare implementation results\n");
code.push_str("//! against the mathematical formulas defined in the Orange Paper.\n\n");
code.push_str("use blvm_consensus::orange_paper_constants::*;\n");
code.push_str("#[cfg(test)]\n");
code.push_str("use proptest::prelude::*;\n\n");
let implementable_functions: Vec<&str> = vec![
"GetBlockSubsidy",
"get_block_subsidy",
"BlockSubsidy",
"TotalSupply",
"total_supply",
"Supply",
];
for func in functions {
if let Some(formula) = &func.formula {
let func_lower = func.name.to_lowercase();
let formula_lower = formula.to_lowercase();
let is_implementable = implementable_functions.iter().any(|&name| {
func_lower.contains(&name.to_lowercase())
|| formula_lower.contains(&name.to_lowercase())
});
if !is_implementable {
continue; }
let helper_name = format!(
"expected_{}_from_orange_paper",
func.name.to_lowercase().replace(" ", "_")
);
let rust_formula = translate_formula_to_rust(formula, &func.name);
code.push_str("/// Expected result from Orange Paper formula\n");
code.push_str("/// \n");
code.push_str(&format!(
"/// Source: Orange Paper Section {}\n",
func.section
));
let formula_cleaned = formula.replace("$$", "");
let formula_trimmed = formula_cleaned.trim();
let formula_doc = if formula_trimmed.len() > 100 {
format!("See Orange Paper Section {} for full formula", func.section)
} else {
formula_trimmed
.replace("\\text{", "")
.replace("\\begin{cases}", "")
.replace("\\end{cases}", "")
.replace("\\times", "×")
.replace("\\geq", "≥")
.replace("\\leq", "≤")
.chars()
.take(100)
.collect::<String>()
};
code.push_str(&format!("/// Formula: {formula_doc}\n"));
code.push_str("/// \n");
if let Some(desc) = &func.description {
let desc_clean = desc.chars().take(200).collect::<String>();
code.push_str(&format!("/// {desc_clean}\n"));
}
code.push_str(&format!("pub fn {helper_name}("));
let params = extract_formula_parameters(formula, &func.name);
if params.is_empty() {
if func.name.contains("Subsidy") || func.name.contains("Supply") {
code.push_str("height: u64");
} else {
code.push_str("_params: u64"); }
} else {
code.push_str(¶ms.join(", "));
}
let return_type = if func.name.contains("valid")
|| func.name.contains("Check")
|| func.name.contains("Validate")
{
"bool"
} else {
"i64"
};
code.push_str(&format!(") -> {return_type} {{\n"));
code.push_str(&format!(" {rust_formula}\n"));
code.push_str("}\n\n");
}
}
code
}
fn translate_formula_to_rust(formula: &str, func_name: &str) -> String {
let func_lower = func_name.to_lowercase();
let formula_lower = formula.to_lowercase();
if func_lower.contains("getblocksubsidy")
|| func_lower.contains("block_subsidy")
|| formula_lower.contains("getblocksubsidy")
|| formula_lower.contains("block_subsidy")
{
generate_get_block_subsidy_helper()
} else if func_lower.contains("totalsupply")
|| func_lower.contains("total_supply")
|| formula_lower.contains("totalsupply")
|| formula_lower.contains("total_supply")
|| formula_lower.contains("sum") && formula_lower.contains("getblocksubsidy")
{
generate_total_supply_helper()
} else if func_lower.contains("calculatefee")
|| func_lower.contains("calculate_fee")
|| formula_lower.contains("calculatefee")
|| formula_lower.contains("calculate_fee")
{
generate_calculate_fee_helper()
} else {
let formula_clean = formula
.replace("$$", "")
.trim()
.chars()
.take(80)
.collect::<String>();
format!(
" // TODO: Implement formula translation for {func_name}\n // Formula: {formula_clean}...\n // This formula requires manual implementation\n unimplemented!(\"Formula translation not yet implemented for {func_name}\")"
)
}
}
fn generate_get_block_subsidy_helper() -> String {
String::from(
" let halving_period = height / H;
let initial_subsidy = 50 * C; // 50 BTC = 50 × C
if halving_period >= 64 {
0
} else {
initial_subsidy >> halving_period // Uses Orange Paper formula: 50 × C × 2^(-⌊h/H⌋)
}",
)
}
fn generate_total_supply_helper() -> String {
String::from(
" // TotalSupply(h) = sum of all block subsidies from 0 to h
// Formula: TotalSupply(h) = sum_{i=0}^{h} GetBlockSubsidy(i)
// This is computed by summing GetBlockSubsidy for each height
let mut total = 0i64;
for h in 0..=height {
let halving_period = h / H;
let initial_subsidy = 50 * C;
if halving_period < 64 {
total += (initial_subsidy >> halving_period) as i64;
}
}
total",
)
}
fn generate_calculate_fee_helper() -> String {
String::from(
" // CalculateFee(inputs, outputs) = sum(inputs.value) - sum(outputs.value)
// Note: This is a placeholder - actual implementation needs input/output values
// TODO: Implement with actual transaction inputs and outputs
0",
)
}
fn handle_extract_property_tests(
spec_paths: Vec<PathBuf>,
bindings_path: Option<PathBuf>,
output_path: Option<PathBuf>,
) -> i32 {
if spec_paths.is_empty() {
eprintln!("Error: --spec-path or SPEC_LOCK_SPEC_PATH required for extract-property-tests");
return 1;
}
let output_path =
output_path.or_else(|| std::env::var("SPEC_LOCK_OUTPUT").ok().map(PathBuf::from));
let output_path = match output_path {
Some(p) => p,
None => {
eprintln!("Error: --output or SPEC_LOCK_OUTPUT required for extract-property-tests");
return 1;
}
};
let bindings_path = bindings_path.unwrap_or_else(|| {
spec_paths
.first()
.and_then(|p| p.parent())
.map(|p| p.join("PROPERTY_BINDINGS.toml"))
.unwrap_or_else(|| PathBuf::from("PROPERTY_BINDINGS.toml"))
});
let parser = match parser::orange_paper::SpecParser::from_paths(&spec_paths) {
Ok(p) => p,
Err(e) => {
eprintln!("Error parsing Orange Paper: {e}");
return 1;
}
};
warn_formula_dep_diagnostics(&parser, "extract-property-tests");
let props = parser.get_all_standalone_properties();
let round_trips: Vec<_> = props
.iter()
.filter(|p| p.property_type == parser::orange_paper::StandalonePropertyType::RoundTrip)
.filter(|p| p.inner_func.is_some() && p.outer_func.is_some())
.copied()
.collect();
let bindings_content = match std::fs::read_to_string(&bindings_path) {
Ok(c) => c,
Err(e) => {
eprintln!(
"Error reading bindings from {}: {}",
bindings_path.display(),
e
);
return 1;
}
};
let bindings: toml::Value = match toml::from_str(&bindings_content) {
Ok(b) => b,
Err(e) => {
eprintln!("Error parsing bindings TOML: {e}");
return 1;
}
};
let rust_code = generate_property_tests(&round_trips, &bindings);
if let Err(e) = std::fs::write(&output_path, rust_code) {
eprintln!(
"Error writing property tests to {}: {}",
output_path.display(),
e
);
return 1;
}
eprintln!(
"✅ Generated {} round-trip property test(s) in {}",
round_trips.len(),
output_path.display()
);
0
}
fn get_binding(bindings: &toml::Value, func_name: &str) -> Option<String> {
let tbl = bindings.get("blvm_consensus")?.get("serialization")?;
tbl.get(func_name)?.as_str().map(String::from)
}
fn generate_property_tests(
round_trips: &[&parser::orange_paper::StandaloneProperty],
bindings: &toml::Value,
) -> String {
let mut code = String::from("//! AUTO-GENERATED from Orange Paper - DO NOT EDIT\n");
code.push_str(
"//! Run: cargo spec-lock extract-property-tests --spec-path ... --output ...\n\n",
);
code.push_str("#![cfg(test)]\n");
code.push_str("#![cfg(feature = \"property-tests\")]\n");
code.push_str("use proptest::prelude::*;\n\n");
for prop in round_trips {
let inner = prop.inner_func.as_deref().unwrap_or("");
let outer = prop.outer_func.as_deref().unwrap_or("");
let inner_path = get_binding(bindings, inner)
.or_else(|| get_binding(bindings, &inner.replace("Header", "BlockHeader")));
let outer_path = get_binding(bindings, outer)
.or_else(|| get_binding(bindings, &outer.replace("Header", "BlockHeader")));
if inner_path.is_none() || outer_path.is_none() {
code.push_str(&format!(
"// Skipped {}: missing binding for {} or {}\n",
prop.name, inner, outer
));
continue;
}
let (inner_path, outer_path) = (inner_path.unwrap(), outer_path.unwrap());
let test_name = format!(
"prop_{}",
prop.name
.to_lowercase()
.replace(" ", "_")
.replace("-", "_")
.replace("(", "")
.replace(")", "")
);
if prop.constraint.is_some() && prop.name.contains("SegWit") {
code.push_str(&format!(
"/// Property ({}) - Orange Paper {}\n",
prop.name, prop.section_id
));
code.push_str(&format!("#[test]\nfn {test_name}() {{\n"));
code.push_str(" proptest!(|((tx, w) in blvm_consensus::test_utils::transaction_with_witness_strategy())| {\n");
code.push_str(&format!(" let bytes = {inner_path}(&tx, &w);\n"));
code.push_str(&format!(
" let (tx2, w2, _) = {outer_path}(&bytes).unwrap();\n"
));
code.push_str(" prop_assert_eq!(tx, tx2);\n");
code.push_str(" prop_assert_eq!(w, w2);\n");
code.push_str(" });\n}\n\n");
} else if prop.name.contains("Transaction") && !prop.name.contains("SegWit") {
code.push_str(&format!(
"/// Property ({}) - Orange Paper {}\n",
prop.name, prop.section_id
));
code.push_str(&format!("#[test]\nfn {test_name}() {{\n"));
code.push_str(
" proptest!(|(tx in blvm_consensus::test_utils::transaction_strategy())| {\n",
);
code.push_str(
" let bytes = blvm_consensus::serialization::serialize_transaction(&tx);\n",
);
code.push_str(" let tx2 = blvm_consensus::serialization::deserialize_transaction(&bytes).unwrap();\n");
code.push_str(" prop_assert_eq!(tx, tx2);\n");
code.push_str(" });\n}\n\n");
} else if prop.name.contains("Block Header") || prop.name.contains("Header") {
code.push_str(&format!(
"/// Property ({}) - Orange Paper {}\n",
prop.name, prop.section_id
));
code.push_str(&format!("#[test]\nfn {test_name}() {{\n"));
code.push_str(" use blvm_consensus::types::BlockHeader;\n");
code.push_str(" proptest!(|(v in any::<i64>(), prev in prop::array::uniform32(any::<u8>()), mr in prop::array::uniform32(any::<u8>()), ts in 0u64..u64::MAX, bits in any::<u64>(), nonce in any::<u64>())| {\n");
code.push_str(" let header = BlockHeader { version: v, prev_block_hash: prev, merkle_root: mr, timestamp: ts, bits, nonce };\n");
code.push_str(" let bytes = blvm_consensus::serialization::serialize_block_header(&header);\n");
code.push_str(" let header2 = blvm_consensus::serialization::deserialize_block_header(&bytes).unwrap();\n");
code.push_str(" prop_assert_eq!(header.version, header2.version);\n");
code.push_str(
" prop_assert_eq!(header.prev_block_hash, header2.prev_block_hash);\n",
);
code.push_str(" prop_assert_eq!(header.merkle_root, header2.merkle_root);\n");
code.push_str(" prop_assert_eq!(header.timestamp, header2.timestamp);\n");
code.push_str(" prop_assert_eq!(header.bits, header2.bits);\n");
code.push_str(" prop_assert_eq!(header.nonce, header2.nonce);\n");
code.push_str(" });\n}\n\n");
} else {
code.push_str(&format!("// TODO: {} - add strategy\n", prop.name));
}
}
code
}
fn extract_formula_parameters(formula: &str, func_name: &str) -> Vec<String> {
let mut params = Vec::new();
if formula.contains("(h)") || formula.contains("(h,") {
params.push("height: u64".to_string());
}
if formula.contains("(tx)") || formula.contains("(tx,") {
params.push("tx: &Transaction".to_string());
}
if formula.contains("(b)") || formula.contains("(b,") {
params.push("block: &Block".to_string());
}
if formula.contains("(us)") || formula.contains("(us,") {
params.push("utxo_set: &UtxoSet".to_string());
}
if params.is_empty() && (func_name.contains("Subsidy") || func_name.contains("Supply")) {
params.push("height: u64".to_string());
}
params
}