#![cfg_attr(
feature = "production-firmware",
allow(
clippy::clone_on_copy,
clippy::collapsible_if,
clippy::manual_is_multiple_of,
clippy::needless_range_loop,
clippy::ptr_arg,
clippy::question_mark,
clippy::too_many_arguments,
clippy::type_complexity
)
)]
use guarded_continuation_checker::{
aiger_obligation::{self, AigerAnd, AigerInputPredicate, AigerLatch, AigerTransition},
btor2, btor2_bitblast, btor2_bounded, btor2_braking, btor2_component, btor2_invariant_chain,
btor2_motion, btor2_phase, btor2_predicate_set, btor2_region, btor2_region_equivalence,
btor2_region_extract, btor2_region_property, btor2_search, compiled_mmio_file,
composed_witness, controller_mtbdd,
controller_plant::ControllerPlantWiring,
controller_plant_artifact::{self, ControllerPlantArtifactInput},
dense_relation::DenseRelation,
revision_impact, revision_local,
source_model_attestation::{self, SourceModelBindingInput},
unsat_proof::{self, CnfClause as Clause},
};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
use std::env;
#[cfg(any(target_os = "android", target_os = "linux", target_os = "macos"))]
use std::ffi::CString;
use std::fs;
use std::io::{self, BufRead, BufReader, Read, Write};
#[cfg(any(target_os = "android", target_os = "linux", target_os = "macos"))]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
use std::time::Instant;
use varisat::{ExtendFormula, Lit, Solver, Var};
mod observed_allocator;
const RTL_ARTIFACT_SCHEMA_VERSION: usize = 4;
const FIRMWARE_CLI_CONTRACT_VERSION: usize = 2;
const AAG_INPUT_LIMIT_BYTES: u64 = 256 * 1024 * 1024;
const YOSYS_MEMORY_LIMIT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
const YOSYS_FILE_LIMIT_BYTES: u64 = 512 * 1024 * 1024;
const EVIDENCE_TOTAL_LIMIT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
const BTOR2_COMPONENT_BATCH_MANIFEST_VERSION: u32 = 1;
const BTOR2_COMPONENT_BATCH_MANIFEST_MAX_BYTES: usize = 64 * 1024;
const CONTROLLER_MTBDD_CLI_VERSION: u32 = 1;
const CONTROLLER_PROOF_MTBDD_CLI_VERSION: u32 = 1;
const CONTROLLER_SPLIT_EVIDENCE_CLI_VERSION: u32 = 1;
const CONTROLLER_SPLIT_RESOURCE_CLI_VERSION: u32 = 1;
const CONTROLLER_SPLIT_OBSERVABILITY_CLI_VERSION: u32 = 1;
const CONTROLLER_SPLIT_PHASE_METRICS_VERSION: u32 = 1;
const CONTROLLER_SPLIT_ALLOCATION_OBSERVABILITY_CLI_VERSION: u32 = 1;
const CONTROLLER_SPLIT_CACHE_OBSERVABILITY_CLI_VERSION: u32 = 1;
const CONTROLLER_SPLIT_RESOURCE_POLICY_VERSION: u32 = 1;
const CONTROLLER_SPLIT_RESOURCE_POLICY_MAX_BYTES: usize = 4096;
const CONTROLLER_PLANT_PORTFOLIO_CLI_VERSION: u32 = 1;
const CONTROLLER_PLANT_RESOURCE_CLI_VERSION: u32 = 1;
const CONTROLLER_PLANT_RESOURCE_POLICY_VERSION: u32 = 1;
const CONTROLLER_PLANT_RESOURCE_POLICY_MAX_BYTES: usize = 4096;
const CONTROLLER_PROOF_MTBDD_RESOURCE_CLI_VERSION: u32 = 1;
const CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_VERSION: u32 = 1;
const CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_MAX_BYTES: usize = 4096;
const CONTROLLER_PROOF_MTBDD_PORTFOLIO_CLI_VERSION: u32 = 1;
const CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION: u32 = 1;
const CONTROLLER_MTBDD_PLANT_MANIFEST_MAX_BYTES: usize = 64 * 1024;
const SOURCE_MODEL_PROVENANCE_MANIFEST_VERSION: u32 = 1;
const SOURCE_MODEL_PROVENANCE_MANIFEST_MAX_BYTES: usize = 64 * 1024;
const REVISION_IMPACT_CLI_VERSION: u32 = 2;
const REVISION_IMPACT_QUERY_MANIFEST_VERSION: u32 = 1;
const REVISION_IMPACT_QUERY_MANIFEST_MAX_BYTES: usize = 16 * 1024;
const BTOR2_CHANNEL_PROPERTY_CLI_VERSION: u32 = 1;
const BTOR2_CHANNEL_PROPERTY_OBSERVABILITY_CLI_VERSION: u32 = 1;
const BTOR2_CHANNEL_PROPERTY_PHASE_METRICS_VERSION: u32 = 1;
const BTOR2_CHANNEL_PROPERTY_QUERY_MANIFEST_VERSION: u32 = 1;
const BTOR2_CHANNEL_PROPERTY_QUERY_MANIFEST_MAX_BYTES: usize = 256 * 1024;
const BTOR2_CHANNEL_PROPERTY_POLICY_VERSION: u32 = 1;
const BTOR2_CHANNEL_PROPERTY_POLICY_MAX_BYTES: usize = 4096;
const BTOR2_CHANNEL_TRACE_CLI_VERSION: u32 = 1;
const BTOR2_CHANNEL_TRACE_QUERY_MANIFEST_VERSION: u32 = 1;
const BTOR2_CHANNEL_TRACE_QUERY_MANIFEST_MAX_BYTES: usize = 256 * 1024;
const BTOR2_CHANNEL_TRACE_POLICY_VERSION: u32 = 1;
const BTOR2_CHANNEL_TRACE_POLICY_MAX_BYTES: usize = 4096;
const BTOR2_CHANNEL_PAIR_TRACE_CLI_VERSION: u32 = 1;
const BTOR2_CHANNEL_PAIR_TRACE_QUERY_MANIFEST_VERSION: u32 = 1;
static CERTIFICATE_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Debug, Eq, PartialEq)]
struct Btor2ChannelPropertyQueryManifest {
expected_channels: usize,
semantic_roots: Vec<btor2::NodeId>,
queries: Vec<btor2_region_property::Btor2ChannelPropertyQuery>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Btor2ChannelTraceQueryManifest {
expected_channels: usize,
semantic_roots: Vec<btor2::NodeId>,
queries: Vec<btor2_region_property::Btor2ChannelTraceQuery>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Btor2ChannelPairTraceQueryManifest {
expected_channels: usize,
semantic_roots: Vec<btor2::NodeId>,
queries: Vec<btor2_region_property::Btor2ChannelPairTraceQuery>,
}
#[derive(Debug)]
struct ComponentBatchManifestMember {
plant_path: PathBuf,
contract_path: PathBuf,
horizon: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ControllerMtbddPlantManifestMember {
plant_source_path: PathBuf,
plant_aiger_path: PathBuf,
wiring: ControllerPlantWiring,
initial_controller_state: usize,
initial_plant_state: usize,
bad_plant_output: usize,
horizon: usize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ControllerMtbddPlantManifest {
controller_source_path: PathBuf,
controller_aiger_path: PathBuf,
relevant_inputs: Vec<usize>,
observed_outputs: Vec<usize>,
members: Vec<ControllerMtbddPlantManifestMember>,
}
#[derive(Debug)]
struct SourceModelProvenanceMember {
source_path: PathBuf,
recipe_path: PathBuf,
model_path: PathBuf,
}
#[derive(Debug)]
struct SourceModelProvenanceManifest {
tool: String,
tool_revision: String,
members: Vec<SourceModelProvenanceMember>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct LoadedSourceModelSubject {
source_path: PathBuf,
model_path: PathBuf,
source_sha256: [u8; 32],
model_sha256: [u8; 32],
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct LoadedSourceModelSnapshot {
subjects: Vec<LoadedSourceModelSubject>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ControllerPlantResourcePolicy {
envelope: controller_plant_artifact::ControllerPlantResourceEnvelope,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ControllerProofMtbddResourcePolicy {
envelope: controller_plant_artifact::ControllerProofMtbddResourceEnvelope,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ControllerSplitResourcePolicy {
controller: controller_plant_artifact::ControllerProofEvidenceResourceEnvelope,
plant: controller_plant_artifact::ControllerPlantResourceEnvelope,
max_batches: usize,
max_total_plant_artifact_bytes: usize,
max_total_members: usize,
max_total_transition_evaluations: usize,
}
fn controller_split_resource_capability_line() -> String {
format!(
"controller_split_resource_cli_version={CONTROLLER_SPLIT_RESOURCE_CLI_VERSION} policy_version={CONTROLLER_SPLIT_RESOURCE_POLICY_VERSION} controller_envelope_version={} plant_envelope_version={} controller_artifact_version={} plant_artifact_version={} manifest_version={CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION} max_policy_bytes={CONTROLLER_SPLIT_RESOURCE_POLICY_MAX_BYTES} max_controller_artifact_bytes={} max_unsat_proof_bytes={} max_plant_artifact_bytes={} max_batches={} max_members_per_batch={} max_horizon={} max_product_states={} refusal_exit=3 admission=once verification=unsat-miter exhaustive_replay=no accounting=conservative-static-per-batch-and-total timing_calibration=none result_on_refusal=none refusal_schema=split-reason-v1 unsupported=fail-closed",
controller_plant_artifact::CONTROLLER_PROOF_EVIDENCE_RESOURCE_ENVELOPE_VERSION,
controller_plant_artifact::CONTROLLER_PLANT_RESOURCE_ENVELOPE_VERSION,
controller_plant_artifact::CONTROLLER_PROOF_EVIDENCE_VERSION,
controller_plant_artifact::BOUND_PLANT_RESULTS_VERSION,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
guarded_continuation_checker::unsat_proof::MAX_UNSAT_PROOF_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
guarded_continuation_checker::controller_plant::MAX_COMPOSITION_HORIZON,
guarded_continuation_checker::controller_plant::MAX_PRODUCT_STATES,
)
}
fn controller_split_observability_capability_line() -> String {
format!(
"controller_split_observability_cli_version={CONTROLLER_SPLIT_OBSERVABILITY_CLI_VERSION} base_cli_version={CONTROLLER_SPLIT_RESOURCE_CLI_VERSION} phase_metrics_version={CONTROLLER_SPLIT_PHASE_METRICS_VERSION} phases=policy-and-input,controller-admission,complete-set-preflight,semantic-replay counters=controller-admissions,manifest-loads,plant-artifact-reads,resource-assessments,batch-verifications,buffered-result-rows,prepared-batches,prepared-members,controller-evidence-bytes,total-plant-artifact-bytes,total-transition-evaluation-bound timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed"
)
}
fn controller_split_allocation_observability_capability_line() -> String {
format!(
"controller_split_allocation_observability_cli_version={CONTROLLER_SPLIT_ALLOCATION_OBSERVABILITY_CLI_VERSION} base_observability_cli_version={CONTROLLER_SPLIT_OBSERVABILITY_CLI_VERSION} allocator=system scope=policy-through-replay counters=allocation-calls,allocated-bytes,deallocation-calls,deallocated-bytes,reallocation-calls,reallocated-bytes overflow=fail-closed timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed"
)
}
fn controller_split_cache_observability_capability_line() -> String {
format!(
"controller_split_cache_observability_cli_version={CONTROLLER_SPLIT_CACHE_OBSERVABILITY_CLI_VERSION} base_allocation_observability_cli_version={CONTROLLER_SPLIT_ALLOCATION_OBSERVABILITY_CLI_VERSION} scope=semantic-replay key=manifest-snapshot,resource-assessment,result-sha256 counters=lookups,hits,misses,entries integrity_preflight=required overflow=fail-closed timing_calibration=none partial_metrics_on_failure=none result_on_refusal=none unsupported=fail-closed"
)
}
fn increment_controller_split_observation(counter: &mut usize, name: &str) -> Result<(), String> {
*counter = counter
.checked_add(1)
.ok_or_else(|| format!("controller split observability {name} count overflow"))?;
Ok(())
}
fn synthesis_memory_limit_kind() -> &'static str {
if cfg!(target_os = "macos") {
"unavailable"
} else {
"address-space"
}
}
fn synthesis_memory_limit_bytes() -> u64 {
if cfg!(target_os = "macos") {
0
} else {
YOSYS_MEMORY_LIMIT_BYTES
}
}
#[derive(Clone, Debug)]
struct CachedHelperFormula {
vars: usize,
ratio: usize,
family: String,
seed: u64,
clauses: Vec<Clause>,
aligned_nodes: usize,
order_nodes: usize,
helper_nodes: usize,
}
type Literal = (usize, bool);
#[derive(Clone, Debug)]
struct Factor {
scope: Vec<usize>,
values: Vec<bool>,
}
impl Factor {
fn from_clause(clause: &Clause) -> Self {
let mut scope: Vec<_> = clause.0.iter().map(|&(v, _)| v).collect();
scope.sort_unstable();
scope.dedup();
let values = (0..(1usize << scope.len()))
.map(|bits| {
clause.0.iter().any(|&(v, positive)| {
let pos = scope.binary_search(&v).unwrap();
(((bits >> pos) & 1) == 1) == positive
})
})
.collect();
Self { scope, values }
}
fn evaluate_from(&self, vars: &[usize], bits: usize) -> bool {
let mut local = 0;
for (i, &v) in self.scope.iter().enumerate() {
let source = vars.binary_search(&v).unwrap();
local |= ((bits >> source) & 1) << i;
}
self.values[local]
}
}
#[derive(Debug)]
struct Layer {
variable: usize,
boundary: Vec<usize>,
witness: Vec<Option<bool>>,
bdd_nodes: usize,
}
#[derive(Debug)]
struct SolveResult {
assignment: Option<Vec<bool>>,
peak_boundary: usize,
peak_entries: usize,
peak_bdd_nodes: usize,
layers: Vec<Layer>,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
struct BddNode {
variable: usize,
low: usize,
high: usize,
}
#[derive(Clone, Default)]
struct BddManager {
nodes: Vec<BddNode>,
node_hits: Vec<usize>,
unique: HashMap<BddNode, usize>,
apply_cache: HashMap<(bool, usize, usize), usize>,
node_limit: Option<usize>,
deadline: Option<Instant>,
budget_exceeded: bool,
}
impl BddManager {
fn node(&self, id: usize) -> BddNode {
self.nodes[id - 2]
}
fn make(&mut self, variable: usize, low: usize, high: usize) -> usize {
if self
.deadline
.is_some_and(|deadline| Instant::now() >= deadline)
{
self.budget_exceeded = true;
return 0;
}
if low == high {
return low;
}
let node = BddNode {
variable,
low,
high,
};
if let Some(&id) = self.unique.get(&node) {
self.node_hits[id - 2] += 1;
return id;
}
if self
.node_limit
.is_some_and(|limit| self.nodes.len() >= limit)
{
self.budget_exceeded = true;
return 0;
}
let id = self.nodes.len() + 2;
self.nodes.push(node);
self.node_hits.push(0);
self.unique.insert(node, id);
id
}
fn literal(&mut self, variable: usize, positive: bool) -> usize {
if positive {
self.make(variable, 0, 1)
} else {
self.make(variable, 1, 0)
}
}
fn apply(&mut self, is_and: bool, a: usize, b: usize) -> usize {
let (a, b) = if a <= b { (a, b) } else { (b, a) };
let terminal = if is_and {
if a == 0 || b == 0 {
Some(0)
} else if a == 1 {
Some(b)
} else if a == b {
Some(a)
} else {
None
}
} else if a == 1 || b == 1 {
Some(1)
} else if a == 0 {
Some(b)
} else if a == b {
Some(a)
} else {
None
};
if let Some(value) = terminal {
return value;
}
if let Some(&result) = self.apply_cache.get(&(is_and, a, b)) {
if result >= 2 {
self.node_hits[result - 2] += 1;
}
return result;
}
let av = self.node(a).variable;
let bv = self.node(b).variable;
let variable = av.min(bv);
let an = self.node(a);
let bn = self.node(b);
let (al, ah) = if av == variable {
(an.low, an.high)
} else {
(a, a)
};
let (bl, bh) = if bv == variable {
(bn.low, bn.high)
} else {
(b, b)
};
let low = self.apply(is_and, al, bl);
let high = self.apply(is_and, ah, bh);
let result = self.make(variable, low, high);
self.apply_cache.insert((is_and, a, b), result);
result
}
fn and(&mut self, a: usize, b: usize) -> usize {
self.apply(true, a, b)
}
fn or(&mut self, a: usize, b: usize) -> usize {
self.apply(false, a, b)
}
fn exists(&mut self, root: usize, variable: usize, memo: &mut HashMap<usize, usize>) -> usize {
if root < 2 {
return root;
}
if let Some(&result) = memo.get(&root) {
return result;
}
let node = self.node(root);
let result = if node.variable > variable {
root
} else if node.variable == variable {
self.or(node.low, node.high)
} else {
let low = self.exists(node.low, variable, memo);
let high = self.exists(node.high, variable, memo);
self.make(node.variable, low, high)
};
memo.insert(root, result);
result
}
fn evaluate(&self, mut root: usize, assignment: &[bool]) -> bool {
while root >= 2 {
let node = self.node(root);
root = if assignment[node.variable] {
node.high
} else {
node.low
};
}
root == 1
}
fn negate(&mut self, root: usize, memo: &mut HashMap<usize, usize>) -> usize {
if root < 2 {
return 1 - root;
}
if let Some(&result) = memo.get(&root) {
return result;
}
let node = self.node(root);
let low = self.negate(node.low, memo);
let high = self.negate(node.high, memo);
let result = self.make(node.variable, low, high);
memo.insert(root, result);
result
}
fn restrict(
&mut self,
root: usize,
variable: usize,
value: bool,
memo: &mut HashMap<usize, usize>,
) -> usize {
if root < 2 {
return root;
}
if let Some(&result) = memo.get(&root) {
return result;
}
let node = self.node(root);
let result = if node.variable > variable {
root
} else if node.variable == variable {
if value { node.high } else { node.low }
} else {
let low = self.restrict(node.low, variable, value, memo);
let high = self.restrict(node.high, variable, value, memo);
self.make(node.variable, low, high)
};
memo.insert(root, result);
result
}
fn satisfying_assignment(&self, mut root: usize, variables: usize) -> Option<Vec<bool>> {
if root == 0 {
return None;
}
let mut assignment = vec![false; variables];
while root >= 2 {
let node = self.node(root);
if node.low != 0 {
assignment[node.variable] = false;
root = node.low;
} else {
assignment[node.variable] = true;
root = node.high;
}
}
(root == 1).then_some(assignment)
}
}
#[derive(Debug)]
struct RingStatePoint {
ring: i32,
processed: usize,
residual_states: usize,
}
fn compile_formula_bdd(
vars: usize,
clauses: &[Clause],
bdd_order: &[usize],
) -> (BddManager, usize) {
let mut manager = BddManager::default();
let formula = compile_formula_bdd_into(&mut manager, vars, clauses, bdd_order);
(manager, formula)
}
fn compile_formula_bdd_into(
manager: &mut BddManager,
vars: usize,
clauses: &[Clause],
bdd_order: &[usize],
) -> usize {
let mut rank = vec![usize::MAX; vars];
for (level, &variable) in bdd_order.iter().enumerate() {
rank[variable] = level;
}
let mut formula = 1;
for clause in clauses {
let mut clause_root = 0;
for &(variable, positive) in &clause.0 {
let literal = manager.literal(rank[variable], positive);
clause_root = manager.or(clause_root, literal);
}
formula = manager.and(formula, clause_root);
}
formula
}
fn residual_state_count(manager: &BddManager, root: usize, prefix: usize) -> usize {
let mut states = BTreeSet::new();
let mut visited = BTreeSet::new();
let mut stack = vec![root];
while let Some(current) = stack.pop() {
if current < 2 {
states.insert(current);
continue;
}
if !visited.insert(current) {
continue;
}
let node = manager.node(current);
if node.variable >= prefix {
states.insert(current);
} else {
stack.push(node.low);
stack.push(node.high);
}
}
states.len()
}
fn flower_ring_state_profile(
vars: usize,
clauses: &[Clause],
outside_in: bool,
) -> (usize, Vec<RingStatePoint>) {
let coordinates = flower_coordinates(vars);
let distance = |v: usize| {
let (q, r) = coordinates[v];
q.abs().max(r.abs()).max((q + r).abs())
};
let mut order: Vec<_> = (0..vars).collect();
order.sort_by_key(|&v| {
if outside_in {
(std::cmp::Reverse(distance(v)), v)
} else {
(std::cmp::Reverse(-distance(v)), v)
}
});
let (manager, root) = compile_formula_bdd(vars, clauses, &order);
let mut profile = Vec::new();
let mut processed = 0;
while processed < vars {
let ring = distance(order[processed]);
while processed < vars && distance(order[processed]) == ring {
processed += 1;
}
profile.push(RingStatePoint {
ring,
processed,
residual_states: residual_state_count(&manager, root, processed),
});
}
(manager.nodes.len(), profile)
}
#[derive(Debug)]
struct BddSolveResult {
assignment: Option<Vec<bool>>,
allocated_nodes: usize,
live_nodes: usize,
interaction_candidates: Vec<((Literal, Literal), usize)>,
}
#[derive(Clone)]
struct ProvenanceFactor {
scope: Vec<usize>,
root: usize,
clauses: BTreeSet<usize>,
}
struct IncrementalBddCache {
vars: usize,
manager: BddManager,
checkpoint_layers: Vec<usize>,
checkpoints: Vec<Vec<ProvenanceFactor>>,
recovery: Vec<(usize, usize)>,
assignment: Option<Vec<bool>>,
}
struct DirectVariantResult {
assignment: Option<Vec<bool>>,
nodes: usize,
}
fn solve_tuple_natural(vars: usize, clauses: &[Clause]) -> DirectVariantResult {
let mut manager = BddManager::default();
let mut factors: Vec<(Vec<usize>, usize)> = clauses
.iter()
.map(|clause| {
let (scope, root) = compile_clause_into(&mut manager, clause);
(scope, root)
})
.collect();
let mut recovery = Vec::with_capacity(vars);
for variable in 0..vars {
let mut combined = 1;
let mut boundary = BTreeSet::new();
let mut retained = Vec::new();
for (scope, root) in factors {
if scope.contains(&variable) {
combined = manager.and(combined, root);
boundary.extend(scope.into_iter().filter(|&item| item != variable));
} else {
retained.push((scope, root));
}
}
let projected = manager.exists(combined, variable, &mut HashMap::new());
retained.push((boundary.into_iter().collect(), projected));
factors = retained;
recovery.push((variable, combined));
}
let satisfiable = factors.iter().all(|factor| factor.1 == 1);
let assignment = satisfiable.then(|| {
let mut assignment = vec![false; vars];
for &(variable, combined) in recovery.iter().rev() {
if !manager.evaluate(combined, &assignment) {
assignment[variable] = true;
assert!(manager.evaluate(combined, &assignment));
}
}
assignment
});
DirectVariantResult {
assignment,
nodes: manager.nodes.len(),
}
}
fn solve_provenance_variant(
vars: usize,
clauses: &[Clause],
keep_provenance: bool,
store_checkpoints: bool,
stride: usize,
) -> DirectVariantResult {
let mut manager = BddManager::default();
let mut factors: Vec<_> = clauses
.iter()
.enumerate()
.map(|(index, clause)| {
let (scope, root) = compile_clause_into(&mut manager, clause);
ProvenanceFactor {
scope,
root,
clauses: if keep_provenance {
BTreeSet::from([index])
} else {
BTreeSet::new()
},
}
})
.collect();
let mut recovery = Vec::with_capacity(vars);
let mut _checkpoints = Vec::new();
for variable in 0..vars {
if store_checkpoints && variable % stride.max(1) == 0 {
_checkpoints.push(factors.clone());
}
let mut combined = 1;
let mut boundary = BTreeSet::new();
let mut provenance = BTreeSet::new();
let mut retained = Vec::new();
for factor in factors {
if factor.scope.contains(&variable) {
combined = manager.and(combined, factor.root);
boundary.extend(factor.scope.into_iter().filter(|&item| item != variable));
if keep_provenance {
provenance.extend(factor.clauses);
}
} else {
retained.push(factor);
}
}
let projected = manager.exists(combined, variable, &mut HashMap::new());
retained.push(ProvenanceFactor {
scope: boundary.into_iter().collect(),
root: projected,
clauses: provenance,
});
factors = retained;
recovery.push((variable, combined));
}
let satisfiable = factors.iter().all(|factor| factor.root == 1);
let assignment = satisfiable.then(|| {
let mut assignment = vec![false; vars];
for &(variable, combined) in recovery.iter().rev() {
if !manager.evaluate(combined, &assignment) {
assignment[variable] = true;
assert!(manager.evaluate(combined, &assignment));
}
}
assignment
});
DirectVariantResult {
assignment,
nodes: manager.nodes.len(),
}
}
struct BranchEvaluation {
satisfiable: bool,
branches: usize,
reuse_us: u128,
fresh_us: u128,
reuse_nodes: usize,
fresh_nodes: usize,
cache_build_us: u128,
sibling_us: u128,
cache_nodes: usize,
sibling_new_nodes: usize,
checkpoint_count: usize,
restored_layer: usize,
replayed_layers: usize,
first_satisfiable: bool,
valid: bool,
}
fn preferred_branch_sign(clauses: &[Clause], variable: usize) -> bool {
let positive = clauses
.iter()
.flat_map(|clause| &clause.0)
.filter(|&&(item, sign)| item == variable && sign)
.count();
let negative = clauses
.iter()
.flat_map(|clause| &clause.0)
.filter(|&&(item, sign)| item == variable && !sign)
.count();
positive >= negative
}
fn evaluate_branch_choice(
vars: usize,
clauses: &[Clause],
variable: usize,
stride: usize,
) -> BranchEvaluation {
let preferred = preferred_branch_sign(clauses, variable);
let mut first_formula = clauses.to_vec();
first_formula.push(Clause(vec![(variable, preferred)]));
let branch_clause = first_formula.len() - 1;
let sibling_clause = Clause(vec![(variable, !preferred)]);
let mut sibling_formula = first_formula.clone();
sibling_formula[branch_clause] = sibling_clause.clone();
let cache_start = Instant::now();
let mut cache = build_incremental_bdd_cache_with_stride(vars, &first_formula, stride);
let cache_build_us = cache_start.elapsed().as_micros();
let cache_nodes = cache.manager.nodes.len();
let checkpoint_count = cache.checkpoints.len();
let first_sat = cache.assignment.is_some();
let first_valid = cache
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&first_formula, assignment));
let (sibling_assignment, branches, sibling_us, sibling_new_nodes, restored_layer) = if first_sat
{
(None, 1, 0, 0, vars)
} else {
let sibling_start = Instant::now();
let (assignment, new_nodes, _, restored) =
incremental_clause_update(&mut cache, branch_clause, &sibling_clause);
(
assignment,
2,
sibling_start.elapsed().as_micros(),
new_nodes,
restored,
)
};
let reuse_us = cache_build_us + sibling_us;
let sibling_valid = sibling_assignment
.as_ref()
.is_none_or(|assignment| satisfies(&sibling_formula, assignment));
let satisfiable = first_sat || sibling_assignment.is_some();
let reuse_nodes = cache.manager.nodes.len();
let natural: Vec<_> = (0..vars).collect();
let fresh_start = Instant::now();
let fresh_first = eliminate_with_bdds(vars, &first_formula, &natural);
let mut fresh_nodes = fresh_first.allocated_nodes;
let fresh_satisfiable = if fresh_first.assignment.is_some() {
true
} else {
let fresh_sibling = eliminate_with_bdds(vars, &sibling_formula, &natural);
fresh_nodes += fresh_sibling.allocated_nodes;
fresh_sibling.assignment.is_some()
};
let fresh_us = fresh_start.elapsed().as_micros();
BranchEvaluation {
satisfiable,
branches,
reuse_us,
fresh_us,
reuse_nodes,
fresh_nodes,
cache_build_us,
sibling_us,
cache_nodes,
sibling_new_nodes,
checkpoint_count,
restored_layer,
replayed_layers: if first_sat { 0 } else { vars - restored_layer },
first_satisfiable: first_sat,
valid: first_valid && sibling_valid && satisfiable == fresh_satisfiable,
}
}
fn evaluate_forced_branch_pair(
vars: usize,
clauses: &[Clause],
variable: usize,
stride: usize,
) -> BranchEvaluation {
let preferred = preferred_branch_sign(clauses, variable);
let mut first_formula = clauses.to_vec();
first_formula.push(Clause(vec![(variable, preferred)]));
let branch_clause = first_formula.len() - 1;
let sibling_clause = Clause(vec![(variable, !preferred)]);
let mut sibling_formula = first_formula.clone();
sibling_formula[branch_clause] = sibling_clause.clone();
let cache_start = Instant::now();
let mut cache = build_incremental_bdd_cache_with_stride(vars, &first_formula, stride);
let cache_build_us = cache_start.elapsed().as_micros();
let cache_nodes = cache.manager.nodes.len();
let checkpoint_count = cache.checkpoints.len();
let first_assignment = cache.assignment.clone();
let sibling_start = Instant::now();
let (sibling_assignment, sibling_new_nodes, _, restored_layer) =
incremental_clause_update(&mut cache, branch_clause, &sibling_clause);
let sibling_us = sibling_start.elapsed().as_micros();
let reuse_us = cache_build_us + sibling_us;
let satisfiable = first_assignment.is_some() || sibling_assignment.is_some();
let valid = first_assignment
.as_ref()
.is_none_or(|assignment| satisfies(&first_formula, assignment))
&& sibling_assignment
.as_ref()
.is_none_or(|assignment| satisfies(&sibling_formula, assignment));
BranchEvaluation {
satisfiable,
branches: 2,
reuse_us,
fresh_us: 0,
reuse_nodes: cache.manager.nodes.len(),
fresh_nodes: 0,
cache_build_us,
sibling_us,
cache_nodes,
sibling_new_nodes,
checkpoint_count,
restored_layer,
replayed_layers: vars - restored_layer,
first_satisfiable: first_assignment.is_some(),
valid,
}
}
fn branching_scores(vars: usize, clauses: &[Clause], alpha: f64) -> Vec<f64> {
let graph = primal_graph(vars, clauses);
let mut positive = vec![0usize; vars];
let mut negative = vec![0usize; vars];
for clause in clauses {
for &(variable, sign) in &clause.0 {
if sign {
positive[variable] += 1
} else {
negative[variable] += 1
}
}
}
(0..vars)
.map(|variable| {
let occurrence = positive[variable] + negative[variable];
let balance = 1 + positive[variable].min(negative[variable]);
let impact =
(1 + occurrence) as f64 * (1 + graph[variable].len()) as f64 * balance as f64;
let recomputation = (vars - variable).max(1) as f64;
impact / recomputation.powf(alpha)
})
.collect()
}
fn supervised_branch_feature_matrix(vars: usize, clauses: &[Clause]) -> Vec<Vec<f64>> {
let graph = primal_graph(vars, clauses);
let scent = diffuse_scent_variant(vars, clauses, 2, 0);
let scent_mean = scent.iter().sum::<f64>() / scent.len().max(1) as f64;
let mut positive = vec![0usize; vars];
let mut negative = vec![0usize; vars];
let mut earliest_sum = vec![0usize; vars];
let mut containing = vec![0usize; vars];
for clause in clauses {
let earliest = clause.0.iter().map(|literal| literal.0).min().unwrap_or(0);
for &(variable, sign) in &clause.0 {
containing[variable] += 1;
earliest_sum[variable] += earliest;
if sign {
positive[variable] += 1
} else {
negative[variable] += 1
}
}
}
(0..vars)
.map(|variable| {
let neighbours: Vec<_> = graph[variable].iter().copied().collect();
let neighbour_mean =
neighbours.iter().sum::<usize>() as f64 / neighbours.len().max(1) as f64;
let neighbour_span = neighbours
.iter()
.map(|&other| variable.abs_diff(other))
.sum::<usize>() as f64
/ neighbours.len().max(1) as f64;
let occurrence = positive[variable] + negative[variable];
vec![
1.0,
variable as f64 / vars.max(1) as f64,
occurrence as f64 / clauses.len().max(1) as f64,
graph[variable].len() as f64 / vars.max(1) as f64,
positive[variable].min(negative[variable]) as f64 / occurrence.max(1) as f64,
positive[variable] as f64 / occurrence.max(1) as f64,
neighbour_mean / vars.max(1) as f64,
neighbour_span / vars.max(1) as f64,
scent[variable] / scent_mean.max(1e-9),
earliest_sum[variable] as f64
/ containing[variable].max(1) as f64
/ vars.max(1) as f64,
]
})
.collect()
}
fn compile_clause_into(manager: &mut BddManager, clause: &Clause) -> (Vec<usize>, usize) {
let mut root = 0;
let mut scope = Vec::new();
for &(variable, positive) in &clause.0 {
let literal = manager.literal(variable, positive);
root = manager.or(root, literal);
scope.push(variable);
}
scope.sort_unstable();
scope.dedup();
(scope, root)
}
fn build_incremental_bdd_cache(vars: usize, clauses: &[Clause]) -> IncrementalBddCache {
build_incremental_bdd_cache_with_stride(vars, clauses, 1)
}
fn build_incremental_bdd_cache_with_stride(
vars: usize,
clauses: &[Clause],
stride: usize,
) -> IncrementalBddCache {
let mut manager = BddManager::default();
let mut factors: Vec<_> = clauses
.iter()
.enumerate()
.map(|(index, clause)| {
let (scope, root) = compile_clause_into(&mut manager, clause);
ProvenanceFactor {
scope,
root,
clauses: BTreeSet::from([index]),
}
})
.collect();
let mut checkpoint_layers = Vec::new();
let mut checkpoints = Vec::new();
let mut recovery = Vec::with_capacity(vars);
for variable in 0..vars {
if variable % stride.max(1) == 0 {
checkpoint_layers.push(variable);
checkpoints.push(factors.clone());
}
let mut combined = 1;
let mut boundary = BTreeSet::new();
let mut provenance = BTreeSet::new();
let mut retained = Vec::new();
for factor in factors {
if factor.scope.contains(&variable) {
combined = manager.and(combined, factor.root);
boundary.extend(factor.scope.into_iter().filter(|&item| item != variable));
provenance.extend(factor.clauses);
} else {
retained.push(factor);
}
}
let projected = manager.exists(combined, variable, &mut HashMap::new());
retained.push(ProvenanceFactor {
scope: boundary.into_iter().collect(),
root: projected,
clauses: provenance,
});
factors = retained;
recovery.push((variable, combined));
}
let satisfiable = factors.iter().all(|factor| factor.root == 1);
let assignment = satisfiable.then(|| {
let mut assignment = vec![false; vars];
for &(variable, combined) in recovery.iter().rev() {
if !manager.evaluate(combined, &assignment) {
assignment[variable] = true;
assert!(manager.evaluate(combined, &assignment));
}
}
assignment
});
IncrementalBddCache {
vars,
manager,
checkpoint_layers,
checkpoints,
recovery,
assignment,
}
}
fn incremental_clause_update(
cache: &mut IncrementalBddCache,
clause_index: usize,
replacement: &Clause,
) -> (Option<Vec<bool>>, usize, usize, usize) {
let earliest = replacement
.0
.iter()
.map(|literal| literal.0)
.min()
.unwrap_or(0);
let checkpoint_index = cache
.checkpoint_layers
.iter()
.rposition(|&layer| layer <= earliest)
.unwrap();
let checkpoint_layer = cache.checkpoint_layers[checkpoint_index];
let mut factors = cache.checkpoints[checkpoint_index].clone();
for variable in checkpoint_layer..earliest {
let mut combined = 1;
let mut boundary = BTreeSet::new();
let mut provenance = BTreeSet::new();
let mut retained = Vec::new();
for factor in factors {
if factor.scope.contains(&variable) {
combined = cache.manager.and(combined, factor.root);
boundary.extend(factor.scope.into_iter().filter(|&item| item != variable));
provenance.extend(factor.clauses);
} else {
retained.push(factor);
}
}
let projected = cache
.manager
.exists(combined, variable, &mut HashMap::new());
retained.push(ProvenanceFactor {
scope: boundary.into_iter().collect(),
root: projected,
clauses: provenance,
});
factors = retained;
}
let target = factors
.iter()
.position(|factor| factor.clauses.contains(&clause_index))
.expect("changed clause must remain identifiable before its first variable");
assert_eq!(factors[target].clauses.len(), 1);
factors.remove(target);
let (scope, root) = compile_clause_into(&mut cache.manager, replacement);
factors.push(ProvenanceFactor {
scope,
root,
clauses: BTreeSet::from([clause_index]),
});
let mut recovery = cache.recovery[..earliest].to_vec();
let initial_nodes = cache.manager.nodes.len();
for variable in earliest..cache.vars {
let mut combined = 1;
let mut boundary = BTreeSet::new();
let mut provenance = BTreeSet::new();
let mut retained = Vec::new();
for factor in factors {
if factor.scope.contains(&variable) {
combined = cache.manager.and(combined, factor.root);
boundary.extend(factor.scope.into_iter().filter(|&item| item != variable));
provenance.extend(factor.clauses);
} else {
retained.push(factor);
}
}
let projected = cache
.manager
.exists(combined, variable, &mut HashMap::new());
retained.push(ProvenanceFactor {
scope: boundary.into_iter().collect(),
root: projected,
clauses: provenance,
});
factors = retained;
recovery.push((variable, combined));
}
let satisfiable = factors.iter().all(|factor| factor.root == 1);
let assignment = satisfiable.then(|| {
let mut assignment = vec![false; cache.vars];
for &(variable, combined) in recovery.iter().rev() {
if !cache.manager.evaluate(combined, &assignment) {
assignment[variable] = true;
assert!(cache.manager.evaluate(combined, &assignment));
}
}
assignment
});
(
assignment,
cache.manager.nodes.len() - initial_nodes,
earliest,
checkpoint_layer,
)
}
fn eliminate_with_bdds(num_vars: usize, clauses: &[Clause], order: &[usize]) -> BddSolveResult {
let mut manager = BddManager::default();
let mut factors: Vec<(Vec<usize>, usize)> = clauses
.iter()
.map(|clause| {
let mut root = 0;
let mut scope = Vec::new();
for &(variable, positive) in &clause.0 {
let literal = manager.literal(variable, positive);
root = manager.or(root, literal);
scope.push(variable);
}
scope.sort_unstable();
scope.dedup();
(scope, root)
})
.collect();
let mut recovery = Vec::new();
for &variable in order {
let mut combined = 1;
let mut boundary = BTreeSet::new();
let mut retained = Vec::new();
for (scope, root) in factors {
if scope.contains(&variable) {
combined = manager.and(combined, root);
boundary.extend(scope.into_iter().filter(|&v| v != variable));
} else {
retained.push((scope, root));
}
}
let projected = manager.exists(combined, variable, &mut HashMap::new());
retained.push((boundary.iter().copied().collect(), projected));
factors = retained;
recovery.push((variable, combined));
}
let satisfiable = factors.iter().all(|(_, root)| *root == 1);
let assignment = satisfiable.then(|| {
let mut assignment = vec![false; num_vars];
for &(variable, combined) in recovery.iter().rev() {
assignment[variable] = false;
if !manager.evaluate(combined, &assignment) {
assignment[variable] = true;
assert!(manager.evaluate(combined, &assignment));
}
}
assignment
});
let mut reachable = BTreeSet::new();
let mut stack: Vec<_> = factors.iter().map(|(_, root)| *root).collect();
stack.extend(recovery.iter().map(|(_, root)| *root));
while let Some(root) = stack.pop() {
if root < 2 || !reachable.insert(root) {
continue;
}
let node = manager.node(root);
stack.push(node.low);
stack.push(node.high);
}
let mut interactions: HashMap<(Literal, Literal), usize> = HashMap::new();
for node in &manager.nodes {
for (parent_sign, child) in [(false, node.low), (true, node.high)] {
if child < 2 {
continue;
}
let child_variable = manager.node(child).variable;
for child_sign in [false, true] {
let a = (node.variable, parent_sign);
let b = (child_variable, child_sign);
let pair = if a <= b { (a, b) } else { (b, a) };
*interactions.entry(pair).or_default() += 1;
}
}
}
let mut interaction_candidates: Vec<_> = interactions.into_iter().collect();
interaction_candidates.sort_by_key(|&(pair, score)| (std::cmp::Reverse(score), pair));
BddSolveResult {
assignment,
allocated_nodes: manager.nodes.len(),
live_nodes: reachable.len(),
interaction_candidates,
}
}
fn eliminate_with_bdds_ordered(
num_vars: usize,
clauses: &[Clause],
elimination_order: &[usize],
bdd_order: &[usize],
) -> BddSolveResult {
assert_eq!(bdd_order.len(), num_vars);
let mut rank = vec![usize::MAX; num_vars];
for (level, &variable) in bdd_order.iter().enumerate() {
assert!(variable < num_vars && rank[variable] == usize::MAX);
rank[variable] = level;
}
let mapped_clauses: Vec<_> = clauses
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(variable, sign)| (rank[variable], sign))
.collect(),
)
})
.collect();
let mapped_elimination: Vec<_> = elimination_order.iter().map(|&v| rank[v]).collect();
let mut result = eliminate_with_bdds(num_vars, &mapped_clauses, &mapped_elimination);
if let Some(mapped_assignment) = result.assignment.take() {
let mut assignment = vec![false; num_vars];
for original in 0..num_vars {
assignment[original] = mapped_assignment[rank[original]];
}
result.assignment = Some(assignment);
}
for ((a, b), _) in &mut result.interaction_candidates {
a.0 = bdd_order[a.0];
b.0 = bdd_order[b.0];
if *a > *b {
std::mem::swap(a, b);
}
}
result
}
fn occurrence_order(vars: usize, clauses: &[Clause], descending: bool) -> Vec<usize> {
let mut counts = vec![0usize; vars];
for clause in clauses {
for &(variable, _) in &clause.0 {
counts[variable] += 1;
}
}
let mut order: Vec<_> = (0..vars).collect();
if descending {
order.sort_by_key(|&v| (std::cmp::Reverse(counts[v]), v));
} else {
order.sort_by_key(|&v| (counts[v], v));
}
order
}
// Cheap, deterministic permutations inspired by musical structure. These are
// only ordering heuristics: they do not change the formula or inspect BDD cost.
fn metrical_order(base: &[usize], beats: usize) -> Vec<usize> {
(0..beats)
.flat_map(|beat| base.iter().skip(beat).step_by(beats).copied())
.collect()
}
fn phrase_order(base: &[usize], phrase: usize) -> Vec<usize> {
base.chunks(phrase)
.enumerate()
.flat_map(|(index, chunk)| {
let mut notes = chunk.to_vec();
if index % 2 == 1 {
notes.reverse();
}
notes
})
.collect()
}
fn counterpoint_order(base: &[usize]) -> Vec<usize> {
let mut result = Vec::with_capacity(base.len());
let (mut left, mut right) = (0usize, base.len());
while left < right {
result.push(base[left]);
left += 1;
if left < right {
right -= 1;
result.push(base[right]);
}
}
result
}
fn motif_order(vars: usize, clauses: &[Clause], base: &[usize]) -> Vec<usize> {
let graph = primal_graph(vars, clauses);
let mut signatures = vec![(0usize, 0usize); vars];
for clause in clauses {
for &(v, sign) in &clause.0 {
if sign {
signatures[v].0 += 1
} else {
signatures[v].1 += 1
}
}
}
let mut rank = vec![0usize; vars];
for (i, &v) in base.iter().enumerate() {
rank[v] = i;
}
let mut result: Vec<_> = (0..vars).collect();
// Variables with the same local "tone colour" become a repeated motif.
result.sort_by_key(|&v| (signatures[v], graph[v].len(), rank[v]));
result
}
#[derive(Clone, Copy, Debug)]
struct PhraseRule {
length: usize,
mode: u8,
}
fn phrase_rule_name(rule: PhraseRule) -> &'static str {
match rule.mode {
0 => "alternate",
1 => "all",
2 => "boundary-forward",
3 => "boundary-backward",
4 => "dense-alternate",
5 => "no-op",
_ => unreachable!(),
}
}
fn apply_phrase_rule(
vars: usize,
clauses: &[Clause],
base: &[usize],
rule: PhraseRule,
) -> Vec<usize> {
let graph = primal_graph(vars, clauses);
let mut rank = vec![0usize; vars];
for (i, &v) in base.iter().enumerate() {
rank[v] = i;
}
let mut result = Vec::with_capacity(vars);
for (chunk_index, chunk) in base.chunks(rule.length).enumerate() {
let start = chunk_index * rule.length;
let end = (start + chunk.len()).min(vars);
let backward: usize = chunk
.iter()
.map(|&v| graph[v].iter().filter(|&&n| rank[n] < start).count())
.sum();
let forward: usize = chunk
.iter()
.map(|&v| graph[v].iter().filter(|&&n| rank[n] >= end).count())
.sum();
let internal: usize = chunk
.iter()
.map(|&v| {
graph[v]
.iter()
.filter(|&&n| rank[n] >= start && rank[n] < end)
.count()
})
.sum::<usize>()
/ 2;
let reverse = match rule.mode {
0 => chunk_index % 2 == 1,
1 => true,
2 => forward > backward,
3 => backward > forward,
4 => chunk_index % 2 == 1 && internal * 2 >= chunk.len(),
5 => false,
_ => unreachable!(),
};
if reverse {
result.extend(chunk.iter().rev().copied());
} else {
result.extend(chunk.iter().copied());
}
}
result
}
fn phrase_rules() -> Vec<PhraseRule> {
let mut rules: Vec<_> = (2..=10)
.flat_map(|length| (0..5).map(move |mode| PhraseRule { length, mode }))
.collect();
rules.push(PhraseRule { length: 1, mode: 5 });
rules
}
#[derive(Clone, Copy, Debug)]
struct ScentRule {
length: usize,
hops: usize,
strongest_first: bool,
}
fn diffuse_scent_variant(vars: usize, clauses: &[Clause], hops: usize, variant: u8) -> Vec<f64> {
let graph = primal_graph(vars, clauses);
let mut positive = vec![0usize; vars];
let mut negative = vec![0usize; vars];
for clause in clauses {
for &(v, sign) in &clause.0 {
if sign {
positive[v] += 1;
} else {
negative[v] += 1;
}
}
}
// Local emission: frequent, highly connected variables with evidence for
// both polarities emit the strongest ambiguity/tension signal.
let mut scent: Vec<_> = (0..vars)
.map(|v| match variant {
0 => {
let occurrences = positive[v] + negative[v];
let conflict = positive[v].min(negative[v]);
(1 + graph[v].len()) as f64 * (1 + conflict) as f64 * (1 + occurrences) as f64
}
1 => (1 + graph[v].len()) as f64,
2 => (1 + positive[v].min(negative[v])) as f64,
3 => {
let mut x = (v as u64 + 1).wrapping_mul(0x9E3779B97F4A7C15);
x ^= x >> 30;
x = x.wrapping_mul(0xBF58476D1CE4E5B9);
(x ^ (x >> 27)) as f64
}
_ => unreachable!(),
})
.collect();
for _ in 0..hops {
let previous = scent.clone();
for v in 0..vars {
let neighbour_mean =
graph[v].iter().map(|&n| previous[n]).sum::<f64>() / graph[v].len().max(1) as f64;
// Attenuated diffusion retains local evidence while carrying a
// summary of increasingly distant structure.
scent[v] = 0.5 * previous[v] + 0.5 * neighbour_mean;
}
}
scent
}
fn apply_scent_rule(
vars: usize,
clauses: &[Clause],
base: &[usize],
rule: ScentRule,
) -> Vec<usize> {
apply_scent_rule_variant(vars, clauses, base, rule, 0)
}
fn apply_scent_rule_variant(
vars: usize,
clauses: &[Clause],
base: &[usize],
rule: ScentRule,
variant: u8,
) -> Vec<usize> {
if rule.hops == 0 && rule.length == 1 {
return base.to_vec();
}
let scent = diffuse_scent_variant(vars, clauses, rule.hops, variant);
base.chunks(rule.length)
.flat_map(|chunk| {
let mut phrase = chunk.to_vec();
phrase.sort_by(|&a, &b| {
let comparison = scent[a].total_cmp(&scent[b]);
let comparison = if rule.strongest_first {
comparison.reverse()
} else {
comparison
};
comparison.then_with(|| a.cmp(&b))
});
phrase
})
.collect()
}
fn scent_rules() -> Vec<ScentRule> {
let mut rules: Vec<_> = (2..=10)
.flat_map(|length| {
(1..=4).flat_map(move |hops| {
[false, true]
.into_iter()
.map(move |strongest_first| ScentRule {
length,
hops,
strongest_first,
})
})
})
.collect();
rules.push(ScentRule {
length: 1,
hops: 0,
strongest_first: false,
});
rules
}
const SCENT_GATE_FEATURES: usize = 8;
fn scent_gate_features(vars: usize, clauses: &[Clause]) -> [f64; SCENT_GATE_FEATURES] {
let graph = primal_graph(vars, clauses);
let order = min_fill_order(vars, clauses);
let mut rank = vec![0usize; vars];
for (i, &v) in order.iter().enumerate() {
rank[v] = i;
}
let degrees: Vec<_> = graph.iter().map(BTreeSet::len).collect();
let degree_mean = degrees.iter().sum::<usize>() as f64 / vars.max(1) as f64;
let degree_std = (degrees
.iter()
.map(|&d| (d as f64 - degree_mean).powi(2))
.sum::<f64>()
/ vars.max(1) as f64)
.sqrt();
let mut triangles = 0usize;
let mut wedges = 0usize;
let mut span_sum = 0usize;
let mut edges = 0usize;
for v in 0..vars {
let neighbours: Vec<_> = graph[v].iter().copied().collect();
wedges += neighbours.len().saturating_sub(1) * neighbours.len() / 2;
for (i, &a) in neighbours.iter().enumerate() {
triangles += neighbours[i + 1..]
.iter()
.filter(|&&b| graph[a].contains(&b))
.count();
}
for &n in graph[v].range((v + 1)..) {
span_sum += rank[v].abs_diff(rank[n]);
edges += 1;
}
}
let mut positive = vec![0usize; vars];
let mut negative = vec![0usize; vars];
for clause in clauses {
for &(v, sign) in &clause.0 {
if sign {
positive[v] += 1
} else {
negative[v] += 1
}
}
}
let polarity_conflict = (0..vars)
.map(|v| positive[v].min(negative[v]))
.sum::<usize>() as f64
/ clauses.len().max(1) as f64;
let (width, work) = elimination_cost(vars, clauses, &order);
[
1.0,
clauses.len() as f64 / vars.max(1) as f64,
degree_mean / vars.max(1) as f64,
degree_std / degree_mean.max(1e-9),
triangles as f64 / wedges.max(1) as f64,
span_sum as f64 / edges.max(1) as f64 / vars.max(1) as f64,
width as f64 / vars.max(1) as f64,
polarity_conflict + work.max(1.0).ln() / 100.0,
]
}
// Deliberately avoids graph construction, min-fill, and elimination simulation.
// Every literal is visited once; variable IDs provide a cheap locality signal.
fn cheap_structure_features(vars: usize, clauses: &[Clause]) -> [f64; SCENT_GATE_FEATURES] {
let mut occurrences = vec![0usize; vars];
let mut positive = vec![0usize; vars];
let mut span_sum = 0usize;
let mut pair_distance = 0usize;
let mut pairs = 0usize;
for clause in clauses {
let mut low = vars;
let mut high = 0usize;
for &(variable, sign) in &clause.0 {
occurrences[variable] += 1;
positive[variable] += usize::from(sign);
low = low.min(variable);
high = high.max(variable);
}
span_sum += high.saturating_sub(low);
for i in 0..clause.0.len() {
for j in i + 1..clause.0.len() {
pair_distance += clause.0[i].0.abs_diff(clause.0[j].0);
pairs += 1;
}
}
}
let mean = occurrences.iter().sum::<usize>() as f64 / vars.max(1) as f64;
let std = (occurrences
.iter()
.map(|&value| (value as f64 - mean).powi(2))
.sum::<f64>()
/ vars.max(1) as f64)
.sqrt();
let active = occurrences.iter().filter(|&&value| value > 0).count();
let conflict = (0..vars)
.map(|variable| positive[variable].min(occurrences[variable] - positive[variable]))
.sum::<usize>();
[
1.0,
clauses.len() as f64 / vars.max(1) as f64,
mean / clauses.len().max(1) as f64,
std / mean.max(1e-9),
active as f64 / vars.max(1) as f64,
span_sum as f64 / clauses.len().max(1) as f64 / vars.max(1) as f64,
pair_distance as f64 / pairs.max(1) as f64 / vars.max(1) as f64,
conflict as f64 / occurrences.iter().sum::<usize>().max(1) as f64,
]
}
fn scent_gate_predict(
training: &[([f64; SCENT_GATE_FEATURES], f64)],
features: &[f64; SCENT_GATE_FEATURES],
neighbours: usize,
) -> f64 {
let mut mean = [0.0; SCENT_GATE_FEATURES];
for (sample, _) in training {
for i in 0..SCENT_GATE_FEATURES {
mean[i] += sample[i];
}
}
for value in &mut mean {
*value /= training.len().max(1) as f64;
}
let mut scale = [0.0; SCENT_GATE_FEATURES];
for (sample, _) in training {
for i in 0..SCENT_GATE_FEATURES {
scale[i] += (sample[i] - mean[i]).powi(2);
}
}
for value in &mut scale {
*value = (*value / training.len().max(1) as f64).sqrt().max(1e-9);
}
let mut distances: Vec<_> = training
.iter()
.map(|(sample, label)| {
let distance = (1..SCENT_GATE_FEATURES)
.map(|i| ((features[i] - sample[i]) / scale[i]).powi(2))
.sum::<f64>();
(distance, *label)
})
.collect();
distances.sort_by(|a, b| a.0.total_cmp(&b.0));
distances
.iter()
.take(neighbours)
.map(|(_, y)| y)
.sum::<f64>()
/ neighbours.min(distances.len()).max(1) as f64
}
fn support_distance(
training: &[([f64; SCENT_GATE_FEATURES], f64)],
features: &[f64; SCENT_GATE_FEATURES],
) -> f64 {
let mut mean = [0.0; SCENT_GATE_FEATURES];
for (sample, _) in training {
for i in 1..SCENT_GATE_FEATURES {
mean[i] += sample[i];
}
}
for value in &mut mean {
*value /= training.len().max(1) as f64;
}
let mut scale = [0.0; SCENT_GATE_FEATURES];
for (sample, _) in training {
for i in 1..SCENT_GATE_FEATURES {
scale[i] += (sample[i] - mean[i]).powi(2);
}
}
for value in &mut scale {
*value = (*value / training.len().max(1) as f64).sqrt().max(1e-9);
}
training
.iter()
.map(|(sample, _)| {
(1..SCENT_GATE_FEATURES)
.map(|i| ((features[i] - sample[i]) / scale[i]).powi(2))
.sum::<f64>()
.sqrt()
})
.fold(f64::INFINITY, f64::min)
}
fn learn_regime_rejection(
records: &[([f64; SCENT_GATE_FEATURES], f64, usize)],
neighbours: usize,
) -> (f64, f64, f64, usize) {
let mut oof = Vec::new();
for &(features, label, regime) in records {
let fold: Vec<_> = records
.iter()
.filter(|record| record.2 != regime)
.map(|record| (record.0, record.1))
.collect();
oof.push((
scent_gate_predict(&fold, &features, neighbours),
support_distance(&fold, &features),
label.exp(),
));
}
let mut prediction_thresholds = vec![f64::NEG_INFINITY, 0.0];
prediction_thresholds.extend(oof.iter().map(|item| item.0));
// Never permit an unbounded cap: a query farther away than every held-out
// training regime is, by definition, outside observed support.
let mut distance_thresholds = vec![0.0];
distance_thresholds.extend(oof.iter().map(|item| item.1));
let mut best = (f64::NEG_INFINITY, 0.0, 1.0, 0usize);
for prediction_threshold in prediction_thresholds {
for &distance_threshold in &distance_thresholds {
let applied = oof
.iter()
.filter(|item| item.0 < prediction_threshold && item.1 <= distance_threshold)
.count();
let ratio = oof
.iter()
.map(|item| {
if item.0 < prediction_threshold && item.1 <= distance_threshold {
item.2
} else {
1.0
}
})
.sum::<f64>()
/ oof.len().max(1) as f64;
if ratio < best.2 - 1e-12 || ((ratio - best.2).abs() <= 1e-12 && applied < best.3) {
best = (prediction_threshold, distance_threshold, ratio, applied);
}
}
}
best
}
fn learn_scent_gate_threshold(
training: &[([f64; SCENT_GATE_FEATURES], f64)],
neighbours: usize,
) -> (f64, f64, usize) {
let mut out_of_fold = Vec::with_capacity(training.len());
for held_out in 0..training.len() {
let fold: Vec<_> = training
.iter()
.enumerate()
.filter(|(index, _)| *index != held_out)
.map(|(_, sample)| *sample)
.collect();
let prediction = scent_gate_predict(&fold, &training[held_out].0, neighbours);
out_of_fold.push((prediction, training[held_out].1));
}
let mut predictions: Vec<_> = out_of_fold
.iter()
.map(|(prediction, _)| *prediction)
.collect();
predictions.sort_by(f64::total_cmp);
predictions.dedup_by(|a, b| a.total_cmp(b).is_eq());
let mut thresholds = vec![f64::NEG_INFINITY];
thresholds.extend(predictions.windows(2).map(|pair| (pair[0] + pair[1]) / 2.0));
thresholds.push(f64::INFINITY);
let mut best = (f64::NEG_INFINITY, 1.0, 0usize);
for threshold in thresholds {
let applied = out_of_fold
.iter()
.filter(|(prediction, _)| *prediction < threshold)
.count();
let mean_ratio = out_of_fold
.iter()
.map(|(prediction, actual_log_ratio)| {
if *prediction < threshold {
actual_log_ratio.exp()
} else {
1.0
}
})
.sum::<f64>()
/ out_of_fold.len().max(1) as f64;
if mean_ratio < best.1 - 1e-12 || ((mean_ratio - best.1).abs() <= 1e-12 && applied < best.2)
{
best = (threshold, mean_ratio, applied);
}
}
best
}
fn helper_gate_predict(
training: &[([f64; HELPER_GATE_FEATURES], f64)],
features: &[f64; HELPER_GATE_FEATURES],
neighbours: usize,
) -> f64 {
let mut mean = [0.0; HELPER_GATE_FEATURES];
for (sample, _) in training {
for i in 0..HELPER_GATE_FEATURES {
mean[i] += sample[i];
}
}
for value in &mut mean {
*value /= training.len().max(1) as f64;
}
let mut scale = [0.0; HELPER_GATE_FEATURES];
for (sample, _) in training {
for i in 0..HELPER_GATE_FEATURES {
scale[i] += (sample[i] - mean[i]).powi(2);
}
}
for value in &mut scale {
*value = (*value / training.len().max(1) as f64).sqrt().max(1e-9);
}
let mut distances: Vec<_> = training
.iter()
.map(|(sample, label)| {
let distance = (1..HELPER_GATE_FEATURES)
.map(|i| ((features[i] - sample[i]) / scale[i]).powi(2))
.sum::<f64>();
(distance, *label)
})
.collect();
distances.sort_by(|a, b| a.0.total_cmp(&b.0));
distances
.iter()
.take(neighbours)
.map(|(_, y)| y)
.sum::<f64>()
/ neighbours.min(distances.len()).max(1) as f64
}
fn learn_helper_gate_threshold(
training: &[([f64; HELPER_GATE_FEATURES], f64)],
neighbours: usize,
) -> (f64, f64, usize) {
let mut out_of_fold = Vec::with_capacity(training.len());
for held_out in 0..training.len() {
let fold: Vec<_> = training
.iter()
.enumerate()
.filter(|(index, _)| *index != held_out)
.map(|(_, sample)| *sample)
.collect();
out_of_fold.push((
helper_gate_predict(&fold, &training[held_out].0, neighbours),
training[held_out].1,
));
}
let mut predictions: Vec<_> = out_of_fold.iter().map(|sample| sample.0).collect();
predictions.sort_by(f64::total_cmp);
predictions.dedup_by(|a, b| a.total_cmp(b).is_eq());
let mut thresholds = vec![f64::NEG_INFINITY];
thresholds.extend(predictions.windows(2).map(|pair| (pair[0] + pair[1]) / 2.0));
thresholds.push(f64::INFINITY);
let mut best = (f64::NEG_INFINITY, 1.0, 0usize);
for threshold in thresholds {
let applied = out_of_fold
.iter()
.filter(|sample| sample.0 < threshold)
.count();
let ratio = out_of_fold
.iter()
.map(|sample| {
if sample.0 < threshold {
sample.1.exp()
} else {
1.0
}
})
.sum::<f64>()
/ out_of_fold.len().max(1) as f64;
if ratio < best.1 - 1e-12 || ((ratio - best.1).abs() <= 1e-12 && applied < best.2) {
best = (threshold, ratio, applied);
}
}
best
}
fn harmonic_order(vars: usize, clauses: &[Clause], base: &[usize]) -> Vec<usize> {
let graph = primal_graph(vars, clauses);
let rank: Vec<_> = {
let mut rank = vec![0; vars];
for (i, &v) in base.iter().enumerate() {
rank[v] = i;
}
rank
};
let mut unused = vec![true; vars];
let mut result = Vec::with_capacity(vars);
let mut current = base[0];
while result.len() < vars {
result.push(current);
unused[current] = false;
current = (0..vars)
.filter(|&v| unused[v])
.min_by_key(|&v| {
// Prefer a consonant (adjacent) variable with smooth movement
// in the min-fill coordinate system.
(
!graph[current].contains(&v),
rank[current].abs_diff(rank[v]),
rank[v],
)
})
.unwrap_or(current);
}
result
}
fn tension_order(vars: usize, clauses: &[Clause], resolving: bool) -> Vec<usize> {
let graph = primal_graph(vars, clauses);
let mut positive = vec![0usize; vars];
let mut negative = vec![0usize; vars];
for clause in clauses {
for &(v, sign) in &clause.0 {
if sign {
positive[v] += 1
} else {
negative[v] += 1
}
}
}
let mut order: Vec<_> = (0..vars).collect();
order.sort_by_key(|&v| {
let tension = graph[v].len() * (1 + positive[v].min(negative[v]));
if resolving {
tension
} else {
usize::MAX - tension
}
});
order
}
#[derive(Debug)]
struct SiftResult {
order: Vec<usize>,
result: BddSolveResult,
swaps_tested: usize,
swaps_accepted: usize,
passes: usize,
}
fn sift_bdd_order(
vars: usize,
clauses: &[Clause],
elimination_order: &[usize],
initial_order: &[usize],
max_passes: usize,
trials_per_pass: usize,
guided: bool,
seed: u64,
) -> SiftResult {
let mut order = initial_order.to_vec();
let mut result = eliminate_with_bdds_ordered(vars, clauses, elimination_order, &order);
let mut swaps_tested = 0;
let mut swaps_accepted = 0;
let mut passes = 0;
for pass in 0..max_passes {
let mut improved = false;
let mut positions: Vec<usize> = if pass % 2 == 0 {
(0..vars.saturating_sub(1)).collect()
} else {
(0..vars.saturating_sub(1)).rev().collect()
};
if trials_per_pass < positions.len() {
if guided {
let mut interaction_strength: HashMap<(usize, usize), usize> = HashMap::new();
for &((a, b), score) in &result.interaction_candidates {
let pair = if a.0 <= b.0 { (a.0, b.0) } else { (b.0, a.0) };
*interaction_strength.entry(pair).or_default() += score;
}
positions.sort_by_key(|&position| {
let a = order[position];
let b = order[position + 1];
let pair = if a <= b { (a, b) } else { (b, a) };
std::cmp::Reverse(interaction_strength.get(&pair).copied().unwrap_or(0))
});
} else {
Rng(seed ^ (pass as u64 + 1).wrapping_mul(0x9e37_79b9)).shuffle(&mut positions);
}
positions.truncate(trials_per_pass);
}
for position in positions {
order.swap(position, position + 1);
swaps_tested += 1;
let candidate = eliminate_with_bdds_ordered(vars, clauses, elimination_order, &order);
if candidate.allocated_nodes < result.allocated_nodes {
result = candidate;
swaps_accepted += 1;
improved = true;
} else {
order.swap(position, position + 1);
}
}
passes += 1;
if !improved {
break;
}
}
SiftResult {
order,
result,
swaps_tested,
swaps_accepted,
passes,
}
}
/// Count non-terminal nodes in the reduced ordered BDD for a truth table.
/// Table bit `i` corresponds to BDD level `i`.
fn reduced_bdd_nodes(values: &[bool], variables: usize) -> usize {
fn build(
values: &[bool],
level: usize,
variables: usize,
offset: usize,
stride: usize,
unique: &mut HashMap<(usize, usize, usize), usize>,
) -> usize {
if level == variables {
return usize::from(values[offset]);
}
let low = build(values, level + 1, variables, offset, stride * 2, unique);
let high = build(
values,
level + 1,
variables,
offset + stride,
stride * 2,
unique,
);
if low == high {
return low;
}
let next_id = unique.len() + 2;
*unique.entry((level, low, high)).or_insert(next_id)
}
assert_eq!(values.len(), 1usize << variables);
let mut unique = HashMap::new();
build(values, 0, variables, 0, 1, &mut unique);
unique.len()
}
fn eliminate(num_vars: usize, clauses: &[Clause], order: &[usize]) -> SolveResult {
let mut factors: Vec<Factor> = clauses.iter().map(Factor::from_clause).collect();
let mut layers = Vec::new();
let mut peak_boundary = 0;
let mut peak_entries = 1;
let mut peak_bdd_nodes = 0;
for &variable in order {
let mut selected = Vec::new();
let mut retained = Vec::new();
for factor in factors {
if factor.scope.contains(&variable) {
selected.push(factor);
} else {
retained.push(factor);
}
}
let mut set = BTreeSet::new();
for factor in &selected {
set.extend(factor.scope.iter().copied().filter(|&v| v != variable));
}
let boundary: Vec<_> = set.into_iter().collect();
let entries = 1usize << boundary.len();
peak_boundary = peak_boundary.max(boundary.len());
peak_entries = peak_entries.max(entries);
let mut witness = vec![None; entries];
let mut projected = vec![false; entries];
let mut combined_vars = boundary.clone();
combined_vars.push(variable);
combined_vars.sort_unstable();
let variable_pos = combined_vars.binary_search(&variable).unwrap();
for boundary_bits in 0..entries {
for value in [false, true] {
let mut combined_bits = 0;
for (i, &v) in boundary.iter().enumerate() {
let pos = combined_vars.binary_search(&v).unwrap();
combined_bits |= ((boundary_bits >> i) & 1) << pos;
}
combined_bits |= (value as usize) << variable_pos;
if selected
.iter()
.all(|f| f.evaluate_from(&combined_vars, combined_bits))
{
projected[boundary_bits] = true;
witness[boundary_bits] = Some(value);
break;
}
}
}
let bdd_nodes = reduced_bdd_nodes(&projected, boundary.len());
peak_bdd_nodes = peak_bdd_nodes.max(bdd_nodes);
retained.push(Factor {
scope: boundary.clone(),
values: projected,
});
factors = retained;
layers.push(Layer {
variable,
boundary,
witness,
bdd_nodes,
});
}
let satisfiable = factors.iter().all(|f| f.values[0]);
let assignment = satisfiable.then(|| {
let mut assignment = vec![false; num_vars];
for layer in layers.iter().rev() {
let bits = layer
.boundary
.iter()
.enumerate()
.fold(0, |acc, (i, &v)| acc | ((assignment[v] as usize) << i));
assignment[layer.variable] =
layer.witness[bits].expect("reachable boundary assignment must have a witness");
}
assignment
});
SolveResult {
assignment,
peak_boundary,
peak_entries,
peak_bdd_nodes,
layers,
}
}
fn satisfies(clauses: &[Clause], assignment: &[bool]) -> bool {
clauses
.iter()
.all(|c| c.0.iter().any(|&(v, sign)| assignment[v] == sign))
}
fn add_to_varisat(solver: &mut Solver<'_>, clauses: &[Clause]) {
for clause in clauses {
let literals: Vec<_> = clause
.0
.iter()
.map(|&(variable, positive)| Lit::from_var(Var::from_index(variable), positive))
.collect();
solver.add_clause(&literals);
}
}
fn solve_with_varisat(vars: usize, clauses: &[Clause]) -> Option<Vec<bool>> {
let mut solver = Solver::new();
add_to_varisat(&mut solver, clauses);
if !solver.solve().expect("Varisat solve") {
return None;
}
let mut assignment = vec![false; vars];
for literal in solver.model().expect("Varisat model") {
if literal.var().index() < vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
}
fn brute_force(num_vars: usize, clauses: &[Clause]) -> Option<Vec<bool>> {
(0..(1usize << num_vars)).find_map(|bits| {
let assignment: Vec<_> = (0..num_vars).map(|v| ((bits >> v) & 1) == 1).collect();
satisfies(clauses, &assignment).then_some(assignment)
})
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
self.next() as usize % n
}
fn shuffle<T>(&mut self, values: &mut [T]) {
for i in (1..values.len()).rev() {
values.swap(i, self.below(i + 1));
}
}
}
fn random_3sat(vars: usize, clauses: usize, seed: u64) -> Vec<Clause> {
let mut rng = Rng(seed.max(1));
(0..clauses)
.map(|_| {
let mut chosen = BTreeSet::new();
while chosen.len() < 3.min(vars) {
chosen.insert(rng.below(vars));
}
Clause(chosen.into_iter().map(|v| (v, rng.below(2) == 1)).collect())
})
.collect()
}
fn force_planted_satisfaction(
mut clauses: Vec<Clause>,
assignment: &[bool],
seed: u64,
) -> Vec<Clause> {
let mut rng = Rng(seed ^ 0xa076_1d64_78bd_642f);
for clause in &mut clauses {
if !clause
.0
.iter()
.any(|&(variable, sign)| assignment[variable] == sign)
{
let index = rng.below(clause.0.len());
let variable = clause.0[index].0;
clause.0[index].1 = assignment[variable];
}
}
clauses
}
fn planted_random_3sat(vars: usize, clauses: usize, seed: u64) -> Vec<Clause> {
let assignment = planted_assignment(vars, seed);
force_planted_satisfaction(random_3sat(vars, clauses, seed), &assignment, seed)
}
fn planted_banded_3sat(vars: usize, clauses: usize, seed: u64, width: usize) -> Vec<Clause> {
let assignment = planted_assignment(vars, seed);
force_planted_satisfaction(banded_3sat(vars, clauses, seed, width), &assignment, seed)
}
fn banded_3sat(vars: usize, clauses: usize, seed: u64, width: usize) -> Vec<Clause> {
let mut rng = Rng(seed.max(1));
(0..clauses)
.map(|_| {
let start = rng.below(vars);
let end = (start + width.max(3)).min(vars);
let start = end.saturating_sub(width.max(3));
let mut chosen = BTreeSet::new();
while chosen.len() < 3.min(end - start) {
chosen.insert(start + rng.below(end - start));
}
Clause(chosen.into_iter().map(|v| (v, rng.below(2) == 1)).collect())
})
.collect()
}
fn identity_expanded_sat(vars: usize, ratio: usize, seed: u64) -> Vec<Clause> {
let helpers = (vars / 4).max(1).min(vars.saturating_sub(3));
let base_vars = vars - helpers;
let base = random_3sat(base_vars, base_vars * ratio, seed);
let mut expanded = Vec::with_capacity(base.len() + helpers);
for (index, clause) in base.into_iter().enumerate() {
if index < helpers {
let variable = base_vars + index;
let mut positive = clause.0.clone();
positive.push((variable, true));
let mut negative = clause.0;
negative.push((variable, false));
expanded.push(Clause(positive));
expanded.push(Clause(negative));
} else {
expanded.push(clause);
}
}
expanded
}
#[derive(Default)]
struct MathTrickStats {
tautologies: usize,
subsumed: usize,
consensus_pairs: usize,
passes: usize,
}
fn normalize_clause(mut literals: Vec<Literal>) -> Option<Clause> {
literals.sort_unstable();
literals.dedup();
if literals
.iter()
.any(|&(variable, sign)| literals.contains(&(variable, !sign)))
{
None
} else {
Some(Clause(literals))
}
}
fn mathematical_identity_preprocess(clauses: &[Clause]) -> (Vec<Clause>, MathTrickStats) {
let mut stats = MathTrickStats::default();
let mut current = Vec::new();
for clause in clauses {
if let Some(normalized) = normalize_clause(clause.0.clone()) {
current.push(normalized);
} else {
stats.tautologies += 1;
}
}
current.sort_by(|a, b| a.0.cmp(&b.0));
current.dedup_by(|a, b| a.0 == b.0);
loop {
stats.passes += 1;
let mut changed = false;
let mut remove = BTreeSet::new();
for i in 0..current.len() {
let a: BTreeSet<_> = current[i].0.iter().copied().collect();
for (j, clause) in current.iter().enumerate() {
if i == j || remove.contains(&j) {
continue;
}
let b: BTreeSet<_> = clause.0.iter().copied().collect();
if a.is_subset(&b) && (a.len() < b.len() || i < j) {
remove.insert(j);
}
}
}
if !remove.is_empty() {
stats.subsumed += remove.len();
current = current
.into_iter()
.enumerate()
.filter(|(index, _)| !remove.contains(index))
.map(|(_, clause)| clause)
.collect();
changed = true;
}
let mut replacement = None;
'pairs: for i in 0..current.len() {
for j in i + 1..current.len() {
for &(variable, sign) in ¤t[i].0 {
if !current[j].0.contains(&(variable, !sign)) {
continue;
}
let mut left = current[i].0.clone();
let mut right = current[j].0.clone();
left.retain(|literal| literal.0 != variable);
right.retain(|literal| literal.0 != variable);
left.sort_unstable();
right.sort_unstable();
if left == right {
replacement = Some((i, j, Clause(left)));
break 'pairs;
}
}
}
}
if let Some((i, j, replacement_clause)) = replacement {
current = current
.into_iter()
.enumerate()
.filter(|(index, _)| *index != i && *index != j)
.map(|(_, clause)| clause)
.collect();
current.push(replacement_clause);
stats.consensus_pairs += 1;
changed = true;
}
current.sort_by(|a, b| a.0.cmp(&b.0));
current.dedup_by(|a, b| a.0 == b.0);
if !changed {
break;
}
}
(current, stats)
}
fn flower_coordinates(vars: usize) -> Vec<(i32, i32)> {
let mut radius = 0i32;
while (1 + 3 * radius * (radius + 1)) < vars as i32 {
radius += 1;
}
let mut coordinates = Vec::new();
for q in -radius..=radius {
for r in -radius..=radius {
let distance = q.abs().max(r.abs()).max((q + r).abs());
if distance <= radius {
coordinates.push((q, r));
}
}
}
coordinates.sort_by_key(|&(q, r)| {
let distance = q.abs().max(r.abs()).max((q + r).abs());
(distance, q, r)
});
coordinates.truncate(vars);
coordinates
}
fn hex_distance((aq, ar): (i32, i32), (bq, br): (i32, i32)) -> i32 {
let q = aq - bq;
let r = ar - br;
q.abs().max(r.abs()).max((q + r).abs())
}
fn local_geometry_3sat(
coordinates: &[(i32, i32, i32)],
clauses: usize,
seed: u64,
planted: Option<&[bool]>,
) -> Vec<Clause> {
let mut rng = Rng(seed.max(1));
(0..clauses)
.map(|_| {
let center = rng.below(coordinates.len());
let (cq, cr, cz) = coordinates[center];
let mut pool: Vec<_> = coordinates
.iter()
.enumerate()
.filter_map(|(index, &(q, r, z))| {
let planar = hex_distance((cq, cr), (q, r));
((z == cz && planar <= 1) || (q == cq && r == cr && (z - cz).abs() == 1))
.then_some(index)
})
.collect();
if pool.len() < 3 {
pool = (0..coordinates.len()).collect();
}
rng.shuffle(&mut pool);
let mut clause = Clause(
pool.into_iter()
.take(3.min(coordinates.len()))
.map(|variable| (variable, rng.below(2) == 1))
.collect(),
);
if let Some(assignment) = planted {
if !clause
.0
.iter()
.any(|&(variable, sign)| assignment[variable] == sign)
{
clause.0[0].1 = assignment[clause.0[0].0];
}
}
clause
})
.collect()
}
fn flower_3sat(vars: usize, clauses: usize, seed: u64) -> Vec<Clause> {
let coordinates: Vec<_> = flower_coordinates(vars)
.into_iter()
.map(|(q, r)| (q, r, 0))
.collect();
local_geometry_3sat(&coordinates, clauses, seed, None)
}
fn planted_assignment(vars: usize, seed: u64) -> Vec<bool> {
let mut rng = Rng(seed.max(1) ^ 0xd1b5_4a32_d192_ed03);
(0..vars).map(|_| rng.below(2) == 1).collect()
}
fn planted_flower_3sat(vars: usize, clauses: usize, seed: u64) -> Vec<Clause> {
let coordinates: Vec<_> = flower_coordinates(vars)
.into_iter()
.map(|(q, r)| (q, r, 0))
.collect();
let assignment = planted_assignment(vars, seed);
local_geometry_3sat(&coordinates, clauses, seed, Some(&assignment))
}
fn symmetric_flower_3sat(vars: usize, target_clauses: usize) -> Vec<Clause> {
let coordinates = flower_coordinates(vars);
let index: HashMap<_, _> = coordinates
.iter()
.copied()
.enumerate()
.map(|(variable, coordinate)| (coordinate, variable))
.collect();
let directions = [(1, 0), (0, 1), (-1, 1), (-1, 0), (0, -1), (1, -1)];
let mut unique: BTreeSet<Vec<Literal>> = BTreeSet::new();
for rule in 0..4 {
for (center, &(q, r)) in coordinates.iter().enumerate() {
for direction in 0..6 {
let (aq, ar) = directions[direction];
let second_direction = if rule < 2 {
(direction + 1) % 6
} else {
(direction + 3) % 6
};
let (bq, br) = directions[second_direction];
let (Some(&a), Some(&b)) =
(index.get(&(q + aq, r + ar)), index.get(&(q + bq, r + br)))
else {
continue;
};
let mut clause = match rule {
0 => vec![(center, true), (a, false), (b, true)],
1 => vec![(center, false), (a, true), (b, true)],
2 => vec![(center, true), (a, true), (b, false)],
_ => vec![(center, false), (a, true), (b, true)],
};
clause.sort_unstable();
unique.insert(clause);
}
}
if unique.len() >= target_clauses {
break;
}
}
unique.into_iter().map(Clause).collect()
}
fn stacked_flower_3sat(vars: usize, clauses: usize, seed: u64) -> Vec<Clause> {
let layers = 3.min(vars);
let per_layer = vars.div_ceil(layers);
let base = flower_coordinates(per_layer);
let mut coordinates = Vec::new();
for z in 0..layers {
for &(q, r) in &base {
if coordinates.len() == vars {
break;
}
coordinates.push((q, r, z as i32));
}
}
local_geometry_3sat(&coordinates, clauses, seed, None)
}
fn planted_stacked_flower_3sat(vars: usize, clauses: usize, seed: u64) -> Vec<Clause> {
let layers = 3.min(vars);
let per_layer = vars.div_ceil(layers);
let base = flower_coordinates(per_layer);
let mut coordinates = Vec::new();
for z in 0..layers {
for &(q, r) in &base {
if coordinates.len() == vars {
break;
}
coordinates.push((q, r, z as i32));
}
}
let assignment = planted_assignment(vars, seed);
local_geometry_3sat(&coordinates, clauses, seed, Some(&assignment))
}
fn flower_outside_in_order(vars: usize) -> Vec<usize> {
let coordinates = flower_coordinates(vars);
let mut order: Vec<_> = (0..vars).collect();
order.sort_by_key(|&v| {
let (q, r) = coordinates[v];
let distance = q.abs().max(r.abs()).max((q + r).abs());
(std::cmp::Reverse(distance), q, r)
});
order
}
fn primal_graph(vars: usize, clauses: &[Clause]) -> Vec<BTreeSet<usize>> {
let mut graph = vec![BTreeSet::new(); vars];
for clause in clauses {
for &(a, _) in &clause.0 {
for &(b, _) in &clause.0 {
if a != b {
graph[a].insert(b);
}
}
}
}
graph
}
fn exact_treewidth(vars: usize, clauses: &[Clause]) -> usize {
assert!(
vars <= 20,
"exact treewidth oracle is limited to 20 vertices"
);
let graph = primal_graph(vars, clauses);
let states = 1usize << vars;
let mut dp = vec![usize::MAX; states];
dp[0] = 0;
for eliminated in 0..states {
if dp[eliminated] == usize::MAX {
continue;
}
for variable in 0..vars {
if eliminated & (1usize << variable) != 0 {
continue;
}
let mut seen = vec![false; vars];
let mut stack = vec![variable];
seen[variable] = true;
let mut boundary = BTreeSet::new();
while let Some(current) = stack.pop() {
for &next in &graph[current] {
if next == variable || seen[next] {
continue;
}
seen[next] = true;
if eliminated & (1usize << next) != 0 {
stack.push(next);
} else {
boundary.insert(next);
}
}
}
let next = eliminated | (1usize << variable);
let width = dp[eliminated].max(boundary.len());
dp[next] = dp[next].min(width);
}
}
dp[states - 1]
}
fn exact_weighted_treewidth(weights: &[usize], graph: &[BTreeSet<usize>]) -> usize {
let vars = weights.len();
assert!(
vars <= 20,
"exact weighted treewidth is limited to 20 vertices"
);
if vars == 0 {
return 0;
}
let states = 1usize << vars;
let mut dp = vec![usize::MAX; states];
dp[0] = 0;
for eliminated in 0..states {
if dp[eliminated] == usize::MAX {
continue;
}
for variable in 0..vars {
if eliminated & (1usize << variable) != 0 {
continue;
}
let mut seen = vec![false; vars];
let mut stack = vec![variable];
seen[variable] = true;
let mut boundary = BTreeSet::new();
while let Some(current) = stack.pop() {
for &next in &graph[current] {
if next == variable || seen[next] {
continue;
}
seen[next] = true;
if eliminated & (1usize << next) != 0 {
stack.push(next);
} else {
boundary.insert(next);
}
}
}
let bag_bits =
weights[variable] + boundary.iter().map(|&next| weights[next]).sum::<usize>();
let width = dp[eliminated].max(bag_bits.saturating_sub(1));
let next = eliminated | (1usize << variable);
dp[next] = dp[next].min(width);
}
}
dp[states - 1]
}
fn quotient_graph(vars: usize, clauses: &[Clause], groups: &[Vec<usize>]) -> Vec<BTreeSet<usize>> {
let mut owner = vec![usize::MAX; vars];
for (group, members) in groups.iter().enumerate() {
for &variable in members {
owner[variable] = group;
}
}
let original = primal_graph(vars, clauses);
let mut quotient = vec![BTreeSet::new(); groups.len()];
for variable in 0..vars {
for &next in &original[variable] {
let left = owner[variable];
let right = owner[next];
if left != right {
quotient[left].insert(right);
quotient[right].insert(left);
}
}
}
quotient
}
struct ShakenFormula {
vars: usize,
clauses: Vec<Clause>,
core_to_original: Vec<usize>,
fixed: Vec<Option<bool>>,
removed: usize,
probes: usize,
inverse_forced: usize,
contradiction: bool,
}
struct SeededBranch {
vars: usize,
clauses: Vec<Clause>,
core_to_original: Vec<usize>,
boundary: Vec<usize>,
interior: Vec<usize>,
witnesses: Vec<Option<Vec<bool>>>,
local_clauses: usize,
summary_clauses: usize,
compilation_trials: usize,
}
fn detachable_branch_candidate(
vars: usize,
clauses: &[Clause],
max_interior: usize,
) -> Option<(Vec<usize>, Vec<usize>)> {
let graph = primal_graph(vars, clauses);
let mut best: Option<(Vec<usize>, Vec<usize>)> = None;
let mut boundary_masks = vec![0usize];
for first in 0..vars {
boundary_masks.push(1usize << first);
for second in first + 1..vars {
boundary_masks.push((1usize << first) | (1usize << second));
}
}
for boundary_mask in boundary_masks {
let mut seen = vec![false; vars];
for start in 0..vars {
if boundary_mask & (1usize << start) != 0 || seen[start] {
continue;
}
let mut stack = vec![start];
seen[start] = true;
let mut component = Vec::new();
while let Some(variable) = stack.pop() {
component.push(variable);
for &next in &graph[variable] {
if boundary_mask & (1usize << next) == 0 && !seen[next] {
seen[next] = true;
stack.push(next);
}
}
}
if component.is_empty() || component.len() > max_interior {
continue;
}
let component_set: BTreeSet<_> = component.iter().copied().collect();
let actual_boundary: BTreeSet<_> = component
.iter()
.flat_map(|&variable| graph[variable].iter().copied())
.filter(|variable| !component_set.contains(variable))
.collect();
if actual_boundary.is_empty() || actual_boundary.len() > 2 {
continue;
}
let candidate = (component, actual_boundary.into_iter().collect::<Vec<_>>());
if best.as_ref().is_none_or(|current| {
candidate.0.len() > current.0.len()
|| (candidate.0.len() == current.0.len() && candidate.1.len() < current.1.len())
}) {
best = Some(candidate);
}
}
}
best
}
fn fast_detachable_branch_candidates(
vars: usize,
clauses: &[Clause],
max_interior: usize,
) -> Vec<(Vec<usize>, Vec<usize>)> {
let graph = compact_primal_graph(vars, clauses);
global_small_separator_candidates(&graph, max_interior)
}
fn compact_primal_graph(vars: usize, clauses: &[Clause]) -> Vec<Vec<usize>> {
let mut graph = vec![Vec::new(); vars];
for clause in clauses {
for left in 0..clause.0.len() {
let a = clause.0[left].0;
for right in left + 1..clause.0.len() {
let b = clause.0[right].0;
if a != b {
graph[a].push(b);
graph[b].push(a);
}
}
}
}
for neighbours in &mut graph {
neighbours.sort_unstable();
neighbours.dedup();
}
graph
}
/// Finds capped DFS subtrees whose exact external neighbourhood contains one or
/// two variables. One global traversal replaces pair enumeration and repeated
/// bounded expansion from every variable.
fn global_small_separator_candidates(
graph: &[Vec<usize>],
max_interior: usize,
) -> Vec<(Vec<usize>, Vec<usize>)> {
let vars = graph.len();
let unseen = usize::MAX;
let mut discovery = vec![unseen; vars];
let mut parent = vec![unseen; vars];
let mut subtree_size = vec![0usize; vars];
let mut preorder = Vec::with_capacity(vars);
let mut candidates = Vec::new();
for root in 0..vars {
if discovery[root] != unseen {
continue;
}
discovery[root] = preorder.len();
subtree_size[root] = 1;
preorder.push(root);
let mut stack = vec![(root, 0usize)];
while let Some((variable, next_edge)) = stack.last_mut() {
if *next_edge < graph[*variable].len() {
let next = graph[*variable][*next_edge];
*next_edge += 1;
if discovery[next] == unseen {
parent[next] = *variable;
discovery[next] = preorder.len();
subtree_size[next] = 1;
preorder.push(next);
stack.push((next, 0));
}
continue;
}
let (finished, _) = stack.pop().expect("non-empty DFS stack");
let p = parent[finished];
if p != unseen {
let size = subtree_size[finished];
subtree_size[p] += size;
}
}
}
// Every DFS subtree is a contiguous preorder interval. Scan only subtrees
// within the cap and stop as soon as a third external neighbour appears.
// This finds both articulation branches and exact two-vertex separators
// without enumerating O(n^2) vertex pairs.
for root in 0..vars {
let size = subtree_size[root];
if parent[root] == unseen || size == 0 || size > max_interior {
continue;
}
let start = discovery[root];
let end = start + size;
let mut boundary = Vec::new();
'scan: for &variable in &preorder[start..end] {
for &next in &graph[variable] {
let position = discovery[next];
if (position < start || position >= end) && !boundary.contains(&next) {
boundary.push(next);
if boundary.len() > 2 {
break 'scan;
}
}
}
}
if !boundary.is_empty() && boundary.len() <= 2 {
boundary.sort_unstable();
candidates.push((preorder[start..end].to_vec(), boundary));
}
}
candidates.sort_by(|left, right| {
right
.0
.len()
.cmp(&left.0.len())
.then_with(|| left.0.cmp(&right.0))
});
let mut used_interior = vec![false; vars];
let mut protected_boundary = vec![false; vars];
candidates
.into_iter()
.filter(|(interior, boundary)| {
if interior
.iter()
.any(|&v| used_interior[v] || protected_boundary[v])
|| boundary.iter().any(|&v| used_interior[v])
{
return false;
}
for &v in interior {
used_interior[v] = true;
}
for &v in boundary {
protected_boundary[v] = true;
}
true
})
.collect()
}
#[allow(dead_code)] // Retained as the frozen pre-rewrite discovery baseline.
fn fast_detachable_branch_candidates_in(
graph: &[BTreeSet<usize>],
max_interior: usize,
) -> Vec<(Vec<usize>, Vec<usize>)> {
let vars = graph.len();
let mut candidates = Vec::new();
let mut consider = |interior: Vec<usize>| {
if interior.is_empty() {
return;
}
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let boundary: BTreeSet<_> = interior
.iter()
.flat_map(|&variable| graph[variable].iter().copied())
.filter(|variable| !interior_set.contains(variable))
.collect();
if !boundary.is_empty() && boundary.len() <= 2 {
candidates.push((interior, boundary.into_iter().collect::<Vec<_>>()));
}
};
for start in 0..vars {
for length in 1..=max_interior.min(vars - start) {
consider((start..start + length).collect());
}
let mut seen = vec![false; vars];
let mut queue = VecDeque::from([start]);
seen[start] = true;
let mut prefix = Vec::new();
while let Some(variable) = queue.pop_front() {
prefix.push(variable);
consider(prefix.clone());
if prefix.len() == max_interior {
break;
}
for &next in &graph[variable] {
if !seen[next] {
seen[next] = true;
queue.push_back(next);
}
}
}
}
candidates.sort_by(|left, right| {
right
.0
.len()
.cmp(&left.0.len())
.then_with(|| left.1.len().cmp(&right.1.len()))
.then_with(|| left.0.cmp(&right.0))
});
candidates.dedup();
let mut used_interior = BTreeSet::new();
let mut protected_boundary = BTreeSet::new();
let mut selected = Vec::new();
for candidate in candidates {
if candidate.0.iter().any(|variable| {
used_interior.contains(variable) || protected_boundary.contains(variable)
}) || candidate
.1
.iter()
.any(|variable| used_interior.contains(variable))
{
continue;
}
used_interior.extend(candidate.0.iter().copied());
protected_boundary.extend(candidate.1.iter().copied());
selected.push(candidate);
}
selected
}
fn seed_detachable_branch(vars: usize, clauses: &[Clause], max_interior: usize) -> SeededBranch {
let Some((mut interior, boundary)) = detachable_branch_candidate(vars, clauses, max_interior)
else {
return SeededBranch {
vars,
clauses: clauses.to_vec(),
core_to_original: (0..vars).collect(),
boundary: Vec::new(),
interior: Vec::new(),
witnesses: Vec::new(),
local_clauses: 0,
summary_clauses: 0,
compilation_trials: 0,
};
};
interior.sort_unstable();
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let local: Vec<_> = clauses
.iter()
.filter(|clause| clause.0.iter().any(|(v, _)| interior_set.contains(v)))
.cloned()
.collect();
let mut witnesses = Vec::new();
let mut summary = Vec::new();
let mut compilation_trials = 0;
for boundary_bits in 0..(1usize << boundary.len()) {
let mut witness = None;
for interior_bits in 0..(1usize << interior.len()) {
compilation_trials += 1;
let mut assignment = vec![false; vars];
for (index, &variable) in boundary.iter().enumerate() {
assignment[variable] = boundary_bits & (1usize << index) != 0;
}
let values: Vec<_> = interior
.iter()
.enumerate()
.map(|(index, &variable)| {
let value = interior_bits & (1usize << index) != 0;
assignment[variable] = value;
value
})
.collect();
if satisfies(&local, &assignment) {
witness = Some(values);
break;
}
}
if witness.is_none() {
summary.push(Clause(
boundary
.iter()
.enumerate()
.map(|(index, &variable)| {
let value = boundary_bits & (1usize << index) != 0;
(variable, !value)
})
.collect(),
));
}
witnesses.push(witness);
}
let mut transformed: Vec<_> = clauses
.iter()
.filter(|clause| !clause.0.iter().any(|(v, _)| interior_set.contains(v)))
.cloned()
.collect();
transformed.extend(summary.iter().cloned());
let core_to_original: Vec<_> = (0..vars)
.filter(|variable| !interior_set.contains(variable))
.collect();
let mut original_to_core = vec![usize::MAX; vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
for clause in &mut transformed {
for (variable, _) in &mut clause.0 {
*variable = original_to_core[*variable];
}
}
SeededBranch {
vars: core_to_original.len(),
clauses: transformed,
core_to_original,
boundary,
interior,
witnesses,
local_clauses: local.len(),
summary_clauses: summary.len(),
compilation_trials,
}
}
struct BddSeededBranch {
vars: usize,
clauses: Vec<Clause>,
core_to_original: Vec<usize>,
boundary: Vec<usize>,
interior: Vec<usize>,
manager: BddManager,
root: usize,
order: Vec<usize>,
local_clauses: usize,
summary_clauses: usize,
summary: Vec<Clause>,
allocated_nodes: usize,
live_nodes: usize,
cache_root: usize,
}
fn reachable_bdd_nodes(manager: &BddManager, root: usize) -> usize {
let mut seen = BTreeSet::new();
let mut stack = vec![root];
while let Some(current) = stack.pop() {
if current < 2 || !seen.insert(current) {
continue;
}
let node = manager.node(current);
stack.push(node.low);
stack.push(node.high);
}
seen.len()
}
fn compact_bdd(manager: &BddManager, root: usize) -> (BddManager, usize) {
fn copy_node(
source: &BddManager,
target: &mut BddManager,
current: usize,
memo: &mut HashMap<usize, usize>,
) -> usize {
if current < 2 {
return current;
}
if let Some(&mapped) = memo.get(¤t) {
return mapped;
}
let node = source.node(current);
let low = copy_node(source, target, node.low, memo);
let high = copy_node(source, target, node.high, memo);
let mapped = target.make(node.variable, low, high);
target.node_hits[mapped - 2] += source.node_hits[current - 2];
memo.insert(current, mapped);
mapped
}
let mut compacted = BddManager::default();
let compacted_root = copy_node(manager, &mut compacted, root, &mut HashMap::new());
(compacted, compacted_root)
}
fn compact_bdd_roots(manager: &BddManager, roots: &[usize]) -> (BddManager, Vec<usize>) {
fn copy_node(
source: &BddManager,
target: &mut BddManager,
current: usize,
memo: &mut HashMap<usize, usize>,
) -> usize {
if current < 2 {
return current;
}
if let Some(&mapped) = memo.get(¤t) {
return mapped;
}
let node = source.node(current);
let low = copy_node(source, target, node.low, memo);
let high = copy_node(source, target, node.high, memo);
let mapped = target.make(node.variable, low, high);
target.node_hits[mapped - 2] += source.node_hits[current - 2];
memo.insert(current, mapped);
mapped
}
let mut compacted = BddManager::default();
let mut memo = HashMap::new();
let mapped = roots
.iter()
.map(|&root| copy_node(manager, &mut compacted, root, &mut memo))
.collect();
(compacted, mapped)
}
fn seed_detachable_branch_bdd(
vars: usize,
clauses: &[Clause],
max_interior: usize,
order_strategy: &str,
) -> BddSeededBranch {
let mut manager = BddManager::default();
seed_detachable_branch_bdd_in(vars, clauses, max_interior, order_strategy, &mut manager)
}
fn seed_detachable_branch_bdd_in(
vars: usize,
clauses: &[Clause],
max_interior: usize,
order_strategy: &str,
manager: &mut BddManager,
) -> BddSeededBranch {
let Some((mut interior, boundary)) = detachable_branch_candidate(vars, clauses, max_interior)
else {
return BddSeededBranch {
vars,
clauses: clauses.to_vec(),
core_to_original: (0..vars).collect(),
boundary: Vec::new(),
interior: Vec::new(),
manager: BddManager::default(),
root: 1,
order: Vec::new(),
local_clauses: 0,
summary_clauses: 0,
summary: Vec::new(),
allocated_nodes: 0,
live_nodes: 0,
cache_root: 1,
};
};
seed_bdd_candidate_in(
vars,
clauses,
&mut interior,
boundary,
order_strategy,
manager,
)
}
fn seed_bdd_candidate_in(
vars: usize,
clauses: &[Clause],
interior: &mut Vec<usize>,
boundary: Vec<usize>,
order_strategy: &str,
manager: &mut BddManager,
) -> BddSeededBranch {
interior.sort_unstable();
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let local: Vec<_> = clauses
.iter()
.filter(|clause| clause.0.iter().any(|(v, _)| interior_set.contains(v)))
.cloned()
.collect();
let relevant: BTreeSet<_> = boundary.iter().chain(interior.iter()).copied().collect();
let mut order = match order_strategy {
"min-fill" => restricted_order(&local, &relevant, true),
"min-degree" => restricted_order(&local, &relevant, false),
"boundary-min-fill" => {
let mut result = boundary.clone();
result.extend(
restricted_order(&local, &relevant, true)
.into_iter()
.filter(|variable| interior.contains(variable)),
);
result
}
"boundary-min-degree" => {
let mut result = boundary.clone();
result.extend(
restricted_order(&local, &relevant, false)
.into_iter()
.filter(|variable| interior.contains(variable)),
);
result
}
_ => {
let mut natural = boundary.clone();
natural.extend(interior.iter().copied());
natural
}
};
order.retain(|variable| relevant.contains(variable));
let order_rank: HashMap<_, _> = order
.iter()
.copied()
.enumerate()
.map(|(rank, variable)| (variable, rank))
.collect();
let allocated_before = manager.nodes.len();
let root = compile_formula_bdd_into(manager, vars, &local, &order);
let mut relation = root;
for variable in interior.iter() {
relation = manager.exists(relation, order_rank[variable], &mut HashMap::new());
}
let mut summary = Vec::new();
for boundary_bits in 0..(1usize << boundary.len()) {
let mut rank_assignment = vec![false; order.len()];
for (index, variable) in boundary.iter().enumerate() {
rank_assignment[order_rank[variable]] = boundary_bits & (1usize << index) != 0;
}
if !manager.evaluate(relation, &rank_assignment) {
summary.push(Clause(
boundary
.iter()
.enumerate()
.map(|(index, &variable)| {
let value = boundary_bits & (1usize << index) != 0;
(variable, !value)
})
.collect(),
));
}
}
let mut transformed: Vec<_> = clauses
.iter()
.filter(|clause| !clause.0.iter().any(|(v, _)| interior_set.contains(v)))
.cloned()
.collect();
transformed.extend(summary.iter().cloned());
let core_to_original: Vec<_> = (0..vars)
.filter(|variable| !interior_set.contains(variable))
.collect();
let mut original_to_core = vec![usize::MAX; vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
for clause in &mut transformed {
for (variable, _) in &mut clause.0 {
*variable = original_to_core[*variable];
}
}
let allocated_nodes = manager.nodes.len() - allocated_before;
let live_nodes = reachable_bdd_nodes(manager, root);
let cache_root = root;
let (manager, root) = compact_bdd(manager, root);
BddSeededBranch {
vars: core_to_original.len(),
clauses: transformed,
core_to_original,
boundary,
interior: interior.clone(),
manager,
root,
order,
local_clauses: local.len(),
summary_clauses: summary.len(),
summary,
allocated_nodes,
live_nodes,
cache_root,
}
}
fn restricted_order(
clauses: &[Clause],
relevant: &BTreeSet<usize>,
use_min_fill: bool,
) -> Vec<usize> {
let originals: Vec<_> = relevant.iter().copied().collect();
let to_local: HashMap<_, _> = originals
.iter()
.copied()
.enumerate()
.map(|(local, original)| (original, local))
.collect();
let compact: Vec<_> = clauses
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.filter_map(|&(variable, positive)| {
to_local.get(&variable).map(|&local| (local, positive))
})
.collect(),
)
})
.collect();
let compact_order = if use_min_fill {
min_fill_order(originals.len(), &compact)
} else {
min_degree_order(originals.len(), &compact)
};
compact_order
.into_iter()
.map(|local| originals[local])
.collect()
}
fn clause_incidence(vars: usize, clauses: &[Clause]) -> Vec<Vec<usize>> {
let mut incidence = vec![Vec::new(); vars];
for (clause_index, clause) in clauses.iter().enumerate() {
for &(variable, _) in &clause.0 {
incidence[variable].push(clause_index);
}
}
incidence
}
fn indexed_local_clauses(
interior: &[usize],
incidence: &[Vec<usize>],
clauses: &[Clause],
) -> Vec<Clause> {
let mut indices: Vec<_> = interior
.iter()
.flat_map(|&variable| incidence[variable].iter().copied())
.collect();
indices.sort_unstable();
indices.dedup();
indices
.into_iter()
.map(|index| clauses[index].clone())
.collect()
}
fn seed_bdd_from_local(
vars: usize,
local: &[Clause],
mut interior: Vec<usize>,
boundary: Vec<usize>,
order_strategy: &str,
manager: &mut BddManager,
) -> BddSeededBranch {
interior.sort_unstable();
let relevant: BTreeSet<_> = boundary.iter().chain(interior.iter()).copied().collect();
let mut order = match order_strategy {
"min-fill" => restricted_order(local, &relevant, true),
"min-degree" => restricted_order(local, &relevant, false),
_ => {
let mut natural = boundary.clone();
natural.extend(interior.iter().copied());
natural
}
};
order.retain(|variable| relevant.contains(variable));
let order_rank: HashMap<_, _> = order
.iter()
.copied()
.enumerate()
.map(|(rank, variable)| (variable, rank))
.collect();
let allocated_before = manager.nodes.len();
let root = compile_formula_bdd_into(manager, vars, local, &order);
let mut relation = root;
for &variable in &interior {
relation = manager.exists(relation, order_rank[&variable], &mut HashMap::new());
}
let mut summary = Vec::new();
for boundary_bits in 0..(1usize << boundary.len()) {
let mut rank_assignment = vec![false; order.len()];
for (index, variable) in boundary.iter().enumerate() {
rank_assignment[order_rank[variable]] = boundary_bits & (1usize << index) != 0;
}
if !manager.evaluate(relation, &rank_assignment) {
summary.push(Clause(
boundary
.iter()
.enumerate()
.map(|(index, &variable)| (variable, boundary_bits & (1usize << index) == 0))
.collect(),
));
}
}
let allocated_nodes = manager.nodes.len() - allocated_before;
let live_nodes = reachable_bdd_nodes(manager, root);
let cache_root = root;
let (manager, root) = compact_bdd(manager, root);
BddSeededBranch {
vars: vars - interior.len(),
clauses: Vec::new(),
core_to_original: Vec::new(),
boundary,
interior,
manager,
root,
order,
local_clauses: local.len(),
summary_clauses: summary.len(),
summary,
allocated_nodes,
live_nodes,
cache_root,
}
}
struct BoundedSeedAttempt {
seed: Option<BddSeededBranch>,
nodes: usize,
node_exceeded: bool,
time_exceeded: bool,
}
fn try_seed_bdd_candidate(
vars: usize,
clauses: &[Clause],
mut interior: Vec<usize>,
boundary: Vec<usize>,
order_strategy: &str,
node_limit: usize,
time_limit: std::time::Duration,
) -> BoundedSeedAttempt {
let start = Instant::now();
let mut manager = BddManager {
node_limit: Some(node_limit),
deadline: Some(start + time_limit),
..BddManager::default()
};
let seed = seed_bdd_candidate_in(
vars,
clauses,
&mut interior,
boundary,
order_strategy,
&mut manager,
);
let elapsed = start.elapsed();
let node_exceeded = manager.nodes.len() >= node_limit && manager.budget_exceeded;
let time_exceeded = (manager.budget_exceeded && !node_exceeded) || elapsed > time_limit;
BoundedSeedAttempt {
seed: (!manager.budget_exceeded && !time_exceeded).then_some(seed),
nodes: manager.nodes.len(),
node_exceeded,
time_exceeded,
}
}
fn try_indexed_seed_bdd_candidate(
vars: usize,
clauses: &[Clause],
incidence: &[Vec<usize>],
interior: Vec<usize>,
boundary: Vec<usize>,
node_limit: usize,
time_limit: std::time::Duration,
) -> BoundedSeedAttempt {
let start = Instant::now();
let local = indexed_local_clauses(&interior, incidence, clauses);
let mut manager = BddManager {
node_limit: Some(node_limit),
deadline: Some(start + time_limit),
..BddManager::default()
};
let seed = seed_bdd_from_local(vars, &local, interior, boundary, "min-fill", &mut manager);
let elapsed = start.elapsed();
let node_exceeded = manager.nodes.len() >= node_limit && manager.budget_exceeded;
let time_exceeded = (manager.budget_exceeded && !node_exceeded) || elapsed > time_limit;
BoundedSeedAttempt {
seed: (!manager.budget_exceeded && !time_exceeded).then_some(seed),
nodes: manager.nodes.len(),
node_exceeded,
time_exceeded,
}
}
fn regrow_bdd_seed(seed: &BddSeededBranch, original_assignment: &[bool]) -> Option<Vec<bool>> {
if seed.interior.is_empty() {
return Some(Vec::new());
}
fn compatible(
seed: &BddSeededBranch,
root: usize,
original_assignment: &[bool],
memo: &mut HashMap<usize, bool>,
) -> bool {
if root < 2 {
return root == 1;
}
if let Some(&result) = memo.get(&root) {
return result;
}
let node = seed.manager.node(root);
let original_variable = seed.order[node.variable];
let result = if seed.boundary.contains(&original_variable) {
compatible(
seed,
if original_assignment[original_variable] {
node.high
} else {
node.low
},
original_assignment,
memo,
)
} else {
compatible(seed, node.low, original_assignment, memo)
|| compatible(seed, node.high, original_assignment, memo)
};
memo.insert(root, result);
result
}
if !compatible(seed, seed.root, original_assignment, &mut HashMap::new()) {
return None;
}
let mut rank_assignment = vec![false; seed.order.len()];
let mut root = seed.root;
while root >= 2 {
let node = seed.manager.node(root);
let original_variable = seed.order[node.variable];
let value = if seed.boundary.contains(&original_variable) {
original_assignment[original_variable]
} else {
!compatible(seed, node.low, original_assignment, &mut HashMap::new())
};
rank_assignment[node.variable] = value;
root = if value { node.high } else { node.low };
}
if root == 0 {
return None;
}
let rank: HashMap<_, _> = seed
.order
.iter()
.copied()
.enumerate()
.map(|(rank, variable)| (variable, rank))
.collect();
Some(
seed.interior
.iter()
.map(|variable| rank_assignment[rank[variable]])
.collect(),
)
}
fn regrow_seed_chain(seeds: &[BddSeededBranch], final_assignment: &[bool]) -> Option<Vec<bool>> {
let mut assignment = final_assignment.to_vec();
for seed in seeds.iter().rev() {
let previous_vars = seed.core_to_original.len() + seed.interior.len();
let mut previous = vec![false; previous_vars];
for (core, &original) in seed.core_to_original.iter().enumerate() {
previous[original] = assignment[core];
}
let values = regrow_bdd_seed(seed, &previous)?;
for (index, &variable) in seed.interior.iter().enumerate() {
previous[variable] = values[index];
}
assignment = previous;
}
Some(assignment)
}
fn deployment_features(vars: usize, clauses: &[Clause], branch_cap: usize) -> Vec<f64> {
let graph = primal_graph(vars, clauses);
let degrees: Vec<_> = graph
.iter()
.map(|neighbors| neighbors.len() as f64)
.collect();
let mean_degree = degrees.iter().sum::<f64>() / vars.max(1) as f64;
let degree_variance = degrees
.iter()
.map(|degree| (degree - mean_degree).powi(2))
.sum::<f64>()
/ vars.max(1) as f64;
let low_degree =
degrees.iter().filter(|&°ree| degree <= 2.0).count() as f64 / vars.max(1) as f64;
let locality = clauses
.iter()
.map(|clause| {
let minimum = clause.0.iter().map(|literal| literal.0).min().unwrap_or(0);
let maximum = clause.0.iter().map(|literal| literal.0).max().unwrap_or(0);
(maximum - minimum) as f64 / vars.max(1) as f64
})
.sum::<f64>()
/ clauses.len().max(1) as f64;
let candidate = detachable_branch_candidate(vars, clauses, branch_cap);
vec![
clauses.len() as f64 / vars.max(1) as f64,
mean_degree / vars.max(1) as f64,
degree_variance / (vars * vars).max(1) as f64,
low_degree,
locality,
leaf_richness(vars, clauses) as f64 / vars.max(1) as f64,
candidate
.as_ref()
.map_or(0.0, |item| item.0.len() as f64 / vars.max(1) as f64),
candidate
.as_ref()
.map_or(0.0, |item| item.1.len() as f64 / 2.0),
]
}
struct DeploymentMeasurement {
incremental_ns: u128,
seeded_ns: u128,
incremental_setup_ns: u128,
seeded_setup_ns: u128,
incremental_query_ns: u128,
seeded_query_ns: u128,
reconstruction_ns: u128,
reconstruction_samples: usize,
seeds: usize,
removed: usize,
live_nodes: usize,
valid: bool,
}
fn measure_assumption_service(
vars: usize,
clauses: &[Clause],
branch_cap: usize,
max_seeds: usize,
queries: usize,
) -> DeploymentMeasurement {
measure_assumption_service_warm(vars, clauses, branch_cap, max_seeds, 0, queries)
}
fn measure_assumption_service_warm(
vars: usize,
clauses: &[Clause],
branch_cap: usize,
max_seeds: usize,
warmup: usize,
queries: usize,
) -> DeploymentMeasurement {
let incremental_start = Instant::now();
let mut incremental = Solver::new();
add_to_varisat(&mut incremental, clauses);
let incremental_setup_ns = incremental_start.elapsed().as_nanos();
let seed_start = Instant::now();
let mut current_vars = vars;
let mut current_clauses = clauses.to_vec();
let mut current_to_original: Vec<_> = (0..vars).collect();
let mut seeds = Vec::new();
for _ in 0..max_seeds {
let compiled =
seed_detachable_branch_bdd(current_vars, ¤t_clauses, branch_cap, "natural");
if compiled.interior.is_empty() {
break;
}
current_to_original = compiled
.core_to_original
.iter()
.map(|&previous| current_to_original[previous])
.collect();
current_vars = compiled.vars;
current_clauses = compiled.clauses.clone();
seeds.push(compiled);
}
let mut seed_solver = Solver::new();
add_to_varisat(&mut seed_solver, ¤t_clauses);
let seeded_setup_ns = seed_start.elapsed().as_nanos();
let mut incremental_query_ns = 0u128;
let mut seeded_query_ns = 0u128;
let mut reconstruction_ns = 0u128;
let mut reconstruction_samples = 0usize;
let mut valid = current_vars > 0;
if current_vars > 0 {
for query in 0..warmup + queries {
let core_variable = query % current_vars;
let original_variable = current_to_original[core_variable];
let value = (query / current_vars + query) % 2 == 0;
incremental.assume(&[Lit::from_var(Var::from_index(original_variable), value)]);
let start = Instant::now();
let incremental_sat = incremental.solve().expect("deployment incremental solve");
let elapsed = start.elapsed().as_nanos();
if query >= warmup {
incremental_query_ns += elapsed;
}
seed_solver.assume(&[Lit::from_var(Var::from_index(core_variable), value)]);
let start = Instant::now();
let seed_sat = seed_solver.solve().expect("deployment seed solve");
let elapsed = start.elapsed().as_nanos();
if query >= warmup {
seeded_query_ns += elapsed;
}
valid &= incremental_sat == seed_sat;
if seed_sat && query >= warmup && (query < warmup + 4 || query + 1 == warmup + queries)
{
let reconstruction_start = Instant::now();
let mut core_assignment = vec![false; current_vars];
for literal in seed_solver.model().expect("deployment seed model") {
if literal.var().index() < current_vars {
core_assignment[literal.var().index()] = literal.is_positive();
}
}
valid &= regrow_seed_chain(&seeds, &core_assignment).is_some_and(|assignment| {
assignment[original_variable] == value && satisfies(clauses, &assignment)
});
reconstruction_ns += reconstruction_start.elapsed().as_nanos();
reconstruction_samples += 1;
}
}
}
DeploymentMeasurement {
incremental_ns: incremental_setup_ns + incremental_query_ns,
seeded_ns: seeded_setup_ns + seeded_query_ns,
incremental_setup_ns,
seeded_setup_ns,
incremental_query_ns,
seeded_query_ns,
reconstruction_ns,
reconstruction_samples,
seeds: seeds.len(),
removed: seeds.iter().map(|seed| seed.interior.len()).sum(),
live_nodes: seeds.iter().map(|seed| seed.live_nodes).sum(),
valid,
}
}
fn deployment_knn(training: &[(Vec<f64>, f64)], features: &[f64], skip: Option<usize>) -> f64 {
let dimensions = features.len();
let scales: Vec<_> = (0..dimensions)
.map(|dimension| {
let mean = training.iter().map(|item| item.0[dimension]).sum::<f64>()
/ training.len().max(1) as f64;
(training
.iter()
.map(|item| (item.0[dimension] - mean).powi(2))
.sum::<f64>()
/ training.len().max(1) as f64)
.sqrt()
.max(1e-9)
})
.collect();
let mut neighbours: Vec<_> = training
.iter()
.enumerate()
.filter(|(index, _)| Some(*index) != skip)
.map(|(_, item)| {
let distance = (0..dimensions)
.map(|dimension| {
((features[dimension] - item.0[dimension]) / scales[dimension]).powi(2)
})
.sum::<f64>();
(distance, item.1)
})
.collect();
neighbours.sort_by(|left, right| left.0.total_cmp(&right.0));
let count = 7.min(neighbours.len()).max(1);
neighbours
.iter()
.take(count)
.map(|item| item.1)
.sum::<f64>()
/ count as f64
}
fn upper_error_margin(training: &[(Vec<f64>, f64)]) -> f64 {
let mut errors: Vec<_> = training
.iter()
.enumerate()
.map(|(index, item)| deployment_knn(training, &item.0, Some(index)) - item.1)
.collect();
errors.sort_by(|left, right| left.total_cmp(right));
errors[((errors.len().saturating_sub(1)) * 9) / 10].max(0.0)
}
fn propagate_units(clauses: &[Clause], assignment: &mut [Option<bool>]) -> Result<Vec<Clause>, ()> {
let mut current = clauses.to_vec();
loop {
let mut reduced = Vec::new();
let mut units = Vec::new();
for clause in ¤t {
if clause
.0
.iter()
.any(|&(variable, sign)| assignment[variable] == Some(sign))
{
continue;
}
let literals: Vec<_> = clause
.0
.iter()
.copied()
.filter(|&(variable, sign)| assignment[variable] != Some(!sign))
.collect();
if literals.is_empty() {
return Err(());
}
if literals.len() == 1 {
units.push(literals[0]);
}
reduced.push(Clause(literals));
}
let mut changed = false;
for (variable, sign) in units {
if assignment[variable].is_some_and(|value| value != sign) {
return Err(());
}
if assignment[variable].is_none() {
assignment[variable] = Some(sign);
changed = true;
}
}
current = reduced;
if !changed {
return Ok(current);
}
}
}
fn probe_contradiction(
clauses: &[Clause],
fixed: &[Option<bool>],
variable: usize,
value: bool,
depth: usize,
probes: &mut usize,
) -> bool {
*probes += 1;
let mut trial = fixed.to_vec();
trial[variable] = Some(value);
let Ok(reduced) = propagate_units(clauses, &mut trial) else {
return true;
};
if depth <= 1 {
return false;
}
let graph = primal_graph(trial.len(), &reduced);
let mut candidates: Vec<_> = (0..trial.len())
.filter(|&candidate| trial[candidate].is_none())
.collect();
candidates.sort_by_key(|&candidate| std::cmp::Reverse(graph[candidate].len()));
candidates.into_iter().any(|candidate| {
probe_contradiction(&reduced, &trial, candidate, false, depth - 1, probes)
&& probe_contradiction(&reduced, &trial, candidate, true, depth - 1, probes)
})
}
fn leaf_richness(vars: usize, clauses: &[Clause]) -> usize {
let graph = primal_graph(vars, clauses);
let mut positive = vec![false; vars];
let mut negative = vec![false; vars];
let mut units = 0;
for clause in clauses {
units += usize::from(clause.0.len() == 1);
for &(variable, sign) in &clause.0 {
if sign {
positive[variable] = true;
} else {
negative[variable] = true;
}
}
}
units
+ (0..vars)
.filter(|&variable| {
positive[variable] != negative[variable] || graph[variable].len() <= 2
})
.count()
}
fn shake_formula(
vars: usize,
clauses: &[Clause],
inverse_depth: usize,
probe_limit: usize,
) -> ShakenFormula {
let mut fixed = vec![None; vars];
let mut current = clauses.to_vec();
let mut probes = 0;
let mut inverse_forced = 0;
let mut contradiction = false;
loop {
current = match propagate_units(¤t, &mut fixed) {
Ok(reduced) => reduced,
Err(()) => {
contradiction = true;
Vec::new()
}
};
if contradiction {
break;
}
let mut positive = vec![false; vars];
let mut negative = vec![false; vars];
for clause in ¤t {
for &(variable, sign) in &clause.0 {
if sign {
positive[variable] = true;
} else {
negative[variable] = true;
}
}
}
let mut changed = false;
for variable in 0..vars {
if fixed[variable].is_none() && positive[variable] != negative[variable] {
fixed[variable] = Some(positive[variable]);
changed = true;
}
}
if changed {
continue;
}
if inverse_depth > 0 {
let graph = primal_graph(vars, ¤t);
let mut candidates: Vec<_> = (0..vars)
.filter(|&variable| fixed[variable].is_none())
.collect();
candidates.sort_by_key(|&variable| std::cmp::Reverse(graph[variable].len()));
for variable in candidates.into_iter().take(probe_limit) {
let false_fails = probe_contradiction(
¤t,
&fixed,
variable,
false,
inverse_depth,
&mut probes,
);
let true_fails = probe_contradiction(
¤t,
&fixed,
variable,
true,
inverse_depth,
&mut probes,
);
if false_fails && true_fails {
contradiction = true;
break;
}
if false_fails || true_fails {
fixed[variable] = Some(false_fails);
inverse_forced += 1;
changed = true;
break;
}
}
}
if contradiction || !changed {
break;
}
}
let mut active = vec![false; vars];
for clause in ¤t {
for &(variable, _) in &clause.0 {
active[variable] = true;
}
}
let core_to_original: Vec<_> = (0..vars).filter(|&variable| active[variable]).collect();
let mut original_to_core = vec![usize::MAX; vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
let compacted = current
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(variable, sign)| (original_to_core[variable], sign))
.collect(),
)
})
.collect();
ShakenFormula {
vars: core_to_original.len(),
clauses: compacted,
core_to_original,
removed: active.iter().filter(|&&item| !item).count(),
fixed,
probes,
inverse_forced,
contradiction,
}
}
fn osmotic_helper_score(
vars: usize,
clauses: &[Clause],
pair: (Literal, Literal),
frequency: usize,
) -> f64 {
let graph = primal_graph(vars, clauses);
let (a, b) = (pair.0.0, pair.1.0);
let shared = graph[a].intersection(&graph[b]).count();
let pressure_difference = graph[a].len().abs_diff(graph[b].len());
frequency as f64 * (shared + 1) as f64 / (pressure_difference + 1) as f64
}
fn warp_helper_score(
vars: usize,
clauses: &[Clause],
pair: (Literal, Literal),
frequency: usize,
) -> f64 {
let order = min_fill_order(vars, clauses);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let graph = primal_graph(vars, clauses);
let (a, b) = (pair.0.0, pair.1.0);
let forward_span = rank[a].abs_diff(rank[b]) + 1;
let shared = graph[a].intersection(&graph[b]).count() + 1;
frequency as f64 * forward_span as f64 * shared as f64
}
fn expand_two_greedy(
vars: usize,
clauses: &[Clause],
candidate_limit: usize,
warp: bool,
) -> (usize, Vec<Clause>, usize) {
let mut current_vars = vars;
let mut current = clauses.to_vec();
let mut added = 0;
for _ in 0..2 {
let candidates: Vec<_> = recurring_pair_candidates(¤t)
.into_iter()
.take(candidate_limit)
.collect();
let choice =
if warp {
candidates.iter().max_by(|a, b| {
warp_helper_score(current_vars, ¤t, a.0, a.1)
.total_cmp(&warp_helper_score(current_vars, ¤t, b.0, b.1))
})
} else {
candidates.first()
};
let Some(&(pair, _)) = choice else { break };
let Some(next) = add_pair_helper(current_vars, ¤t, pair) else {
break;
};
current = next;
current_vars += 1;
added += 1;
}
(current_vars, current, added)
}
fn remove_with_fill(graph: &mut [BTreeSet<usize>], alive: &mut BTreeSet<usize>, v: usize) {
let neighbors: Vec<_> = graph[v].intersection(alive).copied().collect();
for &a in &neighbors {
for &b in &neighbors {
if a != b {
graph[a].insert(b);
}
}
}
alive.remove(&v);
}
fn min_degree_order(vars: usize, clauses: &[Clause]) -> Vec<usize> {
let mut graph = primal_graph(vars, clauses);
let mut alive: BTreeSet<_> = (0..vars).collect();
let mut order = Vec::new();
while !alive.is_empty() {
let &v = alive
.iter()
.min_by_key(|&&v| graph[v].intersection(&alive).count())
.unwrap();
remove_with_fill(&mut graph, &mut alive, v);
order.push(v);
}
order
}
fn min_fill_order(vars: usize, clauses: &[Clause]) -> Vec<usize> {
let mut graph = primal_graph(vars, clauses);
let mut alive: BTreeSet<_> = (0..vars).collect();
let mut order = Vec::new();
while !alive.is_empty() {
let &v = alive
.iter()
.min_by_key(|&&v| {
let neighbors: Vec<_> = graph[v].intersection(&alive).copied().collect();
let missing = neighbors
.iter()
.enumerate()
.map(|(i, &a)| {
neighbors[i + 1..]
.iter()
.filter(|&&b| !graph[a].contains(&b))
.count()
})
.sum::<usize>();
(missing, neighbors.len(), v)
})
.unwrap();
remove_with_fill(&mut graph, &mut alive, v);
order.push(v);
}
order
}
fn elimination_cost(vars: usize, clauses: &[Clause], order: &[usize]) -> (usize, f64) {
let mut graph = primal_graph(vars, clauses);
let mut alive: BTreeSet<_> = (0..vars).collect();
let mut max_width = 0;
let mut estimated_work = 0.0;
for &variable in order {
let degree = graph[variable].intersection(&alive).count();
max_width = max_width.max(degree);
estimated_work += 2.0f64.powi(degree.min(1023) as i32);
remove_with_fill(&mut graph, &mut alive, variable);
}
(max_width, estimated_work)
}
fn greedy_clique_lower_bound(vars: usize, clauses: &[Clause]) -> usize {
if vars == 0 {
return 0;
}
let graph = primal_graph(vars, clauses);
let mut best = 1usize;
for start in 0..vars {
let mut clique = vec![start];
let mut candidates = graph[start].clone();
while !candidates.is_empty() {
let next = *candidates
.iter()
.max_by_key(|&&candidate| {
(
graph[candidate].intersection(&candidates).count(),
graph[candidate].len(),
)
})
.expect("non-empty clique candidates");
clique.push(next);
candidates = candidates.intersection(&graph[next]).copied().collect();
}
best = best.max(clique.len());
}
best.saturating_sub(1)
}
fn minor_min_width_lower_bound(vars: usize, clauses: &[Clause]) -> usize {
let mut graph = primal_graph(vars, clauses);
let mut alive: BTreeSet<_> = (0..vars).collect();
let mut lower = 0usize;
while let Some(&variable) = alive
.iter()
.min_by_key(|&&v| (graph[v].intersection(&alive).count(), v))
{
let neighbours: Vec<_> = graph[variable].intersection(&alive).copied().collect();
lower = lower.max(neighbours.len());
let Some(&target) = neighbours
.iter()
.min_by_key(|&&v| (graph[v].intersection(&alive).count(), v))
else {
alive.remove(&variable);
continue;
};
for neighbour in neighbours {
if neighbour == target {
continue;
}
graph[neighbour].remove(&variable);
graph[neighbour].insert(target);
graph[target].insert(neighbour);
}
graph[target].remove(&variable);
graph[variable].clear();
alive.remove(&variable);
}
lower
}
fn structural_treewidth_lower_bound(vars: usize, clauses: &[Clause]) -> usize {
greedy_clique_lower_bound(vars, clauses).max(minor_min_width_lower_bound(vars, clauses))
}
#[derive(Debug)]
struct PredictedExpansion {
vars: usize,
clauses: Vec<Clause>,
accepted: usize,
recursive_accepted: usize,
candidates_scored: usize,
initial_width: usize,
final_width: usize,
initial_estimated_work: f64,
final_estimated_work: f64,
}
fn predicted_joint_expand(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
candidate_limit: usize,
) -> PredictedExpansion {
let mut current_vars = vars;
let mut current = clauses.to_vec();
let initial_order = min_fill_order(current_vars, ¤t);
let (initial_width, initial_estimated_work) =
elimination_cost(current_vars, ¤t, &initial_order);
let mut current_cost = (initial_width, initial_estimated_work);
let mut accepted = 0;
let mut recursive_accepted = 0;
let mut candidates_scored = 0;
while accepted < max_helpers {
let mut best: Option<((Literal, Literal), Vec<Clause>, (usize, f64))> = None;
for (pair, _) in recurring_pair_candidates(¤t)
.into_iter()
.take(candidate_limit)
{
let Some(candidate) = add_pair_helper(current_vars, ¤t, pair) else {
continue;
};
candidates_scored += 1;
let candidate_vars = current_vars + 1;
let candidate_order = min_fill_order(candidate_vars, &candidate);
let cost = elimination_cost(candidate_vars, &candidate, &candidate_order);
let better_than_best = best.as_ref().is_none_or(|(_, _, best_cost)| {
cost.0 < best_cost.0 || (cost.0 == best_cost.0 && cost.1 < best_cost.1)
});
if better_than_best {
best = Some((pair, candidate, cost));
}
}
let Some((pair, next, next_cost)) = best else {
break;
};
let improves = next_cost.0 < current_cost.0
|| (next_cost.0 == current_cost.0 && next_cost.1 < current_cost.1);
if !improves {
break;
}
if pair.0.0 >= vars || pair.1.0 >= vars {
recursive_accepted += 1;
}
current = next;
current_vars += 1;
current_cost = next_cost;
accepted += 1;
}
PredictedExpansion {
vars: current_vars,
clauses: current,
accepted,
recursive_accepted,
candidates_scored,
initial_width,
final_width: current_cost.0,
initial_estimated_work,
final_estimated_work: current_cost.1,
}
}
fn semantic_batch_expand(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
interactions: &[((Literal, Literal), usize)],
) -> (usize, Vec<Clause>, usize, usize) {
let interaction_score: HashMap<_, _> = interactions.iter().copied().collect();
let mut candidates = recurring_pair_candidates(clauses);
candidates.sort_by_key(|&(pair, frequency)| {
let semantic = interaction_score.get(&pair).copied().unwrap_or(0);
std::cmp::Reverse((frequency as u128) * (semantic as u128 + 1))
});
let mut current = clauses.to_vec();
let mut current_vars = vars;
let mut accepted = 0;
let mut scored = 0;
for (pair, _) in candidates {
if accepted == max_helpers {
break;
}
scored += 1;
if let Some(next) = add_pair_helper(current_vars, ¤t, pair) {
current = next;
current_vars += 1;
accepted += 1;
}
}
(current_vars, current, accepted, scored)
}
fn scent_batch_expand(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
hops: usize,
) -> (usize, Vec<Clause>, usize, usize) {
let scent = diffuse_scent_variant(vars, clauses, hops, 0);
let graph = primal_graph(vars, clauses);
let candidates = scored_scent_candidates(clauses, &graph, &scent);
let scored = candidates.len();
let mut current = clauses.to_vec();
let mut current_vars = vars;
let mut accepted = 0;
for (pair, _, _) in candidates {
if accepted == max_helpers {
break;
}
if let Some(next) = add_pair_helper(current_vars, ¤t, pair) {
current = next;
current_vars += 1;
accepted += 1;
}
}
(current_vars, current, accepted, scored)
}
fn scored_scent_candidates(
clauses: &[Clause],
graph: &[BTreeSet<usize>],
scent: &[f64],
) -> Vec<((Literal, Literal), usize, f64)> {
let mut candidates: Vec<_> = recurring_pair_candidates(clauses)
.into_iter()
.map(|(pair, frequency)| {
let a = pair.0.0;
let b = pair.1.0;
let shared = graph[a].intersection(&graph[b]).count();
let distinct = graph[a].symmetric_difference(&graph[b]).count();
let score =
(frequency as f64 + 1.0).ln() * (scent[a] + scent[b]) * (1.0 + shared as f64)
/ (1.0 + distinct as f64).sqrt();
(pair, frequency, score)
})
.collect();
candidates.sort_by(|a, b| b.2.total_cmp(&a.2));
candidates
}
const HELPER_GATE_FEATURES: usize = 7;
fn helper_gate_features(
vars: usize,
clauses: &[Clause],
hops: usize,
budget: usize,
) -> [f64; HELPER_GATE_FEATURES] {
let scent = diffuse_scent_variant(vars, clauses, hops, 0);
let graph = primal_graph(vars, clauses);
let candidates = scored_scent_candidates(clauses, &graph, &scent);
let scent_mean = scent.iter().sum::<f64>() / scent.len().max(1) as f64;
let scent_variance = scent
.iter()
.map(|value| (value - scent_mean).powi(2))
.sum::<f64>()
/ scent.len().max(1) as f64;
let top_score = candidates
.first()
.map(|candidate| candidate.2)
.unwrap_or(0.0);
let next_score = candidates
.get(budget)
.map(|candidate| candidate.2)
.unwrap_or(0.0);
let frequency_sum = candidates
.iter()
.map(|candidate| candidate.1)
.sum::<usize>();
let top_frequency = candidates
.iter()
.take(budget)
.map(|candidate| candidate.1)
.sum::<usize>();
let selected: Vec<_> = candidates.iter().take(budget).collect();
let overlap = selected
.iter()
.map(|candidate| {
let a = candidate.0.0.0;
let b = candidate.0.1.0;
graph[a].intersection(&graph[b]).count() as f64
/ graph[a].union(&graph[b]).count().max(1) as f64
})
.sum::<f64>()
/ selected.len().max(1) as f64;
let mut variable_uses = HashMap::new();
for candidate in &selected {
*variable_uses.entry(candidate.0.0.0).or_insert(0usize) += 1;
*variable_uses.entry(candidate.0.1.0).or_insert(0usize) += 1;
}
let interacting_uses = variable_uses
.values()
.map(|uses| uses.saturating_sub(1))
.sum::<usize>();
[
1.0,
scent.iter().copied().fold(0.0, f64::max) / scent_mean.max(1e-9),
scent_variance.sqrt() / scent_mean.max(1e-9),
(top_score - next_score) / top_score.max(1e-9),
top_frequency as f64 / frequency_sum.max(1) as f64,
overlap,
interacting_uses as f64 / (2 * selected.len()).max(1) as f64,
]
}
const GRAPH_MESSAGE_WIDTH: usize = 8;
#[derive(Clone, Copy, Debug)]
struct MessageParams {
self_weight: f64,
clause_weight: f64,
variable_weight: f64,
candidate_weight: f64,
memory_retention: f64,
}
const DEFAULT_MESSAGE_PARAMS: MessageParams = MessageParams {
self_weight: 0.5,
clause_weight: 1.0,
variable_weight: 1.0,
candidate_weight: 1.0,
memory_retention: 1.0,
};
fn message_parameter_candidates(ghost: bool) -> Vec<MessageParams> {
let profiles = [
(1.0, 1.0, 1.0),
(2.0, 1.0, 1.0),
(1.0, 2.0, 1.0),
(1.0, 1.0, 2.0),
];
let retentions: &[f64] = if ghost { &[0.5, 1.0] } else { &[1.0] };
let mut result = Vec::new();
for self_weight in [0.25, 0.5, 0.75] {
for &(clause_weight, variable_weight, candidate_weight) in &profiles {
for &memory_retention in retentions {
result.push(MessageParams {
self_weight,
clause_weight,
variable_weight,
candidate_weight,
memory_retention,
});
}
}
}
result
}
fn helper_graph_features(
vars: usize,
clauses: &[Clause],
hops: usize,
candidate_limit: usize,
mode: u8,
) -> Vec<f64> {
helper_graph_features_with_params(
vars,
clauses,
hops,
candidate_limit,
mode,
DEFAULT_MESSAGE_PARAMS,
)
}
fn helper_graph_features_with_params(
vars: usize,
clauses: &[Clause],
hops: usize,
candidate_limit: usize,
mode: u8,
params: MessageParams,
) -> Vec<f64> {
let scent = diffuse_scent_variant(vars, clauses, hops, 0);
let scent_mean = scent.iter().sum::<f64>() / scent.len().max(1) as f64;
let graph = primal_graph(vars, clauses);
let candidates = scored_scent_candidates(clauses, &graph, &scent);
let candidates: Vec<_> = candidates.into_iter().take(candidate_limit).collect();
let clause_offset = vars;
let candidate_offset = vars + clauses.len();
let node_count = candidate_offset + candidates.len();
let mut adjacency = vec![BTreeSet::new(); node_count];
let mut state = vec![[0.0; GRAPH_MESSAGE_WIDTH]; node_count];
let max_score = candidates
.first()
.map(|candidate| candidate.2)
.unwrap_or(1.0);
let max_frequency = candidates
.iter()
.map(|candidate| candidate.1)
.max()
.unwrap_or(1);
let mut positive = vec![0usize; vars];
let mut negative = vec![0usize; vars];
for (clause_index, clause) in clauses.iter().enumerate() {
let clause_node = clause_offset + clause_index;
state[clause_node][1] = 1.0;
state[clause_node][3] = clause.0.iter().map(|literal| scent[literal.0]).sum::<f64>()
/ clause.0.len().max(1) as f64
/ scent_mean.max(1e-9);
state[clause_node][4] = clause.0.len() as f64 / 3.0;
state[clause_node][5] = clause.0.iter().filter(|literal| literal.1).count() as f64
/ clause.0.len().max(1) as f64;
state[clause_node][7] = 1.0;
for &(variable, sign) in &clause.0 {
adjacency[variable].insert(clause_node);
adjacency[clause_node].insert(variable);
if sign {
positive[variable] += 1
} else {
negative[variable] += 1
}
}
}
for variable in 0..vars {
state[variable][0] = 1.0;
state[variable][3] = scent[variable] / scent_mean.max(1e-9);
state[variable][4] = graph[variable].len() as f64 / vars.max(1) as f64;
state[variable][5] = positive[variable].min(negative[variable]) as f64
/ (positive[variable] + negative[variable]).max(1) as f64;
state[variable][7] = 1.0;
}
for (index, &(pair, frequency, score)) in candidates.iter().enumerate() {
let node = candidate_offset + index;
state[node][2] = 1.0;
state[node][3] = (scent[pair.0.0] + scent[pair.1.0]) / (2.0 * scent_mean.max(1e-9));
state[node][4] = frequency as f64 / max_frequency.max(1) as f64;
state[node][5] = score / max_score.max(1e-9);
state[node][6] = f64::from(pair.0.1 == pair.1.1);
state[node][7] = 1.0;
for variable in [pair.0.0, pair.1.0] {
adjacency[node].insert(variable);
adjacency[variable].insert(node);
}
}
for a in 0..candidates.len() {
for b in a + 1..candidates.len() {
let a_vars = [candidates[a].0.0.0, candidates[a].0.1.0];
let b_vars = [candidates[b].0.0.0, candidates[b].0.1.0];
if a_vars.iter().any(|variable| b_vars.contains(variable)) {
adjacency[candidate_offset + a].insert(candidate_offset + b);
adjacency[candidate_offset + b].insert(candidate_offset + a);
}
}
}
let initial_state = state.clone();
let mut active = vec![true; node_count];
if mode == 2 {
for item in active.iter_mut().take(candidate_offset).skip(clause_offset) {
*item = false;
}
}
for round in 0..3 {
let previous = state.clone();
for node in 0..node_count {
if !active[node] {
continue;
}
let neighbours: Vec<_> = adjacency[node]
.iter()
.filter(|&&other| active[other])
.copied()
.collect();
if neighbours.is_empty() {
continue;
}
for feature in 0..GRAPH_MESSAGE_WIDTH {
let mut weighted_sum = 0.0;
let mut weight_sum = 0.0;
for &other in &neighbours {
let weight = if other < clause_offset {
params.variable_weight
} else if other < candidate_offset {
params.clause_weight
} else {
params.candidate_weight
};
weighted_sum += weight * previous[other][feature];
weight_sum += weight;
}
let mean = weighted_sum / weight_sum.max(1e-9);
state[node][feature] = params.self_weight * previous[node][feature]
+ (1.0 - params.self_weight) * mean;
}
}
if mode == 1 && round == 0 {
for item in active.iter_mut().take(candidate_offset).skip(clause_offset) {
*item = false;
}
}
if (mode == 1 && round == 1) || (mode == 2 && round == 0) {
for item in active.iter_mut().take(vars) {
*item = false;
}
}
if mode == 1 && (round == 0 || round == 1) {
for node in candidate_offset..node_count {
for feature in 0..GRAPH_MESSAGE_WIDTH {
state[node][feature] = params.memory_retention * state[node][feature]
+ (1.0 - params.memory_retention) * initial_state[node][feature];
}
}
}
}
let mut features = Vec::with_capacity(GRAPH_MESSAGE_WIDTH * 2);
for feature in 0..GRAPH_MESSAGE_WIDTH {
features.push(
(0..candidates.len())
.map(|index| state[candidate_offset + index][feature])
.sum::<f64>()
/ candidates.len().max(1) as f64,
);
}
for feature in 0..GRAPH_MESSAGE_WIDTH {
features.push(
(0..candidates.len())
.map(|index| state[candidate_offset + index][feature])
.fold(0.0, f64::max),
);
}
features
}
fn vector_knn_predict(training: &[(Vec<f64>, f64)], features: &[f64], neighbours: usize) -> f64 {
let width = features.len();
let mut mean = vec![0.0; width];
for (sample, _) in training {
for i in 0..width {
mean[i] += sample[i];
}
}
for value in &mut mean {
*value /= training.len().max(1) as f64;
}
let mut scale = vec![0.0; width];
for (sample, _) in training {
for i in 0..width {
scale[i] += (sample[i] - mean[i]).powi(2);
}
}
for value in &mut scale {
*value = (*value / training.len().max(1) as f64).sqrt().max(1e-9);
}
let mut distances: Vec<_> = training
.iter()
.map(|(sample, label)| {
let distance = (0..width)
.map(|i| ((features[i] - sample[i]) / scale[i]).powi(2))
.sum::<f64>();
(distance, *label)
})
.collect();
distances.sort_by(|a, b| a.0.total_cmp(&b.0));
distances
.iter()
.take(neighbours)
.map(|item| item.1)
.sum::<f64>()
/ neighbours.min(distances.len()).max(1) as f64
}
fn learn_vector_threshold(training: &[(Vec<f64>, f64)], neighbours: usize) -> (f64, f64, usize) {
let mut out_of_fold = Vec::new();
for held_out in 0..training.len() {
let fold: Vec<_> = training
.iter()
.enumerate()
.filter(|(index, _)| *index != held_out)
.map(|(_, sample)| sample.clone())
.collect();
out_of_fold.push((
vector_knn_predict(&fold, &training[held_out].0, neighbours),
training[held_out].1,
));
}
let mut predictions: Vec<_> = out_of_fold.iter().map(|sample| sample.0).collect();
predictions.sort_by(f64::total_cmp);
let mut thresholds = vec![f64::NEG_INFINITY];
thresholds.extend(predictions.windows(2).map(|pair| (pair[0] + pair[1]) / 2.0));
thresholds.push(f64::INFINITY);
let mut best = (f64::NEG_INFINITY, 1.0, 0usize);
for threshold in thresholds {
let applied = out_of_fold
.iter()
.filter(|sample| sample.0 < threshold)
.count();
let ratio = out_of_fold
.iter()
.map(|sample| {
if sample.0 < threshold {
sample.1.exp()
} else {
1.0
}
})
.sum::<f64>()
/ out_of_fold.len().max(1) as f64;
if ratio < best.1 - 1e-12 || ((ratio - best.1).abs() <= 1e-12 && applied < best.2) {
best = (threshold, ratio, applied);
}
}
best
}
const FEATURE_COUNT: usize = 7;
const FORMULA_FEATURE_COUNT: usize = 6;
fn formula_features(
vars: usize,
clauses: &[Clause],
baseline: &BddSolveResult,
) -> [f64; FORMULA_FEATURE_COUNT] {
let interaction_count = baseline.interaction_candidates.len();
let interaction_sum: usize = baseline
.interaction_candidates
.iter()
.map(|(_, score)| *score)
.sum();
let interaction_max = baseline
.interaction_candidates
.iter()
.map(|(_, score)| *score)
.max()
.unwrap_or(0);
[
1.0,
(baseline.allocated_nodes as f64 + 1.0).ln(),
baseline.live_nodes as f64 / baseline.allocated_nodes.max(1) as f64,
recurring_pair_candidates(clauses).len() as f64 / vars.max(1) as f64,
interaction_sum as f64 / interaction_count.max(1) as f64,
(interaction_max as f64 + 1.0).ln(),
]
}
fn formula_knn_prediction(
training: &[([f64; FORMULA_FEATURE_COUNT], f64)],
features: &[f64; FORMULA_FEATURE_COUNT],
neighbors: usize,
) -> f64 {
let mut scale = [0.0; FORMULA_FEATURE_COUNT];
let mut mean = [0.0; FORMULA_FEATURE_COUNT];
for (sample, _) in training {
for i in 0..FORMULA_FEATURE_COUNT {
mean[i] += sample[i];
}
}
for value in &mut mean {
*value /= training.len().max(1) as f64;
}
for (sample, _) in training {
for i in 0..FORMULA_FEATURE_COUNT {
scale[i] += (sample[i] - mean[i]).powi(2);
}
}
for value in &mut scale {
*value = (*value / training.len().max(1) as f64).sqrt().max(1e-9);
}
let mut distances: Vec<_> = training
.iter()
.map(|(sample, label)| {
let distance = (0..FORMULA_FEATURE_COUNT)
.map(|i| ((features[i] - sample[i]) / scale[i]).powi(2))
.sum::<f64>();
(distance, *label)
})
.collect();
distances.sort_by(|a, b| a.0.total_cmp(&b.0));
distances
.iter()
.take(neighbors)
.map(|(_, label)| label)
.sum::<f64>()
/ neighbors.min(distances.len()).max(1) as f64
}
fn helper_features(
vars: usize,
clauses: &[Clause],
pair: (Literal, Literal),
frequency: usize,
order: &[usize],
interactions: &[((Literal, Literal), usize)],
) -> [f64; FEATURE_COUNT] {
let graph = primal_graph(vars, clauses);
let mut rank = vec![0usize; vars];
for (position, &variable) in order.iter().enumerate() {
rank[variable] = position;
}
let interaction: HashMap<_, _> = interactions.iter().copied().collect();
let a = pair.0.0;
let b = pair.1.0;
let common = graph[a].intersection(&graph[b]).count();
[
1.0,
(frequency as f64 + 1.0).ln(),
rank[a].abs_diff(rank[b]) as f64 / vars.max(1) as f64,
(graph[a].len() + graph[b].len()) as f64 / (2 * vars.max(1)) as f64,
common as f64 / vars.max(1) as f64,
(interaction.get(&pair).copied().unwrap_or(0) as f64 + 1.0).ln(),
f64::from(pair.0.1 == pair.1.1),
]
}
fn fit_ridge(samples: &[([f64; FEATURE_COUNT], f64)], ridge: f64) -> [f64; FEATURE_COUNT] {
let mut matrix = [[0.0; FEATURE_COUNT + 1]; FEATURE_COUNT];
for (features, label) in samples {
for row in 0..FEATURE_COUNT {
for column in 0..FEATURE_COUNT {
matrix[row][column] += features[row] * features[column];
}
matrix[row][FEATURE_COUNT] += features[row] * label;
}
}
for (index, row) in matrix.iter_mut().enumerate() {
row[index] += ridge;
}
for pivot in 0..FEATURE_COUNT {
let best = (pivot..FEATURE_COUNT)
.max_by(|&a, &b| matrix[a][pivot].abs().total_cmp(&matrix[b][pivot].abs()))
.unwrap();
matrix.swap(pivot, best);
let divisor = matrix[pivot][pivot];
if divisor.abs() < 1e-12 {
continue;
}
for column in pivot..=FEATURE_COUNT {
matrix[pivot][column] /= divisor;
}
for row in 0..FEATURE_COUNT {
if row == pivot {
continue;
}
let factor = matrix[row][pivot];
for column in pivot..=FEATURE_COUNT {
matrix[row][column] -= factor * matrix[pivot][column];
}
}
}
std::array::from_fn(|index| matrix[index][FEATURE_COUNT])
}
fn predict(weights: &[f64; FEATURE_COUNT], features: &[f64; FEATURE_COUNT]) -> f64 {
weights
.iter()
.zip(features)
.map(|(weight, feature)| weight * feature)
.sum()
}
fn learned_batch_expand(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
weights: &[f64; FEATURE_COUNT],
baseline: &BddSolveResult,
acceptance_threshold: f64,
) -> (usize, Vec<Clause>, usize, usize) {
let order = min_fill_order(vars, clauses);
let mut candidates: Vec<_> = recurring_pair_candidates(clauses)
.into_iter()
.map(|(pair, frequency)| {
let features = helper_features(
vars,
clauses,
pair,
frequency,
&order,
&baseline.interaction_candidates,
);
(predict(weights, &features), pair)
})
.collect();
candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut current = clauses.to_vec();
let mut current_vars = vars;
let mut accepted = 0;
let scored = candidates.len();
for (predicted_delta, pair) in candidates {
if accepted == max_helpers || predicted_delta >= acceptance_threshold {
break;
}
if let Some(next) = add_pair_helper(current_vars, ¤t, pair) {
current = next;
current_vars += 1;
accepted += 1;
}
}
(current_vars, current, accepted, scored)
}
fn knn_batch_expand(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
samples: &[([f64; FEATURE_COUNT], f64)],
baseline: &BddSolveResult,
neighbors: usize,
) -> (usize, Vec<Clause>, usize, usize) {
let mut mean = [0.0; FEATURE_COUNT];
for (features, _) in samples {
for i in 0..FEATURE_COUNT {
mean[i] += features[i];
}
}
for value in &mut mean {
*value /= samples.len().max(1) as f64;
}
let mut scale = [0.0; FEATURE_COUNT];
for (features, _) in samples {
for i in 0..FEATURE_COUNT {
scale[i] += (features[i] - mean[i]).powi(2);
}
}
for value in &mut scale {
*value = (*value / samples.len().max(1) as f64).sqrt().max(1e-9);
}
let order = min_fill_order(vars, clauses);
let mut candidates: Vec<_> = recurring_pair_candidates(clauses)
.into_iter()
.map(|(pair, frequency)| {
let features = helper_features(
vars,
clauses,
pair,
frequency,
&order,
&baseline.interaction_candidates,
);
let mut distances: Vec<_> = samples
.iter()
.map(|(training, label)| {
let distance = (0..FEATURE_COUNT)
.map(|i| ((features[i] - training[i]) / scale[i]).powi(2))
.sum::<f64>();
(distance, *label)
})
.collect();
distances.sort_by(|a, b| a.0.total_cmp(&b.0));
let prediction = distances
.iter()
.take(neighbors)
.map(|(_, label)| label)
.sum::<f64>()
/ neighbors.min(distances.len()).max(1) as f64;
(prediction, pair)
})
.collect();
candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
let scored = candidates.len();
let mut current = clauses.to_vec();
let mut current_vars = vars;
let mut accepted = 0;
for (prediction, pair) in candidates {
if accepted == max_helpers || prediction >= 0.0 {
break;
}
if let Some(next) = add_pair_helper(current_vars, ¤t, pair) {
current = next;
current_vars += 1;
accepted += 1;
}
}
(current_vars, current, accepted, scored)
}
fn choose_order(name: &str, vars: usize, clauses: &[Clause], seed: u64) -> Vec<usize> {
match name {
"natural" => (0..vars).collect(),
"random" => {
let mut order: Vec<_> = (0..vars).collect();
Rng(seed.max(1)).shuffle(&mut order);
order
}
"min-degree" => min_degree_order(vars, clauses),
"min-fill" => min_fill_order(vars, clauses),
"flower-outside-in" => flower_outside_in_order(vars),
_ => panic!(
"unknown order: {name}; use natural, random, min-degree, min-fill, or flower-outside-in"
),
}
}
fn recurring_pair_candidates(clauses: &[Clause]) -> Vec<((Literal, Literal), usize)> {
let mut frequencies: HashMap<(Literal, Literal), usize> = HashMap::new();
for clause in clauses {
for i in 0..clause.0.len() {
for j in i + 1..clause.0.len() {
let pair = if clause.0[i] <= clause.0[j] {
(clause.0[i], clause.0[j])
} else {
(clause.0[j], clause.0[i])
};
*frequencies.entry(pair).or_default() += 1;
}
}
}
let mut candidates: Vec<_> = frequencies
.into_iter()
.filter(|(_, frequency)| *frequency >= 2)
.collect();
candidates.sort_by_key(|&(pair, frequency)| (std::cmp::Reverse(frequency), pair));
candidates
}
fn add_pair_helper(
vars: usize,
clauses: &[Clause],
(a, b): (Literal, Literal),
) -> Option<Vec<Clause>> {
add_pair_helper_with_minimum(vars, clauses, (a, b), 2)
}
fn add_pair_helper_with_minimum(
vars: usize,
clauses: &[Clause],
(a, b): (Literal, Literal),
minimum_occurrences: usize,
) -> Option<Vec<Clause>> {
let occurrences = clauses
.iter()
.filter(|clause| clause.0.contains(&a) && clause.0.contains(&b))
.count();
if occurrences < minimum_occurrences {
return None;
}
let mut rewritten = clauses.to_vec();
for clause in &mut rewritten {
if clause.0.contains(&a) && clause.0.contains(&b) {
clause.0.retain(|literal| *literal != a && *literal != b);
clause.0.push((vars, true));
}
}
rewritten.push(Clause(vec![(vars, false), a, b]));
rewritten.push(Clause(vec![(vars, true), (a.0, !a.1)]));
rewritten.push(Clause(vec![(vars, true), (b.0, !b.1)]));
Some(rewritten)
}
fn expand_recurring_pairs(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
) -> (usize, Vec<Clause>, usize) {
let mut rewritten = clauses.to_vec();
let mut helpers = 0;
for (pair, _) in recurring_pair_candidates(clauses) {
if helpers == max_helpers {
break;
}
if let Some(next) = add_pair_helper(vars + helpers, &rewritten, pair) {
rewritten = next;
helpers += 1;
}
}
(vars + helpers, rewritten, helpers)
}
#[derive(Debug)]
struct GreedyExpansion {
vars: usize,
clauses: Vec<Clause>,
result: BddSolveResult,
accepted: usize,
candidates_tested: usize,
beneficial_trials: usize,
recursive_accepted: usize,
}
fn feedback_expand(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
first_round_limit: usize,
later_round_limit: usize,
order_name: &str,
seed: u64,
feedback_enabled: bool,
) -> GreedyExpansion {
let mut current_vars = vars;
let mut current = clauses.to_vec();
let mut result = eliminate_with_bdds(
current_vars,
¤t,
&choose_order(order_name, current_vars, ¤t, seed),
);
let mut literal_scores: HashMap<Literal, f64> = HashMap::new();
let mut candidates_tested = 0;
let mut beneficial_trials = 0;
let mut accepted = 0;
let mut recursive_accepted = 0;
while accepted < max_helpers {
let mut candidates = recurring_pair_candidates(¤t);
candidates.sort_by(
|&(left_pair, left_frequency), &(right_pair, right_frequency)| {
let score = |pair: (Literal, Literal), frequency: usize| {
frequency as f64
+ literal_scores.get(&pair.0).copied().unwrap_or(0.0)
+ literal_scores.get(&pair.1).copied().unwrap_or(0.0)
+ if feedback_enabled && (pair.0.0 >= vars || pair.1.0 >= vars) {
0.5
} else {
0.0
}
};
score(right_pair, right_frequency)
.total_cmp(&score(left_pair, left_frequency))
.then_with(|| left_pair.cmp(&right_pair))
},
);
let limit = if accepted == 0 {
first_round_limit
} else {
later_round_limit
};
let mut best: Option<((Literal, Literal), Vec<Clause>, BddSolveResult)> = None;
for (pair, _) in candidates.into_iter().take(limit) {
let Some(candidate) = add_pair_helper(current_vars, ¤t, pair) else {
continue;
};
candidates_tested += 1;
let candidate_vars = current_vars + 1;
let order = choose_order(order_name, candidate_vars, &candidate, seed);
let candidate_result = eliminate_with_bdds(candidate_vars, &candidate, &order);
if candidate_result.allocated_nodes < result.allocated_nodes {
beneficial_trials += 1;
if best.as_ref().is_none_or(|(_, _, best_result)| {
candidate_result.allocated_nodes < best_result.allocated_nodes
}) {
best = Some((pair, candidate, candidate_result));
}
}
}
let Some((pair, next, next_result)) = best else {
break;
};
if pair.0.0 >= vars || pair.1.0 >= vars {
recursive_accepted += 1;
}
let gain = (result.allocated_nodes - next_result.allocated_nodes) as f64
/ result.allocated_nodes.max(1) as f64;
if feedback_enabled {
for literal in [pair.0, pair.1] {
*literal_scores.entry(literal).or_default() += gain * 10.0;
*literal_scores.entry((literal.0, !literal.1)).or_default() += gain * 2.0;
}
literal_scores.insert((current_vars, true), gain * 12.0);
}
current = next;
current_vars += 1;
result = next_result;
accepted += 1;
}
GreedyExpansion {
vars: current_vars,
clauses: current,
result,
accepted,
candidates_tested,
beneficial_trials,
recursive_accepted,
}
}
fn bdd_frontier_expand(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
candidate_limit: usize,
order_name: &str,
seed: u64,
) -> GreedyExpansion {
let mut current_vars = vars;
let mut current = clauses.to_vec();
let mut result = eliminate_with_bdds(
current_vars,
¤t,
&choose_order(order_name, current_vars, ¤t, seed),
);
let mut candidates_tested = 0;
let mut beneficial_trials = 0;
let mut accepted = 0;
let mut recursive_accepted = 0;
while accepted < max_helpers {
let mut best: Option<((Literal, Literal), Vec<Clause>, BddSolveResult)> = None;
let mut applicable = 0;
for &(pair, _) in &result.interaction_candidates {
let Some(candidate) = add_pair_helper_with_minimum(current_vars, ¤t, pair, 1)
else {
continue;
};
applicable += 1;
if applicable > candidate_limit {
break;
}
candidates_tested += 1;
let candidate_vars = current_vars + 1;
let order = choose_order(order_name, candidate_vars, &candidate, seed);
let candidate_result = eliminate_with_bdds(candidate_vars, &candidate, &order);
if candidate_result.allocated_nodes < result.allocated_nodes {
beneficial_trials += 1;
if best.as_ref().is_none_or(|(_, _, best_result)| {
candidate_result.allocated_nodes < best_result.allocated_nodes
}) {
best = Some((pair, candidate, candidate_result));
}
}
}
let Some((pair, next, next_result)) = best else {
break;
};
if pair.0.0 >= vars || pair.1.0 >= vars {
recursive_accepted += 1;
}
current = next;
current_vars += 1;
result = next_result;
accepted += 1;
}
GreedyExpansion {
vars: current_vars,
clauses: current,
result,
accepted,
candidates_tested,
beneficial_trials,
recursive_accepted,
}
}
fn greedy_expand(
vars: usize,
clauses: &[Clause],
max_helpers: usize,
candidate_limit: usize,
order_name: &str,
seed: u64,
bdd_aligned: bool,
) -> GreedyExpansion {
let mut current_vars = vars;
let mut current = clauses.to_vec();
let mut result = solve_bdd_strategy(current_vars, ¤t, order_name, seed, bdd_aligned);
let mut candidates_tested = 0;
let mut beneficial_trials = 0;
let mut accepted = 0;
let mut recursive_accepted = 0;
while accepted < max_helpers {
let candidates = recurring_pair_candidates(¤t);
let mut best: Option<((Literal, Literal), Vec<Clause>, BddSolveResult)> = None;
for (pair, _) in candidates.into_iter().take(candidate_limit) {
let Some(candidate) = add_pair_helper(current_vars, ¤t, pair) else {
continue;
};
candidates_tested += 1;
let candidate_vars = current_vars + 1;
let candidate_result =
solve_bdd_strategy(candidate_vars, &candidate, order_name, seed, bdd_aligned);
if candidate_result.allocated_nodes < result.allocated_nodes {
beneficial_trials += 1;
if best.as_ref().is_none_or(|(_, _, best_result)| {
candidate_result.allocated_nodes < best_result.allocated_nodes
}) {
best = Some((pair, candidate, candidate_result));
}
}
}
let Some((pair, next, next_result)) = best else {
break;
};
if pair.0.0 >= vars || pair.1.0 >= vars {
recursive_accepted += 1;
}
current = next;
current_vars += 1;
result = next_result;
accepted += 1;
}
GreedyExpansion {
vars: current_vars,
clauses: current,
result,
accepted,
candidates_tested,
beneficial_trials,
recursive_accepted,
}
}
fn solve_bdd_strategy(
vars: usize,
clauses: &[Clause],
order_name: &str,
seed: u64,
bdd_aligned: bool,
) -> BddSolveResult {
let elimination = choose_order(order_name, vars, clauses, seed);
if bdd_aligned {
eliminate_with_bdds_ordered(vars, clauses, &elimination, &elimination)
} else {
eliminate_with_bdds(vars, clauses, &elimination)
}
}
fn generate_formula(family: &str, vars: usize, ratio: usize, seed: u64) -> Vec<Clause> {
match family {
"random" => random_3sat(vars, vars * ratio, seed),
"random-planted" => planted_random_3sat(vars, vars * ratio, seed),
"banded" => banded_3sat(vars, vars * ratio, seed, 5),
"banded-planted" => planted_banded_3sat(vars, vars * ratio, seed, 5),
"banded-3" => banded_3sat(vars, vars * ratio, seed, 3),
"banded-9" => banded_3sat(vars, vars * ratio, seed, 9),
"identity-expanded" => identity_expanded_sat(vars, ratio, seed),
"flower" => flower_3sat(vars, vars * ratio, seed),
"stacked-flower" => stacked_flower_3sat(vars, vars * ratio, seed),
"flower-planted" => planted_flower_3sat(vars, vars * ratio, seed),
"flower-symmetric" => symmetric_flower_3sat(vars, vars * ratio),
"stacked-flower-planted" => planted_stacked_flower_3sat(vars, vars * ratio, seed),
_ => panic!("unknown family: {family}"),
}
}
fn parse_dimacs(path: &Path) -> Result<(usize, Vec<Clause>), String> {
let body = fs::read_to_string(path).map_err(|error| error.to_string())?;
let mut declared_vars = None;
let mut declared_clauses = None;
let mut literals = Vec::new();
let mut clauses = Vec::new();
for line in body.lines() {
let line = line.trim();
if line.starts_with('%') {
break;
}
if line.is_empty() || line.starts_with('c') {
continue;
}
if line.starts_with('p') {
let fields: Vec<_> = line.split_whitespace().collect();
if fields.len() < 4 || fields[1] != "cnf" {
return Err(format!("invalid DIMACS header in {}", path.display()));
}
declared_vars = fields[2].parse::<usize>().ok();
declared_clauses = fields[3].parse::<usize>().ok();
continue;
}
for token in line.split_whitespace() {
let literal = token
.parse::<isize>()
.map_err(|_| format!("invalid literal in {}", path.display()))?;
if literal == 0 {
clauses.push(Clause(std::mem::take(&mut literals)));
} else {
literals.push((literal.unsigned_abs() - 1, literal > 0));
}
}
}
let vars = declared_vars.ok_or_else(|| format!("missing header in {}", path.display()))?;
if !literals.is_empty() || clauses.len() != declared_clauses.unwrap_or(clauses.len()) {
return Err(format!("clause count mismatch in {}", path.display()));
}
if clauses
.iter()
.flat_map(|clause| clause.0.iter())
.any(|&(variable, _)| variable >= vars)
{
return Err(format!("variable out of range in {}", path.display()));
}
Ok((vars, clauses))
}
struct CompiledArtifact {
original_vars: usize,
original_clauses: Vec<Clause>,
core_vars: usize,
core_to_original: Vec<usize>,
core_clauses: Vec<Clause>,
seeds: Vec<BddSeededBranch>,
supports_reopening: bool,
}
fn compile_safe_artifact(
vars: usize,
clauses: &[Clause],
branch_cap: usize,
node_limit: usize,
time_limit_ms: u64,
) -> CompiledArtifact {
compile_safe_artifact_with_gate(vars, clauses, branch_cap, node_limit, time_limit_ms, "all")
}
fn solver_gate_accepts(gate: &str, seed: &BddSeededBranch, local: &[Clause]) -> bool {
if gate == "all" {
return true;
}
if gate == "none" {
return false;
}
let local_literals: usize = local.iter().map(|clause| clause.0.len()).sum();
let summary_literals: usize = seed.summary.iter().map(|clause| clause.0.len()).sum();
let local_binary = local.iter().filter(|clause| clause.0.len() == 2).count();
let summary_binary = seed
.summary
.iter()
.filter(|clause| clause.0.len() == 2)
.count();
let preserves_binary_fraction = seed.summary.is_empty()
|| summary_binary * local.len().max(1) >= local_binary * seed.summary.len();
let compresses = seed.summary.len() < local.len() && summary_literals < local_literals;
match gate {
"balanced" => {
seed.interior.len() >= 4
&& compresses
&& preserves_binary_fraction
&& seed.live_nodes <= seed.interior.len() * 6 + 8
}
"strict" => {
seed.interior.len() >= 8
&& seed.summary.len() * 3 <= local.len()
&& summary_literals * 3 <= local_literals
&& preserves_binary_fraction
&& seed.live_nodes <= seed.interior.len() * 3 + 4
}
_ => false,
}
}
fn compile_safe_artifact_with_gate(
vars: usize,
clauses: &[Clause],
branch_cap: usize,
node_limit: usize,
time_limit_ms: u64,
gate: &str,
) -> CompiledArtifact {
let candidates = fast_detachable_branch_candidates(vars, clauses, branch_cap);
let incidence = clause_incidence(vars, clauses);
let topology_graph = (gate == "topology").then(|| compact_primal_graph(vars, clauses));
let mut seeds = Vec::new();
let only_balanced = gate
.strip_prefix("balanced-only-")
.and_then(|value| value.parse::<usize>().ok());
let balanced_prefix = gate
.strip_prefix("balanced-prefix-")
.and_then(|value| value.parse::<usize>().ok());
let mut balanced_ordinal = 0usize;
for (interior, boundary) in candidates {
let attempt = try_indexed_seed_bdd_candidate(
vars,
clauses,
&incidence,
interior,
boundary,
node_limit,
std::time::Duration::from_millis(time_limit_ms),
);
if let Some(seed) = attempt.seed {
let local = indexed_local_clauses(&seed.interior, &incidence, clauses);
let accepted = if gate == "topology" {
let graph = topology_graph.as_ref().expect("topology graph");
let interior_degree_sum: usize =
seed.interior.iter().map(|&v| graph[v].len()).sum();
let boundary_degree_sum: usize =
seed.boundary.iter().map(|&v| graph[v].len()).sum();
solver_gate_accepts("balanced", &seed, &local)
&& (seed.interior.len() >= 28
|| (interior_degree_sum >= 10 && boundary_degree_sum <= 9))
} else if only_balanced.is_some() || balanced_prefix.is_some() {
let eligible = solver_gate_accepts("balanced", &seed, &local);
if eligible {
let ordinal = balanced_ordinal;
balanced_ordinal += 1;
only_balanced.is_some_and(|target| ordinal == target)
|| balanced_prefix.is_some_and(|limit| ordinal < limit)
} else {
false
}
} else {
solver_gate_accepts(gate, &seed, &local)
};
if accepted {
seeds.push(seed);
}
}
}
let all_interior: BTreeSet<_> = seeds
.iter()
.flat_map(|seed| seed.interior.iter().copied())
.collect();
let mut core_clauses: Vec<_> = clauses
.iter()
.filter(|clause| {
!clause
.0
.iter()
.any(|(variable, _)| all_interior.contains(variable))
})
.cloned()
.collect();
for seed in &seeds {
core_clauses.extend(seed.summary.iter().cloned());
}
let core_to_original: Vec<_> = (0..vars)
.filter(|variable| !all_interior.contains(variable))
.collect();
let mut original_to_core = vec![usize::MAX; vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
for clause in &mut core_clauses {
for (variable, _) in &mut clause.0 {
*variable = original_to_core[*variable];
}
}
CompiledArtifact {
original_vars: vars,
original_clauses: clauses.to_vec(),
core_vars: core_to_original.len(),
core_to_original,
core_clauses,
seeds,
supports_reopening: true,
}
}
fn artifact_push_clauses(output: &mut String, clauses: &[Clause]) {
output.push_str(&format!("{} ", clauses.len()));
for clause in clauses {
output.push_str(&format!("{} ", clause.0.len()));
for &(variable, positive) in &clause.0 {
output.push_str(&format!("{} {} ", variable, usize::from(positive)));
}
}
}
fn artifact_push_vec(output: &mut String, values: &[usize]) {
output.push_str(&format!("{} ", values.len()));
for value in values {
output.push_str(&format!("{} ", value));
}
}
fn save_compiled_artifact(path: &Path, artifact: &CompiledArtifact) -> Result<(), String> {
let mut output = format!("LSAT2 {} ", artifact.original_vars);
artifact_push_clauses(&mut output, &artifact.original_clauses);
output.push_str(&format!("{} ", artifact.core_vars));
artifact_push_vec(&mut output, &artifact.core_to_original);
artifact_push_clauses(&mut output, &artifact.core_clauses);
output.push_str(&format!("{} ", artifact.seeds.len()));
for seed in &artifact.seeds {
artifact_push_vec(&mut output, &seed.interior);
artifact_push_vec(&mut output, &seed.boundary);
artifact_push_vec(&mut output, &seed.order);
output.push_str(&format!("{} {} ", seed.root, seed.manager.nodes.len()));
for node in &seed.manager.nodes {
output.push_str(&format!("{} {} {} ", node.variable, node.low, node.high));
}
artifact_push_clauses(&mut output, &seed.summary);
}
fs::write(path, output).map_err(|error| format!("write {}: {error}", path.display()))
}
fn artifact_next<T: std::str::FromStr>(
tokens: &mut std::str::SplitWhitespace<'_>,
) -> Result<T, String> {
tokens
.next()
.ok_or_else(|| "truncated compiled artifact".to_string())?
.parse()
.map_err(|_| "invalid compiled artifact token".to_string())
}
fn artifact_read_vec(tokens: &mut std::str::SplitWhitespace<'_>) -> Result<Vec<usize>, String> {
let length: usize = artifact_next(tokens)?;
(0..length).map(|_| artifact_next(tokens)).collect()
}
fn artifact_read_clauses(
tokens: &mut std::str::SplitWhitespace<'_>,
) -> Result<Vec<Clause>, String> {
let count: usize = artifact_next(tokens)?;
(0..count)
.map(|_| {
let length: usize = artifact_next(tokens)?;
let literals = (0..length)
.map(|_| Ok((artifact_next(tokens)?, artifact_next::<usize>(tokens)? != 0)))
.collect::<Result<Vec<_>, String>>()?;
Ok(Clause(literals))
})
.collect()
}
fn load_compiled_artifact(path: &Path) -> Result<CompiledArtifact, String> {
let body =
fs::read_to_string(path).map_err(|error| format!("read {}: {error}", path.display()))?;
let mut tokens = body.split_whitespace();
let version = tokens
.next()
.ok_or_else(|| "empty compiled artifact".to_string())?;
if version != "LSAT1" && version != "LSAT2" {
return Err("unsupported compiled artifact version".to_string());
}
let original_vars = artifact_next(&mut tokens)?;
let original_clauses = artifact_read_clauses(&mut tokens)?;
let core_vars = artifact_next(&mut tokens)?;
let core_to_original = artifact_read_vec(&mut tokens)?;
let core_clauses = artifact_read_clauses(&mut tokens)?;
let seed_count: usize = artifact_next(&mut tokens)?;
let mut seeds = Vec::new();
for _ in 0..seed_count {
let interior = artifact_read_vec(&mut tokens)?;
let boundary = artifact_read_vec(&mut tokens)?;
let order = artifact_read_vec(&mut tokens)?;
let root = artifact_next(&mut tokens)?;
let node_count: usize = artifact_next(&mut tokens)?;
let mut manager = BddManager::default();
for _ in 0..node_count {
let node = BddNode {
variable: artifact_next(&mut tokens)?,
low: artifact_next(&mut tokens)?,
high: artifact_next(&mut tokens)?,
};
let id = manager.nodes.len() + 2;
manager.nodes.push(node);
manager.node_hits.push(0);
manager.unique.insert(node, id);
}
let summary = if version == "LSAT2" {
artifact_read_clauses(&mut tokens)?
} else {
Vec::new()
};
seeds.push(BddSeededBranch {
vars: 0,
clauses: Vec::new(),
core_to_original: Vec::new(),
boundary,
interior,
manager,
root,
order,
local_clauses: 0,
summary_clauses: 0,
summary,
allocated_nodes: node_count,
live_nodes: node_count,
cache_root: root,
});
}
if tokens.next().is_some() || core_to_original.len() != core_vars {
return Err("inconsistent compiled artifact".to_string());
}
Ok(CompiledArtifact {
original_vars,
original_clauses,
core_vars,
core_to_original,
core_clauses,
seeds,
supports_reopening: version == "LSAT2",
})
}
fn query_compiled_artifact(
artifact: &CompiledArtifact,
assumptions: &[(usize, bool)],
) -> Result<Option<Vec<bool>>, String> {
for &(original, _) in assumptions {
if original >= artifact.original_vars {
return Err(format!(
"assumption variable {} is out of range",
original + 1
));
}
}
let mut original_to_core = vec![usize::MAX; artifact.original_vars];
for (core, &original) in artifact.core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
let reopened: BTreeSet<_> = assumptions
.iter()
.filter(|(variable, _)| original_to_core[*variable] == usize::MAX)
.flat_map(|(variable, _)| {
artifact
.seeds
.iter()
.enumerate()
.filter(move |(_, seed)| seed.interior.contains(variable))
.map(|(index, _)| index)
})
.collect();
if !reopened.is_empty() {
if !artifact.supports_reopening {
return Err("this LSAT1 artifact lacks persisted summaries; recompile it to query compiled-away variables".to_string());
}
let closed_interior: BTreeSet<_> = artifact
.seeds
.iter()
.enumerate()
.filter(|(index, _)| !reopened.contains(index))
.flat_map(|(_, seed)| seed.interior.iter().copied())
.collect();
let mut query_clauses: Vec<_> = artifact
.original_clauses
.iter()
.filter(|clause| {
!clause
.0
.iter()
.any(|(variable, _)| closed_interior.contains(variable))
})
.cloned()
.collect();
for (index, seed) in artifact.seeds.iter().enumerate() {
if !reopened.contains(&index) {
query_clauses.extend(seed.summary.iter().cloned());
}
}
let mut solver = Solver::new();
add_to_varisat(&mut solver, &query_clauses);
solver.assume(
&assumptions
.iter()
.map(|&(variable, value)| Lit::from_var(Var::from_index(variable), value))
.collect::<Vec<_>>(),
);
if !solver
.solve()
.map_err(|error| format!("solver error: {error}"))?
{
return Ok(None);
}
let mut assignment = vec![false; artifact.original_vars];
for literal in solver
.model()
.ok_or_else(|| "SAT solver returned no model".to_string())?
{
if literal.var().index() < artifact.original_vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
for (index, seed) in artifact.seeds.iter().enumerate() {
if reopened.contains(&index) {
continue;
}
let values = regrow_bdd_seed(seed, &assignment)
.ok_or_else(|| "compiled witness reconstruction failed".to_string())?;
for (offset, &variable) in seed.interior.iter().enumerate() {
assignment[variable] = values[offset];
}
}
if !satisfies(&artifact.original_clauses, &assignment)
|| assumptions
.iter()
.any(|&(variable, value)| assignment[variable] != value)
{
return Err("selectively reopened artifact produced an invalid assignment".to_string());
}
return Ok(Some(assignment));
}
let mut solver = Solver::new();
add_to_varisat(&mut solver, &artifact.core_clauses);
let mut core_assumptions = Vec::new();
for &(original, value) in assumptions {
let core = original_to_core[original];
debug_assert_ne!(core, usize::MAX);
core_assumptions.push(Lit::from_var(Var::from_index(core), value));
}
solver.assume(&core_assumptions);
if !solver
.solve()
.map_err(|error| format!("solver error: {error}"))?
{
return Ok(None);
}
let mut assignment = vec![false; artifact.original_vars];
for literal in solver
.model()
.ok_or_else(|| "SAT solver returned no model".to_string())?
{
if literal.var().index() < artifact.core_vars {
assignment[artifact.core_to_original[literal.var().index()]] = literal.is_positive();
}
}
for seed in &artifact.seeds {
let values = regrow_bdd_seed(seed, &assignment)
.ok_or_else(|| "compiled witness reconstruction failed".to_string())?;
for (index, &variable) in seed.interior.iter().enumerate() {
assignment[variable] = values[index];
}
}
if !satisfies(&artifact.original_clauses, &assignment)
|| assumptions
.iter()
.any(|&(variable, value)| assignment[variable] != value)
{
return Err("compiled artifact produced an invalid assignment".to_string());
}
Ok(Some(assignment))
}
fn find_dimacs_files(path: &Path, output: &mut Vec<PathBuf>) -> Result<(), String> {
if path.is_file() {
if path
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| matches!(extension, "cnf" | "dimacs"))
{
output.push(path.to_path_buf());
}
return Ok(());
}
let entries =
fs::read_dir(path).map_err(|error| format!("read {}: {error}", path.display()))?;
for entry in entries {
let entry = entry.map_err(|error| format!("read {}: {error}", path.display()))?;
find_dimacs_files(&entry.path(), output)?;
}
Ok(())
}
fn reopened_formula(artifact: &CompiledArtifact, reopened: usize) -> Vec<Clause> {
let closed_interior: BTreeSet<_> = artifact
.seeds
.iter()
.enumerate()
.filter(|(index, _)| *index != reopened)
.flat_map(|(_, seed)| seed.interior.iter().copied())
.collect();
let mut clauses: Vec<_> = artifact
.original_clauses
.iter()
.filter(|clause| {
!clause
.0
.iter()
.any(|(variable, _)| closed_interior.contains(variable))
})
.cloned()
.collect();
for (index, seed) in artifact.seeds.iter().enumerate() {
if index != reopened {
clauses.extend(seed.summary.iter().cloned());
}
}
clauses
}
const CORPUS_HEADER: &str = "path,vars,clauses,candidates,seeds,rejected,removed,removed_fraction,compile_ns,artifact_bytes,bdd_nodes,baseline_setup_ns,compiled_setup_ns,queries,direct_queries,reopened_queries,baseline_query_ns,direct_query_ns,reopened_query_ns,compiled_query_ns,query_ratio,amortized_ratio,eligible,all_agree,witnesses_valid,status";
const PROFILE_HEADER: &str = "path,vars,clauses,parse_ns,graph_ns,enumeration_ns,candidates,compile_ns,accepted,rejected,removed,removed_fraction,bdd_nodes,last_stage,status";
struct CompileProfile {
path: String,
vars: usize,
clauses: usize,
parse_ns: u128,
graph_ns: u128,
enumeration_ns: u128,
candidates: usize,
compile_ns: u128,
accepted: usize,
removed: usize,
bdd_nodes: usize,
last_stage: &'static str,
}
fn write_profile_checkpoint(
path: &Path,
profile: &CompileProfile,
status: &str,
) -> Result<(), String> {
let escaped = profile.path.replace(',', "%2C");
let row = format!(
"{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{}\n",
escaped,
profile.vars,
profile.clauses,
profile.parse_ns,
profile.graph_ns,
profile.enumeration_ns,
profile.candidates,
profile.compile_ns,
profile.accepted,
profile.candidates.saturating_sub(profile.accepted),
profile.removed,
profile.removed as f64 / profile.vars.max(1) as f64,
profile.bdd_nodes,
profile.last_stage,
status
);
let temporary = path.with_extension("tmp");
fs::write(&temporary, row)
.map_err(|error| format!("write profile checkpoint {}: {error}", temporary.display()))?;
fs::rename(&temporary, path)
.map_err(|error| format!("replace profile checkpoint {}: {error}", path.display()))
}
fn profile_single_formula(input: &Path, checkpoint: &Path) -> Result<(), String> {
let mut profile = CompileProfile {
path: input.to_string_lossy().to_string(),
vars: 0,
clauses: 0,
parse_ns: 0,
graph_ns: 0,
enumeration_ns: 0,
candidates: 0,
compile_ns: 0,
accepted: 0,
removed: 0,
bdd_nodes: 0,
last_stage: "start",
};
write_profile_checkpoint(checkpoint, &profile, "running")?;
let start = Instant::now();
let (vars, clauses) = parse_dimacs(input)?;
profile.parse_ns = start.elapsed().as_nanos();
profile.vars = vars;
profile.clauses = clauses.len();
profile.last_stage = "parse";
write_profile_checkpoint(checkpoint, &profile, "running")?;
let start = Instant::now();
let graph = compact_primal_graph(vars, &clauses);
profile.graph_ns = start.elapsed().as_nanos();
profile.last_stage = "graph";
write_profile_checkpoint(checkpoint, &profile, "running")?;
let start = Instant::now();
let candidates = global_small_separator_candidates(&graph, 64);
profile.enumeration_ns = start.elapsed().as_nanos();
profile.candidates = candidates.len();
profile.last_stage = "enumeration";
write_profile_checkpoint(checkpoint, &profile, "running")?;
let incidence = clause_incidence(vars, &clauses);
let compile_start = Instant::now();
for (interior, boundary) in candidates {
let attempt = try_indexed_seed_bdd_candidate(
vars,
&clauses,
&incidence,
interior,
boundary,
100_000,
std::time::Duration::from_millis(100),
);
if let Some(seed) = attempt.seed {
profile.accepted += 1;
profile.removed += seed.interior.len();
profile.bdd_nodes += seed.manager.nodes.len();
}
profile.compile_ns = compile_start.elapsed().as_nanos();
profile.last_stage = "compile";
write_profile_checkpoint(checkpoint, &profile, "running")?;
}
profile.compile_ns = compile_start.elapsed().as_nanos();
profile.last_stage = "complete";
write_profile_checkpoint(checkpoint, &profile, "ok")
}
fn profile_corpus_isolated(
root: &Path,
output_path: &Path,
timeout_seconds: u64,
) -> Result<(), String> {
let mut paths = Vec::new();
find_dimacs_files(root, &mut paths)?;
paths.sort();
if paths.is_empty() {
return Err(format!("no DIMACS files under {}", root.display()));
}
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create {}: {error}", parent.display()))?;
}
let mut completed = BTreeSet::new();
if output_path.exists() {
for line in fs::read_to_string(output_path)
.map_err(|error| format!("read {}: {error}", output_path.display()))?
.lines()
.skip(1)
{
if let Some(path) = line.split(',').next() {
completed.insert(path.replace("%2C", ","));
}
}
} else {
fs::write(output_path, format!("{PROFILE_HEADER}\n"))
.map_err(|error| format!("write {}: {error}", output_path.display()))?;
}
let executable = env::current_exe().map_err(|error| format!("locate executable: {error}"))?;
for (index, path) in paths.iter().enumerate() {
if completed.contains(&path.to_string_lossy().to_string()) {
continue;
}
let checkpoint = std::env::temp_dir().join(format!(
"layered-sat-profile-{}-{index}.csv",
std::process::id()
));
let mut child = Command::new(&executable)
.arg("profile-single")
.arg(path)
.arg(&checkpoint)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|error| format!("spawn profile {}: {error}", path.display()))?;
let start = Instant::now();
let mut final_status = "child-error";
loop {
if let Some(status) = child
.try_wait()
.map_err(|error| format!("wait profile: {error}"))?
{
if status.success() {
final_status = "ok";
}
break;
}
if start.elapsed() >= std::time::Duration::from_secs(timeout_seconds) {
child
.kill()
.map_err(|error| format!("kill profile: {error}"))?;
child
.wait()
.map_err(|error| format!("reap profile: {error}"))?;
final_status = "timeout";
break;
}
thread::sleep(std::time::Duration::from_millis(50));
}
let mut row = fs::read_to_string(&checkpoint).unwrap_or_else(|_| {
format!(
"{},0,0,0,0,0,0,0,0,0,0,0.000000,0,start,running\n",
path.to_string_lossy().replace(',', "%2C")
)
});
row = row.trim_end().to_string();
if final_status != "ok" {
if let Some(position) = row.rfind(',') {
row.replace_range(position + 1.., final_status);
}
}
let mut output = fs::OpenOptions::new()
.append(true)
.open(output_path)
.map_err(|error| format!("append {}: {error}", output_path.display()))?;
writeln!(output, "{row}").map_err(|error| format!("append profile: {error}"))?;
output
.flush()
.map_err(|error| format!("flush profile: {error}"))?;
let _ = fs::remove_file(&checkpoint);
println!(
"[{}/{}] {} status={}",
index + 1,
paths.len(),
path.display(),
final_status
);
}
Ok(())
}
const QUERY_RACE_HEADER: &str =
"path,mode,variable,value,setup_ns,solve_ns,result,valid,stage,status";
fn write_query_checkpoint(
checkpoint: &Path,
input: &Path,
mode: &str,
variable: usize,
value: bool,
setup_ns: u128,
solve_ns: u128,
result: &str,
valid: bool,
stage: &str,
status: &str,
) -> Result<(), String> {
let row = format!(
"{},{},{},{},{},{},{},{},{},{}\n",
input.to_string_lossy().replace(',', "%2C"),
mode,
variable + 1,
value,
setup_ns,
solve_ns,
result,
valid,
stage,
status
);
let temporary = checkpoint.with_extension("tmp");
fs::write(&temporary, row).map_err(|error| format!("write query checkpoint: {error}"))?;
fs::rename(&temporary, checkpoint).map_err(|error| format!("replace query checkpoint: {error}"))
}
fn query_race_worker(
mode: &str,
input: &Path,
variable: usize,
value: bool,
checkpoint: &Path,
) -> Result<(), String> {
let setup_start = Instant::now();
if mode == "baseline" {
let (vars, clauses) = parse_dimacs(input)?;
let mut solver = Solver::new();
add_to_varisat(&mut solver, &clauses);
let setup_ns = setup_start.elapsed().as_nanos();
write_query_checkpoint(
checkpoint, input, mode, variable, value, setup_ns, 0, "pending", false, "solve",
"running",
)?;
solver.assume(&[Lit::from_var(Var::from_index(variable), value)]);
let solve_start = Instant::now();
let sat = solver
.solve()
.map_err(|error| format!("solver error: {error}"))?;
let solve_ns = solve_start.elapsed().as_nanos();
let valid = if sat {
let mut assignment = vec![false; vars];
for literal in solver
.model()
.ok_or_else(|| "SAT solver returned no model".to_string())?
{
if literal.var().index() < vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
satisfies(&clauses, &assignment) && assignment[variable] == value
} else {
true
};
write_query_checkpoint(
checkpoint,
input,
mode,
variable,
value,
setup_ns,
solve_ns,
if sat { "sat" } else { "unsat" },
valid,
"complete",
"ok",
)
} else if mode == "compiled" {
let artifact = load_compiled_artifact(input)?;
let core_variable = artifact
.core_to_original
.iter()
.position(|&original| original == variable);
if let Some(core_variable) = core_variable {
let mut solver = Solver::new();
add_to_varisat(&mut solver, &artifact.core_clauses);
let setup_ns = setup_start.elapsed().as_nanos();
write_query_checkpoint(
checkpoint,
input,
mode,
variable,
value,
setup_ns,
0,
"pending",
false,
"solve-direct",
"running",
)?;
solver.assume(&[Lit::from_var(Var::from_index(core_variable), value)]);
let solve_start = Instant::now();
let sat = solver
.solve()
.map_err(|error| format!("solver error: {error}"))?;
let valid = if sat {
let mut assignment = vec![false; artifact.original_vars];
for literal in solver
.model()
.ok_or_else(|| "SAT solver returned no model".to_string())?
{
if literal.var().index() < artifact.core_vars {
assignment[artifact.core_to_original[literal.var().index()]] =
literal.is_positive();
}
}
for seed in &artifact.seeds {
let values = regrow_bdd_seed(seed, &assignment)
.ok_or_else(|| "compiled witness reconstruction failed".to_string())?;
for (index, &interior) in seed.interior.iter().enumerate() {
assignment[interior] = values[index];
}
}
satisfies(&artifact.original_clauses, &assignment) && assignment[variable] == value
} else {
true
};
let solve_ns = solve_start.elapsed().as_nanos();
write_query_checkpoint(
checkpoint,
input,
mode,
variable,
value,
setup_ns,
solve_ns,
if sat { "sat" } else { "unsat" },
valid,
"complete-direct",
"ok",
)
} else {
let setup_ns = setup_start.elapsed().as_nanos();
write_query_checkpoint(
checkpoint,
input,
mode,
variable,
value,
setup_ns,
0,
"pending",
false,
"solve-reopened",
"running",
)?;
let solve_start = Instant::now();
let result = query_compiled_artifact(&artifact, &[(variable, value)])?;
let solve_ns = solve_start.elapsed().as_nanos();
write_query_checkpoint(
checkpoint,
input,
mode,
variable,
value,
setup_ns,
solve_ns,
if result.is_some() { "sat" } else { "unsat" },
true,
"complete-reopened",
"ok",
)
}
} else {
Err(format!("unknown query race mode: {mode}"))
}
}
fn run_isolated_query_worker(
executable: &Path,
mode: &str,
input: &Path,
variable: usize,
value: bool,
checkpoint: &Path,
timeout: std::time::Duration,
) -> Result<String, String> {
let mut child = Command::new(executable)
.arg("query-race-worker")
.arg(mode)
.arg(input)
.arg(variable.to_string())
.arg(value.to_string())
.arg(checkpoint)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|error| format!("spawn query worker: {error}"))?;
let start = Instant::now();
let status = loop {
if let Some(exit) = child
.try_wait()
.map_err(|error| format!("wait query worker: {error}"))?
{
break if exit.success() { "ok" } else { "child-error" };
}
if start.elapsed() >= timeout {
child
.kill()
.map_err(|error| format!("kill query worker: {error}"))?;
child
.wait()
.map_err(|error| format!("reap query worker: {error}"))?;
break "timeout";
}
thread::sleep(std::time::Duration::from_millis(25));
};
let mut row = fs::read_to_string(checkpoint).unwrap_or_else(|_| {
format!(
"{},{},{},{},0,0,pending,false,start,running\n",
input.to_string_lossy().replace(',', "%2C"),
mode,
variable + 1,
value
)
});
row = row.trim_end().to_string();
if status != "ok" {
if let Some(position) = row.rfind(',') {
row.replace_range(position + 1.., status);
}
}
let _ = fs::remove_file(checkpoint);
Ok(row)
}
fn benchmark_query_race(
input: &Path,
output: &Path,
queries: usize,
timeout: std::time::Duration,
gate: &str,
) -> Result<(), String> {
let (vars, clauses) = parse_dimacs(input)?;
let binary_fraction = clauses.iter().filter(|clause| clause.0.len() == 2).count() as f64
/ clauses.len().max(1) as f64;
let effective_gate = if gate == "learned" {
if binary_fraction < 0.40 {
"balanced"
} else {
"none"
}
} else {
gate
};
let artifact_path = std::env::temp_dir().join(format!(
"layered-sat-query-race-{}.lsat",
std::process::id()
));
let artifact =
compile_safe_artifact_with_gate(vars, &clauses, 64, 100_000, 100, effective_gate);
println!(
"gate={} effective_gate={} binary_fraction={:.6} seeds={} removed={} removed_fraction={:.6} core_clauses={}",
gate,
effective_gate,
binary_fraction,
artifact.seeds.len(),
vars - artifact.core_vars,
(vars - artifact.core_vars) as f64 / vars.max(1) as f64,
artifact.core_clauses.len()
);
save_compiled_artifact(&artifact_path, &artifact)?;
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create results: {error}"))?;
}
fs::write(output, format!("{QUERY_RACE_HEADER}\n"))
.map_err(|error| format!("write query race: {error}"))?;
let executable = env::current_exe().map_err(|error| format!("locate executable: {error}"))?;
for query in 0..queries {
let variable = query.saturating_mul(vars) / queries.max(1);
let value = query % 2 == 0;
for (mode, source) in [("baseline", input), ("compiled", artifact_path.as_path())] {
let checkpoint = std::env::temp_dir().join(format!(
"layered-sat-query-{}-{query}-{mode}.csv",
std::process::id()
));
let mut row = run_isolated_query_worker(
&executable,
mode,
source,
variable,
value,
&checkpoint,
timeout,
)?;
if mode == "compiled" {
if let Some(comma) = row.find(',') {
row.replace_range(..comma, &input.to_string_lossy().replace(',', "%2C"));
}
}
let mut file = fs::OpenOptions::new()
.append(true)
.open(output)
.map_err(|error| format!("append query race: {error}"))?;
writeln!(file, "{row}").map_err(|error| format!("append query race: {error}"))?;
file.flush()
.map_err(|error| format!("flush query race: {error}"))?;
println!(
"query={}/{} mode={} variable={} value={} status={}",
query + 1,
queries,
mode,
variable + 1,
value,
row.rsplit(',').next().unwrap_or("unknown")
);
}
}
let _ = fs::remove_file(&artifact_path);
Ok(())
}
fn benchmark_query_portfolio(
input: &Path,
output: &Path,
query_start: usize,
queries: usize,
query_total: usize,
deadline: std::time::Duration,
gates: &[String],
) -> Result<(), String> {
let (vars, clauses) = parse_dimacs(input)?;
let executable = env::current_exe().map_err(|error| format!("locate executable: {error}"))?;
let mut artifacts = Vec::new();
for (index, gate) in gates.iter().enumerate() {
let artifact = compile_safe_artifact_with_gate(vars, &clauses, 64, 100_000, 100, gate);
let path = std::env::temp_dir().join(format!(
"layered-sat-portfolio-{}-{index}.lsat",
std::process::id()
));
save_compiled_artifact(&path, &artifact)?;
println!(
"portfolio_gate={} seeds={} removed={}",
gate,
artifact.seeds.len(),
vars - artifact.core_vars
);
artifacts.push((gate.clone(), path));
}
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create portfolio output: {error}"))?;
}
fs::write(
output,
"path,query,variable,value,winner,result,wall_ns,worker_wall_ns,workers,status\n",
)
.map_err(|error| format!("write portfolio output: {error}"))?;
for query in 0..queries {
let query_index = query_start.saturating_add(query);
let variable = query_index.saturating_mul(vars) / query_total.max(1);
let value = query_index % 2 == 0;
let mut specs = vec![("baseline".to_string(), "baseline", input.to_path_buf())];
specs.extend(
artifacts
.iter()
.map(|(gate, path)| (gate.clone(), "compiled", path.clone())),
);
let portfolio_start = Instant::now();
let mut workers = Vec::new();
for (index, (label, mode, source)) in specs.into_iter().enumerate() {
let checkpoint = std::env::temp_dir().join(format!(
"layered-sat-portfolio-worker-{}-{query}-{index}.csv",
std::process::id()
));
let child = Command::new(&executable)
.arg("query-race-worker")
.arg(mode)
.arg(&source)
.arg(variable.to_string())
.arg(value.to_string())
.arg(&checkpoint)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|error| format!("spawn portfolio worker: {error}"))?;
workers.push((label, child, checkpoint));
}
let worker_count = workers.len();
let mut winner = None;
while portfolio_start.elapsed() < deadline && winner.is_none() {
for (index, (_, child, checkpoint)) in workers.iter_mut().enumerate() {
if let Some(status) = child
.try_wait()
.map_err(|error| format!("wait portfolio worker: {error}"))?
{
if status.success() {
let row = fs::read_to_string(checkpoint).unwrap_or_default();
let fields: Vec<_> = row.trim().split(',').collect();
if fields.len() == 10 && fields[9] == "ok" && fields[7] == "true" {
winner = Some((index, fields[6].to_string()));
break;
}
}
}
}
if winner.is_none() {
thread::sleep(std::time::Duration::from_millis(5));
}
}
let wall_ns = portfolio_start.elapsed().as_nanos();
let winner_label = winner
.as_ref()
.map(|(index, _)| workers[*index].0.clone())
.unwrap_or_else(|| "none".to_string());
let result = winner
.as_ref()
.map(|(_, result)| result.clone())
.unwrap_or_else(|| "pending".to_string());
for (_, child, checkpoint) in &mut workers {
if child
.try_wait()
.map_err(|error| format!("poll portfolio worker: {error}"))?
.is_none()
{
child
.kill()
.map_err(|error| format!("kill portfolio worker: {error}"))?;
child
.wait()
.map_err(|error| format!("reap portfolio worker: {error}"))?;
}
let _ = fs::remove_file(checkpoint);
}
let worker_wall_ns = wall_ns.saturating_mul(worker_count as u128);
let status = if winner.is_some() { "ok" } else { "timeout" };
let mut file = fs::OpenOptions::new()
.append(true)
.open(output)
.map_err(|error| format!("append portfolio output: {error}"))?;
writeln!(
file,
"{},{},{},{},{},{},{},{},{},{}",
input.to_string_lossy().replace(',', "%2C"),
query_index + 1,
variable + 1,
value,
winner_label,
result,
wall_ns,
worker_wall_ns,
worker_count,
status
)
.map_err(|error| format!("append portfolio output: {error}"))?;
file.flush()
.map_err(|error| format!("flush portfolio output: {error}"))?;
println!(
"portfolio query={}/{} winner={} wall_ms={:.3} worker_wall_ms={:.3} status={}",
query_index + 1,
query_total,
winner_label,
wall_ns as f64 / 1e6,
worker_wall_ns as f64 / 1e6,
status
);
}
for (_, path) in artifacts {
let _ = fs::remove_file(path);
}
Ok(())
}
fn portfolio_totals(path: &Path) -> Result<(usize, u128), String> {
let text =
fs::read_to_string(path).map_err(|error| format!("read {}: {error}", path.display()))?;
let mut completed = 0usize;
let mut worker_wall_ns = 0u128;
for row in text.lines().skip(1) {
let fields: Vec<_> = row.split(',').collect();
if fields.len() != 10 {
continue;
}
worker_wall_ns = worker_wall_ns.saturating_add(fields[7].parse::<u128>().unwrap_or(0));
completed += usize::from(fields[9] == "ok");
}
Ok((completed, worker_wall_ns))
}
fn benchmark_width_strategy_search(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
output: &Path,
) -> Result<(), String> {
if vars > 20 {
return Err("width strategy search supports at most 20 variables".to_string());
}
let original = generate_formula(family, vars, ratio, formula_seed);
let original_sat = solve_with_varisat(vars, &original).is_some();
let original_order = min_fill_order(vars, &original);
let (original_width, _) = elimination_cost(vars, &original, &original_order);
let original_exact_width = exact_treewidth(vars, &original);
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create strategy output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "family,formula_seed,strategy,math_identities,inverse_depth,probe_limit,branch_cap,max_seeds,seed_order,original_vars,final_vars,removed,seeds,probes,inverse_forced,original_width,final_width,width_change,original_exact_width,final_exact_width,exact_width_change,seed_live_nodes,seed_allocated_nodes,sat_equivalent,reconstruction_valid")
.map_err(|error| format!("write strategy header: {error}"))?;
let depths = [0usize, 1, 2, 3];
let probes = [0usize, 1, 2, 4];
let caps = [2usize, 4, 6, 8];
let seed_limits = [0usize, 1, 2, 4];
let orders = ["natural", "min-fill", "min-degree", "boundary-min-fill"];
let mut strategy = 0usize;
for math_identities in [false, true] {
for &inverse_depth in &depths {
for &probe_limit in &probes {
for &branch_cap in &caps {
for &max_seeds in &seed_limits {
for &seed_order in &orders {
strategy += 1;
let preprocessed = if math_identities {
mathematical_identity_preprocess(&original).0
} else {
original.clone()
};
let shaken =
shake_formula(vars, &preprocessed, inverse_depth, probe_limit);
let mut current_vars = shaken.vars;
let mut current_clauses = shaken.clauses.clone();
let mut seeds = Vec::new();
for _ in 0..max_seeds {
let seed = seed_detachable_branch_bdd(
current_vars,
¤t_clauses,
branch_cap,
seed_order,
);
if seed.interior.is_empty() {
break;
}
current_vars = seed.vars;
current_clauses = seed.clauses.clone();
seeds.push(seed);
}
let final_width = if shaken.contradiction || current_vars == 0 {
0
} else {
let order = min_fill_order(current_vars, ¤t_clauses);
elimination_cost(current_vars, ¤t_clauses, &order).0
};
let final_exact_width = if shaken.contradiction || current_vars == 0 {
0
} else {
exact_treewidth(current_vars, ¤t_clauses)
};
let core_assignment = if shaken.contradiction {
None
} else {
solve_with_varisat(current_vars, ¤t_clauses)
};
let core_sat = core_assignment.is_some();
let sat_equivalent = core_sat == original_sat;
let reconstruction_valid = if let Some(core) = core_assignment {
regrow_seed_chain(&seeds, &core).is_some_and(|shaken_values| {
let mut reconstructed = vec![false; vars];
for (variable, value) in shaken.fixed.iter().enumerate() {
if let Some(value) = value {
reconstructed[variable] = *value;
}
}
for (core_variable, &original_variable) in
shaken.core_to_original.iter().enumerate()
{
reconstructed[original_variable] =
shaken_values[core_variable];
}
satisfies(&original, &reconstructed)
})
} else {
!original_sat
};
let live_nodes: usize = seeds.iter().map(|seed| seed.live_nodes).sum();
let allocated_nodes: usize =
seeds.iter().map(|seed| seed.allocated_nodes).sum();
writeln!(file, "{family},{formula_seed},{strategy},{math_identities},{inverse_depth},{probe_limit},{branch_cap},{max_seeds},{seed_order},{vars},{current_vars},{},{},{},{},{original_width},{final_width},{},{original_exact_width},{final_exact_width},{},{live_nodes},{allocated_nodes},{sat_equivalent},{reconstruction_valid}", vars.saturating_sub(current_vars), seeds.len(), shaken.probes, shaken.inverse_forced, final_width as isize - original_width as isize, final_exact_width as isize - original_exact_width as isize)
.map_err(|error| format!("write strategy row: {error}"))?;
}
}
}
}
}
}
file.flush()
.map_err(|error| format!("flush strategy output: {error}"))?;
println!(
"width strategy search family={family} strategies={strategy} original_width={original_width} output={}",
output.display()
);
Ok(())
}
fn benchmark_frozen_width_strategy(
family: &str,
vars: usize,
ratio: usize,
start_seed: u64,
trials: usize,
output: &Path,
) -> Result<(), String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create frozen output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "family,vars,ratio,seed,strategy,original_upper_width,original_structural_lower,final_upper_width,final_structural_lower,upper_change,certified_strict_reduction,removed,seeds,probes,inverse_forced,seed_live_nodes,seed_allocated_nodes,transform_ns,original_sat,final_sat,sat_equivalent,reconstruction_valid")
.map_err(|error| format!("write frozen header: {error}"))?;
for trial in 0..trials {
let seed = start_seed.saturating_add(trial as u64);
let original = generate_formula(family, vars, ratio, seed);
let original_width = elimination_cost(vars, &original, &min_fill_order(vars, &original)).0;
let original_lower = structural_treewidth_lower_bound(vars, &original);
let original_sat = solve_with_varisat(vars, &original).is_some();
let transform_start = Instant::now();
let preprocessed = mathematical_identity_preprocess(&original).0;
let shaken = shake_formula(vars, &preprocessed, 1, 4);
let mut current_vars = shaken.vars;
let mut current_clauses = shaken.clauses.clone();
let mut seeds = Vec::new();
for _ in 0..2 {
let compiled = seed_detachable_branch_bdd(current_vars, ¤t_clauses, 8, "natural");
if compiled.interior.is_empty() {
break;
}
current_vars = compiled.vars;
current_clauses = compiled.clauses.clone();
seeds.push(compiled);
}
let transform_ns = transform_start.elapsed().as_nanos();
let final_width = if shaken.contradiction || current_vars == 0 {
0
} else {
elimination_cost(
current_vars,
¤t_clauses,
&min_fill_order(current_vars, ¤t_clauses),
)
.0
};
let final_lower = if shaken.contradiction || current_vars == 0 {
0
} else {
structural_treewidth_lower_bound(current_vars, ¤t_clauses)
};
let certified = final_width < original_lower;
let core_assignment = if shaken.contradiction {
None
} else {
solve_with_varisat(current_vars, ¤t_clauses)
};
let final_sat = core_assignment.is_some();
let reconstruction_valid = if let Some(core) = core_assignment {
regrow_seed_chain(&seeds, &core).is_some_and(|shaken_values| {
let mut reconstructed = vec![false; vars];
for (variable, value) in shaken.fixed.iter().enumerate() {
if let Some(value) = value {
reconstructed[variable] = *value;
}
}
for (core_variable, &original_variable) in
shaken.core_to_original.iter().enumerate()
{
reconstructed[original_variable] = shaken_values[core_variable];
}
satisfies(&original, &reconstructed)
})
} else {
!original_sat
};
let live_nodes: usize = seeds.iter().map(|seed| seed.live_nodes).sum();
let allocated_nodes: usize = seeds.iter().map(|seed| seed.allocated_nodes).sum();
writeln!(file, "{family},{vars},{ratio},{seed},1529,{original_width},{original_lower},{final_width},{final_lower},{},{certified},{},{},{},{},{live_nodes},{allocated_nodes},{transform_ns},{original_sat},{final_sat},{},{reconstruction_valid}", final_width as isize - original_width as isize, vars.saturating_sub(current_vars), seeds.len(), shaken.probes, shaken.inverse_forced, original_sat == final_sat)
.map_err(|error| format!("write frozen row: {error}"))?;
}
file.flush()
.map_err(|error| format!("flush frozen output: {error}"))?;
println!(
"frozen width strategy family={family} vars={vars} trials={trials} output={}",
output.display()
);
Ok(())
}
fn ceil_log2(value: usize) -> usize {
if value <= 1 {
0
} else {
usize::BITS as usize - (value - 1).leading_zeros() as usize
}
}
fn benchmark_frontier_width_strategies(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
random_orders: usize,
output: &Path,
) -> Result<(), String> {
if vars > 20 {
return Err("frontier exact-width search supports at most 20 variables".to_string());
}
let original = generate_formula(family, vars, ratio, formula_seed);
let original_width = exact_treewidth(vars, &original);
let original_sat = solve_with_varisat(vars, &original).is_some();
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create frontier output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "family,formula_seed,strategy,order,prefix,interior,boundary,original_width,core_width,boundary_charge,bdd_information_charge,charged_width,core_change,charged_change,summary_clauses,seed_live_nodes,seed_allocated_nodes,sat_equivalent,reconstruction_valid")
.map_err(|error| format!("write frontier header: {error}"))?;
let mut orders = vec![
("natural".to_string(), (0..vars).collect::<Vec<_>>()),
("min-fill".to_string(), min_fill_order(vars, &original)),
("min-degree".to_string(), min_degree_order(vars, &original)),
("flower".to_string(), flower_outside_in_order(vars)),
];
for index in 0..random_orders {
let mut order: Vec<_> = (0..vars).collect();
Rng(formula_seed ^ (index as u64 + 1).wrapping_mul(0x9e37_79b9)).shuffle(&mut order);
orders.push((format!("random-{index}"), order));
}
let graph = primal_graph(vars, &original);
let mut strategy = 0usize;
for (order_name, order) in orders {
for prefix in 1..vars {
strategy += 1;
let mut interior = order[..prefix].to_vec();
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let boundary: Vec<_> = interior
.iter()
.flat_map(|&variable| graph[variable].iter().copied())
.filter(|variable| !interior_set.contains(variable))
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let mut manager = BddManager::default();
let compiled = seed_bdd_candidate_in(
vars,
&original,
&mut interior,
boundary.clone(),
"min-fill",
&mut manager,
);
let core_width = exact_treewidth(compiled.vars, &compiled.clauses);
let bdd_charge = ceil_log2(compiled.live_nodes.saturating_add(2));
let charged_width = core_width.max(boundary.len()).max(bdd_charge);
let core_assignment = solve_with_varisat(compiled.vars, &compiled.clauses);
let core_sat = core_assignment.is_some();
let reconstruction_valid = if let Some(core) = core_assignment {
let mut mapped = vec![false; vars];
for (core_variable, &original_variable) in
compiled.core_to_original.iter().enumerate()
{
mapped[original_variable] = core[core_variable];
}
regrow_bdd_seed(&compiled, &mapped).is_some_and(|values| {
for (index, &variable) in compiled.interior.iter().enumerate() {
mapped[variable] = values[index];
}
satisfies(&original, &mapped)
})
} else {
!original_sat
};
writeln!(file, "{family},{formula_seed},{strategy},{order_name},{prefix},{},{},{original_width},{core_width},{},{bdd_charge},{charged_width},{},{},{},{},{},{},{}", compiled.interior.len(), boundary.len(), boundary.len(), core_width as isize - original_width as isize, charged_width as isize - original_width as isize, compiled.summary_clauses, compiled.live_nodes, compiled.allocated_nodes, core_sat == original_sat, reconstruction_valid)
.map_err(|error| format!("write frontier row: {error}"))?;
}
}
file.flush()
.map_err(|error| format!("flush frontier output: {error}"))?;
println!(
"frontier width search family={family} seed={formula_seed} strategies={strategy} original_width={original_width} output={}",
output.display()
);
Ok(())
}
fn projected_bdd_network_cnf(
vars: usize,
clauses: &[Clause],
interior: &[usize],
boundary: &[usize],
) -> (usize, Vec<Clause>, usize) {
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let local: Vec<_> = clauses
.iter()
.filter(|clause| clause.0.iter().any(|(v, _)| interior_set.contains(v)))
.cloned()
.collect();
let relevant: BTreeSet<_> = boundary.iter().chain(interior.iter()).copied().collect();
let order = restricted_order(&local, &relevant, true);
let rank: HashMap<_, _> = order
.iter()
.copied()
.enumerate()
.map(|(rank, variable)| (variable, rank))
.collect();
let mut manager = BddManager::default();
let mut relation = compile_formula_bdd_into(&mut manager, vars, &local, &order);
for &variable in interior {
relation = manager.exists(relation, rank[&variable], &mut HashMap::new());
}
let (manager, relation) = compact_bdd(&manager, relation);
let core_to_original: Vec<_> = (0..vars)
.filter(|variable| !interior_set.contains(variable))
.collect();
let mut original_to_core = vec![usize::MAX; vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
let core_vars = core_to_original.len();
let mut transformed: Vec<_> = clauses
.iter()
.filter(|clause| !clause.0.iter().any(|(v, _)| interior_set.contains(v)))
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(variable, sign)| (original_to_core[variable], sign))
.collect(),
)
})
.collect();
let helper = |node: usize| core_vars + node - 2;
for node_id in 2..manager.nodes.len() + 2 {
let node = manager.node(node_id);
let decision_original = order[node.variable];
let decision = original_to_core[decision_original];
let output = helper(node_id);
for decision_value in [false, true] {
let child = if decision_value { node.high } else { node.low };
if child < 2 {
let child_value = child == 1;
for output_value in [false, true] {
if output_value != child_value {
transformed.push(Clause(vec![
(output, !output_value),
(decision, !decision_value),
]));
}
}
} else {
for output_value in [false, true] {
for child_value in [false, true] {
if output_value != child_value {
transformed.push(Clause(vec![
(output, !output_value),
(decision, !decision_value),
(helper(child), !child_value),
]));
}
}
}
}
}
}
if relation == 0 {
transformed.push(Clause(Vec::new()));
} else if relation >= 2 {
transformed.push(Clause(vec![(helper(relation), true)]));
}
(
core_vars + manager.nodes.len(),
transformed,
manager.nodes.len(),
)
}
fn direct_bdd_network_cnf(
vars: usize,
clauses: &[Clause],
interior: &[usize],
boundary: &[usize],
) -> (usize, Vec<Clause>, usize) {
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let local: Vec<_> = clauses
.iter()
.filter(|clause| clause.0.iter().any(|(v, _)| interior_set.contains(v)))
.cloned()
.collect();
let relevant: BTreeSet<_> = boundary.iter().chain(interior.iter()).copied().collect();
let order = restricted_order(&local, &relevant, true);
let mut manager = BddManager::default();
let root = compile_formula_bdd_into(&mut manager, vars, &local, &order);
let (manager, root) = compact_bdd(&manager, root);
let mut transformed: Vec<_> = clauses
.iter()
.filter(|clause| !clause.0.iter().any(|(v, _)| interior_set.contains(v)))
.cloned()
.collect();
let helper = |node: usize| vars + node - 2;
for node_id in 2..manager.nodes.len() + 2 {
let node = manager.node(node_id);
let decision = order[node.variable];
let output = helper(node_id);
for decision_value in [false, true] {
let child = if decision_value { node.high } else { node.low };
if child < 2 {
let child_value = child == 1;
for output_value in [false, true] {
if output_value != child_value {
transformed.push(Clause(vec![
(output, !output_value),
(decision, !decision_value),
]));
}
}
} else {
for output_value in [false, true] {
for child_value in [false, true] {
if output_value != child_value {
transformed.push(Clause(vec![
(output, !output_value),
(decision, !decision_value),
(helper(child), !child_value),
]));
}
}
}
}
}
}
if root == 0 {
transformed.push(Clause(Vec::new()));
} else if root >= 2 {
transformed.push(Clause(vec![(helper(root), true)]));
}
(vars + manager.nodes.len(), transformed, manager.nodes.len())
}
fn benchmark_direct_bdd_network_expansion(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
random_orders: usize,
output: &Path,
) -> Result<(), String> {
if vars > 20 {
return Err("direct BDD expansion requires exact original width (max 20 vars)".to_string());
}
let original = generate_formula(family, vars, ratio, formula_seed);
let original_width = exact_treewidth(vars, &original);
let original_sat = solve_with_varisat(vars, &original).is_some();
let graph = primal_graph(vars, &original);
let mut orders = vec![
("natural".to_string(), (0..vars).collect::<Vec<_>>()),
("min-fill".to_string(), min_fill_order(vars, &original)),
("min-degree".to_string(), min_degree_order(vars, &original)),
("flower".to_string(), flower_outside_in_order(vars)),
];
for index in 0..random_orders {
let mut order: Vec<_> = (0..vars).collect();
Rng(formula_seed ^ (index as u64 + 1).wrapping_mul(0xd1b5_4a32)).shuffle(&mut order);
orders.push((format!("random-{index}"), order));
}
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create direct network output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create direct network output: {error}"))?;
writeln!(file, "family,formula_seed,strategy,order,prefix,interior,boundary,original_width,expanded_vars,expanded_clauses,bdd_helpers,expanded_upper_width,certified_change,size_ratio,sat_equivalent,witness_valid")
.map_err(|error| format!("write direct network header: {error}"))?;
let mut strategy = 0usize;
for (order_name, order) in orders {
for prefix in 1..=vars {
strategy += 1;
let interior = order[..prefix].to_vec();
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let boundary: Vec<_> = interior
.iter()
.flat_map(|&variable| graph[variable].iter().copied())
.filter(|variable| !interior_set.contains(variable))
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let (expanded_vars, expanded, helpers) =
direct_bdd_network_cnf(vars, &original, &interior, &boundary);
let expanded_width = elimination_cost(
expanded_vars,
&expanded,
&min_fill_order(expanded_vars, &expanded),
)
.0;
let assignment = solve_with_varisat(expanded_vars, &expanded);
let expanded_sat = assignment.is_some();
let witness_valid = assignment
.as_ref()
.is_some_and(|values| satisfies(&original, &values[..vars]))
|| (!original_sat && assignment.is_none());
writeln!(file, "{family},{formula_seed},{strategy},{order_name},{prefix},{},{},{original_width},{expanded_vars},{},{helpers},{expanded_width},{},{:.6},{},{}", interior.len(), boundary.len(), expanded.len(), expanded_width as isize - original_width as isize, expanded.len() as f64 / original.len().max(1) as f64, expanded_sat == original_sat, witness_valid)
.map_err(|error| format!("write direct network row: {error}"))?;
}
}
file.flush()
.map_err(|error| format!("flush direct network output: {error}"))?;
println!(
"direct BDD network expansion family={family} seed={formula_seed} strategies={strategy} original_width={original_width} output={}",
output.display()
);
Ok(())
}
fn benchmark_finite_domain_groupings(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
strategies: usize,
output: &Path,
) -> Result<(), String> {
if vars > 20 {
return Err("finite-domain grouping exact search supports at most 20 Booleans".to_string());
}
let formula = generate_formula(family, vars, ratio, formula_seed);
let original_width = exact_treewidth(vars, &formula);
let witness = solve_with_varisat(vars, &formula);
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create grouping output: {error}"))?;
}
let mut file =
fs::File::create(output).map_err(|error| format!("create grouping output: {error}"))?;
writeln!(file, "family,formula_seed,strategy,order_kind,max_group,groups,max_domain_bits,original_width,unweighted_group_width,weighted_group_width,weighted_change,total_domain_bits,assignment_roundtrip,witness_valid")
.map_err(|error| format!("write grouping header: {error}"))?;
let structural_orders = [
(0..vars).collect::<Vec<_>>(),
min_fill_order(vars, &formula),
min_degree_order(vars, &formula),
];
for strategy in 0..strategies {
let max_group = 2 + strategy % 3;
let order_kind = (strategy / 3) % 4;
let variant = strategy / 12;
let mut order = if order_kind < 3 {
structural_orders[order_kind].clone()
} else {
let mut order: Vec<_> = (0..vars).collect();
Rng((variant as u64 + 1).wrapping_mul(0x9e37_79b9)).shuffle(&mut order);
order
};
if order_kind < 3 {
order.rotate_left(variant % vars.max(1));
if variant & 1 == 1 {
order.reverse();
}
}
let mut rng = Rng(formula_seed ^ (strategy as u64 + 1).wrapping_mul(0xd6e8_feb8_6659_fd93));
let mut groups = Vec::new();
let mut cursor = 0usize;
while cursor < vars {
let remaining = vars - cursor;
let size = (1 + rng.below(max_group)).min(remaining);
groups.push(order[cursor..cursor + size].to_vec());
cursor += size;
}
let weights: Vec<_> = groups.iter().map(Vec::len).collect();
let graph = quotient_graph(vars, &formula, &groups);
let unweighted = exact_weighted_treewidth(&vec![1; groups.len()], &graph);
let weighted = exact_weighted_treewidth(&weights, &graph);
let (assignment_roundtrip, witness_valid) = if let Some(assignment) = &witness {
let encoded: Vec<usize> = groups
.iter()
.map(|members| {
members
.iter()
.enumerate()
.fold(0usize, |bits, (bit, &variable)| {
bits | ((assignment[variable] as usize) << bit)
})
})
.collect();
let mut decoded = vec![false; vars];
for (group, members) in groups.iter().enumerate() {
for (bit, &variable) in members.iter().enumerate() {
decoded[variable] = encoded[group] & (1usize << bit) != 0;
}
}
(decoded == *assignment, satisfies(&formula, &decoded))
} else {
(true, true)
};
writeln!(file, "{family},{formula_seed},{},{},{max_group},{},{},{original_width},{unweighted},{weighted},{},{},{assignment_roundtrip},{witness_valid}", strategy + 1, ["natural", "min-fill", "min-degree", "random"][order_kind], groups.len(), weights.iter().copied().max().unwrap_or(0), weighted as isize - original_width as isize, weights.iter().sum::<usize>())
.map_err(|error| format!("write grouping row: {error}"))?;
}
file.flush()
.map_err(|error| format!("flush grouping output: {error}"))?;
println!(
"finite-domain grouping family={family} seed={formula_seed} strategies={strategies} original_width={original_width} output={}",
output.display()
);
Ok(())
}
fn invert_binary_matrix(rows: &[u32], vars: usize) -> Option<Vec<u32>> {
let mut augmented: Vec<u64> = rows
.iter()
.enumerate()
.map(|(index, &row)| row as u64 | (1u64 << (vars + index)))
.collect();
for column in 0..vars {
let pivot = (column..vars).find(|&row| augmented[row] & (1u64 << column) != 0)?;
augmented.swap(column, pivot);
for row in 0..vars {
if row != column && augmented[row] & (1u64 << column) != 0 {
augmented[row] ^= augmented[column];
}
}
}
Some(
augmented
.into_iter()
.map(|row| (row >> vars) as u32)
.collect(),
)
}
fn apply_binary_matrix(rows: &[u32], values: u32) -> u32 {
rows.iter().enumerate().fold(0u32, |result, (index, &row)| {
result | (((row & values).count_ones() & 1) << index)
})
}
fn benchmark_affine_basis_strategies(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
strategies: usize,
output: &Path,
) -> Result<(), String> {
if vars > 20 {
return Err("affine exact-width search supports at most 20 variables".to_string());
}
let formula = generate_formula(family, vars, ratio, formula_seed);
let original_width = exact_treewidth(vars, &formula);
let witness = solve_with_varisat(vars, &formula);
let witness_bits = witness.as_ref().map(|assignment| {
assignment
.iter()
.enumerate()
.fold(0u32, |bits, (index, &value)| {
bits | ((value as u32) << index)
})
});
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create affine output: {error}"))?;
}
let mut file =
fs::File::create(output).map_err(|error| format!("create affine output: {error}"))?;
writeln!(file, "family,formula_seed,strategy,row_operations,offset_weight,matrix_ones,original_width,min_fill_upper,screened_width,exact_checked,width_change,max_factor_arity,sum_factor_log2_entries,assignment_roundtrip,witness_valid")
.map_err(|error| format!("write affine header: {error}"))?;
let mut exact_cache: HashMap<Vec<Vec<usize>>, usize> = HashMap::new();
for strategy in 0..strategies {
let operations = strategy % 33;
let mut rng = Rng(formula_seed ^ (strategy as u64 + 1).wrapping_mul(0xa076_1d64_78bd_642f));
let mut matrix: Vec<u32> = (0..vars).map(|index| 1u32 << index).collect();
for _ in 0..operations {
let target = rng.below(vars);
let mut source = rng.below(vars - 1);
if source >= target {
source += 1;
}
matrix[target] ^= matrix[source];
}
let offset = rng.next() as u32 & ((1u32 << vars) - 1);
let mut transformed = Vec::with_capacity(formula.len());
let mut max_arity = 0usize;
let mut table_log2_entries = 0usize;
for clause in &formula {
let scope_mask = clause
.0
.iter()
.fold(0u32, |mask, &(variable, _)| mask | matrix[variable]);
let scope: Vec<_> = (0..vars)
.filter(|&variable| scope_mask & (1u32 << variable) != 0)
.collect();
max_arity = max_arity.max(scope.len());
table_log2_entries = table_log2_entries.saturating_add(scope.len());
transformed.push(Clause(
scope.into_iter().map(|variable| (variable, true)).collect(),
));
}
let upper = elimination_cost(vars, &transformed, &min_fill_order(vars, &transformed)).0;
let graph_key: Vec<Vec<usize>> = primal_graph(vars, &transformed)
.into_iter()
.map(|neighbors| neighbors.into_iter().collect())
.collect();
let exact = if upper <= original_width {
if let Some(&cached) = exact_cache.get(&graph_key) {
cached
} else {
let width = exact_treewidth(vars, &transformed);
exact_cache.insert(graph_key, width);
width
}
} else {
upper
};
let exact_checked = upper <= original_width;
let (roundtrip, witness_valid) = if let (Some(bits), Some(inverse)) =
(witness_bits, invert_binary_matrix(&matrix, vars))
{
let coordinates = apply_binary_matrix(&inverse, bits ^ offset);
let reconstructed = apply_binary_matrix(&matrix, coordinates) ^ offset;
let assignment: Vec<_> = (0..vars)
.map(|index| reconstructed & (1u32 << index) != 0)
.collect();
(reconstructed == bits, satisfies(&formula, &assignment))
} else {
(witness.is_none(), witness.is_none())
};
writeln!(file, "{family},{formula_seed},{},{operations},{},{},{original_width},{upper},{exact},{exact_checked},{},{max_arity},{table_log2_entries},{roundtrip},{witness_valid}", strategy + 1, offset.count_ones(), matrix.iter().map(|row| row.count_ones() as usize).sum::<usize>(), exact as isize - original_width as isize)
.map_err(|error| format!("write affine row: {error}"))?;
}
file.flush()
.map_err(|error| format!("flush affine output: {error}"))?;
println!(
"affine basis search family={family} seed={formula_seed} strategies={strategies} original_width={original_width} exact_graphs={} output={}",
exact_cache.len(),
output.display()
);
Ok(())
}
fn tensor_flatten_rank(values: &[i64; 8], axis: usize) -> usize {
let mut rows = [[0i64; 4]; 2];
for bits in 0..8 {
let coordinates = [bits & 1, (bits >> 1) & 1, (bits >> 2) & 1];
let row = coordinates[axis];
let mut column = 0usize;
for other in 0..3 {
if other != axis {
column = (column << 1) | coordinates[other];
}
}
rows[row][column] = values[bits];
}
if rows.iter().all(|row| row.iter().all(|&value| value == 0)) {
return 0;
}
let dependent = (0..4).all(|left| {
(0..4).all(|right| rows[0][left] * rows[1][right] == rows[0][right] * rows[1][left])
});
if dependent { 1 } else { 2 }
}
fn benchmark_holographic_tensor_strategies(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
strategies: usize,
output: &Path,
) -> Result<(), String> {
let formula = generate_formula(family, vars, ratio, formula_seed);
let width = if vars <= 20 {
exact_treewidth(vars, &formula)
} else {
elimination_cost(vars, &formula, &min_fill_order(vars, &formula)).0
};
let bases: Vec<[[i64; 2]; 2]> = vec![
[[1, 0], [0, 1]],
[[0, 1], [1, 0]],
[[1, 0], [0, -1]],
[[-1, 0], [0, 1]],
[[1, 1], [1, -1]],
[[1, -1], [1, 1]],
[[1, 1], [0, 1]],
[[1, 0], [1, 1]],
[[1, -1], [0, 1]],
[[1, 0], [-1, 1]],
[[0, 1], [-1, 0]],
[[0, -1], [1, 0]],
];
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create tensor output: {error}"))?;
}
let mut file =
fs::File::create(output).map_err(|error| format!("create tensor output: {error}"))?;
writeln!(file, "family,formula_seed,strategy,bases_used,treewidth,bond_bits,total_nonzeros,max_clause_nonzeros,rank1_flattenings,rank2_flattenings,clauses_with_rank_drop,all_bases_invertible")
.map_err(|error| format!("write tensor header: {error}"))?;
for strategy in 0..strategies {
let mut rng = Rng(formula_seed ^ (strategy as u64 + 1).wrapping_mul(0x94d0_49bb_1331_11eb));
let selected: Vec<_> = (0..vars).map(|_| rng.below(bases.len())).collect();
let mut total_nonzeros = 0usize;
let mut max_nonzeros = 0usize;
let mut rank1 = 0usize;
let mut rank2 = 0usize;
let mut rank_drop_clauses = 0usize;
for clause in &formula {
let mut tensor = [0i64; 8];
for output_bits in 0..8 {
let mut value = 0i64;
for input_bits in 0..8 {
let satisfies_clause =
clause.0.iter().enumerate().any(|(position, &(_, sign))| {
(input_bits & (1usize << position) != 0) == sign
});
if !satisfies_clause {
continue;
}
let mut coefficient = 1i64;
for position in 0..3 {
let input = (input_bits >> position) & 1;
let output = (output_bits >> position) & 1;
coefficient *= bases[selected[clause.0[position].0]][input][output];
}
value += coefficient;
}
tensor[output_bits] = value;
}
let nonzeros = tensor.iter().filter(|&&value| value != 0).count();
total_nonzeros += nonzeros;
max_nonzeros = max_nonzeros.max(nonzeros);
let ranks = [
tensor_flatten_rank(&tensor, 0),
tensor_flatten_rank(&tensor, 1),
tensor_flatten_rank(&tensor, 2),
];
rank1 += ranks.iter().filter(|&&rank| rank == 1).count();
rank2 += ranks.iter().filter(|&&rank| rank == 2).count();
rank_drop_clauses += usize::from(ranks.iter().any(|&rank| rank < 2));
}
let all_invertible = selected.iter().all(|&index| {
let basis = bases[index];
basis[0][0] * basis[1][1] - basis[0][1] * basis[1][0] != 0
});
let bases_used = selected.iter().copied().collect::<BTreeSet<_>>().len();
writeln!(file, "{family},{formula_seed},{},{bases_used},{width},1,{total_nonzeros},{max_nonzeros},{rank1},{rank2},{rank_drop_clauses},{all_invertible}", strategy + 1)
.map_err(|error| format!("write tensor row: {error}"))?;
}
file.flush()
.map_err(|error| format!("flush tensor output: {error}"))?;
println!(
"holographic tensor search family={family} seed={formula_seed} strategies={strategies} width={width} output={}",
output.display()
);
Ok(())
}
fn benchmark_holographic_network_cost(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
strategy: usize,
output: &Path,
) -> Result<(), String> {
let formula = generate_formula(family, vars, ratio, formula_seed);
let bases: Vec<[[f64; 2]; 2]> = vec![
[[1.0, 0.0], [0.0, 1.0]],
[[0.0, 1.0], [1.0, 0.0]],
[[1.0, 0.0], [0.0, -1.0]],
[[-1.0, 0.0], [0.0, 1.0]],
[[1.0, 1.0], [1.0, -1.0]],
[[1.0, -1.0], [1.0, 1.0]],
[[1.0, 1.0], [0.0, 1.0]],
[[1.0, 0.0], [1.0, 1.0]],
[[1.0, -1.0], [0.0, 1.0]],
[[1.0, 0.0], [-1.0, 1.0]],
[[0.0, 1.0], [-1.0, 0.0]],
[[0.0, -1.0], [1.0, 0.0]],
];
let mut rng = Rng(formula_seed ^ (strategy as u64).wrapping_mul(0x94d0_49bb_1331_11eb));
let selected: Vec<_> = (0..vars).map(|_| rng.below(bases.len())).collect();
let mut degrees = vec![0usize; vars];
let mut clause_nonzeros = 0usize;
for clause in &formula {
let mut tensor = [0.0f64; 8];
for &(variable, _) in &clause.0 {
degrees[variable] += 1;
}
for output_bits in 0..8 {
for input_bits in 0..8 {
if !clause
.0
.iter()
.enumerate()
.any(|(position, &(_, sign))| (input_bits & (1usize << position) != 0) == sign)
{
continue;
}
let mut coefficient = 1.0;
for position in 0..3 {
let input = (input_bits >> position) & 1;
let output = (output_bits >> position) & 1;
coefficient *= bases[selected[clause.0[position].0]][input][output];
}
tensor[output_bits] += coefficient;
}
}
clause_nonzeros += tensor.iter().filter(|&&value| value.abs() > 1e-12).count();
}
let mut equality_nonzeros = 0usize;
let mut equality_dense_entries = 0usize;
let mut max_equality_nonzeros = 0usize;
for variable in 0..vars {
let degree = degrees[variable];
if degree >= usize::BITS as usize {
return Err("equality tensor degree exceeds addressable table".to_string());
}
let basis = bases[selected[variable]];
let determinant = basis[0][0] * basis[1][1] - basis[0][1] * basis[1][0];
let inverse = [
[basis[1][1] / determinant, -basis[0][1] / determinant],
[-basis[1][0] / determinant, basis[0][0] / determinant],
];
let entries = 1usize << degree;
equality_dense_entries = equality_dense_entries.saturating_add(entries);
let mut nonzeros = 0usize;
for output_bits in 0..entries {
let mut value = 0.0;
for original_value in 0..2 {
let mut product = 1.0;
for edge in 0..degree {
let output = (output_bits >> edge) & 1;
product *= inverse[output][original_value];
}
value += product;
}
nonzeros += usize::from(value.abs() > 1e-12);
}
equality_nonzeros += nonzeros;
max_equality_nonzeros = max_equality_nonzeros.max(nonzeros);
}
let baseline_clause_nonzeros = formula.len() * 7;
let baseline_equality_nonzeros = vars * 2;
let baseline_total = baseline_clause_nonzeros + baseline_equality_nonzeros;
let transformed_total = clause_nonzeros + equality_nonzeros;
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create network cost output: {error}"))?;
}
fs::write(
output,
format!("family,formula_seed,strategy,clauses,variables,baseline_clause_nonzeros,transformed_clause_nonzeros,baseline_equality_nonzeros,transformed_equality_nonzeros,equality_dense_entries,max_equality_nonzeros,baseline_total_nonzeros,transformed_total_nonzeros,total_ratio\n{family},{formula_seed},{strategy},{},{vars},{baseline_clause_nonzeros},{clause_nonzeros},{baseline_equality_nonzeros},{equality_nonzeros},{equality_dense_entries},{max_equality_nonzeros},{baseline_total},{transformed_total},{:.6}\n", formula.len(), transformed_total as f64 / baseline_total.max(1) as f64),
)
.map_err(|error| format!("write network cost output: {error}"))?;
println!(
"holographic network cost family={family} seed={formula_seed} strategy={strategy} clause_nonzeros={clause_nonzeros} equality_nonzeros={equality_nonzeros} total_ratio={:.6}",
transformed_total as f64 / baseline_total.max(1) as f64
);
Ok(())
}
fn canonical_residual_after_choice(
residual: &[Vec<Literal>],
variable: usize,
value: bool,
work: &mut usize,
) -> Vec<Vec<Literal>> {
let mut next = Vec::new();
for clause in residual {
*work = work.saturating_add(clause.len());
if clause
.iter()
.any(|&(candidate, sign)| candidate == variable && sign == value)
{
continue;
}
let mut reduced: Vec<_> = clause
.iter()
.copied()
.filter(|&(candidate, _)| candidate != variable)
.collect();
reduced.sort_unstable();
reduced.dedup();
if reduced.is_empty() {
return vec![Vec::new()];
}
next.push(reduced);
}
next.sort_unstable();
next.dedup();
next
}
fn continuation_frontier_profile(vars: usize, formula: &[Clause], order: &[usize]) -> Vec<u128> {
let mut position = vec![0usize; vars];
for (index, &variable) in order.iter().enumerate() {
position[variable] = index;
}
let mut profile = Vec::with_capacity(vars.saturating_add(1));
profile.push(1);
for cut in 1..vars {
let mut crossing_clauses = 0usize;
let mut past_boundary = BTreeSet::new();
for clause in formula {
let crosses = clause
.0
.iter()
.any(|&(variable, _)| position[variable] < cut)
&& clause
.0
.iter()
.any(|&(variable, _)| position[variable] >= cut);
if crosses {
crossing_clauses += 1;
past_boundary.extend(
clause.0.iter().filter_map(|&(variable, _)| {
(position[variable] < cut).then_some(variable)
}),
);
}
}
let bits = crossing_clauses.min(past_boundary.len());
profile.push(
1u128
.checked_shl(bits.min(127) as u32)
.unwrap_or(u128::MAX)
.saturating_add(1),
);
}
profile.push(2);
profile
}
fn ceil_log2_u128(value: u128) -> usize {
if value <= 1 {
0
} else {
(u128::BITS - (value - 1).leading_zeros()) as usize
}
}
fn continuation_frontier_bound_bits(vars: usize, formula: &[Clause], order: &[usize]) -> usize {
let maximum_states = continuation_frontier_profile(vars, formula, order)
.into_iter()
.max()
.unwrap_or(1);
ceil_log2_u128(maximum_states)
}
struct CompiledContinuation {
order: Vec<usize>,
transitions: Vec<Vec<[usize; 2]>>,
residual_layers: Vec<Vec<Vec<Vec<Literal>>>>,
terminal_sat: Vec<bool>,
peak_classes: usize,
}
struct ContinuationScratch {
reachable: Vec<bool>,
next: Vec<bool>,
parents: Vec<Vec<Option<(usize, bool)>>>,
}
impl ContinuationScratch {
fn new(compiled: &CompiledContinuation) -> Self {
let parents = (0..compiled.order.len())
.map(|layer_index| {
let size = if layer_index + 1 < compiled.transitions.len() {
compiled.transitions[layer_index + 1].len()
} else {
compiled.terminal_sat.len()
};
vec![None; size]
})
.collect();
Self {
reachable: vec![false; compiled.peak_classes],
next: vec![false; compiled.peak_classes],
parents,
}
}
}
fn compile_continuation(formula: &[Clause], order: &[usize]) -> CompiledContinuation {
let mut base: Vec<Vec<Literal>> = formula
.iter()
.map(|clause| {
let mut literals = clause.0.clone();
literals.sort_unstable();
literals.dedup();
literals
})
.collect();
base.sort_unstable();
base.dedup();
let mut current = vec![base];
let mut transitions = Vec::with_capacity(order.len());
let mut residual_layers = vec![current.clone()];
let mut peak_classes = 1usize;
let mut work = 0usize;
for &variable in order {
let mut next_ids = HashMap::new();
let mut next_residuals = Vec::new();
let mut layer = vec![[0usize; 2]; current.len()];
for (state, residual) in current.iter().enumerate() {
for value in [false, true] {
let canonical =
canonical_residual_after_choice(residual, variable, value, &mut work);
let next = if let Some(&existing) = next_ids.get(&canonical) {
existing
} else {
let id = next_residuals.len();
next_ids.insert(canonical.clone(), id);
next_residuals.push(canonical);
id
};
layer[state][value as usize] = next;
}
}
transitions.push(layer);
current = next_residuals;
residual_layers.push(current.clone());
peak_classes = peak_classes.max(current.len());
}
let terminal_sat = current.iter().map(Vec::is_empty).collect();
CompiledContinuation {
order: order.to_vec(),
transitions,
residual_layers,
terminal_sat,
peak_classes,
}
}
fn apply_clause_change_to_residual(
old: &[Vec<Literal>],
changed: &[Literal],
insertion: bool,
) -> Vec<Vec<Literal>> {
let mut residual = old.to_vec();
if residual != vec![Vec::new()] {
if insertion {
residual.push(changed.to_vec());
} else if let Some(index) = residual.iter().position(|clause| clause == changed) {
residual.remove(index);
}
residual.sort_unstable();
residual.dedup();
}
residual
}
fn repair_continuation(
compiled: &CompiledContinuation,
changed_clause: &Clause,
insertion: bool,
) -> CompiledContinuation {
let mut position = vec![0usize; compiled.order.len()];
for (index, &variable) in compiled.order.iter().enumerate() {
position[variable] = index;
}
// Canonical residuals intentionally discard duplicate clauses. After
// substitution, distinct source clauses can collapse to the same residual,
// so deleting one source clause from a suffix requires provenance counts.
// Until those are retained, deletion safely rebuilds from the root.
let start = if insertion {
changed_clause
.0
.iter()
.map(|&(variable, _)| position[variable])
.min()
.unwrap_or(0)
} else {
0
};
let mut changed = changed_clause.0.clone();
changed.sort_unstable();
changed.dedup();
let mut boundary_ids = HashMap::new();
let mut boundary = Vec::new();
let mut remap = Vec::with_capacity(compiled.residual_layers[start].len());
for old in &compiled.residual_layers[start] {
let residual = apply_clause_change_to_residual(old, &changed, insertion);
let id = if let Some(&existing) = boundary_ids.get(&residual) {
existing
} else {
let id = boundary.len();
boundary_ids.insert(residual.clone(), id);
boundary.push(residual);
id
};
remap.push(id);
}
let mut transitions = compiled.transitions[..start].to_vec();
if start > 0 {
for targets in &mut transitions[start - 1] {
targets[0] = remap[targets[0]];
targets[1] = remap[targets[1]];
}
}
let mut residual_layers: Vec<_> = compiled.residual_layers[..start]
.iter()
.map(|layer| {
layer
.iter()
.map(|residual| apply_clause_change_to_residual(residual, &changed, insertion))
.collect()
})
.collect();
residual_layers.push(boundary.clone());
let mut current = boundary;
let mut peak_classes = residual_layers.iter().map(Vec::len).max().unwrap_or(1);
let mut work = 0usize;
for &variable in &compiled.order[start..] {
let mut next_ids = HashMap::new();
let mut next_residuals = Vec::new();
let mut layer = vec![[0usize; 2]; current.len()];
for (state, residual) in current.iter().enumerate() {
for value in [false, true] {
let canonical =
canonical_residual_after_choice(residual, variable, value, &mut work);
let next = if let Some(&existing) = next_ids.get(&canonical) {
existing
} else {
let id = next_residuals.len();
next_ids.insert(canonical.clone(), id);
next_residuals.push(canonical);
id
};
layer[state][value as usize] = next;
}
}
transitions.push(layer);
current = next_residuals;
residual_layers.push(current.clone());
peak_classes = peak_classes.max(current.len());
}
let terminal_sat = current.iter().map(Vec::is_empty).collect();
CompiledContinuation {
order: compiled.order.clone(),
transitions,
residual_layers,
terminal_sat,
peak_classes,
}
}
fn query_continuation(
compiled: &CompiledContinuation,
assumptions: &[Option<bool>],
scratch: &mut ContinuationScratch,
) -> Option<Vec<bool>> {
scratch.reachable.fill(false);
scratch.reachable[0] = true;
let mut current_len = 1usize;
for (layer_index, &variable) in compiled.order.iter().enumerate() {
let next_len = if layer_index + 1 < compiled.transitions.len() {
compiled.transitions[layer_index + 1].len()
} else {
compiled.terminal_sat.len()
};
scratch.next[..next_len].fill(false);
scratch.parents[layer_index].fill(None);
for state in 0..current_len {
let is_reachable = scratch.reachable[state];
if !is_reachable {
continue;
}
for value in [false, true] {
if assumptions[variable].is_some_and(|required| required != value) {
continue;
}
let target = compiled.transitions[layer_index][state][value as usize];
if !scratch.next[target] {
scratch.next[target] = true;
scratch.parents[layer_index][target] = Some((state, value));
}
}
}
std::mem::swap(&mut scratch.reachable, &mut scratch.next);
current_len = next_len;
}
let mut state = scratch.reachable[..current_len]
.iter()
.zip(&compiled.terminal_sat)
.position(|(&is_reachable, &is_sat)| is_reachable && is_sat)?;
let mut assignment = vec![false; assumptions.len()];
for layer_index in (0..compiled.order.len()).rev() {
let (previous, value) = scratch.parents[layer_index][state]?;
assignment[compiled.order[layer_index]] = value;
state = previous;
}
Some(assignment)
}
fn benchmark_continuation_reuse(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
query_count: usize,
max_assumptions: usize,
output: &Path,
) -> Result<(), String> {
let formula = generate_formula(family, vars, ratio, formula_seed);
let order: Vec<_> = (0..vars).collect();
let bound_bits = continuation_frontier_bound_bits(vars, &formula, &order);
if bound_bits > 16 {
return Err(format!(
"continuation reuse rejected by 16-bit gate: bound is {bound_bits} bits"
));
}
let compile_start = Instant::now();
let compiled = compile_continuation(&formula, &order);
let compile_ns = compile_start.elapsed().as_nanos();
let mut rng = Rng(formula_seed ^ 0xa076_1d64_78bd_642f);
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
let width = 1 + query_index % max_assumptions.max(1);
let mut chosen = BTreeSet::new();
while chosen.len() < width.min(vars) {
chosen.insert(rng.below(vars));
}
for variable in chosen {
assumptions[variable] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let quotient_start = Instant::now();
let mut scratch = ContinuationScratch::new(&compiled);
let mut quotient_answers = Vec::with_capacity(queries.len());
for assumptions in &queries {
quotient_answers.push(query_continuation(&compiled, assumptions, &mut scratch));
}
let quotient_query_ns = quotient_start.elapsed().as_nanos();
let varisat_start = Instant::now();
let varisat_answers: Vec<_> = queries
.iter()
.map(|assumptions| {
let mut queried = formula.clone();
queried.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Clause(vec![(variable, value)]))
}),
);
solve_with_varisat(vars, &queried)
})
.collect();
let varisat_query_ns = varisat_start.elapsed().as_nanos();
let mut incremental_solver = Solver::new();
add_to_varisat(&mut incremental_solver, &formula);
let incremental_start = Instant::now();
let incremental_answers: Vec<Option<Vec<bool>>> = queries
.iter()
.map(|assumptions| {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
incremental_solver.assume(&literals);
let sat = incremental_solver
.solve()
.expect("incremental Varisat solve");
if !sat {
return None;
}
let mut assignment = vec![false; vars];
for literal in incremental_solver
.model()
.expect("incremental Varisat model")
{
if literal.var().index() < vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
})
.collect();
let incremental_query_ns = incremental_start.elapsed().as_nanos();
let agreement = quotient_answers
.iter()
.zip(&varisat_answers)
.all(|(left, right)| left.is_some() == right.is_some());
let sat_queries = quotient_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count.saturating_sub(sat_queries);
let incremental_agreement = quotient_answers
.iter()
.zip(&incremental_answers)
.all(|(left, right)| left.is_some() == right.is_some());
let incremental_witnesses_valid =
incremental_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let witnesses_valid = quotient_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let quotient_per_query = quotient_query_ns as f64 / query_count.max(1) as f64;
let varisat_per_query = varisat_query_ns as f64 / query_count.max(1) as f64;
let incremental_per_query = incremental_query_ns as f64 / query_count.max(1) as f64;
let break_even_queries = if varisat_per_query > quotient_per_query {
(compile_ns as f64 / (varisat_per_query - quotient_per_query)).ceil() as u128
} else {
u128::MAX
};
let incremental_break_even_queries = if incremental_per_query > quotient_per_query {
(compile_ns as f64 / (incremental_per_query - quotient_per_query)).ceil() as u128
} else {
u128::MAX
};
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create reuse output: {error}"))?;
}
fs::write(
output,
format!("family,formula_seed,variables,queries,max_assumptions,sat_queries,unsat_queries,frontier_bound_bits,peak_classes,compile_ns,quotient_query_ns,fresh_varisat_query_ns,incremental_varisat_query_ns,quotient_ns_per_query,fresh_varisat_ns_per_query,incremental_varisat_ns_per_query,speedup_vs_fresh,speedup_vs_incremental,break_even_fresh_queries,break_even_incremental_queries,agreement,incremental_agreement,witnesses_valid,incremental_witnesses_valid\n{family},{formula_seed},{vars},{query_count},{max_assumptions},{sat_queries},{unsat_queries},{bound_bits},{},{compile_ns},{quotient_query_ns},{varisat_query_ns},{incremental_query_ns},{quotient_per_query:.3},{varisat_per_query:.3},{incremental_per_query:.3},{:.6},{:.6},{break_even_queries},{incremental_break_even_queries},{agreement},{incremental_agreement},{witnesses_valid},{incremental_witnesses_valid}\n", compiled.peak_classes, varisat_per_query / quotient_per_query.max(1.0), incremental_per_query / quotient_per_query.max(1.0)),
)
.map_err(|error| format!("write reuse output: {error}"))?;
println!(
"continuation reuse family={family} seed={formula_seed} vars={vars} queries={query_count} max_assumptions={max_assumptions} sat={sat_queries} unsat={unsat_queries} peak={} agreement={agreement} incremental_agreement={incremental_agreement} witnesses_valid={witnesses_valid} incremental_witnesses_valid={incremental_witnesses_valid} output={}",
compiled.peak_classes,
output.display()
);
Ok(())
}
fn temporal_memory_formula(width: usize, horizon: usize) -> (usize, Vec<Clause>) {
let vars = width * (horizon + 1);
let mut formula = Vec::with_capacity(2 * width * horizon);
for time in 0..horizon {
for bit in 0..width {
let current = time * width + bit;
let next = (time + 1) * width + bit;
// next == current
formula.push(Clause(vec![(current, false), (next, true)]));
formula.push(Clause(vec![(current, true), (next, false)]));
}
}
(vars, formula)
}
fn compile_temporal_memory_continuation(width: usize, horizon: usize) -> CompiledContinuation {
assert!(width < usize::BITS as usize);
let vars = width * (horizon + 1);
let order: Vec<_> = (0..vars).collect();
let live_states = 1usize << width;
let contradiction = live_states;
let mut transitions = Vec::with_capacity(vars);
// The first frame chooses the remembered state.
for bit in 0..width {
let states = 1usize << bit;
let mut layer = Vec::with_capacity(states);
for state in 0..states {
layer.push([state, state | (1usize << bit)]);
}
transitions.push(layer);
}
// Every later frame must reproduce it. A mismatching observation enters the
// unique contradictory continuation, which remains contradictory forever.
for _time in 1..=horizon {
for bit in 0..width {
let mut layer = Vec::with_capacity(live_states + 1);
for state in 0..live_states {
let required = (state >> bit) & 1;
layer.push(if required == 0 {
[state, contradiction]
} else {
[contradiction, state]
});
}
layer.push([contradiction, contradiction]);
transitions.push(layer);
}
}
let mut terminal_sat = vec![true; live_states + 1];
terminal_sat[contradiction] = false;
CompiledContinuation {
order,
transitions,
residual_layers: Vec::new(),
terminal_sat,
peak_classes: live_states + 1,
}
}
fn query_temporal_memory_kernel(
width: usize,
horizon: usize,
assumptions: &[Option<bool>],
) -> Option<Vec<bool>> {
let vars = width * (horizon + 1);
debug_assert_eq!(assumptions.len(), vars);
let mut state = vec![None; width];
for (variable, required) in assumptions.iter().enumerate() {
let Some(value) = required else {
continue;
};
let bit = variable % width;
if state[bit].is_some_and(|known| known != *value) {
return None;
}
state[bit] = Some(*value);
}
let state: Vec<_> = state
.into_iter()
.map(|value| value.unwrap_or(false))
.collect();
let mut assignment = Vec::with_capacity(vars);
for _ in 0..=horizon {
assignment.extend_from_slice(&state);
}
Some(assignment)
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum TemporalRule {
Copy(usize),
Negate(usize),
Xor(usize, usize),
Circuit(usize, usize, usize),
}
impl TemporalRule {
fn dependencies(&self) -> Vec<usize> {
let mut dependencies = match *self {
Self::Copy(a) | Self::Negate(a) => vec![a],
Self::Xor(a, b) => vec![a, b],
Self::Circuit(a, b, c) => vec![a, b, c],
};
dependencies.sort_unstable();
dependencies.dedup();
dependencies
}
fn evaluate(&self, state: usize) -> bool {
let bit = |index: usize| (state >> index) & 1 == 1;
match *self {
Self::Copy(a) => bit(a),
Self::Negate(a) => !bit(a),
Self::Xor(a, b) => bit(a) ^ bit(b),
Self::Circuit(a, b, c) => (bit(a) & bit(b)) ^ bit(c),
}
}
}
fn temporal_rules(kind: &str, width: usize) -> Result<Vec<TemporalRule>, String> {
if width < 3 && kind == "circuit" {
return Err("circuit transition requires width at least 3".to_string());
}
(0..width)
.map(|bit| match kind {
"copy" => Ok(TemporalRule::Copy(bit)),
"negate" => Ok(TemporalRule::Negate(bit)),
"permute" => Ok(TemporalRule::Copy((bit + 1) % width)),
"xor" => Ok(TemporalRule::Xor(bit, (bit + 1) % width)),
"circuit" => Ok(TemporalRule::Circuit(
bit,
(bit + 1) % width,
(bit + width - 1) % width,
)),
_ => Err(format!("unknown temporal transition kind: {kind}")),
})
.collect()
}
fn temporal_rule_clauses(
rule: &TemporalRule,
output: usize,
current_offset: usize,
next_offset: usize,
) -> Vec<Clause> {
let dependencies = rule.dependencies();
let mut clauses = Vec::with_capacity(1usize << dependencies.len());
for pattern in 0..(1usize << dependencies.len()) {
let mut state = 0usize;
let mut literals = Vec::with_capacity(dependencies.len() + 1);
for (position, &dependency) in dependencies.iter().enumerate() {
let value = (pattern >> position) & 1 == 1;
if value {
state |= 1usize << dependency;
}
literals.push((current_offset + dependency, !value));
}
literals.push((next_offset + output, rule.evaluate(state)));
literals.sort_unstable();
clauses.push(Clause(literals));
}
clauses
}
fn temporal_vocabulary_formula(
kind: &str,
width: usize,
horizon: usize,
) -> Result<(usize, Vec<Clause>), String> {
let rules = temporal_rules(kind, width)?;
let mut formula = Vec::new();
for time in 0..horizon {
let current = time * width;
let next = (time + 1) * width;
for (output, rule) in rules.iter().enumerate() {
formula.extend(temporal_rule_clauses(rule, output, current, next));
}
}
Ok((width * (horizon + 1), formula))
}
fn composed_transition_dependencies(
kind: &str,
width: usize,
output: usize,
) -> Result<Vec<usize>, String> {
if width < 4 {
return Err("composed transitions require width at least 4".to_string());
}
let count = if matches!(kind, "cascade4" | "watchdog4") {
4
} else {
3
};
match kind {
"majority3" | "sensor-vote3" | "mux3" | "mixed3" | "cascade4" | "watchdog4" => {
Ok((0..count).map(|offset| (output + offset) % width).collect())
}
"hub3" => {
let mut dependencies = vec![output, 0, (output + 1) % width];
dependencies.dedup();
if dependencies.len() < 3 {
for candidate in 0..width {
if !dependencies.contains(&candidate) {
dependencies.push(candidate);
}
if dependencies.len() == 3 {
break;
}
}
}
Ok(dependencies)
}
"tree3" | "irregular3" => {
let preferred = if kind == "tree3" {
vec![
output,
output.saturating_sub(1) / 2,
(2 * output + 1) % width,
]
} else {
vec![output, (output * 3 + 1) % width, (output * 5 + 2) % width]
};
let mut dependencies = Vec::with_capacity(3);
for candidate in preferred.into_iter().chain(0..width) {
if !dependencies.contains(&candidate) {
dependencies.push(candidate);
}
if dependencies.len() == 3 {
break;
}
}
Ok(dependencies)
}
_ => Err(format!("unknown composed transition kind: {kind}")),
}
}
fn evaluate_composed_transition(kind: &str, values: &[bool]) -> bool {
match kind {
"majority3" | "sensor-vote3" => {
(values[0] & values[1]) | (values[0] & values[2]) | (values[1] & values[2])
}
"mux3" => {
if values[0] {
values[1]
} else {
values[2]
}
}
"mixed3" => (values[0] ^ values[1]) & !values[2],
"cascade4" | "watchdog4" => (values[0] ^ values[1]) ^ (values[2] & values[3]),
"hub3" => {
if values[0] {
values[1]
} else {
values[2]
}
}
"tree3" => (values[0] & values[1]) | (values[0] & values[2]) | (values[1] & values[2]),
"irregular3" => (values[0] ^ values[1]) & !values[2],
_ => unreachable!("validated composed transition kind"),
}
}
fn temporal_composition_formula(
kind: &str,
width: usize,
horizon: usize,
) -> Result<(usize, Vec<Clause>), String> {
let mut formula = Vec::new();
for time in 0..horizon {
let current = time * width;
let next = (time + 1) * width;
for output in 0..width {
let dependencies = composed_transition_dependencies(kind, width, output)?;
for pattern in 0..(1usize << dependencies.len()) {
let mut values = Vec::with_capacity(dependencies.len());
let mut literals = Vec::with_capacity(dependencies.len() + 1);
for (position, &dependency) in dependencies.iter().enumerate() {
let value = (pattern >> position) & 1 == 1;
values.push(value);
literals.push((current + dependency, !value));
}
literals.push((next + output, evaluate_composed_transition(kind, &values)));
literals.sort_unstable();
formula.push(Clause(literals));
}
}
}
Ok((width * (horizon + 1), formula))
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct AagLatch {
current: usize,
next: usize,
initial: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct AagAnd {
output: usize,
left: usize,
right: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct AagModel {
max_variable: usize,
inputs: Vec<usize>,
input_names: Vec<String>,
latches: Vec<AagLatch>,
latch_names: Vec<String>,
outputs: Vec<usize>,
output_names: Vec<String>,
ands: Vec<AagAnd>,
}
type AagTemporalEncoding = (usize, Vec<Clause>, Vec<Option<bool>>);
type AagPropertyQuery = (usize, usize, Vec<Option<bool>>);
fn parse_aag_usize(token: Option<&str>, context: &str) -> Result<usize, String> {
token
.ok_or_else(|| format!("missing {context}"))?
.parse::<usize>()
.map_err(|_| format!("invalid {context}"))
}
fn parse_aag(path: &Path) -> Result<AagModel, String> {
let metadata =
fs::metadata(path).map_err(|error| format!("inspect AIGER {}: {error}", path.display()))?;
if !metadata.is_file() {
return Err(format!("AIGER input is not a file: {}", path.display()));
}
if metadata.len() > AAG_INPUT_LIMIT_BYTES {
return Err(format!(
"AIGER input exceeds safety limit {AAG_INPUT_LIMIT_BYTES} bytes"
));
}
let bytes =
fs::read(path).map_err(|error| format!("read AIGER {}: {error}", path.display()))?;
if bytes.len() as u64 > AAG_INPUT_LIMIT_BYTES {
return Err(format!(
"AIGER input exceeds safety limit {AAG_INPUT_LIMIT_BYTES} bytes"
));
}
parse_aiger_bytes(&bytes)
}
fn parse_aiger_bytes(bytes: &[u8]) -> Result<AagModel, String> {
if bytes.starts_with(b"aag ") {
parse_ascii_aiger(bytes)
} else if bytes.starts_with(b"aig ") {
parse_binary_aiger(bytes)
} else {
Err("only ASCII (`aag`) or binary (`aig`) AIGER input is supported".to_string())
}
}
fn parse_binary_aiger_delta(bytes: &[u8], cursor: &mut usize) -> Result<usize, String> {
let mut value = 0usize;
let mut shift = 0u32;
loop {
let byte = *bytes
.get(*cursor)
.ok_or_else(|| "truncated binary AIGER delta".to_string())?;
*cursor += 1;
let payload = usize::from(byte & 0x7f);
if payload > (usize::MAX >> shift) {
return Err("binary AIGER delta overflow".to_string());
}
let shifted = payload << shift;
value = value
.checked_add(shifted)
.ok_or_else(|| "binary AIGER delta overflow".to_string())?;
if byte & 0x80 == 0 {
return Ok(value);
}
shift = shift
.checked_add(7)
.ok_or_else(|| "binary AIGER delta overflow".to_string())?;
if shift >= usize::BITS {
return Err("binary AIGER delta overflow".to_string());
}
}
}
fn binary_aiger_line<'a>(
bytes: &'a [u8],
cursor: &mut usize,
context: &str,
) -> Result<&'a str, String> {
let remaining = bytes
.get(*cursor..)
.ok_or_else(|| format!("truncated binary AIGER {context}"))?;
let length = remaining
.iter()
.position(|byte| *byte == b'\n')
.ok_or_else(|| format!("unterminated binary AIGER {context}"))?;
let line = &remaining[..length];
*cursor = (*cursor)
.checked_add(length + 1)
.ok_or_else(|| "binary AIGER cursor overflow".to_string())?;
if line.contains(&b'\r') || !line.is_ascii() {
return Err(format!("invalid binary AIGER {context}"));
}
std::str::from_utf8(line).map_err(|_| format!("invalid binary AIGER {context}"))
}
fn parse_binary_aiger(bytes: &[u8]) -> Result<AagModel, String> {
let mut cursor = 0usize;
let header_line = binary_aiger_line(bytes, &mut cursor, "header")?;
let mut header = header_line.split_whitespace();
if header.next() != Some("aig") {
return Err("invalid binary AIGER header".to_string());
}
let max_variable = parse_aag_usize(header.next(), "AIGER maximum variable")?;
let input_count = parse_aag_usize(header.next(), "AIGER input count")?;
let latch_count = parse_aag_usize(header.next(), "AIGER latch count")?;
let output_count = parse_aag_usize(header.next(), "AIGER output count")?;
let and_count = parse_aag_usize(header.next(), "AIGER AND count")?;
if header.next().is_some() {
return Err("extended binary AIGER headers are not supported yet".to_string());
}
if max_variable > 1_000_000 {
return Err("AIGER maximum variable exceeds safety limit 1000000".to_string());
}
let definitions = input_count
.checked_add(latch_count)
.and_then(|value| value.checked_add(and_count))
.ok_or_else(|| "AIGER definition count overflow".to_string())?;
if definitions != max_variable {
return Err(format!(
"AIGER header requires M = I + L + A; found {max_variable} != {definitions}"
));
}
if latch_count == 0 {
return Err("AIGER model must contain at least one latch".to_string());
}
if output_count > 1_000_000 {
return Err("AIGER output count exceeds safety limit 1000000".to_string());
}
let estimated_ascii_bytes = definitions
.checked_add(output_count)
.and_then(|lines| lines.checked_add(1))
.and_then(|lines| lines.checked_mul(32))
.ok_or_else(|| "binary AIGER decoded-size estimate overflow".to_string())?;
let mut ascii = String::new();
ascii
.try_reserve(estimated_ascii_bytes)
.map_err(|_| "cannot allocate decoded binary AIGER buffer".to_string())?;
ascii.push_str(&format!(
"aag {max_variable} {input_count} {latch_count} {output_count} {and_count}\n"
));
for variable in 1..=input_count {
ascii.push_str(&format!("{}\n", variable * 2));
}
for index in 0..latch_count {
let line = binary_aiger_line(bytes, &mut cursor, "latch section")?;
let fields = line.split_whitespace().collect::<Vec<_>>();
if !(1..=2).contains(&fields.len()) {
return Err(format!("invalid binary AIGER latch {index}"));
}
let current = input_count
.checked_add(index + 1)
.and_then(|variable| variable.checked_mul(2))
.ok_or_else(|| "binary AIGER latch literal overflow".to_string())?;
ascii.push_str(¤t.to_string());
ascii.push(' ');
ascii.push_str(fields[0]);
if let Some(initial) = fields.get(1) {
ascii.push(' ');
ascii.push_str(initial);
}
ascii.push('\n');
}
for index in 0..output_count {
let line = binary_aiger_line(bytes, &mut cursor, "output section")?;
let mut fields = line.split_whitespace();
let literal = parse_aag_usize(fields.next(), "output literal")?;
if fields.next().is_some() {
return Err(format!("invalid binary AIGER output {index}"));
}
ascii.push_str(&format!("{literal}\n"));
}
for index in 0..and_count {
let lhs_variable = input_count
.checked_add(latch_count)
.and_then(|value| value.checked_add(index + 1))
.ok_or_else(|| "binary AIGER AND literal overflow".to_string())?;
let lhs = lhs_variable
.checked_mul(2)
.ok_or_else(|| "binary AIGER AND literal overflow".to_string())?;
let delta_zero = parse_binary_aiger_delta(bytes, &mut cursor)?;
let right_zero = lhs
.checked_sub(delta_zero)
.ok_or_else(|| format!("invalid binary AIGER first delta at gate {index}"))?;
let delta_one = parse_binary_aiger_delta(bytes, &mut cursor)?;
let right_one = right_zero
.checked_sub(delta_one)
.ok_or_else(|| format!("invalid binary AIGER second delta at gate {index}"))?;
ascii.push_str(&format!("{lhs} {right_zero} {right_one}\n"));
}
let tail = bytes
.get(cursor..)
.ok_or_else(|| "binary AIGER cursor exceeds input".to_string())?;
if !tail.is_ascii() || tail.contains(&b'\r') {
return Err("binary AIGER symbol or comment section contains invalid bytes".to_string());
}
ascii.push_str(
std::str::from_utf8(tail)
.map_err(|_| "binary AIGER symbol or comment section is not UTF-8".to_string())?,
);
parse_ascii_aiger(ascii.as_bytes())
}
fn parse_ascii_aiger(bytes: &[u8]) -> Result<AagModel, String> {
if !bytes.is_ascii() {
return Err("ASCII AIGER input contains non-ASCII bytes".to_string());
}
let source = std::str::from_utf8(bytes)
.map_err(|_| "ASCII AIGER input is not valid UTF-8".to_string())?;
let mut lines = source.lines();
let mut header = lines
.next()
.ok_or_else(|| "empty ASCII AIGER input".to_string())?
.split_whitespace();
if header.next() != Some("aag") {
return Err("only ASCII AIGER (`aag`) input is supported".to_string());
}
let max_variable = parse_aag_usize(header.next(), "AIGER maximum variable")?;
let inputs = parse_aag_usize(header.next(), "AIGER input count")?;
let latch_count = parse_aag_usize(header.next(), "AIGER latch count")?;
let output_count = parse_aag_usize(header.next(), "AIGER output count")?;
let and_count = parse_aag_usize(header.next(), "AIGER AND count")?;
if header.next().is_some() {
return Err("extended AIGER headers are not supported yet".to_string());
}
if latch_count == 0 {
return Err("AIGER model must contain at least one latch".to_string());
}
if max_variable > 1_000_000 {
return Err("AIGER maximum variable exceeds safety limit 1000000".to_string());
}
let defined_variables = inputs
.checked_add(latch_count)
.and_then(|value| value.checked_add(and_count))
.ok_or_else(|| "AIGER definition count overflow".to_string())?;
if defined_variables != max_variable {
return Err(format!(
"AIGER header requires M = I + L + A; found {max_variable} != {defined_variables}"
));
}
if output_count > 1_000_000 {
return Err("AIGER output count exceeds safety limit 1000000".to_string());
}
let mut input_literals = Vec::with_capacity(inputs);
for index in 0..inputs {
let line = lines
.next()
.ok_or_else(|| format!("truncated AIGER input section at input {index}"))?;
let mut fields = line.split_whitespace();
let literal = parse_aag_usize(fields.next(), "input literal")?;
if fields.next().is_some() || literal == 0 || literal & 1 == 1 || literal / 2 > max_variable
{
return Err(format!("invalid AIGER input {index}"));
}
input_literals.push(literal);
}
let mut latches = Vec::with_capacity(latch_count);
for index in 0..latch_count {
let line = lines
.next()
.ok_or_else(|| format!("truncated AIGER latch section at latch {index}"))?;
let fields: Vec<_> = line.split_whitespace().collect();
if !(2..=3).contains(&fields.len()) {
return Err(format!("invalid AIGER latch {index}"));
}
let current = parse_aag_usize(fields.first().copied(), "latch literal")?;
let next = parse_aag_usize(fields.get(1).copied(), "latch next literal")?;
if current == 0 || current & 1 == 1 || current / 2 > max_variable {
return Err(format!(
"invalid current literal {current} for latch {index}"
));
}
let initial = match fields.get(2).copied() {
None | Some("0") => Some(false),
Some("1") => Some(true),
Some(value) if value.parse::<usize>().ok() == Some(current) => None,
Some(_) => return Err(format!("unsupported initial value for latch {index}")),
};
latches.push(AagLatch {
current,
next,
initial,
});
}
let mut outputs = Vec::with_capacity(output_count);
for index in 0..output_count {
let line = lines
.next()
.ok_or_else(|| format!("truncated AIGER output section at output {index}"))?;
let mut fields = line.split_whitespace();
let literal = parse_aag_usize(fields.next(), "output literal")?;
if fields.next().is_some() {
return Err(format!("invalid AIGER output {index}"));
}
outputs.push(literal);
}
let mut ands = Vec::with_capacity(and_count);
for index in 0..and_count {
let line = lines
.next()
.ok_or_else(|| format!("truncated AIGER AND section at gate {index}"))?;
let mut fields = line.split_whitespace();
let output = parse_aag_usize(fields.next(), "AND output literal")?;
let left = parse_aag_usize(fields.next(), "AND left literal")?;
let right = parse_aag_usize(fields.next(), "AND right literal")?;
if fields.next().is_some()
|| output == 0
|| output & 1 == 1
|| output / 2 > max_variable
|| left / 2 >= output / 2
|| right / 2 >= output / 2
{
return Err(format!("invalid or non-topological AIGER AND gate {index}"));
}
ands.push(AagAnd {
output,
left,
right,
});
}
let literal_limit = max_variable
.checked_mul(2)
.and_then(|value| value.checked_add(1))
.ok_or_else(|| "AIGER literal range overflow".to_string())?;
if input_literals
.iter()
.copied()
.chain(latches.iter().flat_map(|latch| [latch.current, latch.next]))
.chain(outputs.iter().copied())
.chain(
ands.iter()
.flat_map(|gate| [gate.output, gate.left, gate.right]),
)
.any(|literal| literal > literal_limit)
{
return Err("AIGER literal exceeds declared maximum variable".to_string());
}
let mut definitions = BTreeSet::new();
for &literal in &input_literals {
if !definitions.insert(literal / 2) {
return Err("duplicate AIGER variable definition".to_string());
}
}
for latch in &latches {
if !definitions.insert(latch.current / 2) {
return Err("duplicate AIGER variable definition".to_string());
}
}
for gate in &ands {
if !definitions.insert(gate.output / 2) {
return Err("duplicate AIGER variable definition".to_string());
}
}
if latches
.iter()
.map(|latch| latch.next)
.chain(outputs.iter().copied())
.chain(ands.iter().flat_map(|gate| [gate.left, gate.right]))
.any(|literal| literal >= 2 && !definitions.contains(&(literal / 2)))
{
return Err("AIGER literal references an undefined variable".to_string());
}
let mut input_names = (0..inputs)
.map(|index| format!("input_{index}"))
.collect::<Vec<_>>();
let mut latch_names = (0..latch_count)
.map(|index| format!("latch_{index}"))
.collect::<Vec<_>>();
let mut output_names = (0..output_count)
.map(|index| format!("bad_{index}"))
.collect::<Vec<_>>();
let mut seen_symbols = BTreeSet::new();
for line in lines {
if line == "c" {
break;
}
let Some((designator, name)) = line.split_once(' ') else {
return Err("invalid AIGER symbol line".to_string());
};
if name.is_empty() || name.len() > 4096 {
return Err("invalid AIGER symbol name".to_string());
}
let mut chars = designator.chars();
let kind = chars
.next()
.ok_or_else(|| "empty AIGER symbol designator".to_string())?;
let index = chars
.as_str()
.parse::<usize>()
.map_err(|_| "invalid AIGER symbol index".to_string())?;
if !seen_symbols.insert((kind, index)) {
return Err("duplicate AIGER symbol definition".to_string());
}
let target = match kind {
'i' => input_names.get_mut(index),
'l' => latch_names.get_mut(index),
'o' => output_names.get_mut(index),
_ => None,
}
.ok_or_else(|| "unsupported or out-of-range AIGER symbol".to_string())?;
*target = name.to_string();
}
Ok(AagModel {
max_variable,
inputs: input_literals,
input_names,
latches,
latch_names,
outputs,
output_names,
ands,
})
}
fn evaluate_aag_literal(literal: usize, values: &[bool]) -> bool {
if literal < 2 {
return literal == 1;
}
let base = values[literal / 2];
if literal & 1 == 1 { !base } else { base }
}
const INTERFACE_QUOTIENT_MAX_LATCHES: usize = 8;
const INTERFACE_QUOTIENT_MAX_DECLARED_INPUTS: usize = 64;
const INTERFACE_QUOTIENT_MAX_PROJECTED_INPUTS: usize = 8;
const INTERFACE_QUOTIENT_MAX_HORIZON: usize = 64;
const INTERFACE_QUOTIENT_MAX_TABLE_CELLS: usize = 1_048_576;
const INTERFACE_QUOTIENT_MAX_SUMMARIES: usize = 16_384;
const PREDICATE_INTERFACE_MIN_INPUTS: usize = 9;
const PREDICATE_INTERFACE_MAX_INPUTS: usize = 16;
const PREDICATE_INTERFACE_MAX_LATCHES: usize = 4;
const PREDICATE_INTERFACE_MAX_BDD_NODES: usize = 100_000;
const PREDICATE_INTERFACE_MAX_QUERY_CACHE: usize = 4_096;
const PREDICATE_QUOTIENT_MIN_EXPECTED_QUERIES: usize = 100;
const PREDICATE_CERTIFICATE_MAX_EVALUATIONS: usize = 80_000_000;
const PREDICATE_CERTIFICATE_MAX_BYTES: u64 = 4 * 1024 * 1024;
const PREDICATE_CERTIFICATE_VERSION: usize = 1;
const PREDICATE_CERTIFICATE_V2_VERSION: usize = 2;
const PREDICATE_CLI_CONTRACT_VERSION: usize = 1;
const PREDICATE_CERTIFICATE_V2_PROOF_FORMAT: &str = "varisat-native-0.2.2";
const PREDICATE_CERTIFICATE_V2_MAX_BYTES: u64 = 16 * 1024 * 1024;
const PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES: usize = 1024 * 1024;
const PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES: usize = 8 * 1024 * 1024;
const PREDICATE_CERTIFICATE_COST_SCHEMA_VERSION: usize = 1;
const PREDICATE_CERTIFICATE_V2_COST_SCHEMA_VERSION: usize = 1;
const PREDICATE_PROOF_RELATION_SCHEMA_VERSION: usize = 1;
const PREDICATE_PROOF_TERMINAL_SCHEMA_VERSION: usize = 1;
const EVENT_CONTRACT_VERSION: usize = 1;
const EVENT_CONTRACT_BENCHMARK_SCHEMA_VERSION: usize = 1;
const EVENT_CONTRACT_MAX_BYTES: u64 = 1024 * 1024;
const EVENT_CONTRACT_MAX_PHASES: usize = INTERFACE_QUOTIENT_MAX_HORIZON;
const EVENT_CONTRACT_MAX_CLAUSES: usize = 64;
const EVENT_CONTRACT_MAX_LITERALS: usize = 16;
const EVENT_CONTRACT_CERTIFICATE_VERSION: usize = 3;
const EVENT_CONTRACT_CERTIFICATE_MAX_BYTES: u64 = 32 * 1024 * 1024;
const EVENT_CONTRACT_CERTIFICATE_SEMANTICS: &str = "bounded-named-cnf-terminal-bad-avoidance";
const EVENT_CONTRACT_CERTIFICATE_COST_SCHEMA_VERSION: usize = 1;
const EVENT_CONTRACT_CLI_CONTRACT_VERSION: usize = 1;
const EVENT_CONTRACT_PORTFOLIO_VERSION: usize = 1;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct InputPredicate {
clauses: Vec<Vec<(usize, bool)>>,
}
impl InputPredicate {
fn allows(&self, input: usize) -> bool {
self.clauses.iter().all(|clause| {
clause
.iter()
.any(|(bit, positive)| ((input >> bit & 1) == 1) == *positive)
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct EventContractPhase {
start: usize,
length: usize,
predicate: InputPredicate,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct EventContract {
horizon: usize,
phases: Vec<EventContractPhase>,
terminal: InputPredicate,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct EventContractCertificatePhaseV3 {
start: usize,
length: usize,
predicate: InputPredicate,
base_rows: Vec<u16>,
powered_rows: Vec<u16>,
edges: Vec<PredicateCertificateV2Edge>,
proofs: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct EventContractCertificateV3 {
input_sha256: String,
contract_sha256: String,
declared_inputs: usize,
relevant_inputs: Vec<usize>,
latches: usize,
horizon: usize,
bad_output: usize,
initial_state: usize,
avoidable: bool,
phases: Vec<EventContractCertificatePhaseV3>,
terminal: InputPredicate,
terminal_safe_states: u16,
terminal_witnesses: Vec<(usize, u64)>,
terminal_proof: Vec<u8>,
states: Vec<usize>,
inputs: Vec<u64>,
}
fn predicate_quotient_admitted(
relevant_inputs: usize,
latches: usize,
horizon: usize,
expected_queries: usize,
) -> bool {
if !(PREDICATE_INTERFACE_MIN_INPUTS..=PREDICATE_INTERFACE_MAX_INPUTS).contains(&relevant_inputs)
|| !(1..=PREDICATE_INTERFACE_MAX_LATCHES).contains(&latches)
|| expected_queries < PREDICATE_QUOTIENT_MIN_EXPECTED_QUERIES
{
return false;
}
let minimum_horizon = match relevant_inputs {
9..=10 => 8,
11..=13 => 16,
14..=16 => 32,
_ => unreachable!(),
};
horizon >= minimum_horizon
}
fn event_contract_v3_admitted(
relevant_inputs: usize,
latches: usize,
horizon: usize,
declared_initial_state: bool,
) -> bool {
(PREDICATE_INTERFACE_MIN_INPUTS..=PREDICATE_INTERFACE_MAX_INPUTS).contains(&relevant_inputs)
&& (1..=PREDICATE_INTERFACE_MAX_LATCHES).contains(&latches)
&& (1..=INTERFACE_QUOTIENT_MAX_HORIZON).contains(&horizon)
&& declared_initial_state
}
#[derive(Debug)]
struct AagInterfaceTable {
width: usize,
declared_input_count: usize,
input_count: usize,
projected_inputs: Vec<usize>,
next: Vec<Vec<usize>>,
bad_masks: Vec<Vec<u128>>,
}
impl AagInterfaceTable {
fn projected_input_support(model: &AagModel) -> Result<Vec<usize>, String> {
if model.inputs.is_empty() || model.inputs.len() > INTERFACE_QUOTIENT_MAX_DECLARED_INPUTS {
return Err(format!(
"interface quotient requires 1..={INTERFACE_QUOTIENT_MAX_DECLARED_INPUTS} declared inputs; found {}",
model.inputs.len()
));
}
let mut support = vec![0u64; model.max_variable + 1];
for (input, literal) in model.inputs.iter().enumerate() {
support[literal / 2] = 1u64 << input;
}
for gate in &model.ands {
support[gate.output / 2] = support[gate.left / 2] | support[gate.right / 2];
}
let mask = model
.latches
.iter()
.map(|latch| support[latch.next / 2])
.chain(model.outputs.iter().map(|literal| support[literal / 2]))
.fold(0u64, |combined, item| combined | item);
Ok((0..model.inputs.len())
.filter(|input| mask & (1u64 << input) != 0)
.collect())
}
fn compile(model: &AagModel) -> Result<Self, String> {
let width = model.latches.len();
let projected_inputs = Self::projected_input_support(model)?;
let input_count = projected_inputs.len();
if width == 0 || width > INTERFACE_QUOTIENT_MAX_LATCHES {
return Err(format!(
"interface quotient requires 1..={INTERFACE_QUOTIENT_MAX_LATCHES} latches; found {width}"
));
}
if input_count == 0 || input_count > INTERFACE_QUOTIENT_MAX_PROJECTED_INPUTS {
return Err(format!(
"interface quotient projected support requires 1..={INTERFACE_QUOTIENT_MAX_PROJECTED_INPUTS} inputs; found {input_count} across {} declared inputs",
model.inputs.len()
));
}
if model.outputs.len() > u128::BITS as usize {
return Err(format!(
"interface quotient supports at most {} bad outputs",
u128::BITS
));
}
let states = 1usize << width;
let inputs = 1usize << input_count;
let cells = states
.checked_mul(inputs)
.ok_or_else(|| "interface quotient table size overflow".to_string())?;
if cells > INTERFACE_QUOTIENT_MAX_TABLE_CELLS {
return Err(format!(
"interface quotient table requires {cells} state/input cells; limit is {INTERFACE_QUOTIENT_MAX_TABLE_CELLS}"
));
}
let mut next = vec![vec![0usize; inputs]; states];
let mut bad_masks = vec![vec![0u128; inputs]; states];
for state in 0..states {
for input_pattern in 0..inputs {
let mut values = vec![false; model.max_variable + 1];
for (bit, latch) in model.latches.iter().enumerate() {
values[latch.current / 2] = state >> bit & 1 == 1;
}
for (bit, input) in projected_inputs.iter().enumerate() {
values[model.inputs[*input] / 2] = input_pattern >> bit & 1 == 1;
}
for gate in &model.ands {
values[gate.output / 2] = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
}
let mut successor = 0usize;
for (bit, latch) in model.latches.iter().enumerate() {
if evaluate_aag_literal(latch.next, &values) {
successor |= 1usize << bit;
}
}
next[state][input_pattern] = successor;
for (output, literal) in model.outputs.iter().enumerate() {
if evaluate_aag_literal(*literal, &values) {
bad_masks[state][input_pattern] |= 1u128 << output;
}
}
}
}
Ok(Self {
width,
declared_input_count: model.inputs.len(),
input_count,
projected_inputs,
next,
bad_masks,
})
}
fn input_allowed(&self, input_pattern: usize, constraints: &[Option<bool>]) -> bool {
constraints.iter().enumerate().all(|(input, required)| {
required.is_none_or(|value| (input_pattern >> input & 1 == 1) == value)
})
}
fn project_input(&self, declared_pattern: u64) -> usize {
self.projected_inputs
.iter()
.enumerate()
.fold(0usize, |pattern, (bit, input)| {
pattern | (((declared_pattern >> input) & 1) as usize) << bit
})
}
fn lift_input(&self, projected_pattern: usize) -> u64 {
self.projected_inputs
.iter()
.enumerate()
.fold(0u64, |pattern, (bit, input)| {
pattern | (((projected_pattern >> bit) & 1) as u64) << input
})
}
}
struct AagPredicateInterface {
manager: BddManager,
transition: usize,
bad_outputs: Vec<usize>,
projected_inputs: Vec<usize>,
input_levels: Vec<usize>,
current_levels: Vec<usize>,
next_levels: Vec<usize>,
witness_cache: HashMap<(usize, Option<usize>, Option<usize>, Vec<Option<bool>>), Option<u64>>,
}
impl AagPredicateInterface {
fn literal_root(manager: &mut BddManager, roots: &[usize], literal: usize) -> usize {
if literal < 2 {
return usize::from(literal == 1);
}
let root = roots[literal / 2];
if literal & 1 == 0 {
root
} else {
manager.negate(root, &mut HashMap::new())
}
}
fn equivalent(manager: &mut BddManager, left: usize, right: usize) -> usize {
let both = manager.and(left, right);
let not_left = manager.negate(left, &mut HashMap::new());
let not_right = manager.negate(right, &mut HashMap::new());
let neither = manager.and(not_left, not_right);
manager.or(both, neither)
}
fn compile(model: &AagModel) -> Result<Self, String> {
Self::compile_with_node_limit(model, PREDICATE_INTERFACE_MAX_BDD_NODES)
}
fn compile_with_node_limit(model: &AagModel, node_limit: usize) -> Result<Self, String> {
let projected_inputs = AagInterfaceTable::projected_input_support(model)?;
if !(PREDICATE_INTERFACE_MIN_INPUTS..=PREDICATE_INTERFACE_MAX_INPUTS)
.contains(&projected_inputs.len())
{
return Err(format!(
"predicate interface requires {PREDICATE_INTERFACE_MIN_INPUTS}..={PREDICATE_INTERFACE_MAX_INPUTS} relevant inputs; found {}",
projected_inputs.len()
));
}
if model.latches.is_empty() || model.latches.len() > PREDICATE_INTERFACE_MAX_LATCHES {
return Err(format!(
"predicate interface requires 1..={PREDICATE_INTERFACE_MAX_LATCHES} latches; found {}",
model.latches.len()
));
}
if model.outputs.len() > u128::BITS as usize {
return Err("predicate interface supports at most 128 bad outputs".to_string());
}
let input_levels = (0..projected_inputs.len()).collect::<Vec<_>>();
let current_levels = (projected_inputs.len()..projected_inputs.len() + model.latches.len())
.collect::<Vec<_>>();
let next_levels = (projected_inputs.len() + model.latches.len()
..projected_inputs.len() + model.latches.len() * 2)
.collect::<Vec<_>>();
let mut manager = BddManager {
node_limit: Some(node_limit),
..BddManager::default()
};
let mut roots = vec![0usize; model.max_variable + 1];
for (level, input) in projected_inputs.iter().enumerate() {
roots[model.inputs[*input] / 2] = manager.literal(level, true);
}
for (latch, level) in model.latches.iter().zip(¤t_levels) {
roots[latch.current / 2] = manager.literal(*level, true);
}
for gate in &model.ands {
let left = Self::literal_root(&mut manager, &roots, gate.left);
let right = Self::literal_root(&mut manager, &roots, gate.right);
roots[gate.output / 2] = manager.and(left, right);
}
let mut transition = 1usize;
for (latch, level) in model.latches.iter().zip(&next_levels) {
let function = Self::literal_root(&mut manager, &roots, latch.next);
let next = manager.literal(*level, true);
let equality = Self::equivalent(&mut manager, function, next);
transition = manager.and(transition, equality);
}
let bad_outputs = model
.outputs
.iter()
.map(|literal| Self::literal_root(&mut manager, &roots, *literal))
.collect::<Vec<_>>();
if manager.budget_exceeded {
return Err(format!(
"predicate interface BDD exceeds {node_limit} nodes"
));
}
Ok(Self {
manager,
transition,
bad_outputs,
projected_inputs,
input_levels,
current_levels,
next_levels,
witness_cache: HashMap::new(),
})
}
fn restrict_constraints(
&mut self,
mut root: usize,
state: usize,
constraints: &[Option<bool>],
) -> Result<usize, String> {
if constraints.len() != self.input_levels.len() {
return Err("predicate interface constraint dimensions mismatch".to_string());
}
for (bit, level) in self.current_levels.iter().enumerate() {
root = self
.manager
.restrict(root, *level, state >> bit & 1 == 1, &mut HashMap::new());
}
for (level, required) in self.input_levels.iter().zip(constraints) {
if let Some(value) = required {
root = self
.manager
.restrict(root, *level, *value, &mut HashMap::new());
}
}
Ok(root)
}
fn relation(&mut self, constraints: &[Option<bool>]) -> Result<InterfaceRelation, String> {
let states = 1usize << self.current_levels.len();
let variables =
self.input_levels.len() + self.current_levels.len() + self.next_levels.len();
let mut relation = InterfaceRelation::empty(states);
for source in 0..states {
let mut root = self.restrict_constraints(self.transition, source, constraints)?;
for level in &self.input_levels {
root = self.manager.exists(root, *level, &mut HashMap::new());
}
let mut assignment = vec![false; variables];
for target in 0..states {
for (bit, level) in self.next_levels.iter().enumerate() {
assignment[*level] = target >> bit & 1 == 1;
}
if self.manager.evaluate(root, &assignment) {
relation.insert(source, target);
}
}
}
if self.manager.budget_exceeded {
return Err("predicate interface BDD query exceeded its node bound".to_string());
}
Ok(relation)
}
fn input_predicate_root(&mut self, predicate: &InputPredicate) -> Result<usize, String> {
let mut root = 1usize;
for clause in &predicate.clauses {
let mut clause_root = 0usize;
for &(input, positive) in clause {
let level = *self
.input_levels
.get(input)
.ok_or_else(|| "event predicate input is out of range".to_string())?;
let literal = self.manager.literal(level, positive);
clause_root = self.manager.or(clause_root, literal);
}
root = self.manager.and(root, clause_root);
}
if self.manager.budget_exceeded {
return Err("event predicate BDD exceeds the static node bound".to_string());
}
Ok(root)
}
fn relation_predicate(
&mut self,
predicate: &InputPredicate,
) -> Result<InterfaceRelation, String> {
let predicate_root = self.input_predicate_root(predicate)?;
let constrained = self.manager.and(self.transition, predicate_root);
let states = 1usize << self.current_levels.len();
let variables =
self.input_levels.len() + self.current_levels.len() + self.next_levels.len();
let mut relation = InterfaceRelation::empty(states);
for source in 0..states {
let mut root = constrained;
for (bit, level) in self.current_levels.iter().enumerate() {
root = self.manager.restrict(
root,
*level,
source >> bit & 1 == 1,
&mut HashMap::new(),
);
}
for level in &self.input_levels {
root = self.manager.exists(root, *level, &mut HashMap::new());
}
let mut assignment = vec![false; variables];
for target in 0..states {
for (bit, level) in self.next_levels.iter().enumerate() {
assignment[*level] = target >> bit & 1 == 1;
}
if self.manager.evaluate(root, &assignment) {
relation.insert(source, target);
}
}
}
if self.manager.budget_exceeded {
return Err("event predicate relation exceeded the static node bound".to_string());
}
Ok(relation)
}
fn witness_input_predicate(
&mut self,
source: usize,
target: Option<usize>,
output: Option<usize>,
predicate: &InputPredicate,
) -> Result<Option<u64>, String> {
let predicate_root = self.input_predicate_root(predicate)?;
let mut root = if target.is_some() {
self.transition
} else {
let bad = *self
.bad_outputs
.get(output.ok_or_else(|| "event predicate output is missing".to_string())?)
.ok_or_else(|| "event predicate output is out of range".to_string())?;
self.manager.negate(bad, &mut HashMap::new())
};
root = self.manager.and(root, predicate_root);
for (bit, level) in self.current_levels.iter().enumerate() {
root = self
.manager
.restrict(root, *level, source >> bit & 1 == 1, &mut HashMap::new());
}
if let Some(target) = target {
for (bit, level) in self.next_levels.iter().enumerate() {
root = self.manager.restrict(
root,
*level,
target >> bit & 1 == 1,
&mut HashMap::new(),
);
}
}
let variables =
self.input_levels.len() + self.current_levels.len() + self.next_levels.len();
let Some(assignment) = self.manager.satisfying_assignment(root, variables) else {
return Ok(None);
};
Ok(Some(self.projected_inputs.iter().enumerate().fold(
0u64,
|pattern, (bit, input)| {
pattern | (u64::from(assignment[self.input_levels[bit]]) << input)
},
)))
}
fn witness_input(
&mut self,
source: usize,
target: Option<usize>,
output: Option<usize>,
constraints: &[Option<bool>],
) -> Result<Option<u64>, String> {
let key = (source, target, output, constraints.to_vec());
if let Some(cached) = self.witness_cache.get(&key) {
return Ok(*cached);
}
let mut root = if target.is_some() {
self.transition
} else {
let bad = *self
.bad_outputs
.get(output.ok_or_else(|| "predicate output is missing".to_string())?)
.ok_or_else(|| "predicate output is out of range".to_string())?;
self.manager.negate(bad, &mut HashMap::new())
};
root = self.restrict_constraints(root, source, constraints)?;
if let Some(target) = target {
for (bit, level) in self.next_levels.iter().enumerate() {
root = self.manager.restrict(
root,
*level,
target >> bit & 1 == 1,
&mut HashMap::new(),
);
}
}
let Some(assignment) = self.manager.satisfying_assignment(
root,
self.input_levels.len() + self.current_levels.len() + self.next_levels.len(),
) else {
if self.witness_cache.len() >= PREDICATE_INTERFACE_MAX_QUERY_CACHE {
return Err("predicate interface witness cache exceeds 4096 entries".to_string());
}
self.witness_cache.insert(key, None);
return Ok(None);
};
let declared =
self.projected_inputs
.iter()
.enumerate()
.fold(0u64, |pattern, (bit, input)| {
let value = constraints[bit].unwrap_or(assignment[self.input_levels[bit]]);
pattern | (u64::from(value) << input)
});
if self.witness_cache.len() >= PREDICATE_INTERFACE_MAX_QUERY_CACHE {
return Err("predicate interface witness cache exceeds 4096 entries".to_string());
}
self.witness_cache.insert(key, Some(declared));
Ok(Some(declared))
}
}
struct PredicateQuotient {
interface: AagPredicateInterface,
relation_cache: HashMap<Vec<Option<bool>>, InterfaceRelation>,
power_cache: HashMap<(Vec<Option<bool>>, usize), InterfaceRelation>,
}
impl PredicateQuotient {
fn new(model: &AagModel) -> Result<Self, String> {
Self::new_with_node_limit(model, PREDICATE_INTERFACE_MAX_BDD_NODES)
}
fn new_with_node_limit(model: &AagModel, node_limit: usize) -> Result<Self, String> {
Ok(Self {
interface: AagPredicateInterface::compile_with_node_limit(model, node_limit)?,
relation_cache: HashMap::new(),
power_cache: HashMap::new(),
})
}
fn relation(&mut self, constraints: &[Option<bool>]) -> Result<InterfaceRelation, String> {
if let Some(relation) = self.relation_cache.get(constraints) {
return Ok(relation.clone());
}
let relation = self.interface.relation(constraints)?;
if self.relation_cache.len() >= PREDICATE_INTERFACE_MAX_QUERY_CACHE {
return Err("predicate quotient relation cache exceeds 4096 entries".to_string());
}
self.relation_cache
.insert(constraints.to_vec(), relation.clone());
Ok(relation)
}
fn relation_power(
&mut self,
constraints: &[Option<bool>],
length: usize,
) -> Result<InterfaceRelation, String> {
let key = (constraints.to_vec(), length);
if let Some(relation) = self.power_cache.get(&key) {
return Ok(relation.clone());
}
let states = 1usize << self.interface.current_levels.len();
let relation = if length == 0 {
InterfaceRelation::identity(states)
} else if length == 1 {
self.relation(constraints)?
} else {
let left_length = length / 2;
let left = self.relation_power(constraints, left_length)?;
let right = self.relation_power(constraints, length - left_length)?;
InterfaceRelation::compose(&left, &right)?
};
if self.power_cache.len() >= INTERFACE_QUOTIENT_MAX_SUMMARIES {
return Err(format!(
"predicate quotient power cache exceeds {INTERFACE_QUOTIENT_MAX_SUMMARIES} entries"
));
}
self.power_cache.insert(key, relation.clone());
Ok(relation)
}
fn compressed_relation(
&mut self,
constraints: &[Vec<Option<bool>>],
) -> Result<InterfaceRelation, String> {
let states = 1usize << self.interface.current_levels.len();
let mut result = InterfaceRelation::identity(states);
let mut start = 0usize;
while start < constraints.len() {
let mut end = start + 1;
while end < constraints.len() && constraints[end] == constraints[start] {
end += 1;
}
let block = self.relation_power(&constraints[start], end - start)?;
result = InterfaceRelation::compose(&result, &block)?;
start = end;
}
Ok(result)
}
fn query(
&mut self,
initial_state: usize,
output: usize,
constraints: &[Vec<Option<bool>>],
) -> Result<Option<InterfaceQueryResult>, String> {
if constraints.is_empty()
|| constraints
.iter()
.any(|frame| frame.len() != self.interface.input_levels.len())
{
return Err("predicate quotient constraint dimensions mismatch".to_string());
}
let horizon = constraints.len() - 1;
let relation = self.compressed_relation(&constraints[..horizon])?;
let terminal = relation
.targets(initial_state)
.find_map(|state| {
self.interface
.witness_input(state, None, Some(output), &constraints[horizon])
.transpose()
.map(|witness| witness.map(|input| (state, input)))
})
.transpose()?;
let Some((terminal_state, terminal_input)) = terminal else {
return Ok(None);
};
let mut reachable = vec![vec![false; relation.states()]; horizon + 1];
let mut predecessor = vec![vec![None; relation.states()]; horizon + 1];
reachable[0][initial_state] = true;
for frame in 0..horizon {
let step = self.relation(&constraints[frame])?;
for source in 0..step.states() {
if !reachable[frame][source] {
continue;
}
for target in step.targets(source) {
if !reachable[frame + 1][target] {
reachable[frame + 1][target] = true;
predecessor[frame + 1][target] = Some(source);
}
}
}
}
if !reachable[horizon][terminal_state] {
return Err("predicate quotient compressed relation disagrees with replay".to_string());
}
let mut states = vec![0usize; horizon + 1];
states[horizon] = terminal_state;
for frame in (1..=horizon).rev() {
states[frame - 1] = predecessor[frame][states[frame]]
.ok_or_else(|| "predicate quotient witness predecessor is missing".to_string())?;
}
let mut declared_inputs = Vec::with_capacity(horizon + 1);
for frame in 0..horizon {
declared_inputs.push(
self.interface
.witness_input(
states[frame],
Some(states[frame + 1]),
None,
&constraints[frame],
)?
.ok_or_else(|| {
"predicate quotient transition witness is missing".to_string()
})?,
);
}
declared_inputs.push(terminal_input);
let inputs = declared_inputs
.iter()
.map(|declared| {
self.interface
.projected_inputs
.iter()
.enumerate()
.fold(0usize, |pattern, (bit, input)| {
pattern | (((declared >> input) & 1) as usize) << bit
})
})
.collect();
Ok(Some(InterfaceQueryResult {
states,
inputs,
declared_inputs,
}))
}
fn query_event_contract(
&mut self,
initial_state: usize,
output: usize,
contract: &EventContract,
) -> Result<Option<InterfaceQueryResult>, String> {
let states_count = 1usize << self.interface.current_levels.len();
let mut composed = InterfaceRelation::identity(states_count);
let mut frame_predicates = Vec::with_capacity(contract.horizon + 1);
for phase in &contract.phases {
let base = self.interface.relation_predicate(&phase.predicate)?;
let powered = InterfaceRelation::power(&base, phase.length)?;
composed = InterfaceRelation::compose(&composed, &powered)?;
frame_predicates.extend(std::iter::repeat_n(phase.predicate.clone(), phase.length));
}
frame_predicates.push(contract.terminal.clone());
let terminal = composed
.targets(initial_state)
.find_map(|state| {
self.interface
.witness_input_predicate(state, None, Some(output), &contract.terminal)
.transpose()
.map(|input| input.map(|input| (state, input)))
})
.transpose()?;
let Some((terminal_state, terminal_input)) = terminal else {
return Ok(None);
};
let mut reachable = vec![vec![false; states_count]; contract.horizon + 1];
let mut predecessor = vec![vec![None; states_count]; contract.horizon + 1];
reachable[0][initial_state] = true;
for frame in 0..contract.horizon {
let step = self
.interface
.relation_predicate(&frame_predicates[frame])?;
for source in 0..states_count {
if !reachable[frame][source] {
continue;
}
for target in step.targets(source) {
if !reachable[frame + 1][target] {
reachable[frame + 1][target] = true;
predecessor[frame + 1][target] = Some(source);
}
}
}
}
if !reachable[contract.horizon][terminal_state] {
return Err("event contract compressed relation disagrees with replay".to_string());
}
let mut states = vec![0usize; contract.horizon + 1];
states[contract.horizon] = terminal_state;
for frame in (1..=contract.horizon).rev() {
states[frame - 1] = predecessor[frame][states[frame]]
.ok_or_else(|| "event contract witness predecessor is missing".to_string())?;
}
let mut declared_inputs = Vec::with_capacity(contract.horizon + 1);
for frame in 0..contract.horizon {
declared_inputs.push(
self.interface
.witness_input_predicate(
states[frame],
Some(states[frame + 1]),
None,
&frame_predicates[frame],
)?
.ok_or_else(|| "event contract transition witness is missing".to_string())?,
);
}
declared_inputs.push(terminal_input);
let inputs = declared_inputs
.iter()
.map(|declared| {
self.interface
.projected_inputs
.iter()
.enumerate()
.fold(0usize, |pattern, (bit, input)| {
pattern | (((declared >> input) & 1) as usize) << bit
})
})
.collect();
Ok(Some(InterfaceQueryResult {
states,
inputs,
declared_inputs,
}))
}
}
struct IndependentPredicateChecker<'a> {
model: &'a AagModel,
relevant_inputs: Vec<usize>,
states: usize,
evaluations: usize,
}
impl<'a> IndependentPredicateChecker<'a> {
fn support(model: &AagModel) -> Result<Vec<usize>, String> {
if model.inputs.is_empty() || model.inputs.len() > u64::BITS as usize {
return Err("predicate certificate requires 1..=64 declared inputs".to_string());
}
let mut dependencies = vec![0u64; model.max_variable + 1];
for (input, literal) in model.inputs.iter().enumerate() {
dependencies[literal / 2] = 1u64 << input;
}
for gate in &model.ands {
dependencies[gate.output / 2] =
dependencies[gate.left / 2] | dependencies[gate.right / 2];
}
let combined = model
.latches
.iter()
.map(|latch| dependencies[latch.next / 2])
.chain(
model
.outputs
.iter()
.map(|literal| dependencies[literal / 2]),
)
.fold(0u64, |mask, item| mask | item);
Ok((0..model.inputs.len())
.filter(|input| combined & (1u64 << input) != 0)
.collect())
}
fn new(model: &'a AagModel) -> Result<Self, String> {
let relevant_inputs = Self::support(model)?;
if !(PREDICATE_INTERFACE_MIN_INPUTS..=PREDICATE_INTERFACE_MAX_INPUTS)
.contains(&relevant_inputs.len())
{
return Err(format!(
"predicate certificate requires 9..=16 relevant inputs; found {}",
relevant_inputs.len()
));
}
if !(1..=PREDICATE_INTERFACE_MAX_LATCHES).contains(&model.latches.len()) {
return Err(format!(
"predicate certificate requires 1..=4 latches; found {}",
model.latches.len()
));
}
Ok(Self {
model,
relevant_inputs,
states: 1usize << model.latches.len(),
evaluations: 0,
})
}
fn evaluate(&mut self, state: usize, declared_input: u64) -> Result<(usize, u128), String> {
self.evaluations = self
.evaluations
.checked_add(1)
.ok_or_else(|| "predicate certificate evaluation count overflow".to_string())?;
if self.evaluations > PREDICATE_CERTIFICATE_MAX_EVALUATIONS {
return Err(format!(
"predicate certificate exceeds {PREDICATE_CERTIFICATE_MAX_EVALUATIONS} exhaustive evaluations"
));
}
let mut values = vec![false; self.model.max_variable + 1];
for (bit, latch) in self.model.latches.iter().enumerate() {
values[latch.current / 2] = state >> bit & 1 == 1;
}
for (input, literal) in self.model.inputs.iter().enumerate() {
values[literal / 2] = declared_input >> input & 1 == 1;
}
for gate in &self.model.ands {
values[gate.output / 2] = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
}
let next = self
.model
.latches
.iter()
.enumerate()
.fold(0usize, |next, (bit, latch)| {
next | (usize::from(evaluate_aag_literal(latch.next, &values)) << bit)
});
let bad = self
.model
.outputs
.iter()
.enumerate()
.fold(0u128, |bad, (output, literal)| {
bad | (u128::from(evaluate_aag_literal(*literal, &values)) << output)
});
Ok((next, bad))
}
fn declared_input(&self, projected: usize) -> u64 {
self.relevant_inputs
.iter()
.enumerate()
.fold(0u64, |declared, (bit, input)| {
declared | (((projected >> bit) & 1) as u64) << input
})
}
fn allowed(projected: usize, constraints: &[Option<bool>]) -> bool {
constraints.iter().enumerate().all(|(bit, required)| {
required.is_none_or(|value| (projected >> bit & 1 == 1) == value)
})
}
fn one_step_relation(&mut self, constraints: &[Option<bool>]) -> Result<Vec<u16>, String> {
if constraints.len() != self.relevant_inputs.len() {
return Err("predicate certificate constraint dimensions mismatch".to_string());
}
let mut rows = vec![0u16; self.states];
for source in 0..self.states {
for projected in 0..(1usize << self.relevant_inputs.len()) {
if !Self::allowed(projected, constraints) {
continue;
}
let declared = self.declared_input(projected);
let (target, _) = self.evaluate(source, declared)?;
rows[source] |= 1u16 << target;
}
}
Ok(rows)
}
fn compose(left: &[u16], right: &[u16]) -> Result<Vec<u16>, String> {
if left.len() != right.len() || left.len() > u16::BITS as usize {
return Err("predicate certificate relation dimensions mismatch".to_string());
}
let mut result = vec![0u16; left.len()];
for (source, targets) in left.iter().enumerate() {
let mut remaining = *targets;
while remaining != 0 {
let middle = remaining.trailing_zeros() as usize;
if middle >= right.len() {
return Err("predicate certificate relation target is out of range".to_string());
}
result[source] |= right[middle];
remaining &= remaining - 1;
}
}
Ok(result)
}
fn power(base: &[u16], mut exponent: usize) -> Result<Vec<u16>, String> {
let mut result = (0..base.len())
.map(|state| 1u16 << state)
.collect::<Vec<_>>();
let mut factor = base.to_vec();
while exponent != 0 {
if exponent & 1 == 1 {
result = Self::compose(&result, &factor)?;
}
exponent >>= 1;
if exponent != 0 {
factor = Self::compose(&factor, &factor)?;
}
}
Ok(result)
}
fn terminal_safe_states(
&mut self,
output: usize,
constraints: &[Option<bool>],
) -> Result<u16, String> {
if output >= self.model.outputs.len() {
return Err("predicate certificate bad output is out of range".to_string());
}
if constraints.len() != self.relevant_inputs.len() {
return Err("predicate certificate constraint dimensions mismatch".to_string());
}
let mut safe = 0u16;
for state in 0..self.states {
for projected in 0..(1usize << self.relevant_inputs.len()) {
if !Self::allowed(projected, constraints) {
continue;
}
let declared = self.declared_input(projected);
let (_, bad) = self.evaluate(state, declared)?;
if bad & (1u128 << output) == 0 {
safe |= 1u16 << state;
break;
}
}
}
Ok(safe)
}
}
fn aiger_obligation_transition(model: &AagModel) -> AigerTransition {
AigerTransition {
max_variable: model.max_variable,
inputs: model.inputs.clone(),
latches: model
.latches
.iter()
.map(|latch| AigerLatch {
current: latch.current,
next: latch.next,
})
.collect(),
outputs: model.outputs.clone(),
ands: model
.ands
.iter()
.map(|gate| AigerAnd {
output: gate.output,
left: gate.left,
right: gate.right,
})
.collect(),
}
}
fn predicate_relation_completeness_clauses(
model: &AagModel,
relevant_inputs: &[usize],
source: usize,
constraints: &[Option<bool>],
claimed_targets: u16,
) -> Result<Vec<Clause>, String> {
if constraints.len() != relevant_inputs.len() {
return Err("predicate proof obligation dimensions mismatch".to_string());
}
let predicate = InputPredicate {
clauses: constraints
.iter()
.enumerate()
.filter_map(|(input, required)| required.map(|value| vec![(input, value)]))
.collect(),
};
predicate_relation_completeness_clauses_for_predicate(
model,
relevant_inputs,
source,
&predicate,
claimed_targets,
)
}
fn predicate_relation_completeness_clauses_for_predicate(
model: &AagModel,
relevant_inputs: &[usize],
source: usize,
predicate: &InputPredicate,
claimed_targets: u16,
) -> Result<Vec<Clause>, String> {
let transition = aiger_obligation_transition(model);
let predicate = AigerInputPredicate {
clauses: predicate.clauses.clone(),
};
let targets = (0..(1usize << model.latches.len()))
.filter(|target| claimed_targets & (1u16 << target) != 0)
.collect::<Vec<_>>();
aiger_obligation::relation_row_completeness_cnf(
&transition,
relevant_inputs,
source,
&predicate,
&targets,
)
.map_err(|error| error.to_string())
}
fn predicate_terminal_completeness_clauses(
model: &AagModel,
relevant_inputs: &[usize],
constraints: &[Option<bool>],
bad_output: usize,
claimed_safe_states: u16,
) -> Result<Vec<Clause>, String> {
if constraints.len() != relevant_inputs.len() {
return Err("predicate terminal proof obligation dimensions mismatch".to_string());
}
let predicate = InputPredicate {
clauses: constraints
.iter()
.enumerate()
.filter_map(|(input, required)| required.map(|value| vec![(input, value)]))
.collect(),
};
predicate_terminal_completeness_clauses_for_predicate(
model,
relevant_inputs,
&predicate,
bad_output,
claimed_safe_states,
)
}
fn predicate_terminal_completeness_clauses_for_predicate(
model: &AagModel,
relevant_inputs: &[usize],
predicate: &InputPredicate,
bad_output: usize,
claimed_safe_states: u16,
) -> Result<Vec<Clause>, String> {
let transition = aiger_obligation_transition(model);
let predicate = AigerInputPredicate {
clauses: predicate.clauses.clone(),
};
let safe_states = (0..(1usize << model.latches.len()))
.filter(|state| claimed_safe_states & (1u16 << state) != 0)
.collect::<Vec<_>>();
aiger_obligation::terminal_completeness_cnf(
&transition,
relevant_inputs,
&predicate,
bad_output,
&safe_states,
)
.map_err(|error| error.to_string())
}
fn generate_varisat_unsat_proof(clauses: &[Clause]) -> Result<Vec<u8>, String> {
unsat_proof::generate_unsat_proof(clauses).map_err(|error| error.to_string())
}
#[allow(dead_code)]
fn read_predicate_proof_vli(proof: &[u8], offset: &mut usize) -> Result<u64, String> {
let start = *offset;
let mut marker = None;
for byte_index in 0..10 {
let byte = *proof
.get(start + byte_index)
.ok_or_else(|| "predicate native proof has a truncated integer".to_string())?;
if byte != 0 {
marker = Some(byte_index * 8 + byte.trailing_zeros() as usize);
break;
}
}
let marker = marker.ok_or_else(|| "predicate native proof integer is invalid".to_string())?;
let encoded_len = marker + 1;
if encoded_len > 10 || start + encoded_len > proof.len() {
return Err("predicate native proof integer is out of bounds".to_string());
}
let mut raw = 0u128;
for (index, &byte) in proof[start..start + encoded_len].iter().enumerate() {
raw |= u128::from(byte) << (index * 8);
}
let value = raw >> encoded_len;
if value > u128::from(u64::MAX) {
return Err("predicate native proof integer exceeds u64".to_string());
}
*offset += encoded_len;
Ok(value as u64)
}
#[allow(dead_code)]
fn preflight_varisat_unsat_proof(clauses: &[Clause], proof: &[u8]) -> Result<(), String> {
const CODE_END: u64 = 0x9ac3_391f_4294_c211;
const MAX_PROOF_STEPS: usize = 100_000;
let max_variable = clauses
.iter()
.flat_map(|clause| clause.0.iter().map(|&(variable, _)| variable))
.max()
.unwrap_or(0);
let max_literal_code = max_variable
.checked_mul(2)
.and_then(|value| value.checked_add(1))
.ok_or_else(|| "predicate native proof variable bound overflow".to_string())?
as u64;
let mut offset = 0usize;
let mut steps = 0usize;
let read_variable = |offset: &mut usize| -> Result<(), String> {
let variable = read_predicate_proof_vli(proof, offset)?;
if variable > max_variable as u64 {
return Err("predicate native proof variable exceeds obligation".to_string());
}
Ok(())
};
let read_list = |offset: &mut usize, literals: bool| -> Result<(), String> {
let count = read_predicate_proof_vli(proof, offset)?;
if count > (proof.len() - *offset) as u64 {
return Err("predicate native proof list count exceeds remaining input".to_string());
}
for _ in 0..count {
let value = read_predicate_proof_vli(proof, offset)?;
if literals && value > max_literal_code {
return Err("predicate native proof literal exceeds obligation".to_string());
}
}
Ok(())
};
while offset < proof.len() {
steps += 1;
if steps > MAX_PROOF_STEPS {
return Err("predicate native proof step count exceeds limit".to_string());
}
match read_predicate_proof_vli(proof, &mut offset)? {
0 | 2 => {
read_variable(&mut offset)?;
read_variable(&mut offset)?;
}
1 | 3 | 4 | 5 | 6 => read_variable(&mut offset)?,
7 | 8 | 17 => {
read_list(&mut offset, true)?;
read_list(&mut offset, false)?;
}
9 => {
let count = read_predicate_proof_vli(proof, &mut offset)?;
if count > ((proof.len() - offset) / 2) as u64 {
return Err(
"predicate native proof unit count exceeds remaining input".to_string()
);
}
for _ in 0..count {
let literal = read_predicate_proof_vli(proof, &mut offset)?;
if literal > max_literal_code {
return Err("predicate native proof literal exceeds obligation".to_string());
}
let _ = read_predicate_proof_vli(proof, &mut offset)?;
}
}
10 | 11 | 12 | 14 | 15 | 16 => read_list(&mut offset, true)?,
13 => {
if read_predicate_proof_vli(proof, &mut offset)? > 64 {
return Err("predicate native proof hash width exceeds 64".to_string());
}
}
CODE_END => {
if offset != proof.len() {
return Err("predicate native proof has trailing bytes".to_string());
}
return Ok(());
}
_ => return Err("predicate native proof step code is invalid".to_string()),
}
}
Err("predicate native proof has no end step".to_string())
}
fn verify_varisat_unsat_proof(clauses: &[Clause], proof: &[u8]) -> Result<(), String> {
unsat_proof::verify_unsat_proof(clauses, proof).map_err(|error| error.to_string())
}
#[derive(Debug)]
struct PredicateProofRelationMetrics {
relation: Vec<u16>,
witness_count: usize,
proof_bytes: usize,
producer_ns: u128,
generation_ns: u128,
verification_ns: u128,
}
fn predicate_proof_relation_experiment(
model: &AagModel,
constraints: &[Option<bool>],
) -> Result<PredicateProofRelationMetrics, String> {
let producer_start = Instant::now();
let mut quotient = PredicateQuotient::new(model)?;
let relation = quotient.relation(constraints)?;
let rows = relation
.rows()
.iter()
.map(|row| row[0] as u16)
.collect::<Vec<_>>();
let producer_ns = producer_start.elapsed().as_nanos();
let mut proofs = Vec::with_capacity(rows.len());
let mut witness_inputs = Vec::new();
let generation_start = Instant::now();
for (source, &targets) in rows.iter().enumerate() {
if targets == 0 {
return Err("predicate proof relation unexpectedly has no target".to_string());
}
for target in 0..rows.len() {
if targets & (1u16 << target) != 0 {
let witness = quotient
.interface
.witness_input(source, Some(target), None, constraints)?
.ok_or_else(|| "predicate proof relation edge lacks witness".to_string())?;
witness_inputs.push((source, target, witness));
}
}
let clauses = predicate_relation_completeness_clauses(
model,
"ient.interface.projected_inputs,
source,
constraints,
targets,
)?;
let proof = generate_varisat_unsat_proof(&clauses)?;
proofs.push((clauses, proof));
}
let generation_ns = generation_start.elapsed().as_nanos();
let proof_bytes = proofs.iter().map(|(_, proof)| proof.len()).sum();
let verification_start = Instant::now();
let mut checker = IndependentPredicateChecker::new(model)?;
for &(source, target, input) in &witness_inputs {
let (actual_target, _) = checker.evaluate(source, input)?;
if actual_target != target
|| checker
.relevant_inputs
.iter()
.enumerate()
.any(|(bit, declared)| {
constraints[bit].is_some_and(|value| (input >> declared & 1 == 1) != value)
})
{
return Err("predicate proof relation witness is invalid".to_string());
}
}
for (clauses, proof) in &proofs {
verify_varisat_unsat_proof(clauses, proof)?;
}
let verification_ns = verification_start.elapsed().as_nanos();
Ok(PredicateProofRelationMetrics {
relation: rows,
witness_count: witness_inputs.len(),
proof_bytes,
producer_ns,
generation_ns,
verification_ns,
})
}
#[derive(Debug)]
struct PredicateProofTerminalMetrics {
safe_states: u16,
witness_count: usize,
proof_bytes: usize,
producer_ns: u128,
generation_ns: u128,
verification_ns: u128,
}
fn predicate_proof_terminal_experiment(
model: &AagModel,
constraints: &[Option<bool>],
bad_output: usize,
) -> Result<PredicateProofTerminalMetrics, String> {
let producer_start = Instant::now();
let mut quotient = PredicateQuotient::new(model)?;
if bad_output >= quotient.interface.bad_outputs.len() {
return Err("predicate proof terminal output is out of range".to_string());
}
let mut safe_states = 0u16;
let mut witness_inputs = Vec::new();
for state in 0..(1usize << model.latches.len()) {
if let Some(input) =
quotient
.interface
.witness_input(state, None, Some(bad_output), constraints)?
{
safe_states |= 1u16 << state;
witness_inputs.push((state, input));
}
}
let producer_ns = producer_start.elapsed().as_nanos();
let generation_start = Instant::now();
let clauses = predicate_terminal_completeness_clauses(
model,
"ient.interface.projected_inputs,
constraints,
bad_output,
safe_states,
)?;
let proof = generate_varisat_unsat_proof(&clauses)?;
let generation_ns = generation_start.elapsed().as_nanos();
let verification_start = Instant::now();
let mut checker = IndependentPredicateChecker::new(model)?;
for &(state, input) in &witness_inputs {
let (_, bad) = checker.evaluate(state, input)?;
if bad & (1u128 << bad_output) != 0
|| checker
.relevant_inputs
.iter()
.enumerate()
.any(|(bit, declared)| {
constraints[bit].is_some_and(|value| (input >> declared & 1 == 1) != value)
})
{
return Err("predicate proof terminal witness is invalid".to_string());
}
}
verify_varisat_unsat_proof(&clauses, &proof)?;
let verification_ns = verification_start.elapsed().as_nanos();
Ok(PredicateProofTerminalMetrics {
safe_states,
witness_count: witness_inputs.len(),
proof_bytes: proof.len(),
producer_ns,
generation_ns,
verification_ns,
})
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PredicateCertificatePhase {
start: usize,
length: usize,
constraints: Vec<Option<bool>>,
rows: Vec<u16>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PredicateCertificate {
input_sha256: String,
declared_inputs: usize,
relevant_inputs: Vec<usize>,
latches: usize,
horizon: usize,
bad_output: usize,
initial_state: usize,
avoidable: bool,
phases: Vec<PredicateCertificatePhase>,
terminal_constraint: Vec<Option<bool>>,
terminal_safe_states: u16,
states: Vec<usize>,
inputs: Vec<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PredicateCertificateV2Edge {
source: usize,
target: usize,
input: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PredicateCertificateV2Phase {
start: usize,
length: usize,
constraints: Vec<Option<bool>>,
base_rows: Vec<u16>,
powered_rows: Vec<u16>,
edges: Vec<PredicateCertificateV2Edge>,
proofs: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct PredicateCertificateV2 {
input_sha256: String,
declared_inputs: usize,
relevant_inputs: Vec<usize>,
latches: usize,
horizon: usize,
bad_output: usize,
initial_state: usize,
avoidable: bool,
phases: Vec<PredicateCertificateV2Phase>,
terminal_constraint: Vec<Option<bool>>,
terminal_safe_states: u16,
terminal_witnesses: Vec<(usize, u64)>,
terminal_proof: Vec<u8>,
states: Vec<usize>,
inputs: Vec<u64>,
}
fn predicate_bytes_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut text = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
text.push(HEX[(byte >> 4) as usize] as char);
text.push(HEX[(byte & 15) as usize] as char);
}
text
}
fn parse_predicate_bytes_hex(text: &str, name: &str) -> Result<Vec<u8>, String> {
if text.is_empty()
|| text.len() & 1 == 1
|| text.len() > PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES * 2
|| text
.bytes()
.any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
{
return Err(format!(
"predicate certificate v2 `{name}` is not bounded canonical byte hex"
));
}
let mut bytes = Vec::with_capacity(text.len() / 2);
for pair in text.as_bytes().chunks_exact(2) {
let digit = |value: u8| -> u8 {
if value <= b'9' {
value - b'0'
} else {
value - b'a' + 10
}
};
bytes.push((digit(pair[0]) << 4) | digit(pair[1]));
}
Ok(bytes)
}
fn predicate_constraint_text(constraints: &[Option<bool>]) -> String {
constraints
.iter()
.map(|required| match required {
Some(false) => '0',
Some(true) => '1',
None => 'x',
})
.collect()
}
fn parse_predicate_constraints(text: &str, width: usize) -> Result<Vec<Option<bool>>, String> {
if text.len() != width || !text.is_ascii() {
return Err("predicate certificate constraint width mismatch".to_string());
}
text.bytes()
.map(|symbol| match symbol {
b'0' => Ok(Some(false)),
b'1' => Ok(Some(true)),
b'x' => Ok(None),
_ => Err("predicate certificate constraint symbol must be 0, 1, or x".to_string()),
})
.collect()
}
fn predicate_rows_text(rows: &[u16]) -> String {
rows.iter()
.map(|row| format!("{row:x}"))
.collect::<Vec<_>>()
.join(":")
}
fn parse_predicate_hex(text: &str, bits: usize, name: &str) -> Result<u16, String> {
if text.is_empty()
|| text
.bytes()
.any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
{
return Err(format!(
"predicate certificate `{name}` is not canonical hex"
));
}
let value = u16::from_str_radix(text, 16)
.map_err(|_| format!("predicate certificate `{name}` overflow"))?;
if format!("{value:x}") != text || (bits < u16::BITS as usize && value >> bits != 0) {
return Err(format!(
"predicate certificate `{name}` is non-canonical or out of range"
));
}
Ok(value)
}
fn parse_predicate_rows(text: &str, states: usize) -> Result<Vec<u16>, String> {
let parts = text.split(':').collect::<Vec<_>>();
if parts.len() != states {
return Err("predicate certificate relation row count mismatch".to_string());
}
parts
.into_iter()
.map(|part| parse_predicate_hex(part, states, "relation row"))
.collect()
}
fn predicate_certificate_body(certificate: &PredicateCertificate) -> String {
let mut lines = vec![
format!("predicate_certificate_version={PREDICATE_CERTIFICATE_VERSION}"),
"semantics=bounded-terminal-bad-avoidance".to_string(),
format!("input_sha256={}", certificate.input_sha256),
format!("declared_inputs={}", certificate.declared_inputs),
format!("relevant_inputs={}", certificate.relevant_inputs.len()),
format!("latches={}", certificate.latches),
format!("horizon={}", certificate.horizon),
format!("bad_output={}", certificate.bad_output),
format!("initial_state={}", certificate.initial_state),
format!(
"result={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
}
),
format!("phase_count={}", certificate.phases.len()),
];
for (index, input) in certificate.relevant_inputs.iter().enumerate() {
lines.push(format!("relevant_{index}={input}"));
}
for (index, phase) in certificate.phases.iter().enumerate() {
lines.push(format!(
"phase_{index}={},{},{},{}",
phase.start,
phase.length,
predicate_constraint_text(&phase.constraints),
predicate_rows_text(&phase.rows)
));
}
lines.push(format!(
"terminal_constraint={}",
predicate_constraint_text(&certificate.terminal_constraint)
));
lines.push(format!(
"terminal_safe_states={:x}",
certificate.terminal_safe_states
));
lines.push(format!("state_count={}", certificate.states.len()));
lines.push(format!(
"states={}",
if certificate.states.is_empty() {
"-".to_string()
} else {
certificate
.states
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(",")
}
));
lines.push(format!("input_count={}", certificate.inputs.len()));
lines.push(format!(
"inputs={}",
if certificate.inputs.is_empty() {
"-".to_string()
} else {
certificate
.inputs
.iter()
.map(u64::to_string)
.collect::<Vec<_>>()
.join(",")
}
));
lines.join("\n") + "\n"
}
fn predicate_certificate_v2_body(certificate: &PredicateCertificateV2) -> String {
let mut lines = vec![
format!("predicate_certificate_version={PREDICATE_CERTIFICATE_V2_VERSION}"),
"semantics=bounded-terminal-bad-avoidance".to_string(),
format!("proof_format={PREDICATE_CERTIFICATE_V2_PROOF_FORMAT}"),
format!("input_sha256={}", certificate.input_sha256),
format!("declared_inputs={}", certificate.declared_inputs),
format!("relevant_inputs={}", certificate.relevant_inputs.len()),
format!("latches={}", certificate.latches),
format!("horizon={}", certificate.horizon),
format!("bad_output={}", certificate.bad_output),
format!("initial_state={}", certificate.initial_state),
format!(
"result={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
}
),
format!("phase_count={}", certificate.phases.len()),
];
for (index, input) in certificate.relevant_inputs.iter().enumerate() {
lines.push(format!("relevant_{index}={input}"));
}
for (index, phase) in certificate.phases.iter().enumerate() {
lines.push(format!(
"phase_{index}={},{},{}",
phase.start,
phase.length,
predicate_constraint_text(&phase.constraints)
));
lines.push(format!(
"phase_{index}_base_rows={}",
predicate_rows_text(&phase.base_rows)
));
lines.push(format!(
"phase_{index}_powered_rows={}",
predicate_rows_text(&phase.powered_rows)
));
lines.push(format!("phase_{index}_edge_count={}", phase.edges.len()));
for (edge_index, edge) in phase.edges.iter().enumerate() {
lines.push(format!(
"phase_{index}_edge_{edge_index}={},{},{}",
edge.source, edge.target, edge.input
));
}
lines.push(format!("phase_{index}_proof_count={}", phase.proofs.len()));
for (source, proof) in phase.proofs.iter().enumerate() {
lines.push(format!(
"phase_{index}_proof_{source}={}",
predicate_bytes_hex(proof)
));
}
}
lines.push(format!(
"terminal_constraint={}",
predicate_constraint_text(&certificate.terminal_constraint)
));
lines.push(format!(
"terminal_safe_states={:x}",
certificate.terminal_safe_states
));
lines.push(format!(
"terminal_witness_count={}",
certificate.terminal_witnesses.len()
));
for (index, (state, input)) in certificate.terminal_witnesses.iter().enumerate() {
lines.push(format!("terminal_witness_{index}={state},{input}"));
}
lines.push(format!(
"terminal_proof={}",
predicate_bytes_hex(&certificate.terminal_proof)
));
lines.push(format!("state_count={}", certificate.states.len()));
lines.push(format!(
"states={}",
if certificate.states.is_empty() {
"-".to_string()
} else {
certificate
.states
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(",")
}
));
lines.push(format!("input_count={}", certificate.inputs.len()));
lines.push(format!(
"inputs={}",
if certificate.inputs.is_empty() {
"-".to_string()
} else {
certificate
.inputs
.iter()
.map(u64::to_string)
.collect::<Vec<_>>()
.join(",")
}
));
lines.join("\n") + "\n"
}
fn event_certificate_clause_text(clause: &[(usize, bool)]) -> String {
clause
.iter()
.map(|(input, positive)| {
if *positive {
input.to_string()
} else {
format!("!{input}")
}
})
.collect::<Vec<_>>()
.join("|")
}
fn push_event_certificate_predicate(
lines: &mut Vec<String>,
prefix: &str,
predicate: &InputPredicate,
) {
lines.push(format!("{prefix}_clause_count={}", predicate.clauses.len()));
for (index, clause) in predicate.clauses.iter().enumerate() {
lines.push(format!(
"{prefix}_clause_{index}={}",
event_certificate_clause_text(clause)
));
}
}
fn event_contract_certificate_v3_body(certificate: &EventContractCertificateV3) -> String {
let mut lines = vec![
format!("event_contract_certificate_version={EVENT_CONTRACT_CERTIFICATE_VERSION}"),
format!("semantics={EVENT_CONTRACT_CERTIFICATE_SEMANTICS}"),
format!("proof_format={PREDICATE_CERTIFICATE_V2_PROOF_FORMAT}"),
format!("input_sha256={}", certificate.input_sha256),
format!("contract_sha256={}", certificate.contract_sha256),
format!("declared_inputs={}", certificate.declared_inputs),
format!("relevant_inputs={}", certificate.relevant_inputs.len()),
format!("latches={}", certificate.latches),
format!("horizon={}", certificate.horizon),
format!("bad_output={}", certificate.bad_output),
format!("initial_state={}", certificate.initial_state),
format!(
"result={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
}
),
format!("phase_count={}", certificate.phases.len()),
];
for (index, input) in certificate.relevant_inputs.iter().enumerate() {
lines.push(format!("relevant_{index}={input}"));
}
for (index, phase) in certificate.phases.iter().enumerate() {
lines.push(format!("phase_{index}={},{}", phase.start, phase.length));
push_event_certificate_predicate(&mut lines, &format!("phase_{index}"), &phase.predicate);
lines.push(format!(
"phase_{index}_base_rows={}",
predicate_rows_text(&phase.base_rows)
));
lines.push(format!(
"phase_{index}_powered_rows={}",
predicate_rows_text(&phase.powered_rows)
));
lines.push(format!("phase_{index}_edge_count={}", phase.edges.len()));
for (edge_index, edge) in phase.edges.iter().enumerate() {
lines.push(format!(
"phase_{index}_edge_{edge_index}={},{},{}",
edge.source, edge.target, edge.input
));
}
lines.push(format!("phase_{index}_proof_count={}", phase.proofs.len()));
for (source, proof) in phase.proofs.iter().enumerate() {
lines.push(format!(
"phase_{index}_proof_{source}={}",
predicate_bytes_hex(proof)
));
}
}
push_event_certificate_predicate(&mut lines, "terminal", &certificate.terminal);
lines.push(format!(
"terminal_safe_states={:x}",
certificate.terminal_safe_states
));
lines.push(format!(
"terminal_witness_count={}",
certificate.terminal_witnesses.len()
));
for (index, (state, input)) in certificate.terminal_witnesses.iter().enumerate() {
lines.push(format!("terminal_witness_{index}={state},{input}"));
}
lines.push(format!(
"terminal_proof={}",
predicate_bytes_hex(&certificate.terminal_proof)
));
lines.push(format!("state_count={}", certificate.states.len()));
lines.push(format!(
"states={}",
if certificate.states.is_empty() {
"-".to_string()
} else {
certificate
.states
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(",")
}
));
lines.push(format!("input_count={}", certificate.inputs.len()));
lines.push(format!(
"inputs={}",
if certificate.inputs.is_empty() {
"-".to_string()
} else {
certificate
.inputs
.iter()
.map(u64::to_string)
.collect::<Vec<_>>()
.join(",")
}
));
lines.join("\n") + "\n"
}
fn parse_predicate_number<T: std::str::FromStr>(text: &str, name: &str) -> Result<T, String> {
if text.is_empty()
|| !text.bytes().all(|byte| byte.is_ascii_digit())
|| (text.len() > 1 && text.starts_with('0'))
{
return Err(format!(
"predicate certificate `{name}` is not canonical decimal"
));
}
text.parse::<T>()
.map_err(|_| format!("invalid predicate certificate `{name}`"))
}
fn parse_predicate_number_list<T: std::str::FromStr>(
text: &str,
count: usize,
name: &str,
) -> Result<Vec<T>, String> {
if count == 0 {
if text == "-" {
return Ok(Vec::new());
}
return Err(format!(
"predicate certificate `{name}` requires sentinel -"
));
}
if text == "-" {
return Err(format!("predicate certificate `{name}` is missing"));
}
let values = text
.split(',')
.map(|part| parse_predicate_number(part, name))
.collect::<Result<Vec<_>, _>>()?;
if values.len() != count {
return Err(format!("predicate certificate `{name}` count mismatch"));
}
Ok(values)
}
fn parse_predicate_certificate(path: &Path) -> Result<PredicateCertificate, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect predicate certificate {}: {error}", path.display()))?;
if !metadata.file_type().is_file() || metadata.len() > PREDICATE_CERTIFICATE_MAX_BYTES {
return Err(format!(
"predicate certificate must be a regular file no larger than {PREDICATE_CERTIFICATE_MAX_BYTES} bytes"
));
}
let body = fs::read_to_string(path)
.map_err(|error| format!("read predicate certificate {}: {error}", path.display()))?;
if !body.ends_with('\n') || body.contains('\r') {
return Err(
"predicate certificate must use canonical newline-terminated LF text".to_string(),
);
}
let mut fields = BTreeMap::new();
for (line_number, line) in body.lines().enumerate() {
let (key, value) = line
.split_once('=')
.ok_or_else(|| format!("invalid predicate certificate line {}", line_number + 1))?;
if key.is_empty()
|| value.is_empty()
|| fields.insert(key.to_string(), value.to_string()).is_some()
{
return Err(format!(
"invalid or duplicate predicate certificate field at line {}",
line_number + 1
));
}
}
let take = |fields: &mut BTreeMap<String, String>, key: &str| {
fields
.remove(key)
.ok_or_else(|| format!("missing predicate certificate field `{key}`"))
};
if parse_predicate_number::<usize>(
&take(&mut fields, "predicate_certificate_version")?,
"predicate_certificate_version",
)? != PREDICATE_CERTIFICATE_VERSION
|| take(&mut fields, "semantics")? != "bounded-terminal-bad-avoidance"
{
return Err("unsupported predicate certificate contract".to_string());
}
let input_sha256 = take(&mut fields, "input_sha256")?;
if input_sha256.len() != 64
|| input_sha256
.bytes()
.any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
{
return Err("predicate certificate input digest is invalid".to_string());
}
let declared_inputs =
parse_predicate_number(&take(&mut fields, "declared_inputs")?, "declared_inputs")?;
let relevant_count =
parse_predicate_number::<usize>(&take(&mut fields, "relevant_inputs")?, "relevant_inputs")?;
if !(PREDICATE_INTERFACE_MIN_INPUTS..=PREDICATE_INTERFACE_MAX_INPUTS).contains(&relevant_count)
{
return Err("predicate certificate relevant input count is out of bounds".to_string());
}
let latches = parse_predicate_number::<usize>(&take(&mut fields, "latches")?, "latches")?;
if !(1..=PREDICATE_INTERFACE_MAX_LATCHES).contains(&latches) {
return Err("predicate certificate latch count is out of bounds".to_string());
}
let horizon = parse_predicate_number::<usize>(&take(&mut fields, "horizon")?, "horizon")?;
if horizon > INTERFACE_QUOTIENT_MAX_HORIZON {
return Err("predicate certificate horizon is out of bounds".to_string());
}
let bad_output = parse_predicate_number(&take(&mut fields, "bad_output")?, "bad_output")?;
let initial_state =
parse_predicate_number(&take(&mut fields, "initial_state")?, "initial_state")?;
let avoidable = match take(&mut fields, "result")?.as_str() {
"avoidable" => true,
"unavoidable" => false,
_ => return Err("predicate certificate result is invalid".to_string()),
};
let phase_count =
parse_predicate_number::<usize>(&take(&mut fields, "phase_count")?, "phase_count")?;
if phase_count > horizon.min(65) {
return Err("predicate certificate phase count is out of bounds".to_string());
}
let mut relevant_inputs = Vec::with_capacity(relevant_count);
for index in 0..relevant_count {
relevant_inputs.push(parse_predicate_number(
&take(&mut fields, &format!("relevant_{index}"))?,
"relevant input",
)?);
}
let states = 1usize << latches;
let mut phases = Vec::with_capacity(phase_count);
for index in 0..phase_count {
let value = take(&mut fields, &format!("phase_{index}"))?;
let mut parts = value.splitn(4, ',');
let start = parse_predicate_number(parts.next().unwrap_or(""), "phase start")?;
let length = parse_predicate_number(parts.next().unwrap_or(""), "phase length")?;
let constraints = parse_predicate_constraints(parts.next().unwrap_or(""), relevant_count)?;
let rows = parse_predicate_rows(parts.next().unwrap_or(""), states)?;
phases.push(PredicateCertificatePhase {
start,
length,
constraints,
rows,
});
}
let terminal_constraint =
parse_predicate_constraints(&take(&mut fields, "terminal_constraint")?, relevant_count)?;
let terminal_safe_states = parse_predicate_hex(
&take(&mut fields, "terminal_safe_states")?,
states,
"terminal_safe_states",
)?;
let state_count = parse_predicate_number(&take(&mut fields, "state_count")?, "state_count")?;
let state_values =
parse_predicate_number_list(&take(&mut fields, "states")?, state_count, "states")?;
let input_count = parse_predicate_number(&take(&mut fields, "input_count")?, "input_count")?;
let inputs = parse_predicate_number_list(&take(&mut fields, "inputs")?, input_count, "inputs")?;
if !fields.is_empty() {
return Err(format!(
"unknown predicate certificate field `{}`",
fields.keys().next().unwrap()
));
}
Ok(PredicateCertificate {
input_sha256,
declared_inputs,
relevant_inputs,
latches,
horizon,
bad_output,
initial_state,
avoidable,
phases,
terminal_constraint,
terminal_safe_states,
states: state_values,
inputs,
})
}
struct PredicateCertificateV2Reader<'a> {
lines: Vec<&'a str>,
index: usize,
}
impl<'a> PredicateCertificateV2Reader<'a> {
fn take(&mut self, key: &str) -> Result<&'a str, String> {
let line = self
.lines
.get(self.index)
.ok_or_else(|| format!("predicate certificate v2 is missing ordered field `{key}`"))?;
self.index += 1;
let (actual, value) = line
.split_once('=')
.ok_or_else(|| format!("invalid predicate certificate v2 line {}", self.index))?;
if actual != key || value.is_empty() || value.contains('=') {
return Err(format!(
"predicate certificate v2 expected `{key}` at line {}",
self.index
));
}
Ok(value)
}
fn finish(self) -> Result<(), String> {
if self.index != self.lines.len() {
return Err(format!(
"predicate certificate v2 has unexpected field at line {}",
self.index + 1
));
}
Ok(())
}
}
fn parse_predicate_certificate_v2(path: &Path) -> Result<PredicateCertificateV2, String> {
let metadata = fs::symlink_metadata(path).map_err(|error| {
format!(
"inspect predicate certificate v2 {}: {error}",
path.display()
)
})?;
if !metadata.file_type().is_file() || metadata.len() > PREDICATE_CERTIFICATE_V2_MAX_BYTES {
return Err(format!(
"predicate certificate v2 must be a regular file no larger than {PREDICATE_CERTIFICATE_V2_MAX_BYTES} bytes"
));
}
let body = fs::read_to_string(path)
.map_err(|error| format!("read predicate certificate v2 {}: {error}", path.display()))?;
if !body.ends_with('\n') || body.contains('\r') {
return Err(
"predicate certificate v2 must use canonical newline-terminated LF text".to_string(),
);
}
let mut reader = PredicateCertificateV2Reader {
lines: body.lines().collect(),
index: 0,
};
if parse_predicate_number::<usize>(
reader.take("predicate_certificate_version")?,
"predicate_certificate_version",
)? != PREDICATE_CERTIFICATE_V2_VERSION
|| reader.take("semantics")? != "bounded-terminal-bad-avoidance"
|| reader.take("proof_format")? != PREDICATE_CERTIFICATE_V2_PROOF_FORMAT
{
return Err("unsupported predicate certificate v2 contract".to_string());
}
let input_sha256 = reader.take("input_sha256")?.to_string();
if input_sha256.len() != 64
|| input_sha256
.bytes()
.any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
{
return Err("predicate certificate v2 input digest is invalid".to_string());
}
let declared_inputs =
parse_predicate_number(reader.take("declared_inputs")?, "declared_inputs")?;
let relevant_count =
parse_predicate_number::<usize>(reader.take("relevant_inputs")?, "relevant_inputs")?;
if !(PREDICATE_INTERFACE_MIN_INPUTS..=PREDICATE_INTERFACE_MAX_INPUTS).contains(&relevant_count)
{
return Err("predicate certificate v2 relevant input count is out of bounds".to_string());
}
let latches = parse_predicate_number::<usize>(reader.take("latches")?, "latches")?;
if !(1..=PREDICATE_INTERFACE_MAX_LATCHES).contains(&latches) {
return Err("predicate certificate v2 latch count is out of bounds".to_string());
}
let states = 1usize << latches;
let horizon = parse_predicate_number::<usize>(reader.take("horizon")?, "horizon")?;
if horizon > INTERFACE_QUOTIENT_MAX_HORIZON {
return Err("predicate certificate v2 horizon is out of bounds".to_string());
}
let bad_output = parse_predicate_number(reader.take("bad_output")?, "bad_output")?;
let initial_state = parse_predicate_number(reader.take("initial_state")?, "initial_state")?;
let avoidable = match reader.take("result")? {
"avoidable" => true,
"unavoidable" => false,
_ => return Err("predicate certificate v2 result is invalid".to_string()),
};
let phase_count = parse_predicate_number::<usize>(reader.take("phase_count")?, "phase_count")?;
if phase_count > horizon.min(INTERFACE_QUOTIENT_MAX_HORIZON) {
return Err("predicate certificate v2 phase count is out of bounds".to_string());
}
let mut relevant_inputs = Vec::with_capacity(relevant_count);
for index in 0..relevant_count {
relevant_inputs.push(parse_predicate_number(
reader.take(&format!("relevant_{index}"))?,
"relevant input",
)?);
}
let mut total_proof_bytes = 0usize;
let mut phases = Vec::with_capacity(phase_count);
for index in 0..phase_count {
let phase = reader.take(&format!("phase_{index}"))?;
let parts = phase.split(',').collect::<Vec<_>>();
if parts.len() != 3 {
return Err(format!("predicate certificate v2 phase {index} is invalid"));
}
let start = parse_predicate_number::<usize>(parts[0], "phase start")?;
let length = parse_predicate_number::<usize>(parts[1], "phase length")?;
let constraints = parse_predicate_constraints(parts[2], relevant_count)?;
let base_rows =
parse_predicate_rows(reader.take(&format!("phase_{index}_base_rows"))?, states)?;
let powered_rows =
parse_predicate_rows(reader.take(&format!("phase_{index}_powered_rows"))?, states)?;
let edge_count = parse_predicate_number::<usize>(
reader.take(&format!("phase_{index}_edge_count"))?,
"edge count",
)?;
if edge_count > states * states {
return Err("predicate certificate v2 edge count is out of bounds".to_string());
}
let mut edges = Vec::with_capacity(edge_count);
for edge_index in 0..edge_count {
let edge = reader.take(&format!("phase_{index}_edge_{edge_index}"))?;
let parts = edge.split(',').collect::<Vec<_>>();
if parts.len() != 3 {
return Err("predicate certificate v2 edge is invalid".to_string());
}
edges.push(PredicateCertificateV2Edge {
source: parse_predicate_number(parts[0], "edge source")?,
target: parse_predicate_number(parts[1], "edge target")?,
input: parse_predicate_number(parts[2], "edge input")?,
});
}
let proof_count = parse_predicate_number::<usize>(
reader.take(&format!("phase_{index}_proof_count"))?,
"proof count",
)?;
if proof_count != states {
return Err("predicate certificate v2 phase proof count mismatch".to_string());
}
let mut proofs = Vec::with_capacity(proof_count);
for source in 0..proof_count {
let proof = parse_predicate_bytes_hex(
reader.take(&format!("phase_{index}_proof_{source}"))?,
"phase proof",
)?;
total_proof_bytes = total_proof_bytes
.checked_add(proof.len())
.ok_or_else(|| "predicate certificate v2 proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err("predicate certificate v2 aggregate proofs exceed limit".to_string());
}
proofs.push(proof);
}
phases.push(PredicateCertificateV2Phase {
start,
length,
constraints,
base_rows,
powered_rows,
edges,
proofs,
});
}
let terminal_constraint =
parse_predicate_constraints(reader.take("terminal_constraint")?, relevant_count)?;
let terminal_safe_states = parse_predicate_hex(
reader.take("terminal_safe_states")?,
states,
"terminal_safe_states",
)?;
let terminal_witness_count = parse_predicate_number::<usize>(
reader.take("terminal_witness_count")?,
"terminal witness count",
)?;
if terminal_witness_count > states {
return Err("predicate certificate v2 terminal witness count is out of bounds".to_string());
}
let mut terminal_witnesses = Vec::with_capacity(terminal_witness_count);
for index in 0..terminal_witness_count {
let witness = reader.take(&format!("terminal_witness_{index}"))?;
let parts = witness.split(',').collect::<Vec<_>>();
if parts.len() != 2 {
return Err("predicate certificate v2 terminal witness is invalid".to_string());
}
terminal_witnesses.push((
parse_predicate_number(parts[0], "terminal witness state")?,
parse_predicate_number(parts[1], "terminal witness input")?,
));
}
let terminal_proof =
parse_predicate_bytes_hex(reader.take("terminal_proof")?, "terminal proof")?;
total_proof_bytes = total_proof_bytes
.checked_add(terminal_proof.len())
.ok_or_else(|| "predicate certificate v2 proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err("predicate certificate v2 aggregate proofs exceed limit".to_string());
}
let state_count = parse_predicate_number::<usize>(reader.take("state_count")?, "state_count")?;
if state_count > horizon + 1 {
return Err("predicate certificate v2 state count is out of bounds".to_string());
}
let state_values = parse_predicate_number_list(reader.take("states")?, state_count, "states")?;
let input_count = parse_predicate_number::<usize>(reader.take("input_count")?, "input_count")?;
if input_count > horizon + 1 {
return Err("predicate certificate v2 input count is out of bounds".to_string());
}
let inputs = parse_predicate_number_list(reader.take("inputs")?, input_count, "inputs")?;
reader.finish()?;
Ok(PredicateCertificateV2 {
input_sha256,
declared_inputs,
relevant_inputs,
latches,
horizon,
bad_output,
initial_state,
avoidable,
phases,
terminal_constraint,
terminal_safe_states,
terminal_witnesses,
terminal_proof,
states: state_values,
inputs,
})
}
struct EventContractCertificateV3Reader<'a> {
lines: Vec<&'a str>,
index: usize,
}
impl<'a> EventContractCertificateV3Reader<'a> {
fn take(&mut self, key: &str) -> Result<&'a str, String> {
let line = self.lines.get(self.index).ok_or_else(|| {
format!("event contract certificate v3 is missing ordered field `{key}`")
})?;
self.index += 1;
let (actual, value) = line
.split_once('=')
.ok_or_else(|| format!("invalid event contract certificate v3 line {}", self.index))?;
if actual != key || value.is_empty() || value.contains('=') {
return Err(format!(
"event contract certificate v3 expected `{key}` at line {}",
self.index
));
}
Ok(value)
}
fn finish(self) -> Result<(), String> {
if self.index != self.lines.len() {
return Err(format!(
"event contract certificate v3 has unexpected field at line {}",
self.index + 1
));
}
Ok(())
}
}
fn parse_event_certificate_predicate(
reader: &mut EventContractCertificateV3Reader<'_>,
prefix: &str,
relevant_inputs: usize,
) -> Result<InputPredicate, String> {
let clause_count = parse_predicate_number::<usize>(
reader.take(&format!("{prefix}_clause_count"))?,
"event clause count",
)?;
if clause_count > EVENT_CONTRACT_MAX_CLAUSES {
return Err("event contract certificate v3 clause count exceeds limit".to_string());
}
let mut clauses = Vec::with_capacity(clause_count);
for index in 0..clause_count {
let text = reader.take(&format!("{prefix}_clause_{index}"))?;
let parts = text.split('|').collect::<Vec<_>>();
if parts.is_empty()
|| parts.len() > EVENT_CONTRACT_MAX_LITERALS
|| parts.iter().any(|part| part.is_empty())
{
return Err("event contract certificate v3 clause is out of bounds".to_string());
}
let mut clause = Vec::with_capacity(parts.len());
for part in parts {
let (positive, number) = part
.strip_prefix('!')
.map_or((true, part), |number| (false, number));
let input = parse_predicate_number::<usize>(number, "event clause input")?;
if input >= relevant_inputs {
return Err(
"event contract certificate v3 clause input is out of range".to_string()
);
}
if clause.iter().any(|(existing, _)| *existing == input) {
return Err("event contract certificate v3 clause repeats an input".to_string());
}
clause.push((input, positive));
}
clause.sort_unstable();
if event_certificate_clause_text(&clause) != text
|| clauses.last().is_some_and(|previous| previous >= &clause)
{
return Err("event contract certificate v3 clauses are not canonical".to_string());
}
clauses.push(clause);
}
Ok(InputPredicate { clauses })
}
fn read_event_contract_certificate_v3(path: &Path) -> Result<String, String> {
let metadata = fs::symlink_metadata(path).map_err(|error| {
format!(
"inspect event contract certificate v3 {}: {error}",
path.display()
)
})?;
if !metadata.file_type().is_file() || metadata.len() > EVENT_CONTRACT_CERTIFICATE_MAX_BYTES {
return Err(format!(
"event contract certificate v3 must be a regular file no larger than {EVENT_CONTRACT_CERTIFICATE_MAX_BYTES} bytes"
));
}
let mut options = fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
options.custom_flags(libc::O_NOFOLLOW);
let file = options.open(path).map_err(|error| {
format!(
"open event contract certificate v3 {}: {error}",
path.display()
)
})?;
let opened = file.metadata().map_err(|error| {
format!(
"inspect open event contract certificate v3 {}: {error}",
path.display()
)
})?;
if !opened.is_file() || opened.len() > EVENT_CONTRACT_CERTIFICATE_MAX_BYTES {
return Err("event contract certificate v3 opened file exceeds limit".to_string());
}
let mut bytes = Vec::with_capacity(opened.len() as usize);
file.take(EVENT_CONTRACT_CERTIFICATE_MAX_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| {
format!(
"read event contract certificate v3 {}: {error}",
path.display()
)
})?;
if bytes.len() as u64 > EVENT_CONTRACT_CERTIFICATE_MAX_BYTES {
return Err("event contract certificate v3 read exceeds limit".to_string());
}
let body = String::from_utf8(bytes)
.map_err(|_| "event contract certificate v3 must contain valid UTF-8".to_string())?;
if !body.ends_with('\n') || body.contains('\r') {
return Err(
"event contract certificate v3 must use canonical newline-terminated LF text"
.to_string(),
);
}
Ok(body)
}
fn parse_event_contract_certificate_v3(path: &Path) -> Result<EventContractCertificateV3, String> {
let body = read_event_contract_certificate_v3(path)?;
let mut reader = EventContractCertificateV3Reader {
lines: body.lines().collect(),
index: 0,
};
if parse_predicate_number::<usize>(
reader.take("event_contract_certificate_version")?,
"event_contract_certificate_version",
)? != EVENT_CONTRACT_CERTIFICATE_VERSION
|| reader.take("semantics")? != EVENT_CONTRACT_CERTIFICATE_SEMANTICS
|| reader.take("proof_format")? != PREDICATE_CERTIFICATE_V2_PROOF_FORMAT
{
return Err("unsupported event contract certificate v3 contract".to_string());
}
let parse_digest = |text: &str, label: &str| -> Result<String, String> {
if text.len() != 64
|| text
.bytes()
.any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
{
return Err(format!("event contract certificate v3 {label} is invalid"));
}
Ok(text.to_string())
};
let input_sha256 = parse_digest(reader.take("input_sha256")?, "input digest")?;
let contract_sha256 = parse_digest(reader.take("contract_sha256")?, "contract digest")?;
let declared_inputs =
parse_predicate_number(reader.take("declared_inputs")?, "declared inputs")?;
if !(1..=INTERFACE_QUOTIENT_MAX_DECLARED_INPUTS).contains(&declared_inputs) {
return Err("event contract certificate v3 declared inputs are out of bounds".to_string());
}
let relevant_count =
parse_predicate_number::<usize>(reader.take("relevant_inputs")?, "relevant inputs")?;
if !(PREDICATE_INTERFACE_MIN_INPUTS..=PREDICATE_INTERFACE_MAX_INPUTS).contains(&relevant_count)
{
return Err("event contract certificate v3 relevant inputs are out of bounds".to_string());
}
let latches = parse_predicate_number::<usize>(reader.take("latches")?, "latches")?;
if !(1..=PREDICATE_INTERFACE_MAX_LATCHES).contains(&latches) {
return Err("event contract certificate v3 latches are out of bounds".to_string());
}
let state_total = 1usize << latches;
let horizon = parse_predicate_number::<usize>(reader.take("horizon")?, "horizon")?;
if horizon == 0 || horizon > INTERFACE_QUOTIENT_MAX_HORIZON {
return Err("event contract certificate v3 horizon is out of bounds".to_string());
}
let bad_output = parse_predicate_number(reader.take("bad_output")?, "bad output")?;
let initial_state = parse_predicate_number(reader.take("initial_state")?, "initial state")?;
if initial_state >= state_total {
return Err("event contract certificate v3 initial state is out of bounds".to_string());
}
let avoidable = match reader.take("result")? {
"avoidable" => true,
"unavoidable" => false,
_ => return Err("event contract certificate v3 result is invalid".to_string()),
};
let phase_count = parse_predicate_number::<usize>(reader.take("phase_count")?, "phase count")?;
if phase_count == 0 || phase_count > horizon.min(EVENT_CONTRACT_MAX_PHASES) {
return Err("event contract certificate v3 phase count is out of bounds".to_string());
}
let mut relevant_inputs = Vec::with_capacity(relevant_count);
for index in 0..relevant_count {
let input =
parse_predicate_number(reader.take(&format!("relevant_{index}"))?, "relevant input")?;
if input >= declared_inputs
|| relevant_inputs
.last()
.is_some_and(|previous| *previous >= input)
{
return Err(
"event contract certificate v3 relevant inputs are not canonical".to_string(),
);
}
relevant_inputs.push(input);
}
let mut total_proof_bytes = 0usize;
let mut phases = Vec::with_capacity(phase_count);
let mut expected_start = 0usize;
for index in 0..phase_count {
let phase_text = reader.take(&format!("phase_{index}"))?;
let parts = phase_text.split(',').collect::<Vec<_>>();
if parts.len() != 2 {
return Err("event contract certificate v3 phase is invalid".to_string());
}
let start: usize = parse_predicate_number(parts[0], "phase start")?;
let length: usize = parse_predicate_number(parts[1], "phase length")?;
let phase_end = start
.checked_add(length)
.ok_or_else(|| "event contract certificate v3 phase range overflow".to_string())?;
if start != expected_start || length == 0 || phase_end > horizon {
return Err(
"event contract certificate v3 phases are not a bounded partition".to_string(),
);
}
expected_start = phase_end;
let predicate = parse_event_certificate_predicate(
&mut reader,
&format!("phase_{index}"),
relevant_count,
)?;
let base_rows = parse_predicate_rows(
reader.take(&format!("phase_{index}_base_rows"))?,
state_total,
)?;
let powered_rows = parse_predicate_rows(
reader.take(&format!("phase_{index}_powered_rows"))?,
state_total,
)?;
let edge_count = parse_predicate_number::<usize>(
reader.take(&format!("phase_{index}_edge_count"))?,
"edge count",
)?;
if edge_count > state_total * state_total {
return Err("event contract certificate v3 edge count exceeds limit".to_string());
}
let mut edges = Vec::with_capacity(edge_count);
for edge_index in 0..edge_count {
let edge = reader.take(&format!("phase_{index}_edge_{edge_index}"))?;
let parts = edge.split(',').collect::<Vec<_>>();
if parts.len() != 3 {
return Err("event contract certificate v3 edge is invalid".to_string());
}
let source = parse_predicate_number(parts[0], "edge source")?;
let target = parse_predicate_number(parts[1], "edge target")?;
let input = parse_predicate_number(parts[2], "edge input")?;
if source >= state_total
|| target >= state_total
|| (declared_inputs < u64::BITS as usize && input >> declared_inputs != 0)
{
return Err("event contract certificate v3 edge value is out of bounds".to_string());
}
edges.push(PredicateCertificateV2Edge {
source,
target,
input,
});
}
let proof_count = parse_predicate_number::<usize>(
reader.take(&format!("phase_{index}_proof_count"))?,
"proof count",
)?;
if proof_count != state_total {
return Err("event contract certificate v3 proof count mismatch".to_string());
}
let mut proofs = Vec::with_capacity(proof_count);
for source in 0..proof_count {
let proof = parse_predicate_bytes_hex(
reader.take(&format!("phase_{index}_proof_{source}"))?,
"event phase proof",
)?;
total_proof_bytes = total_proof_bytes
.checked_add(proof.len())
.ok_or_else(|| "event contract certificate v3 proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err(
"event contract certificate v3 proofs exceed aggregate limit".to_string(),
);
}
proofs.push(proof);
}
phases.push(EventContractCertificatePhaseV3 {
start,
length,
predicate,
base_rows,
powered_rows,
edges,
proofs,
});
}
if expected_start != horizon {
return Err("event contract certificate v3 phases do not cover horizon".to_string());
}
let terminal = parse_event_certificate_predicate(&mut reader, "terminal", relevant_count)?;
let terminal_safe_states = parse_predicate_hex(
reader.take("terminal_safe_states")?,
state_total,
"terminal safe states",
)?;
let terminal_witness_count = parse_predicate_number::<usize>(
reader.take("terminal_witness_count")?,
"terminal witness count",
)?;
if terminal_witness_count > state_total {
return Err("event contract certificate v3 terminal witnesses exceed limit".to_string());
}
let mut terminal_witnesses = Vec::with_capacity(terminal_witness_count);
for index in 0..terminal_witness_count {
let witness = reader.take(&format!("terminal_witness_{index}"))?;
let parts = witness.split(',').collect::<Vec<_>>();
if parts.len() != 2 {
return Err("event contract certificate v3 terminal witness is invalid".to_string());
}
let state = parse_predicate_number(parts[0], "terminal witness state")?;
let input = parse_predicate_number(parts[1], "terminal witness input")?;
if state >= state_total
|| (declared_inputs < u64::BITS as usize && input >> declared_inputs != 0)
{
return Err(
"event contract certificate v3 terminal witness is out of bounds".to_string(),
);
}
terminal_witnesses.push((state, input));
}
let terminal_proof =
parse_predicate_bytes_hex(reader.take("terminal_proof")?, "event terminal proof")?;
total_proof_bytes = total_proof_bytes
.checked_add(terminal_proof.len())
.ok_or_else(|| "event contract certificate v3 proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err("event contract certificate v3 proofs exceed aggregate limit".to_string());
}
let state_count = parse_predicate_number::<usize>(reader.take("state_count")?, "state count")?;
if state_count > horizon + 1 {
return Err("event contract certificate v3 state count exceeds limit".to_string());
}
let states = parse_predicate_number_list(reader.take("states")?, state_count, "states")?;
let input_count = parse_predicate_number::<usize>(reader.take("input_count")?, "input count")?;
if input_count > horizon + 1 {
return Err("event contract certificate v3 input count exceeds limit".to_string());
}
let inputs = parse_predicate_number_list(reader.take("inputs")?, input_count, "inputs")?;
if states.iter().any(|state| *state >= state_total)
|| inputs
.iter()
.any(|input| declared_inputs < u64::BITS as usize && *input >> declared_inputs != 0)
|| (avoidable && (state_count != horizon + 1 || input_count != horizon + 1))
|| (!avoidable && (state_count != 0 || input_count != 0))
{
return Err("event contract certificate v3 trace values are out of bounds".to_string());
}
reader.finish()?;
Ok(EventContractCertificateV3 {
input_sha256,
contract_sha256,
declared_inputs,
relevant_inputs,
latches,
horizon,
bad_output,
initial_state,
avoidable,
phases,
terminal,
terminal_safe_states,
terminal_witnesses,
terminal_proof,
states,
inputs,
})
}
fn parse_predicate_transcript(path: &Path, width: usize) -> Result<Vec<Vec<Option<bool>>>, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect predicate transcript {}: {error}", path.display()))?;
if !metadata.file_type().is_file() || metadata.len() > 1_048_576 {
return Err(
"predicate transcript must be a regular file no larger than 1048576 bytes".to_string(),
);
}
let body = fs::read_to_string(path)
.map_err(|error| format!("read predicate transcript {}: {error}", path.display()))?;
if !body.ends_with('\n') || body.contains('\r') {
return Err(
"predicate transcript must use canonical newline-terminated LF text".to_string(),
);
}
let frames = body
.lines()
.map(|line| parse_predicate_constraints(line, width))
.collect::<Result<Vec<_>, _>>()?;
if frames.is_empty() || frames.len() > INTERFACE_QUOTIENT_MAX_HORIZON + 1 {
return Err("predicate transcript frame count must be in 1..=65".to_string());
}
Ok(frames)
}
fn parse_event_predicate(
fields: &mut BTreeMap<String, String>,
prefix: &str,
names: &BTreeMap<String, usize>,
) -> Result<InputPredicate, String> {
let count_key = format!("{prefix}_clause_count");
let clause_count = parse_predicate_number::<usize>(
fields
.remove(&count_key)
.ok_or_else(|| format!("event contract is missing `{count_key}`"))?
.as_str(),
&count_key,
)?;
if clause_count > EVENT_CONTRACT_MAX_CLAUSES {
return Err(format!(
"event contract `{prefix}` exceeds {EVENT_CONTRACT_MAX_CLAUSES} clauses"
));
}
let mut clauses = Vec::with_capacity(clause_count);
for clause_index in 0..clause_count {
let key = format!("{prefix}_clause_{clause_index}");
let text = fields
.remove(&key)
.ok_or_else(|| format!("event contract is missing `{key}`"))?;
let parts = text.split('|').collect::<Vec<_>>();
if parts.is_empty()
|| parts.len() > EVENT_CONTRACT_MAX_LITERALS
|| parts.iter().any(|part| part.is_empty())
{
return Err(format!(
"event contract `{key}` requires 1..={EVENT_CONTRACT_MAX_LITERALS} literals"
));
}
let mut clause = Vec::with_capacity(parts.len());
for part in parts {
let (positive, name) = part
.strip_prefix('!')
.map_or((true, part), |name| (false, name));
if name.is_empty()
|| !name.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'[' | b']')
})
{
return Err(format!("event contract `{key}` has an invalid input name"));
}
let input = *names.get(name).ok_or_else(|| {
format!("event contract `{key}` names unsupported input `{name}`")
})?;
if clause
.iter()
.any(|(existing, sign)| *existing == input && *sign != positive)
{
return Err(format!("event contract `{key}` is tautological"));
}
if clause.contains(&(input, positive)) {
return Err(format!("event contract `{key}` duplicates input `{name}`"));
}
clause.push((input, positive));
}
clause.sort_unstable();
if clauses.contains(&clause) {
return Err(format!("event contract `{key}` duplicates a clause"));
}
clauses.push(clause);
}
clauses.sort();
Ok(InputPredicate { clauses })
}
fn parse_event_contract(
path: &Path,
model: &AagModel,
relevant_inputs: &[usize],
) -> Result<EventContract, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect event contract {}: {error}", path.display()))?;
if !metadata.file_type().is_file() || metadata.len() > EVENT_CONTRACT_MAX_BYTES {
return Err(format!(
"event contract must be a regular file no larger than {EVENT_CONTRACT_MAX_BYTES} bytes"
));
}
let mut options = fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
options.custom_flags(libc::O_NOFOLLOW);
let file = options
.open(path)
.map_err(|error| format!("open event contract {}: {error}", path.display()))?;
let opened_metadata = file
.metadata()
.map_err(|error| format!("inspect open event contract {}: {error}", path.display()))?;
if !opened_metadata.is_file() || opened_metadata.len() > EVENT_CONTRACT_MAX_BYTES {
return Err(format!(
"event contract must be a regular file no larger than {EVENT_CONTRACT_MAX_BYTES} bytes"
));
}
let mut bytes = Vec::with_capacity(opened_metadata.len() as usize);
file.take(EVENT_CONTRACT_MAX_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| format!("read event contract {}: {error}", path.display()))?;
if bytes.len() as u64 > EVENT_CONTRACT_MAX_BYTES {
return Err(format!(
"event contract must be a regular file no larger than {EVENT_CONTRACT_MAX_BYTES} bytes"
));
}
let body = String::from_utf8(bytes)
.map_err(|_| "event contract must contain valid UTF-8".to_string())?;
if !body.ends_with('\n') || body.contains('\r') {
return Err("event contract must use canonical newline-terminated LF text".to_string());
}
let mut fields = BTreeMap::new();
for (index, line) in body.lines().enumerate() {
let (key, value) = line
.split_once('=')
.ok_or_else(|| format!("invalid event contract line {}", index + 1))?;
if key.is_empty()
|| value.is_empty()
|| value.contains('=')
|| fields.insert(key.to_string(), value.to_string()).is_some()
{
return Err(format!(
"invalid or duplicate event contract line {}",
index + 1
));
}
}
let (version, horizon, phase_count) = {
let mut take_number = |key: &str| -> Result<usize, String> {
parse_predicate_number(
fields
.remove(key)
.ok_or_else(|| format!("event contract is missing `{key}`"))?
.as_str(),
key,
)
};
(
take_number("event_contract_version")?,
take_number("horizon")?,
take_number("phase_count")?,
)
};
if version != EVENT_CONTRACT_VERSION {
return Err("unsupported event contract version".to_string());
}
if horizon == 0 || horizon > INTERFACE_QUOTIENT_MAX_HORIZON {
return Err(format!(
"event contract horizon must be in 1..={INTERFACE_QUOTIENT_MAX_HORIZON}"
));
}
if phase_count == 0 || phase_count > EVENT_CONTRACT_MAX_PHASES {
return Err(format!(
"event contract phase count must be in 1..={EVENT_CONTRACT_MAX_PHASES}"
));
}
let mut names = BTreeMap::new();
for (projected, declared) in relevant_inputs.iter().enumerate() {
let name = model
.input_names
.get(*declared)
.ok_or_else(|| "event contract input symbol is missing".to_string())?;
if names.insert(name.clone(), projected).is_some() {
return Err(format!("event contract input name `{name}` is ambiguous"));
}
}
let mut phases = Vec::with_capacity(phase_count);
let mut expected_start = 0usize;
for phase_index in 0..phase_count {
let phase_key = format!("phase_{phase_index}");
let value = fields
.remove(&phase_key)
.ok_or_else(|| format!("event contract is missing `{phase_key}`"))?;
let parts = value.split(',').collect::<Vec<_>>();
if parts.len() != 2 {
return Err(format!("event contract `{phase_key}` is invalid"));
}
let start = parse_predicate_number::<usize>(parts[0], "event phase start")?;
let length = parse_predicate_number::<usize>(parts[1], "event phase length")?;
let end = start
.checked_add(length)
.ok_or_else(|| format!("event contract `{phase_key}` exceeds the bounded horizon"))?;
if start != expected_start || length == 0 || end > horizon {
return Err(format!(
"event contract `{phase_key}` does not form a contiguous bounded partition"
));
}
let predicate = parse_event_predicate(&mut fields, &phase_key, &names)?;
phases.push(EventContractPhase {
start,
length,
predicate,
});
expected_start = end;
}
if expected_start != horizon {
return Err("event contract phases do not cover the horizon".to_string());
}
let terminal = parse_event_predicate(&mut fields, "terminal", &names)?;
if let Some(key) = fields.keys().next() {
return Err(format!("unknown event contract field `{key}`"));
}
Ok(EventContract {
horizon,
phases,
terminal,
})
}
fn event_contract_frames(contract: &EventContract) -> Vec<&InputPredicate> {
let mut frames = Vec::with_capacity(contract.horizon + 1);
for phase in &contract.phases {
frames.extend(std::iter::repeat_n(&phase.predicate, phase.length));
}
frames.push(&contract.terminal);
frames
}
fn solve_event_contract_cdcl(
model: &AagModel,
relevant_inputs: &[usize],
bad_output: usize,
contract: &EventContract,
) -> Result<bool, String> {
let mut encoding = aag_bmc_encoding(model, contract.horizon)?;
for (frame, predicate) in event_contract_frames(contract).into_iter().enumerate() {
for clause in &predicate.clauses {
encoding.clauses.push(Clause(
clause
.iter()
.map(|(projected, positive)| {
let declared = relevant_inputs[*projected];
(
frame * model.max_variable + model.inputs[declared] / 2 - 1,
*positive,
)
})
.collect(),
));
}
}
let mut assumptions = vec![None; encoding.variables];
let target = aag_cnf_literal(model, contract.horizon, model.outputs[bad_output]);
match target {
AagCnfLiteral::Constant(false) => {}
AagCnfLiteral::Constant(true) => return Ok(false),
AagCnfLiteral::Variable(_) => {
if add_causal_counterfactual_assumption(&mut assumptions, target)? {
return Ok(false);
}
}
}
let mut solver = Solver::new();
add_to_varisat(&mut solver, &encoding.clauses);
solve_causal_persistent(&mut solver, &assumptions)
}
fn replay_event_contract_witness(
model: &AagModel,
relevant_inputs: &[usize],
bad_output: usize,
contract: &EventContract,
witness: &InterfaceQueryResult,
) -> Result<(), String> {
if witness.states.len() != contract.horizon + 1
|| witness.declared_inputs.len() != contract.horizon + 1
{
return Err("event contract witness dimensions are invalid".to_string());
}
let expected_initial = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
if witness.states[0] != expected_initial {
return Err("event contract witness initial state is invalid".to_string());
}
let frames = event_contract_frames(contract);
let mut checker = IndependentPredicateChecker::new(model)?;
for (frame, predicate) in frames.iter().enumerate().take(contract.horizon + 1) {
let declared = witness.declared_inputs[frame];
let projected =
relevant_inputs
.iter()
.enumerate()
.fold(0usize, |input, (bit, declared_index)| {
input | (((declared >> declared_index) & 1) as usize) << bit
});
if !predicate.allows(projected) {
return Err(format!(
"event contract witness violates its predicate at frame {frame}"
));
}
let (next, bad) = checker.evaluate(witness.states[frame], declared)?;
if frame < contract.horizon && next != witness.states[frame + 1] {
return Err(format!(
"event contract witness transition is invalid at frame {frame}"
));
}
if frame == contract.horizon && bad & (1u128 << bad_output) != 0 {
return Err("event contract terminal witness reaches the bad output".to_string());
}
}
Ok(())
}
fn benchmark_aiger_event_contract(
input: &Path,
bad_output: usize,
contract_path: &Path,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=100).contains(&repeats) {
return Err("event contract benchmark repeats must be in 1..=100".to_string());
}
if output.exists() {
return Err("event contract benchmark refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err("event contract requires declared initial latch values".to_string());
}
if bad_output >= model.outputs.len() {
return Err("event contract bad output is out of range".to_string());
}
let compile_start = Instant::now();
let mut quotient = PredicateQuotient::new(&model)?;
let relevant_inputs = quotient.interface.projected_inputs.clone();
let contract = parse_event_contract(contract_path, &model, &relevant_inputs)?;
let compile_ns = compile_start.elapsed().as_nanos();
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
let input_sha256 = sha256_file(input)?;
let contract_sha256 = sha256_file(contract_path)?;
let clause_count = contract
.phases
.iter()
.map(|phase| phase.predicate.clauses.len())
.sum::<usize>()
+ contract.terminal.clauses.len();
let mut lines = vec!["schema_version,input_sha256,contract_sha256,bad_output,horizon,relevant_inputs,latches,phases,clauses,trial,compile_ns,predicate_query_ns,cdcl_query_ns,result,answers_agree,witness_valid,status".to_string()];
for trial in 0..repeats {
let predicate_start = Instant::now();
let witness = quotient.query_event_contract(initial_state, bad_output, &contract)?;
let predicate_query_ns = predicate_start.elapsed().as_nanos();
let cdcl_start = Instant::now();
let cdcl_avoidable =
solve_event_contract_cdcl(&model, &relevant_inputs, bad_output, &contract)?;
let cdcl_query_ns = cdcl_start.elapsed().as_nanos();
let avoidable = witness.is_some();
if avoidable != cdcl_avoidable {
return Err("event contract predicate and CDCL answers disagree".to_string());
}
if let Some(witness) = &witness {
replay_event_contract_witness(
&model,
&relevant_inputs,
bad_output,
&contract,
witness,
)?;
}
lines.push(format!(
"{EVENT_CONTRACT_BENCHMARK_SCHEMA_VERSION},{input_sha256},{contract_sha256},{bad_output},{},{},{},{},{clause_count},{trial},{compile_ns},{predicate_query_ns},{cdcl_query_ns},{},true,true,ok",
contract.horizon,
relevant_inputs.len(),
model.latches.len(),
contract.phases.len(),
if avoidable { "avoidable" } else { "unavoidable" },
));
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"event-contract-benchmark status=VALID trials={repeats} output={}",
output.display()
);
Ok(())
}
#[derive(Debug)]
struct EventContractProofMetrics {
producer_ns: u128,
proof_generation_ns: u128,
proof_verification_ns: u128,
obligations: usize,
witnesses: usize,
proof_bytes: usize,
}
fn declared_input_satisfies_predicate(
declared_input_count: usize,
relevant_inputs: &[usize],
predicate: &InputPredicate,
declared: u64,
) -> bool {
if declared_input_count < u64::BITS as usize && declared >> declared_input_count != 0 {
return false;
}
let projected = relevant_inputs
.iter()
.enumerate()
.fold(0usize, |input, (bit, declared_index)| {
input | (((declared >> declared_index) & 1) as usize) << bit
});
predicate.allows(projected)
}
fn event_contract_proof_experiment(
model: &AagModel,
bad_output: usize,
contract: &EventContract,
) -> Result<EventContractProofMetrics, String> {
if bad_output >= model.outputs.len() {
return Err("event contract proof output is out of range".to_string());
}
let producer_start = Instant::now();
let mut quotient = PredicateQuotient::new(model)?;
let relevant_inputs = quotient.interface.projected_inputs.clone();
let states_count = 1usize << model.latches.len();
let mut phase_rows = Vec::with_capacity(contract.phases.len());
let mut edge_witnesses = Vec::new();
for (phase_index, phase) in contract.phases.iter().enumerate() {
let relation = quotient.interface.relation_predicate(&phase.predicate)?;
let rows = relation
.rows()
.iter()
.map(|row| row[0] as u16)
.collect::<Vec<_>>();
for (source, &targets) in rows.iter().enumerate() {
for target in 0..states_count {
if targets & (1u16 << target) != 0 {
let input = quotient
.interface
.witness_input_predicate(source, Some(target), None, &phase.predicate)?
.ok_or_else(|| {
"event contract proof relation edge lacks witness".to_string()
})?;
edge_witnesses.push((source, target, input, phase_index));
}
}
}
phase_rows.push((phase.predicate.clone(), rows));
}
let mut terminal_safe_states = 0u16;
let mut terminal_witnesses = Vec::new();
for state in 0..states_count {
if let Some(input) = quotient.interface.witness_input_predicate(
state,
None,
Some(bad_output),
&contract.terminal,
)? {
terminal_safe_states |= 1u16 << state;
terminal_witnesses.push((state, input));
}
}
let producer_ns = producer_start.elapsed().as_nanos();
let generation_start = Instant::now();
let mut obligations = Vec::with_capacity(phase_rows.len() * states_count + 1);
let mut total_proof_bytes = 0usize;
for (predicate, rows) in &phase_rows {
for (source, &targets) in rows.iter().enumerate() {
let clauses = predicate_relation_completeness_clauses_for_predicate(
model,
&relevant_inputs,
source,
predicate,
targets,
)?;
let proof = generate_varisat_unsat_proof(&clauses)?;
if proof.len() > PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES {
return Err("event contract proof exceeds the individual limit".to_string());
}
total_proof_bytes = total_proof_bytes
.checked_add(proof.len())
.ok_or_else(|| "event contract aggregate proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err("event contract proofs exceed the aggregate limit".to_string());
}
obligations.push((clauses, proof));
}
}
let terminal_clauses = predicate_terminal_completeness_clauses_for_predicate(
model,
&relevant_inputs,
&contract.terminal,
bad_output,
terminal_safe_states,
)?;
let terminal_proof = generate_varisat_unsat_proof(&terminal_clauses)?;
if terminal_proof.len() > PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES {
return Err("event contract terminal proof exceeds the individual limit".to_string());
}
total_proof_bytes = total_proof_bytes
.checked_add(terminal_proof.len())
.ok_or_else(|| "event contract aggregate proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err("event contract proofs exceed the aggregate limit".to_string());
}
obligations.push((terminal_clauses, terminal_proof));
let proof_generation_ns = generation_start.elapsed().as_nanos();
let verification_start = Instant::now();
let mut checker = IndependentPredicateChecker::new(model)?;
for (source, target, input, phase_index) in &edge_witnesses {
let predicate = &contract.phases[*phase_index].predicate;
if !declared_input_satisfies_predicate(
model.inputs.len(),
&relevant_inputs,
predicate,
*input,
) || checker.evaluate(*source, *input)?.0 != *target
{
return Err("event contract proof relation witness is invalid".to_string());
}
}
for &(state, input) in &terminal_witnesses {
if !declared_input_satisfies_predicate(
model.inputs.len(),
&relevant_inputs,
&contract.terminal,
input,
) || checker.evaluate(state, input)?.1 & (1u128 << bad_output) != 0
{
return Err("event contract proof terminal witness is invalid".to_string());
}
}
for (clauses, proof) in &obligations {
verify_varisat_unsat_proof(clauses, proof)?;
}
let proof_verification_ns = verification_start.elapsed().as_nanos();
Ok(EventContractProofMetrics {
producer_ns,
proof_generation_ns,
proof_verification_ns,
obligations: obligations.len(),
witnesses: edge_witnesses.len() + terminal_witnesses.len(),
proof_bytes: total_proof_bytes,
})
}
fn benchmark_aiger_event_contract_proofs(
input: &Path,
bad_output: usize,
contract_path: &Path,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=100).contains(&repeats) {
return Err("event contract proof benchmark repeats must be in 1..=100".to_string());
}
if output.exists() {
return Err("event contract proof benchmark refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let contract = parse_event_contract(contract_path, &model, &relevant_inputs)?;
let input_sha256 = sha256_file(input)?;
let contract_sha256 = sha256_file(contract_path)?;
let mut lines = vec!["schema_version,input_sha256,contract_sha256,bad_output,horizon,relevant_inputs,latches,phases,trial,producer_ns,proof_generation_ns,proof_verification_ns,obligations,witnesses,proof_bytes,status".to_string()];
for trial in 0..repeats {
let metrics = event_contract_proof_experiment(&model, bad_output, &contract)?;
lines.push(format!(
"1,{input_sha256},{contract_sha256},{bad_output},{},{},{},{},{trial},{},{},{},{},{},{},ok",
contract.horizon,
relevant_inputs.len(),
model.latches.len(),
contract.phases.len(),
metrics.producer_ns,
metrics.proof_generation_ns,
metrics.proof_verification_ns,
metrics.obligations,
metrics.witnesses,
metrics.proof_bytes,
));
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"event-contract-proof-benchmark status=VALID trials={repeats} output={}",
output.display()
);
Ok(())
}
fn certify_aiger_event_contract_v3(
input: &Path,
bad_output: usize,
contract_path: &Path,
certificate_path: &Path,
) -> Result<(), String> {
certify_aiger_event_contract_v3_with_node_limit(
input,
bad_output,
contract_path,
certificate_path,
PREDICATE_INTERFACE_MAX_BDD_NODES,
)
}
fn certify_aiger_event_contract_v3_with_node_limit(
input: &Path,
bad_output: usize,
contract_path: &Path,
certificate_path: &Path,
predicate_node_limit: usize,
) -> Result<(), String> {
if fs::symlink_metadata(certificate_path).is_ok() {
return Err("event contract certificate v3 refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err("event contract certificate v3 requires declared initial latches".to_string());
}
if bad_output >= model.outputs.len() {
return Err("event contract certificate v3 bad output is out of range".to_string());
}
let mut quotient = PredicateQuotient::new_with_node_limit(&model, predicate_node_limit)?;
let relevant_inputs = quotient.interface.projected_inputs.clone();
let contract = parse_event_contract(contract_path, &model, &relevant_inputs)?;
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
let witness = quotient.query_event_contract(initial_state, bad_output, &contract)?;
let states_count = 1usize << model.latches.len();
let mut total_proof_bytes = 0usize;
let mut phases = Vec::with_capacity(contract.phases.len());
for phase in &contract.phases {
let base = quotient.interface.relation_predicate(&phase.predicate)?;
let base_rows = base
.rows()
.iter()
.map(|row| row[0] as u16)
.collect::<Vec<_>>();
let powered = InterfaceRelation::power(&base, phase.length)?;
let powered_rows = powered
.rows()
.iter()
.map(|row| row[0] as u16)
.collect::<Vec<_>>();
let mut edges = Vec::new();
let mut proofs = Vec::with_capacity(states_count);
for (source, &targets) in base_rows.iter().enumerate() {
for target in 0..states_count {
if targets & (1u16 << target) != 0 {
let edge_input = quotient
.interface
.witness_input_predicate(source, Some(target), None, &phase.predicate)?
.ok_or_else(|| {
"event contract certificate v3 relation edge lacks witness".to_string()
})?;
edges.push(PredicateCertificateV2Edge {
source,
target,
input: edge_input,
});
}
}
let clauses = predicate_relation_completeness_clauses_for_predicate(
&model,
&relevant_inputs,
source,
&phase.predicate,
targets,
)?;
let proof = generate_varisat_unsat_proof(&clauses)?;
if proof.len() > PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES {
return Err("event contract certificate v3 phase proof exceeds limit".to_string());
}
total_proof_bytes = total_proof_bytes
.checked_add(proof.len())
.ok_or_else(|| "event contract certificate v3 proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err(
"event contract certificate v3 proofs exceed aggregate limit".to_string(),
);
}
proofs.push(proof);
}
phases.push(EventContractCertificatePhaseV3 {
start: phase.start,
length: phase.length,
predicate: phase.predicate.clone(),
base_rows,
powered_rows,
edges,
proofs,
});
}
let mut terminal_safe_states = 0u16;
let mut terminal_witnesses = Vec::new();
for state in 0..states_count {
if let Some(terminal_input) = quotient.interface.witness_input_predicate(
state,
None,
Some(bad_output),
&contract.terminal,
)? {
terminal_safe_states |= 1u16 << state;
terminal_witnesses.push((state, terminal_input));
}
}
let terminal_clauses = predicate_terminal_completeness_clauses_for_predicate(
&model,
&relevant_inputs,
&contract.terminal,
bad_output,
terminal_safe_states,
)?;
let terminal_proof = generate_varisat_unsat_proof(&terminal_clauses)?;
if terminal_proof.len() > PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES {
return Err("event contract certificate v3 terminal proof exceeds limit".to_string());
}
total_proof_bytes = total_proof_bytes
.checked_add(terminal_proof.len())
.ok_or_else(|| "event contract certificate v3 proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err("event contract certificate v3 proofs exceed aggregate limit".to_string());
}
let (states, inputs) = witness
.map(|witness| (witness.states, witness.declared_inputs))
.unwrap_or_default();
let certificate = EventContractCertificateV3 {
input_sha256: sha256_file(input)?,
contract_sha256: sha256_file(contract_path)?,
declared_inputs: model.inputs.len(),
relevant_inputs,
latches: model.latches.len(),
horizon: contract.horizon,
bad_output,
initial_state,
avoidable: !states.is_empty(),
phases,
terminal: contract.terminal,
terminal_safe_states,
terminal_witnesses,
terminal_proof,
states,
inputs,
};
let body = event_contract_certificate_v3_body(&certificate);
if body.len() as u64 > EVENT_CONTRACT_CERTIFICATE_MAX_BYTES {
return Err("event contract certificate v3 serialized artifact exceeds limit".to_string());
}
publish_causal_comparison(certificate_path, body.as_bytes())?;
println!(
"event-contract-certificate-v3 status=CREATED result={} proofs={} proof_bytes={} output={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
},
certificate
.phases
.iter()
.map(|phase| phase.proofs.len())
.sum::<usize>()
+ 1,
total_proof_bytes,
certificate_path.display()
);
Ok(())
}
fn verify_aiger_event_contract_certificate_v3(
input: &Path,
contract_path: &Path,
certificate_path: &Path,
) -> Result<(), String> {
let model = parse_aag(input)?;
let certificate = parse_event_contract_certificate_v3(certificate_path)?;
if sha256_file(input)? != certificate.input_sha256
|| sha256_file(contract_path)? != certificate.contract_sha256
{
return Err("event contract certificate v3 source binding mismatch".to_string());
}
let mut checker = IndependentPredicateChecker::new(&model)?;
let contract = parse_event_contract(contract_path, &model, &checker.relevant_inputs)?;
if certificate.declared_inputs != model.inputs.len()
|| certificate.relevant_inputs != checker.relevant_inputs
|| certificate.latches != model.latches.len()
|| certificate.horizon != contract.horizon
|| certificate.bad_output >= model.outputs.len()
|| certificate.phases.len() != contract.phases.len()
|| certificate.terminal != contract.terminal
{
return Err("event contract certificate v3 source dimensions mismatch".to_string());
}
let expected_initial =
model
.latches
.iter()
.enumerate()
.try_fold(0usize, |state, (bit, latch)| {
latch
.initial
.map(|value| state | (usize::from(value) << bit))
.ok_or_else(|| {
"event contract certificate v3 source has undeclared initial latch"
.to_string()
})
})?;
if certificate.initial_state != expected_initial {
return Err("event contract certificate v3 initial state mismatch".to_string());
}
let mut composed = (0..checker.states)
.map(|state| 1u16 << state)
.collect::<Vec<_>>();
let mut frame_predicates = Vec::with_capacity(certificate.horizon + 1);
let mut expected_start = 0usize;
let mut checked_proofs = 0usize;
for (phase_index, (phase, source_phase)) in
certificate.phases.iter().zip(&contract.phases).enumerate()
{
if phase.start != expected_start
|| phase.start != source_phase.start
|| phase.length == 0
|| phase.length != source_phase.length
|| phase.predicate != source_phase.predicate
{
return Err("event contract certificate v3 phase binding mismatch".to_string());
}
expected_start = expected_start
.checked_add(phase.length)
.ok_or_else(|| "event contract certificate v3 phase length overflow".to_string())?;
if expected_start > certificate.horizon
|| phase.base_rows.len() != checker.states
|| phase.powered_rows.len() != checker.states
|| phase.proofs.len() != checker.states
{
return Err("event contract certificate v3 phase dimensions mismatch".to_string());
}
let mut expected_edges = Vec::new();
for (source, &targets) in phase.base_rows.iter().enumerate() {
for target in 0..checker.states {
if targets & (1u16 << target) != 0 {
expected_edges.push((source, target));
}
}
let clauses = predicate_relation_completeness_clauses_for_predicate(
&model,
&checker.relevant_inputs,
source,
&phase.predicate,
targets,
)?;
verify_varisat_unsat_proof(&clauses, &phase.proofs[source]).map_err(|error| {
format!(
"event contract certificate v3 phase {phase_index} source {source}: {error}"
)
})?;
checked_proofs += 1;
}
if phase.edges.len() != expected_edges.len() {
return Err("event contract certificate v3 edge count mismatch".to_string());
}
for (edge, &(source, target)) in phase.edges.iter().zip(&expected_edges) {
if (edge.source, edge.target) != (source, target)
|| !declared_input_satisfies_predicate(
model.inputs.len(),
&checker.relevant_inputs,
&phase.predicate,
edge.input,
)
|| checker.evaluate(source, edge.input)?.0 != target
{
return Err("event contract certificate v3 edge witness is invalid".to_string());
}
}
let expected_power = IndependentPredicateChecker::power(&phase.base_rows, phase.length)?;
if expected_power != phase.powered_rows {
return Err("event contract certificate v3 powered relation mismatch".to_string());
}
composed = IndependentPredicateChecker::compose(&composed, &phase.powered_rows)?;
frame_predicates.extend(std::iter::repeat_n(phase.predicate.clone(), phase.length));
}
if expected_start != certificate.horizon {
return Err("event contract certificate v3 phases do not cover horizon".to_string());
}
frame_predicates.push(certificate.terminal.clone());
let expected_terminal_states = (0..checker.states)
.filter(|state| certificate.terminal_safe_states & (1u16 << state) != 0)
.collect::<Vec<_>>();
if certificate.terminal_witnesses.len() != expected_terminal_states.len() {
return Err("event contract certificate v3 terminal witness count mismatch".to_string());
}
for (&expected_state, &(state, terminal_input)) in expected_terminal_states
.iter()
.zip(&certificate.terminal_witnesses)
{
if state != expected_state
|| !declared_input_satisfies_predicate(
model.inputs.len(),
&checker.relevant_inputs,
&certificate.terminal,
terminal_input,
)
|| checker.evaluate(state, terminal_input)?.1 & (1u128 << certificate.bad_output) != 0
{
return Err("event contract certificate v3 terminal witness is invalid".to_string());
}
}
let terminal_clauses = predicate_terminal_completeness_clauses_for_predicate(
&model,
&checker.relevant_inputs,
&certificate.terminal,
certificate.bad_output,
certificate.terminal_safe_states,
)?;
verify_varisat_unsat_proof(&terminal_clauses, &certificate.terminal_proof)
.map_err(|error| format!("event contract certificate v3 terminal: {error}"))?;
checked_proofs += 1;
if certificate.avoidable {
if certificate.states.len() != certificate.horizon + 1
|| certificate.inputs.len() != certificate.horizon + 1
|| certificate.states[0] != certificate.initial_state
{
return Err("event contract certificate v3 trace dimensions mismatch".to_string());
}
for (frame, predicate) in frame_predicates.iter().enumerate() {
let state = certificate.states[frame];
let trace_input = certificate.inputs[frame];
if state >= checker.states
|| !declared_input_satisfies_predicate(
model.inputs.len(),
&checker.relevant_inputs,
predicate,
trace_input,
)
{
return Err(format!(
"event contract certificate v3 trace is invalid at frame {frame}"
));
}
let (next, bad) = checker.evaluate(state, trace_input)?;
if frame < certificate.horizon {
if next != certificate.states[frame + 1] {
return Err(format!(
"event contract certificate v3 transition mismatch at frame {frame}"
));
}
} else if bad & (1u128 << certificate.bad_output) != 0 {
return Err("event contract certificate v3 terminal trace is bad".to_string());
}
}
let terminal_state = *certificate.states.last().unwrap();
if composed[certificate.initial_state] & (1u16 << terminal_state) == 0
|| certificate.terminal_safe_states & (1u16 << terminal_state) == 0
{
return Err("event contract certificate v3 trace is absent from evidence".to_string());
}
} else {
if !certificate.states.is_empty() || !certificate.inputs.is_empty() {
return Err(
"event contract certificate v3 unavoidable result contains trace".to_string(),
);
}
if composed[certificate.initial_state] & certificate.terminal_safe_states != 0 {
return Err(
"event contract certificate v3 unavoidable claim has safe terminal".to_string(),
);
}
}
println!(
"event-contract-certificate-v3 status=VERIFIED result={} proofs={} direct_evaluations={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
},
checked_proofs,
checker.evaluations
);
Ok(())
}
fn event_contract_resource_error(error: &str) -> bool {
predicate_resource_error(error)
|| error == "event contract certificate v3 phase proof exceeds limit"
|| error == "event contract certificate v3 terminal proof exceeds limit"
|| error == "event contract certificate v3 proofs exceed aggregate limit"
|| error == "event contract certificate v3 serialized artifact exceeds limit"
}
#[allow(clippy::too_many_arguments)]
fn verify_aiger_event_contract_portfolio(
input: &Path,
bad_output: usize,
contract_path: &Path,
report_path: &Path,
certificate_path: &Path,
) -> Result<(), String> {
verify_aiger_event_contract_portfolio_with_node_limit(
input,
bad_output,
contract_path,
report_path,
certificate_path,
PREDICATE_INTERFACE_MAX_BDD_NODES,
)
}
#[allow(clippy::too_many_arguments)]
fn verify_aiger_event_contract_portfolio_with_node_limit(
input: &Path,
bad_output: usize,
contract_path: &Path,
report_path: &Path,
certificate_path: &Path,
predicate_node_limit: usize,
) -> Result<(), String> {
if fs::symlink_metadata(report_path).is_ok() || fs::symlink_metadata(certificate_path).is_ok() {
return Err("event contract portfolio refuses to overwrite output artifacts".to_string());
}
let model = parse_aag(input)?;
if bad_output >= model.outputs.len() {
return Err("event contract portfolio bad output is out of range".to_string());
}
let gate_start = Instant::now();
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let contract = parse_event_contract(contract_path, &model, &relevant_inputs)?;
let admitted = event_contract_v3_admitted(
relevant_inputs.len(),
model.latches.len(),
contract.horizon,
model.latches.iter().all(|latch| latch.initial.is_some()),
);
let gate_ns = gate_start.elapsed().as_nanos();
let backend_start = Instant::now();
let mut certificate_verified = false;
let mut verifier_ns = 0u128;
let (avoidable, backend, reason) = if admitted {
match certify_aiger_event_contract_v3_with_node_limit(
input,
bad_output,
contract_path,
certificate_path,
predicate_node_limit,
) {
Ok(()) => {
let verify_start = Instant::now();
if let Err(error) = verify_aiger_event_contract_certificate_v3(
input,
contract_path,
certificate_path,
) {
let _ = fs::remove_file(certificate_path);
return Err(error);
}
verifier_ns = verify_start.elapsed().as_nanos();
certificate_verified = true;
let certificate = match parse_event_contract_certificate_v3(certificate_path) {
Ok(certificate) => certificate,
Err(error) => {
let _ = fs::remove_file(certificate_path);
return Err(error);
}
};
(
certificate.avoidable,
"event-contract-certificate-v3",
"static-admission",
)
}
Err(error) if event_contract_resource_error(&error) => {
let _ = fs::remove_file(certificate_path);
let avoidable =
solve_event_contract_cdcl(&model, &relevant_inputs, bad_output, &contract)?;
(
avoidable,
"persistent-cdcl",
"event-contract-resource-fallback",
)
}
Err(error) => return Err(error),
}
} else {
let avoidable = solve_event_contract_cdcl(&model, &relevant_inputs, bad_output, &contract)?;
(avoidable, "persistent-cdcl", "static-rejection")
};
let backend_ns = backend_start.elapsed().as_nanos();
let body = format!(
"event_contract_portfolio_version={EVENT_CONTRACT_PORTFOLIO_VERSION}\ninput_sha256={}\ncontract_sha256={}\nbad_output={bad_output}\nhorizon={}\nrelevant_inputs={}\nlatches={}\nadmitted={}\nbackend={backend}\nreason={reason}\nresult={}\ncertificate={}\ncertificate_verified={}\ngate_ns={gate_ns}\nbackend_ns={backend_ns}\nverifier_ns={verifier_ns}\nstatus=ok\n",
sha256_file(input)?,
sha256_file(contract_path)?,
contract.horizon,
relevant_inputs.len(),
model.latches.len(),
usize::from(admitted),
if avoidable {
"avoidable"
} else {
"unavoidable"
},
if certificate_verified { "present" } else { "-" },
usize::from(certificate_verified),
);
if let Err(error) = publish_causal_comparison(report_path, body.as_bytes()) {
if certificate_verified {
let _ = fs::remove_file(certificate_path);
}
return Err(error);
}
println!(
"event-contract-portfolio status=VERIFIED result={} backend={backend} reason={reason} report={}",
if avoidable {
"avoidable"
} else {
"unavoidable"
},
report_path.display()
);
Ok(())
}
#[derive(Debug, Eq, PartialEq)]
struct EventContractPortfolioReport {
input_sha256: String,
contract_sha256: String,
bad_output: usize,
horizon: usize,
relevant_inputs: usize,
latches: usize,
admitted: bool,
backend: String,
reason: String,
avoidable: bool,
certificate_present: bool,
certificate_verified: bool,
gate_ns: u128,
backend_ns: u128,
verifier_ns: u128,
}
fn read_event_contract_portfolio_report(path: &Path) -> Result<String, String> {
const MAX_BYTES: u64 = 16 * 1024;
let metadata = fs::symlink_metadata(path).map_err(|error| {
format!(
"inspect event contract portfolio report {}: {error}",
path.display()
)
})?;
if !metadata.file_type().is_file() || metadata.len() > MAX_BYTES {
return Err(
"event contract portfolio report must be a regular file no larger than 16384 bytes"
.to_string(),
);
}
let mut options = fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
options.custom_flags(libc::O_NOFOLLOW);
let file = options.open(path).map_err(|error| {
format!(
"open event contract portfolio report {}: {error}",
path.display()
)
})?;
let opened = file.metadata().map_err(|error| {
format!(
"inspect open event contract portfolio report {}: {error}",
path.display()
)
})?;
if !opened.is_file() || opened.len() > MAX_BYTES {
return Err("event contract portfolio opened report exceeds limit".to_string());
}
let mut bytes = Vec::with_capacity(opened.len() as usize);
file.take(MAX_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|error| {
format!(
"read event contract portfolio report {}: {error}",
path.display()
)
})?;
if bytes.len() as u64 > MAX_BYTES {
return Err("event contract portfolio report read exceeds limit".to_string());
}
String::from_utf8(bytes)
.map_err(|_| "event contract portfolio report must contain valid UTF-8".to_string())
}
fn parse_event_contract_portfolio_bool(value: &str, name: &str) -> Result<bool, String> {
match value {
"0" => Ok(false),
"1" => Ok(true),
_ => Err(format!(
"event contract portfolio `{name}` must be canonical 0 or 1"
)),
}
}
fn parse_event_contract_portfolio_number<T: std::str::FromStr>(
value: &str,
name: &str,
) -> Result<T, String> {
if value.is_empty()
|| !value.bytes().all(|byte| byte.is_ascii_digit())
|| (value.len() > 1 && value.starts_with('0'))
{
return Err(format!(
"event contract portfolio `{name}` is not canonical decimal"
));
}
value
.parse()
.map_err(|_| format!("invalid event contract portfolio `{name}`"))
}
fn parse_event_contract_portfolio_report(
path: &Path,
) -> Result<EventContractPortfolioReport, String> {
const KEYS: [&str; 16] = [
"event_contract_portfolio_version",
"input_sha256",
"contract_sha256",
"bad_output",
"horizon",
"relevant_inputs",
"latches",
"admitted",
"backend",
"reason",
"result",
"certificate",
"certificate_verified",
"gate_ns",
"backend_ns",
"verifier_ns",
];
let body = read_event_contract_portfolio_report(path)?;
if !body.ends_with('\n') || body.contains('\r') {
return Err(
"event contract portfolio report must use canonical newline-terminated LF text"
.to_string(),
);
}
let lines = body.lines().collect::<Vec<_>>();
if lines.len() != KEYS.len() + 1 || lines.last() != Some(&"status=ok") {
return Err(
"event contract portfolio report has invalid field count or status".to_string(),
);
}
let mut values = Vec::with_capacity(KEYS.len());
for (index, key) in KEYS.iter().enumerate() {
let value = lines[index]
.strip_prefix(&format!("{key}="))
.filter(|value| !value.is_empty())
.ok_or_else(|| {
format!(
"event contract portfolio report expected `{key}` at line {}",
index + 1
)
})?;
values.push(value);
}
if values[0] != EVENT_CONTRACT_PORTFOLIO_VERSION.to_string() {
return Err("unsupported event contract portfolio report version".to_string());
}
let parse_digest = |value: &str, label: &str| -> Result<String, String> {
if value.len() != 64
|| value
.bytes()
.any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
{
return Err(format!(
"event contract portfolio {label} digest is invalid"
));
}
Ok(value.to_string())
};
let admitted = parse_event_contract_portfolio_bool(values[7], "admitted")?;
let avoidable = match values[10] {
"avoidable" => true,
"unavoidable" => false,
_ => return Err("event contract portfolio result is invalid".to_string()),
};
let certificate_present = match values[11] {
"present" => true,
"-" => false,
_ => return Err("event contract portfolio certificate field is invalid".to_string()),
};
Ok(EventContractPortfolioReport {
input_sha256: parse_digest(values[1], "input")?,
contract_sha256: parse_digest(values[2], "contract")?,
bad_output: parse_event_contract_portfolio_number(values[3], "bad_output")?,
horizon: parse_event_contract_portfolio_number(values[4], "horizon")?,
relevant_inputs: parse_event_contract_portfolio_number(values[5], "relevant_inputs")?,
latches: parse_event_contract_portfolio_number(values[6], "latches")?,
admitted,
backend: values[8].to_string(),
reason: values[9].to_string(),
avoidable,
certificate_present,
certificate_verified: parse_event_contract_portfolio_bool(
values[12],
"certificate_verified",
)?,
gate_ns: parse_event_contract_portfolio_number(values[13], "gate_ns")?,
backend_ns: parse_event_contract_portfolio_number(values[14], "backend_ns")?,
verifier_ns: parse_event_contract_portfolio_number(values[15], "verifier_ns")?,
})
}
fn verify_aiger_event_contract_portfolio_report(
input: &Path,
expected_bad_output: usize,
contract_path: &Path,
report_path: &Path,
certificate_path: &Path,
) -> Result<(), String> {
let report = parse_event_contract_portfolio_report(report_path)?;
let model = parse_aag(input)?;
if expected_bad_output >= model.outputs.len() || report.bad_output != expected_bad_output {
return Err(
"event contract portfolio report bad output mismatch or range error".to_string(),
);
}
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let contract = parse_event_contract(contract_path, &model, &relevant_inputs)?;
let admitted = event_contract_v3_admitted(
relevant_inputs.len(),
model.latches.len(),
contract.horizon,
model.latches.iter().all(|latch| latch.initial.is_some()),
);
if report.input_sha256 != sha256_file(input)?
|| report.contract_sha256 != sha256_file(contract_path)?
|| report.horizon != contract.horizon
|| report.relevant_inputs != relevant_inputs.len()
|| report.latches != model.latches.len()
|| report.admitted != admitted
{
return Err("event contract portfolio report identity or dimensions mismatch".to_string());
}
match (report.backend.as_str(), report.reason.as_str()) {
("event-contract-certificate-v3", "static-admission") => {
if !admitted || !report.certificate_present || !report.certificate_verified {
return Err("event contract certificate report has inconsistent status".to_string());
}
verify_aiger_event_contract_certificate_v3(input, contract_path, certificate_path)?;
let certificate = parse_event_contract_certificate_v3(certificate_path)?;
if certificate.input_sha256 != report.input_sha256
|| certificate.contract_sha256 != report.contract_sha256
|| certificate.bad_output != report.bad_output
|| certificate.horizon != report.horizon
|| certificate.avoidable != report.avoidable
{
return Err("event contract report and certificate disagree".to_string());
}
}
("persistent-cdcl", "static-rejection")
| ("persistent-cdcl", "event-contract-resource-fallback") => {
if report.certificate_present
|| report.certificate_verified
|| fs::symlink_metadata(certificate_path).is_ok()
|| (report.reason == "static-rejection" && admitted)
|| (report.reason == "event-contract-resource-fallback" && !admitted)
{
return Err("event contract CDCL report has inconsistent status".to_string());
}
let avoidable =
solve_event_contract_cdcl(&model, &relevant_inputs, report.bad_output, &contract)?;
if avoidable != report.avoidable {
return Err("event contract CDCL report answer mismatch".to_string());
}
}
_ => return Err("event contract portfolio backend or reason is invalid".to_string()),
}
println!(
"event-contract-portfolio-report status=VERIFIED result={} backend={} report={}",
if report.avoidable {
"avoidable"
} else {
"unavoidable"
},
report.backend,
report_path.display()
);
Ok(())
}
fn benchmark_aiger_event_contract_certificate_v3_cost(
input: &Path,
bad_output: usize,
contract_path: &Path,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=100).contains(&repeats) {
return Err("event contract certificate v3 cost repeats must be in 1..=100".to_string());
}
if output.exists() {
return Err("event contract certificate v3 cost refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let contract = parse_event_contract(contract_path, &model, &relevant_inputs)?;
let input_sha256 = sha256_file(input)?;
let contract_sha256 = sha256_file(contract_path)?;
let mut lines = vec!["schema_version,input_sha256,contract_sha256,bad_output,horizon,relevant_inputs,latches,phases,trial,result,certificate_generate_ns,certificate_verify_ns,cdcl_query_ns,certificate_bytes,raw_proof_bytes,proof_count,direct_evaluations,answers_agree,status".to_string()];
for trial in 0..repeats {
let certificate_path = std::env::temp_dir().join(format!(
"guarded-continuation-event-v3-cost-{}-{trial}.cert3",
std::process::id()
));
if fs::symlink_metadata(&certificate_path).is_ok() {
return Err(format!(
"event contract certificate v3 cost temporary path already exists: {}",
certificate_path.display()
));
}
let run = (|| {
let generation_start = Instant::now();
certify_aiger_event_contract_v3(input, bad_output, contract_path, &certificate_path)?;
let certificate_generate_ns = generation_start.elapsed().as_nanos();
let certificate_bytes = fs::symlink_metadata(&certificate_path)
.map_err(|error| format!("inspect benchmark v3 certificate: {error}"))?
.len();
let certificate = parse_event_contract_certificate_v3(&certificate_path)?;
let raw_proof_bytes = certificate
.phases
.iter()
.flat_map(|phase| &phase.proofs)
.map(Vec::len)
.sum::<usize>()
+ certificate.terminal_proof.len();
let proof_count = certificate
.phases
.iter()
.map(|phase| phase.proofs.len())
.sum::<usize>()
+ 1;
let direct_evaluations = certificate
.phases
.iter()
.map(|phase| phase.edges.len())
.sum::<usize>()
+ certificate.terminal_witnesses.len()
+ certificate.states.len();
let verification_start = Instant::now();
verify_aiger_event_contract_certificate_v3(input, contract_path, &certificate_path)?;
let certificate_verify_ns = verification_start.elapsed().as_nanos();
let cdcl_start = Instant::now();
let cdcl_result =
solve_event_contract_cdcl(&model, &relevant_inputs, bad_output, &contract)?;
let cdcl_query_ns = cdcl_start.elapsed().as_nanos();
if certificate.avoidable != cdcl_result {
return Err("event contract certificate v3 cost backends disagree".to_string());
}
lines.push(format!(
"{EVENT_CONTRACT_CERTIFICATE_COST_SCHEMA_VERSION},{input_sha256},{contract_sha256},{bad_output},{},{},{},{},{trial},{},{certificate_generate_ns},{certificate_verify_ns},{cdcl_query_ns},{certificate_bytes},{raw_proof_bytes},{proof_count},{direct_evaluations},true,ok",
contract.horizon,
relevant_inputs.len(),
model.latches.len(),
contract.phases.len(),
if cdcl_result { "avoidable" } else { "unavoidable" },
));
Ok(())
})();
let cleanup = fs::remove_file(&certificate_path);
if let Err(error) = run {
return Err(error);
}
cleanup.map_err(|error| format!("remove benchmark v3 certificate: {error}"))?;
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"event-contract-certificate-v3-cost status=VALID trials={repeats} output={}",
output.display()
);
Ok(())
}
fn certify_aiger_predicate(
input: &Path,
bad_output: usize,
transcript: &Path,
certificate_path: &Path,
) -> Result<(), String> {
let model = parse_aag(input)?;
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err("predicate certificate requires declared initial latch values".to_string());
}
if bad_output >= model.outputs.len() {
return Err("predicate certificate bad output is out of range".to_string());
}
let mut quotient = PredicateQuotient::new(&model)?;
let constraints =
parse_predicate_transcript(transcript, quotient.interface.projected_inputs.len())?;
let horizon = constraints.len() - 1;
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
let witness = quotient.query(initial_state, bad_output, &constraints)?;
let mut phases = Vec::new();
let mut start = 0usize;
while start < horizon {
let mut end = start + 1;
while end < horizon && constraints[end] == constraints[start] {
end += 1;
}
let relation = quotient.relation_power(&constraints[start], end - start)?;
phases.push(PredicateCertificatePhase {
start,
length: end - start,
constraints: constraints[start].clone(),
rows: relation.rows().iter().map(|row| row[0] as u16).collect(),
});
start = end;
}
let mut terminal_safe_states = 0u16;
for state in 0..(1usize << model.latches.len()) {
if quotient
.interface
.witness_input(state, None, Some(bad_output), &constraints[horizon])?
.is_some()
{
terminal_safe_states |= 1u16 << state;
}
}
let (states, inputs) = witness
.map(|witness| (witness.states, witness.declared_inputs))
.unwrap_or_default();
let certificate = PredicateCertificate {
input_sha256: sha256_file(input)?,
declared_inputs: model.inputs.len(),
relevant_inputs: quotient.interface.projected_inputs.clone(),
latches: model.latches.len(),
horizon,
bad_output,
initial_state,
avoidable: !states.is_empty(),
phases,
terminal_constraint: constraints[horizon].clone(),
terminal_safe_states,
states,
inputs,
};
publish_causal_comparison(
certificate_path,
predicate_certificate_body(&certificate).as_bytes(),
)?;
println!(
"predicate-certificate status=CREATED result={} output={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
},
certificate_path.display()
);
Ok(())
}
fn verify_aiger_predicate_certificate(input: &Path, certificate_path: &Path) -> Result<(), String> {
let model = parse_aag(input)?;
let certificate = parse_predicate_certificate(certificate_path)?;
if sha256_file(input)? != certificate.input_sha256 {
return Err("predicate certificate input digest mismatch".to_string());
}
let mut checker = IndependentPredicateChecker::new(&model)?;
if certificate.declared_inputs != model.inputs.len()
|| certificate.relevant_inputs != checker.relevant_inputs
|| certificate.latches != model.latches.len()
|| certificate.bad_output >= model.outputs.len()
{
return Err("predicate certificate source dimensions mismatch".to_string());
}
let expected_initial =
model
.latches
.iter()
.enumerate()
.try_fold(0usize, |state, (bit, latch)| {
latch
.initial
.map(|value| state | (usize::from(value) << bit))
.ok_or_else(|| {
"predicate certificate source has an undeclared initial latch".to_string()
})
})?;
if certificate.initial_state != expected_initial {
return Err("predicate certificate initial state mismatch".to_string());
}
let identity = (0..checker.states)
.map(|state| 1u16 << state)
.collect::<Vec<_>>();
let mut composed = identity;
let mut frame_constraints = Vec::with_capacity(certificate.horizon + 1);
let mut expected_start = 0usize;
for phase in &certificate.phases {
if phase.start != expected_start || phase.length == 0 {
return Err("predicate certificate phases are not contiguous".to_string());
}
expected_start = expected_start
.checked_add(phase.length)
.ok_or_else(|| "predicate certificate phase length overflow".to_string())?;
if expected_start > certificate.horizon {
return Err("predicate certificate phase exceeds horizon".to_string());
}
let base = checker.one_step_relation(&phase.constraints)?;
let expected = IndependentPredicateChecker::power(&base, phase.length)?;
if expected != phase.rows {
return Err(format!(
"predicate certificate phase relation mismatch at frame {}",
phase.start
));
}
composed = IndependentPredicateChecker::compose(&composed, &phase.rows)?;
frame_constraints.extend(std::iter::repeat_n(phase.constraints.clone(), phase.length));
}
if expected_start != certificate.horizon {
return Err("predicate certificate phases do not cover the horizon".to_string());
}
frame_constraints.push(certificate.terminal_constraint.clone());
let expected_safe =
checker.terminal_safe_states(certificate.bad_output, &certificate.terminal_constraint)?;
if expected_safe != certificate.terminal_safe_states {
return Err("predicate certificate terminal safe-state mismatch".to_string());
}
if certificate.avoidable {
if certificate.states.len() != certificate.horizon + 1
|| certificate.inputs.len() != certificate.horizon + 1
|| certificate.states[0] != certificate.initial_state
{
return Err("predicate certificate avoiding witness dimensions mismatch".to_string());
}
for frame in 0..=certificate.horizon {
let state = certificate.states[frame];
let input = certificate.inputs[frame];
if state >= checker.states
|| (model.inputs.len() < u64::BITS as usize && input >> model.inputs.len() != 0)
|| checker
.relevant_inputs
.iter()
.enumerate()
.any(|(bit, declared)| {
frame_constraints[frame][bit]
.is_some_and(|value| (input >> declared & 1 == 1) != value)
})
{
return Err(format!(
"predicate certificate witness is invalid at frame {frame}"
));
}
let (next, bad) = checker.evaluate(state, input)?;
if frame < certificate.horizon {
if next != certificate.states[frame + 1] {
return Err(format!(
"predicate certificate transition mismatch at frame {frame}"
));
}
} else if bad & (1u128 << certificate.bad_output) != 0 {
return Err("predicate certificate terminal witness is bad".to_string());
}
}
let terminal = *certificate.states.last().unwrap();
if composed[certificate.initial_state] & (1u16 << terminal) == 0
|| certificate.terminal_safe_states & (1u16 << terminal) == 0
{
return Err("predicate certificate witness is absent from its relations".to_string());
}
} else {
if !certificate.states.is_empty() || !certificate.inputs.is_empty() {
return Err("predicate certificate unavoidable result contains a witness".to_string());
}
if composed[certificate.initial_state] & certificate.terminal_safe_states != 0 {
return Err("predicate certificate unavoidable claim has a safe terminal".to_string());
}
}
println!(
"predicate-certificate status=VERIFIED result={} evaluations={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
},
checker.evaluations
);
Ok(())
}
fn certify_aiger_predicate_v2(
input: &Path,
bad_output: usize,
transcript: &Path,
certificate_path: &Path,
) -> Result<(), String> {
if fs::symlink_metadata(certificate_path).is_ok() {
return Err("predicate certificate v2 refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err("predicate certificate v2 requires declared initial latch values".to_string());
}
if bad_output >= model.outputs.len() {
return Err("predicate certificate v2 bad output is out of range".to_string());
}
let mut quotient = PredicateQuotient::new(&model)?;
let relevant_inputs = quotient.interface.projected_inputs.clone();
let constraints = parse_predicate_transcript(transcript, relevant_inputs.len())?;
let horizon = constraints.len() - 1;
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
let witness = quotient.query(initial_state, bad_output, &constraints)?;
let states_count = 1usize << model.latches.len();
let mut total_proof_bytes = 0usize;
let mut phases = Vec::new();
let mut start = 0usize;
while start < horizon {
let mut end = start + 1;
while end < horizon && constraints[end] == constraints[start] {
end += 1;
}
let base = quotient.relation(&constraints[start])?;
let base_rows = base
.rows()
.iter()
.map(|row| row[0] as u16)
.collect::<Vec<_>>();
let powered = quotient.relation_power(&constraints[start], end - start)?;
let powered_rows = powered
.rows()
.iter()
.map(|row| row[0] as u16)
.collect::<Vec<_>>();
let mut edges = Vec::new();
let mut proofs = Vec::with_capacity(states_count);
for (source, &targets) in base_rows.iter().enumerate() {
if targets == 0 {
return Err("predicate certificate v2 relation has no target".to_string());
}
for target in 0..states_count {
if targets & (1u16 << target) != 0 {
let input = quotient
.interface
.witness_input(source, Some(target), None, &constraints[start])?
.ok_or_else(|| {
"predicate certificate v2 relation edge lacks witness".to_string()
})?;
edges.push(PredicateCertificateV2Edge {
source,
target,
input,
});
}
}
let clauses = predicate_relation_completeness_clauses(
&model,
&relevant_inputs,
source,
&constraints[start],
targets,
)?;
let proof = generate_varisat_unsat_proof(&clauses)?;
if proof.len() > PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES {
return Err("predicate certificate v2 individual proof exceeds limit".to_string());
}
total_proof_bytes = total_proof_bytes
.checked_add(proof.len())
.ok_or_else(|| "predicate certificate v2 proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err("predicate certificate v2 aggregate proofs exceed limit".to_string());
}
proofs.push(proof);
}
phases.push(PredicateCertificateV2Phase {
start,
length: end - start,
constraints: constraints[start].clone(),
base_rows,
powered_rows,
edges,
proofs,
});
start = end;
}
let mut terminal_safe_states = 0u16;
let mut terminal_witnesses = Vec::new();
for state in 0..states_count {
if let Some(input) = quotient.interface.witness_input(
state,
None,
Some(bad_output),
&constraints[horizon],
)? {
terminal_safe_states |= 1u16 << state;
terminal_witnesses.push((state, input));
}
}
let terminal_clauses = predicate_terminal_completeness_clauses(
&model,
&relevant_inputs,
&constraints[horizon],
bad_output,
terminal_safe_states,
)?;
let terminal_proof = generate_varisat_unsat_proof(&terminal_clauses)?;
if terminal_proof.len() > PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES {
return Err("predicate certificate v2 terminal proof exceeds limit".to_string());
}
total_proof_bytes = total_proof_bytes
.checked_add(terminal_proof.len())
.ok_or_else(|| "predicate certificate v2 proof size overflow".to_string())?;
if total_proof_bytes > PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES {
return Err("predicate certificate v2 aggregate proofs exceed limit".to_string());
}
let (states, inputs) = witness
.map(|witness| (witness.states, witness.declared_inputs))
.unwrap_or_default();
let certificate = PredicateCertificateV2 {
input_sha256: sha256_file(input)?,
declared_inputs: model.inputs.len(),
relevant_inputs,
latches: model.latches.len(),
horizon,
bad_output,
initial_state,
avoidable: !states.is_empty(),
phases,
terminal_constraint: constraints[horizon].clone(),
terminal_safe_states,
terminal_witnesses,
terminal_proof,
states,
inputs,
};
let body = predicate_certificate_v2_body(&certificate);
if body.len() as u64 > PREDICATE_CERTIFICATE_V2_MAX_BYTES {
return Err("predicate certificate v2 serialized artifact exceeds limit".to_string());
}
publish_causal_comparison(certificate_path, body.as_bytes())?;
println!(
"predicate-certificate-v2 status=CREATED result={} proofs={} proof_bytes={} output={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
},
certificate
.phases
.iter()
.map(|phase| phase.proofs.len())
.sum::<usize>()
+ 1,
total_proof_bytes,
certificate_path.display()
);
Ok(())
}
fn predicate_v2_witness_respects_constraints(
checker: &IndependentPredicateChecker<'_>,
constraints: &[Option<bool>],
input: u64,
) -> bool {
(checker.model.inputs.len() == u64::BITS as usize || input >> checker.model.inputs.len() == 0)
&& checker
.relevant_inputs
.iter()
.enumerate()
.all(|(bit, declared)| {
constraints[bit].is_none_or(|value| (input >> declared & 1 == 1) == value)
})
}
fn verify_aiger_predicate_certificate_v2(
input: &Path,
certificate_path: &Path,
) -> Result<(), String> {
let model = parse_aag(input)?;
let certificate = parse_predicate_certificate_v2(certificate_path)?;
if sha256_file(input)? != certificate.input_sha256 {
return Err("predicate certificate v2 input digest mismatch".to_string());
}
let mut checker = IndependentPredicateChecker::new(&model)?;
if certificate.declared_inputs != model.inputs.len()
|| certificate.relevant_inputs != checker.relevant_inputs
|| certificate.latches != model.latches.len()
|| certificate.bad_output >= model.outputs.len()
{
return Err("predicate certificate v2 source dimensions mismatch".to_string());
}
let expected_initial =
model
.latches
.iter()
.enumerate()
.try_fold(0usize, |state, (bit, latch)| {
latch
.initial
.map(|value| state | (usize::from(value) << bit))
.ok_or_else(|| {
"predicate certificate v2 source has undeclared initial latch".to_string()
})
})?;
if certificate.initial_state != expected_initial {
return Err("predicate certificate v2 initial state mismatch".to_string());
}
let mut composed = (0..checker.states)
.map(|state| 1u16 << state)
.collect::<Vec<_>>();
let mut frame_constraints = Vec::with_capacity(certificate.horizon + 1);
let mut expected_start = 0usize;
let mut checked_proofs = 0usize;
for (phase_index, phase) in certificate.phases.iter().enumerate() {
if phase.start != expected_start || phase.length == 0 {
return Err("predicate certificate v2 phases are not contiguous".to_string());
}
expected_start = expected_start
.checked_add(phase.length)
.ok_or_else(|| "predicate certificate v2 phase length overflow".to_string())?;
if expected_start > certificate.horizon
|| phase.base_rows.len() != checker.states
|| phase.powered_rows.len() != checker.states
|| phase.proofs.len() != checker.states
{
return Err("predicate certificate v2 phase dimensions mismatch".to_string());
}
let mut expected_edges = Vec::new();
for (source, &targets) in phase.base_rows.iter().enumerate() {
if targets == 0 {
return Err("predicate certificate v2 relation has no target".to_string());
}
for target in 0..checker.states {
if targets & (1u16 << target) != 0 {
expected_edges.push((source, target));
}
}
let clauses = predicate_relation_completeness_clauses(
&model,
&checker.relevant_inputs,
source,
&phase.constraints,
targets,
)?;
verify_varisat_unsat_proof(&clauses, &phase.proofs[source]).map_err(|error| {
format!("predicate certificate v2 phase {phase_index} source {source}: {error}")
})?;
checked_proofs += 1;
}
if phase.edges.len() != expected_edges.len() {
return Err("predicate certificate v2 edge count mismatch".to_string());
}
for (edge, &(source, target)) in phase.edges.iter().zip(&expected_edges) {
if (edge.source, edge.target) != (source, target)
|| !predicate_v2_witness_respects_constraints(
&checker,
&phase.constraints,
edge.input,
)
{
return Err("predicate certificate v2 edge order or input is invalid".to_string());
}
let (actual, _) = checker.evaluate(source, edge.input)?;
if actual != target {
return Err("predicate certificate v2 edge witness mismatch".to_string());
}
}
let expected_power = IndependentPredicateChecker::power(&phase.base_rows, phase.length)?;
if expected_power != phase.powered_rows {
return Err("predicate certificate v2 powered relation mismatch".to_string());
}
composed = IndependentPredicateChecker::compose(&composed, &phase.powered_rows)?;
frame_constraints.extend(std::iter::repeat_n(phase.constraints.clone(), phase.length));
}
if expected_start != certificate.horizon {
return Err("predicate certificate v2 phases do not cover horizon".to_string());
}
frame_constraints.push(certificate.terminal_constraint.clone());
let expected_terminal_states = (0..checker.states)
.filter(|state| certificate.terminal_safe_states & (1u16 << state) != 0)
.collect::<Vec<_>>();
if certificate.terminal_witnesses.len() != expected_terminal_states.len() {
return Err("predicate certificate v2 terminal witness count mismatch".to_string());
}
for (&expected_state, &(state, terminal_input)) in expected_terminal_states
.iter()
.zip(&certificate.terminal_witnesses)
{
if state != expected_state
|| !predicate_v2_witness_respects_constraints(
&checker,
&certificate.terminal_constraint,
terminal_input,
)
{
return Err("predicate certificate v2 terminal witness order is invalid".to_string());
}
let (_, bad) = checker.evaluate(state, terminal_input)?;
if bad & (1u128 << certificate.bad_output) != 0 {
return Err("predicate certificate v2 terminal witness is bad".to_string());
}
}
let terminal_clauses = predicate_terminal_completeness_clauses(
&model,
&checker.relevant_inputs,
&certificate.terminal_constraint,
certificate.bad_output,
certificate.terminal_safe_states,
)?;
verify_varisat_unsat_proof(&terminal_clauses, &certificate.terminal_proof)
.map_err(|error| format!("predicate certificate v2 terminal: {error}"))?;
checked_proofs += 1;
if certificate.avoidable {
if certificate.states.len() != certificate.horizon + 1
|| certificate.inputs.len() != certificate.horizon + 1
|| certificate.states[0] != certificate.initial_state
{
return Err("predicate certificate v2 avoiding trace dimensions mismatch".to_string());
}
for frame in 0..=certificate.horizon {
let state = certificate.states[frame];
let witness_input = certificate.inputs[frame];
if state >= checker.states
|| !predicate_v2_witness_respects_constraints(
&checker,
&frame_constraints[frame],
witness_input,
)
{
return Err(format!(
"predicate certificate v2 trace is invalid at frame {frame}"
));
}
let (next, bad) = checker.evaluate(state, witness_input)?;
if frame < certificate.horizon {
if next != certificate.states[frame + 1] {
return Err(format!(
"predicate certificate v2 transition mismatch at frame {frame}"
));
}
} else if bad & (1u128 << certificate.bad_output) != 0 {
return Err("predicate certificate v2 terminal trace is bad".to_string());
}
}
let terminal = *certificate.states.last().unwrap();
if composed[certificate.initial_state] & (1u16 << terminal) == 0
|| certificate.terminal_safe_states & (1u16 << terminal) == 0
{
return Err("predicate certificate v2 trace is absent from evidence".to_string());
}
} else {
if !certificate.states.is_empty() || !certificate.inputs.is_empty() {
return Err("predicate certificate v2 unavoidable result contains trace".to_string());
}
if composed[certificate.initial_state] & certificate.terminal_safe_states != 0 {
return Err("predicate certificate v2 unavoidable claim has safe terminal".to_string());
}
}
println!(
"predicate-certificate-v2 status=VERIFIED result={} proofs={} direct_evaluations={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
},
checked_proofs,
checker.evaluations
);
Ok(())
}
fn predicate_obligation_dimacs(variable_count: usize, clauses: &[Clause]) -> Vec<u8> {
let mut body = format!("p cnf {variable_count} {}\n", clauses.len());
for clause in clauses {
for &(variable, positive) in &clause.0 {
if !positive {
body.push('-');
}
body.push_str(&(variable + 1).to_string());
body.push(' ');
}
body.push_str("0\n");
}
body.into_bytes()
}
fn aggregate_predicate_obligations(
variables_per_obligation: usize,
obligations: &[Vec<Clause>],
) -> Result<(usize, Vec<Clause>), String> {
if variables_per_obligation == 0 || obligations.is_empty() {
return Err("predicate aggregate obligation requires variables and clauses".to_string());
}
let variable_count = variables_per_obligation
.checked_mul(obligations.len())
.and_then(|variables| variables.checked_add(obligations.len()))
.ok_or_else(|| "predicate aggregate obligation variable overflow".to_string())?;
let selector_start = variables_per_obligation * obligations.len();
let mut clauses = Vec::new();
for (index, obligation) in obligations.iter().enumerate() {
let offset = index * variables_per_obligation;
let selector = selector_start + index;
for clause in obligation {
if clause
.0
.iter()
.any(|&(variable, _)| variable >= variables_per_obligation)
{
return Err("predicate aggregate obligation literal is out of range".to_string());
}
let mut guarded = clause
.0
.iter()
.map(|&(variable, positive)| (offset + variable, positive))
.collect::<Vec<_>>();
guarded.push((selector, false));
clauses.push(Clause(guarded));
}
}
clauses.push(Clause(
(0..obligations.len())
.map(|index| (selector_start + index, true))
.collect(),
));
Ok((variable_count, clauses))
}
fn export_aiger_predicate_v2_obligations(
input: &Path,
certificate_path: &Path,
output: &Path,
) -> Result<(), String> {
if fs::symlink_metadata(output).is_ok() {
return Err("predicate obligation export refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
let certificate = parse_predicate_certificate_v2(certificate_path)?;
if sha256_file(input)? != certificate.input_sha256 {
return Err("predicate obligation export input digest mismatch".to_string());
}
let checker = IndependentPredicateChecker::new(&model)?;
if certificate.declared_inputs != model.inputs.len()
|| certificate.relevant_inputs != checker.relevant_inputs
|| certificate.latches != model.latches.len()
|| certificate.bad_output >= model.outputs.len()
{
return Err("predicate obligation export source dimensions mismatch".to_string());
}
let parent = causal_output_parent(output);
fs::create_dir_all(parent)
.map_err(|error| format!("create predicate obligation parent: {error}"))?;
let name = output
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.ok_or_else(|| {
"predicate obligation output requires a valid final component".to_string()
})?;
let staging = parent.join(format!(".{name}.stage-{}", std::process::id()));
fs::create_dir(&staging)
.map_err(|error| format!("create predicate obligation staging directory: {error}"))?;
let publish = (|| {
let mut manifest = vec![
"predicate_obligation_bundle_version=1".to_string(),
format!("input_sha256={}", certificate.input_sha256),
format!("certificate_sha256={}", sha256_file(certificate_path)?),
format!("cnf_variables={}", model.max_variable),
];
let mut obligation_count = 0usize;
let mut obligation_clauses = Vec::new();
for (phase_index, phase) in certificate.phases.iter().enumerate() {
if phase.base_rows.len() != checker.states {
return Err("predicate obligation export phase dimensions mismatch".to_string());
}
for (source, &targets) in phase.base_rows.iter().enumerate() {
let clauses = predicate_relation_completeness_clauses(
&model,
&checker.relevant_inputs,
source,
&phase.constraints,
targets,
)?;
let filename = format!("phase-{phase_index:04}-source-{source:04}.cnf");
let path = staging.join(&filename);
write_causal_file(
&path,
&predicate_obligation_dimacs(model.max_variable, &clauses),
)?;
manifest.push(format!(
"obligation_{obligation_count}={filename},relation-completeness,{phase_index},{source},{}",
sha256_file(&path)?
));
obligation_clauses.push(clauses);
obligation_count += 1;
}
}
let clauses = predicate_terminal_completeness_clauses(
&model,
&checker.relevant_inputs,
&certificate.terminal_constraint,
certificate.bad_output,
certificate.terminal_safe_states,
)?;
let filename = "terminal.cnf";
let path = staging.join(filename);
write_causal_file(
&path,
&predicate_obligation_dimacs(model.max_variable, &clauses),
)?;
manifest.push(format!(
"obligation_{obligation_count}={filename},terminal-completeness,-,-,{}",
sha256_file(&path)?
));
obligation_clauses.push(clauses);
obligation_count += 1;
let (aggregate_variables, aggregate_clauses) =
aggregate_predicate_obligations(model.max_variable, &obligation_clauses)?;
let aggregate_path = staging.join("aggregate.cnf");
write_causal_file(
&aggregate_path,
&predicate_obligation_dimacs(aggregate_variables, &aggregate_clauses),
)?;
manifest.insert(4, format!("obligation_count={obligation_count}"));
manifest.insert(5, "aggregate_cnf=aggregate.cnf".to_string());
manifest.insert(6, format!("aggregate_variables={aggregate_variables}"));
manifest.insert(
7,
format!("aggregate_sha256={}", sha256_file(&aggregate_path)?),
);
write_causal_file(
&staging.join("manifest.txt"),
(manifest.join("\n") + "\n").as_bytes(),
)?;
sync_causal_directory(&staging)?;
rename_causal_bundle_noreplace(&staging, output)?;
sync_causal_directory(parent)
})();
if publish.is_err() {
let _ = fs::remove_dir_all(&staging);
}
publish?;
println!(
"predicate-obligations status=EXPORTED output={}",
output.display()
);
Ok(())
}
fn export_aiger_event_contract_v3_obligations(
input: &Path,
contract_path: &Path,
certificate_path: &Path,
output: &Path,
) -> Result<(), String> {
if fs::symlink_metadata(output).is_ok() {
return Err("event contract obligation export refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
let certificate = parse_event_contract_certificate_v3(certificate_path)?;
if sha256_file(input)? != certificate.input_sha256
|| sha256_file(contract_path)? != certificate.contract_sha256
{
return Err("event contract obligation export source binding mismatch".to_string());
}
let checker = IndependentPredicateChecker::new(&model)?;
let contract = parse_event_contract(contract_path, &model, &checker.relevant_inputs)?;
if certificate.declared_inputs != model.inputs.len()
|| certificate.relevant_inputs != checker.relevant_inputs
|| certificate.latches != model.latches.len()
|| certificate.bad_output >= model.outputs.len()
|| certificate.horizon != contract.horizon
|| certificate.phases.len() != contract.phases.len()
|| certificate.terminal != contract.terminal
{
return Err("event contract obligation export dimensions mismatch".to_string());
}
let parent = causal_output_parent(output);
fs::create_dir_all(parent)
.map_err(|error| format!("create event contract obligation parent: {error}"))?;
let name = output
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.ok_or_else(|| {
"event contract obligation output requires a valid final component".to_string()
})?;
let staging = parent.join(format!(".{name}.stage-{}", std::process::id()));
fs::create_dir(&staging)
.map_err(|error| format!("create event contract obligation staging directory: {error}"))?;
let publish = (|| {
let mut manifest = vec![
"event_contract_obligation_bundle_version=1".to_string(),
format!("input_sha256={}", certificate.input_sha256),
format!("contract_sha256={}", certificate.contract_sha256),
format!("certificate_sha256={}", sha256_file(certificate_path)?),
format!("cnf_variables={}", model.max_variable),
format!(
"result={}",
if certificate.avoidable {
"avoidable"
} else {
"unavoidable"
}
),
];
let mut obligation_count = 0usize;
let mut obligation_clauses = Vec::new();
for (phase_index, (phase, source_phase)) in
certificate.phases.iter().zip(&contract.phases).enumerate()
{
if phase.start != source_phase.start
|| phase.length != source_phase.length
|| phase.predicate != source_phase.predicate
|| phase.base_rows.len() != checker.states
{
return Err("event contract obligation export phase mismatch".to_string());
}
for (source, &targets) in phase.base_rows.iter().enumerate() {
let clauses = predicate_relation_completeness_clauses_for_predicate(
&model,
&checker.relevant_inputs,
source,
&phase.predicate,
targets,
)?;
let filename = format!("phase-{phase_index:04}-source-{source:04}.cnf");
let path = staging.join(&filename);
write_causal_file(
&path,
&predicate_obligation_dimacs(model.max_variable, &clauses),
)?;
manifest.push(format!(
"obligation_{obligation_count}={filename},relation-completeness,{phase_index},{source},{}",
sha256_file(&path)?
));
obligation_clauses.push(clauses);
obligation_count += 1;
}
}
let clauses = predicate_terminal_completeness_clauses_for_predicate(
&model,
&checker.relevant_inputs,
&certificate.terminal,
certificate.bad_output,
certificate.terminal_safe_states,
)?;
let filename = "terminal.cnf";
let path = staging.join(filename);
write_causal_file(
&path,
&predicate_obligation_dimacs(model.max_variable, &clauses),
)?;
manifest.push(format!(
"obligation_{obligation_count}={filename},terminal-completeness,-,-,{}",
sha256_file(&path)?
));
obligation_clauses.push(clauses);
obligation_count += 1;
let (aggregate_variables, aggregate_clauses) =
aggregate_predicate_obligations(model.max_variable, &obligation_clauses)?;
let aggregate_path = staging.join("aggregate.cnf");
write_causal_file(
&aggregate_path,
&predicate_obligation_dimacs(aggregate_variables, &aggregate_clauses),
)?;
manifest.insert(6, format!("obligation_count={obligation_count}"));
manifest.insert(7, "aggregate_cnf=aggregate.cnf".to_string());
manifest.insert(8, format!("aggregate_variables={aggregate_variables}"));
manifest.insert(
9,
format!("aggregate_sha256={}", sha256_file(&aggregate_path)?),
);
write_causal_file(
&staging.join("manifest.txt"),
(manifest.join("\n") + "\n").as_bytes(),
)?;
sync_causal_directory(&staging)?;
rename_causal_bundle_noreplace(&staging, output)?;
sync_causal_directory(parent)
})();
if publish.is_err() {
let _ = fs::remove_dir_all(&staging);
}
publish?;
println!(
"event-contract-obligations status=EXPORTED output={}",
output.display()
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn verify_aiger_counterfactual_portfolio(
input: &Path,
bad_output: usize,
transcript: &Path,
expected_queries: usize,
report: &Path,
certificate_path: &Path,
) -> Result<(), String> {
verify_aiger_counterfactual_portfolio_with_node_limit(
input,
bad_output,
transcript,
expected_queries,
report,
certificate_path,
PREDICATE_INTERFACE_MAX_BDD_NODES,
)
}
fn predicate_resource_error(error: &str) -> bool {
(error.starts_with("predicate interface BDD exceeds ") && error.ends_with(" nodes"))
|| error == "predicate interface witness cache exceeds 4096 entries"
|| error == "predicate quotient relation cache exceeds 4096 entries"
|| error.starts_with("predicate quotient power cache exceeds ")
}
#[allow(clippy::too_many_arguments)]
fn verify_aiger_counterfactual_portfolio_with_node_limit(
input: &Path,
bad_output: usize,
transcript: &Path,
expected_queries: usize,
report: &Path,
certificate_path: &Path,
predicate_node_limit: usize,
) -> Result<(), String> {
if report.exists() || certificate_path.exists() {
return Err("counterfactual portfolio refuses to overwrite output artifacts".to_string());
}
let model = parse_aag(input)?;
if bad_output >= model.outputs.len() {
return Err("counterfactual portfolio bad output is out of range".to_string());
}
let gate_start = Instant::now();
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let constraints = parse_predicate_transcript(transcript, relevant_inputs.len())?;
let horizon = constraints.len() - 1;
let admitted = predicate_quotient_admitted(
relevant_inputs.len(),
model.latches.len(),
horizon,
expected_queries,
) && model.latches.iter().all(|latch| latch.initial.is_some());
let gate_ns = gate_start.elapsed().as_nanos();
let backend_start = Instant::now();
let mut certificate_verified = false;
let mut verifier_ns = 0u128;
let (avoidable, backend, reason) = if admitted {
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
match PredicateQuotient::new_with_node_limit(&model, predicate_node_limit)
.and_then(|mut quotient| quotient.query(initial_state, bad_output, &constraints))
{
Ok(expected) => {
certify_aiger_predicate(input, bad_output, transcript, certificate_path)?;
let verify_start = Instant::now();
if let Err(error) = verify_aiger_predicate_certificate(input, certificate_path) {
let _ = fs::remove_file(certificate_path);
return Err(error);
}
verifier_ns = verify_start.elapsed().as_nanos();
certificate_verified = true;
let certificate = match parse_predicate_certificate(certificate_path) {
Ok(certificate) => certificate,
Err(error) => {
let _ = fs::remove_file(certificate_path);
return Err(error);
}
};
if certificate.avoidable != expected.is_some() {
let _ = fs::remove_file(certificate_path);
return Err(
"counterfactual certificate disagrees with predicate preflight".to_string(),
);
}
(
certificate.avoidable,
"predicate-certificate",
"static-admission",
)
}
Err(error) if predicate_resource_error(&error) => {
let avoidable = solve_counterfactual_persistent(
&model,
bad_output,
&relevant_inputs,
&constraints,
)?;
(avoidable, "persistent-cdcl", "predicate-resource-fallback")
}
Err(error) => return Err(error),
}
} else {
let avoidable =
solve_counterfactual_persistent(&model, bad_output, &relevant_inputs, &constraints)?;
(avoidable, "persistent-cdcl", "static-rejection")
};
let backend_ns = backend_start.elapsed().as_nanos();
let body = format!(
"counterfactual_portfolio_version=1\ninput_sha256={}\nbad_output={bad_output}\nhorizon={horizon}\nrelevant_inputs={}\nlatches={}\nexpected_queries={expected_queries}\nadmitted={}\nbackend={backend}\nreason={reason}\nresult={}\ncertificate={}\ncertificate_verified={}\ngate_ns={gate_ns}\nbackend_ns={backend_ns}\nverifier_ns={verifier_ns}\nstatus=ok\n",
sha256_file(input)?,
relevant_inputs.len(),
model.latches.len(),
usize::from(admitted),
if avoidable {
"avoidable"
} else {
"unavoidable"
},
if certificate_verified { "present" } else { "-" },
usize::from(certificate_verified),
);
if let Err(error) = publish_causal_comparison(report, body.as_bytes()) {
if certificate_verified {
let _ = fs::remove_file(certificate_path);
}
return Err(error);
}
println!(
"counterfactual-portfolio status=VERIFIED result={} backend={backend} reason={reason} report={}",
if avoidable {
"avoidable"
} else {
"unavoidable"
},
report.display()
);
Ok(())
}
fn solve_counterfactual_persistent(
model: &AagModel,
bad_output: usize,
relevant_inputs: &[usize],
constraints: &[Vec<Option<bool>>],
) -> Result<bool, String> {
if constraints.is_empty() || constraints.len() > INTERFACE_QUOTIENT_MAX_HORIZON + 1 {
return Err("counterfactual persistent transcript dimensions mismatch".to_string());
}
let horizon = constraints.len() - 1;
let encoding = aag_bmc_encoding(model, horizon)?;
let mut assumptions = vec![None; encoding.variables];
for (frame, frame_constraints) in constraints.iter().enumerate() {
if frame_constraints.len() != relevant_inputs.len() {
return Err("counterfactual persistent constraint dimensions mismatch".to_string());
}
for (projected, required) in frame_constraints.iter().enumerate() {
let input = relevant_inputs[projected];
assumptions[frame * model.max_variable + model.inputs[input] / 2 - 1] = *required;
}
}
let target = aag_cnf_literal(model, horizon, model.outputs[bad_output]);
match target {
AagCnfLiteral::Constant(false) => return Ok(true),
AagCnfLiteral::Constant(true) => return Ok(false),
AagCnfLiteral::Variable(_) => {}
}
if add_causal_counterfactual_assumption(&mut assumptions, target)? {
return Ok(false);
}
let mut solver = Solver::new();
add_to_varisat(&mut solver, &encoding.clauses);
solve_causal_persistent(&mut solver, &assumptions)
}
#[derive(Debug, Eq, PartialEq)]
struct CounterfactualPortfolioReport {
input_sha256: String,
bad_output: usize,
horizon: usize,
relevant_inputs: usize,
latches: usize,
expected_queries: usize,
admitted: bool,
backend: String,
reason: String,
avoidable: bool,
certificate_present: bool,
certificate_verified: bool,
gate_ns: u128,
backend_ns: u128,
verifier_ns: u128,
}
fn parse_counterfactual_bool(value: &str, name: &str) -> Result<bool, String> {
match value {
"0" => Ok(false),
"1" => Ok(true),
_ => Err(format!(
"counterfactual portfolio `{name}` must be canonical 0 or 1"
)),
}
}
fn parse_counterfactual_number<T: std::str::FromStr>(value: &str, name: &str) -> Result<T, String> {
if value.is_empty()
|| !value.bytes().all(|byte| byte.is_ascii_digit())
|| (value.len() > 1 && value.starts_with('0'))
{
return Err(format!(
"counterfactual portfolio `{name}` is not canonical decimal"
));
}
value
.parse()
.map_err(|_| format!("invalid counterfactual portfolio `{name}`"))
}
fn parse_counterfactual_report(path: &Path) -> Result<CounterfactualPortfolioReport, String> {
const KEYS: [&str; 16] = [
"counterfactual_portfolio_version",
"input_sha256",
"bad_output",
"horizon",
"relevant_inputs",
"latches",
"expected_queries",
"admitted",
"backend",
"reason",
"result",
"certificate",
"certificate_verified",
"gate_ns",
"backend_ns",
"verifier_ns",
];
let metadata = fs::symlink_metadata(path).map_err(|error| {
format!(
"inspect counterfactual portfolio report {}: {error}",
path.display()
)
})?;
if !metadata.file_type().is_file() || metadata.len() > 16 * 1024 {
return Err(
"counterfactual portfolio report must be a regular file no larger than 16384 bytes"
.to_string(),
);
}
let body = fs::read_to_string(path).map_err(|error| {
format!(
"read counterfactual portfolio report {}: {error}",
path.display()
)
})?;
if !body.ends_with('\n') || body.contains('\r') {
return Err(
"counterfactual portfolio report must use canonical newline-terminated LF text"
.to_string(),
);
}
let lines = body.lines().collect::<Vec<_>>();
if lines.len() != KEYS.len() + 1 || lines.last() != Some(&"status=ok") {
return Err(
"counterfactual portfolio report has invalid field count or status".to_string(),
);
}
let mut values = Vec::with_capacity(KEYS.len());
for (index, key) in KEYS.iter().enumerate() {
let prefix = format!("{key}=");
let value = lines[index].strip_prefix(&prefix).ok_or_else(|| {
format!(
"counterfactual portfolio report expected `{key}` at line {}",
index + 1
)
})?;
if value.is_empty() {
return Err(format!("counterfactual portfolio report `{key}` is empty"));
}
values.push(value);
}
if values[0] != "1" {
return Err("unsupported counterfactual portfolio report version".to_string());
}
let input_sha256 = values[1].to_string();
if input_sha256.len() != 64
|| input_sha256
.bytes()
.any(|byte| !byte.is_ascii_hexdigit() || byte.is_ascii_uppercase())
{
return Err("counterfactual portfolio input digest is invalid".to_string());
}
let backend = values[8].to_string();
let reason = values[9].to_string();
let avoidable = match values[10] {
"avoidable" => true,
"unavoidable" => false,
_ => return Err("counterfactual portfolio result is invalid".to_string()),
};
let certificate_present = match values[11] {
"present" => true,
"-" => false,
_ => return Err("counterfactual portfolio certificate field is invalid".to_string()),
};
Ok(CounterfactualPortfolioReport {
input_sha256,
bad_output: parse_counterfactual_number(values[2], "bad_output")?,
horizon: parse_counterfactual_number(values[3], "horizon")?,
relevant_inputs: parse_counterfactual_number(values[4], "relevant_inputs")?,
latches: parse_counterfactual_number(values[5], "latches")?,
expected_queries: parse_counterfactual_number(values[6], "expected_queries")?,
admitted: parse_counterfactual_bool(values[7], "admitted")?,
backend,
reason,
avoidable,
certificate_present,
certificate_verified: parse_counterfactual_bool(values[12], "certificate_verified")?,
gate_ns: parse_counterfactual_number(values[13], "gate_ns")?,
backend_ns: parse_counterfactual_number(values[14], "backend_ns")?,
verifier_ns: parse_counterfactual_number(values[15], "verifier_ns")?,
})
}
fn verify_aiger_counterfactual_report(
input: &Path,
transcript: &Path,
report_path: &Path,
certificate_path: &Path,
) -> Result<(), String> {
let report = parse_counterfactual_report(report_path)?;
let model = parse_aag(input)?;
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let constraints = parse_predicate_transcript(transcript, relevant_inputs.len())?;
let admitted = predicate_quotient_admitted(
relevant_inputs.len(),
model.latches.len(),
constraints.len() - 1,
report.expected_queries,
) && model.latches.iter().all(|latch| latch.initial.is_some());
if report.input_sha256 != sha256_file(input)?
|| report.bad_output >= model.outputs.len()
|| report.horizon != constraints.len() - 1
|| report.relevant_inputs != relevant_inputs.len()
|| report.latches != model.latches.len()
|| report.admitted != admitted
{
return Err("counterfactual portfolio report identity or dimensions mismatch".to_string());
}
match (report.backend.as_str(), report.reason.as_str()) {
("predicate-certificate", "static-admission") => {
if !admitted || !report.certificate_present || !report.certificate_verified {
return Err("counterfactual predicate report has inconsistent status".to_string());
}
verify_aiger_predicate_certificate(input, certificate_path)?;
let certificate = parse_predicate_certificate(certificate_path)?;
let mut certified_constraints = Vec::with_capacity(certificate.horizon + 1);
for phase in &certificate.phases {
certified_constraints
.extend(std::iter::repeat_n(phase.constraints.clone(), phase.length));
}
certified_constraints.push(certificate.terminal_constraint.clone());
if certificate.input_sha256 != report.input_sha256
|| certificate.bad_output != report.bad_output
|| certificate.horizon != report.horizon
|| certificate.relevant_inputs != relevant_inputs
|| certificate.avoidable != report.avoidable
|| certified_constraints != constraints
{
return Err("counterfactual report and certificate disagree".to_string());
}
}
("persistent-cdcl", "static-rejection")
| ("persistent-cdcl", "predicate-resource-fallback") => {
if report.certificate_present
|| report.certificate_verified
|| fs::symlink_metadata(certificate_path).is_ok()
|| (report.reason == "static-rejection" && admitted)
|| (report.reason == "predicate-resource-fallback" && !admitted)
{
return Err("counterfactual CDCL report has inconsistent status".to_string());
}
let avoidable = solve_counterfactual_persistent(
&model,
report.bad_output,
&relevant_inputs,
&constraints,
)?;
if avoidable != report.avoidable {
return Err("counterfactual CDCL report answer mismatch".to_string());
}
}
_ => return Err("counterfactual portfolio backend or reason is invalid".to_string()),
}
println!(
"counterfactual-portfolio-report status=VERIFIED result={} backend={} report={}",
if report.avoidable {
"avoidable"
} else {
"unavoidable"
},
report.backend,
report_path.display()
);
Ok(())
}
fn benchmark_aiger_predicate_certificate_cost(
input: &Path,
bad_output: usize,
transcript: &Path,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=1_000).contains(&repeats) {
return Err("predicate certificate cost repeats must be in 1..=1000".to_string());
}
if output.exists() {
return Err("predicate certificate cost refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
if bad_output >= model.outputs.len() {
return Err("predicate certificate cost bad output is out of range".to_string());
}
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err("predicate certificate cost requires declared initial latches".to_string());
}
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let constraints = parse_predicate_transcript(transcript, relevant_inputs.len())?;
let horizon = constraints.len() - 1;
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
let input_sha256 = sha256_file(input)?;
let transcript_sha256 = sha256_file(transcript)?;
let mut lines = vec!["schema_version,input_sha256,transcript_sha256,bad_output,horizon,relevant_inputs,latches,trial,result,predicate_query_ns,certificate_generate_ns,certificate_verify_ns,cdcl_query_ns,certificate_bytes,answers_agree,status".to_string()];
for trial in 0..repeats {
let certificate_path = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-cost-{}-{trial}.cert",
std::process::id()
));
if fs::symlink_metadata(&certificate_path).is_ok() {
return Err(format!(
"predicate certificate cost temporary path already exists: {}",
certificate_path.display()
));
}
let run = (|| {
let predicate_start = Instant::now();
let predicate_result = PredicateQuotient::new(&model)?
.query(initial_state, bad_output, &constraints)?
.is_some();
let predicate_query_ns = predicate_start.elapsed().as_nanos();
let generation_start = Instant::now();
certify_aiger_predicate(input, bad_output, transcript, &certificate_path)?;
let certificate_generate_ns = generation_start.elapsed().as_nanos();
let certificate_bytes = fs::symlink_metadata(&certificate_path)
.map_err(|error| format!("inspect benchmark certificate: {error}"))?
.len();
let certificate_result = parse_predicate_certificate(&certificate_path)?.avoidable;
let verification_start = Instant::now();
verify_aiger_predicate_certificate(input, &certificate_path)?;
let certificate_verify_ns = verification_start.elapsed().as_nanos();
let cdcl_start = Instant::now();
let cdcl_result = solve_counterfactual_persistent(
&model,
bad_output,
&relevant_inputs,
&constraints,
)?;
let cdcl_query_ns = cdcl_start.elapsed().as_nanos();
let answers_agree =
predicate_result == certificate_result && certificate_result == cdcl_result;
if !answers_agree {
return Err("predicate certificate cost backends disagree".to_string());
}
lines.push(format!(
"{PREDICATE_CERTIFICATE_COST_SCHEMA_VERSION},{input_sha256},{transcript_sha256},{bad_output},{horizon},{},{},{trial},{},{predicate_query_ns},{certificate_generate_ns},{certificate_verify_ns},{cdcl_query_ns},{certificate_bytes},true,ok",
relevant_inputs.len(),
model.latches.len(),
if cdcl_result { "avoidable" } else { "unavoidable" },
));
Ok(())
})();
let cleanup = fs::remove_file(&certificate_path);
if let Err(error) = run {
return Err(error);
}
cleanup.map_err(|error| format!("remove benchmark certificate: {error}"))?;
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"predicate-certificate-cost status=VALID trials={repeats} output={}",
output.display()
);
Ok(())
}
fn benchmark_aiger_predicate_certificate_v2_cost(
input: &Path,
bad_output: usize,
transcript: &Path,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=1_000).contains(&repeats) {
return Err("predicate certificate v2 cost repeats must be in 1..=1000".to_string());
}
if output.exists() {
return Err("predicate certificate v2 cost refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
if bad_output >= model.outputs.len() {
return Err("predicate certificate v2 cost bad output is out of range".to_string());
}
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let constraints = parse_predicate_transcript(transcript, relevant_inputs.len())?;
let horizon = constraints.len() - 1;
let input_sha256 = sha256_file(input)?;
let transcript_sha256 = sha256_file(transcript)?;
let mut lines = vec!["schema_version,input_sha256,transcript_sha256,bad_output,horizon,relevant_inputs,latches,trial,result,certificate_generate_ns,certificate_verify_ns,cdcl_query_ns,certificate_bytes,raw_proof_bytes,proof_count,direct_evaluations,answers_agree,status".to_string()];
for trial in 0..repeats {
let certificate_path = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-v2-cost-{}-{trial}.cert2",
std::process::id()
));
if fs::symlink_metadata(&certificate_path).is_ok() {
return Err(format!(
"predicate certificate v2 cost temporary path already exists: {}",
certificate_path.display()
));
}
let run = (|| {
let generation_start = Instant::now();
certify_aiger_predicate_v2(input, bad_output, transcript, &certificate_path)?;
let certificate_generate_ns = generation_start.elapsed().as_nanos();
let certificate_bytes = fs::symlink_metadata(&certificate_path)
.map_err(|error| format!("inspect benchmark v2 certificate: {error}"))?
.len();
let certificate = parse_predicate_certificate_v2(&certificate_path)?;
let raw_proof_bytes = certificate
.phases
.iter()
.flat_map(|phase| &phase.proofs)
.map(Vec::len)
.sum::<usize>()
+ certificate.terminal_proof.len();
let proof_count = certificate
.phases
.iter()
.map(|phase| phase.proofs.len())
.sum::<usize>()
+ 1;
let direct_evaluations = certificate
.phases
.iter()
.map(|phase| phase.edges.len())
.sum::<usize>()
+ certificate.terminal_witnesses.len()
+ certificate.states.len();
let verification_start = Instant::now();
verify_aiger_predicate_certificate_v2(input, &certificate_path)?;
let certificate_verify_ns = verification_start.elapsed().as_nanos();
let cdcl_start = Instant::now();
let cdcl_result = solve_counterfactual_persistent(
&model,
bad_output,
&relevant_inputs,
&constraints,
)?;
let cdcl_query_ns = cdcl_start.elapsed().as_nanos();
if certificate.avoidable != cdcl_result {
return Err("predicate certificate v2 cost backends disagree".to_string());
}
lines.push(format!(
"{PREDICATE_CERTIFICATE_V2_COST_SCHEMA_VERSION},{input_sha256},{transcript_sha256},{bad_output},{horizon},{},{},{trial},{},{certificate_generate_ns},{certificate_verify_ns},{cdcl_query_ns},{certificate_bytes},{raw_proof_bytes},{proof_count},{direct_evaluations},true,ok",
relevant_inputs.len(),
model.latches.len(),
if cdcl_result { "avoidable" } else { "unavoidable" },
));
Ok(())
})();
let cleanup = fs::remove_file(&certificate_path);
if let Err(error) = run {
return Err(error);
}
cleanup.map_err(|error| format!("remove benchmark v2 certificate: {error}"))?;
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"predicate-certificate-v2-cost status=VALID trials={repeats} output={}",
output.display()
);
Ok(())
}
fn benchmark_aiger_predicate_proof_relation(
input: &Path,
transcript: &Path,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=100).contains(&repeats) {
return Err("predicate proof relation repeats must be in 1..=100".to_string());
}
if output.exists() {
return Err("predicate proof relation refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let frames = parse_predicate_transcript(transcript, relevant_inputs.len())?;
if frames.iter().any(|frame| frame != &frames[0]) {
return Err(
"predicate proof relation benchmark requires one repeated constraint phase".to_string(),
);
}
let constraints = &frames[0];
let input_sha256 = sha256_file(input)?;
let transcript_sha256 = sha256_file(transcript)?;
let mut lines = vec!["schema_version,input_sha256,transcript_sha256,relevant_inputs,latches,trial,producer_relation_ns,proof_generate_ns,proof_verify_ns,exhaustive_verify_ns,exhaustive_evaluations,witnesses,proof_bytes,relation_agrees,status".to_string()];
for trial in 0..repeats {
let proof = predicate_proof_relation_experiment(&model, constraints)?;
let exhaustive_start = Instant::now();
let mut exhaustive = IndependentPredicateChecker::new(&model)?;
let exhaustive_relation = exhaustive.one_step_relation(constraints)?;
let exhaustive_verify_ns = exhaustive_start.elapsed().as_nanos();
if proof.relation != exhaustive_relation {
return Err("predicate proof and exhaustive relations disagree".to_string());
}
lines.push(format!(
"{PREDICATE_PROOF_RELATION_SCHEMA_VERSION},{input_sha256},{transcript_sha256},{},{},{trial},{},{},{},{exhaustive_verify_ns},{},{},{},true,ok",
relevant_inputs.len(),
model.latches.len(),
proof.producer_ns,
proof.generation_ns,
proof.verification_ns,
exhaustive.evaluations,
proof.witness_count,
proof.proof_bytes,
));
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"predicate-proof-relation status=VALID trials={repeats} output={}",
output.display()
);
Ok(())
}
fn benchmark_aiger_predicate_proof_terminal(
input: &Path,
bad_output: usize,
transcript: &Path,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=100).contains(&repeats) {
return Err("predicate proof terminal repeats must be in 1..=100".to_string());
}
if output.exists() {
return Err("predicate proof terminal refuses to overwrite output".to_string());
}
let model = parse_aag(input)?;
if bad_output >= model.outputs.len() {
return Err("predicate proof terminal output is out of range".to_string());
}
let relevant_inputs = IndependentPredicateChecker::support(&model)?;
let frames = parse_predicate_transcript(transcript, relevant_inputs.len())?;
if frames.iter().any(|frame| frame != &frames[0]) {
return Err(
"predicate proof terminal benchmark requires one repeated constraint phase".to_string(),
);
}
let constraints = &frames[0];
let input_sha256 = sha256_file(input)?;
let transcript_sha256 = sha256_file(transcript)?;
let mut lines = vec!["schema_version,input_sha256,transcript_sha256,bad_output,relevant_inputs,latches,trial,producer_terminal_ns,proof_generate_ns,proof_verify_ns,exhaustive_verify_ns,exhaustive_evaluations,witnesses,proof_bytes,safe_states,terminal_agrees,status".to_string()];
for trial in 0..repeats {
let proof = predicate_proof_terminal_experiment(&model, constraints, bad_output)?;
let exhaustive_start = Instant::now();
let mut exhaustive = IndependentPredicateChecker::new(&model)?;
let exhaustive_safe = exhaustive.terminal_safe_states(bad_output, constraints)?;
let exhaustive_verify_ns = exhaustive_start.elapsed().as_nanos();
if proof.safe_states != exhaustive_safe {
return Err("predicate proof and exhaustive terminal sets disagree".to_string());
}
lines.push(format!(
"{PREDICATE_PROOF_TERMINAL_SCHEMA_VERSION},{input_sha256},{transcript_sha256},{bad_output},{},{},{trial},{},{},{},{exhaustive_verify_ns},{},{},{},{:x},true,ok",
relevant_inputs.len(),
model.latches.len(),
proof.producer_ns,
proof.generation_ns,
proof.verification_ns,
exhaustive.evaluations,
proof.witness_count,
proof.proof_bytes,
proof.safe_states,
));
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"predicate-proof-terminal status=VALID trials={repeats} output={}",
output.display()
);
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct InterfaceRelation(DenseRelation);
impl InterfaceRelation {
fn states(&self) -> usize {
self.0.states()
}
fn empty(states: usize) -> Self {
Self(DenseRelation::empty(states).expect("validated interface state count"))
}
fn identity(states: usize) -> Self {
Self(DenseRelation::identity(states).expect("validated interface state count"))
}
fn insert(&mut self, source: usize, target: usize) {
self.0
.insert(source, target)
.expect("validated interface edge");
}
fn contains(&self, source: usize, target: usize) -> bool {
self.0
.contains(source, target)
.expect("validated interface query")
}
fn targets(&self, source: usize) -> impl Iterator<Item = usize> + '_ {
self.0
.targets(source)
.expect("validated interface source")
.into_iter()
}
fn compose(left: &Self, right: &Self) -> Result<Self, String> {
DenseRelation::compose(&left.0, &right.0)
.map(Self)
.map_err(|error| error.to_string())
}
fn power(base: &Self, exponent: usize) -> Result<Self, String> {
let exponent = u64::try_from(exponent)
.map_err(|_| "interface relation exponent exceeds u64".to_string())?;
DenseRelation::power(&base.0, exponent)
.map(Self)
.map_err(|error| error.to_string())
}
fn pairs(&self) -> usize {
self.0.pair_count()
}
fn rows(&self) -> &[Vec<u64>] {
self.0.row_words()
}
}
struct InterfaceQuotientTree<'a> {
table: &'a AagInterfaceTable,
horizon: usize,
leaf_count: usize,
nodes: Vec<InterfaceRelation>,
constraints: Vec<Vec<Option<bool>>>,
summary_cache: HashMap<(Vec<Option<bool>>, usize), InterfaceRelation>,
summary_cache_hits: usize,
summary_cache_misses: usize,
}
#[derive(Debug)]
struct InterfaceQueryResult {
states: Vec<usize>,
inputs: Vec<usize>,
declared_inputs: Vec<u64>,
}
#[derive(Debug)]
struct InterfaceQueryOutcome {
avoidable: bool,
witness: Option<InterfaceQueryResult>,
repaired_leaves: usize,
repaired_internal_nodes: usize,
}
impl<'a> InterfaceQuotientTree<'a> {
fn new(table: &'a AagInterfaceTable, horizon: usize) -> Result<Self, String> {
if horizon > INTERFACE_QUOTIENT_MAX_HORIZON {
return Err(format!(
"interface quotient horizon {horizon} exceeds {INTERFACE_QUOTIENT_MAX_HORIZON}"
));
}
let leaf_count = horizon.max(1).next_power_of_two();
let states = table.next.len();
let mut tree = Self {
table,
horizon,
leaf_count,
nodes: vec![InterfaceRelation::identity(states); leaf_count * 2],
constraints: vec![vec![None; table.input_count]; horizon + 1],
summary_cache: HashMap::new(),
summary_cache_hits: 0,
summary_cache_misses: 0,
};
for frame in 0..horizon {
tree.nodes[leaf_count + frame] = tree.leaf_relation(frame);
}
for node in (1..leaf_count).rev() {
tree.nodes[node] =
InterfaceRelation::compose(&tree.nodes[node * 2], &tree.nodes[node * 2 + 1])?;
}
Ok(tree)
}
fn leaf_relation(&self, frame: usize) -> InterfaceRelation {
let mut relation = InterfaceRelation::empty(self.table.next.len());
for state in 0..self.table.next.len() {
for input in 0..self.table.next[state].len() {
if self.table.input_allowed(input, &self.constraints[frame]) {
relation.insert(state, self.table.next[state][input]);
}
}
}
relation
}
fn relation_for_constraints(&self, constraints: &[Option<bool>]) -> InterfaceRelation {
let mut relation = InterfaceRelation::empty(self.table.next.len());
for state in 0..self.table.next.len() {
for input in 0..self.table.next[state].len() {
if self.table.input_allowed(input, constraints) {
relation.insert(state, self.table.next[state][input]);
}
}
}
relation
}
fn relation_power(
&mut self,
constraints: &[Option<bool>],
length: usize,
) -> Result<InterfaceRelation, String> {
let key = (constraints.to_vec(), length);
if let Some(relation) = self.summary_cache.get(&key) {
self.summary_cache_hits += 1;
return Ok(relation.clone());
}
let relation = if length == 0 {
InterfaceRelation::identity(self.table.next.len())
} else if length == 1 {
self.relation_for_constraints(constraints)
} else {
let left_length = length / 2;
let left = self.relation_power(constraints, left_length)?;
let right = self.relation_power(constraints, length - left_length)?;
InterfaceRelation::compose(&left, &right)?
};
if self.summary_cache.len() >= INTERFACE_QUOTIENT_MAX_SUMMARIES {
return Err(format!(
"interface quotient summary cache exceeds {INTERFACE_QUOTIENT_MAX_SUMMARIES} entries"
));
}
self.summary_cache.insert(key, relation.clone());
self.summary_cache_misses += 1;
Ok(relation)
}
fn compressed_relation(
&mut self,
constraints: &[Vec<Option<bool>>],
) -> Result<InterfaceRelation, String> {
let mut result = InterfaceRelation::identity(self.table.next.len());
let mut start = 0usize;
while start < self.horizon {
let mut end = start + 1;
while end < self.horizon && constraints[end] == constraints[start] {
end += 1;
}
let block = self.relation_power(&constraints[start], end - start)?;
result = InterfaceRelation::compose(&result, &block)?;
start = end;
}
Ok(result)
}
fn update_constraints(
&mut self,
constraints: &[Vec<Option<bool>>],
) -> Result<(usize, usize), String> {
if constraints.len() != self.horizon + 1
|| constraints
.iter()
.any(|frame| frame.len() != self.table.input_count)
{
return Err("interface quotient constraint dimensions mismatch".to_string());
}
let mut dirty = BTreeSet::new();
let mut repaired_leaves = 0usize;
for (frame, constraint) in constraints.iter().enumerate().take(self.horizon) {
if self.constraints[frame] != *constraint {
self.constraints[frame] = constraint.clone();
let leaf = self.leaf_count + frame;
let relation = self.leaf_relation(frame);
if self.nodes[leaf] != relation {
self.nodes[leaf] = relation;
dirty.insert(leaf / 2);
repaired_leaves += 1;
}
}
}
self.constraints[self.horizon] = constraints[self.horizon].clone();
let mut repaired_internal_nodes = 0usize;
while !dirty.is_empty() {
let current = std::mem::take(&mut dirty);
for node in current {
let composed =
InterfaceRelation::compose(&self.nodes[node * 2], &self.nodes[node * 2 + 1])?;
if self.nodes[node] != composed {
self.nodes[node] = composed;
if node > 1 {
dirty.insert(node / 2);
}
}
repaired_internal_nodes += 1;
}
}
Ok((repaired_leaves, repaired_internal_nodes))
}
fn initial_allowed(model: &AagModel, state: usize) -> bool {
model.latches.iter().enumerate().all(|(bit, latch)| {
latch
.initial
.is_none_or(|value| (state >> bit & 1 == 1) == value)
})
}
fn terminal_input(&self, state: usize, output: usize) -> Option<usize> {
(0..self.table.next[state].len()).find(|&input| {
self.table
.input_allowed(input, &self.constraints[self.horizon])
&& self.table.bad_masks[state][input] & (1u128 << output) == 0
})
}
#[allow(clippy::too_many_arguments)]
fn recover_node(
&self,
node: usize,
start: usize,
end: usize,
source: usize,
target: usize,
states: &mut [usize],
inputs: &mut [usize],
) -> Result<(), String> {
if end - start == 1 {
if start >= self.horizon {
if source != target {
return Err("interface quotient padding witness changed state".to_string());
}
states[start] = source;
states[end] = target;
return Ok(());
}
let input = (0..self.table.next[source].len())
.find(|&input| {
self.table.input_allowed(input, &self.constraints[start])
&& self.table.next[source][input] == target
})
.ok_or_else(|| "interface quotient leaf witness is missing".to_string())?;
states[start] = source;
states[end] = target;
inputs[start] = input;
return Ok(());
}
let middle_frame = start + (end - start) / 2;
let middle_state = self.nodes[node * 2]
.targets(source)
.find(|&middle| self.nodes[node * 2 + 1].contains(middle, target))
.ok_or_else(|| "interface quotient composition witness is missing".to_string())?;
self.recover_node(
node * 2,
start,
middle_frame,
source,
middle_state,
states,
inputs,
)?;
self.recover_node(
node * 2 + 1,
middle_frame,
end,
middle_state,
target,
states,
inputs,
)
}
fn query_avoiding(
&mut self,
model: &AagModel,
output: usize,
constraints: &[Vec<Option<bool>>],
recover_witness: bool,
) -> Result<InterfaceQueryOutcome, String> {
if output >= model.outputs.len() {
return Err("interface quotient output is out of range".to_string());
}
let (repaired_leaves, repaired_internal_nodes, relation) = if recover_witness {
let (leaves, internal) = self.update_constraints(constraints)?;
(leaves, internal, self.nodes[1].clone())
} else {
if constraints.len() != self.horizon + 1
|| constraints
.iter()
.any(|frame| frame.len() != self.table.input_count)
{
return Err("interface quotient constraint dimensions mismatch".to_string());
}
self.constraints[self.horizon] = constraints[self.horizon].clone();
(0, 0, self.compressed_relation(constraints)?)
};
let candidate = (0..self.table.next.len()).find_map(|source| {
if !Self::initial_allowed(model, source) {
return None;
}
relation.targets(source).find_map(|target| {
self.terminal_input(target, output)
.map(|input| (source, target, input))
})
});
let Some((source, target, terminal_input)) = candidate else {
return Ok(InterfaceQueryOutcome {
avoidable: false,
witness: None,
repaired_leaves,
repaired_internal_nodes,
});
};
if !recover_witness {
return Ok(InterfaceQueryOutcome {
avoidable: true,
witness: None,
repaired_leaves,
repaired_internal_nodes,
});
}
let mut states = vec![0usize; self.leaf_count + 1];
let mut inputs = vec![0usize; self.leaf_count + 1];
if self.horizon == 0 {
states[0] = source;
} else {
self.recover_node(
1,
0,
self.leaf_count,
source,
target,
&mut states,
&mut inputs,
)?;
}
states.truncate(self.horizon + 1);
inputs.truncate(self.horizon + 1);
inputs[self.horizon] = terminal_input;
let declared_inputs = inputs
.iter()
.map(|input| self.table.lift_input(*input))
.collect();
Ok(InterfaceQueryOutcome {
avoidable: true,
witness: Some(InterfaceQueryResult {
states,
inputs,
declared_inputs,
}),
repaired_leaves,
repaired_internal_nodes,
})
}
fn root_pairs(&self) -> usize {
self.nodes[1].pairs()
}
}
fn aag_temporal_formula(model: &AagModel, horizon: usize) -> Result<AagTemporalEncoding, String> {
if horizon == 0 {
return Err("AIGER horizon must be at least one".to_string());
}
if !model.inputs.is_empty() {
return Err("deterministic CQ-SAT/GCC encoding requires zero primary inputs".to_string());
}
if model.latches.len() > 9 {
return Err(format!(
"deterministic CQ-SAT/GCC encoding supports at most 9 latches; found {}",
model.latches.len()
));
}
let width = model.latches.len();
let patterns = 1usize << width;
let variables = horizon
.checked_add(1)
.and_then(|frames| frames.checked_mul(width))
.ok_or_else(|| "deterministic AIGER variable count overflow".to_string())?;
if variables > 2_000_000 {
return Err(format!(
"deterministic AIGER encoding requires {variables} variables; safety limit is 2000000"
));
}
let clause_count = horizon
.checked_mul(width)
.and_then(|value| value.checked_mul(patterns))
.ok_or_else(|| "deterministic AIGER clause count overflow".to_string())?;
if clause_count > 10_000_000 {
return Err(format!(
"deterministic AIGER encoding requires {clause_count} clauses; safety limit is 10000000"
));
}
let mut tables = Vec::with_capacity(patterns);
for pattern in 0..patterns {
let mut values = vec![false; model.max_variable + 1];
for (bit, latch) in model.latches.iter().enumerate() {
values[latch.current / 2] = pattern >> bit & 1 == 1;
}
for gate in &model.ands {
values[gate.output / 2] = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
}
tables.push(
model
.latches
.iter()
.map(|latch| evaluate_aag_literal(latch.next, &values))
.collect::<Vec<_>>(),
);
}
let mut formula = Vec::with_capacity(clause_count);
for time in 0..horizon {
let current = time * width;
let next = (time + 1) * width;
for (pattern, values) in tables.iter().enumerate() {
for (output, &value) in values.iter().enumerate() {
let mut literals = Vec::with_capacity(width + 1);
for bit in 0..width {
literals.push((current + bit, pattern >> bit & 1 == 0));
}
literals.push((next + output, value));
literals.sort_unstable();
formula.push(Clause(literals));
}
}
}
Ok((
variables,
formula,
model.latches.iter().map(|latch| latch.initial).collect(),
))
}
fn aag_bad_state_patterns(model: &AagModel) -> Vec<usize> {
let width = model.latches.len();
(0..(1usize << width))
.filter(|&pattern| {
let mut values = vec![false; model.max_variable + 1];
for (bit, latch) in model.latches.iter().enumerate() {
values[latch.current / 2] = pattern >> bit & 1 == 1;
}
for gate in &model.ands {
values[gate.output / 2] = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
}
model
.outputs
.iter()
.any(|&literal| evaluate_aag_literal(literal, &values))
})
.collect()
}
fn aag_property_query_space(
model: &AagModel,
horizon: usize,
) -> Result<Vec<AagPropertyQuery>, String> {
let width = model.latches.len();
let variables = width * (horizon + 1);
let bad_patterns = aag_bad_state_patterns(model);
if bad_patterns.is_empty() {
return Err("AIGER model has no state where a declared output is true".to_string());
}
let upper_bound = bad_patterns
.len()
.checked_mul(horizon + 1)
.ok_or_else(|| "AIGER property query count overflow".to_string())?;
if upper_bound > 1_000_000 {
return Err(format!(
"AIGER exhaustive property space has up to {upper_bound} queries; safety limit is 1000000"
));
}
let initial: Vec<_> = model.latches.iter().map(|latch| latch.initial).collect();
let mut queries = Vec::new();
for frame in 0..=horizon {
for &pattern in &bad_patterns {
if frame == 0
&& initial.iter().enumerate().any(|(bit, required)| {
required.is_some_and(|value| value != (pattern >> bit & 1 == 1))
})
{
continue;
}
let mut assumptions = vec![None; variables];
assumptions[..width].copy_from_slice(&initial);
for bit in 0..width {
assumptions[frame * width + bit] = Some(pattern >> bit & 1 == 1);
}
queries.push((frame, pattern, assumptions));
}
}
Ok(queries)
}
fn aag_property_queries(
model: &AagModel,
horizon: usize,
query_count: usize,
) -> Result<Vec<Vec<Option<bool>>>, String> {
let space = aag_property_query_space(model, horizon)?;
Ok((0..query_count)
.map(|index| space[index % space.len()].2.clone())
.collect())
}
#[derive(Clone, Copy)]
enum AagCnfLiteral {
Constant(bool),
Variable(Literal),
}
impl AagCnfLiteral {
fn negate(self) -> Self {
match self {
Self::Constant(value) => Self::Constant(!value),
Self::Variable((variable, positive)) => Self::Variable((variable, !positive)),
}
}
}
#[derive(Clone)]
struct AagBmcQuery {
frame: usize,
output: usize,
assumption: AagCnfLiteral,
}
struct AagBmcEncoding {
variables: usize,
clauses: Vec<Clause>,
queries: Vec<AagBmcQuery>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct AagInputConstraint {
name: String,
pattern: AagInputConstraintPattern,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum AagInputConstraintPattern {
Constant(bool),
StartupReset {
asserted_frames: usize,
asserted_value: bool,
},
}
impl AagInputConstraintPattern {
fn value_at(&self, frame: usize) -> bool {
match *self {
Self::Constant(value) => value,
Self::StartupReset {
asserted_frames,
asserted_value,
} => {
if frame < asserted_frames {
asserted_value
} else {
!asserted_value
}
}
}
}
fn report(&self) -> String {
match *self {
Self::Constant(value) => usize::from(value).to_string(),
Self::StartupReset {
asserted_frames,
asserted_value,
} => format!(
"startup(asserted_frames={asserted_frames},asserted_value={})",
usize::from(asserted_value)
),
}
}
}
fn resolve_aag_input_constraints(
model: &AagModel,
constraints: &[AagInputConstraint],
) -> Result<Vec<(usize, AagInputConstraintPattern)>, String> {
let mut resolved = Vec::with_capacity(constraints.len());
let mut seen = BTreeSet::new();
for constraint in constraints {
if !seen.insert(constraint.name.as_str()) {
return Err(format!(
"duplicate environment assumption: {}",
constraint.name
));
}
let matches = model
.input_names
.iter()
.enumerate()
.filter(|(_, name)| *name == &constraint.name)
.collect::<Vec<_>>();
if matches.len() != 1 {
return Err(format!(
"environment assumption input `{}` matched {} synthesized inputs; expected exactly one",
constraint.name,
matches.len()
));
}
resolved.push((matches[0].0, constraint.pattern.clone()));
}
Ok(resolved)
}
fn aag_cnf_literal(model: &AagModel, frame: usize, literal: usize) -> AagCnfLiteral {
if literal < 2 {
return AagCnfLiteral::Constant(literal == 1);
}
AagCnfLiteral::Variable((
frame * model.max_variable + literal / 2 - 1,
literal & 1 == 0,
))
}
fn push_simplified_clause(clauses: &mut Vec<Clause>, literals: &[AagCnfLiteral]) {
let mut clause = Vec::with_capacity(literals.len());
for &literal in literals {
match literal {
AagCnfLiteral::Constant(true) => return,
AagCnfLiteral::Constant(false) => {}
AagCnfLiteral::Variable(literal) => clause.push(literal),
}
}
clause.sort_unstable();
clause.dedup();
if clause
.windows(2)
.any(|pair| pair[0].0 == pair[1].0 && pair[0].1 != pair[1].1)
{
return;
}
clauses.push(Clause(clause));
}
fn encode_aag_equivalence(clauses: &mut Vec<Clause>, output: AagCnfLiteral, value: AagCnfLiteral) {
push_simplified_clause(clauses, &[output.negate(), value]);
push_simplified_clause(clauses, &[output, value.negate()]);
}
fn aag_bmc_encoding_with_constraints(
model: &AagModel,
horizon: usize,
constraints: &[AagInputConstraint],
) -> Result<AagBmcEncoding, String> {
let frames = horizon
.checked_add(1)
.ok_or_else(|| "AIGER horizon overflow".to_string())?;
let variables = frames
.checked_mul(model.max_variable)
.ok_or_else(|| "AIGER BMC variable count overflow".to_string())?;
if variables > 2_000_000 {
return Err(format!(
"AIGER BMC requires {variables} variables; safety limit is 2000000"
));
}
let resolved_constraints = resolve_aag_input_constraints(model, constraints)?;
let clause_bound = frames
.checked_mul(model.ands.len().saturating_mul(3))
.and_then(|value| value.checked_add(horizon.saturating_mul(model.latches.len() * 2)))
.and_then(|value| value.checked_add(frames.saturating_mul(resolved_constraints.len())))
.ok_or_else(|| "AIGER BMC clause count overflow".to_string())?;
if clause_bound > 10_000_000 {
return Err(format!(
"AIGER BMC requires up to {clause_bound} clauses; safety limit is 10000000"
));
}
let mut clauses = Vec::with_capacity(clause_bound + model.latches.len());
for latch in &model.latches {
if let Some(initial) = latch.initial {
push_simplified_clause(
&mut clauses,
&[AagCnfLiteral::Variable((latch.current / 2 - 1, initial))],
);
}
}
for frame in 0..=horizon {
for (input, pattern) in &resolved_constraints {
let value = pattern.value_at(frame);
push_simplified_clause(
&mut clauses,
&[AagCnfLiteral::Variable((
frame * model.max_variable + model.inputs[*input] / 2 - 1,
value,
))],
);
}
for gate in &model.ands {
let output = aag_cnf_literal(model, frame, gate.output);
let left = aag_cnf_literal(model, frame, gate.left);
let right = aag_cnf_literal(model, frame, gate.right);
push_simplified_clause(&mut clauses, &[output.negate(), left]);
push_simplified_clause(&mut clauses, &[output.negate(), right]);
push_simplified_clause(&mut clauses, &[output, left.negate(), right.negate()]);
}
if frame < horizon {
for latch in &model.latches {
encode_aag_equivalence(
&mut clauses,
aag_cnf_literal(model, frame + 1, latch.current),
aag_cnf_literal(model, frame, latch.next),
);
}
}
}
let mut queries = Vec::new();
for frame in 0..=horizon {
for (output, &literal) in model.outputs.iter().enumerate() {
let assumption = aag_cnf_literal(model, frame, literal);
if !matches!(assumption, AagCnfLiteral::Constant(false)) {
queries.push(AagBmcQuery {
frame,
output,
assumption,
});
}
}
}
Ok(AagBmcEncoding {
variables,
clauses,
queries,
})
}
fn aag_bmc_encoding(model: &AagModel, horizon: usize) -> Result<AagBmcEncoding, String> {
aag_bmc_encoding_with_constraints(model, horizon, &[])
}
fn normalized_temporal_steps(
formula: &[Clause],
width: usize,
horizon: usize,
) -> Result<Vec<Vec<Vec<Literal>>>, String> {
let mut steps = vec![Vec::new(); horizon];
for clause in formula {
let minimum = clause
.0
.iter()
.map(|&(variable, _)| variable / width)
.min()
.ok_or_else(|| "empty temporal clause".to_string())?;
let maximum = clause
.0
.iter()
.map(|&(variable, _)| variable / width)
.max()
.unwrap();
if maximum != minimum + 1 || minimum >= horizon {
return Err("clause is not local to adjacent temporal frames".to_string());
}
let offset = minimum * width;
let mut literals: Vec<_> = clause
.0
.iter()
.map(|&(variable, value)| (variable - offset, value))
.collect();
literals.sort_unstable();
steps[minimum].push(literals);
}
for step in &mut steps {
step.sort_unstable();
}
Ok(steps)
}
fn recognize_temporal_rules(
formula: &[Clause],
width: usize,
horizon: usize,
) -> Result<Vec<TemporalRule>, String> {
let steps = normalized_temporal_steps(formula, width, horizon)?;
let template = steps
.first()
.ok_or_else(|| "temporal horizon must be positive".to_string())?;
for (time, step) in steps.iter().enumerate().skip(1) {
if step != template {
return Err(format!("transition template changes at time {time}"));
}
}
let mut rules = Vec::with_capacity(width);
for output in 0..width {
let output_variable = width + output;
let local: Vec<_> = template
.iter()
.filter(|clause| {
clause
.iter()
.any(|&(variable, _)| variable == output_variable)
})
.collect();
if local.is_empty() {
return Err(format!("output {output} has no defining clauses"));
}
let mut dependencies: Vec<_> = local
.iter()
.flat_map(|clause| clause.iter())
.filter_map(|&(variable, _)| (variable < width).then_some(variable))
.collect();
dependencies.sort_unstable();
dependencies.dedup();
let mut table = Vec::with_capacity(1usize << dependencies.len());
for pattern in 0..(1usize << dependencies.len()) {
let mut valid = Vec::new();
for output_value in [false, true] {
let satisfies_local = local.iter().all(|clause| {
clause.iter().any(|&(variable, positive)| {
let value = if variable == output_variable {
output_value
} else {
let position = dependencies
.iter()
.position(|&dependency| dependency == variable)
.expect("recognized dependency");
(pattern >> position) & 1 == 1
};
value == positive
})
});
if satisfies_local {
valid.push(output_value);
}
}
if valid.len() != 1 {
return Err(format!(
"output {output} is not a deterministic local function"
));
}
table.push(valid[0]);
}
let mut candidates = Vec::new();
for a in 0..width {
candidates.push(TemporalRule::Copy(a));
candidates.push(TemporalRule::Negate(a));
for b in a + 1..width {
candidates.push(TemporalRule::Xor(a, b));
}
}
if width >= 3 {
for a in 0..width {
for b in 0..width {
for c in 0..width {
if a != b && a != c && b != c {
candidates.push(TemporalRule::Circuit(a, b, c));
}
}
}
}
}
let rule = candidates
.into_iter()
.find(|candidate| {
candidate.dependencies() == dependencies
&& (0..(1usize << dependencies.len())).all(|pattern| {
let mut state = 0usize;
for (position, &dependency) in dependencies.iter().enumerate() {
if (pattern >> position) & 1 == 1 {
state |= 1usize << dependency;
}
}
candidate.evaluate(state) == table[pattern]
})
})
.ok_or_else(|| format!("output {output} is outside the fixed vocabulary"))?;
rules.push(rule);
}
Ok(rules)
}
#[derive(Clone)]
struct LocalBooleanFunction {
dependencies: Vec<usize>,
table: Vec<bool>,
}
fn recover_local_transition(
formula: &[Clause],
width: usize,
horizon: usize,
) -> Result<Vec<LocalBooleanFunction>, String> {
let steps = normalized_temporal_steps(formula, width, horizon)?;
let template = steps
.first()
.ok_or_else(|| "temporal horizon must be positive".to_string())?;
if steps.iter().skip(1).any(|step| step != template) {
return Err("transition template is not repeated exactly".to_string());
}
let mut output_clauses: Vec<Vec<&Vec<Literal>>> = vec![Vec::new(); width];
for clause in template {
let mut outputs: Vec<_> = clause
.iter()
.filter(|&&(variable, _)| variable >= width)
.map(|&(variable, _)| variable - width)
.collect();
outputs.sort_unstable();
outputs.dedup();
if outputs.len() != 1 {
return Err(
"local recovery requires every clause to constrain exactly one output".to_string(),
);
}
output_clauses[outputs[0]].push(clause);
}
output_clauses
.iter()
.enumerate()
.map(|(output, clauses)| {
if clauses.is_empty() {
return Err(format!("output {output} has no defining clauses"));
}
let output_variable = width + output;
let mut dependencies: Vec<_> = clauses
.iter()
.flat_map(|clause| clause.iter())
.filter_map(|&(variable, _)| (variable < width).then_some(variable))
.collect();
dependencies.sort_unstable();
dependencies.dedup();
let mut table = Vec::with_capacity(1usize << dependencies.len());
for pattern in 0..(1usize << dependencies.len()) {
let valid: Vec<_> = [false, true]
.into_iter()
.filter(|&output_value| {
clauses.iter().all(|clause| {
clause.iter().any(|&(variable, positive)| {
let value = if variable == output_variable {
output_value
} else {
let position = dependencies
.iter()
.position(|&dependency| dependency == variable)
.expect("local dependency");
(pattern >> position) & 1 == 1
};
value == positive
})
})
})
.collect();
if valid.len() != 1 {
return Err(format!(
"output {output} is not a total deterministic local function"
));
}
table.push(valid[0]);
}
Ok(LocalBooleanFunction {
dependencies,
table,
})
})
.collect()
}
struct SymbolicTemporalTransition {
width: usize,
horizon: usize,
functions: Vec<LocalBooleanFunction>,
}
impl SymbolicTemporalTransition {
fn recognize(formula: &[Clause], width: usize, horizon: usize) -> Result<Self, String> {
Ok(Self {
width,
horizon,
functions: recover_local_transition(formula, width, horizon)?,
})
}
fn next_state(&self, state: &[bool]) -> Vec<bool> {
self.functions
.iter()
.map(|function| {
let pattern = function.dependencies.iter().enumerate().fold(
0usize,
|pattern, (position, &dependency)| {
pattern | (usize::from(state[dependency]) << position)
},
);
function.table[pattern]
})
.collect()
}
fn query(&self, assumptions: &[Option<bool>]) -> Option<Vec<bool>> {
let mut state: Vec<_> = assumptions
.iter()
.take(self.width)
.copied()
.collect::<Option<_>>()?;
let mut assignment = Vec::with_capacity(self.width * (self.horizon + 1));
for time in 0..=self.horizon {
if (0..self.width).any(|bit| {
assumptions[time * self.width + bit].is_some_and(|required| required != state[bit])
}) {
return None;
}
assignment.extend_from_slice(&state);
if time < self.horizon {
state = self.next_state(&state);
}
}
Some(assignment)
}
fn representation_entries(&self) -> usize {
self.functions
.iter()
.map(|function| function.dependencies.len() + function.table.len())
.sum()
}
}
fn compose_local_bdd(
manager: &mut BddManager,
inputs: &[usize],
table: &[bool],
level: usize,
pattern: usize,
) -> usize {
if level == inputs.len() {
return usize::from(table[pattern]);
}
let low = compose_local_bdd(manager, inputs, table, level + 1, pattern);
let high = compose_local_bdd(
manager,
inputs,
table,
level + 1,
pattern | (1usize << level),
);
if low == high {
return low;
}
let condition = inputs[level];
let mut memo = HashMap::new();
let not_condition = manager.negate(condition, &mut memo);
let low_branch = manager.and(not_condition, low);
let high_branch = manager.and(condition, high);
manager.or(low_branch, high_branch)
}
struct SymbolicPreimageTransition {
transition: SymbolicTemporalTransition,
manager: BddManager,
frames: Vec<Vec<usize>>,
rank_to_variable: Vec<usize>,
cycle_start: Option<usize>,
cycle_length: usize,
}
enum HybridTemporalPreimage {
Bdd(Box<SymbolicPreimageTransition>),
Cdcl {
solver: Solver<'static>,
variables: usize,
},
}
impl HybridTemporalPreimage {
fn recognize(
formula: &[Clause],
width: usize,
horizon: usize,
node_limit: usize,
) -> Result<(Self, &'static str, Option<String>), String> {
match SymbolicPreimageTransition::recognize_ordered(
formula,
width,
horizon,
node_limit,
"dependency-guard",
) {
Ok(preimage) => Ok((Self::Bdd(Box::new(preimage)), "bdd", None)),
Err(error) if error.contains("growth guard") => {
let mut solver = Solver::new();
add_to_varisat(&mut solver, formula);
Ok((
Self::Cdcl {
solver,
variables: width * (horizon + 1),
},
"cdcl-fallback",
Some(error),
))
}
Err(error) => Err(error),
}
}
fn query(&mut self, assumptions: &[Option<bool>]) -> Option<Vec<bool>> {
match self {
Self::Bdd(preimage) => preimage.query(assumptions),
Self::Cdcl { solver, variables } => {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
solver.assume(&literals);
if !solver.solve().expect("hybrid temporal CDCL solve") {
return None;
}
let mut assignment = vec![false; *variables];
for literal in solver.model().expect("hybrid temporal CDCL model") {
if literal.var().index() < *variables {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
}
}
}
fn bdd_metrics(&self) -> (usize, usize, usize, usize) {
match self {
Self::Bdd(preimage) => {
let (cycle_start, cycle_length) =
preimage.cycle().map_or((usize::MAX, 0usize), |cycle| cycle);
(
preimage.bdd_nodes(),
preimage.compiled_frames(),
cycle_start,
cycle_length,
)
}
Self::Cdcl { .. } => (0, 0, usize::MAX, 0),
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
enum EncodedLiteral {
Constant(bool),
Variable(usize, bool),
}
impl EncodedLiteral {
fn negated(self) -> Self {
match self {
Self::Constant(value) => Self::Constant(!value),
Self::Variable(variable, positive) => Self::Variable(variable, !positive),
}
}
}
struct AigBuilder {
next_variable: usize,
gates: Vec<(usize, EncodedLiteral, EncodedLiteral)>,
unique: HashMap<(EncodedLiteral, EncodedLiteral), EncodedLiteral>,
}
impl AigBuilder {
fn new(first_gate_variable: usize) -> Self {
Self {
next_variable: first_gate_variable,
gates: Vec::new(),
unique: HashMap::new(),
}
}
fn and(&mut self, a: EncodedLiteral, b: EncodedLiteral) -> EncodedLiteral {
match (a, b) {
(EncodedLiteral::Constant(false), _) | (_, EncodedLiteral::Constant(false)) => {
return EncodedLiteral::Constant(false);
}
(EncodedLiteral::Constant(true), other) | (other, EncodedLiteral::Constant(true)) => {
return other;
}
_ => {}
}
if a == b {
return a;
}
if a == b.negated() {
return EncodedLiteral::Constant(false);
}
let key = if a <= b { (a, b) } else { (b, a) };
if let Some(&literal) = self.unique.get(&key) {
return literal;
}
let output = EncodedLiteral::Variable(self.next_variable, true);
self.next_variable += 1;
self.gates.push((self.next_variable - 1, key.0, key.1));
self.unique.insert(key, output);
output
}
fn or(&mut self, a: EncodedLiteral, b: EncodedLiteral) -> EncodedLiteral {
let both_false = self.and(a.negated(), b.negated());
both_false.negated()
}
fn ite(
&mut self,
condition: EncodedLiteral,
high: EncodedLiteral,
low: EncodedLiteral,
) -> EncodedLiteral {
if high == low {
return high;
}
let high_branch = self.and(condition, high);
let low_branch = self.and(condition.negated(), low);
self.or(high_branch, low_branch)
}
}
fn add_encoded_clause(solver: &mut Solver<'_>, literals: &[EncodedLiteral]) {
if literals
.iter()
.any(|literal| matches!(literal, EncodedLiteral::Constant(true)))
{
return;
}
let clause: Vec<_> = literals
.iter()
.filter_map(|literal| match *literal {
EncodedLiteral::Constant(false) => None,
EncodedLiteral::Variable(variable, positive) => {
Some(Lit::from_var(Var::from_index(variable), positive))
}
EncodedLiteral::Constant(true) => unreachable!(),
})
.collect();
solver.add_clause(&clause);
}
fn encoded_bdd_literal(root: usize, positive: bool, original_variables: usize) -> EncodedLiteral {
if root < 2 {
EncodedLiteral::Constant((root == 1) == positive)
} else {
EncodedLiteral::Variable(original_variables + root - 2, positive)
}
}
struct LazyBddEncoding {
nodes: Vec<BddNode>,
rank_to_variable: Vec<usize>,
frames: Vec<Vec<usize>>,
encoded_nodes: Vec<bool>,
linked_variables: Vec<bool>,
}
fn encode_bdd_cone(
solver: &mut Solver<'_>,
lazy: &mut LazyBddEncoding,
variables: usize,
root: usize,
) -> usize {
if root < 2 || lazy.encoded_nodes[root - 2] {
return 0;
}
let node = lazy.nodes[root - 2];
let mut added = encode_bdd_cone(solver, lazy, variables, node.low);
added += encode_bdd_cone(solver, lazy, variables, node.high);
let output = variables + root - 2;
let input = lazy.rank_to_variable[node.variable];
let high = |positive| encoded_bdd_literal(node.high, positive, variables);
let low = |positive| encoded_bdd_literal(node.low, positive, variables);
add_encoded_clause(
solver,
&[
EncodedLiteral::Variable(input, false),
high(false),
EncodedLiteral::Variable(output, true),
],
);
add_encoded_clause(
solver,
&[
EncodedLiteral::Variable(input, false),
high(true),
EncodedLiteral::Variable(output, false),
],
);
add_encoded_clause(
solver,
&[
EncodedLiteral::Variable(input, true),
low(false),
EncodedLiteral::Variable(output, true),
],
);
add_encoded_clause(
solver,
&[
EncodedLiteral::Variable(input, true),
low(true),
EncodedLiteral::Variable(output, false),
],
);
lazy.encoded_nodes[root - 2] = true;
added + 1
}
fn link_lazy_frame_variable(
solver: &mut Solver<'_>,
lazy: &mut LazyBddEncoding,
variables: usize,
width: usize,
variable: usize,
) -> usize {
if lazy.linked_variables[variable] {
return 0;
}
let time = variable / width;
let bit = variable % width;
let root = lazy.frames[time][bit];
let added = encode_bdd_cone(solver, lazy, variables, root);
let literal = encoded_bdd_literal(root, true, variables);
add_encoded_clause(
solver,
&[EncodedLiteral::Variable(variable, false), literal],
);
add_encoded_clause(
solver,
&[EncodedLiteral::Variable(variable, true), literal.negated()],
);
lazy.linked_variables[variable] = true;
added
}
fn evaluate_bdd_root(lazy: &LazyBddEncoding, assignment: &[bool], mut root: usize) -> bool {
while root >= 2 {
let node = lazy.nodes[root - 2];
let variable = lazy.rank_to_variable[node.variable];
root = if assignment[variable] {
node.high
} else {
node.low
};
}
root == 1
}
struct CheckpointCdclPreimage {
solver: Solver<'static>,
variables: usize,
checkpoint: usize,
bdd_nodes: usize,
encoding_nodes: usize,
encoding_clauses: usize,
width: usize,
lazy: Option<LazyBddEncoding>,
}
impl CheckpointCdclPreimage {
fn recognize_with_encoding(
formula: &[Clause],
width: usize,
horizon: usize,
checkpoint: usize,
node_limit: usize,
encoding: &str,
) -> Result<Self, String> {
if checkpoint == 0 || checkpoint >= horizon {
return Err("checkpoint must be inside the temporal horizon".to_string());
}
let prefix: Vec<_> = formula
.iter()
.filter(|clause| {
clause
.0
.iter()
.map(|&(variable, _)| variable / width)
.min()
.is_some_and(|time| time < checkpoint)
})
.cloned()
.collect();
let preimage = SymbolicPreimageTransition::recognize_ordered(
&prefix,
width,
checkpoint,
node_limit,
"dependency",
)?;
let variables = width * (horizon + 1);
let mut solver = Solver::new();
let mut lazy = None;
let (roots, mut encoding_nodes, mut encoding_clauses) = match encoding {
"bdd" => {
for (index, node) in preimage.manager.nodes.iter().copied().enumerate() {
let output = variables + index;
let input = preimage.rank_to_variable[node.variable];
let high = |positive| encoded_bdd_literal(node.high, positive, variables);
let low = |positive| encoded_bdd_literal(node.low, positive, variables);
add_encoded_clause(
&mut solver,
&[
EncodedLiteral::Variable(input, false),
high(false),
EncodedLiteral::Variable(output, true),
],
);
add_encoded_clause(
&mut solver,
&[
EncodedLiteral::Variable(input, false),
high(true),
EncodedLiteral::Variable(output, false),
],
);
add_encoded_clause(
&mut solver,
&[
EncodedLiteral::Variable(input, true),
low(false),
EncodedLiteral::Variable(output, true),
],
);
add_encoded_clause(
&mut solver,
&[
EncodedLiteral::Variable(input, true),
low(true),
EncodedLiteral::Variable(output, false),
],
);
}
let roots = (0..=checkpoint)
.map(|time| {
let frame = preimage.frame(time);
frame
.iter()
.map(|&root| encoded_bdd_literal(root, true, variables))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
(roots, preimage.bdd_nodes(), preimage.bdd_nodes() * 4)
}
"aig" => {
let mut builder = AigBuilder::new(variables);
let mut literals = vec![
EncodedLiteral::Constant(false),
EncodedLiteral::Constant(true),
];
for node in preimage.manager.nodes.iter().copied() {
let condition =
EncodedLiteral::Variable(preimage.rank_to_variable[node.variable], true);
literals.push(builder.ite(condition, literals[node.high], literals[node.low]));
}
let roots = (0..=checkpoint)
.map(|time| {
preimage
.frame(time)
.iter()
.map(|&root| literals[root])
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
for &(output, a, b) in &builder.gates {
let out = EncodedLiteral::Variable(output, true);
add_encoded_clause(&mut solver, &[a.negated(), b.negated(), out]);
add_encoded_clause(&mut solver, &[a, out.negated()]);
add_encoded_clause(&mut solver, &[b, out.negated()]);
}
(roots, builder.gates.len(), builder.gates.len() * 3)
}
"lazy-bdd" => {
let expanded_frames = (0..=checkpoint)
.map(|time| preimage.frame(time).to_vec())
.collect::<Vec<_>>();
let roots = expanded_frames
.iter()
.map(|frame| {
frame
.iter()
.map(|&root| encoded_bdd_literal(root, true, variables))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
lazy = Some(LazyBddEncoding {
nodes: preimage.manager.nodes.clone(),
rank_to_variable: preimage.rank_to_variable.clone(),
frames: expanded_frames,
encoded_nodes: vec![false; preimage.bdd_nodes()],
linked_variables: vec![false; variables],
});
(roots, 0, 0)
}
_ => return Err(format!("unknown checkpoint encoding: {encoding}")),
};
let first_linked_frame = if encoding == "lazy-bdd" {
checkpoint
} else {
1
};
for (time, frame) in roots
.iter()
.enumerate()
.take(checkpoint + 1)
.skip(first_linked_frame)
{
for (bit, &root) in frame.iter().enumerate().take(width) {
let variable = time * width + bit;
if let Some(lazy) = lazy.as_mut() {
encoding_nodes +=
link_lazy_frame_variable(&mut solver, lazy, variables, width, variable);
encoding_clauses = encoding_nodes * 4;
} else {
add_encoded_clause(
&mut solver,
&[EncodedLiteral::Variable(variable, false), root],
);
add_encoded_clause(
&mut solver,
&[EncodedLiteral::Variable(variable, true), root.negated()],
);
}
}
}
let suffix: Vec<_> = formula
.iter()
.filter(|clause| {
clause
.0
.iter()
.map(|&(variable, _)| variable / width)
.min()
.is_some_and(|time| time >= checkpoint)
})
.cloned()
.collect();
add_to_varisat(&mut solver, &suffix);
Ok(Self {
solver,
variables,
checkpoint,
bdd_nodes: preimage.bdd_nodes(),
encoding_nodes,
encoding_clauses,
width,
lazy,
})
}
fn query(&mut self, assumptions: &[Option<bool>]) -> Option<Vec<bool>> {
let mut translated = Vec::new();
if let Some(lazy) = self.lazy.as_mut() {
for (variable, value) in assumptions.iter().enumerate() {
let time = variable / self.width;
if let Some(value) = value
&& time > 0
&& time < self.checkpoint
{
let root = lazy.frames[time][variable % self.width];
self.encoding_nodes +=
encode_bdd_cone(&mut self.solver, lazy, self.variables, root);
translated.push(encoded_bdd_literal(root, *value, self.variables));
}
}
self.encoding_clauses = self.encoding_nodes * 4;
}
for (variable, value) in assumptions.iter().enumerate() {
let time = variable / self.width;
if let Some(value) = value
&& (self.lazy.is_none() || time == 0 || time >= self.checkpoint)
{
translated.push(EncodedLiteral::Variable(variable, *value));
}
}
if translated
.iter()
.any(|literal| matches!(literal, EncodedLiteral::Constant(false)))
{
return None;
}
let literals: Vec<_> = translated
.iter()
.filter_map(|literal| match *literal {
EncodedLiteral::Variable(variable, positive) => {
Some(Lit::from_var(Var::from_index(variable), positive))
}
EncodedLiteral::Constant(true) => None,
EncodedLiteral::Constant(false) => unreachable!(),
})
.collect();
self.solver.assume(&literals);
if !self.solver.solve().expect("checkpoint CDCL solve") {
return None;
}
let mut assignment = vec![false; self.variables];
for literal in self.solver.model().expect("checkpoint CDCL model") {
if literal.var().index() < self.variables {
assignment[literal.var().index()] = literal.is_positive();
}
}
if let Some(lazy) = &self.lazy {
for time in 1..=self.checkpoint {
for bit in 0..self.width {
assignment[time * self.width + bit] =
evaluate_bdd_root(lazy, &assignment, lazy.frames[time][bit]);
}
}
}
Some(assignment)
}
}
fn generalized_checkpoint_core(
preimage: &mut SymbolicPreimageTransition,
checkpoint: usize,
width: usize,
constraint: usize,
state: &[bool],
) -> Vec<usize> {
let literals = state
.iter()
.enumerate()
.map(|(bit, &value)| {
let root = preimage.frame(checkpoint)[bit];
if value {
root
} else {
let mut memo = HashMap::new();
preimage.manager.negate(root, &mut memo)
}
})
.collect::<Vec<_>>();
let mut suffix = vec![1usize; width + 1];
for bit in (0..width).rev() {
suffix[bit] = preimage.manager.and(literals[bit], suffix[bit + 1]);
}
let mut prefix = constraint;
let mut core = Vec::with_capacity(width);
for bit in 0..width {
let without_current = preimage.manager.and(prefix, suffix[bit + 1]);
if without_current != 0 {
core.push(bit);
prefix = preimage.manager.and(prefix, literals[bit]);
}
}
debug_assert_eq!(prefix, 0);
core
}
struct NativeBddTheoryPreimage {
solver: Solver<'static>,
preimage: SymbolicPreimageTransition,
variables: usize,
width: usize,
checkpoint: usize,
activations: Vec<Var>,
theory_conflicts: usize,
theory_clauses: usize,
learned_literals: usize,
max_learned_width: usize,
global_clauses: usize,
global_literals: usize,
}
struct CqPortfolioDecision {
specialized: bool,
reason: &'static str,
clauses_per_bit_step: f64,
maximum_fanout: usize,
assumptions_per_query: f64,
}
fn cq_portfolio_decision(
formula: &[Clause],
width: usize,
horizon: usize,
expected_queries: usize,
assumptions_per_query: f64,
) -> Result<CqPortfolioDecision, String> {
let clauses_per_bit_step = formula.len() as f64 / horizon.max(1) as f64 / width as f64;
let mut maximum_fanout = 0usize;
let (specialized, reason) = if width <= 9
&& clauses_per_bit_step >= 12.0
&& expected_queries >= 8
&& assumptions_per_query <= width as f64
{
(true, "dense-transition")
} else if width <= 7 && expected_queries >= 128 && assumptions_per_query <= width as f64 {
let functions = recover_local_transition(formula, width, horizon)?;
let mut fanout = vec![0usize; width];
for function in &functions {
for &dependency in &function.dependencies {
fanout[dependency] += 1;
}
}
maximum_fanout = fanout.into_iter().max().unwrap_or(0);
if maximum_fanout >= width.saturating_sub(1) {
(true, "narrow-hub")
} else {
(false, "cdcl-fallback")
}
} else {
(false, "cdcl-fallback")
};
Ok(CqPortfolioDecision {
specialized,
reason,
clauses_per_bit_step,
maximum_fanout,
assumptions_per_query,
})
}
impl NativeBddTheoryPreimage {
fn recognize(
formula: &[Clause],
width: usize,
horizon: usize,
checkpoint: usize,
node_limit: usize,
) -> Result<Self, String> {
if checkpoint == 0 || checkpoint >= horizon {
return Err("checkpoint must be inside the temporal horizon".to_string());
}
let prefix = formula
.iter()
.filter(|clause| {
clause
.0
.iter()
.map(|&(variable, _)| variable / width)
.min()
.is_some_and(|time| time < checkpoint)
})
.cloned()
.collect::<Vec<_>>();
let preimage = SymbolicPreimageTransition::recognize_ordered(
&prefix,
width,
checkpoint,
node_limit,
"dependency",
)?;
let suffix = formula
.iter()
.filter(|clause| {
clause
.0
.iter()
.map(|&(variable, _)| variable / width)
.min()
.is_some_and(|time| time >= checkpoint)
})
.cloned()
.collect::<Vec<_>>();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &suffix);
let mut result = Self {
solver,
preimage,
variables: width * (horizon + 1),
width,
checkpoint,
activations: Vec::new(),
theory_conflicts: 0,
theory_clauses: 0,
learned_literals: 0,
max_learned_width: 0,
global_clauses: 0,
global_literals: 0,
};
result.compile_global_checkpoint_clauses(4_096);
Ok(result)
}
fn compile_global_checkpoint_clauses(&mut self, clause_limit: usize) {
if self.width >= usize::BITS as usize || self.width > 16 {
return;
}
let mut forbidden_cores: Vec<Vec<(usize, bool)>> = Vec::new();
for bits in 0..(1usize << self.width) {
if forbidden_cores.len() >= clause_limit {
break;
}
let state = (0..self.width)
.map(|bit| bits & (1usize << bit) != 0)
.collect::<Vec<_>>();
if forbidden_cores
.iter()
.any(|core| core.iter().all(|&(bit, value)| state[bit] == value))
{
continue;
}
let mut joint = 1usize;
for (bit, &value) in state.iter().enumerate() {
let root = self.preimage.frame(self.checkpoint)[bit];
let literal = if value {
root
} else {
let mut memo = HashMap::new();
self.preimage.manager.negate(root, &mut memo)
};
joint = self.preimage.manager.and(joint, literal);
if joint == 0 {
break;
}
}
if joint != 0 {
continue;
}
let core_bits = generalized_checkpoint_core(
&mut self.preimage,
self.checkpoint,
self.width,
1,
&state,
);
let core = core_bits
.iter()
.map(|&bit| (bit, state[bit]))
.collect::<Vec<_>>();
let clause = core
.iter()
.map(|&(bit, value)| {
Lit::from_var(Var::from_index(self.checkpoint * self.width + bit), !value)
})
.collect::<Vec<_>>();
self.solver.add_clause(&clause);
self.global_literals += core.len();
self.global_clauses += 1;
forbidden_cores.push(core);
}
}
fn query(&mut self, assumptions: &[Option<bool>]) -> Option<Vec<bool>> {
let mut constraint = 1usize;
for (variable, required) in assumptions.iter().enumerate() {
let Some(required) = required else { continue };
let time = variable / self.width;
if time > self.checkpoint {
continue;
}
let root = self.preimage.frame(time)[variable % self.width];
let literal = if *required {
root
} else {
let mut memo = HashMap::new();
self.preimage.manager.negate(root, &mut memo)
};
constraint = self.preimage.manager.and(constraint, literal);
if constraint == 0 {
return None;
}
}
let activation = Var::from_index(self.variables + self.activations.len());
let mut base_assumptions = self
.activations
.iter()
.map(|&old| Lit::from_var(old, false))
.collect::<Vec<_>>();
base_assumptions.push(Lit::from_var(activation, true));
self.activations.push(activation);
for (variable, required) in assumptions.iter().enumerate() {
if variable / self.width >= self.checkpoint
&& let Some(value) = required
{
base_assumptions.push(Lit::from_var(Var::from_index(variable), *value));
}
}
for bit in 0..self.width {
let root = self.preimage.frame(self.checkpoint)[bit];
let mut memo = HashMap::new();
let not_root = self.preimage.manager.negate(root, &mut memo);
if self.preimage.manager.and(constraint, not_root) == 0 {
base_assumptions.push(Lit::from_var(
Var::from_index(self.checkpoint * self.width + bit),
true,
));
} else if self.preimage.manager.and(constraint, root) == 0 {
base_assumptions.push(Lit::from_var(
Var::from_index(self.checkpoint * self.width + bit),
false,
));
}
}
for first in 0..self.width {
for second in (first + 1)..self.width {
for first_value in [false, true] {
for second_value in [false, true] {
let mut candidate = constraint;
for (bit, value) in [(first, first_value), (second, second_value)] {
let root = self.preimage.frame(self.checkpoint)[bit];
let literal = if value {
root
} else {
let mut memo = HashMap::new();
self.preimage.manager.negate(root, &mut memo)
};
candidate = self.preimage.manager.and(candidate, literal);
}
if candidate == 0 {
self.solver.add_clause(&[
Lit::from_var(activation, false),
Lit::from_var(
Var::from_index(self.checkpoint * self.width + first),
!first_value,
),
Lit::from_var(
Var::from_index(self.checkpoint * self.width + second),
!second_value,
),
]);
self.theory_clauses += 1;
}
}
}
}
}
loop {
self.solver.assume(&base_assumptions);
if !self.solver.solve().expect("native BDD theory solve") {
return None;
}
let model = self.solver.model().expect("native BDD theory model");
let mut assignment = vec![false; self.variables];
for literal in model {
if literal.var().index() < self.variables {
assignment[literal.var().index()] = literal.is_positive();
}
}
let checkpoint_state = (0..self.width)
.map(|bit| assignment[self.checkpoint * self.width + bit])
.collect::<Vec<_>>();
let mut joint = constraint;
for (bit, &value) in checkpoint_state.iter().enumerate() {
let root = self.preimage.frame(self.checkpoint)[bit];
let literal = if value {
root
} else {
let mut memo = HashMap::new();
self.preimage.manager.negate(root, &mut memo)
};
joint = self.preimage.manager.and(joint, literal);
if joint == 0 {
break;
}
}
if let Some(ranked) = self
.preimage
.manager
.satisfying_assignment(joint, self.width)
{
for time in 0..=self.checkpoint {
for bit in 0..self.width {
assignment[time * self.width + bit] = self
.preimage
.manager
.evaluate(self.preimage.frame(time)[bit], &ranked);
}
}
return Some(assignment);
}
let core = generalized_checkpoint_core(
&mut self.preimage,
self.checkpoint,
self.width,
constraint,
&checkpoint_state,
);
let mut block = Vec::with_capacity(core.len() + 1);
block.push(Lit::from_var(activation, false));
for &bit in &core {
block.push(Lit::from_var(
Var::from_index(self.checkpoint * self.width + bit),
!checkpoint_state[bit],
));
}
self.solver.add_clause(&block);
self.theory_conflicts += 1;
self.theory_clauses += 1;
self.learned_literals += core.len();
self.max_learned_width = self.max_learned_width.max(core.len());
}
}
}
impl SymbolicPreimageTransition {
fn recognize_ordered(
formula: &[Clause],
width: usize,
horizon: usize,
node_limit: usize,
order_kind: &str,
) -> Result<Self, String> {
let transition = SymbolicTemporalTransition::recognize(formula, width, horizon)?;
let growth_guard = order_kind == "dependency-guard";
let effective_order = if growth_guard {
"dependency"
} else {
order_kind
};
let rank_to_variable =
preimage_variable_order(&transition.functions, width, effective_order)?;
let mut variable_to_rank = vec![0usize; width];
for (rank, &variable) in rank_to_variable.iter().enumerate() {
variable_to_rank[variable] = rank;
}
let mut manager = BddManager {
node_limit: Some(node_limit),
..BddManager::default()
};
let initial: Vec<_> = (0..width)
.map(|variable| manager.literal(variable_to_rank[variable], true))
.collect();
let mut frames = vec![initial];
let mut seen = HashMap::new();
seen.insert(frames[0].clone(), 0usize);
let mut cycle_start = None;
let mut cycle_length = 0usize;
let mut previous_nodes = manager.nodes.len();
let mut previous_increment = 0usize;
for time in 0..horizon {
let previous = &frames[time];
let next: Vec<_> = transition
.functions
.iter()
.map(|function| {
let inputs: Vec<_> = function
.dependencies
.iter()
.map(|&dependency| previous[dependency])
.collect();
compose_local_bdd(&mut manager, &inputs, &function.table, 0, 0)
})
.collect();
if manager.budget_exceeded {
return Err(format!("BDD node limit exceeded at frame {}", time + 1));
}
let current_nodes = manager.nodes.len();
let increment = current_nodes.saturating_sub(previous_nodes);
if growth_guard
&& time >= 2
&& increment > previous_increment
&& current_nodes.saturating_add(increment.saturating_mul(4)) > node_limit
{
return Err(format!(
"BDD growth guard projected node exhaustion at frame {} ({current_nodes} nodes)",
time + 1
));
}
previous_nodes = current_nodes;
previous_increment = increment;
if let Some(&previous_time) = seen.get(&next) {
cycle_start = Some(previous_time);
cycle_length = time + 1 - previous_time;
break;
}
seen.insert(next.clone(), time + 1);
frames.push(next);
}
manager.node_limit = None;
Ok(Self {
transition,
manager,
frames,
rank_to_variable,
cycle_start,
cycle_length,
})
}
fn frame(&self, time: usize) -> &[usize] {
if time < self.frames.len() {
return &self.frames[time];
}
let start = self
.cycle_start
.expect("time beyond compiled acyclic frames");
&self.frames[start + (time - start) % self.cycle_length]
}
fn query(&mut self, assumptions: &[Option<bool>]) -> Option<Vec<bool>> {
let mut constraint = 1usize;
for (variable, required) in assumptions.iter().enumerate() {
let Some(required) = required else {
continue;
};
let time = variable / self.transition.width;
let bit = variable % self.transition.width;
let root = self.frame(time)[bit];
let literal = if *required {
root
} else {
let mut memo = HashMap::new();
self.manager.negate(root, &mut memo)
};
constraint = self.manager.and(constraint, literal);
if constraint == 0 {
return None;
}
}
let ranked = self
.manager
.satisfying_assignment(constraint, self.transition.width)?;
let mut initial = vec![false; self.transition.width];
for (rank, &variable) in self.rank_to_variable.iter().enumerate() {
initial[variable] = ranked[rank];
}
let mut concrete = vec![None; assumptions.len()];
for (bit, &value) in initial.iter().enumerate() {
concrete[bit] = Some(value);
}
for (variable, required) in assumptions.iter().enumerate().skip(self.transition.width) {
concrete[variable] = *required;
}
self.transition.query(&concrete)
}
fn bdd_nodes(&self) -> usize {
self.manager.nodes.len()
}
fn compiled_frames(&self) -> usize {
self.frames.len()
}
fn cycle(&self) -> Option<(usize, usize)> {
self.cycle_start.map(|start| (start, self.cycle_length))
}
}
fn preimage_variable_order(
functions: &[LocalBooleanFunction],
width: usize,
kind: &str,
) -> Result<Vec<usize>, String> {
match kind {
"natural" => Ok((0..width).collect()),
"reverse" => Ok((0..width).rev().collect()),
"evenodd" => Ok((0..width)
.filter(|variable| variable % 2 == 0)
.chain((0..width).filter(|variable| variable % 2 == 1))
.collect()),
"dependency" => {
let mut adjacency = vec![BTreeSet::new(); width];
for function in functions {
for &left in &function.dependencies {
for &right in &function.dependencies {
if left != right {
adjacency[left].insert(right);
}
}
}
}
let start = (0..width)
.max_by_key(|&variable| (adjacency[variable].len(), usize::MAX - variable))
.unwrap_or(0);
let mut order = Vec::with_capacity(width);
let mut seen = vec![false; width];
let mut frontier = vec![start];
while let Some(variable) = frontier.pop() {
if seen[variable] {
continue;
}
seen[variable] = true;
order.push(variable);
let mut neighbours: Vec<_> = adjacency[variable]
.iter()
.copied()
.filter(|&next| !seen[next])
.collect();
neighbours.sort_by_key(|&next| (adjacency[next].len(), next));
frontier.extend(neighbours.into_iter().rev());
}
order.extend((0..width).filter(|&variable| !seen[variable]));
Ok(order)
}
_ => Err(format!("unknown preimage variable order: {kind}")),
}
}
struct RecognizedTemporalKernel {
width: usize,
horizon: usize,
jumps: Vec<Vec<usize>>,
}
impl RecognizedTemporalKernel {
fn recognize(formula: &[Clause], width: usize, horizon: usize) -> Result<Self, String> {
let rules = recognize_temporal_rules(formula, width, horizon)?;
let states = 1usize << width;
let mut base = vec![0usize; states];
for (state, target) in base.iter_mut().enumerate() {
for (output, rule) in rules.iter().enumerate() {
if rule.evaluate(state) {
*target |= 1usize << output;
}
}
}
Ok(Self::from_base(width, horizon, base))
}
fn recognize_exact_composition(
formula: &[Clause],
width: usize,
horizon: usize,
) -> Result<Self, String> {
let steps = normalized_temporal_steps(formula, width, horizon)?;
let template = steps
.first()
.ok_or_else(|| "temporal horizon must be positive".to_string())?;
if steps.iter().skip(1).any(|step| step != template) {
return Err("transition template is not repeated exactly".to_string());
}
let states = 1usize << width;
let mut base = vec![0usize; states];
for current in 0..states {
let mut target = None;
for next in 0..states {
let satisfies = template.iter().all(|clause| {
clause.iter().any(|&(variable, positive)| {
let value = if variable < width {
(current >> variable) & 1 == 1
} else {
(next >> (variable - width)) & 1 == 1
};
value == positive
})
});
if satisfies {
if target.replace(next).is_some() {
return Err(format!(
"transition is nondeterministic for state {current}"
));
}
}
}
base[current] =
target.ok_or_else(|| format!("transition is incomplete for state {current}"))?;
}
Ok(Self::from_base(width, horizon, base))
}
fn recognize_local_composition(
formula: &[Clause],
width: usize,
horizon: usize,
) -> Result<Self, String> {
let steps = normalized_temporal_steps(formula, width, horizon)?;
let template = steps
.first()
.ok_or_else(|| "temporal horizon must be positive".to_string())?;
if steps.iter().skip(1).any(|step| step != template) {
return Err("transition template is not repeated exactly".to_string());
}
let mut output_clauses: Vec<Vec<&Vec<Literal>>> = vec![Vec::new(); width];
for clause in template {
let mut outputs: Vec<_> = clause
.iter()
.filter(|&&(variable, _)| variable >= width)
.map(|&(variable, _)| variable - width)
.collect();
outputs.sort_unstable();
outputs.dedup();
if outputs.len() != 1 {
return Err(
"local recovery requires every clause to constrain exactly one output"
.to_string(),
);
}
output_clauses[outputs[0]].push(clause);
}
let mut recovered = Vec::with_capacity(width);
for (output, clauses) in output_clauses.iter().enumerate() {
if clauses.is_empty() {
return Err(format!("output {output} has no defining clauses"));
}
let output_variable = width + output;
let mut dependencies: Vec<_> = clauses
.iter()
.flat_map(|clause| clause.iter())
.filter_map(|&(variable, _)| (variable < width).then_some(variable))
.collect();
dependencies.sort_unstable();
dependencies.dedup();
let mut table = Vec::with_capacity(1usize << dependencies.len());
for pattern in 0..(1usize << dependencies.len()) {
let valid: Vec<_> = [false, true]
.into_iter()
.filter(|&output_value| {
clauses.iter().all(|clause| {
clause.iter().any(|&(variable, positive)| {
let value = if variable == output_variable {
output_value
} else {
let position = dependencies
.iter()
.position(|&dependency| dependency == variable)
.expect("local dependency");
(pattern >> position) & 1 == 1
};
value == positive
})
})
})
.collect();
if valid.len() != 1 {
return Err(format!(
"output {output} is not a total deterministic local function"
));
}
table.push(valid[0]);
}
recovered.push((dependencies, table));
}
let states = 1usize << width;
let mut base = vec![0usize; states];
for (state, target) in base.iter_mut().enumerate() {
for (output, (dependencies, table)) in recovered.iter().enumerate() {
let mut pattern = 0usize;
for (position, &dependency) in dependencies.iter().enumerate() {
pattern |= ((state >> dependency) & 1) << position;
}
if table[pattern] {
*target |= 1usize << output;
}
}
}
Ok(Self::from_base(width, horizon, base))
}
fn from_base(width: usize, horizon: usize, base: Vec<usize>) -> Self {
let levels = (usize::BITS - horizon.max(1).leading_zeros()) as usize;
let mut jumps = vec![base];
for level in 1..levels {
let previous = &jumps[level - 1];
let next = previous.iter().map(|&state| previous[state]).collect();
jumps.push(next);
}
Self {
width,
horizon,
jumps,
}
}
fn advance(&self, mut state: usize, mut steps: usize) -> usize {
let mut level = 0usize;
while steps > 0 {
if steps & 1 == 1 {
state = self.jumps[level][state];
}
steps >>= 1;
level += 1;
}
state
}
fn query(&self, assumptions: &[Option<bool>]) -> Option<Vec<bool>> {
let states = 1usize << self.width;
let observations: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| (variable / self.width, variable % self.width, value))
})
.collect();
let initial = (0..states).find(|&initial| {
observations
.iter()
.all(|&(time, bit, value)| ((self.advance(initial, time) >> bit) & 1 == 1) == value)
})?;
let mut assignment = Vec::with_capacity(self.width * (self.horizon + 1));
let mut state = initial;
for time in 0..=self.horizon {
for bit in 0..self.width {
assignment.push((state >> bit) & 1 == 1);
}
if time < self.horizon {
state = self.jumps[0][state];
}
}
Some(assignment)
}
}
fn parse_size_grid(value: &str, name: &str) -> Result<Vec<usize>, String> {
let values: Result<Vec<_>, _> = value
.split(',')
.filter(|item| !item.is_empty())
.map(str::parse::<usize>)
.collect();
let values = values.map_err(|_| format!("invalid {name} grid"))?;
if values.is_empty() || values.contains(&0) {
return Err(format!("{name} grid must contain positive integers"));
}
Ok(values)
}
fn benchmark_continuation_temporal_phase(
widths: &[usize],
horizons: &[usize],
query_count: usize,
max_bound_bits: usize,
seed: u64,
output: &Path,
) -> Result<(), String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create temporal output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "width,horizon,variables,clauses,queries,max_bound_bits,frontier_bound_bits,admitted,peak_classes,total_layer_states,compile_ns,quotient_ns_per_query,kernel_ns_per_query,incremental_varisat_ns_per_query,quotient_speedup_vs_incremental,kernel_speedup_vs_incremental,quotient_break_even_queries,sat_queries,unsat_queries,agreement,kernel_agreement,witnesses_valid,kernel_witnesses_valid")
.map_err(|error| format!("write temporal header: {error}"))?;
for &width in widths {
for &horizon in horizons {
let vars = width * (horizon + 1);
let clause_count = 2 * width * horizon;
let bound_bits = width + 1;
if bound_bits > max_bound_bits {
writeln!(file, "{width},{horizon},{vars},{clause_count},{query_count},{max_bound_bits},{bound_bits},false,0,0,0,0,0,0,0,0,0,0,true,true,true,true,true")
.map_err(|error| format!("write rejected temporal row: {error}"))?;
continue;
}
let (_, formula) = temporal_memory_formula(width, horizon);
let compile_start = Instant::now();
let compiled = compile_temporal_memory_continuation(width, horizon);
let compile_ns = compile_start.elapsed().as_nanos();
let total_layer_states: usize =
compiled.transitions.iter().map(Vec::len).sum::<usize>()
+ compiled.terminal_sat.len();
let mut rng = Rng(seed ^ (width as u64).rotate_left(17) ^ horizon as u64);
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
let observed_bits = 1 + query_index % width.min(4);
let mut chosen = BTreeSet::new();
while chosen.len() < observed_bits {
chosen.insert(rng.below(width));
}
for bit in chosen {
let first_time = rng.below(horizon + 1);
let mut second_time = rng.below(horizon + 1);
if horizon > 0 && second_time == first_time {
second_time = (second_time + 1) % (horizon + 1);
}
assumptions[first_time * width + bit] = Some(rng.next() & 1 == 1);
assumptions[second_time * width + bit] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let mut scratch = ContinuationScratch::new(&compiled);
let quotient_start = Instant::now();
let quotient_answers: Vec<_> = queries
.iter()
.map(|assumptions| query_continuation(&compiled, assumptions, &mut scratch))
.collect();
let quotient_ns = quotient_start.elapsed().as_nanos();
let kernel_start = Instant::now();
let kernel_answers: Vec<_> = queries
.iter()
.map(|assumptions| query_temporal_memory_kernel(width, horizon, assumptions))
.collect();
let kernel_ns = kernel_start.elapsed().as_nanos();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &formula);
let varisat_start = Instant::now();
let varisat_answers: Vec<Option<Vec<bool>>> = queries
.iter()
.map(|assumptions| {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
solver.assume(&literals);
if !solver.solve().expect("temporal Varisat solve") {
return None;
}
let mut assignment = vec![false; vars];
for literal in solver.model().expect("temporal Varisat model") {
if literal.var().index() < vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
})
.collect();
let varisat_ns = varisat_start.elapsed().as_nanos();
let agreement = quotient_answers
.iter()
.zip(&varisat_answers)
.all(|(left, right)| left.is_some() == right.is_some());
let kernel_agreement = kernel_answers
.iter()
.zip(&varisat_answers)
.all(|(left, right)| left.is_some() == right.is_some());
let witnesses_valid =
quotient_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let kernel_witnesses_valid =
kernel_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = quotient_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count - sat_queries;
let quotient_per_query = quotient_ns as f64 / query_count as f64;
let kernel_per_query = kernel_ns as f64 / query_count as f64;
let varisat_per_query = varisat_ns as f64 / query_count as f64;
let quotient_speedup = varisat_per_query / quotient_per_query.max(1.0);
let kernel_speedup = varisat_per_query / kernel_per_query.max(1.0);
let break_even = if varisat_per_query > quotient_per_query {
(compile_ns as f64 / (varisat_per_query - quotient_per_query)).ceil() as u128
} else {
u128::MAX
};
writeln!(file, "{width},{horizon},{vars},{},{query_count},{max_bound_bits},{bound_bits},true,{},{total_layer_states},{compile_ns},{quotient_per_query:.3},{kernel_per_query:.3},{varisat_per_query:.3},{quotient_speedup:.6},{kernel_speedup:.6},{break_even},{sat_queries},{unsat_queries},{agreement},{kernel_agreement},{witnesses_valid},{kernel_witnesses_valid}", formula.len(), compiled.peak_classes)
.map_err(|error| format!("write temporal row: {error}"))?;
file.flush()
.map_err(|error| format!("flush temporal output: {error}"))?;
println!(
"temporal phase width={width} horizon={horizon} vars={vars} admitted=true peak={} quotient_speedup={quotient_speedup:.3} kernel_speedup={kernel_speedup:.3} agreement={agreement} kernel_agreement={kernel_agreement} witnesses_valid={witnesses_valid} kernel_witnesses_valid={kernel_witnesses_valid}",
compiled.peak_classes
);
}
}
Ok(())
}
fn benchmark_temporal_vocabulary(
kinds: &[&str],
widths: &[usize],
horizons: &[usize],
query_count: usize,
max_width: usize,
seed: u64,
output: &Path,
) -> Result<(), String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create temporal vocabulary output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "kind,width,horizon,variables,clauses,queries,max_width,admitted,recognition_ns,jump_table_states,kernel_ns_per_query,incremental_varisat_ns_per_query,speedup_vs_incremental,break_even_queries,sat_queries,unsat_queries,agreement,witnesses_valid,status")
.map_err(|error| format!("write temporal vocabulary header: {error}"))?;
for &kind in kinds {
for &width in widths {
for &horizon in horizons {
let vars = width * (horizon + 1);
if width > max_width {
writeln!(file, "{kind},{width},{horizon},{vars},0,{query_count},{max_width},false,0,0,0,0,0,0,0,0,true,true,width_gate")
.map_err(|error| format!("write vocabulary rejection: {error}"))?;
continue;
}
let (_, formula) = temporal_vocabulary_formula(kind, width, horizon)?;
let recognition_start = Instant::now();
let kernel = RecognizedTemporalKernel::recognize(&formula, width, horizon)?;
let recognition_ns = recognition_start.elapsed().as_nanos();
let jump_table_states: usize = kernel.jumps.iter().map(Vec::len).sum();
let mut rng =
Rng(seed ^ (width as u64).rotate_left(13) ^ (horizon as u64).rotate_left(31));
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
let observations = 2 + query_index % 5;
for _ in 0..observations {
let time = rng.below(horizon + 1);
let bit = rng.below(width);
assumptions[time * width + bit] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let kernel_start = Instant::now();
let kernel_answers: Vec<_> = queries
.iter()
.map(|assumptions| kernel.query(assumptions))
.collect();
let kernel_ns = kernel_start.elapsed().as_nanos();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &formula);
let varisat_start = Instant::now();
let varisat_answers: Vec<Option<Vec<bool>>> = queries
.iter()
.map(|assumptions| {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
solver.assume(&literals);
if !solver.solve().expect("temporal vocabulary Varisat solve") {
return None;
}
let mut assignment = vec![false; vars];
for literal in solver.model().expect("temporal vocabulary model") {
if literal.var().index() < vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
})
.collect();
let varisat_ns = varisat_start.elapsed().as_nanos();
let agreement = kernel_answers
.iter()
.zip(&varisat_answers)
.all(|(left, right)| left.is_some() == right.is_some());
let witnesses_valid =
kernel_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = kernel_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count - sat_queries;
let kernel_per_query = kernel_ns as f64 / query_count as f64;
let varisat_per_query = varisat_ns as f64 / query_count as f64;
let speedup = varisat_per_query / kernel_per_query.max(1.0);
let break_even = if varisat_per_query > kernel_per_query {
(recognition_ns as f64 / (varisat_per_query - kernel_per_query)).ceil() as u128
} else {
u128::MAX
};
writeln!(file, "{kind},{width},{horizon},{vars},{},{query_count},{max_width},true,{recognition_ns},{jump_table_states},{kernel_per_query:.3},{varisat_per_query:.3},{speedup:.6},{break_even},{sat_queries},{unsat_queries},{agreement},{witnesses_valid},ok", formula.len())
.map_err(|error| format!("write temporal vocabulary row: {error}"))?;
file.flush()
.map_err(|error| format!("flush temporal vocabulary output: {error}"))?;
println!(
"temporal vocabulary kind={kind} width={width} horizon={horizon} vars={vars} speedup={speedup:.3} agreement={agreement} witnesses_valid={witnesses_valid}"
);
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn benchmark_temporal_compositions(
kinds: &[&str],
widths: &[usize],
horizons: &[usize],
query_count: usize,
max_width: usize,
seed: u64,
output: &Path,
local_recovery: bool,
) -> Result<(), String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create temporal composition output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "kind,width,horizon,variables,clauses,queries,max_width,admitted,recognition_ns,jump_table_states,kernel_ns_per_query,incremental_varisat_ns_per_query,speedup_vs_incremental,break_even_queries,sat_queries,unsat_queries,agreement,witnesses_valid,status")
.map_err(|error| format!("write temporal composition header: {error}"))?;
for &kind in kinds {
for &width in widths {
for &horizon in horizons {
let vars = width * (horizon + 1);
if width > max_width {
writeln!(file, "{kind},{width},{horizon},{vars},0,{query_count},{max_width},false,0,0,0,0,0,0,0,0,true,true,width_gate")
.map_err(|error| format!("write composition rejection: {error}"))?;
continue;
}
let (_, formula) = temporal_composition_formula(kind, width, horizon)?;
let recognition_start = Instant::now();
let kernel = if local_recovery {
RecognizedTemporalKernel::recognize_local_composition(&formula, width, horizon)?
} else {
RecognizedTemporalKernel::recognize_exact_composition(&formula, width, horizon)?
};
let recognition_ns = recognition_start.elapsed().as_nanos();
let jump_table_states: usize = kernel.jumps.iter().map(Vec::len).sum();
let mut rng =
Rng(seed ^ (width as u64).rotate_left(11) ^ (horizon as u64).rotate_left(29));
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
for _ in 0..(2 + query_index % 5) {
let variable = rng.below(vars);
assumptions[variable] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let kernel_start = Instant::now();
let kernel_answers: Vec<_> = queries
.iter()
.map(|assumptions| kernel.query(assumptions))
.collect();
let kernel_ns = kernel_start.elapsed().as_nanos();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &formula);
let varisat_start = Instant::now();
let varisat_answers: Vec<Option<Vec<bool>>> = queries
.iter()
.map(|assumptions| {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
solver.assume(&literals);
if !solver.solve().expect("temporal composition Varisat solve") {
return None;
}
let mut assignment = vec![false; vars];
for literal in solver.model().expect("temporal composition model") {
if literal.var().index() < vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
})
.collect();
let varisat_ns = varisat_start.elapsed().as_nanos();
let agreement = kernel_answers
.iter()
.zip(&varisat_answers)
.all(|(left, right)| left.is_some() == right.is_some());
let witnesses_valid =
kernel_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = kernel_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count - sat_queries;
let kernel_per_query = kernel_ns as f64 / query_count as f64;
let varisat_per_query = varisat_ns as f64 / query_count as f64;
let speedup = varisat_per_query / kernel_per_query.max(1.0);
let break_even = if varisat_per_query > kernel_per_query {
(recognition_ns as f64 / (varisat_per_query - kernel_per_query)).ceil() as u128
} else {
u128::MAX
};
writeln!(file, "{kind},{width},{horizon},{vars},{},{query_count},{max_width},true,{recognition_ns},{jump_table_states},{kernel_per_query:.3},{varisat_per_query:.3},{speedup:.6},{break_even},{sat_queries},{unsat_queries},{agreement},{witnesses_valid},ok", formula.len())
.map_err(|error| format!("write temporal composition row: {error}"))?;
file.flush()
.map_err(|error| format!("flush temporal composition output: {error}"))?;
println!(
"temporal composition recognizer={} kind={kind} width={width} horizon={horizon} vars={vars} speedup={speedup:.3} agreement={agreement} witnesses_valid={witnesses_valid}",
if local_recovery { "local" } else { "exact" }
);
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn benchmark_symbolic_temporal_compositions(
kinds: &[&str],
widths: &[usize],
horizons: &[usize],
query_count: usize,
max_width: usize,
seed: u64,
output: &Path,
) -> Result<(), String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create symbolic temporal output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "kind,width,horizon,variables,clauses,queries,max_width,admitted,recognition_ns,representation_entries,symbolic_ns_per_query,incremental_varisat_ns_per_query,speedup_vs_incremental,sat_queries,unsat_queries,agreement,witnesses_valid,status")
.map_err(|error| format!("write symbolic temporal header: {error}"))?;
for &kind in kinds {
for &width in widths {
for &horizon in horizons {
let vars = width * (horizon + 1);
if width > max_width {
writeln!(file, "{kind},{width},{horizon},{vars},0,{query_count},{max_width},false,0,0,0,0,0,0,0,true,true,width_gate")
.map_err(|error| format!("write symbolic rejection: {error}"))?;
continue;
}
let (_, formula) = temporal_composition_formula(kind, width, horizon)?;
let recognition_start = Instant::now();
let transition = SymbolicTemporalTransition::recognize(&formula, width, horizon)?;
let recognition_ns = recognition_start.elapsed().as_nanos();
let representation_entries = transition.representation_entries();
let mut rng =
Rng(seed ^ (width as u64).rotate_left(17) ^ (horizon as u64).rotate_left(37));
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
for value in assumptions.iter_mut().take(width) {
*value = Some(rng.next() & 1 == 1);
}
for _ in 0..(1 + query_index % 4) {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let symbolic_start = Instant::now();
let symbolic_answers: Vec<_> = queries
.iter()
.map(|assumptions| transition.query(assumptions))
.collect();
let symbolic_ns = symbolic_start.elapsed().as_nanos();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &formula);
let varisat_start = Instant::now();
let varisat_answers: Vec<_> = queries
.iter()
.map(|assumptions| {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
solver.assume(&literals);
solver.solve().expect("symbolic temporal Varisat solve")
})
.collect();
let varisat_ns = varisat_start.elapsed().as_nanos();
let agreement = symbolic_answers
.iter()
.zip(&varisat_answers)
.all(|(answer, &sat)| answer.is_some() == sat);
let witnesses_valid =
symbolic_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = symbolic_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count - sat_queries;
let symbolic_per_query = symbolic_ns as f64 / query_count as f64;
let varisat_per_query = varisat_ns as f64 / query_count as f64;
let speedup = varisat_per_query / symbolic_per_query.max(1.0);
writeln!(file, "{kind},{width},{horizon},{vars},{},{query_count},{max_width},true,{recognition_ns},{representation_entries},{symbolic_per_query:.3},{varisat_per_query:.3},{speedup:.6},{sat_queries},{unsat_queries},{agreement},{witnesses_valid},ok", formula.len())
.map_err(|error| format!("write symbolic temporal row: {error}"))?;
file.flush()
.map_err(|error| format!("flush symbolic temporal output: {error}"))?;
println!(
"symbolic temporal kind={kind} width={width} horizon={horizon} entries={representation_entries} speedup={speedup:.3} agreement={agreement} witnesses_valid={witnesses_valid}"
);
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn benchmark_symbolic_preimages(
kinds: &[&str],
widths: &[usize],
horizons: &[usize],
query_count: usize,
node_limit: usize,
seed: u64,
output: &Path,
order_kind: &str,
) -> Result<(), String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create preimage output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "kind,width,horizon,variables,clauses,queries,node_limit,order,backend,admitted,recognition_ns,bdd_nodes,compiled_frames,cycle_start,cycle_length,preimage_ns_per_query,incremental_varisat_ns_per_query,speedup_vs_incremental,sat_queries,unsat_queries,agreement,witnesses_valid,status")
.map_err(|error| format!("write preimage header: {error}"))?;
for &kind in kinds {
for &width in widths {
for &horizon in horizons {
let vars = width * (horizon + 1);
let (_, formula) = temporal_composition_formula(kind, width, horizon)?;
let recognition_start = Instant::now();
let engine_result = if order_kind == "hybrid" {
HybridTemporalPreimage::recognize(&formula, width, horizon, node_limit)
} else {
SymbolicPreimageTransition::recognize_ordered(
&formula, width, horizon, node_limit, order_kind,
)
.map(|preimage| (HybridTemporalPreimage::Bdd(Box::new(preimage)), "bdd", None))
};
let (mut preimage, backend, fallback_reason) = match engine_result {
Ok(engine) => engine,
Err(error) => {
let recognition_ns = recognition_start.elapsed().as_nanos();
writeln!(file, "{kind},{width},{horizon},{vars},{},{query_count},{node_limit},{order_kind},rejected,false,{recognition_ns},{node_limit},0,0,0,0,0,0,0,0,0,true,true,{}", formula.len(), error.replace(',', ";"))
.map_err(|write_error| format!("write preimage rejection: {write_error}"))?;
continue;
}
};
let recognition_ns = recognition_start.elapsed().as_nanos();
let (bdd_nodes, compiled_frames, cycle_start, cycle_length) =
preimage.bdd_metrics();
let mut rng =
Rng(seed ^ (width as u64).rotate_left(19) ^ (horizon as u64).rotate_left(41));
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
for _ in 0..(2 + query_index % 7) {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let preimage_start = Instant::now();
let preimage_answers: Vec<_> = queries
.iter()
.map(|assumptions| preimage.query(assumptions))
.collect();
let preimage_ns = preimage_start.elapsed().as_nanos();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &formula);
let varisat_start = Instant::now();
let varisat_answers: Vec<_> = queries
.iter()
.map(|assumptions| {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
solver.assume(&literals);
solver.solve().expect("preimage Varisat solve")
})
.collect();
let varisat_ns = varisat_start.elapsed().as_nanos();
let agreement = preimage_answers
.iter()
.zip(&varisat_answers)
.all(|(answer, &sat)| answer.is_some() == sat);
let witnesses_valid =
preimage_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = preimage_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count - sat_queries;
let preimage_per_query = preimage_ns as f64 / query_count as f64;
let varisat_per_query = varisat_ns as f64 / query_count as f64;
let speedup = varisat_per_query / preimage_per_query.max(1.0);
let status = fallback_reason.unwrap_or_else(|| "ok".to_string());
writeln!(file, "{kind},{width},{horizon},{vars},{},{query_count},{node_limit},{order_kind},{backend},true,{recognition_ns},{bdd_nodes},{compiled_frames},{cycle_start},{cycle_length},{preimage_per_query:.3},{varisat_per_query:.3},{speedup:.6},{sat_queries},{unsat_queries},{agreement},{witnesses_valid},{}", formula.len(), status.replace(',', ";"))
.map_err(|error| format!("write preimage row: {error}"))?;
file.flush()
.map_err(|error| format!("flush preimage output: {error}"))?;
println!(
"symbolic preimage kind={kind} width={width} horizon={horizon} backend={backend} nodes={bdd_nodes} frames={compiled_frames} cycle={cycle_start}/{cycle_length} speedup={speedup:.3} agreement={agreement} witnesses_valid={witnesses_valid}"
);
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn benchmark_checkpoint_cdcl(
kind: &str,
widths: &[usize],
horizons: &[usize],
query_count: usize,
checkpoint: usize,
node_limit: usize,
seed: u64,
output: &Path,
encoding: &str,
) -> Result<(), String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create checkpoint output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "kind,width,horizon,variables,clauses,queries,checkpoint,node_limit,encoding,recognition_ns,bdd_prefix_nodes,encoding_nodes,encoding_clauses,checkpoint_ns_per_query,full_cdcl_ns_per_query,speedup_vs_full,sat_queries,unsat_queries,agreement,witnesses_valid,status")
.map_err(|error| format!("write checkpoint header: {error}"))?;
for &width in widths {
for &horizon in horizons {
let vars = width * (horizon + 1);
let (_, formula) = temporal_composition_formula(kind, width, horizon)?;
let recognition_start = Instant::now();
let mut engine = CheckpointCdclPreimage::recognize_with_encoding(
&formula,
width,
horizon,
checkpoint.min(horizon.saturating_sub(1)),
node_limit,
encoding,
)?;
let recognition_ns = recognition_start.elapsed().as_nanos();
let mut rng =
Rng(seed ^ (width as u64).rotate_left(23) ^ (horizon as u64).rotate_left(43));
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
for _ in 0..(2 + query_index % 7) {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let checkpoint_start = Instant::now();
let checkpoint_answers: Vec<_> = queries
.iter()
.map(|assumptions| engine.query(assumptions))
.collect();
let checkpoint_ns = checkpoint_start.elapsed().as_nanos();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &formula);
let full_start = Instant::now();
let full_answers: Vec<_> = queries
.iter()
.map(|assumptions| {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
solver.assume(&literals);
solver.solve().expect("full checkpoint baseline solve")
})
.collect();
let full_ns = full_start.elapsed().as_nanos();
let agreement = checkpoint_answers
.iter()
.zip(&full_answers)
.all(|(answer, &sat)| answer.is_some() == sat);
let witnesses_valid =
checkpoint_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = checkpoint_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count - sat_queries;
let checkpoint_per_query = checkpoint_ns as f64 / query_count as f64;
let full_per_query = full_ns as f64 / query_count as f64;
let speedup = full_per_query / checkpoint_per_query.max(1.0);
writeln!(file, "{kind},{width},{horizon},{vars},{},{query_count},{},{node_limit},{encoding},{recognition_ns},{},{},{},{checkpoint_per_query:.3},{full_per_query:.3},{speedup:.6},{sat_queries},{unsat_queries},{agreement},{witnesses_valid},ok", formula.len(), engine.checkpoint, engine.bdd_nodes, engine.encoding_nodes, engine.encoding_clauses)
.map_err(|error| format!("write checkpoint row: {error}"))?;
file.flush()
.map_err(|error| format!("flush checkpoint output: {error}"))?;
println!(
"checkpoint CDCL kind={kind} width={width} horizon={horizon} checkpoint={} encoding={encoding} bdd_nodes={} encoding_nodes={} encoding_clauses={} speedup={speedup:.3} agreement={agreement} witnesses_valid={witnesses_valid}",
engine.checkpoint, engine.bdd_nodes, engine.encoding_nodes, engine.encoding_clauses
);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn benchmark_native_bdd_theory(
kind: &str,
widths: &[usize],
horizons: &[usize],
query_count: usize,
checkpoint: usize,
node_limit: usize,
seed: u64,
output: &Path,
) -> Result<(), String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create theory output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "kind,width,horizon,variables,clauses,queries,checkpoint,node_limit,recognition_ns,bdd_nodes,global_clauses,average_global_width,theory_clauses,theory_conflicts,average_learned_width,max_learned_width,theory_ns_per_query,full_cdcl_ns_per_query,speedup_vs_full,break_even_queries,sat_queries,unsat_queries,agreement,witnesses_valid,status")
.map_err(|error| format!("write theory header: {error}"))?;
for &width in widths {
for &horizon in horizons {
let vars = width * (horizon + 1);
let (_, formula) = temporal_composition_formula(kind, width, horizon)?;
let effective_checkpoint = checkpoint.min(horizon.saturating_sub(1));
let recognition_start = Instant::now();
let mut engine = NativeBddTheoryPreimage::recognize(
&formula,
width,
horizon,
effective_checkpoint,
node_limit,
)?;
let recognition_ns = recognition_start.elapsed().as_nanos();
let bdd_nodes = engine.preimage.bdd_nodes();
let mut rng =
Rng(seed ^ (width as u64).rotate_left(23) ^ (horizon as u64).rotate_left(43));
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
for _ in 0..(2 + query_index % 7) {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let theory_start = Instant::now();
let theory_answers = queries
.iter()
.map(|assumptions| engine.query(assumptions))
.collect::<Vec<_>>();
let theory_ns = theory_start.elapsed().as_nanos();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &formula);
let full_start = Instant::now();
let full_answers = queries
.iter()
.map(|assumptions| {
let literals = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect::<Vec<_>>();
solver.assume(&literals);
solver.solve().expect("native theory baseline solve")
})
.collect::<Vec<_>>();
let full_ns = full_start.elapsed().as_nanos();
let agreement = theory_answers
.iter()
.zip(&full_answers)
.all(|(answer, &sat)| answer.is_some() == sat);
let witnesses_valid =
theory_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = theory_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count - sat_queries;
let theory_per_query = theory_ns as f64 / query_count as f64;
let full_per_query = full_ns as f64 / query_count as f64;
let speedup = full_per_query / theory_per_query.max(1.0);
let break_even_queries = if full_per_query > theory_per_query {
recognition_ns as f64 / (full_per_query - theory_per_query)
} else {
f64::INFINITY
};
let average_learned_width =
engine.learned_literals as f64 / engine.theory_conflicts.max(1) as f64;
let average_global_width =
engine.global_literals as f64 / engine.global_clauses.max(1) as f64;
writeln!(file, "{kind},{width},{horizon},{vars},{},{query_count},{effective_checkpoint},{node_limit},{recognition_ns},{bdd_nodes},{},{average_global_width:.3},{},{},{average_learned_width:.3},{},{theory_per_query:.3},{full_per_query:.3},{speedup:.6},{break_even_queries:.3},{sat_queries},{unsat_queries},{agreement},{witnesses_valid},ok", formula.len(), engine.global_clauses, engine.theory_clauses, engine.theory_conflicts, engine.max_learned_width)
.map_err(|error| format!("write theory row: {error}"))?;
file.flush()
.map_err(|error| format!("flush theory output: {error}"))?;
println!(
"native BDD theory kind={kind} width={width} horizon={horizon} checkpoint={effective_checkpoint} bdd_nodes={bdd_nodes} global_clauses={} global_width={average_global_width:.2} conflicts={} learned_width={average_learned_width:.2}/{} speedup={speedup:.3} break_even={break_even_queries:.1} agreement={agreement} witnesses_valid={witnesses_valid}",
engine.global_clauses, engine.theory_conflicts, engine.max_learned_width
);
}
}
Ok(())
}
struct CqPortfolioRun {
backend: &'static str,
first_sat_query: Option<usize>,
first_witness: Option<Vec<bool>>,
}
#[allow(clippy::too_many_arguments)]
fn write_cq_portfolio_case(
file: &mut fs::File,
kind: &str,
width: usize,
horizon: usize,
formula: &[Clause],
initial: &[Option<bool>],
provided_queries: Option<Vec<Vec<Option<bool>>>>,
query_count: usize,
checkpoint: usize,
node_limit: usize,
seed: u64,
) -> Result<CqPortfolioRun, String> {
if initial.len() != width {
return Err("portfolio initial-state width mismatch".to_string());
}
let variables = width * (horizon + 1);
let effective_checkpoint = checkpoint.min(horizon.saturating_sub(1));
let queries = if let Some(queries) = provided_queries {
if queries.len() != query_count
|| queries
.iter()
.any(|assumptions| assumptions.len() != variables)
{
return Err("provided portfolio query dimensions do not match".to_string());
}
queries
} else {
let mut rng = Rng(seed ^ (width as u64).rotate_left(23) ^ (horizon as u64).rotate_left(43));
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; variables];
for _ in 0..(2 + query_index % 7) {
assumptions[rng.below(variables)] = Some(rng.next() & 1 == 1);
}
assumptions[..width].copy_from_slice(initial);
queries.push(assumptions);
}
queries
};
let gate_start = Instant::now();
let assumptions_per_query = queries
.iter()
.map(|assumptions| assumptions.iter().filter(|value| value.is_some()).count())
.sum::<usize>() as f64
/ query_count as f64;
let decision =
cq_portfolio_decision(formula, width, horizon, query_count, assumptions_per_query)?;
let gate_ns = gate_start.elapsed().as_nanos();
let mut recognition_ns = gate_ns;
let mut specialized_recognition_ns = 0u128;
let mut bdd_nodes = 0usize;
let mut global_clauses = 0usize;
let portfolio_start = Instant::now();
let portfolio_answers = if decision.specialized {
let recognition_start = Instant::now();
let mut engine = NativeBddTheoryPreimage::recognize(
formula,
width,
horizon,
effective_checkpoint,
node_limit,
)?;
specialized_recognition_ns = recognition_start.elapsed().as_nanos();
recognition_ns += specialized_recognition_ns;
bdd_nodes = engine.preimage.bdd_nodes();
global_clauses = engine.global_clauses;
queries
.iter()
.map(|assumptions| engine.query(assumptions))
.collect::<Vec<_>>()
} else {
let mut solver = Solver::new();
add_to_varisat(&mut solver, formula);
queries
.iter()
.map(|assumptions| {
let literals = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect::<Vec<_>>();
solver.assume(&literals);
if !solver.solve().expect("portfolio CDCL solve") {
return None;
}
let mut assignment = vec![false; variables];
for literal in solver.model().expect("portfolio CDCL model") {
if literal.var().index() < variables {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
})
.collect::<Vec<_>>()
};
let mut portfolio_ns = portfolio_start
.elapsed()
.as_nanos()
.saturating_sub(specialized_recognition_ns);
let mut baseline = Solver::new();
add_to_varisat(&mut baseline, formula);
let baseline_start = Instant::now();
let baseline_answers = queries
.iter()
.map(|assumptions| {
let literals = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect::<Vec<_>>();
baseline.assume(&literals);
baseline.solve().expect("portfolio baseline solve")
})
.collect::<Vec<_>>();
let baseline_ns = baseline_start.elapsed().as_nanos();
if !decision.specialized {
portfolio_ns = baseline_ns;
}
let agreement = portfolio_answers
.iter()
.zip(&baseline_answers)
.all(|(answer, &sat)| answer.is_some() == sat);
let witnesses_valid = portfolio_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = portfolio_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let first_sat_query = portfolio_answers.iter().position(Option::is_some);
let first_witness = first_sat_query
.and_then(|index| portfolio_answers[index].as_ref())
.cloned();
let unsat_queries = query_count - sat_queries;
let portfolio_per_query = portfolio_ns as f64 / query_count as f64;
let baseline_per_query = baseline_ns as f64 / query_count as f64;
let query_speedup = baseline_per_query / portfolio_per_query.max(1.0);
let amortized_per_query = portfolio_per_query + recognition_ns as f64 / query_count as f64;
let amortized_speedup = baseline_per_query / amortized_per_query.max(1.0);
let backend = if decision.specialized {
"cq-gcc"
} else {
"cdcl"
};
writeln!(file, "{kind},{width},{horizon},{variables},{},{query_count},{effective_checkpoint},{node_limit},{backend},{},{:.3},{},{:.3},{gate_ns},{recognition_ns},{bdd_nodes},{global_clauses},{portfolio_per_query:.3},{baseline_per_query:.3},{query_speedup:.6},{amortized_speedup:.6},{sat_queries},{unsat_queries},{agreement},{witnesses_valid},ok", formula.len(), decision.reason, decision.clauses_per_bit_step, decision.maximum_fanout, decision.assumptions_per_query)
.map_err(|error| format!("write portfolio row: {error}"))?;
file.flush()
.map_err(|error| format!("flush portfolio output: {error}"))?;
println!(
"CQ portfolio kind={kind} width={width} horizon={horizon} backend={backend} reason={} query_speedup={query_speedup:.3} amortized_speedup={amortized_speedup:.3} agreement={agreement} witnesses_valid={witnesses_valid}",
decision.reason
);
Ok(CqPortfolioRun {
backend,
first_sat_query,
first_witness,
})
}
fn create_cq_portfolio_output(output: &Path) -> Result<fs::File, String> {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create portfolio output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create {}: {error}", output.display()))?;
writeln!(file, "kind,width,horizon,variables,clauses,queries,checkpoint,node_limit,backend,gate_reason,clauses_per_bit_step,maximum_fanout,assumptions_per_query,gate_ns,recognition_ns,bdd_nodes,global_clauses,portfolio_ns_per_query,full_cdcl_ns_per_query,query_speedup,amortized_speedup,sat_queries,unsat_queries,agreement,witnesses_valid,status")
.map_err(|error| format!("write portfolio header: {error}"))?;
Ok(file)
}
#[allow(clippy::too_many_arguments)]
fn benchmark_cq_portfolio(
kind: &str,
widths: &[usize],
horizons: &[usize],
query_count: usize,
checkpoint: usize,
node_limit: usize,
seed: u64,
output: &Path,
) -> Result<(), String> {
let mut file = create_cq_portfolio_output(output)?;
for &width in widths {
for &horizon in horizons {
let (_, formula) = temporal_composition_formula(kind, width, horizon)?;
write_cq_portfolio_case(
&mut file,
kind,
width,
horizon,
&formula,
&vec![None; width],
None,
query_count,
checkpoint,
node_limit,
seed,
)?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn benchmark_cq_aiger(
input: &Path,
horizon: usize,
query_count: usize,
checkpoint: usize,
node_limit: usize,
seed: u64,
output: &Path,
) -> Result<(), String> {
let model = parse_aag(input)?;
let (_, formula, initial) = aag_temporal_formula(&model, horizon)?;
let property_queries = aag_property_queries(&model, horizon, query_count)?;
let label = input
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("external.aag");
let mut file = create_cq_portfolio_output(output)?;
write_cq_portfolio_case(
&mut file,
label,
model.latches.len(),
horizon,
&formula,
&initial,
Some(property_queries),
query_count,
checkpoint,
node_limit,
seed,
)?;
println!(
"AIGER model={} latches={} outputs={} ands={}",
input.display(),
model.latches.len(),
model.outputs.len(),
model.ands.len()
);
Ok(())
}
fn write_aiger_safety_result(
path: &Path,
input: &Path,
horizon: usize,
width: usize,
run: &CqPortfolioRun,
query_metadata: &[AagPropertyQuery],
) -> Result<(), String> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create AIGER result directory: {error}"))?;
}
let body = if let Some(index) = run.first_sat_query {
let (bad_frame, bad_pattern, _) = &query_metadata[index];
let witness = run
.first_witness
.as_ref()
.ok_or_else(|| "unsafe AIGER result is missing its witness".to_string())?;
let mut lines = vec![
"status=UNSAFE".to_string(),
format!("input={}", input.display()),
format!("horizon={horizon}"),
format!("backend={}", run.backend),
format!("bad_frame={bad_frame}"),
format!("bad_pattern={bad_pattern}"),
"frame,state_bits_low_to_high".to_string(),
];
for frame in 0..=horizon {
let state = (0..width)
.map(|bit| {
if witness[frame * width + bit] {
'1'
} else {
'0'
}
})
.collect::<String>();
lines.push(format!("{frame},{state}"));
}
lines.join("\n") + "\n"
} else {
format!(
"status=SAFE\ninput={}\nhorizon={horizon}\nbackend={}\n",
input.display(),
run.backend
)
};
fs::write(path, body).map_err(|error| format!("write {}: {error}", path.display()))
}
struct AagBmcRun {
first_sat_query: Option<usize>,
first_witness: Option<Vec<bool>>,
sat_queries: usize,
witnesses_valid: bool,
query_ns: u128,
}
fn validate_aag_trace(model: &AagModel, horizon: usize, assignment: &[bool]) -> bool {
if assignment.len() < (horizon + 1) * model.max_variable {
return false;
}
for latch in &model.latches {
if latch
.initial
.is_some_and(|initial| assignment[latch.current / 2 - 1] != initial)
{
return false;
}
}
for frame in 0..=horizon {
let offset = frame * model.max_variable;
let mut values = vec![false; model.max_variable + 1];
for &literal in &model.inputs {
values[literal / 2] = assignment[offset + literal / 2 - 1];
}
for latch in &model.latches {
values[latch.current / 2] = assignment[offset + latch.current / 2 - 1];
}
for gate in &model.ands {
let value = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
if assignment[offset + gate.output / 2 - 1] != value {
return false;
}
values[gate.output / 2] = value;
}
if frame < horizon {
let next_offset = offset + model.max_variable;
for latch in &model.latches {
if assignment[next_offset + latch.current / 2 - 1]
!= evaluate_aag_literal(latch.next, &values)
{
return false;
}
}
}
}
true
}
fn run_aag_bmc(
model: &AagModel,
horizon: usize,
encoding: &AagBmcEncoding,
) -> Result<AagBmcRun, String> {
if encoding.queries.is_empty() {
return Ok(AagBmcRun {
first_sat_query: None,
first_witness: None,
sat_queries: 0,
witnesses_valid: true,
query_ns: 0,
});
}
let mut solver = Solver::new();
add_to_varisat(&mut solver, &encoding.clauses);
let aggregate_is_true = encoding
.queries
.iter()
.any(|query| matches!(query.assumption, AagCnfLiteral::Constant(true)));
if !aggregate_is_true {
let clause = encoding
.queries
.iter()
.filter_map(|query| match query.assumption {
AagCnfLiteral::Variable((variable, positive)) => {
Some(Lit::from_var(Var::from_index(variable), positive))
}
AagCnfLiteral::Constant(_) => None,
})
.collect::<Vec<_>>();
if clause.is_empty() {
return Ok(AagBmcRun {
first_sat_query: None,
first_witness: None,
sat_queries: 0,
witnesses_valid: true,
query_ns: 0,
});
}
solver.add_clause(&clause);
}
let query_start = Instant::now();
let sat = solver
.solve()
.map_err(|error| format!("solve aggregate AIGER BMC query: {error}"))?;
let query_ns = query_start.elapsed().as_nanos();
if !sat {
return Ok(AagBmcRun {
first_sat_query: None,
first_witness: None,
sat_queries: 0,
witnesses_valid: true,
query_ns,
});
}
let mut assignment = vec![false; encoding.variables];
for literal in solver
.model()
.ok_or_else(|| "AIGER BMC SAT result has no model".to_string())?
{
if literal.var().index() < encoding.variables {
assignment[literal.var().index()] = literal.is_positive();
}
}
let first_sat_query = encoding
.queries
.iter()
.position(|query| match query.assumption {
AagCnfLiteral::Constant(value) => value,
AagCnfLiteral::Variable((variable, positive)) => assignment[variable] == positive,
})
.ok_or_else(|| "aggregate AIGER model does not satisfy a bad output".to_string())?;
let witnesses_valid = satisfies(&encoding.clauses, &assignment)
&& validate_aag_trace(model, horizon, &assignment);
Ok(AagBmcRun {
first_sat_query: Some(first_sat_query),
first_witness: Some(assignment),
sat_queries: 1,
witnesses_valid,
query_ns,
})
}
fn solve_aag_query(solver: &mut Solver, query: &AagBmcQuery) -> Result<bool, String> {
match query.assumption {
AagCnfLiteral::Constant(value) => Ok(value),
AagCnfLiteral::Variable((variable, positive)) => {
solver.assume(&[Lit::from_var(Var::from_index(variable), positive)]);
solver
.solve()
.map_err(|error| format!("solve AIGER property query: {error}"))
}
}
}
fn aag_property_batch(
model: &AagModel,
horizon: usize,
encoding: &AagBmcEncoding,
) -> (Vec<Clause>, Vec<AagBmcQuery>) {
let mut selector_clauses = Vec::new();
let mut property_queries = Vec::with_capacity(model.outputs.len());
for output in 0..model.outputs.len() {
let candidates = encoding
.queries
.iter()
.filter(|query| query.output == output)
.map(|query| query.assumption)
.collect::<Vec<_>>();
let assumption = if candidates
.iter()
.any(|candidate| matches!(candidate, AagCnfLiteral::Constant(true)))
{
AagCnfLiteral::Constant(true)
} else {
let selector = encoding.variables + output;
let mut clause = vec![(selector, false)];
clause.extend(candidates.iter().filter_map(|candidate| match candidate {
AagCnfLiteral::Variable(literal) => Some(*literal),
AagCnfLiteral::Constant(_) => None,
}));
if clause.len() == 1 {
AagCnfLiteral::Constant(false)
} else {
selector_clauses.push(Clause(clause));
AagCnfLiteral::Variable((selector, true))
}
};
property_queries.push(AagBmcQuery {
frame: horizon,
output,
assumption,
});
}
(selector_clauses, property_queries)
}
fn aiger_reuse_gate(clause_count: usize, property_count: usize) -> bool {
property_count >= 2 && clause_count <= 15_000
}
fn benchmark_aiger_query_reuse(
input: &Path,
horizons: &[usize],
repeats: usize,
output: &Path,
) -> Result<(), String> {
if horizons.is_empty() {
return Err("AIGER reuse benchmark requires at least one horizon".to_string());
}
if repeats == 0 || repeats > 10_000 {
return Err("AIGER reuse benchmark repeats must be between 1 and 10000".to_string());
}
let model = parse_aag(input)?;
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create AIGER reuse benchmark output: {error}"))?;
}
let mut file = fs::File::create(output)
.map_err(|error| format!("create AIGER reuse benchmark output: {error}"))?;
writeln!(file, "input,horizon,latches,inputs,outputs,ands,variables,clauses,distinct_queries,reuse_batch,repeats,total_queries,sat_queries,encoding_ns,reusable_build_ns,reusable_query_ns,cold_total_ns,reusable_amortized_ns,cold_amortized_ns,query_speedup,full_speedup,selected_backend,selected_ns,selected_speedup,agreement,status")
.map_err(|error| format!("write AIGER reuse benchmark header: {error}"))?;
for &horizon in horizons {
if horizon == 0 {
return Err("AIGER reuse benchmark horizons must be at least one".to_string());
}
let encoding_start = Instant::now();
let encoding = aag_bmc_encoding(&model, horizon)?;
let encoding_ns = encoding_start.elapsed().as_nanos();
if encoding.queries.is_empty() {
return Err(format!(
"AIGER model has no property queries at horizon {horizon}"
));
}
let (selector_clauses, property_queries) = aag_property_batch(&model, horizon, &encoding);
let total_queries = property_queries
.len()
.checked_mul(repeats)
.ok_or_else(|| "AIGER reuse query count overflow".to_string())?;
let mut reusable_build_ns = 0u128;
let mut reusable_query_ns = 0u128;
let mut reusable_answers = Vec::with_capacity(total_queries);
for _ in 0..repeats {
for batch in property_queries.chunks(2) {
let build_start = Instant::now();
let mut reusable = Solver::new();
add_to_varisat(&mut reusable, &encoding.clauses);
add_to_varisat(&mut reusable, &selector_clauses);
reusable_build_ns += build_start.elapsed().as_nanos();
let query_start = Instant::now();
for query in batch {
reusable_answers.push(solve_aag_query(&mut reusable, query)?);
}
reusable_query_ns += query_start.elapsed().as_nanos();
}
}
let cold_start = Instant::now();
let mut cold_answers = Vec::with_capacity(total_queries);
for _ in 0..repeats {
for query in &property_queries {
let mut cold = Solver::new();
add_to_varisat(&mut cold, &encoding.clauses);
add_to_varisat(&mut cold, &selector_clauses);
cold_answers.push(solve_aag_query(&mut cold, query)?);
}
}
let cold_total_ns = cold_start.elapsed().as_nanos();
let agreement = reusable_answers == cold_answers;
if !agreement {
return Err(format!(
"reusable and cold BMC disagree at horizon {horizon}"
));
}
let sat_queries = reusable_answers.iter().filter(|&&answer| answer).count();
let reusable_amortized_ns = encoding_ns + reusable_build_ns + reusable_query_ns;
let cold_amortized_ns = encoding_ns + cold_total_ns;
let query_speedup = cold_total_ns as f64 / reusable_query_ns.max(1) as f64;
let full_speedup = cold_amortized_ns as f64 / reusable_amortized_ns.max(1) as f64;
let selected_reuse = aiger_reuse_gate(encoding.clauses.len(), property_queries.len());
let (selected_backend, selected_ns) = if selected_reuse {
("bounded-reuse", reusable_amortized_ns)
} else {
("cold-bmc", cold_amortized_ns)
};
let selected_speedup = cold_amortized_ns as f64 / selected_ns.max(1) as f64;
writeln!(file, "{},{horizon},{},{},{},{},{},{},{},2,{repeats},{total_queries},{sat_queries},{encoding_ns},{reusable_build_ns},{reusable_query_ns},{cold_total_ns},{reusable_amortized_ns},{cold_amortized_ns},{query_speedup:.6},{full_speedup:.6},{selected_backend},{selected_ns},{selected_speedup:.6},{agreement},ok", report_csv_field(&input.to_string_lossy()), model.latches.len(), model.inputs.len(), model.outputs.len(), model.ands.len(), encoding.variables + property_queries.len(), encoding.clauses.len() + selector_clauses.len(), property_queries.len())
.map_err(|error| format!("write AIGER reuse benchmark row: {error}"))?;
println!(
"AIGER reuse horizon={horizon} queries={total_queries} query_speedup={query_speedup:.3} full_speedup={full_speedup:.3} agreement={agreement}"
);
}
file.flush()
.map_err(|error| format!("flush AIGER reuse benchmark output: {error}"))
}
fn earliest_aag_counterexample(
model: &AagModel,
unsafe_horizon: usize,
constraints: &[AagInputConstraint],
) -> Result<(AagBmcEncoding, AagBmcRun), String> {
let mut low = 0usize;
let mut high = unsafe_horizon;
while low < high {
let middle = low + (high - low) / 2;
let encoding = aag_bmc_encoding_with_constraints(model, middle, constraints)?;
if run_aag_bmc(model, middle, &encoding)?
.first_sat_query
.is_some()
{
high = middle;
} else {
low = middle + 1;
}
}
let encoding = aag_bmc_encoding_with_constraints(model, low, constraints)?;
let run = run_aag_bmc(model, low, &encoding)?;
if run.first_sat_query.is_none() {
return Err("failed to reproduce AIGER counterexample at minimal horizon".to_string());
}
Ok((encoding, run))
}
const CAUSAL_CERTIFICATE_VERSION: usize = 1;
const CAUSAL_MAX_EVENTS: usize = 512;
const CAUSAL_MAX_QUERY_WORK: usize = 250_000_000;
const CAUSAL_CQ_MAX_VARIABLES: usize = 256;
const CAUSAL_CQ_MAX_CLAUSES: usize = 4_096;
const CAUSAL_METRICS_HEADER: &str = "input_sha256,requested_horizon,bad_frame,bad_output,candidates,causes,queries,cq_admitted,cq_bound_bits,cq_peak_classes,cq_compile_ns,cq_query_ns,persistent_cdcl_ns,fresh_cdcl_ns,cq_query_speedup,cq_amortized_speedup,agreement,certificate_valid,status";
const CAUSAL_STRATEGY_HEADER: &str = "causal_strategy_schema_version,input_sha256,requested_horizon,bad_frame,bad_output,strategy,candidates,causes,cause_indices,cause_sha256,search_queries,validation_queries,total_queries,unique_queries,cq_admitted,cq_bound_bits,cq_peak_classes,cq_prepare_ns,cq_query_ns,persistent_setup_ns,persistent_query_ns,fresh_total_ns,persistent_total_speedup_vs_fresh,cq_query_speedup_vs_persistent,cq_total_speedup_vs_persistent,agreement,minimality_valid,status";
const CAUSAL_BATCH_SCHEMA_VERSION: usize = 1;
const CAUSAL_BATCH_MAX_TARGETS: usize = 256;
const CAUSAL_BATCH_MAX_CAUSES: usize = 16;
const CAUSAL_BATCH_MAX_REPEATS: usize = 10_000;
const CAUSAL_BATCH_MAX_MAP_QUERIES: usize = 4_096;
const CAUSAL_BATCH_HEADER: &str = "causal_batch_schema_version,input_sha256,horizon,bad_frame,bad_output,vocabulary,candidates,causes,cause_indices,enumeration_complete,oracle_queries,unique_queries,cq_admitted,cq_bound_bits,cq_peak_classes,cq_prepare_ns,cq_query_ns,persistent_setup_ns,persistent_query_ns,repeats,workload_measured_break_even_query,workload_projected_break_even_query,agreement,minimality_valid,status";
const INTERFACE_QUOTIENT_SCHEMA_VERSION: usize = 1;
const INTERFACE_QUOTIENT_HEADER: &str = "interface_quotient_schema_version,input_sha256,horizon,bad_frame,bad_output,candidates,causes,oracle_queries,repeats,interface_compile_ns,tree_prepare_ns,interface_query_ns,interface_witness_ns,persistent_setup_ns,persistent_query_ns,persistent_witness_ns,repaired_leaves,repaired_internal_nodes,root_pairs,summary_cache_hits,summary_cache_misses,interface_query_speedup,workload_total_speedup,agreement,witness_valid,status";
#[derive(Clone, Debug, Eq, PartialEq)]
struct CausalEvent {
input: usize,
start_frame: usize,
end_frame: usize,
value: bool,
}
#[derive(Clone, Debug)]
struct CausalCertificate {
input_sha256: String,
requested_horizon: usize,
bad_frame: usize,
bad_output: usize,
bad_output_name: String,
candidate_count: usize,
events: Vec<CausalEvent>,
}
struct CausalMetrics {
queries: usize,
fresh_cdcl_ns: u128,
persistent_cdcl_ns: u128,
cq_ns: u128,
cq_compile_ns: u128,
cq_bound_bits: Option<usize>,
cq_peak_classes: Option<usize>,
cq_admitted: bool,
}
struct CausalProblem {
encoding: AagBmcEncoding,
run: AagBmcRun,
clauses: Vec<Clause>,
fixed: Vec<(usize, bool)>,
}
#[derive(Clone)]
struct CausalQueryRecord {
active: Vec<bool>,
unsat: bool,
}
struct CausalStrategyResult {
name: &'static str,
active: Vec<bool>,
search_queries: usize,
validation_queries: usize,
transcript: Vec<CausalQueryRecord>,
fresh_total_ns: u128,
}
struct CausalEnumeration {
causes: Vec<Vec<bool>>,
transcript: Vec<CausalQueryRecord>,
complete: bool,
}
fn causal_interface_constraints(
table: &AagInterfaceTable,
horizon: usize,
events: &[CausalEvent],
active: &[bool],
) -> Result<Vec<Vec<Option<bool>>>, String> {
if events.len() != active.len() {
return Err("interface quotient event selection length mismatch".to_string());
}
let mut constraints = vec![vec![None; table.input_count]; horizon + 1];
for (event, selected) in events.iter().zip(active) {
if !selected {
continue;
}
let Some(projected_input) = table
.projected_inputs
.iter()
.position(|input| *input == event.input)
else {
return Err("interface quotient event is outside projected support".to_string());
};
if event.end_frame > horizon {
return Err("interface quotient event is out of range".to_string());
}
for frame in event.start_frame..=event.end_frame {
let slot = &mut constraints[frame][projected_input];
if slot.is_some_and(|value| value != event.value) {
return Err("interface quotient events conflict".to_string());
}
*slot = Some(event.value);
}
}
Ok(constraints)
}
fn validate_interface_witness(
model: &AagModel,
table: &AagInterfaceTable,
output: usize,
constraints: &[Vec<Option<bool>>],
witness: &InterfaceQueryResult,
) -> bool {
if witness.states.len() != constraints.len()
|| witness.inputs.len() != constraints.len()
|| witness.declared_inputs.len() != constraints.len()
{
return false;
}
for frame in 0..constraints.len() {
let declared_input = witness.declared_inputs[frame];
if table.project_input(declared_input) != witness.inputs[frame]
|| (table.declared_input_count < u64::BITS as usize
&& declared_input >> table.declared_input_count != 0)
{
return false;
}
if !table.input_allowed(witness.inputs[frame], &constraints[frame]) {
return false;
}
let mut values = vec![false; model.max_variable + 1];
for (bit, latch) in model.latches.iter().enumerate() {
values[latch.current / 2] = witness.states[frame] >> bit & 1 == 1;
}
for (input, literal) in model.inputs.iter().enumerate() {
values[literal / 2] = declared_input >> input & 1 == 1;
}
for gate in &model.ands {
values[gate.output / 2] = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
}
if frame + 1 < constraints.len() {
let successor = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(evaluate_aag_literal(latch.next, &values)) << bit)
});
if successor != witness.states[frame + 1]
|| table.next[witness.states[frame]][witness.inputs[frame]] != successor
{
return false;
}
} else if evaluate_aag_literal(model.outputs[output], &values) {
return false;
}
}
table.bad_masks[*witness.states.last().unwrap()][*witness.inputs.last().unwrap()]
& (1u128 << output)
== 0
}
fn validate_predicate_witness(
model: &AagModel,
projected_inputs: &[usize],
output: usize,
constraints: &[Vec<Option<bool>>],
witness: &InterfaceQueryResult,
) -> bool {
if witness.states.len() != constraints.len()
|| witness.declared_inputs.len() != constraints.len()
|| model.inputs.len() > u64::BITS as usize
{
return false;
}
for frame in 0..constraints.len() {
let declared = witness.declared_inputs[frame];
if constraints[frame]
.iter()
.enumerate()
.any(|(bit, required)| {
required
.is_some_and(|value| declared >> projected_inputs[bit] & 1 != u64::from(value))
})
{
return false;
}
let mut values = vec![false; model.max_variable + 1];
for (bit, latch) in model.latches.iter().enumerate() {
values[latch.current / 2] = witness.states[frame] >> bit & 1 == 1;
}
for (input, literal) in model.inputs.iter().enumerate() {
values[literal / 2] = declared >> input & 1 == 1;
}
for gate in &model.ands {
values[gate.output / 2] = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
}
if frame + 1 < constraints.len() {
let successor = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(evaluate_aag_literal(latch.next, &values)) << bit)
});
if successor != witness.states[frame + 1] {
return false;
}
} else if evaluate_aag_literal(model.outputs[output], &values) {
return false;
}
}
true
}
fn causal_events_from_witness(
model: &AagModel,
witness: &[bool],
bad_frame: usize,
) -> Vec<CausalEvent> {
causal_events_from_witness_for_inputs(model, witness, bad_frame, 0..model.inputs.len())
}
fn causal_events_from_witness_for_inputs(
model: &AagModel,
witness: &[bool],
bad_frame: usize,
inputs: impl IntoIterator<Item = usize>,
) -> Vec<CausalEvent> {
let mut events = Vec::new();
for input in inputs {
let literal = model.inputs[input];
let mut start = 0usize;
let mut value = witness[literal / 2 - 1];
for frame in 1..=bad_frame {
let next = witness[frame * model.max_variable + literal / 2 - 1];
if next != value {
events.push(CausalEvent {
input,
start_frame: start,
end_frame: frame - 1,
value,
});
start = frame;
value = next;
}
}
events.push(CausalEvent {
input,
start_frame: start,
end_frame: bad_frame,
value,
});
}
events
}
fn causal_assumptions(
model: &AagModel,
variables: usize,
fixed: &[(usize, bool)],
events: &[CausalEvent],
active: &[bool],
) -> Result<Vec<Option<bool>>, String> {
if events.len() != active.len() {
return Err("causal event selection length mismatch".to_string());
}
let mut assumptions = vec![None; variables];
for &(variable, value) in fixed {
assumptions[variable] = Some(value);
}
for (event, &retained) in events.iter().zip(active) {
if !retained {
continue;
}
let literal = *model
.inputs
.get(event.input)
.ok_or_else(|| "causal event input index is out of range".to_string())?;
for frame in event.start_frame..=event.end_frame {
let variable = frame
.checked_mul(model.max_variable)
.and_then(|offset| offset.checked_add(literal / 2 - 1))
.ok_or_else(|| "causal event variable overflow".to_string())?;
if variable >= variables {
return Err("causal event exceeds encoded horizon".to_string());
}
if assumptions[variable].is_some_and(|existing| existing != event.value) {
return Err("causal event assumptions conflict".to_string());
}
assumptions[variable] = Some(event.value);
}
}
Ok(assumptions)
}
fn solve_causal_fresh(clauses: &[Clause], assumptions: &[Option<bool>]) -> Result<bool, String> {
let mut solver = Solver::new();
add_to_varisat(&mut solver, clauses);
let literals = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect::<Vec<_>>();
solver.assume(&literals);
solver
.solve()
.map_err(|error| format!("solve fresh causal intervention: {error}"))
}
fn solve_causal_persistent(
solver: &mut Solver<'_>,
assumptions: &[Option<bool>],
) -> Result<bool, String> {
let literals = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect::<Vec<_>>();
solver.assume(&literals);
solver
.solve()
.map_err(|error| format!("solve persistent causal intervention: {error}"))
}
fn solve_causal_persistent_assignment(
solver: &mut Solver<'_>,
variables: usize,
assumptions: &[Option<bool>],
) -> Result<Option<Vec<bool>>, String> {
let literals = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect::<Vec<_>>();
solver.assume(&literals);
if !solver
.solve()
.map_err(|error| format!("solve persistent causal intervention: {error}"))?
{
return Ok(None);
}
let mut assignment = vec![false; variables];
for literal in solver
.model()
.ok_or_else(|| "persistent causal SAT result has no model".to_string())?
{
if literal.var().index() < variables {
assignment[literal.var().index()] = literal.is_positive();
}
}
Ok(Some(assignment))
}
fn minimize_causal_events<F>(count: usize, mut is_unsat: F) -> Result<Vec<bool>, String>
where
F: FnMut(&[bool]) -> Result<bool, String>,
{
let mut active = vec![true; count];
if !is_unsat(&active)? {
return Err(
"complete counterexample observations do not force the target failure".to_string(),
);
}
// UNSAT is monotonic under adding observations: once removing an event makes
// the formula SAT, removing still more events cannot make it UNSAT again.
// One deterministic deletion pass therefore reaches a 1-minimal set.
for index in 0..count {
active[index] = false;
if !is_unsat(&active)? {
active[index] = true;
}
}
for index in 0..count {
if active[index] {
active[index] = false;
let still_unsat = is_unsat(&active)?;
active[index] = true;
if still_unsat {
return Err("causal minimizer failed its 1-minimality check".to_string());
}
}
}
Ok(active)
}
fn quickxplain_causal_recursive<F>(
background: &[bool],
candidates: &[usize],
is_unsat: &mut F,
) -> Result<Vec<usize>, String>
where
F: FnMut(&[bool]) -> Result<bool, String>,
{
if candidates.is_empty() || is_unsat(background)? {
return Ok(Vec::new());
}
if candidates.len() == 1 {
return Ok(vec![candidates[0]]);
}
let middle = candidates.len() / 2;
let (left, right) = candidates.split_at(middle);
let mut background_with_left = background.to_vec();
for &index in left {
background_with_left[index] = true;
}
let right_cause = quickxplain_causal_recursive(&background_with_left, right, is_unsat)?;
let mut background_with_right_cause = background.to_vec();
for &index in &right_cause {
background_with_right_cause[index] = true;
}
let mut left_cause =
quickxplain_causal_recursive(&background_with_right_cause, left, is_unsat)?;
left_cause.extend(right_cause);
Ok(left_cause)
}
fn quickxplain_causal_events<F>(count: usize, mut is_unsat: F) -> Result<Vec<bool>, String>
where
F: FnMut(&[bool]) -> Result<bool, String>,
{
let full = vec![true; count];
if !is_unsat(&full)? {
return Err(
"complete counterexample observations do not force the target failure".to_string(),
);
}
let candidates = (0..count).collect::<Vec<_>>();
let selected = quickxplain_causal_recursive(&vec![false; count], &candidates, &mut is_unsat)?;
let mut active = vec![false; count];
for index in selected {
active[index] = true;
}
Ok(active)
}
fn validate_causal_selection<F>(active: &[bool], mut is_unsat: F) -> Result<(), String>
where
F: FnMut(&[bool]) -> Result<bool, String>,
{
if !is_unsat(active)? {
return Err("causal selection does not force the target failure".to_string());
}
for index in 0..active.len() {
if !active[index] {
continue;
}
let mut reduced = active.to_vec();
reduced[index] = false;
if is_unsat(&reduced)? {
return Err(format!(
"causal selection is not 1-minimal at event {index}"
));
}
}
Ok(())
}
fn ensure_causal_query_work(
variables: usize,
clause_count: usize,
query_count: usize,
) -> Result<(), String> {
let per_query = clause_count
.checked_add(variables)
.ok_or_else(|| "causal query work overflow".to_string())?;
let work = per_query
.checked_mul(query_count)
.ok_or_else(|| "causal query work overflow".to_string())?;
if work > CAUSAL_MAX_QUERY_WORK {
return Err(format!(
"causal analysis conservative work bound {work} exceeds {CAUSAL_MAX_QUERY_WORK}"
));
}
Ok(())
}
fn causal_base_formula(
model: &AagModel,
requested_horizon: usize,
) -> Result<CausalProblem, String> {
let (encoding, run) = earliest_aag_counterexample(model, requested_horizon, &[])?;
let query_index = run
.first_sat_query
.ok_or_else(|| "AIGER model has no counterexample in the requested horizon".to_string())?;
let query = &encoding.queries[query_index];
let mut clauses = encoding.clauses.clone();
push_simplified_clause(&mut clauses, &[query.assumption.negate()]);
let witness = run
.first_witness
.as_ref()
.ok_or_else(|| "counterexample is missing its witness".to_string())?;
let fixed = model
.latches
.iter()
.filter(|latch| latch.initial.is_none())
.map(|latch| {
let variable = latch.current / 2 - 1;
(variable, witness[variable])
})
.collect();
Ok(CausalProblem {
encoding,
run,
clauses,
fixed,
})
}
fn causal_certificate_body(
input: &Path,
model: &AagModel,
certificate: &CausalCertificate,
) -> Result<String, String> {
let bad_name = model
.output_names
.get(certificate.bad_output)
.ok_or_else(|| "causal certificate bad output is out of range".to_string())?;
let mut lines = vec![
format!("causal_certificate_version={CAUSAL_CERTIFICATE_VERSION}"),
format!("input={}", input.display()),
format!("input_sha256={}", certificate.input_sha256),
format!("requested_horizon={}", certificate.requested_horizon),
format!("bad_frame={}", certificate.bad_frame),
format!("bad_output={}", certificate.bad_output),
format!("bad_output_name={bad_name}"),
"semantics=minimal-sufficient-input-segments".to_string(),
"minimality=1-minimal".to_string(),
format!("candidate_count={}", certificate.candidate_count),
format!("cause_count={}", certificate.events.len()),
];
for (index, event) in certificate.events.iter().enumerate() {
lines.push(format!(
"cause_{index}={},{},{},{}",
event.input,
event.start_frame,
event.end_frame,
usize::from(event.value)
));
}
Ok(lines.join("\n") + "\n")
}
fn parse_causal_certificate(path: &Path) -> Result<CausalCertificate, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect causal certificate {}: {error}", path.display()))?;
if !metadata.file_type().is_file() || metadata.len() > 1_048_576 {
return Err(
"causal certificate must be a regular file no larger than 1048576 bytes".to_string(),
);
}
let body = fs::read_to_string(path)
.map_err(|error| format!("read causal certificate {}: {error}", path.display()))?;
let mut fields = BTreeMap::new();
for (line_number, line) in body.lines().enumerate() {
let (key, value) = line
.split_once('=')
.ok_or_else(|| format!("invalid causal certificate line {}", line_number + 1))?;
if key.is_empty()
|| value.is_empty()
|| fields.insert(key.to_string(), value.to_string()).is_some()
{
return Err(format!(
"invalid or duplicate causal certificate field at line {}",
line_number + 1
));
}
}
let value = |key: &str| {
fields
.get(key)
.map(String::as_str)
.ok_or_else(|| format!("missing causal certificate field `{key}`"))
};
if value("causal_certificate_version")? != CAUSAL_CERTIFICATE_VERSION.to_string()
|| value("semantics")? != "minimal-sufficient-input-segments"
|| value("minimality")? != "1-minimal"
{
return Err("unsupported causal certificate contract".to_string());
}
let parse_usize = |key: &str| {
value(key)?
.parse::<usize>()
.map_err(|_| format!("invalid causal certificate numeric field `{key}`"))
};
let candidate_count = parse_usize("candidate_count")?;
let cause_count = parse_usize("cause_count")?;
if candidate_count > CAUSAL_MAX_EVENTS || cause_count > candidate_count {
return Err("causal certificate event counts exceed bounds".to_string());
}
let mut events = Vec::with_capacity(cause_count);
for index in 0..cause_count {
let raw = value(&format!("cause_{index}"))?;
let parts = raw.split(',').collect::<Vec<_>>();
if parts.len() != 4 {
return Err(format!("invalid causal certificate cause_{index}"));
}
let input = parts[0]
.parse::<usize>()
.map_err(|_| format!("invalid causal certificate cause_{index} input"))?;
let start_frame = parts[1]
.parse::<usize>()
.map_err(|_| format!("invalid causal certificate cause_{index} start frame"))?;
let end_frame = parts[2]
.parse::<usize>()
.map_err(|_| format!("invalid causal certificate cause_{index} end frame"))?;
let value = match parts[3] {
"0" => false,
"1" => true,
_ => return Err(format!("invalid causal certificate cause_{index} value")),
};
if start_frame > end_frame {
return Err(format!(
"invalid causal certificate cause_{index} frame range"
));
}
events.push(CausalEvent {
input,
start_frame,
end_frame,
value,
});
}
let allowed = 11usize
.checked_add(cause_count)
.ok_or_else(|| "causal certificate field count overflow".to_string())?;
if fields.len() != allowed {
return Err("causal certificate contains unexpected fields".to_string());
}
let digest = value("input_sha256")?.to_string();
if digest.len() != 64
|| !digest
.chars()
.all(|character| character.is_ascii_hexdigit())
{
return Err("causal certificate input SHA-256 is malformed".to_string());
}
Ok(CausalCertificate {
input_sha256: digest,
requested_horizon: parse_usize("requested_horizon")?,
bad_frame: parse_usize("bad_frame")?,
bad_output: parse_usize("bad_output")?,
bad_output_name: value("bad_output_name")?.to_string(),
candidate_count,
events,
})
}
fn verify_causal_certificate(input: &Path, certificate_path: &Path) -> Result<(), String> {
let model = parse_aag(input)?;
let certificate = parse_causal_certificate(certificate_path)?;
if sha256_file(input)? != certificate.input_sha256 {
return Err("causal certificate input SHA-256 mismatch".to_string());
}
if certificate.requested_horizon == 0 || certificate.bad_frame > certificate.requested_horizon {
return Err("causal certificate horizon is invalid".to_string());
}
let problem = causal_base_formula(&model, certificate.requested_horizon)?;
let CausalProblem {
encoding,
run,
clauses,
fixed,
} = problem;
let query_index = run.first_sat_query.unwrap();
let query = &encoding.queries[query_index];
if query.frame != certificate.bad_frame || query.output != certificate.bad_output {
return Err(
"causal certificate target disagrees with the earliest counterexample".to_string(),
);
}
if model.output_names[query.output] != certificate.bad_output_name {
return Err("causal certificate output name disagrees with the source model".to_string());
}
let witness = run
.first_witness
.as_ref()
.ok_or_else(|| "counterexample is missing its witness".to_string())?;
let candidates = causal_events_from_witness(&model, witness, query.frame);
if candidates.len() != certificate.candidate_count {
return Err(
"causal certificate candidate count disagrees with the counterexample".to_string(),
);
}
let verification_queries = certificate
.events
.len()
.checked_add(1)
.ok_or_else(|| "causal verification query count overflow".to_string())?;
ensure_causal_query_work(encoding.variables, clauses.len(), verification_queries)?;
let mut unique = BTreeSet::new();
for event in &certificate.events {
if !candidates.contains(event) {
return Err(
"causal certificate contains an event outside the counterexample segmentation"
.to_string(),
);
}
if !unique.insert((event.input, event.start_frame, event.end_frame, event.value)) {
return Err("causal certificate contains a duplicate cause event".to_string());
}
}
let active = vec![true; certificate.events.len()];
let assumptions = causal_assumptions(
&model,
encoding.variables,
&fixed,
&certificate.events,
&active,
)?;
if solve_causal_fresh(&clauses, &assumptions)? {
return Err("causal certificate does not force the target failure".to_string());
}
for index in 0..certificate.events.len() {
let mut reduced = active.clone();
reduced[index] = false;
let assumptions = causal_assumptions(
&model,
encoding.variables,
&fixed,
&certificate.events,
&reduced,
)?;
if !solve_causal_fresh(&clauses, &assumptions)? {
return Err(format!(
"causal certificate is not 1-minimal at cause_{index}"
));
}
}
println!(
"causal-certificate-verify status=VALID causes={} target={}@{} certificate={}",
certificate.events.len(),
model.output_names[certificate.bad_output],
certificate.bad_frame,
certificate_path.display()
);
Ok(())
}
fn parse_small_key_value_file(
path: &Path,
label: &str,
) -> Result<BTreeMap<String, String>, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect {label} {}: {error}", path.display()))?;
if !metadata.file_type().is_file() || metadata.len() > 1_048_576 {
return Err(format!(
"{label} must be a regular file no larger than 1048576 bytes"
));
}
let body = fs::read_to_string(path)
.map_err(|error| format!("read {label} {}: {error}", path.display()))?;
let mut fields = BTreeMap::new();
for (line_number, line) in body.lines().enumerate() {
let (key, value) = line
.split_once('=')
.ok_or_else(|| format!("invalid {label} line {}", line_number + 1))?;
if key.is_empty()
|| value.is_empty()
|| fields.insert(key.to_string(), value.to_string()).is_some()
{
return Err(format!(
"invalid or duplicate {label} field at line {}",
line_number + 1
));
}
}
Ok(fields)
}
fn write_causal_file(path: &Path, body: &[u8]) -> Result<(), String> {
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|error| format!("create causal evidence {}: {error}", path.display()))?;
file.write_all(body)
.map_err(|error| format!("write causal evidence {}: {error}", path.display()))?;
file.sync_all()
.map_err(|error| format!("sync causal evidence {}: {error}", path.display()))
}
#[cfg(any(target_os = "android", target_os = "linux"))]
fn rename_causal_bundle_noreplace(from: &Path, to: &Path) -> Result<(), String> {
let from = CString::new(from.as_os_str().as_bytes())
.map_err(|_| "causal staging path contains a NUL byte".to_string())?;
let to = CString::new(to.as_os_str().as_bytes())
.map_err(|_| "causal output path contains a NUL byte".to_string())?;
// SAFETY: both pointers are valid NUL-terminated strings for the duration of
// the syscall. RENAME_NOREPLACE provides the required atomic no-clobber step.
let result = unsafe {
libc::syscall(
libc::SYS_renameat2,
libc::AT_FDCWD,
from.as_ptr(),
libc::AT_FDCWD,
to.as_ptr(),
libc::RENAME_NOREPLACE,
)
};
if result == 0 {
Ok(())
} else {
Err(format!(
"publish causal output atomically: {}",
std::io::Error::last_os_error()
))
}
}
#[cfg(target_os = "macos")]
fn rename_causal_bundle_noreplace(from: &Path, to: &Path) -> Result<(), String> {
let from = CString::new(from.as_os_str().as_bytes())
.map_err(|_| "causal staging path contains a NUL byte".to_string())?;
let to = CString::new(to.as_os_str().as_bytes())
.map_err(|_| "causal output path contains a NUL byte".to_string())?;
// SAFETY: both pointers are valid NUL-terminated strings for the duration of
// renamex_np. RENAME_EXCL provides the required atomic no-clobber step.
let result = unsafe { libc::renamex_np(from.as_ptr(), to.as_ptr(), libc::RENAME_EXCL) };
if result == 0 {
Ok(())
} else {
Err(format!(
"publish causal output atomically: {}",
std::io::Error::last_os_error()
))
}
}
#[cfg(not(any(target_os = "android", target_os = "linux", target_os = "macos")))]
fn rename_causal_bundle_noreplace(_from: &Path, _to: &Path) -> Result<(), String> {
Err(
"atomic no-clobber causal publication currently requires Linux, Android, or macOS"
.to_string(),
)
}
fn sync_causal_directory(path: &Path) -> Result<(), String> {
#[cfg(unix)]
{
fs::File::open(path)
.and_then(|directory| directory.sync_all())
.map_err(|error| format!("sync causal directory {}: {error}", path.display()))?;
}
Ok(())
}
fn causal_output_parent(output: &Path) -> &Path {
output
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
}
fn verify_causal_bundle(input: &Path, bundle: &Path) -> Result<(), String> {
let metadata = fs::symlink_metadata(bundle)
.map_err(|error| format!("inspect causal bundle {}: {error}", bundle.display()))?;
if !metadata.file_type().is_dir() {
return Err("causal bundle must be a directory".to_string());
}
let mut names = Vec::new();
for entry in fs::read_dir(bundle)
.map_err(|error| format!("read causal bundle {}: {error}", bundle.display()))?
{
let entry = entry.map_err(|error| format!("read causal bundle entry: {error}"))?;
let metadata = fs::symlink_metadata(entry.path())
.map_err(|error| format!("inspect causal bundle entry: {error}"))?;
if !metadata.file_type().is_file() || metadata.len() > 1_048_576 {
return Err(
"causal bundle may contain only regular files no larger than 1048576 bytes"
.to_string(),
);
}
names.push(entry.file_name().to_string_lossy().into_owned());
}
names.sort();
if names
!= [
"causal-certificate.txt",
"causal-manifest.txt",
"causal-metrics.csv",
]
{
return Err("causal bundle file inventory is invalid".to_string());
}
let manifest = parse_small_key_value_file(
&bundle.join("causal-manifest.txt"),
"causal bundle manifest",
)?;
let expected_keys = BTreeSet::from([
"causal_bundle_version",
"input_sha256",
"certificate",
"certificate_sha256",
"metrics",
"metrics_sha256",
]);
if manifest.keys().map(String::as_str).collect::<BTreeSet<_>>() != expected_keys {
return Err("causal bundle manifest fields are invalid".to_string());
}
if manifest["causal_bundle_version"] != "1"
|| manifest["certificate"] != "causal-certificate.txt"
|| manifest["metrics"] != "causal-metrics.csv"
{
return Err("unsupported causal bundle contract".to_string());
}
if sha256_file(input)? != manifest["input_sha256"] {
return Err("causal bundle input SHA-256 mismatch".to_string());
}
if sha256_file(&bundle.join("causal-certificate.txt"))? != manifest["certificate_sha256"]
|| sha256_file(&bundle.join("causal-metrics.csv"))? != manifest["metrics_sha256"]
{
return Err("causal bundle evidence SHA-256 mismatch".to_string());
}
let certificate = parse_causal_certificate(&bundle.join("causal-certificate.txt"))?;
let metrics = fs::read_to_string(bundle.join("causal-metrics.csv"))
.map_err(|error| format!("read causal metrics: {error}"))?;
let lines = metrics.lines().collect::<Vec<_>>();
if metrics.len() > 1_048_576 || lines.len() != 2 || lines[0] != CAUSAL_METRICS_HEADER {
return Err("causal metrics contract is invalid".to_string());
}
let values = lines[1].split(',').collect::<Vec<_>>();
if values.len() != 19
|| values[0] != certificate.input_sha256
|| values[1] != certificate.requested_horizon.to_string()
|| values[2] != certificate.bad_frame.to_string()
|| values[3] != certificate.bad_output.to_string()
|| values[4] != certificate.candidate_count.to_string()
|| values[5] != certificate.events.len().to_string()
|| values[16..] != ["true", "true", "ok"]
{
return Err("causal metrics disagree with the certificate".to_string());
}
for index in [6usize, 10, 11, 12, 13] {
values[index]
.parse::<u128>()
.map_err(|_| "causal metrics contain an invalid integer".to_string())?;
}
if values[6] == "0" || !matches!(values[7], "true" | "false") {
return Err("causal metrics contain an invalid query result".to_string());
}
for index in [8usize, 9] {
if values[index] != "none" {
values[index]
.parse::<usize>()
.map_err(|_| "causal metrics contain an invalid CQ bound".to_string())?;
}
}
if (values[7] == "true" && (values[8] == "none" || values[9] == "none"))
|| (values[7] == "false" && values[9] != "none")
{
return Err("causal metrics contain inconsistent CQ admission data".to_string());
}
for index in [14usize, 15] {
let value = values[index]
.parse::<f64>()
.map_err(|_| "causal metrics contain an invalid ratio".to_string())?;
if !value.is_finite() || value < 0.0 {
return Err("causal metrics contain an invalid ratio".to_string());
}
}
verify_causal_certificate(input, &bundle.join("causal-certificate.txt"))?;
println!(
"causal-bundle-verify status=VALID bundle={}",
bundle.display()
);
Ok(())
}
fn explain_aiger_counterexample(
input: &Path,
requested_horizon: usize,
max_bound_bits: usize,
output_dir: &Path,
) -> Result<(), String> {
if output_dir.exists() {
return Err(format!(
"causal output directory already exists: {}",
output_dir.display()
));
}
let model = parse_aag(input)?;
if model.inputs.is_empty() {
return Err("causal analysis requires at least one primary input".to_string());
}
let CausalProblem {
encoding,
run,
clauses,
fixed,
} = causal_base_formula(&model, requested_horizon)?;
let query_index = run.first_sat_query.unwrap();
let query = &encoding.queries[query_index];
let witness = run.first_witness.as_ref().unwrap();
let events = causal_events_from_witness(&model, witness, query.frame);
if events.is_empty() || events.len() > CAUSAL_MAX_EVENTS {
return Err(format!(
"causal analysis event count {} is outside supported range 1..={CAUSAL_MAX_EVENTS}",
events.len()
));
}
// Includes minimisation, its explicit 1-minimality pass, and the two
// independent certificate verifications performed before publication.
let maximum_queries = events
.len()
.checked_mul(4)
.and_then(|value| value.checked_add(3))
.ok_or_else(|| "causal query count overflow".to_string())?;
ensure_causal_query_work(encoding.variables, clauses.len(), maximum_queries)?;
let mut persistent = Solver::new();
add_to_varisat(&mut persistent, &clauses);
let mut cq_compile_ns = 0u128;
let mut cq_bound_bits = None;
let mut compiled = None;
let mut scratch = None;
if encoding.variables <= CAUSAL_CQ_MAX_VARIABLES && clauses.len() <= CAUSAL_CQ_MAX_CLAUSES {
let order = min_fill_order(encoding.variables, &clauses);
let bound = continuation_frontier_bound_bits(encoding.variables, &clauses, &order);
cq_bound_bits = Some(bound);
if bound <= max_bound_bits {
let start = Instant::now();
let value = compile_continuation(&clauses, &order);
cq_compile_ns = start.elapsed().as_nanos();
scratch = Some(ContinuationScratch::new(&value));
compiled = Some(value);
}
}
let mut metrics = CausalMetrics {
queries: 0,
fresh_cdcl_ns: 0,
persistent_cdcl_ns: 0,
cq_ns: 0,
cq_compile_ns,
cq_bound_bits,
cq_peak_classes: compiled.as_ref().map(|value| value.peak_classes),
cq_admitted: compiled.is_some(),
};
let active = minimize_causal_events(events.len(), |active| {
let assumptions = causal_assumptions(&model, encoding.variables, &fixed, &events, active)?;
let fresh_start = Instant::now();
let fresh_sat = solve_causal_fresh(&clauses, &assumptions)?;
metrics.fresh_cdcl_ns += fresh_start.elapsed().as_nanos();
let persistent_start = Instant::now();
let persistent_sat = solve_causal_persistent(&mut persistent, &assumptions)?;
metrics.persistent_cdcl_ns += persistent_start.elapsed().as_nanos();
if fresh_sat != persistent_sat {
return Err("fresh and persistent CDCL disagree on causal intervention".to_string());
}
if let (Some(compiled), Some(scratch)) = (compiled.as_ref(), scratch.as_mut()) {
let cq_start = Instant::now();
let cq_sat = query_continuation(compiled, &assumptions, scratch).is_some();
metrics.cq_ns += cq_start.elapsed().as_nanos();
if cq_sat != fresh_sat {
return Err("CQ and CDCL disagree on causal intervention".to_string());
}
}
metrics.queries += 1;
Ok(!fresh_sat)
})?;
let retained = events
.iter()
.zip(&active)
.filter_map(|(event, &active)| active.then_some(event.clone()))
.collect::<Vec<_>>();
let certificate = CausalCertificate {
input_sha256: sha256_file(input)?,
requested_horizon,
bad_frame: query.frame,
bad_output: query.output,
bad_output_name: model.output_names[query.output].clone(),
candidate_count: events.len(),
events: retained,
};
let cq_query_ratio = if metrics.cq_admitted {
metrics.persistent_cdcl_ns as f64 / metrics.cq_ns.max(1) as f64
} else {
0.0
};
let cq_amortized_ratio = if metrics.cq_admitted {
metrics.persistent_cdcl_ns as f64
/ metrics.cq_ns.saturating_add(metrics.cq_compile_ns).max(1) as f64
} else {
0.0
};
let metrics_body = format!(
"{CAUSAL_METRICS_HEADER}\n{},{requested_horizon},{},{},{},{},{},{},{},{},{},{},{},{},{cq_query_ratio:.6},{cq_amortized_ratio:.6},true,true,ok\n",
certificate.input_sha256,
query.frame,
query.output,
events.len(),
certificate.events.len(),
metrics.queries,
metrics.cq_admitted,
metrics
.cq_bound_bits
.map_or_else(|| "none".to_string(), |value| value.to_string()),
metrics
.cq_peak_classes
.map_or_else(|| "none".to_string(), |value| value.to_string()),
metrics.cq_compile_ns,
metrics.cq_ns,
metrics.persistent_cdcl_ns,
metrics.fresh_cdcl_ns,
);
let parent = causal_output_parent(output_dir);
fs::create_dir_all(parent).map_err(|error| format!("create causal output parent: {error}"))?;
let name = output_dir
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.ok_or_else(|| "causal output directory requires a valid final component".to_string())?;
let staging = parent.join(format!(".{name}.stage-{}", std::process::id()));
if staging.exists() {
return Err(format!(
"causal staging directory already exists: {}",
staging.display()
));
}
fs::create_dir(&staging)
.map_err(|error| format!("create causal staging directory: {error}"))?;
let publish = (|| {
let certificate_path = staging.join("causal-certificate.txt");
let metrics_path = staging.join("causal-metrics.csv");
write_causal_file(
&certificate_path,
causal_certificate_body(input, &model, &certificate)?.as_bytes(),
)?;
write_causal_file(&metrics_path, metrics_body.as_bytes())?;
verify_causal_certificate(input, &certificate_path)?;
let manifest = format!(
"causal_bundle_version=1\ninput_sha256={}\ncertificate=causal-certificate.txt\ncertificate_sha256={}\nmetrics=causal-metrics.csv\nmetrics_sha256={}\n",
certificate.input_sha256,
sha256_file(&certificate_path)?,
sha256_file(&metrics_path)?,
);
write_causal_file(&staging.join("causal-manifest.txt"), manifest.as_bytes())?;
verify_causal_bundle(input, &staging)?;
sync_causal_directory(&staging)?;
rename_causal_bundle_noreplace(&staging, output_dir)?;
sync_causal_directory(parent)
})();
if publish.is_err() {
let _ = fs::remove_dir_all(&staging);
}
publish?;
println!(
"causal-explanation status=VALID target={}@{} candidates={} causes={} queries={} cq_admitted={} bundle={}",
model.output_names[query.output],
query.frame,
events.len(),
certificate.events.len(),
metrics.queries,
metrics.cq_admitted,
output_dir.display(),
);
Ok(())
}
fn causal_selection_fingerprint(events: &[CausalEvent], active: &[bool]) -> Result<String, String> {
if events.len() != active.len() {
return Err("causal fingerprint selection length mismatch".to_string());
}
let mut hasher = Sha256::new();
hasher.update(b"cq-causal-selection-v1\0");
for (index, (event, selected)) in events.iter().zip(active).enumerate() {
if !selected {
continue;
}
hasher.update((index as u64).to_le_bytes());
hasher.update((event.input as u64).to_le_bytes());
hasher.update((event.start_frame as u64).to_le_bytes());
hasher.update((event.end_frame as u64).to_le_bytes());
hasher.update([u8::from(event.value)]);
}
Ok(hasher
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
fn causal_point_events(model: &AagModel, witness: &[bool], bad_frame: usize) -> Vec<CausalEvent> {
let mut events = Vec::new();
for input in 0..model.inputs.len() {
let literal = model.inputs[input];
for frame in 0..=bad_frame {
events.push(CausalEvent {
input,
start_frame: frame,
end_frame: frame,
value: witness[frame * model.max_variable + literal / 2 - 1],
});
}
}
events
}
fn causal_dyadic_events(model: &AagModel, witness: &[bool], bad_frame: usize) -> Vec<CausalEvent> {
let mut events = Vec::new();
for input in 0..model.inputs.len() {
let literal = model.inputs[input];
let frame_count = bad_frame + 1;
let mut length = 1usize;
while length <= frame_count {
for start in (0..frame_count).step_by(length) {
let Some(end) = start.checked_add(length - 1) else {
break;
};
if end >= frame_count {
continue;
}
let value = witness[start * model.max_variable + literal / 2 - 1];
if (start + 1..=end)
.all(|frame| witness[frame * model.max_variable + literal / 2 - 1] == value)
{
events.push(CausalEvent {
input,
start_frame: start,
end_frame: end,
value,
});
}
}
let Some(next) = length.checked_mul(2) else {
break;
};
length = next;
}
}
events
}
fn add_causal_counterfactual_assumption(
assumptions: &mut [Option<bool>],
target: AagCnfLiteral,
) -> Result<bool, String> {
match target {
// Negating a constant-true bad output makes the counterfactual formula
// immediately UNSAT: the failure is unconditional.
AagCnfLiteral::Constant(true) => Ok(true),
AagCnfLiteral::Constant(false) => {
Err("constant-false AIGER target cannot have a counterexample".to_string())
}
AagCnfLiteral::Variable((variable, value)) => {
let slot = assumptions
.get_mut(variable)
.ok_or_else(|| "AIGER target variable is out of range".to_string())?;
if slot.is_some_and(|existing| existing == value) {
return Ok(true);
}
*slot = Some(!value);
Ok(false)
}
}
}
fn causal_query_witness(
solver: &mut Solver<'_>,
variables: usize,
target: AagCnfLiteral,
) -> Result<Option<Vec<bool>>, String> {
match target {
AagCnfLiteral::Constant(false) => return Ok(None),
AagCnfLiteral::Constant(true) => solver.assume(&[]),
AagCnfLiteral::Variable((variable, value)) => {
solver.assume(&[Lit::from_var(Var::from_index(variable), value)])
}
}
if !solver
.solve()
.map_err(|error| format!("solve causal batch target: {error}"))?
{
return Ok(None);
}
let mut assignment = vec![false; variables];
for literal in solver
.model()
.ok_or_else(|| "causal batch SAT result has no model".to_string())?
{
if literal.var().index() < variables {
assignment[literal.var().index()] = literal.is_positive();
}
}
Ok(Some(assignment))
}
fn enumerate_minimal_causal_sets<F>(
count: usize,
max_causes: usize,
mut is_unsat: F,
) -> Result<CausalEnumeration, String>
where
F: FnMut(&[bool]) -> Result<bool, String>,
{
let mut map = Solver::new();
for index in 0..count {
let variable = Var::from_index(index);
map.add_clause(&[
Lit::from_var(variable, true),
Lit::from_var(variable, false),
]);
}
let mut causes = Vec::new();
let mut transcript = Vec::new();
let mut complete = false;
for _ in 0..CAUSAL_BATCH_MAX_MAP_QUERIES {
if !map
.solve()
.map_err(|error| format!("solve causal subset map: {error}"))?
{
complete = true;
break;
}
if causes.len() >= max_causes {
break;
}
let mut seed = vec![false; count];
for literal in map
.model()
.ok_or_else(|| "causal subset map SAT result has no model".to_string())?
{
if literal.var().index() < count {
seed[literal.var().index()] = literal.is_positive();
}
}
if transcript.len() >= CAUSAL_BATCH_MAX_MAP_QUERIES {
break;
}
let unsat = is_unsat(&seed)?;
transcript.push(CausalQueryRecord {
active: seed.clone(),
unsat,
});
if unsat {
for index in 0..count {
if !seed[index] {
continue;
}
seed[index] = false;
if transcript.len() >= CAUSAL_BATCH_MAX_MAP_QUERIES {
return Ok(CausalEnumeration {
causes,
transcript,
complete: false,
});
}
let reduced_unsat = is_unsat(&seed)?;
transcript.push(CausalQueryRecord {
active: seed.clone(),
unsat: reduced_unsat,
});
if !reduced_unsat {
seed[index] = true;
}
}
let blocker = seed
.iter()
.enumerate()
.filter_map(|(index, selected)| {
selected.then_some(Lit::from_var(Var::from_index(index), false))
})
.collect::<Vec<_>>();
map.add_clause(&blocker);
causes.push(seed);
} else {
for index in 0..count {
if seed[index] {
continue;
}
seed[index] = true;
if transcript.len() >= CAUSAL_BATCH_MAX_MAP_QUERIES {
return Ok(CausalEnumeration {
causes,
transcript,
complete: false,
});
}
let grown_unsat = is_unsat(&seed)?;
transcript.push(CausalQueryRecord {
active: seed.clone(),
unsat: grown_unsat,
});
if grown_unsat {
seed[index] = false;
}
}
let blocker = seed
.iter()
.enumerate()
.filter_map(|(index, selected)| {
(!selected).then_some(Lit::from_var(Var::from_index(index), true))
})
.collect::<Vec<_>>();
map.add_clause(&blocker);
}
}
Ok(CausalEnumeration {
causes,
transcript,
complete,
})
}
fn causal_break_even(
cq_prepare_ns: u128,
persistent_setup_ns: u128,
cq_samples: &[u128],
persistent_samples: &[u128],
) -> (Option<usize>, Option<usize>) {
let mut cq_total = cq_prepare_ns;
let mut persistent_total = persistent_setup_ns;
let mut measured = None;
for (index, (&cq, &persistent)) in cq_samples.iter().zip(persistent_samples).enumerate() {
cq_total = cq_total.saturating_add(cq);
persistent_total = persistent_total.saturating_add(persistent);
if measured.is_none() && cq_total <= persistent_total {
measured = Some(index + 1);
}
}
let cq_average = cq_samples
.iter()
.fold(0u128, |total, sample| total.saturating_add(*sample))
/ cq_samples.len().max(1) as u128;
let persistent_average = persistent_samples
.iter()
.fold(0u128, |total, sample| total.saturating_add(*sample))
/ persistent_samples.len().max(1) as u128;
let projected = if cq_average < persistent_average && cq_prepare_ns > persistent_setup_ns {
let deficit = cq_prepare_ns - persistent_setup_ns;
let saving = persistent_average - cq_average;
usize::try_from(deficit.div_ceil(saving)).ok()
} else if cq_prepare_ns <= persistent_setup_ns {
Some(0)
} else {
None
};
(measured, projected)
}
fn publish_causal_comparison(path: &Path, body: &[u8]) -> Result<(), String> {
if path.exists() {
return Err(format!(
"causal comparison output already exists: {}",
path.display()
));
}
let parent = causal_output_parent(path);
fs::create_dir_all(parent)
.map_err(|error| format!("create causal comparison parent: {error}"))?;
let name = path
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.is_empty())
.ok_or_else(|| "causal comparison output requires a valid final component".to_string())?;
let staging = parent.join(format!(".{name}.stage-{}", std::process::id()));
if staging.exists() {
return Err(format!(
"causal comparison staging path already exists: {}",
staging.display()
));
}
let publish = (|| {
write_causal_file(&staging, body)?;
rename_causal_bundle_noreplace(&staging, path)?;
sync_causal_directory(parent)
})();
if publish.is_err() {
let _ = fs::remove_file(staging);
}
publish
}
fn benchmark_aiger_predicate_interface(
input: &Path,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=CAUSAL_BATCH_MAX_REPEATS).contains(&repeats) {
return Err(format!(
"predicate interface repeats must be in 1..={CAUSAL_BATCH_MAX_REPEATS}"
));
}
let model = parse_aag(input)?;
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err("predicate interface requires declared initial latch values".to_string());
}
let compile_start = Instant::now();
let mut predicate = AagPredicateInterface::compile(&model)?;
let compile_ns = compile_start.elapsed().as_nanos();
let encoding = aag_bmc_encoding(&model, 0)?;
let query = encoding
.queries
.iter()
.find(|query| query.frame == 0 && query.output == 0)
.ok_or_else(|| "predicate interface requires bad output zero at frame zero".to_string())?;
let setup_start = Instant::now();
let mut persistent = Solver::new();
add_to_varisat(&mut persistent, &encoding.clauses);
let persistent_setup_ns = setup_start.elapsed().as_nanos();
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
let mut lines = vec!["predicate_interface_schema_version,input_sha256,declared_inputs,relevant_inputs,bdd_nodes,scenario,repeats,predicate_compile_ns,persistent_setup_ns,predicate_query_ns,persistent_query_ns,predicate_query_speedup,workload_speedup,agreement,witness_valid,status".to_string()];
let digest = sha256_file(input)?;
for released in 0..=predicate.projected_inputs.len() {
let mut constraints = vec![Some(true); predicate.projected_inputs.len()];
let scenario = if released == predicate.projected_inputs.len() {
"all-true".to_string()
} else {
constraints[released] = Some(false);
format!("release-{released}")
};
let mut predicate_ns = 0u128;
let mut persistent_ns = 0u128;
let mut agreement = true;
let mut witness_valid = true;
for _ in 0..repeats {
let start = Instant::now();
let predicate_witness =
predicate.witness_input(initial_state, None, Some(0), &constraints)?;
predicate_ns = predicate_ns.saturating_add(start.elapsed().as_nanos());
let mut assumptions = vec![None; encoding.variables];
for (projected, required) in constraints.iter().enumerate() {
let input = predicate.projected_inputs[projected];
assumptions[model.inputs[input] / 2 - 1] = *required;
}
let unconditional =
add_causal_counterfactual_assumption(&mut assumptions, query.assumption)?;
let start = Instant::now();
let persistent_avoidable =
!unconditional && solve_causal_persistent(&mut persistent, &assumptions)?;
persistent_ns = persistent_ns.saturating_add(start.elapsed().as_nanos());
agreement &= predicate_witness.is_some() == persistent_avoidable;
if let Some(declared) = predicate_witness {
let mut values = vec![false; model.max_variable + 1];
for (bit, latch) in model.latches.iter().enumerate() {
values[latch.current / 2] = initial_state >> bit & 1 == 1;
}
for (input, literal) in model.inputs.iter().enumerate() {
values[literal / 2] = declared >> input & 1 == 1;
}
for gate in &model.ands {
values[gate.output / 2] = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
}
witness_valid &= !evaluate_aag_literal(model.outputs[0], &values)
&& constraints.iter().enumerate().all(|(projected, required)| {
required.is_none_or(|value| {
(declared >> predicate.projected_inputs[projected] & 1 == 1) == value
})
});
}
}
if !agreement || !witness_valid {
return Err(format!(
"predicate interface scenario {scenario} disagrees with persistent CDCL (agreement={agreement}, witness_valid={witness_valid})"
));
}
let query_speedup = persistent_ns as f64 / predicate_ns.max(1) as f64;
let workload_speedup = persistent_setup_ns.saturating_add(persistent_ns) as f64
/ compile_ns.saturating_add(predicate_ns).max(1) as f64;
lines.push(format!(
"1,{digest},{},{},{},{scenario},{repeats},{compile_ns},{persistent_setup_ns},{predicate_ns},{persistent_ns},{query_speedup:.6},{workload_speedup:.6},true,true,ok",
model.inputs.len(),
predicate.projected_inputs.len(),
predicate.manager.nodes.len()
));
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"predicate-interface status=VALID relevant_inputs={} bdd_nodes={} output={}",
predicate.projected_inputs.len(),
predicate.manager.nodes.len(),
output.display()
);
Ok(())
}
fn query_aiger_predicate_quotient(
input: &Path,
horizon: usize,
output: usize,
) -> Result<(), String> {
if horizon > INTERFACE_QUOTIENT_MAX_HORIZON {
return Err(format!(
"predicate quotient horizon exceeds {INTERFACE_QUOTIENT_MAX_HORIZON}"
));
}
let model = parse_aag(input)?;
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err("predicate quotient requires declared initial latch values".to_string());
}
if output >= model.outputs.len() {
return Err(format!(
"predicate quotient output {output} is out of range for {} outputs",
model.outputs.len()
));
}
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
let mut quotient = PredicateQuotient::new(&model)?;
let constraints = vec![vec![None; quotient.interface.projected_inputs.len()]; horizon + 1];
match quotient.query(initial_state, output, &constraints)? {
Some(witness) => {
if !validate_predicate_witness(
&model,
"ient.interface.projected_inputs,
output,
&constraints,
&witness,
) {
return Err("predicate quotient witness failed original-AIG replay".to_string());
}
println!("status=AVOIDABLE witness_valid=true");
println!("frame,state,input_pattern_binary");
for (frame, (state, input)) in witness
.states
.iter()
.zip(&witness.declared_inputs)
.enumerate()
{
println!(
"{frame},{state},{:0width$b}",
input,
width = model.inputs.len()
);
}
}
None => println!("status=UNAVOIDABLE"),
}
Ok(())
}
fn benchmark_aiger_predicate_symbolic(
input: &Path,
horizon: usize,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if horizon > INTERFACE_QUOTIENT_MAX_HORIZON {
return Err(format!(
"predicate symbolic horizon exceeds {INTERFACE_QUOTIENT_MAX_HORIZON}"
));
}
if !(1..=100).contains(&repeats) {
return Err("predicate symbolic repeats must be in 1..=100".to_string());
}
let canonical = input
.canonicalize()
.map_err(|error| format!("canonicalize predicate symbolic input: {error}"))?;
let path = canonical.to_string_lossy();
if !path
.chars()
.all(|character| character.is_ascii_alphanumeric() || "/._-".contains(character))
{
return Err("predicate symbolic input path contains unsupported characters".to_string());
}
let model = parse_aag(&canonical)?;
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err(
"predicate symbolic benchmark requires declared initial latch values".to_string(),
);
}
let output_name = model
.output_names
.first()
.filter(|name| !name.is_empty())
.ok_or_else(|| "predicate symbolic benchmark requires a named output zero".to_string())?;
if !output_name
.chars()
.all(|character| character.is_ascii_alphanumeric() || "_.$".contains(character))
{
return Err("predicate symbolic output name contains unsupported characters".to_string());
}
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
let compile_start = Instant::now();
let mut quotient = PredicateQuotient::new(&model)?;
let predicate_compile_ns = compile_start.elapsed().as_nanos();
let mut constraints =
vec![vec![Some(true); quotient.interface.projected_inputs.len()]; horizon + 1];
let released = quotient.interface.projected_inputs.len() - 1;
constraints[horizon][released] = Some(false);
let query_start = Instant::now();
for _ in 0..repeats {
let witness = quotient
.query(initial_state, 0, &constraints)?
.ok_or_else(|| {
"predicate quotient unexpectedly found the output unavoidable".to_string()
})?;
if !validate_predicate_witness(
&model,
"ient.interface.projected_inputs,
0,
&constraints,
&witness,
) {
return Err("predicate quotient witness failed original-AIG replay".to_string());
}
}
let predicate_query_ns = query_start.elapsed().as_nanos();
let persistent_encoding_start = Instant::now();
let encoding = aag_bmc_encoding(&model, horizon)?;
let persistent_encoding_ns = persistent_encoding_start.elapsed().as_nanos();
let query = encoding
.queries
.iter()
.find(|query| query.frame == horizon && query.output == 0)
.ok_or_else(|| "predicate symbolic benchmark is missing its terminal query".to_string())?;
let persistent_setup_start = Instant::now();
let mut persistent = Solver::new();
add_to_varisat(&mut persistent, &encoding.clauses);
let persistent_setup_ns = persistent_setup_start.elapsed().as_nanos();
let mut assumptions = vec![None; encoding.variables];
for (frame, frame_constraints) in constraints.iter().enumerate() {
for (projected, required) in frame_constraints.iter().enumerate() {
let input = quotient.interface.projected_inputs[projected];
assumptions[frame * model.max_variable + model.inputs[input] / 2 - 1] = *required;
}
}
let unconditional = add_causal_counterfactual_assumption(&mut assumptions, query.assumption)?;
if unconditional {
return Err("predicate symbolic benchmark requires a non-constant output".to_string());
}
let persistent_query_start = Instant::now();
for _ in 0..repeats {
if !solve_causal_persistent(&mut persistent, &assumptions)? {
return Err(
"persistent CDCL disagrees with the avoiding predicate witness".to_string(),
);
}
}
let persistent_query_ns = persistent_query_start.elapsed().as_nanos();
let projected_names = quotient
.interface
.projected_inputs
.iter()
.map(|input| {
model
.input_names
.get(*input)
.filter(|name| {
!name.is_empty()
&& name.chars().all(|character| {
character.is_ascii_alphanumeric() || "_.$[]".contains(character)
})
})
.ok_or_else(|| {
"predicate symbolic benchmark requires safe named relevant inputs".to_string()
})
})
.collect::<Result<Vec<_>, _>>()?;
let mut script = format!("read_aiger {path}; hierarchy -auto-top;");
script.push_str(&format!(" sat -seq {}", horizon + 1));
for (frame, frame_constraints) in constraints.iter().enumerate() {
for (projected, required) in frame_constraints.iter().enumerate() {
script.push_str(&format!(
" -set-at {} {} {}",
frame + 1,
projected_names[projected],
usize::from(required.unwrap())
));
}
}
script.push_str(&format!(" -set-at {} {} 0;", horizon + 1, output_name));
let yosys_start = Instant::now();
let yosys = Command::new("yosys")
.args(["-Q", "-p", &script])
.output()
.map_err(|error| format!("run maintained Yosys symbolic baseline: {error}"))?;
let yosys_ns = yosys_start.elapsed().as_nanos();
if !yosys.status.success() {
return Err(format!(
"maintained Yosys symbolic baseline failed: {}",
String::from_utf8_lossy(&yosys.stderr)
));
}
let stdout = String::from_utf8_lossy(&yosys.stdout);
let models = stdout
.matches("SAT solving finished - model found:")
.count();
if models != 1 {
return Err(format!(
"maintained Yosys symbolic baseline returned {models} SAT results; expected 1"
));
}
let predicate_total_ns = predicate_compile_ns.saturating_add(predicate_query_ns);
let persistent_total_ns = persistent_encoding_ns
.saturating_add(persistent_setup_ns)
.saturating_add(persistent_query_ns);
let body = format!(
"predicate_symbolic_schema_version,input_sha256,horizon,repeats,relevant_inputs,bdd_nodes,predicate_compile_ns,predicate_query_ns,predicate_total_ns,persistent_encoding_ns,persistent_setup_ns,persistent_query_ns,persistent_total_ns,persistent_over_predicate_speedup,yosys_version,yosys_repeats,yosys_total_ns,yosys_over_predicate_speedup,agreement,witness_valid,static_admission,status\n4,{},{horizon},{repeats},{},{},{predicate_compile_ns},{predicate_query_ns},{predicate_total_ns},{persistent_encoding_ns},{persistent_setup_ns},{persistent_query_ns},{persistent_total_ns},{:.6},{},1,{yosys_ns},{:.6},true,true,{},ok\n",
sha256_file(&canonical)?,
quotient.interface.projected_inputs.len(),
quotient.interface.manager.nodes.len(),
persistent_total_ns as f64 / predicate_total_ns.max(1) as f64,
report_csv_field(
&String::from_utf8_lossy(
&Command::new("yosys")
.arg("-V")
.output()
.map_err(|error| format!("read Yosys version: {error}"))?
.stdout
)
.trim()
.replace(',', ";")
),
yosys_ns as f64 / predicate_total_ns.max(1) as f64,
predicate_quotient_admitted(
quotient.interface.projected_inputs.len(),
model.latches.len(),
horizon,
repeats
)
);
publish_causal_comparison(output, body.as_bytes())?;
println!(
"predicate-symbolic status=VALID agreement=true witness_valid=true output={}",
output.display()
);
Ok(())
}
fn benchmark_aiger_interface_quotient(
input: &Path,
horizon: usize,
max_causes: usize,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if horizon > INTERFACE_QUOTIENT_MAX_HORIZON {
return Err(format!(
"interface quotient horizon exceeds {INTERFACE_QUOTIENT_MAX_HORIZON}"
));
}
if !(1..=CAUSAL_BATCH_MAX_CAUSES).contains(&max_causes) {
return Err(format!(
"interface quotient cause limit must be in 1..={CAUSAL_BATCH_MAX_CAUSES}"
));
}
if !(1..=CAUSAL_BATCH_MAX_REPEATS).contains(&repeats) {
return Err(format!(
"interface quotient repeats must be in 1..={CAUSAL_BATCH_MAX_REPEATS}"
));
}
let model = parse_aag(input)?;
if model.latches.iter().any(|latch| latch.initial.is_none()) {
return Err(
"interface quotient benchmark requires every initial latch value to be declared"
.to_string(),
);
}
let interface_compile_start = Instant::now();
let table = AagInterfaceTable::compile(&model)?;
let interface_compile_ns = interface_compile_start.elapsed().as_nanos();
let encoding = aag_bmc_encoding(&model, horizon)?;
if encoding.queries.len() > CAUSAL_BATCH_MAX_TARGETS {
return Err(format!(
"interface quotient has {} targets; safety limit is {CAUSAL_BATCH_MAX_TARGETS}",
encoding.queries.len()
));
}
ensure_causal_query_work(
encoding.variables,
encoding.clauses.len(),
CAUSAL_BATCH_MAX_MAP_QUERIES,
)?;
let persistent_setup_start = Instant::now();
let mut persistent = Solver::new();
add_to_varisat(&mut persistent, &encoding.clauses);
let persistent_setup_ns = persistent_setup_start.elapsed().as_nanos();
let mut target_solver = Solver::new();
add_to_varisat(&mut target_solver, &encoding.clauses);
let input_sha256 = sha256_file(input)?;
let mut lines = vec![INTERFACE_QUOTIENT_HEADER.to_string()];
let mut workload_tree_prepare_ns = 0u128;
let mut workload_interface_query_ns = 0u128;
let mut workload_interface_witness_ns = 0u128;
let mut workload_persistent_query_ns = 0u128;
let mut workload_persistent_witness_ns = 0u128;
for query in &encoding.queries {
let Some(witness) =
causal_query_witness(&mut target_solver, encoding.variables, query.assumption)?
else {
continue;
};
let events = causal_events_from_witness_for_inputs(
&model,
&witness,
query.frame,
table.projected_inputs.iter().copied(),
);
if events.is_empty() || events.len() > CAUSAL_MAX_EVENTS {
return Err(format!(
"interface quotient event count {} is outside 1..={CAUSAL_MAX_EVENTS}",
events.len()
));
}
let mut discovery = Solver::new();
add_to_varisat(&mut discovery, &encoding.clauses);
let enumeration = enumerate_minimal_causal_sets(events.len(), max_causes, |active| {
let mut assumptions =
causal_assumptions(&model, encoding.variables, &[], &events, active)?;
if add_causal_counterfactual_assumption(&mut assumptions, query.assumption)? {
Ok(true)
} else {
Ok(!solve_causal_persistent(&mut discovery, &assumptions)?)
}
})?;
if enumeration.causes.is_empty() {
return Err(format!(
"interface quotient found no cause for output {} at frame {}",
query.output, query.frame
));
}
let tree_prepare_start = Instant::now();
let mut tree = InterfaceQuotientTree::new(&table, query.frame)?;
let tree_prepare_ns = tree_prepare_start.elapsed().as_nanos();
let root_pairs = tree.root_pairs();
let mut interface_query_ns = 0u128;
let mut interface_witness_ns = 0u128;
let mut persistent_query_ns = 0u128;
let mut persistent_witness_ns = 0u128;
let mut repaired_leaves = 0usize;
let mut repaired_internal_nodes = 0usize;
let mut witnesses_valid = true;
let replay_queries = enumeration
.transcript
.len()
.checked_mul(repeats)
.ok_or_else(|| "interface quotient replay count overflow".to_string())?;
ensure_causal_query_work(encoding.variables, encoding.clauses.len(), replay_queries)?;
for _ in 0..repeats {
for record in &enumeration.transcript {
let constraints =
causal_interface_constraints(&table, query.frame, &events, &record.active)?;
let start = Instant::now();
let interface_outcome =
tree.query_avoiding(&model, query.output, &constraints, false)?;
interface_query_ns = interface_query_ns.saturating_add(start.elapsed().as_nanos());
repaired_leaves = repaired_leaves.saturating_add(interface_outcome.repaired_leaves);
repaired_internal_nodes = repaired_internal_nodes
.saturating_add(interface_outcome.repaired_internal_nodes);
let mut assumptions =
causal_assumptions(&model, encoding.variables, &[], &events, &record.active)?;
let immediate_unsat =
add_causal_counterfactual_assumption(&mut assumptions, query.assumption)?;
let start = Instant::now();
let persistent_unsat = if immediate_unsat {
true
} else {
!solve_causal_persistent(&mut persistent, &assumptions)?
};
persistent_query_ns =
persistent_query_ns.saturating_add(start.elapsed().as_nanos());
let interface_unsat = !interface_outcome.avoidable;
if interface_unsat != record.unsat || persistent_unsat != record.unsat {
return Err(format!(
"interface quotient disagrees at output {} frame {}",
query.output, query.frame
));
}
}
}
if let Some(record) = enumeration.transcript.iter().find(|record| !record.unsat) {
let constraints =
causal_interface_constraints(&table, query.frame, &events, &record.active)?;
let start = Instant::now();
let recovered = tree.query_avoiding(&model, query.output, &constraints, true)?;
interface_witness_ns = start.elapsed().as_nanos();
witnesses_valid &= recovered.avoidable
&& recovered.witness.as_ref().is_some_and(|witness| {
validate_interface_witness(&model, &table, query.output, &constraints, witness)
});
let mut assumptions =
causal_assumptions(&model, encoding.variables, &[], &events, &record.active)?;
if add_causal_counterfactual_assumption(&mut assumptions, query.assumption)? {
return Err("SAT transcript unexpectedly has an unconditional failure".to_string());
}
let start = Instant::now();
let mut witness_solver = Solver::new();
add_to_varisat(&mut witness_solver, &encoding.clauses);
let persistent_witness = solve_causal_persistent_assignment(
&mut witness_solver,
encoding.variables,
&assumptions,
)?;
persistent_witness_ns = start.elapsed().as_nanos();
witnesses_valid &= persistent_witness.as_ref().is_some_and(|assignment| {
satisfies(&encoding.clauses, assignment)
&& validate_aag_trace(&model, horizon, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
});
}
if !witnesses_valid {
return Err("interface quotient reconstructed an invalid witness".to_string());
}
let interface_query_speedup = persistent_query_ns as f64 / interface_query_ns.max(1) as f64;
workload_tree_prepare_ns = workload_tree_prepare_ns.saturating_add(tree_prepare_ns);
workload_interface_query_ns =
workload_interface_query_ns.saturating_add(interface_query_ns);
workload_interface_witness_ns =
workload_interface_witness_ns.saturating_add(interface_witness_ns);
workload_persistent_query_ns =
workload_persistent_query_ns.saturating_add(persistent_query_ns);
workload_persistent_witness_ns =
workload_persistent_witness_ns.saturating_add(persistent_witness_ns);
let workload_total_speedup = persistent_setup_ns
.saturating_add(workload_persistent_query_ns)
.saturating_add(workload_persistent_witness_ns)
as f64
/ interface_compile_ns
.saturating_add(workload_tree_prepare_ns)
.saturating_add(workload_interface_query_ns)
.saturating_add(workload_interface_witness_ns)
.max(1) as f64;
lines.push(format!(
"{INTERFACE_QUOTIENT_SCHEMA_VERSION},{input_sha256},{horizon},{},{},{},{},{},{repeats},{interface_compile_ns},{tree_prepare_ns},{interface_query_ns},{interface_witness_ns},{persistent_setup_ns},{persistent_query_ns},{persistent_witness_ns},{repaired_leaves},{repaired_internal_nodes},{root_pairs},{},{},{interface_query_speedup:.6},{workload_total_speedup:.6},true,true,ok",
query.frame,
query.output,
events.len(),
enumeration.causes.len(),
enumeration.transcript.len(),
tree.summary_cache_hits,
tree.summary_cache_misses,
));
}
if lines.len() == 1 {
return Err("AIGER model has no failing target within the requested horizon".to_string());
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"interface-quotient status=VALID rows={} states={} declared_inputs={} projected_inputs={} output={}",
lines.len() - 1,
1usize << table.width,
table.declared_input_count,
table.input_count,
output.display()
);
Ok(())
}
fn verify_aiger_interface_quotient(input: &Path, report: &Path) -> Result<(), String> {
let body = fs::read_to_string(report).map_err(|error| {
format!(
"read interface quotient report {}: {error}",
report.display()
)
})?;
if body.len() > 16 * 1024 * 1024 {
return Err("interface quotient report exceeds 16 MiB safety limit".to_string());
}
let mut rows = body.lines();
if rows.next() != Some(INTERFACE_QUOTIENT_HEADER) {
return Err("invalid interface quotient report header".to_string());
}
let model = parse_aag(input)?;
let table = AagInterfaceTable::compile(&model)?;
let input_sha256 = sha256_file(input)?;
let mut report_horizon = None;
let mut seen = BTreeSet::new();
let mut validated = 0usize;
let mut witness_cache = BTreeMap::<(usize, usize), Vec<bool>>::new();
for (row_index, row) in rows.enumerate() {
let fields = row.split(',').collect::<Vec<_>>();
if fields.len() != 26 {
return Err(format!(
"interface quotient row {} has {} fields; expected 26",
row_index + 2,
fields.len()
));
}
if fields[0] != INTERFACE_QUOTIENT_SCHEMA_VERSION.to_string()
|| fields[1] != input_sha256
|| fields[23..] != ["true", "true", "ok"]
{
return Err(format!(
"interface quotient row {} has invalid identity or status",
row_index + 2
));
}
let horizon = parse_causal_batch_usize(fields[2], "interface horizon")?;
if report_horizon
.replace(horizon)
.is_some_and(|old| old != horizon)
{
return Err("interface quotient report mixes horizons".to_string());
}
let bad_frame = parse_causal_batch_usize(fields[3], "interface bad frame")?;
let bad_output = parse_causal_batch_usize(fields[4], "interface bad output")?;
let candidate_count = parse_causal_batch_usize(fields[5], "interface candidates")?;
let cause_count = parse_causal_batch_usize(fields[6], "interface causes")?;
if !(1..=CAUSAL_BATCH_MAX_CAUSES).contains(&cause_count) {
return Err(
"interface quotient cause count is outside the supported range".to_string(),
);
}
let oracle_queries = parse_causal_batch_usize(fields[7], "interface oracle queries")?;
if !seen.insert((bad_frame, bad_output)) {
return Err("interface quotient report contains a duplicate target".to_string());
}
let encoding = aag_bmc_encoding(&model, horizon)?;
let query = encoding
.queries
.iter()
.find(|query| query.frame == bad_frame && query.output == bad_output)
.ok_or_else(|| "interface quotient target does not exist".to_string())?;
if !witness_cache.contains_key(&(bad_frame, bad_output)) {
let mut solver = Solver::new();
add_to_varisat(&mut solver, &encoding.clauses);
for candidate in &encoding.queries {
if let Some(witness) =
causal_query_witness(&mut solver, encoding.variables, candidate.assumption)?
{
witness_cache.insert((candidate.frame, candidate.output), witness);
}
}
}
let witness = witness_cache
.get(&(bad_frame, bad_output))
.ok_or_else(|| "interface quotient target is unreachable".to_string())?;
let events = causal_events_from_witness_for_inputs(
&model,
witness,
bad_frame,
table.projected_inputs.iter().copied(),
);
if events.len() != candidate_count {
return Err("interface quotient candidate count mismatch".to_string());
}
let enumeration = enumerate_minimal_causal_sets(events.len(), cause_count, |active| {
let mut assumptions =
causal_assumptions(&model, encoding.variables, &[], &events, active)?;
if add_causal_counterfactual_assumption(&mut assumptions, query.assumption)? {
Ok(true)
} else {
Ok(!solve_causal_fresh(&encoding.clauses, &assumptions)?)
}
})?;
if enumeration.causes.len() != cause_count || enumeration.transcript.len() != oracle_queries
{
return Err("interface quotient enumeration metadata mismatch".to_string());
}
let mut tree = InterfaceQuotientTree::new(&table, bad_frame)?;
for record in &enumeration.transcript {
let constraints =
causal_interface_constraints(&table, bad_frame, &events, &record.active)?;
let answer = tree.query_avoiding(&model, bad_output, &constraints, false)?;
if answer.avoidable == record.unsat {
return Err("interface quotient replay disagrees with fresh CDCL".to_string());
}
}
if let Some(record) = enumeration.transcript.iter().find(|record| !record.unsat) {
let constraints =
causal_interface_constraints(&table, bad_frame, &events, &record.active)?;
let recovered = tree.query_avoiding(&model, bad_output, &constraints, true)?;
if !recovered.witness.as_ref().is_some_and(|witness| {
validate_interface_witness(&model, &table, bad_output, &constraints, witness)
}) {
return Err("interface quotient replay witness is invalid".to_string());
}
}
validated += 1;
}
let horizon = report_horizon.ok_or_else(|| "interface report has no rows".to_string())?;
let encoding = aag_bmc_encoding(&model, horizon)?;
let mut expected = BTreeSet::new();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &encoding.clauses);
for query in &encoding.queries {
if causal_query_witness(&mut solver, encoding.variables, query.assumption)?.is_some() {
expected.insert((query.frame, query.output));
}
}
if seen != expected {
return Err("interface quotient report omits a reachable target".to_string());
}
println!(
"interface-quotient-verify status=VALID rows={validated} report={}",
report.display()
);
Ok(())
}
fn benchmark_aiger_causal_batch(
input: &Path,
horizon: usize,
max_bound_bits: usize,
max_causes: usize,
repeats: usize,
output: &Path,
) -> Result<(), String> {
if !(1..=CAUSAL_BATCH_MAX_CAUSES).contains(&max_causes) {
return Err(format!(
"causal batch cause limit must be in 1..={CAUSAL_BATCH_MAX_CAUSES}"
));
}
if !(1..=CAUSAL_BATCH_MAX_REPEATS).contains(&repeats) {
return Err(format!(
"causal batch repeats must be in 1..={CAUSAL_BATCH_MAX_REPEATS}"
));
}
let model = parse_aag(input)?;
if model.inputs.is_empty() {
return Err("causal batch requires at least one primary input".to_string());
}
let encoding = aag_bmc_encoding(&model, horizon)?;
if encoding.queries.len() > CAUSAL_BATCH_MAX_TARGETS {
return Err(format!(
"causal batch has {} targets; safety limit is {CAUSAL_BATCH_MAX_TARGETS}",
encoding.queries.len()
));
}
let cq_prepare_start = Instant::now();
let mut cq_bound_bits = None;
let mut compiled = None;
if encoding.variables <= CAUSAL_CQ_MAX_VARIABLES
&& encoding.clauses.len() <= CAUSAL_CQ_MAX_CLAUSES
{
let order = min_fill_order(encoding.variables, &encoding.clauses);
let bound = continuation_frontier_bound_bits(encoding.variables, &encoding.clauses, &order);
cq_bound_bits = Some(bound);
if bound <= max_bound_bits {
compiled = Some(compile_continuation(&encoding.clauses, &order));
}
}
let cq_prepare_ns = cq_prepare_start.elapsed().as_nanos();
let cq_peak_classes = compiled.as_ref().map(|value| value.peak_classes);
let persistent_setup_start = Instant::now();
let mut persistent = Solver::new();
add_to_varisat(&mut persistent, &encoding.clauses);
let persistent_setup_ns = persistent_setup_start.elapsed().as_nanos();
let mut target_solver = Solver::new();
add_to_varisat(&mut target_solver, &encoding.clauses);
let input_sha256 = sha256_file(input)?;
let mut lines = vec![CAUSAL_BATCH_HEADER.to_string()];
let mut failing_targets = 0usize;
let mut workload_persistent_samples = Vec::new();
let mut workload_cq_samples = Vec::new();
for query in &encoding.queries {
let Some(witness) =
causal_query_witness(&mut target_solver, encoding.variables, query.assumption)?
else {
continue;
};
failing_targets += 1;
let fixed = model
.latches
.iter()
.filter(|latch| latch.initial.is_none())
.map(|latch| {
let variable = latch.current / 2 - 1;
(variable, witness[variable])
})
.collect::<Vec<_>>();
let vocabularies = [
(
"segments",
causal_events_from_witness(&model, &witness, query.frame),
),
("points", causal_point_events(&model, &witness, query.frame)),
(
"dyadic",
causal_dyadic_events(&model, &witness, query.frame),
),
];
for (vocabulary, events) in vocabularies {
if events.is_empty() || events.len() > CAUSAL_MAX_EVENTS {
return Err(format!(
"causal batch {vocabulary} vocabulary has {} events; supported range is 1..={CAUSAL_MAX_EVENTS}",
events.len()
));
}
ensure_causal_query_work(
encoding.variables,
encoding.clauses.len(),
CAUSAL_BATCH_MAX_MAP_QUERIES,
)?;
let mut discovery = Solver::new();
add_to_varisat(&mut discovery, &encoding.clauses);
let enumeration = enumerate_minimal_causal_sets(events.len(), max_causes, |active| {
let mut assumptions =
causal_assumptions(&model, encoding.variables, &fixed, &events, active)?;
if add_causal_counterfactual_assumption(&mut assumptions, query.assumption)? {
Ok(true)
} else {
Ok(!solve_causal_persistent(&mut discovery, &assumptions)?)
}
})?;
if enumeration.causes.is_empty() {
return Err(format!(
"causal batch found no sufficient cause for {}@{} using {vocabulary}",
query.output, query.frame
));
}
let replay_count = enumeration
.transcript
.len()
.checked_mul(repeats)
.ok_or_else(|| "causal batch replay count overflow".to_string())?;
ensure_causal_query_work(encoding.variables, encoding.clauses.len(), replay_count)?;
let mut persistent_samples = Vec::with_capacity(replay_count);
let mut cq_samples = Vec::with_capacity(replay_count);
let mut scratch = compiled.as_ref().map(ContinuationScratch::new);
for _ in 0..repeats {
for record in &enumeration.transcript {
let mut assumptions = causal_assumptions(
&model,
encoding.variables,
&fixed,
&events,
&record.active,
)?;
let unconditional =
add_causal_counterfactual_assumption(&mut assumptions, query.assumption)?;
let start = Instant::now();
let persistent_unsat =
unconditional || !solve_causal_persistent(&mut persistent, &assumptions)?;
persistent_samples.push(start.elapsed().as_nanos());
if persistent_unsat != record.unsat {
return Err("causal batch persistent CDCL replay disagrees".to_string());
}
if let (Some(compiled), Some(scratch)) = (compiled.as_ref(), scratch.as_mut()) {
let start = Instant::now();
let cq_unsat = unconditional
|| query_continuation(compiled, &assumptions, scratch).is_none();
cq_samples.push(start.elapsed().as_nanos());
if cq_unsat != record.unsat {
return Err("causal batch CQ replay disagrees".to_string());
}
}
}
}
workload_persistent_samples.extend_from_slice(&persistent_samples);
workload_cq_samples.extend_from_slice(&cq_samples);
let (measured, projected) = if compiled.is_some() {
causal_break_even(
cq_prepare_ns,
persistent_setup_ns,
&workload_cq_samples,
&workload_persistent_samples,
)
} else {
(None, None)
};
let causes = enumeration
.causes
.iter()
.map(|cause| {
let indices = cause
.iter()
.enumerate()
.filter_map(|(index, selected)| selected.then_some(index.to_string()))
.collect::<Vec<_>>()
.join(";");
if indices.is_empty() {
"-".to_string()
} else {
indices
}
})
.collect::<Vec<_>>()
.join("|");
let unique_queries = enumeration
.transcript
.iter()
.map(|record| record.active.clone())
.collect::<BTreeSet<_>>()
.len();
let cq_query_ns = cq_samples
.iter()
.fold(0u128, |total, sample| total.saturating_add(*sample));
let persistent_query_ns = persistent_samples
.iter()
.fold(0u128, |total, sample| total.saturating_add(*sample));
lines.push(format!(
"{CAUSAL_BATCH_SCHEMA_VERSION},{input_sha256},{horizon},{},{},{vocabulary},{},{},{causes},{},{},{},{},{},{},{cq_prepare_ns},{cq_query_ns},{persistent_setup_ns},{persistent_query_ns},{repeats},{},{},true,true,ok",
query.frame,
query.output,
events.len(),
enumeration.causes.len(),
enumeration.complete,
enumeration.transcript.len(),
unique_queries,
compiled.is_some(),
cq_bound_bits.map_or_else(|| "none".to_string(), |value| value.to_string()),
cq_peak_classes.map_or_else(|| "none".to_string(), |value| value.to_string()),
measured.map_or_else(|| "none".to_string(), |value| value.to_string()),
projected.map_or_else(|| "none".to_string(), |value| value.to_string()),
));
}
}
if failing_targets == 0 {
return Err("AIGER model has no failing target within the requested horizon".to_string());
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"causal-batch status=VALID failing_targets={failing_targets} rows={} cq_admitted={} output={}",
lines.len() - 1,
compiled.is_some(),
output.display()
);
Ok(())
}
fn parse_causal_batch_usize(value: &str, field: &str) -> Result<usize, String> {
value
.parse::<usize>()
.map_err(|_| format!("invalid causal batch {field}"))
}
fn verify_aiger_causal_batch(input: &Path, report: &Path) -> Result<(), String> {
let body = fs::read_to_string(report)
.map_err(|error| format!("read causal batch report {}: {error}", report.display()))?;
if body.len() > 16 * 1024 * 1024 {
return Err("causal batch report exceeds 16 MiB safety limit".to_string());
}
let mut rows = body.lines();
if rows.next() != Some(CAUSAL_BATCH_HEADER) {
return Err("invalid causal batch report header".to_string());
}
let model = parse_aag(input)?;
let input_sha256 = sha256_file(input)?;
let mut validated = 0usize;
let mut witness_cache = BTreeMap::<(usize, usize, usize), Vec<bool>>::new();
let mut report_horizon = None;
let mut seen = BTreeSet::new();
for (row_index, row) in rows.enumerate() {
let fields = row.split(',').collect::<Vec<_>>();
if fields.len() != 25 {
return Err(format!(
"causal batch row {} has {} fields; expected 25",
row_index + 2,
fields.len()
));
}
if fields[0] != CAUSAL_BATCH_SCHEMA_VERSION.to_string() || fields[1] != input_sha256 {
return Err(format!(
"causal batch row {} has wrong schema or input digest",
row_index + 2
));
}
if fields[22..] != ["true", "true", "ok"] {
return Err(format!(
"causal batch row {} is not a valid agreeing result",
row_index + 2
));
}
let horizon = parse_causal_batch_usize(fields[2], "horizon")?;
if report_horizon
.replace(horizon)
.is_some_and(|value| value != horizon)
{
return Err("causal batch report mixes horizons".to_string());
}
let bad_frame = parse_causal_batch_usize(fields[3], "bad frame")?;
let bad_output = parse_causal_batch_usize(fields[4], "bad output")?;
if bad_frame > horizon {
return Err("causal batch bad frame exceeds horizon".to_string());
}
let encoding = aag_bmc_encoding(&model, horizon)?;
let query = encoding
.queries
.iter()
.find(|query| query.frame == bad_frame && query.output == bad_output)
.ok_or_else(|| "causal batch target does not exist in encoded model".to_string())?;
if !witness_cache.contains_key(&(horizon, bad_frame, bad_output)) {
let mut target_solver = Solver::new();
add_to_varisat(&mut target_solver, &encoding.clauses);
for candidate in &encoding.queries {
if let Some(witness) = causal_query_witness(
&mut target_solver,
encoding.variables,
candidate.assumption,
)? {
witness_cache.insert((horizon, candidate.frame, candidate.output), witness);
}
}
}
let witness = witness_cache
.get(&(horizon, bad_frame, bad_output))
.ok_or_else(|| "causal batch target is not reachable".to_string())?;
let fixed = model
.latches
.iter()
.filter(|latch| latch.initial.is_none())
.map(|latch| {
let variable = latch.current / 2 - 1;
(variable, witness[variable])
})
.collect::<Vec<_>>();
let events = match fields[5] {
"segments" => causal_events_from_witness(&model, witness, bad_frame),
"points" => causal_point_events(&model, witness, bad_frame),
"dyadic" => causal_dyadic_events(&model, witness, bad_frame),
_ => return Err("unknown causal batch vocabulary".to_string()),
};
if !seen.insert((bad_frame, bad_output, fields[5].to_string())) {
return Err(
"causal batch report contains a duplicate target/vocabulary row".to_string(),
);
}
if parse_causal_batch_usize(fields[6], "candidate count")? != events.len() {
return Err("causal batch candidate count mismatch".to_string());
}
let encoded_causes = if fields[8].is_empty() {
Vec::new()
} else {
fields[8].split('|').collect::<Vec<_>>()
};
if parse_causal_batch_usize(fields[7], "cause count")? != encoded_causes.len() {
return Err("causal batch cause count mismatch".to_string());
}
for encoded in encoded_causes {
let mut active = vec![false; events.len()];
if encoded != "-" {
for token in encoded.split(';') {
let index = parse_causal_batch_usize(token, "cause index")?;
let slot = active
.get_mut(index)
.ok_or_else(|| "causal batch cause index is out of range".to_string())?;
if *slot {
return Err("causal batch cause contains duplicate index".to_string());
}
*slot = true;
}
}
let mut oracle = |selection: &[bool]| -> Result<bool, String> {
let mut assumptions =
causal_assumptions(&model, encoding.variables, &fixed, &events, selection)?;
if add_causal_counterfactual_assumption(&mut assumptions, query.assumption)? {
return Ok(true);
}
Ok(!solve_causal_fresh(&encoding.clauses, &assumptions)?)
};
validate_causal_selection(&active, &mut oracle)?;
}
validated += 1;
}
if validated == 0 {
return Err("causal batch report contains no data rows".to_string());
}
let horizon = report_horizon.unwrap();
let encoding = aag_bmc_encoding(&model, horizon)?;
let mut expected = BTreeSet::new();
let mut target_solver = Solver::new();
add_to_varisat(&mut target_solver, &encoding.clauses);
for query in &encoding.queries {
if causal_query_witness(&mut target_solver, encoding.variables, query.assumption)?.is_some()
{
for vocabulary in ["segments", "points", "dyadic"] {
expected.insert((query.frame, query.output, vocabulary.to_string()));
}
}
}
if seen != expected {
return Err(
"causal batch report does not cover every reachable target/vocabulary".to_string(),
);
}
println!(
"causal-batch-verify status=VALID rows={validated} report={}",
report.display()
);
Ok(())
}
fn benchmark_aiger_causal_strategies(
input: &Path,
requested_horizon: usize,
max_bound_bits: usize,
output: &Path,
) -> Result<(), String> {
if output.exists() {
return Err(format!(
"causal comparison output already exists: {}",
output.display()
));
}
let model = parse_aag(input)?;
if model.inputs.is_empty() {
return Err("causal comparison requires at least one primary input".to_string());
}
let CausalProblem {
encoding,
run,
clauses,
fixed,
} = causal_base_formula(&model, requested_horizon)?;
let query_index = run.first_sat_query.unwrap();
let query = &encoding.queries[query_index];
let witness = run.first_witness.as_ref().unwrap();
let events = causal_events_from_witness(&model, witness, query.frame);
if events.is_empty() || events.len() > CAUSAL_MAX_EVENTS {
return Err(format!(
"causal comparison event count {} is outside supported range 1..={CAUSAL_MAX_EVENTS}",
events.len()
));
}
// Across both strategies: fresh discovery/validation plus identical replay
// through persistent CDCL and CQ. The bound deliberately assumes CQ is used.
let maximum_backend_queries = events
.len()
.checked_mul(15)
.and_then(|value| value.checked_add(12))
.ok_or_else(|| "causal comparison query count overflow".to_string())?;
ensure_causal_query_work(encoding.variables, clauses.len(), maximum_backend_queries)?;
let cq_prepare_start = Instant::now();
let mut cq_bound_bits = None;
let mut compiled = None;
if encoding.variables <= CAUSAL_CQ_MAX_VARIABLES && clauses.len() <= CAUSAL_CQ_MAX_CLAUSES {
let order = min_fill_order(encoding.variables, &clauses);
let bound = continuation_frontier_bound_bits(encoding.variables, &clauses, &order);
cq_bound_bits = Some(bound);
if bound <= max_bound_bits {
compiled = Some(compile_continuation(&clauses, &order));
}
}
let cq_prepare_ns = cq_prepare_start.elapsed().as_nanos();
let cq_peak_classes = compiled.as_ref().map(|value| value.peak_classes);
let input_sha256 = sha256_file(input)?;
let per_strategy_limit = events
.len()
.checked_mul(3)
.and_then(|value| value.checked_add(2))
.ok_or_else(|| "causal strategy query limit overflow".to_string())?;
let mut results = Vec::new();
for strategy in ["deletion", "quickxplain"] {
let mut transcript = Vec::new();
let mut fresh_total_ns = 0u128;
let query_count = std::cell::Cell::new(0usize);
let (active, search_queries, validation_queries) = {
let mut oracle = |active: &[bool]| -> Result<bool, String> {
if query_count.get() >= per_strategy_limit {
return Err(format!(
"causal strategy `{strategy}` exceeded query limit {per_strategy_limit}"
));
}
let assumptions =
causal_assumptions(&model, encoding.variables, &fixed, &events, active)?;
let start = Instant::now();
let sat = solve_causal_fresh(&clauses, &assumptions)?;
fresh_total_ns += start.elapsed().as_nanos();
let unsat = !sat;
transcript.push(CausalQueryRecord {
active: active.to_vec(),
unsat,
});
query_count.set(query_count.get() + 1);
Ok(unsat)
};
let active = match strategy {
"deletion" => {
let mut active = vec![true; events.len()];
if !oracle(&active)? {
return Err(
"complete counterexample observations do not force the target failure"
.to_string(),
);
}
for index in 0..active.len() {
active[index] = false;
if !oracle(&active)? {
active[index] = true;
}
}
active
}
"quickxplain" => quickxplain_causal_events(events.len(), &mut oracle)?,
_ => unreachable!(),
};
let search_queries = query_count.get();
validate_causal_selection(&active, &mut oracle)?;
let validation_queries = query_count.get() - search_queries;
(active, search_queries, validation_queries)
};
results.push(CausalStrategyResult {
name: strategy,
active,
search_queries,
validation_queries,
transcript,
fresh_total_ns,
});
}
let mut lines = vec![CAUSAL_STRATEGY_HEADER.to_string()];
for result in results {
let persistent_setup_start = Instant::now();
let mut persistent = Solver::new();
add_to_varisat(&mut persistent, &clauses);
let persistent_setup_ns = persistent_setup_start.elapsed().as_nanos();
let mut persistent_query_ns = 0u128;
let mut cq_query_ns = 0u128;
let mut scratch = compiled.as_ref().map(ContinuationScratch::new);
for record in &result.transcript {
let assumptions =
causal_assumptions(&model, encoding.variables, &fixed, &events, &record.active)?;
let persistent_start = Instant::now();
let persistent_unsat = !solve_causal_persistent(&mut persistent, &assumptions)?;
persistent_query_ns += persistent_start.elapsed().as_nanos();
if persistent_unsat != record.unsat {
return Err(format!(
"persistent CDCL disagrees with fresh CDCL for `{}`",
result.name
));
}
if let (Some(compiled), Some(scratch)) = (compiled.as_ref(), scratch.as_mut()) {
let cq_start = Instant::now();
let cq_unsat = query_continuation(compiled, &assumptions, scratch).is_none();
cq_query_ns += cq_start.elapsed().as_nanos();
if cq_unsat != record.unsat {
return Err(format!(
"CQ disagrees with fresh CDCL for `{}`",
result.name
));
}
}
}
let persistent_total_ns = persistent_setup_ns.saturating_add(persistent_query_ns);
let persistent_total_speedup =
result.fresh_total_ns as f64 / persistent_total_ns.max(1) as f64;
let cq_query_speedup = if compiled.is_some() {
persistent_query_ns as f64 / cq_query_ns.max(1) as f64
} else {
0.0
};
let cq_total_speedup = if compiled.is_some() {
persistent_total_ns as f64 / cq_prepare_ns.saturating_add(cq_query_ns).max(1) as f64
} else {
0.0
};
let cause_indices = result
.active
.iter()
.enumerate()
.filter_map(|(index, active)| active.then_some(index.to_string()))
.collect::<Vec<_>>()
.join(";");
let unique_queries = result
.transcript
.iter()
.map(|record| record.active.clone())
.collect::<BTreeSet<_>>()
.len();
lines.push(format!(
"1,{input_sha256},{requested_horizon},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{cq_prepare_ns},{cq_query_ns},{persistent_setup_ns},{persistent_query_ns},{},{persistent_total_speedup:.6},{cq_query_speedup:.6},{cq_total_speedup:.6},true,true,ok",
query.frame,
query.output,
result.name,
events.len(),
result.active.iter().filter(|active| **active).count(),
cause_indices,
causal_selection_fingerprint(&events, &result.active)?,
result.search_queries,
result.validation_queries,
result.transcript.len(),
unique_queries,
compiled.is_some(),
cq_bound_bits.map_or_else(|| "none".to_string(), |value| value.to_string()),
cq_peak_classes.map_or_else(|| "none".to_string(), |value| value.to_string()),
result.fresh_total_ns,
));
}
publish_causal_comparison(output, (lines.join("\n") + "\n").as_bytes())?;
println!(
"causal-strategy-comparison status=VALID target={}@{} candidates={} cq_admitted={} output={}",
model.output_names[query.output],
query.frame,
events.len(),
compiled.is_some(),
output.display()
);
Ok(())
}
fn report_csv_field(value: &str) -> String {
if value
.chars()
.any(|character| matches!(character, ',' | '"' | '\n' | '\r'))
{
format!("\"{}\"", value.replace('"', "\"\""))
} else {
value.to_string()
}
}
fn append_named_aag_trace(
lines: &mut Vec<String>,
model: &AagModel,
witness: &[bool],
bad_frame: usize,
) {
let mut header = vec!["named_frame".to_string()];
header.extend(model.latch_names.iter().map(|name| report_csv_field(name)));
header.extend(model.input_names.iter().map(|name| report_csv_field(name)));
lines.push(header.join(","));
for frame in 0..=bad_frame {
let offset = frame * model.max_variable;
let mut row = vec![frame.to_string()];
row.extend(
model
.latches
.iter()
.map(|latch| usize::from(witness[offset + latch.current / 2 - 1]).to_string()),
);
row.extend(
model
.inputs
.iter()
.map(|literal| usize::from(witness[offset + literal / 2 - 1]).to_string()),
);
lines.push(row.join(","));
}
}
#[allow(clippy::too_many_arguments)]
fn write_general_aiger_safety_result(
path: &Path,
input: &Path,
horizon: usize,
model: &AagModel,
encoding: &AagBmcEncoding,
run: &AagBmcRun,
gate_reason: &str,
constraints: &[AagInputConstraint],
) -> Result<(), String> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create AIGER result directory: {error}"))?;
}
let body = if let Some(index) = run.first_sat_query {
let query = &encoding.queries[index];
let witness = run
.first_witness
.as_ref()
.ok_or_else(|| "unsafe AIGER result is missing its witness".to_string())?;
let mut lines = vec![
"status=UNSAFE".to_string(),
format!("input={}", input.display()),
format!("horizon={horizon}"),
"backend=cdcl".to_string(),
format!("gate_reason={gate_reason}"),
format!("assumption_count={}", constraints.len()),
format!("bad_frame={}", query.frame),
format!("bad_output={}", query.output),
format!("bad_output_name={}", model.output_names[query.output]),
];
lines.extend(constraints.iter().enumerate().map(|(index, constraint)| {
format!(
"assumption_{index}={}={}",
constraint.name,
constraint.pattern.report()
)
}));
lines.push("frame,latch_bits_low_to_high,input_bits_low_to_high".to_string());
for frame in 0..=query.frame {
let latches = model
.latches
.iter()
.map(|latch| {
let variable = frame * model.max_variable + latch.current / 2 - 1;
if witness[variable] { '1' } else { '0' }
})
.collect::<String>();
let inputs = model
.inputs
.iter()
.map(|literal| {
let variable = frame * model.max_variable + literal / 2 - 1;
if witness[variable] { '1' } else { '0' }
})
.collect::<String>();
lines.push(format!("{frame},{latches},{inputs}"));
}
append_named_aag_trace(&mut lines, model, witness, query.frame);
lines.join("\n") + "\n"
} else {
let mut lines = vec![
"status=SAFE".to_string(),
format!("input={}", input.display()),
format!("horizon={horizon}"),
"backend=cdcl".to_string(),
format!("gate_reason={gate_reason}"),
format!("assumption_count={}", constraints.len()),
];
lines.extend(constraints.iter().enumerate().map(|(index, constraint)| {
format!(
"assumption_{index}={}={}",
constraint.name,
constraint.pattern.report()
)
}));
lines.join("\n") + "\n"
};
fs::write(path, body).map_err(|error| format!("write {}: {error}", path.display()))
}
#[allow(clippy::too_many_arguments)]
fn verify_general_aiger(
input: &Path,
model: &AagModel,
horizon: usize,
checkpoint: usize,
node_limit: usize,
output: &Path,
safety_result: &Path,
constraints: &[AagInputConstraint],
) -> Result<(), String> {
let gate_start = Instant::now();
let gate_reason = if model.inputs.is_empty() {
"aiger-width-limit"
} else {
"aiger-primary-inputs"
};
let gate_ns = gate_start.elapsed().as_nanos();
let encoding_start = Instant::now();
let encoding = aag_bmc_encoding_with_constraints(model, horizon, constraints)?;
let encoding_ns = encoding_start.elapsed().as_nanos();
let mut run = run_aag_bmc(model, horizon, &encoding)?;
let trace_search_start = Instant::now();
let earliest = if run.first_sat_query.is_some() {
Some(earliest_aag_counterexample(model, horizon, constraints)?)
} else {
None
};
run.query_ns += trace_search_start.elapsed().as_nanos();
let query_count = usize::from(!encoding.queries.is_empty());
let unsat_queries = query_count.saturating_sub(run.sat_queries);
let per_query = run.query_ns as f64 / query_count.max(1) as f64;
let amortized = per_query + (gate_ns + encoding_ns) as f64 / query_count.max(1) as f64;
let label = input
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("external.aag");
let density =
encoding.clauses.len() as f64 / horizon.max(1) as f64 / model.latches.len().max(1) as f64;
let assumptions_per_query = encoding
.queries
.iter()
.filter(|query| matches!(query.assumption, AagCnfLiteral::Variable(_)))
.count() as f64;
let mut file = create_cq_portfolio_output(output)?;
writeln!(file, "{label},{},{horizon},{},{},{query_count},{},{node_limit},cdcl,{gate_reason},{density:.3},0,{assumptions_per_query:.3},{gate_ns},{encoding_ns},0,0,{per_query:.3},{per_query:.3},1.000000,{:.6},{},{unsat_queries},true,{},ok", model.latches.len(), encoding.variables, encoding.clauses.len(), checkpoint.min(horizon.saturating_sub(1)), per_query / amortized.max(1.0), run.sat_queries, run.witnesses_valid)
.map_err(|error| format!("write general AIGER portfolio row: {error}"))?;
let (trace_encoding, trace_run) = earliest
.as_ref()
.map_or((&encoding, &run), |(encoding, run)| (encoding, run));
write_general_aiger_safety_result(
safety_result,
input,
horizon,
model,
trace_encoding,
trace_run,
gate_reason,
constraints,
)?;
if let Some(index) = trace_run.first_sat_query {
println!(
"AIGER safety status=UNSAFE bad_frame={} bad_output={} backend=cdcl reason={gate_reason} witness={}",
trace_encoding.queries[index].frame,
trace_encoding.queries[index].output,
safety_result.display()
);
} else {
println!(
"AIGER safety status=SAFE horizon={horizon} backend=cdcl reason={gate_reason} result={}",
safety_result.display()
);
}
Ok(())
}
fn verify_cq_aiger_with_constraints(
input: &Path,
horizon: usize,
checkpoint: usize,
node_limit: usize,
output: &Path,
safety_result: &Path,
constraints: &[AagInputConstraint],
) -> Result<(), String> {
let model = parse_aag(input)?;
if !model.inputs.is_empty() || model.latches.len() > 9 {
return verify_general_aiger(
input,
&model,
horizon,
checkpoint,
node_limit,
output,
safety_result,
constraints,
);
}
if !constraints.is_empty() {
return Err(
"environment assumptions require an AIGER model with primary inputs".to_string(),
);
}
let (_, formula, initial) = aag_temporal_formula(&model, horizon)?;
if aag_bad_state_patterns(&model).is_empty() {
let label = input
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("external.aag");
let mut file = create_cq_portfolio_output(output)?;
let width = model.latches.len();
let variables = width * (horizon + 1);
let density = formula.len() as f64 / horizon as f64 / width as f64;
writeln!(file, "{label},{width},{horizon},{variables},{},0,{},{},static,constant-false-output,{density:.3},0,0.000,0,0,0,0,0.000,0.000,1.000000,1.000000,0,0,true,true,ok", formula.len(), checkpoint.min(horizon.saturating_sub(1)), node_limit)
.map_err(|error| format!("write constant-safe AIGER row: {error}"))?;
let run = CqPortfolioRun {
backend: "static",
first_sat_query: None,
first_witness: None,
};
write_aiger_safety_result(safety_result, input, horizon, width, &run, &[])?;
println!(
"AIGER safety status=SAFE horizon={horizon} backend=static result={}",
safety_result.display()
);
return Ok(());
}
let query_metadata = aag_property_query_space(&model, horizon)?;
let queries = query_metadata
.iter()
.map(|(_, _, assumptions)| assumptions.clone())
.collect::<Vec<_>>();
let label = input
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("external.aag");
let mut file = create_cq_portfolio_output(output)?;
let run = write_cq_portfolio_case(
&mut file,
label,
model.latches.len(),
horizon,
&formula,
&initial,
Some(queries),
query_metadata.len(),
checkpoint,
node_limit,
0,
)?;
write_aiger_safety_result(
safety_result,
input,
horizon,
model.latches.len(),
&run,
&query_metadata,
)?;
if let Some(index) = run.first_sat_query {
println!(
"AIGER safety status=UNSAFE bad_frame={} backend={} witness={}",
query_metadata[index].0,
run.backend,
safety_result.display()
);
} else {
println!(
"AIGER safety status=SAFE horizon={horizon} backend={} result={}",
run.backend,
safety_result.display()
);
}
Ok(())
}
fn verify_cq_aiger(
input: &Path,
horizon: usize,
checkpoint: usize,
node_limit: usize,
output: &Path,
safety_result: &Path,
) -> Result<(), String> {
verify_cq_aiger_with_constraints(
input,
horizon,
checkpoint,
node_limit,
output,
safety_result,
&[],
)
}
fn firmware_safety_gate_with_constraints(
input: &Path,
horizon: usize,
artifact_dir: &Path,
constraints: &[AagInputConstraint],
) -> Result<bool, String> {
fs::create_dir_all(artifact_dir)
.map_err(|error| format!("create firmware safety artifact directory: {error}"))?;
let metrics = artifact_dir.join("solver-metrics.csv");
let report = artifact_dir.join("safety-report.txt");
verify_cq_aiger_with_constraints(input, horizon, 10, 200_000, &metrics, &report, constraints)?;
let result = fs::read_to_string(&report)
.map_err(|error| format!("read firmware safety report: {error}"))?;
let safe = result.lines().next() == Some("status=SAFE");
if safe {
println!(
"::notice title=Firmware safety gate passed::No declared bad state is reachable through frame {horizon}"
);
println!(
"firmware-safety-gate status=SAFE report={}",
report.display()
);
} else if result.lines().next() == Some("status=UNSAFE") {
let bad_frame = result
.lines()
.find_map(|line| line.strip_prefix("bad_frame="))
.unwrap_or("unknown");
println!(
"::error title=Firmware safety gate failed::A declared bad state is reachable at frame {bad_frame}; download the firmware-safety-report artifact to replay it"
);
println!(
"firmware-safety-gate status=UNSAFE bad_frame={bad_frame} report={}",
report.display()
);
} else {
return Err(format!(
"firmware safety report has no recognized status: {}",
report.display()
));
}
Ok(safe)
}
fn firmware_safety_gate(input: &Path, horizon: usize, artifact_dir: &Path) -> Result<bool, String> {
firmware_safety_gate_with_constraints(input, horizon, artifact_dir, &[])
}
fn valid_verilog_identifier(value: &str) -> bool {
let mut characters = value.chars();
characters
.next()
.is_some_and(|character| character == '_' || character.is_ascii_alphabetic())
&& characters.all(|character| {
character == '_' || character == '$' || character.is_ascii_alphanumeric()
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RtlProjectConfig {
document: Vec<u8>,
version: usize,
top: String,
horizon: usize,
sources: Vec<PathBuf>,
include_dirs: Vec<PathBuf>,
parameters: Vec<(String, String)>,
clock: (String, String),
reset: RtlResetPolicy,
assumptions: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum RtlResetPolicy {
None,
Deasserted {
signal: String,
level: bool,
},
Startup {
signal: String,
active_low: bool,
asserted_frames: usize,
},
}
fn rtl_reset_policy_label(policy: &RtlResetPolicy) -> String {
match policy {
RtlResetPolicy::None => "none".to_string(),
RtlResetPolicy::Deasserted { signal, level } => format!(
"{signal}:deasserted-{}",
if *level { "high" } else { "low" }
),
RtlResetPolicy::Startup {
signal,
active_low,
asserted_frames,
} => format!(
"{signal}:active-{}:{asserted_frames}",
if *active_low { "low" } else { "high" }
),
}
}
#[derive(Debug, Clone)]
struct RtlIncludeSnapshot {
label: String,
files: Vec<(PathBuf, Vec<u8>)>,
}
#[derive(Debug, Clone)]
struct RtlBuildOptions {
parameters: Vec<(String, String)>,
clock: (String, String),
reset: RtlResetPolicy,
includes: Vec<RtlIncludeSnapshot>,
project_config: Vec<u8>,
}
fn path_within(base: &Path, relative: &Path, label: &str) -> Result<PathBuf, String> {
let resolved = fs::canonicalize(base.join(relative))
.map_err(|error| format!("resolve {label} {}: {error}", relative.display()))?;
if !resolved.starts_with(base) {
return Err(format!(
"{label} escapes the project directory: {}",
relative.display()
));
}
Ok(resolved)
}
fn load_rtl_build_options(
config_path: &Path,
config: &RtlProjectConfig,
) -> Result<(Vec<PathBuf>, Option<PathBuf>, RtlBuildOptions), String> {
let config_path = fs::canonicalize(config_path).map_err(|error| {
format!(
"resolve RTL project config {}: {error}",
config_path.display()
)
})?;
let base = config_path
.parent()
.ok_or_else(|| "RTL project config has no parent directory".to_string())?;
let sources = config
.sources
.iter()
.map(|path| path_within(base, path, "RTL source"))
.collect::<Result<Vec<_>, _>>()?;
let assumptions = config
.assumptions
.as_ref()
.map(|path| path_within(base, path, "assumptions file"))
.transpose()?;
let mut total_files = 0usize;
let mut total_bytes = 0u64;
let mut includes = Vec::new();
for (index, relative) in config.include_dirs.iter().enumerate() {
let directory = path_within(base, relative, "include directory")?;
if !directory.is_dir() {
return Err(format!(
"include path is not a directory: {}",
relative.display()
));
}
let mut entries = fs::read_dir(&directory)
.map_err(|error| format!("read include directory {}: {error}", relative.display()))?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("read include entry: {error}"))?;
entries.sort_by_key(|entry| entry.file_name());
let mut files = Vec::new();
for entry in entries {
let kind = entry
.file_type()
.map_err(|error| format!("inspect include entry: {error}"))?;
if !kind.is_file() {
return Err(format!(
"include directories may contain only regular files: {}",
entry.path().display()
));
}
let bytes = fs::read(entry.path()).map_err(|error| {
format!("read include file {}: {error}", entry.path().display())
})?;
if bytes.len() > 1024 * 1024 {
return Err(format!(
"include file exceeds 1 MiB safety limit: {}",
entry.path().display()
));
}
total_files += 1;
total_bytes = total_bytes
.checked_add(bytes.len() as u64)
.ok_or_else(|| "include byte count overflow".to_string())?;
if total_files > 256 || total_bytes > 10 * 1024 * 1024 {
return Err("include snapshot exceeds 256 files or 10 MiB safety limit".to_string());
}
files.push((PathBuf::from(entry.file_name()), bytes));
}
includes.push(RtlIncludeSnapshot {
label: format!("include-{index:04}"),
files,
});
}
Ok((
sources,
assumptions,
RtlBuildOptions {
parameters: config.parameters.clone(),
clock: config.clock.clone(),
reset: config.reset.clone(),
includes,
project_config: config.document.clone(),
},
))
}
fn safe_project_relative_path(value: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(value);
if value.is_empty()
|| path.is_absolute()
|| path
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(format!(
"project path must be a non-empty relative path without traversal: `{value}`"
));
}
Ok(path)
}
fn parse_rtl_project_config(path: &Path) -> Result<RtlProjectConfig, String> {
let bytes = fs::read(path)
.map_err(|error| format!("read RTL project config {}: {error}", path.display()))?;
if bytes.len() > 65_536 {
return Err("RTL project config exceeds safety limit 65536 bytes".to_string());
}
let body = std::str::from_utf8(&bytes)
.map_err(|_| "RTL project config must be valid UTF-8".to_string())?;
let mut scalar = BTreeMap::new();
let mut sources = Vec::new();
let mut include_dirs = Vec::new();
let mut parameters = Vec::new();
for (index, raw) in body.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, value) = line
.split_once('=')
.ok_or_else(|| format!("invalid RTL project config line {}", index + 1))?;
let key = key.trim();
let value = value.trim();
match key {
"source" => sources.push(safe_project_relative_path(value)?),
"include_dir" => include_dirs.push(safe_project_relative_path(value)?),
"parameter" => {
let (name, setting) = value.split_once(':').ok_or_else(|| {
format!(
"invalid parameter at line {}: expected NAME:VALUE",
index + 1
)
})?;
if !valid_verilog_identifier(name)
|| setting.is_empty()
|| !setting
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '\''))
{
return Err(format!("invalid parameter at line {}", index + 1));
}
parameters.push((name.to_string(), setting.to_string()));
}
"project_version" | "top" | "horizon" | "clock" | "reset" | "assumptions" => {
if value.is_empty() || scalar.insert(key.to_string(), value.to_string()).is_some() {
return Err(format!("duplicate or empty RTL project field `{key}`"));
}
}
_ => return Err(format!("unknown RTL project field `{key}`")),
}
}
let version = scalar
.get("project_version")
.ok_or_else(|| "RTL project config is missing `project_version`".to_string())?
.parse::<usize>()
.map_err(|_| "RTL project version must be an integer".to_string())?;
if !matches!(version, 1 | 2) {
return Err("RTL project config supports project_version=1 or 2".to_string());
}
let top = scalar
.remove("top")
.ok_or_else(|| "RTL project config is missing `top`".to_string())?;
if !valid_verilog_identifier(&top) {
return Err("RTL project top must be a simple Verilog identifier".to_string());
}
let horizon = scalar
.remove("horizon")
.ok_or_else(|| "RTL project config is missing `horizon`".to_string())?
.parse::<usize>()
.map_err(|_| "RTL project horizon must be an integer".to_string())?;
if horizon == 0 || horizon > 1_000_000 {
return Err("RTL project horizon must be between 1 and 1000000".to_string());
}
if sources.is_empty() || sources.len() > 64 {
return Err("RTL project must contain between 1 and 64 source files".to_string());
}
if include_dirs.len() > 16 || parameters.len() > 64 {
return Err("RTL project include or parameter count exceeds safety limit".to_string());
}
let clock = scalar
.remove("clock")
.ok_or_else(|| "RTL project config is missing `clock`".to_string())?;
let (clock_signal, clock_edge) = clock
.split_once(':')
.ok_or_else(|| "clock must be SIGNAL:posedge or SIGNAL:negedge".to_string())?;
if !valid_verilog_identifier(clock_signal) || !matches!(clock_edge, "posedge" | "negedge") {
return Err("clock must be SIGNAL:posedge or SIGNAL:negedge".to_string());
}
let reset = match scalar
.remove("reset")
.ok_or_else(|| "RTL project config is missing `reset`".to_string())?
.as_str()
{
"none" => RtlResetPolicy::None,
value => {
let parts = value.split(':').collect::<Vec<_>>();
let signal = parts.first().copied().unwrap_or_default();
if !valid_verilog_identifier(signal) {
return Err("reset signal is invalid".to_string());
}
match parts.as_slice() {
[_, state] => match *state {
"deasserted-low" => RtlResetPolicy::Deasserted {
signal: signal.to_string(),
level: false,
},
"deasserted-high" => RtlResetPolicy::Deasserted {
signal: signal.to_string(),
level: true,
},
_ => return Err("reset must be none, SIGNAL:deasserted-low/deasserted-high, or SIGNAL:active-low/active-high:ASSERTED_FRAMES".to_string()),
},
[_, active @ ("active-low" | "active-high"), frames] => {
if version < 2 {
return Err("startup reset sequences require project_version=2".to_string());
}
let asserted_frames = frames.parse::<usize>().map_err(|_| "reset asserted frame count must be an integer".to_string())?;
if asserted_frames == 0 || asserted_frames > horizon {
return Err("reset asserted frame count must be between 1 and the horizon".to_string());
}
RtlResetPolicy::Startup {
signal: signal.to_string(),
active_low: *active == "active-low",
asserted_frames,
}
}
_ => return Err("reset must be none, SIGNAL:deasserted-low/deasserted-high, or SIGNAL:active-low/active-high:ASSERTED_FRAMES".to_string()),
}
}
};
let assumptions = scalar
.remove("assumptions")
.map(|value| safe_project_relative_path(&value))
.transpose()?;
Ok(RtlProjectConfig {
document: bytes,
version,
top,
horizon,
sources,
include_dirs,
parameters,
clock: (clock_signal.to_string(), clock_edge.to_string()),
reset,
assumptions,
})
}
fn parse_environment_assumptions(
path: &Path,
) -> Result<(Vec<AagInputConstraint>, Vec<u8>), String> {
let metadata = fs::metadata(path).map_err(|error| {
format!(
"inspect environment assumptions {}: {error}",
path.display()
)
})?;
if !metadata.is_file() {
return Err(format!(
"environment assumptions are not a file: {}",
path.display()
));
}
if metadata.len() > 65_536 {
return Err("environment assumptions exceed safety limit 65536 bytes".to_string());
}
let bytes = fs::read(path)
.map_err(|error| format!("read environment assumptions {}: {error}", path.display()))?;
if bytes.len() > 65_536 {
return Err("environment assumptions exceed safety limit 65536 bytes".to_string());
}
let body = std::str::from_utf8(&bytes)
.map_err(|_| "environment assumptions must be valid UTF-8".to_string())?;
let mut constraints = Vec::new();
let mut seen = BTreeSet::new();
for (index, raw) in body.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (name, value) = line.split_once('=').ok_or_else(|| {
format!(
"invalid environment assumption at line {}: expected NAME=0 or NAME=1",
index + 1
)
})?;
let name = name.trim();
let value = value.trim();
if !valid_verilog_identifier(name) || !matches!(value, "0" | "1") {
return Err(format!(
"invalid environment assumption at line {}: expected NAME=0 or NAME=1",
index + 1
));
}
if !seen.insert(name.to_string()) {
return Err(format!("duplicate environment assumption `{name}`"));
}
constraints.push(AagInputConstraint {
name: name.to_string(),
pattern: AagInputConstraintPattern::Constant(value == "1"),
});
if constraints.len() > 256 {
return Err("environment assumptions exceed safety limit 256 entries".to_string());
}
}
if constraints.is_empty() {
return Err("environment assumptions file contains no assumptions".to_string());
}
Ok((constraints, bytes))
}
fn replace_file(source: &Path, destination: &Path) -> Result<(), String> {
if destination.exists() {
fs::remove_file(destination)
.map_err(|error| format!("remove {}: {error}", destination.display()))?;
}
fs::rename(source, destination).map_err(|error| {
format!(
"publish {} as {}: {error}",
source.display(),
destination.display()
)
})
}
fn configure_contained_process(
command: &mut Command,
memory_limit_bytes: u64,
file_limit_bytes: u64,
) -> Result<(), String> {
#[cfg(unix)]
{
#[cfg(not(target_os = "macos"))]
let memory_limit = libc::rlim_t::try_from(memory_limit_bytes).map_err(|_| {
"process memory limit is not representable on this platform".to_string()
})?;
#[cfg(target_os = "macos")]
let _ = memory_limit_bytes;
let file_limit = libc::rlim_t::try_from(file_limit_bytes)
.map_err(|_| "process file limit is not representable on this platform".to_string())?;
// SAFETY: pre_exec runs after fork and before exec. The closure only invokes
// async-signal-safe libc functions and constructs errors from errno on failure.
unsafe {
command.pre_exec(move || {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
#[cfg(not(target_os = "macos"))]
{
let memory = libc::rlimit {
rlim_cur: memory_limit,
rlim_max: memory_limit,
};
if libc::setrlimit(libc::RLIMIT_AS, &memory) == -1 {
return Err(std::io::Error::last_os_error());
}
}
let file = libc::rlimit {
rlim_cur: file_limit,
rlim_max: file_limit,
};
if libc::setrlimit(libc::RLIMIT_FSIZE, &file) == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
Ok(())
}
#[cfg(not(unix))]
{
let _ = (command, memory_limit_bytes, file_limit_bytes);
Err("contained synthesis currently requires Linux or macOS".to_string())
}
}
#[cfg(unix)]
fn kill_process_group(process_id: u32) -> Result<(), String> {
let process_group = i32::try_from(process_id)
.map_err(|_| "child process identifier exceeds platform range".to_string())?;
// SAFETY: a negative PID asks kill(2) to signal the process group created by setsid.
if unsafe { libc::kill(-process_group, libc::SIGKILL) } == -1 {
let error = std::io::Error::last_os_error();
if error.raw_os_error() != Some(libc::ESRCH) {
return Err(format!("terminate contained process group: {error}"));
}
}
Ok(())
}
fn wait_for_contained_process(
child: &mut Child,
timeout: std::time::Duration,
label: &str,
) -> Result<Option<ExitStatus>, String> {
let process_id = child.id();
let deadline = Instant::now() + timeout;
loop {
if let Some(status) = child
.try_wait()
.map_err(|error| format!("wait for {label}: {error}"))?
{
return Ok(Some(status));
}
if Instant::now() >= deadline {
#[cfg(unix)]
kill_process_group(process_id)?;
child
.wait()
.map_err(|error| format!("reap timed-out {label}: {error}"))?;
return Ok(None);
}
thread::sleep(std::time::Duration::from_millis(50));
}
}
fn is_rtl_source_snapshot(name: &str) -> bool {
name == "source.sv"
|| name
.strip_prefix("source-")
.and_then(|rest| rest.strip_suffix(".sv"))
.is_some_and(|index| index.len() == 4 && index.chars().all(|c| c.is_ascii_digit()))
}
fn is_rtl_include_snapshot(name: &str) -> bool {
name.strip_prefix("include-").is_some_and(|index| {
index.len() == 4 && index.chars().all(|character| character.is_ascii_digit())
})
}
fn source_revision() -> String {
env::var("GITHUB_SHA")
.ok()
.filter(|value| {
(7..=64).contains(&value.len())
&& value.chars().all(|character| character.is_ascii_hexdigit())
})
.unwrap_or_else(|| "unknown".to_string())
}
fn report_value(value: &str) -> String {
value
.replace('%', "%25")
.replace('\n', "%0A")
.replace('\r', "%0D")
}
fn sha256_file(path: &Path) -> Result<String, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect evidence file {}: {error}", path.display()))?;
if !metadata.file_type().is_file() {
return Err(format!(
"evidence path is not a regular file: {}",
path.display()
));
}
let mut file = fs::File::open(path)
.map_err(|error| format!("open evidence file {}: {error}", path.display()))?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 64 * 1024];
loop {
let read = std::io::Read::read(&mut file, &mut buffer)
.map_err(|error| format!("hash evidence file {}: {error}", path.display()))?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(hasher
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
fn collect_evidence_files(root: &Path, directory: &Path) -> Result<Vec<String>, String> {
let mut files = Vec::new();
for entry in fs::read_dir(directory)
.map_err(|error| format!("read evidence directory {}: {error}", directory.display()))?
{
let entry = entry.map_err(|error| format!("read evidence entry: {error}"))?;
let kind = entry
.file_type()
.map_err(|error| format!("inspect evidence entry: {error}"))?;
if kind.is_symlink() {
return Err(format!(
"evidence bundle contains a symlink: {}",
entry.path().display()
));
}
if kind.is_dir() {
files.extend(collect_evidence_files(root, &entry.path())?);
} else if kind.is_file() {
let path = entry.path();
let relative = path
.strip_prefix(root)
.map_err(|_| format!("evidence path escaped bundle: {}", path.display()))?;
let relative = relative
.to_str()
.ok_or_else(|| "evidence paths must be valid UTF-8".to_string())?;
if relative.contains('\n') || relative.contains('\r') {
return Err("evidence paths may not contain line breaks".to_string());
}
files.push(relative.to_string());
} else {
return Err(format!(
"evidence bundle contains a non-file entry: {}",
entry.path().display()
));
}
if files.len() > 4096 {
return Err("evidence bundle exceeds 4096 files".to_string());
}
}
files.sort();
Ok(files)
}
fn write_evidence_index(root: &Path) -> Result<String, String> {
let index = root.join("evidence.sha256");
let manifest = root.join("run-manifest.txt");
if index.exists() || manifest.exists() {
return Err("evidence index must be generated before manifest publication".to_string());
}
let files = collect_evidence_files(root, root)?;
let mut body = String::new();
for relative in files {
let digest = sha256_file(&root.join(&relative))?;
body.push_str(&format!("{digest} {relative}\n"));
}
if body.len() > 1_048_576 {
return Err("evidence index exceeds 1048576 bytes".to_string());
}
fs::write(&index, body).map_err(|error| format!("write evidence index: {error}"))?;
sha256_file(&index)
}
fn validate_evidence_index(root: &Path, expected_index_digest: &str) -> Result<(), String> {
if expected_index_digest.len() != 64
|| !expected_index_digest
.chars()
.all(|character| character.is_ascii_hexdigit())
{
return Err("evidence index SHA-256 is malformed".to_string());
}
let index = root.join("evidence.sha256");
let metadata =
fs::symlink_metadata(&index).map_err(|error| format!("inspect evidence index: {error}"))?;
if !metadata.file_type().is_file() {
return Err("evidence index is not a regular file".to_string());
}
if metadata.len() > 1_048_576 {
return Err("evidence index exceeds 1048576 bytes".to_string());
}
if sha256_file(&index)? != expected_index_digest {
return Err("evidence index SHA-256 disagrees with manifest".to_string());
}
let body =
fs::read_to_string(&index).map_err(|error| format!("read evidence index: {error}"))?;
let mut previous = None::<String>;
let mut count = 0usize;
let mut total_bytes = 0u64;
for (line_number, line) in body.lines().enumerate() {
let (digest, relative) = line
.split_once(" ")
.ok_or_else(|| format!("invalid evidence index line {}", line_number + 1))?;
if digest.len() != 64
|| !digest
.chars()
.all(|character| character.is_ascii_hexdigit())
{
return Err(format!(
"invalid evidence digest at line {}",
line_number + 1
));
}
let relative_path = Path::new(relative);
if relative.is_empty()
|| relative == "evidence.sha256"
|| relative == "run-manifest.txt"
|| relative_path.is_absolute()
|| relative_path
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(format!("invalid evidence path at line {}", line_number + 1));
}
if previous.as_deref().is_some_and(|value| value >= relative) {
return Err("evidence index paths must be unique and sorted".to_string());
}
let evidence_path = root.join(relative);
let evidence_metadata = fs::symlink_metadata(&evidence_path)
.map_err(|error| format!("inspect evidence `{relative}`: {error}"))?;
if !evidence_metadata.file_type().is_file() {
return Err(format!("evidence path is not a regular file: {relative}"));
}
if evidence_metadata.len() > YOSYS_FILE_LIMIT_BYTES {
return Err(format!(
"evidence file exceeds {YOSYS_FILE_LIMIT_BYTES} bytes: {relative}"
));
}
total_bytes = total_bytes
.checked_add(evidence_metadata.len())
.ok_or_else(|| "evidence byte count overflow".to_string())?;
if total_bytes > EVIDENCE_TOTAL_LIMIT_BYTES {
return Err(format!(
"evidence bundle exceeds {EVIDENCE_TOTAL_LIMIT_BYTES} indexed bytes"
));
}
if sha256_file(&evidence_path)? != digest {
return Err(format!("evidence SHA-256 mismatch for `{relative}`"));
}
previous = Some(relative.to_string());
count += 1;
if count > 4096 {
return Err("evidence index exceeds 4096 files".to_string());
}
}
if count == 0 {
return Err("evidence index contains no files".to_string());
}
Ok(())
}
fn read_rtl_manifest(path: &Path) -> Result<Vec<(String, String)>, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect RTL artifact manifest {}: {error}", path.display()))?;
if !metadata.is_file() || metadata.len() > 65_536 {
return Err(
"RTL artifact manifest must be a regular file no larger than 65536 bytes".to_string(),
);
}
let body = fs::read_to_string(path)
.map_err(|error| format!("read RTL artifact manifest {}: {error}", path.display()))?;
if body.len() > 65_536 {
return Err("RTL artifact manifest exceeds 65536 bytes".to_string());
}
let mut fields = Vec::new();
let mut seen = BTreeSet::new();
for (index, line) in body.lines().enumerate() {
let (key, value) = line
.split_once('=')
.ok_or_else(|| format!("invalid RTL artifact manifest line {}", index + 1))?;
if key.is_empty()
|| !key
.chars()
.all(|character| character == '_' || character.is_ascii_alphanumeric())
|| value.is_empty()
{
return Err(format!("invalid RTL artifact manifest line {}", index + 1));
}
if !seen.insert(key.to_string()) {
return Err(format!("duplicate RTL artifact manifest field `{key}`"));
}
fields.push((key.to_string(), value.to_string()));
}
Ok(fields)
}
fn rtl_manifest_value<'a>(fields: &'a [(String, String)], key: &str) -> Result<&'a str, String> {
fields
.iter()
.find_map(|(candidate, value)| (candidate == key).then_some(value.as_str()))
.ok_or_else(|| format!("missing RTL artifact manifest field `{key}`"))
}
fn validate_rtl_artifact_bundle(artifact_dir: &Path) -> Result<(), String> {
let root_metadata = fs::symlink_metadata(artifact_dir).map_err(|error| {
format!(
"inspect RTL artifact bundle {}: {error}",
artifact_dir.display()
)
})?;
if !root_metadata.file_type().is_dir() {
return Err(format!(
"RTL artifact bundle is not a directory: {}",
artifact_dir.display()
));
}
let fields = read_rtl_manifest(&artifact_dir.join("run-manifest.txt"))?;
if rtl_manifest_value(&fields, "schema_version")? != RTL_ARTIFACT_SCHEMA_VERSION.to_string() {
return Err(format!(
"unsupported RTL artifact schema; expected {}",
RTL_ARTIFACT_SCHEMA_VERSION
));
}
if rtl_manifest_value(&fields, "firmware_cli_version")?
!= FIRMWARE_CLI_CONTRACT_VERSION.to_string()
{
return Err(format!(
"unsupported firmware CLI contract; expected {}",
FIRMWARE_CLI_CONTRACT_VERSION
));
}
let status = rtl_manifest_value(&fields, "status")?;
if !matches!(status, "SAFE" | "UNSAFE") {
return Err("RTL artifact status must be SAFE or UNSAFE".to_string());
}
let source_count = rtl_manifest_value(&fields, "source_count")?
.parse::<usize>()
.map_err(|_| "invalid RTL artifact source_count".to_string())?;
if !(1..=64).contains(&source_count) {
return Err("RTL artifact source_count must be between 1 and 64".to_string());
}
let ordered_source = (0..source_count)
.map(|index| rtl_manifest_value(&fields, &format!("source_{index}")))
.collect::<Result<Vec<_>, _>>()?
.join(";");
if rtl_manifest_value(&fields, "source")? != ordered_source {
return Err("RTL artifact source aggregate disagrees with ordered sources".to_string());
}
let revision = rtl_manifest_value(&fields, "source_revision")?;
if revision != "unknown"
&& (!(7..=64).contains(&revision.len())
|| !revision
.chars()
.all(|character| character.is_ascii_hexdigit()))
{
return Err("RTL artifact source_revision is malformed".to_string());
}
let assumption_count = rtl_manifest_value(&fields, "assumption_count")?
.parse::<usize>()
.map_err(|_| "invalid RTL artifact assumption_count".to_string())?;
if assumption_count > 256 {
return Err("RTL artifact assumption_count exceeds 256".to_string());
}
let source_bytes = rtl_manifest_value(&fields, "source_bytes")?
.parse::<u64>()
.map_err(|_| "invalid RTL artifact numeric field `source_bytes`".to_string())?;
let horizon = rtl_manifest_value(&fields, "horizon")?
.parse::<u64>()
.map_err(|_| "invalid RTL artifact numeric field `horizon`".to_string())?;
if horizon == 0 {
return Err("RTL artifact horizon must be at least one".to_string());
}
for key in [
"include_dir_count",
"include_file_count",
"include_bytes",
"parameter_count",
"synthesis_timeout_seconds",
"synthesis_memory_limit_bytes",
"synthesis_file_limit_bytes",
] {
rtl_manifest_value(&fields, key)?
.parse::<u64>()
.map_err(|_| format!("invalid RTL artifact numeric field `{key}`"))?;
}
if rtl_manifest_value(&fields, "process_group_timeout_kill")? != "true" {
return Err("RTL artifact process-group containment is not asserted".to_string());
}
if !valid_verilog_identifier(rtl_manifest_value(&fields, "top")?) {
return Err("RTL artifact top is not a simple Verilog identifier".to_string());
}
let platform = rtl_manifest_value(&fields, "containment_platform")?;
let memory_kind = rtl_manifest_value(&fields, "synthesis_memory_limit_kind")?;
let memory_bytes = rtl_manifest_value(&fields, "synthesis_memory_limit_bytes")?;
match platform {
"linux"
if memory_kind == "address-space"
&& memory_bytes == YOSYS_MEMORY_LIMIT_BYTES.to_string() => {}
"macos" if memory_kind == "unavailable" && memory_bytes == "0" => {}
_ => return Err("RTL artifact containment fields are inconsistent".to_string()),
}
if rtl_manifest_value(&fields, "synthesis_timeout_seconds")? != "120"
|| rtl_manifest_value(&fields, "synthesis_file_limit_bytes")?
!= YOSYS_FILE_LIMIT_BYTES.to_string()
{
return Err("RTL artifact synthesis limits do not match schema v4".to_string());
}
if !rtl_manifest_value(&fields, "yosys")?.starts_with("Yosys ") {
return Err("RTL artifact Yosys version is malformed".to_string());
}
let mut expected_keys = vec![
"status".to_string(),
"schema_version".to_string(),
"firmware_cli_version".to_string(),
"source".to_string(),
"source_count".to_string(),
];
expected_keys.extend((0..source_count).map(|index| format!("source_{index}")));
expected_keys.extend(
[
"source_revision",
"source_bytes",
"assumption_source",
"assumption_count",
"project_config",
"include_dir_count",
"include_file_count",
"include_bytes",
"parameter_count",
"parameters",
"clock_policy",
"reset_policy",
"top",
"horizon",
"synthesis_timeout_seconds",
"containment_platform",
"process_group_timeout_kill",
"synthesis_memory_limit_kind",
"synthesis_memory_limit_bytes",
"synthesis_file_limit_bytes",
"yosys",
"evidence_digest_algorithm",
"evidence_index",
"evidence_index_sha256",
]
.into_iter()
.map(str::to_string),
);
let actual_keys = fields
.iter()
.map(|(key, _)| key.clone())
.collect::<Vec<_>>();
if actual_keys != expected_keys {
return Err("RTL artifact manifest fields or ordering do not match schema v4".to_string());
}
if rtl_manifest_value(&fields, "evidence_digest_algorithm")? != "sha256"
|| rtl_manifest_value(&fields, "evidence_index")? != "evidence.sha256"
{
return Err("RTL artifact evidence digest contract is invalid".to_string());
}
validate_evidence_index(
artifact_dir,
rtl_manifest_value(&fields, "evidence_index_sha256")?,
)?;
let expected_sources = if source_count == 1 {
vec!["source.sv".to_string()]
} else {
(0..source_count)
.map(|index| format!("source-{index:04}.sv"))
.collect::<Vec<_>>()
};
for name in [
"model.aag",
"signal.map",
"synthesis.ys",
"yosys.log",
"yosys-errors.log",
"solver-metrics.csv",
"safety-report.txt",
]
.into_iter()
.chain(expected_sources.iter().map(String::as_str))
{
if !artifact_dir.join(name).is_file() {
return Err(format!("RTL artifact bundle is missing `{name}`"));
}
}
let snapshot_bytes = expected_sources.iter().try_fold(0u64, |total, name| {
let bytes = fs::metadata(artifact_dir.join(name))
.map_err(|error| format!("inspect RTL source snapshot `{name}`: {error}"))?
.len();
total
.checked_add(bytes)
.ok_or_else(|| "RTL source snapshot byte count overflow".to_string())
})?;
if snapshot_bytes != source_bytes {
return Err("RTL artifact source_bytes disagrees with snapshots".to_string());
}
let assumptions_exist = artifact_dir.join("assumptions.txt").is_file();
if assumptions_exist != (assumption_count > 0) {
return Err(
"RTL artifact assumptions snapshot does not match assumption_count".to_string(),
);
}
let assumption_source = rtl_manifest_value(&fields, "assumption_source")?;
if (assumption_source == "none") != (assumption_count == 0) {
return Err("RTL artifact assumption_source disagrees with assumption_count".to_string());
}
if assumptions_exist {
let (constraints, _) =
parse_environment_assumptions(&artifact_dir.join("assumptions.txt"))?;
if constraints.len() != assumption_count {
return Err("RTL artifact assumption_count disagrees with snapshot".to_string());
}
}
let project_config = rtl_manifest_value(&fields, "project_config")?;
let include_dir_count = rtl_manifest_value(&fields, "include_dir_count")?
.parse::<usize>()
.unwrap();
let include_file_count = rtl_manifest_value(&fields, "include_file_count")?
.parse::<usize>()
.unwrap();
let include_bytes = rtl_manifest_value(&fields, "include_bytes")?
.parse::<u64>()
.unwrap();
let parameter_count = rtl_manifest_value(&fields, "parameter_count")?
.parse::<usize>()
.unwrap();
if (project_config == "none")
!= (include_dir_count == 0
&& parameter_count == 0
&& rtl_manifest_value(&fields, "clock_policy")? == "unspecified"
&& rtl_manifest_value(&fields, "reset_policy")? == "unspecified")
{
return Err("RTL project configuration fields are inconsistent".to_string());
}
if project_config != "none" && project_config != "cq-project.conf" {
return Err("RTL artifact project_config is invalid".to_string());
}
if (project_config == "cq-project.conf") != artifact_dir.join("cq-project.conf").is_file() {
return Err("RTL project config snapshot is missing or unexpected".to_string());
}
if project_config == "cq-project.conf" {
let config = parse_rtl_project_config(&artifact_dir.join("cq-project.conf"))?;
let config_parameters = if config.parameters.is_empty() {
"none".to_string()
} else {
config
.parameters
.iter()
.map(|(name, value)| format!("{name}:{value}"))
.collect::<Vec<_>>()
.join(";")
};
let config_reset = rtl_reset_policy_label(&config.reset);
if config.top != rtl_manifest_value(&fields, "top")?
|| config.horizon.to_string() != rtl_manifest_value(&fields, "horizon")?
|| config.sources.len() != source_count
|| config.include_dirs.len() != include_dir_count
|| config.parameters.len() != parameter_count
|| config_parameters != rtl_manifest_value(&fields, "parameters")?
|| format!("{}:{}", config.clock.0, config.clock.1)
!= rtl_manifest_value(&fields, "clock_policy")?
|| config_reset != rtl_manifest_value(&fields, "reset_policy")?
|| config.assumptions.is_some() != (assumption_count > 0)
{
return Err("RTL project config snapshot disagrees with manifest".to_string());
}
}
let mut actual_include_files = 0usize;
let mut actual_include_bytes = 0u64;
for index in 0..include_dir_count {
let directory = artifact_dir.join(format!("include-{index:04}"));
if !directory.is_dir() {
return Err("RTL include snapshot directory is missing".to_string());
}
for entry in
fs::read_dir(directory).map_err(|error| format!("read include snapshot: {error}"))?
{
let entry = entry.map_err(|error| format!("read include snapshot entry: {error}"))?;
if !entry
.file_type()
.map_err(|error| format!("inspect include snapshot: {error}"))?
.is_file()
{
return Err("RTL include snapshot contains a non-file".to_string());
}
actual_include_files += 1;
actual_include_bytes += entry
.metadata()
.map_err(|error| format!("inspect include snapshot: {error}"))?
.len();
}
}
if actual_include_files != include_file_count || actual_include_bytes != include_bytes {
return Err("RTL include snapshot counts disagree with manifest".to_string());
}
let actual_include_dirs = fs::read_dir(artifact_dir)
.map_err(|error| format!("inspect RTL artifact includes: {error}"))?
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.file_type().is_ok_and(|kind| kind.is_dir())
&& is_rtl_include_snapshot(&entry.file_name().to_string_lossy())
})
.count();
if actual_include_dirs != include_dir_count {
return Err("RTL include snapshot directories disagree with manifest".to_string());
}
let parameters = rtl_manifest_value(&fields, "parameters")?;
if (parameters == "none") != (parameter_count == 0)
|| (parameter_count > 0 && parameters.split(';').count() != parameter_count)
{
return Err("RTL parameter count disagrees with manifest".to_string());
}
let mut actual_sources = Vec::new();
for entry in fs::read_dir(artifact_dir)
.map_err(|error| format!("inspect RTL artifact bundle: {error}"))?
{
let entry = entry.map_err(|error| format!("inspect RTL artifact entry: {error}"))?;
let name = entry.file_name().to_string_lossy().to_string();
if is_rtl_source_snapshot(&name) {
actual_sources.push(name);
}
}
actual_sources.sort();
if actual_sources != expected_sources {
return Err("RTL artifact source snapshots do not match source_count".to_string());
}
let report = fs::File::open(artifact_dir.join("safety-report.txt"))
.map_err(|error| format!("open RTL safety report: {error}"))?;
let mut lines = BufReader::new(report).lines();
if lines
.next()
.transpose()
.map_err(|error| format!("read RTL safety status: {error}"))?
.as_deref()
!= Some(&format!("status={status}"))
{
return Err("RTL safety report status disagrees with manifest".to_string());
}
if lines
.next()
.transpose()
.map_err(|error| format!("read RTL safety schema: {error}"))?
.as_deref()
!= Some(&format!("schema_version={RTL_ARTIFACT_SCHEMA_VERSION}"))
{
return Err("RTL safety report schema disagrees with manifest".to_string());
}
if lines
.next()
.transpose()
.map_err(|error| format!("read RTL safety CLI contract: {error}"))?
.as_deref()
!= Some(&format!(
"firmware_cli_version={FIRMWARE_CLI_CONTRACT_VERSION}"
))
{
return Err("RTL safety report CLI contract disagrees with manifest".to_string());
}
println!(
"firmware-artifact-validate status=VALID schema={} result={status} bundle={}",
RTL_ARTIFACT_SCHEMA_VERSION,
artifact_dir.display()
);
Ok(())
}
fn annotate_rtl_safety_report(
report: &Path,
source: &str,
top: &str,
yosys_version: &str,
build_options: Option<&RtlBuildOptions>,
) -> Result<(), String> {
let body =
fs::read_to_string(report).map_err(|error| format!("read RTL safety report: {error}"))?;
let revision = source_revision();
let mut lines = body.lines();
let status = lines
.next()
.ok_or_else(|| "RTL safety report is empty".to_string())?;
let mut annotated = vec![
status.to_string(),
format!("schema_version={RTL_ARTIFACT_SCHEMA_VERSION}"),
format!("firmware_cli_version={FIRMWARE_CLI_CONTRACT_VERSION}"),
format!("source={}", report_value(source)),
format!("source_revision={revision}"),
format!("top={top}"),
format!(
"clock_policy={}",
build_options.map_or_else(
|| "unspecified".to_string(),
|options| format!("{}:{}", options.clock.0, options.clock.1)
)
),
format!(
"reset_policy={}",
build_options.map_or_else(
|| "unspecified".to_string(),
|options| rtl_reset_policy_label(&options.reset)
)
),
format!("yosys={yosys_version}"),
format!("containment_platform={}", std::env::consts::OS),
"process_group_timeout_kill=true".to_string(),
format!(
"synthesis_memory_limit_kind={}",
synthesis_memory_limit_kind()
),
format!(
"synthesis_memory_limit_bytes={}",
synthesis_memory_limit_bytes()
),
format!("synthesis_file_limit_bytes={YOSYS_FILE_LIMIT_BYTES}"),
"generated_model=model.aag".to_string(),
];
annotated.extend(
lines
.filter(|line| !line.starts_with("input="))
.map(str::to_string),
);
fs::write(report, annotated.join("\n") + "\n")
.map_err(|error| format!("write annotated RTL safety report: {error}"))
}
fn firmware_rtl_project_safety_gate_with_assumptions(
inputs: &[PathBuf],
top: &str,
horizon: usize,
artifact_dir: &Path,
assumptions_path: Option<&Path>,
build_options: Option<&RtlBuildOptions>,
) -> Result<bool, String> {
if !valid_verilog_identifier(top) {
return Err("RTL top must be a simple Verilog identifier".to_string());
}
if inputs.is_empty() || inputs.len() > 64 {
return Err("RTL project must contain between 1 and 64 source files".to_string());
}
let assumption_document = assumptions_path
.map(parse_environment_assumptions)
.transpose()?;
let mut effective_assumptions = assumption_document
.as_ref()
.map_or_else(Vec::new, |(constraints, _)| constraints.clone());
let reset_constraint = build_options.and_then(|options| match &options.reset {
RtlResetPolicy::None => None,
RtlResetPolicy::Deasserted { signal, level } => Some(AagInputConstraint {
name: signal.clone(),
pattern: AagInputConstraintPattern::Constant(*level),
}),
RtlResetPolicy::Startup {
signal,
active_low,
asserted_frames,
} => Some(AagInputConstraint {
name: signal.clone(),
pattern: AagInputConstraintPattern::StartupReset {
asserted_frames: *asserted_frames,
asserted_value: !*active_low,
},
}),
});
if let Some(reset_constraint) = reset_constraint {
if effective_assumptions
.iter()
.any(|constraint| constraint.name == reset_constraint.name.as_str())
{
return Err(format!(
"reset signal `{}` duplicates an environment assumption",
reset_constraint.name
));
}
effective_assumptions.push(reset_constraint);
}
let assumptions = effective_assumptions.as_slice();
let mut sources = Vec::with_capacity(inputs.len());
let mut seen_sources = BTreeSet::new();
let mut source_bytes = 0u64;
for input in inputs {
let source = fs::canonicalize(input)
.map_err(|error| format!("resolve RTL source {}: {error}", input.display()))?;
if !source.is_file() {
return Err(format!("RTL source is not a file: {}", source.display()));
}
if !seen_sources.insert(source.clone()) {
return Err(format!("duplicate RTL source: {}", input.display()));
}
let metadata_bytes = fs::metadata(&source)
.map_err(|error| format!("inspect RTL source {}: {error}", source.display()))?
.len();
if metadata_bytes > 10 * 1024 * 1024 {
return Err(format!(
"RTL source {} is {metadata_bytes} bytes; per-file safety limit is 10485760",
input.display()
));
}
let bytes = fs::read(&source)
.map_err(|error| format!("read RTL source {}: {error}", source.display()))?;
if bytes.len() > 10 * 1024 * 1024 {
return Err(format!(
"RTL source {} exceeds per-file safety limit 10485760",
input.display()
));
}
source_bytes = source_bytes
.checked_add(bytes.len() as u64)
.ok_or_else(|| "RTL project source byte count overflow".to_string())?;
sources.push((source, bytes));
}
if source_bytes > 25 * 1024 * 1024 {
return Err(format!(
"RTL project is {source_bytes} bytes; total safety limit is 26214400"
));
}
let source_labels = inputs
.iter()
.map(|input| input.to_string_lossy().to_string())
.collect::<Vec<_>>();
let source_label = source_labels.join(";");
fs::create_dir_all(artifact_dir)
.map_err(|error| format!("create RTL safety artifact directory: {error}"))?;
let artifact_metadata = fs::symlink_metadata(artifact_dir)
.map_err(|error| format!("inspect RTL safety artifact directory: {error}"))?;
if !artifact_metadata.file_type().is_dir() {
return Err("RTL safety artifact path must be a real directory, not a symlink".to_string());
}
for entry in fs::read_dir(artifact_dir)
.map_err(|error| format!("inspect RTL safety artifact directory: {error}"))?
{
let entry = entry.map_err(|error| format!("inspect RTL safety artifact entry: {error}"))?;
if entry
.file_type()
.map_err(|error| format!("inspect RTL safety artifact entry: {error}"))?
.is_symlink()
{
return Err(format!(
"RTL safety artifact directory contains a symlink: {}",
entry.path().display()
));
}
}
let stage = artifact_dir.join(format!(".rtl-stage-{}", std::process::id()));
fs::create_dir(&stage)
.map_err(|error| format!("create RTL safety staging directory: {error}"))?;
let stage = fs::canonicalize(&stage)
.map_err(|error| format!("resolve RTL safety staging directory: {error}"))?;
let model = stage.join("model.aag");
let synthesis = stage.join("synthesis.ys");
let yosys_log = stage.join("yosys.log");
let yosys_errors = stage.join("yosys-errors.log");
let staged_source_names = sources
.iter()
.enumerate()
.map(|(index, (source, bytes))| {
let name = if sources.len() == 1 {
"source.sv".to_string()
} else {
format!("source-{index:04}.sv")
};
fs::write(stage.join(&name), bytes)
.map_err(|error| format!("stage RTL source {}: {error}", source.display()))?;
Ok(name)
})
.collect::<Result<Vec<_>, String>>()?;
let staged_sources = staged_source_names.join(" ");
let mut include_flags = Vec::new();
if let Some(options) = build_options {
fs::write(stage.join("cq-project.conf"), &options.project_config)
.map_err(|error| format!("stage RTL project config: {error}"))?;
for include in &options.includes {
let directory = stage.join(&include.label);
fs::create_dir(&directory)
.map_err(|error| format!("stage include directory: {error}"))?;
for (name, bytes) in &include.files {
fs::write(directory.join(name), bytes)
.map_err(|error| format!("stage include file {}: {error}", name.display()))?;
}
include_flags.push(format!("-I{}", include.label));
}
}
if let Some((_, bytes)) = &assumption_document {
fs::write(stage.join("assumptions.txt"), bytes)
.map_err(|error| format!("stage environment assumptions: {error}"))?;
}
let parameter_commands = build_options.map_or_else(String::new, |options| {
options
.parameters
.iter()
.map(|(name, value)| format!("chparam -set {name} {value} {top}\n"))
.collect()
});
let clock_check = build_options.map_or_else(String::new, |options| {
format!("select -assert-count 1 {top}/{}\n", options.clock.0)
});
let includes = include_flags.join(" ");
let script = format!(
"read_verilog -formal -sv -D CQ_AIGER_EXPORT {includes} {staged_sources}\n{parameter_commands}prep -top {top}\n{clock_check}flatten\nasync2sync\nopt\nmemory_map\nopt\ntechmap\nopt\ndffunmap\npmuxtree\nsimplemap\ndffunmap\naigmap\nsetundef -zero\nwrite_aiger -ascii -symbols -map signal.map model.aag\n"
);
fs::write(&synthesis, script)
.map_err(|error| format!("write Yosys synthesis script: {error}"))?;
let error_file = fs::File::create(&yosys_errors)
.map_err(|error| format!("create Yosys error log: {error}"))?;
let mut yosys_command = Command::new("yosys");
yosys_command
.arg("-l")
.arg("yosys.log")
.arg("-s")
.arg("synthesis.ys")
.current_dir(&stage)
.stdout(Stdio::null())
.stderr(Stdio::from(error_file));
configure_contained_process(
&mut yosys_command,
YOSYS_MEMORY_LIMIT_BYTES,
YOSYS_FILE_LIMIT_BYTES,
)?;
let mut child = yosys_command
.spawn()
.map_err(|error| format!("run Yosys synthesis: {error}"))?;
let Some(status) = wait_for_contained_process(
&mut child,
std::time::Duration::from_secs(120),
"Yosys synthesis",
)?
else {
return Err(format!(
"Yosys synthesis exceeded 120 seconds; inspect {} and {}",
yosys_log.display(),
yosys_errors.display()
));
};
if !status.success() {
let error_tail = fs::read_to_string(&yosys_errors)
.ok()
.and_then(|body| body.lines().last().map(str::to_string))
.filter(|line| !line.is_empty())
.or_else(|| {
fs::read_to_string(&yosys_log)
.ok()
.and_then(|body| body.lines().last().map(str::to_string))
.filter(|line| !line.is_empty())
})
.unwrap_or_else(|| "no Yosys diagnostic was captured".to_string());
return Err(format!(
"Yosys synthesis failed: {error_tail}; inspect {} and {}",
yosys_log.display(),
yosys_errors.display()
));
}
let yosys_version = fs::read_to_string(&yosys_log)
.map_err(|error| format!("read Yosys synthesis log: {error}"))?
.lines()
.find(|line| line.trim_start().starts_with("Yosys "))
.map(str::trim)
.ok_or_else(|| "Yosys synthesis log contains no version banner".to_string())?
.to_string();
let safe = firmware_safety_gate_with_constraints(&model, horizon, &stage, assumptions)?;
annotate_rtl_safety_report(
&stage.join("safety-report.txt"),
&source_label,
top,
&yosys_version,
build_options,
)?;
let status_name = if safe { "SAFE" } else { "UNSAFE" };
let revision = source_revision();
let manifest_sources = source_labels
.iter()
.enumerate()
.map(|(index, source)| format!("source_{index}={}", report_value(source)))
.collect::<Vec<_>>()
.join("\n");
let assumption_source_label = assumptions_path.map_or_else(
|| "none".to_string(),
|path| report_value(&path.to_string_lossy()),
);
let assumption_count = assumption_document
.as_ref()
.map_or(0, |(constraints, _)| constraints.len());
let project_config = if build_options.is_some() {
"cq-project.conf"
} else {
"none"
};
let include_dir_count = build_options.map_or(0, |options| options.includes.len());
let include_file_count = build_options.map_or(0, |options| {
options
.includes
.iter()
.map(|include| include.files.len())
.sum()
});
let include_bytes: usize = build_options.map_or(0, |options| {
options
.includes
.iter()
.flat_map(|include| &include.files)
.map(|(_, bytes)| bytes.len())
.sum()
});
let parameters = build_options.map_or_else(
|| "none".to_string(),
|options| {
if options.parameters.is_empty() {
"none".to_string()
} else {
options
.parameters
.iter()
.map(|(name, value)| format!("{name}:{value}"))
.collect::<Vec<_>>()
.join(";")
}
},
);
let parameter_count = build_options.map_or(0, |options| options.parameters.len());
let clock_policy = build_options.map_or_else(
|| "unspecified".to_string(),
|options| format!("{}:{}", options.clock.0, options.clock.1),
);
let reset_policy = build_options.map_or_else(
|| "unspecified".to_string(),
|options| rtl_reset_policy_label(&options.reset),
);
let evidence_index_sha256 = write_evidence_index(&stage)?;
fs::write(
stage.join("run-manifest.txt"),
format!(
"status={status_name}\nschema_version={RTL_ARTIFACT_SCHEMA_VERSION}\nfirmware_cli_version={FIRMWARE_CLI_CONTRACT_VERSION}\nsource={}\nsource_count={}\n{manifest_sources}\nsource_revision={revision}\nsource_bytes={source_bytes}\nassumption_source={assumption_source_label}\nassumption_count={assumption_count}\nproject_config={project_config}\ninclude_dir_count={include_dir_count}\ninclude_file_count={include_file_count}\ninclude_bytes={include_bytes}\nparameter_count={parameter_count}\nparameters={parameters}\nclock_policy={clock_policy}\nreset_policy={reset_policy}\ntop={top}\nhorizon={horizon}\nsynthesis_timeout_seconds=120\ncontainment_platform={}\nprocess_group_timeout_kill=true\nsynthesis_memory_limit_kind={}\nsynthesis_memory_limit_bytes={}\nsynthesis_file_limit_bytes={YOSYS_FILE_LIMIT_BYTES}\nyosys={yosys_version}\nevidence_digest_algorithm=sha256\nevidence_index=evidence.sha256\nevidence_index_sha256={evidence_index_sha256}\n",
report_value(&source_label), sources.len(), std::env::consts::OS,
synthesis_memory_limit_kind(), synthesis_memory_limit_bytes()
),
)
.map_err(|error| format!("write RTL safety manifest: {error}"))?;
let published_manifest = artifact_dir.join("run-manifest.txt");
if published_manifest.exists() {
fs::remove_file(&published_manifest)
.map_err(|error| format!("remove stale RTL safety manifest: {error}"))?;
}
for entry in fs::read_dir(artifact_dir)
.map_err(|error| format!("inspect RTL safety artifact directory: {error}"))?
{
let entry = entry.map_err(|error| format!("inspect RTL safety artifact: {error}"))?;
let name = entry.file_name();
let name = name.to_string_lossy();
if is_rtl_source_snapshot(&name) && entry.path().is_file() {
fs::remove_file(entry.path())
.map_err(|error| format!("remove stale RTL source snapshot: {error}"))?;
}
if is_rtl_include_snapshot(&name) && entry.path().is_dir() {
fs::remove_dir_all(entry.path())
.map_err(|error| format!("remove stale RTL include snapshot: {error}"))?;
}
}
let mut published_names = vec![
"model.aag",
"signal.map",
"synthesis.ys",
"yosys.log",
"yosys-errors.log",
"solver-metrics.csv",
"safety-report.txt",
"evidence.sha256",
];
published_names.extend(staged_source_names.iter().map(String::as_str));
if assumptions_path.is_some() {
published_names.push("assumptions.txt");
} else {
let stale = artifact_dir.join("assumptions.txt");
if stale.is_file() {
fs::remove_file(stale)
.map_err(|error| format!("remove stale environment assumptions: {error}"))?;
}
}
for name in published_names {
replace_file(&stage.join(name), &artifact_dir.join(name))?;
}
if let Some(options) = build_options {
replace_file(
&stage.join("cq-project.conf"),
&artifact_dir.join("cq-project.conf"),
)?;
for include in &options.includes {
let destination = artifact_dir.join(&include.label);
if destination.exists() {
fs::remove_dir_all(&destination)
.map_err(|error| format!("remove stale include snapshot: {error}"))?;
}
fs::rename(stage.join(&include.label), &destination)
.map_err(|error| format!("publish include snapshot: {error}"))?;
}
} else {
let stale = artifact_dir.join("cq-project.conf");
if stale.is_file() {
fs::remove_file(stale)
.map_err(|error| format!("remove stale project config: {error}"))?;
}
}
replace_file(&stage.join("run-manifest.txt"), &published_manifest)?;
fs::remove_dir(&stage)
.map_err(|error| format!("remove RTL safety staging directory: {error}"))?;
println!(
"firmware-rtl-safety-gate status={status_name} source={} top={top} report={}",
source_label,
artifact_dir.join("safety-report.txt").display()
);
Ok(safe)
}
fn firmware_rtl_project_safety_gate(
inputs: &[PathBuf],
top: &str,
horizon: usize,
artifact_dir: &Path,
) -> Result<bool, String> {
firmware_rtl_project_safety_gate_with_assumptions(
inputs,
top,
horizon,
artifact_dir,
None,
None,
)
}
fn firmware_rtl_safety_gate(
input: &Path,
top: &str,
horizon: usize,
artifact_dir: &Path,
) -> Result<bool, String> {
firmware_rtl_project_safety_gate(&[input.to_path_buf()], top, horizon, artifact_dir)
}
fn firmware_rtl_config_safety_gate(
config_path: &Path,
artifact_dir: &Path,
) -> Result<bool, String> {
let config = parse_rtl_project_config(config_path)?;
let (sources, assumptions, options) = load_rtl_build_options(config_path, &config)?;
firmware_rtl_project_safety_gate_with_assumptions(
&sources,
&config.top,
config.horizon,
artifact_dir,
assumptions.as_deref(),
Some(&options),
)
}
fn run_firmware_gate_cli(args: &[String]) -> Result<Option<bool>, String> {
match args.first().map(String::as_str) {
Some("production-profile-version") => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker production-profile-version".to_string(),
);
}
#[cfg(feature = "production-firmware")]
{
println!(
"production_support_profile=firmware-rtl-v1 firmware_cli_version={FIRMWARE_CLI_CONTRACT_VERSION} artifact_schema_version={RTL_ARTIFACT_SCHEMA_VERSION}"
);
Ok(Some(true))
}
#[cfg(not(feature = "production-firmware"))]
{
Err("this binary is not a production support-profile build".to_string())
}
}
Some("firmware-cli-version") => {
if args.len() != 1 {
return Err("usage: guarded-continuation-checker firmware-cli-version".to_string());
}
println!(
"firmware_cli_version={FIRMWARE_CLI_CONTRACT_VERSION} artifact_schema_version={RTL_ARTIFACT_SCHEMA_VERSION}"
);
Ok(Some(true))
}
Some("firmware-artifact-validate") => {
if args.len() != 2 {
return Err(
"usage: guarded-continuation-checker firmware-artifact-validate ARTIFACT_DIR"
.to_string(),
);
}
validate_rtl_artifact_bundle(Path::new(&args[1]))?;
Ok(Some(true))
}
Some("firmware-rtl-config-safety-gate") => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker firmware-rtl-config-safety-gate PROJECT.conf ARTIFACT_DIR".to_string());
}
firmware_rtl_config_safety_gate(Path::new(&args[1]), Path::new(&args[2])).map(Some)
}
Some("firmware-safety-gate") => {
if args.len() != 4 {
return Err("usage: guarded-continuation-checker firmware-safety-gate INPUT.aag HORIZON ARTIFACT_DIR".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid firmware safety horizon".to_string())?
.max(1);
firmware_safety_gate(Path::new(&args[1]), horizon, Path::new(&args[3])).map(Some)
}
Some("firmware-rtl-safety-gate") => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker firmware-rtl-safety-gate INPUT.sv TOP HORIZON ARTIFACT_DIR".to_string());
}
let horizon = args[3]
.parse::<usize>()
.map_err(|_| "invalid RTL firmware safety horizon".to_string())?
.max(1);
firmware_rtl_safety_gate(Path::new(&args[1]), &args[2], horizon, Path::new(&args[4]))
.map(Some)
}
Some("firmware-rtl-project-safety-gate") => {
if args.len() < 5 {
return Err("usage: guarded-continuation-checker firmware-rtl-project-safety-gate TOP HORIZON ARTIFACT_DIR SOURCE.sv SOURCE2.sv [...]".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid RTL project safety horizon".to_string())?
.max(1);
let sources = args[4..].iter().map(PathBuf::from).collect::<Vec<_>>();
firmware_rtl_project_safety_gate(&sources, &args[1], horizon, Path::new(&args[3]))
.map(Some)
}
Some("firmware-rtl-constrained-project-safety-gate") => {
if args.len() < 6 {
return Err("usage: guarded-continuation-checker firmware-rtl-constrained-project-safety-gate TOP HORIZON ARTIFACT_DIR ASSUMPTIONS.txt SOURCE.sv SOURCE2.sv [...]".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid constrained RTL project safety horizon".to_string())?
.max(1);
let sources = args[5..].iter().map(PathBuf::from).collect::<Vec<_>>();
firmware_rtl_project_safety_gate_with_assumptions(
&sources,
&args[1],
horizon,
Path::new(&args[3]),
Some(Path::new(&args[4])),
None,
)
.map(Some)
}
_ => Ok(None),
}
}
fn benchmark_continuation_dimacs(
input: &Path,
query_count: usize,
max_assumptions: usize,
output: &Path,
) -> Result<(), String> {
let (vars, formula) = parse_dimacs(input)?;
let order_start = Instant::now();
let order = min_fill_order(vars, &formula);
let order_ns = order_start.elapsed().as_nanos();
let profile = continuation_frontier_profile(vars, &formula, &order);
let bound_bits = ceil_log2_u128(profile.iter().copied().max().unwrap_or(1));
let profile_state_bound = profile.iter().copied().fold(0u128, u128::saturating_add);
let admitted = bound_bits <= 16;
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create DIMACS output: {error}"))?;
}
let header = "input,variables,clauses,queries,max_assumptions,order_kind,order_ns,frontier_bound_bits,profile_state_bound,admitted,peak_classes,total_layer_states,transition_bytes,repair_residual_bytes,compile_ns,quotient_ns_per_query,incremental_varisat_ns_per_query,speedup_vs_incremental,break_even_queries,sat_queries,unsat_queries,agreement,witnesses_valid\n";
if !admitted {
fs::write(
output,
format!("{header}{},{vars},{},{query_count},{max_assumptions},min-fill,{order_ns},{bound_bits},{profile_state_bound},false,0,0,0,0,0,0,0,0,0,0,0,true,true\n", input.display(), formula.len()),
)
.map_err(|error| format!("write rejected DIMACS output: {error}"))?;
println!(
"continuation DIMACS input={} admitted=false bound_bits={bound_bits} output={}",
input.display(),
output.display()
);
return Ok(());
}
let compile_start = Instant::now();
let compiled = compile_continuation(&formula, &order);
let compile_ns = compile_start.elapsed().as_nanos();
let total_layer_states: usize = compiled.residual_layers.iter().map(Vec::len).sum();
let transition_bytes: usize = compiled
.transitions
.iter()
.map(|layer| layer.len() * std::mem::size_of::<[usize; 2]>())
.sum::<usize>()
.saturating_add(compiled.terminal_sat.len())
.saturating_add(compiled.order.len() * std::mem::size_of::<usize>());
let repair_residual_bytes: usize = compiled
.residual_layers
.iter()
.flat_map(|layer| layer.iter())
.flat_map(|residual| residual.iter())
.map(|clause| clause.len() * std::mem::size_of::<Literal>())
.sum();
let mut rng = Rng(formula
.iter()
.flat_map(|clause| clause.0.iter())
.fold(vars as u64 ^ 0xd6e8_feb8_6659_fd93, |hash, &(v, sign)| {
hash.rotate_left(7) ^ v as u64 ^ (sign as u64)
}));
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
let width = 1 + query_index % max_assumptions.max(1).min(vars.max(1));
let mut chosen = BTreeSet::new();
while chosen.len() < width.min(vars) {
chosen.insert(rng.below(vars));
}
for variable in chosen {
assumptions[variable] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let mut scratch = ContinuationScratch::new(&compiled);
let quotient_start = Instant::now();
let quotient_answers: Vec<_> = queries
.iter()
.map(|assumptions| query_continuation(&compiled, assumptions, &mut scratch))
.collect();
let quotient_ns = quotient_start.elapsed().as_nanos();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &formula);
let varisat_start = Instant::now();
let varisat_answers: Vec<_> = queries
.iter()
.map(|assumptions| solve_varisat_assumptions(&mut solver, assumptions, vars))
.collect();
let varisat_ns = varisat_start.elapsed().as_nanos();
let agreement = quotient_answers
.iter()
.zip(&varisat_answers)
.all(|(left, right)| left.is_some() == right.is_some());
let witnesses_valid = quotient_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = quotient_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = query_count - sat_queries;
let quotient_per_query = quotient_ns as f64 / query_count.max(1) as f64;
let varisat_per_query = varisat_ns as f64 / query_count.max(1) as f64;
let break_even_queries = if varisat_per_query > quotient_per_query {
(compile_ns as f64 / (varisat_per_query - quotient_per_query)).ceil() as u128
} else {
u128::MAX
};
fs::write(
output,
format!("{header}{},{vars},{},{query_count},{max_assumptions},min-fill,{order_ns},{bound_bits},{profile_state_bound},true,{},{total_layer_states},{transition_bytes},{repair_residual_bytes},{compile_ns},{quotient_per_query:.3},{varisat_per_query:.3},{:.6},{break_even_queries},{sat_queries},{unsat_queries},{agreement},{witnesses_valid}\n", input.display(), formula.len(), compiled.peak_classes, varisat_per_query / quotient_per_query.max(1.0)),
)
.map_err(|error| format!("write DIMACS output: {error}"))?;
println!(
"continuation DIMACS input={} admitted=true peak={} speedup={:.6} agreement={agreement} witnesses_valid={witnesses_valid} output={}",
input.display(),
compiled.peak_classes,
varisat_per_query / quotient_per_query.max(1.0),
output.display()
);
Ok(())
}
fn benchmark_continuation_repairs(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
update_count: usize,
query_count: usize,
output: &Path,
) -> Result<(), String> {
let formula = generate_formula(family, vars, ratio, formula_seed);
let order: Vec<_> = (0..vars).collect();
let bound_bits = continuation_frontier_bound_bits(vars, &formula, &order);
if bound_bits > 16 {
return Err(format!(
"continuation repair rejected by 16-bit gate: bound is {bound_bits} bits"
));
}
let base = compile_continuation(&formula, &order);
let base_witness = solve_with_varisat(vars, &formula)
.ok_or_else(|| "repair benchmark requires a satisfiable base formula".to_string())?;
let mut rng = Rng(formula_seed ^ 0xe703_7ed1_a0b4_28db);
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create repair output: {error}"))?;
}
let mut file =
fs::File::create(output).map_err(|error| format!("create repair output: {error}"))?;
writeln!(file, "family,formula_seed,update,kind,start_layer,updated_bound_bits,local_repair_ns,full_recompile_ns,repair_speedup,cdcl_update_ns,local_query_ns,cdcl_query_ns,local_ns_per_query,cdcl_ns_per_query,query_speedup,break_even_queries,local_total_ns,cdcl_total_ns,total_speedup,local_peak_classes,full_peak_classes,queries,sat_queries,unsat_queries,local_full_agreement,varisat_agreement,witnesses_valid")
.map_err(|error| format!("write repair header: {error}"))?;
for update in 0..update_count {
let insertion = update % 2 == 0;
let changed_clause = if insertion {
let mut variables = BTreeSet::new();
while variables.len() < 3.min(vars) {
variables.insert(rng.below(vars));
}
let mut literals: Vec<_> = variables
.into_iter()
.map(|variable| (variable, rng.next() & 1 == 1))
.collect();
if !literals
.iter()
.any(|&(variable, sign)| base_witness[variable] == sign)
{
literals[0].1 = base_witness[literals[0].0];
}
Clause(literals)
} else {
formula[rng.below(formula.len())].clone()
};
let mut updated = formula.clone();
if insertion {
updated.push(changed_clause.clone());
} else if let Some(index) = updated
.iter()
.position(|clause| clause.0 == changed_clause.0)
{
updated.remove(index);
}
let start_layer = changed_clause
.0
.iter()
.map(|&(variable, _)| variable)
.min()
.unwrap_or(0);
let repair_start = Instant::now();
let repaired = repair_continuation(&base, &changed_clause, insertion);
let local_repair_ns = repair_start.elapsed().as_nanos();
let full_start = Instant::now();
let full = compile_continuation(&updated, &order);
let full_recompile_ns = full_start.elapsed().as_nanos();
let updated_bound_bits = continuation_frontier_bound_bits(vars, &updated, &order);
let mut queries = Vec::with_capacity(query_count);
for query_index in 0..query_count {
let mut assumptions = vec![None; vars];
let width = 1 + query_index % 10.min(vars.max(1));
let mut chosen = BTreeSet::new();
while chosen.len() < width.min(vars) {
chosen.insert(rng.below(vars));
}
for variable in chosen {
assumptions[variable] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let mut repaired_scratch = ContinuationScratch::new(&repaired);
let mut full_scratch = ContinuationScratch::new(&full);
let local_query_start = Instant::now();
let repaired_answers: Vec<_> = queries
.iter()
.map(|assumptions| query_continuation(&repaired, assumptions, &mut repaired_scratch))
.collect();
let local_query_ns = local_query_start.elapsed().as_nanos();
let mut cdcl_solver = Solver::new();
let cdcl_update_ns = if insertion {
add_to_varisat(&mut cdcl_solver, &formula);
let update_start = Instant::now();
add_to_varisat(&mut cdcl_solver, std::slice::from_ref(&changed_clause));
update_start.elapsed().as_nanos()
} else {
let update_start = Instant::now();
add_to_varisat(&mut cdcl_solver, &updated);
update_start.elapsed().as_nanos()
};
let cdcl_query_start = Instant::now();
let cdcl_answers: Vec<Option<Vec<bool>>> = queries
.iter()
.map(|assumptions| {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
cdcl_solver.assume(&literals);
if !cdcl_solver.solve().expect("repair Varisat solve") {
return None;
}
let mut assignment = vec![false; vars];
for literal in cdcl_solver.model().expect("repair Varisat model") {
if literal.var().index() < vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
})
.collect();
let cdcl_query_ns = cdcl_query_start.elapsed().as_nanos();
let mut local_full_agreement = true;
let mut varisat_agreement = true;
let mut witnesses_valid = true;
let mut sat_queries = 0usize;
for ((assumptions, repaired_answer), cdcl_answer) in
queries.iter().zip(&repaired_answers).zip(&cdcl_answers)
{
let full_answer = query_continuation(&full, assumptions, &mut full_scratch);
sat_queries += repaired_answer.is_some() as usize;
local_full_agreement &= repaired_answer.is_some() == full_answer.is_some();
varisat_agreement &= repaired_answer.is_some() == cdcl_answer.is_some();
witnesses_valid &= repaired_answer.as_ref().is_none_or(|assignment| {
satisfies(&updated, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
});
witnesses_valid &= cdcl_answer.as_ref().is_none_or(|assignment| {
satisfies(&updated, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
});
}
let unsat_queries = query_count.saturating_sub(sat_queries);
let local_per_query = local_query_ns as f64 / query_count as f64;
let cdcl_per_query = cdcl_query_ns as f64 / query_count as f64;
let break_even_queries = if cdcl_per_query > local_per_query {
(local_repair_ns.saturating_sub(cdcl_update_ns) as f64
/ (cdcl_per_query - local_per_query))
.ceil() as u128
} else {
u128::MAX
};
let local_total_ns = local_repair_ns.saturating_add(local_query_ns);
let cdcl_total_ns = cdcl_update_ns.saturating_add(cdcl_query_ns);
writeln!(file, "{family},{formula_seed},{},{},{start_layer},{updated_bound_bits},{local_repair_ns},{full_recompile_ns},{:.6},{cdcl_update_ns},{local_query_ns},{cdcl_query_ns},{local_per_query:.3},{cdcl_per_query:.3},{:.6},{break_even_queries},{local_total_ns},{cdcl_total_ns},{:.6},{},{},{query_count},{sat_queries},{unsat_queries},{local_full_agreement},{varisat_agreement},{witnesses_valid}", update + 1, if insertion { "insert" } else { "delete" }, full_recompile_ns as f64 / local_repair_ns.max(1) as f64, cdcl_per_query / local_per_query.max(1.0), cdcl_total_ns as f64 / local_total_ns.max(1) as f64, repaired.peak_classes, full.peak_classes)
.map_err(|error| format!("write repair row: {error}"))?;
}
file.flush()
.map_err(|error| format!("flush repair output: {error}"))?;
println!(
"continuation repairs family={family} seed={formula_seed} vars={vars} updates={update_count} queries={query_count} output={}",
output.display()
);
Ok(())
}
fn solve_varisat_assumptions(
solver: &mut Solver<'_>,
assumptions: &[Option<bool>],
vars: usize,
) -> Option<Vec<bool>> {
let literals: Vec<_> = assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Lit::from_var(Var::from_index(variable), value))
})
.collect();
solver.assume(&literals);
if !solver.solve().expect("hybrid Varisat solve") {
return None;
}
let mut assignment = vec![false; vars];
for literal in solver.model().expect("hybrid Varisat model") {
if literal.var().index() < vars {
assignment[literal.var().index()] = literal.is_positive();
}
}
Some(assignment)
}
fn benchmark_continuation_hybrid(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
phase_count: usize,
update_span: usize,
output: &Path,
) -> Result<(), String> {
let mut formula = generate_formula(family, vars, ratio, formula_seed);
let order: Vec<_> = (0..vars).collect();
let mut rng = Rng(formula_seed ^ 0x8ebc_6af0_9c88_c6e3);
let mut baseline_solver = Solver::new();
let baseline_setup_start = Instant::now();
add_to_varisat(&mut baseline_solver, &formula);
let mut baseline_total_ns = baseline_setup_start.elapsed().as_nanos();
let mut hybrid_solver = Solver::new();
let hybrid_setup_start = Instant::now();
add_to_varisat(&mut hybrid_solver, &formula);
let mut hybrid_total_ns = hybrid_setup_start.elapsed().as_nanos();
let mut compiled: Option<CompiledContinuation> = None;
let query_schedule = [100usize, 5_000, 25_000, 20_000, 1_000, 30_000];
let width_schedule = [3usize, 10, 10, 40, 5, 20];
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create hybrid output: {error}"))?;
}
let mut file =
fs::File::create(output).map_err(|error| format!("create hybrid output: {error}"))?;
writeln!(file, "family,formula_seed,phase,update_kind,start_layer,queries,max_assumptions,declared_horizon_queries,frontier_bound_bits,profile_state_bound,suffix_state_bound,required_horizon_queries,decision,decision_ns,hybrid_phase_ns,baseline_phase_ns,phase_speedup,sat_queries,unsat_queries,agreement,witnesses_valid")
.map_err(|error| format!("write hybrid header: {error}"))?;
let mut all_agree = true;
let mut all_valid = true;
for phase in 0..phase_count {
let queries_count = query_schedule[phase % query_schedule.len()];
let max_assumptions = width_schedule[phase % width_schedule.len()].min(vars.max(1));
let stable_end = ((phase / update_span.max(1) + 1) * update_span.max(1)).min(phase_count);
let declared_horizon_queries: usize = (phase..stable_end)
.filter(|&future| width_schedule[future % width_schedule.len()] <= 20)
.map(|future| query_schedule[future % query_schedule.len()])
.sum();
let mut update_kind = "none";
let mut start_layer = 0usize;
let mut changed: Option<(Clause, bool)> = None;
let mut baseline_update_ns = 0u128;
if phase > 0 && phase % update_span.max(1) == 0 {
let insertion = (phase / update_span.max(1)) % 2 == 1;
update_kind = if insertion { "insert" } else { "delete" };
let clause = if insertion {
let witness = solve_with_varisat(vars, &formula)
.ok_or_else(|| "hybrid workload unexpectedly became UNSAT".to_string())?;
let mut variables = BTreeSet::new();
while variables.len() < 3.min(vars) {
variables.insert(rng.below(vars));
}
let mut literals: Vec<_> = variables
.into_iter()
.map(|variable| (variable, rng.next() & 1 == 1))
.collect();
if !literals
.iter()
.any(|&(variable, sign)| witness[variable] == sign)
{
literals[0].1 = witness[literals[0].0];
}
Clause(literals)
} else {
formula[rng.below(formula.len())].clone()
};
start_layer = clause
.0
.iter()
.map(|&(variable, _)| variable)
.min()
.unwrap_or(0);
if insertion {
formula.push(clause.clone());
let update_start = Instant::now();
add_to_varisat(&mut baseline_solver, std::slice::from_ref(&clause));
baseline_update_ns = update_start.elapsed().as_nanos();
} else {
let index = formula
.iter()
.position(|candidate| candidate.0 == clause.0)
.unwrap();
formula.remove(index);
let update_start = Instant::now();
baseline_solver = Solver::new();
add_to_varisat(&mut baseline_solver, &formula);
baseline_update_ns = update_start.elapsed().as_nanos();
}
changed = Some((clause, insertion));
}
let hybrid_update_start = Instant::now();
if let Some((clause, insertion)) = &changed {
if *insertion {
add_to_varisat(&mut hybrid_solver, std::slice::from_ref(clause));
} else {
hybrid_solver = Solver::new();
add_to_varisat(&mut hybrid_solver, &formula);
}
}
let hybrid_update_ns = hybrid_update_start.elapsed().as_nanos();
let profile = continuation_frontier_profile(vars, &formula, &order);
let bound_bits = ceil_log2_u128(profile.iter().copied().max().unwrap_or(1));
let profile_state_bound = profile.iter().copied().fold(0u128, u128::saturating_add);
let suffix_state_bound = profile[start_layer.min(profile.len())..]
.iter()
.copied()
.fold(0u128, u128::saturating_add);
let decision_start = Instant::now();
let query_regime = max_assumptions <= 20;
let compilation_threshold = profile_state_bound
.saturating_mul(16)
.min(usize::MAX as u128) as usize;
let repair_threshold = suffix_state_bound
.saturating_mul(16)
.min(usize::MAX as u128) as usize;
let mut decision = "cdcl";
if let Some((clause, insertion)) = &changed {
if compiled.is_some()
&& *insertion
&& bound_bits <= 16
&& query_regime
&& declared_horizon_queries >= repair_threshold
&& start_layer >= 10
{
compiled = Some(repair_continuation(
compiled.as_ref().unwrap(),
clause,
*insertion,
));
decision = "repair";
} else {
compiled = None;
}
}
if compiled.is_none()
&& bound_bits <= 16
&& query_regime
&& declared_horizon_queries >= compilation_threshold
{
compiled = Some(compile_continuation(&formula, &order));
decision = "compile";
} else if compiled.is_some() && query_regime {
decision = if decision == "repair" {
"repair"
} else {
"quotient"
};
}
let decision_ns = decision_start.elapsed().as_nanos();
let mut queries = Vec::with_capacity(queries_count);
for query_index in 0..queries_count {
let mut assumptions = vec![None; vars];
let width = 1 + query_index % max_assumptions;
let mut chosen = BTreeSet::new();
while chosen.len() < width.min(vars) {
chosen.insert(rng.below(vars));
}
for variable in chosen {
assumptions[variable] = Some(rng.next() & 1 == 1);
}
queries.push(assumptions);
}
let baseline_query_start = Instant::now();
let baseline_answers: Vec<_> = queries
.iter()
.map(|assumptions| solve_varisat_assumptions(&mut baseline_solver, assumptions, vars))
.collect();
let baseline_query_ns = baseline_query_start.elapsed().as_nanos();
let hybrid_query_start = Instant::now();
let hybrid_answers: Vec<_> = if decision == "cdcl" {
queries
.iter()
.map(|assumptions| solve_varisat_assumptions(&mut hybrid_solver, assumptions, vars))
.collect()
} else {
let active = compiled.as_ref().unwrap();
let mut scratch = ContinuationScratch::new(active);
queries
.iter()
.map(|assumptions| query_continuation(active, assumptions, &mut scratch))
.collect()
};
let hybrid_query_ns = hybrid_query_start.elapsed().as_nanos();
let agreement = hybrid_answers
.iter()
.zip(&baseline_answers)
.all(|(left, right)| left.is_some() == right.is_some());
let witnesses_valid = hybrid_answers
.iter()
.zip(&queries)
.all(|(answer, assumptions)| {
answer.as_ref().is_none_or(|assignment| {
satisfies(&formula, assignment)
&& assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
})
})
});
let sat_queries = hybrid_answers
.iter()
.filter(|answer| answer.is_some())
.count();
let unsat_queries = queries_count - sat_queries;
let hybrid_phase_ns = hybrid_update_ns
.saturating_add(decision_ns)
.saturating_add(hybrid_query_ns);
let baseline_phase_ns = baseline_update_ns.saturating_add(baseline_query_ns);
hybrid_total_ns = hybrid_total_ns.saturating_add(hybrid_phase_ns);
baseline_total_ns = baseline_total_ns.saturating_add(baseline_phase_ns);
all_agree &= agreement;
all_valid &= witnesses_valid;
let required_horizon_queries = if decision == "repair" {
repair_threshold
} else {
compilation_threshold
};
writeln!(file, "{family},{formula_seed},{},{update_kind},{start_layer},{queries_count},{max_assumptions},{declared_horizon_queries},{bound_bits},{profile_state_bound},{suffix_state_bound},{required_horizon_queries},{decision},{decision_ns},{hybrid_phase_ns},{baseline_phase_ns},{:.6},{sat_queries},{unsat_queries},{agreement},{witnesses_valid}", phase + 1, baseline_phase_ns as f64 / hybrid_phase_ns.max(1) as f64)
.map_err(|error| format!("write hybrid row: {error}"))?;
}
writeln!(file, "{family},{formula_seed},total,none,0,0,0,0,0,0,0,0,summary,0,{hybrid_total_ns},{baseline_total_ns},{:.6},0,0,{all_agree},{all_valid}", baseline_total_ns as f64 / hybrid_total_ns.max(1) as f64)
.map_err(|error| format!("write hybrid total: {error}"))?;
file.flush()
.map_err(|error| format!("flush hybrid output: {error}"))?;
println!(
"continuation hybrid family={family} seed={formula_seed} vars={vars} phases={phase_count} speedup={:.6} agreement={all_agree} witnesses_valid={all_valid} output={}",
baseline_total_ns as f64 / hybrid_total_ns.max(1) as f64,
output.display()
);
Ok(())
}
fn benchmark_continuation_quotients(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
strategies: usize,
gate_limit_bits: Option<usize>,
output: &Path,
) -> Result<(), String> {
if vars > 256 {
return Err("continuation quotient search supports at most 256 variables".to_string());
}
let formula = generate_formula(family, vars, ratio, formula_seed);
let (reference_width, reference_kind) = if vars <= 20 {
(exact_treewidth(vars, &formula), "exact")
} else {
(
structural_treewidth_lower_bound(vars, &formula),
"structural-lower-bound",
)
};
let base: Vec<Vec<Literal>> = formula
.iter()
.map(|clause| {
let mut literals = clause.0.clone();
literals.sort_unstable();
literals.dedup();
literals
})
.collect();
let structural = [
(0..vars).collect::<Vec<_>>(),
min_fill_order(vars, &formula),
min_degree_order(vars, &formula),
flower_outside_in_order(vars),
];
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create quotient output: {error}"))?;
}
let mut file =
fs::File::create(output).map_err(|error| format!("create quotient output: {error}"))?;
writeln!(file, "family,formula_seed,strategy,order_kind,reference_width,reference_kind,order_width,frontier_bound_bits,gate_limit_bits,gate_admitted,route,peak_classes,peak_class_bits,class_bit_change_vs_reference,canonical_work,bruteforce_literal_work,work_ratio,final_classes,sat,witness_valid,gate_ns,quotient_ns,varisat_ns,routed_total_ns,time_ratio_vs_varisat")
.map_err(|error| format!("write quotient header: {error}"))?;
let literal_count = formula.iter().map(|clause| clause.0.len()).sum::<usize>();
let brute_work =
(1u128.checked_shl(vars as u32).unwrap_or(u128::MAX)).saturating_mul(literal_count as u128);
let brute_work_float = 2f64.powi(vars as i32) * literal_count as f64;
let varisat_start = Instant::now();
let varisat_witness = solve_with_varisat(vars, &formula);
let varisat_ns = varisat_start.elapsed().as_nanos().max(1);
for strategy in 0..strategies {
let kind = strategy % 5;
let mut order = if kind < 4 {
structural[kind].clone()
} else {
let mut order: Vec<_> = (0..vars).collect();
Rng((strategy as u64 + 1).wrapping_mul(0x9e37_79b9)).shuffle(&mut order);
order
};
if kind < 4 {
let variant = strategy / 5;
order.rotate_left(variant % vars.max(1));
if variant & 1 == 1 {
order.reverse();
}
}
let order_width = elimination_cost(vars, &formula, &order).0;
let gate_start = Instant::now();
let frontier_bound_bits = continuation_frontier_bound_bits(vars, &formula, &order);
let gate_ns = gate_start.elapsed().as_nanos();
let gate_admitted = gate_limit_bits.is_none_or(|limit| frontier_bound_bits <= limit);
if !gate_admitted {
let sat = varisat_witness.is_some();
let routed_total_ns = gate_ns.saturating_add(varisat_ns);
writeln!(file, "{family},{formula_seed},{},{},{reference_width},{reference_kind},{order_width},{frontier_bound_bits},{},{gate_admitted},varisat,0,0,0,0,{brute_work},0.000000000,0,{sat},true,{gate_ns},0,{varisat_ns},{routed_total_ns},{:.6}", strategy + 1, ["natural", "min-fill", "min-degree", "flower", "random"][kind], gate_limit_bits.unwrap_or(0), routed_total_ns as f64 / varisat_ns as f64)
.map_err(|error| format!("write gated quotient row: {error}"))?;
continue;
}
let quotient_start = Instant::now();
let mut classes: HashMap<Vec<Vec<Literal>>, Vec<bool>> = HashMap::new();
classes.insert(base.clone(), vec![false; vars]);
let mut peak_classes = 1usize;
let mut work = 0usize;
for &variable in &order {
let mut next = HashMap::new();
for (residual, representative) in classes {
for value in [false, true] {
let canonical =
canonical_residual_after_choice(&residual, variable, value, &mut work);
let mut assignment = representative.clone();
assignment[variable] = value;
next.entry(canonical).or_insert(assignment);
}
}
classes = next;
peak_classes = peak_classes.max(classes.len());
}
let quotient_ns = quotient_start.elapsed().as_nanos();
let satisfying = classes.get(&Vec::<Vec<Literal>>::new());
let sat = satisfying.is_some();
let witness_valid = satisfying.is_some_and(|assignment| satisfies(&formula, assignment))
|| (!sat && varisat_witness.is_none());
let class_bits = ceil_log2(peak_classes);
let routed_total_ns = gate_ns.saturating_add(quotient_ns);
writeln!(file, "{family},{formula_seed},{},{},{reference_width},{reference_kind},{order_width},{frontier_bound_bits},{},{gate_admitted},quotient,{peak_classes},{class_bits},{},{work},{brute_work},{:.9},{},{sat},{witness_valid},{gate_ns},{quotient_ns},{varisat_ns},{routed_total_ns},{:.6}", strategy + 1, ["natural", "min-fill", "min-degree", "flower", "random"][kind], gate_limit_bits.unwrap_or(0), class_bits as isize - reference_width as isize, work as f64 / brute_work_float.max(1.0), classes.len(), routed_total_ns as f64 / varisat_ns as f64)
.map_err(|error| format!("write quotient row: {error}"))?;
}
file.flush()
.map_err(|error| format!("flush quotient output: {error}"))?;
println!(
"continuation quotient search family={family} seed={formula_seed} strategies={strategies} reference_width={reference_width} reference_kind={reference_kind} output={}",
output.display()
);
Ok(())
}
fn benchmark_bdd_network_expansion(
family: &str,
vars: usize,
ratio: usize,
formula_seed: u64,
random_orders: usize,
output: &Path,
) -> Result<(), String> {
if vars > 20 {
return Err(
"BDD network expansion requires an exact original width (max 20 vars)".to_string(),
);
}
let original = generate_formula(family, vars, ratio, formula_seed);
let original_width = exact_treewidth(vars, &original);
let original_sat = solve_with_varisat(vars, &original).is_some();
let graph = primal_graph(vars, &original);
let mut orders = vec![
("natural".to_string(), (0..vars).collect::<Vec<_>>()),
("min-fill".to_string(), min_fill_order(vars, &original)),
("min-degree".to_string(), min_degree_order(vars, &original)),
("flower".to_string(), flower_outside_in_order(vars)),
];
for index in 0..random_orders {
let mut order: Vec<_> = (0..vars).collect();
Rng(formula_seed ^ (index as u64 + 1).wrapping_mul(0x517c_c1b7)).shuffle(&mut order);
orders.push((format!("random-{index}"), order));
}
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create network output: {error}"))?;
}
let mut file =
fs::File::create(output).map_err(|error| format!("create network output: {error}"))?;
writeln!(file, "family,formula_seed,strategy,order,prefix,interior,boundary,original_width,expanded_vars,expanded_clauses,relation_helpers,expanded_upper_width,reconstruction_live_nodes,reconstruction_allocated_nodes,reconstruction_information_charge,fully_charged_width,certified_change,sat_equivalent,reconstruction_valid")
.map_err(|error| format!("write network header: {error}"))?;
let mut strategy = 0usize;
for (order_name, order) in orders {
for prefix in 1..vars {
strategy += 1;
let mut interior = order[..prefix].to_vec();
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let boundary: Vec<_> = interior
.iter()
.flat_map(|&variable| graph[variable].iter().copied())
.filter(|variable| !interior_set.contains(variable))
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let mut manager = BddManager::default();
let reconstruction_seed = seed_bdd_candidate_in(
vars,
&original,
&mut interior,
boundary.clone(),
"min-fill",
&mut manager,
);
let (expanded_vars, expanded, helpers) =
projected_bdd_network_cnf(vars, &original, &interior, &boundary);
let expanded_width = elimination_cost(
expanded_vars,
&expanded,
&min_fill_order(expanded_vars, &expanded),
)
.0;
let reconstruction_charge = ceil_log2(reconstruction_seed.live_nodes.saturating_add(2));
let fully_charged_width = expanded_width.max(reconstruction_charge);
let expanded_assignment = solve_with_varisat(expanded_vars, &expanded);
let expanded_sat = expanded_assignment.is_some();
let reconstruction_valid = if let Some(values) = expanded_assignment {
let mut mapped = vec![false; vars];
for (core, &original_variable) in
reconstruction_seed.core_to_original.iter().enumerate()
{
mapped[original_variable] = values[core];
}
regrow_bdd_seed(&reconstruction_seed, &mapped).is_some_and(|inside| {
for (index, &variable) in reconstruction_seed.interior.iter().enumerate() {
mapped[variable] = inside[index];
}
satisfies(&original, &mapped)
})
} else {
!original_sat
};
writeln!(file, "{family},{formula_seed},{strategy},{order_name},{prefix},{},{},{original_width},{expanded_vars},{},{helpers},{expanded_width},{},{},{reconstruction_charge},{fully_charged_width},{},{},{}", interior.len(), boundary.len(), expanded.len(), reconstruction_seed.live_nodes, reconstruction_seed.allocated_nodes, fully_charged_width as isize - original_width as isize, expanded_sat == original_sat, reconstruction_valid)
.map_err(|error| format!("write network row: {error}"))?;
}
}
file.flush()
.map_err(|error| format!("flush network output: {error}"))?;
println!(
"BDD network expansion family={family} seed={formula_seed} strategies={strategy} original_width={original_width} output={}",
output.display()
);
Ok(())
}
fn benchmark_query_calibrated(
input: &Path,
output: &Path,
calibration_queries: usize,
evaluation_queries: usize,
deadline: std::time::Duration,
gate: &str,
) -> Result<(), String> {
let total = calibration_queries.saturating_add(evaluation_queries);
let stem = output
.file_stem()
.and_then(|value| value.to_str())
.unwrap_or("calibrated");
let parent = output.parent().unwrap_or_else(|| Path::new("."));
let calibration_path = parent.join(format!("{stem}-calibration.csv"));
let evaluation_path = parent.join(format!("{stem}-evaluation.csv"));
let baseline_path = parent.join(format!("{stem}-baseline.csv"));
benchmark_query_portfolio(
input,
&calibration_path,
0,
calibration_queries,
total,
deadline,
&[gate.to_string()],
)?;
let calibration_text = fs::read_to_string(&calibration_path)
.map_err(|error| format!("read calibration: {error}"))?;
let helper_wins = calibration_text
.lines()
.skip(1)
.filter(|row| row.split(',').nth(4) == Some(gate))
.count();
let selected = helper_wins > 0;
let selected_gates = if selected {
vec![gate.to_string()]
} else {
Vec::new()
};
benchmark_query_portfolio(
input,
&evaluation_path,
calibration_queries,
evaluation_queries,
total,
deadline,
&selected_gates,
)?;
benchmark_query_portfolio(input, &baseline_path, 0, total, total, deadline, &[])?;
let (calibration_completed, calibration_worker_ns) = portfolio_totals(&calibration_path)?;
let (evaluation_completed, evaluation_worker_ns) = portfolio_totals(&evaluation_path)?;
let (baseline_completed, baseline_worker_ns) = portfolio_totals(&baseline_path)?;
let calibrated_worker_ns = calibration_worker_ns.saturating_add(evaluation_worker_ns);
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create calibrated output: {error}"))?;
}
fs::write(
output,
format!(
"path,gate,selected,calibration_queries,helper_wins,calibration_completed,evaluation_queries,evaluation_completed,baseline_completed,calibrated_worker_wall_ns,baseline_worker_wall_ns,worker_wall_delta_ns\n{},{},{},{},{},{},{},{},{},{},{},{}\n",
input.to_string_lossy().replace(',', "%2C"), gate, selected, calibration_queries,
helper_wins, calibration_completed, evaluation_queries, evaluation_completed,
baseline_completed, calibrated_worker_ns, baseline_worker_ns,
calibrated_worker_ns as i128 - baseline_worker_ns as i128
),
)
.map_err(|error| format!("write calibrated output: {error}"))?;
println!(
"calibrated selected={} helper_wins={} evaluation_completed={}/{} baseline_completed={}/{} worker_delta_ms={:.3}",
selected,
helper_wins,
evaluation_completed,
evaluation_queries,
baseline_completed,
total,
(calibrated_worker_ns as i128 - baseline_worker_ns as i128) as f64 / 1e6
);
Ok(())
}
fn export_balanced_candidates(input: &Path, output: &Path) -> Result<(), String> {
let (vars, clauses) = parse_dimacs(input)?;
let candidates = fast_detachable_branch_candidates(vars, &clauses, 64);
let incidence = clause_incidence(vars, &clauses);
let mut rows = vec!["ordinal,interior,boundary,local_clauses,local_literals,local_binary,summary_clauses,summary_literals,summary_binary,live_bdd_nodes,allocated_bdd_nodes".to_string()];
let mut ordinal = 0usize;
for (interior, boundary) in candidates {
let attempt = try_indexed_seed_bdd_candidate(
vars,
&clauses,
&incidence,
interior,
boundary,
100_000,
std::time::Duration::from_millis(100),
);
let Some(seed) = attempt.seed else { continue };
let local = indexed_local_clauses(&seed.interior, &incidence, &clauses);
if !solver_gate_accepts("balanced", &seed, &local) {
continue;
}
let local_literals: usize = local.iter().map(|clause| clause.0.len()).sum();
let local_binary = local.iter().filter(|clause| clause.0.len() == 2).count();
let summary_literals: usize = seed.summary.iter().map(|clause| clause.0.len()).sum();
let summary_binary = seed
.summary
.iter()
.filter(|clause| clause.0.len() == 2)
.count();
rows.push(format!(
"{},{},{},{},{},{},{},{},{},{},{}",
ordinal,
seed.interior.len(),
seed.boundary.len(),
local.len(),
local_literals,
local_binary,
seed.summary.len(),
summary_literals,
summary_binary,
seed.live_nodes,
seed.allocated_nodes
));
ordinal += 1;
}
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create candidate output: {error}"))?;
}
fs::write(output, format!("{}\n", rows.join("\n")))
.map_err(|error| format!("write candidate output: {error}"))?;
println!(
"balanced_candidates={} output={}",
ordinal,
output.display()
);
Ok(())
}
fn graph_distances(graph: &[Vec<usize>], start: usize) -> Vec<usize> {
let mut distance = vec![usize::MAX; graph.len()];
if start >= graph.len() {
return distance;
}
distance[start] = 0;
let mut queue = VecDeque::from([start]);
while let Some(variable) = queue.pop_front() {
for &next in &graph[variable] {
if distance[next] == usize::MAX {
distance[next] = distance[variable] + 1;
queue.push_back(next);
}
}
}
distance
}
fn export_query_candidate_features(
input: &Path,
output: &Path,
queries: &[(usize, bool)],
) -> Result<(), String> {
let (vars, clauses) = parse_dimacs(input)?;
let graph = compact_primal_graph(vars, &clauses);
let candidates = global_small_separator_candidates(&graph, 64);
let incidence = clause_incidence(vars, &clauses);
let query_distances: Vec<_> = queries
.iter()
.map(|&(variable, _)| graph_distances(&graph, variable))
.collect();
let mut rows = vec!["ordinal,query_variable,query_value,interior,boundary,boundary_degree_sum,boundary_degree_max,boundary_occurrences,interior_degree_sum,query_to_boundary,query_to_interior,query_neighbour_overlap".to_string()];
let mut ordinal = 0usize;
for (interior, boundary) in candidates {
let attempt = try_indexed_seed_bdd_candidate(
vars,
&clauses,
&incidence,
interior,
boundary,
100_000,
std::time::Duration::from_millis(100),
);
let Some(seed) = attempt.seed else { continue };
let local = indexed_local_clauses(&seed.interior, &incidence, &clauses);
if !solver_gate_accepts("balanced", &seed, &local) {
continue;
}
let boundary_degree_sum: usize = seed.boundary.iter().map(|&v| graph[v].len()).sum();
let boundary_degree_max = seed
.boundary
.iter()
.map(|&v| graph[v].len())
.max()
.unwrap_or(0);
let boundary_occurrences: usize = seed.boundary.iter().map(|&v| incidence[v].len()).sum();
let interior_degree_sum: usize = seed.interior.iter().map(|&v| graph[v].len()).sum();
let interior_set: BTreeSet<_> = seed.interior.iter().copied().collect();
let boundary_set: BTreeSet<_> = seed.boundary.iter().copied().collect();
for (query_index, &(query_variable, query_value)) in queries.iter().enumerate() {
let distances = &query_distances[query_index];
let boundary_distance = seed
.boundary
.iter()
.map(|&v| distances[v])
.min()
.unwrap_or(usize::MAX);
let interior_distance = seed
.interior
.iter()
.map(|&v| distances[v])
.min()
.unwrap_or(usize::MAX);
let overlap = graph[query_variable]
.iter()
.filter(|v| interior_set.contains(v) || boundary_set.contains(v))
.count();
let display_distance = |distance: usize| {
if distance == usize::MAX {
"-1".to_string()
} else {
distance.to_string()
}
};
rows.push(format!(
"{},{},{},{},{},{},{},{},{},{},{},{}",
ordinal,
query_variable + 1,
query_value,
seed.interior.len(),
seed.boundary.len(),
boundary_degree_sum,
boundary_degree_max,
boundary_occurrences,
interior_degree_sum,
display_distance(boundary_distance),
display_distance(interior_distance),
overlap
));
}
ordinal += 1;
}
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create query features: {error}"))?;
}
fs::write(output, format!("{}\n", rows.join("\n")))
.map_err(|error| format!("write query features: {error}"))?;
println!(
"balanced_candidates={} queries={} output={}",
ordinal,
queries.len(),
output.display()
);
Ok(())
}
fn benchmark_corpus_inner(root: &Path, output_path: &Path, queries: usize) -> Result<(), String> {
let mut paths = Vec::new();
find_dimacs_files(root, &mut paths)?;
paths.sort();
if paths.is_empty() {
return Err(format!(
"no .cnf or .dimacs files found under {}",
root.display()
));
}
let mut rows = vec![CORPUS_HEADER.to_string()];
for (case_index, path) in paths.iter().enumerate() {
let (vars, clauses) = parse_dimacs(path)?;
if vars == 0 {
continue;
}
let candidate_count = fast_detachable_branch_candidates(vars, &clauses, 64).len();
let compile_start = Instant::now();
let artifact = compile_safe_artifact(vars, &clauses, 64, 100_000, 100);
let compile_ns = compile_start.elapsed().as_nanos();
let artifact_path = std::env::temp_dir().join(format!(
"layered-sat-corpus-{}-{case_index}.lsat",
std::process::id()
));
save_compiled_artifact(&artifact_path, &artifact)?;
let artifact_bytes = fs::metadata(&artifact_path)
.map_err(|error| format!("stat {}: {error}", artifact_path.display()))?
.len();
fs::remove_file(&artifact_path)
.map_err(|error| format!("remove {}: {error}", artifact_path.display()))?;
let baseline_setup_start = Instant::now();
let mut baseline = Solver::new();
add_to_varisat(&mut baseline, &clauses);
let baseline_setup_ns = baseline_setup_start.elapsed().as_nanos();
let compiled_setup_start = Instant::now();
let mut direct = Solver::new();
add_to_varisat(&mut direct, &artifact.core_clauses);
let mut reopened_solvers = Vec::new();
for seed_index in 0..artifact.seeds.len() {
let mut solver = Solver::new();
add_to_varisat(&mut solver, &reopened_formula(&artifact, seed_index));
reopened_solvers.push(solver);
}
let compiled_setup_ns = compiled_setup_start.elapsed().as_nanos();
let mut original_to_core = vec![usize::MAX; vars];
for (core, &original) in artifact.core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
let mut owner = vec![usize::MAX; vars];
for (seed_index, seed) in artifact.seeds.iter().enumerate() {
for &variable in &seed.interior {
owner[variable] = seed_index;
}
}
let mut baseline_query_ns = 0u128;
let mut direct_query_ns = 0u128;
let mut reopened_query_ns = 0u128;
let mut direct_queries = 0usize;
let mut reopened_queries = 0usize;
let mut all_agree = true;
let mut witnesses_valid = true;
for query in 0..queries {
let variable = query % vars;
let value = (query / vars + query) % 2 == 0;
baseline.assume(&[Lit::from_var(Var::from_index(variable), value)]);
let start = Instant::now();
let baseline_sat = baseline
.solve()
.map_err(|error| format!("baseline solve: {error}"))?;
baseline_query_ns += start.elapsed().as_nanos();
let compiled_sat = if owner[variable] == usize::MAX {
direct.assume(&[Lit::from_var(
Var::from_index(original_to_core[variable]),
value,
)]);
let start = Instant::now();
let sat = direct
.solve()
.map_err(|error| format!("direct solve: {error}"))?;
direct_query_ns += start.elapsed().as_nanos();
direct_queries += 1;
sat
} else {
let solver = &mut reopened_solvers[owner[variable]];
solver.assume(&[Lit::from_var(Var::from_index(variable), value)]);
let start = Instant::now();
let sat = solver
.solve()
.map_err(|error| format!("reopened solve: {error}"))?;
reopened_query_ns += start.elapsed().as_nanos();
reopened_queries += 1;
sat
};
all_agree &= baseline_sat == compiled_sat;
if query < 4 || query + 1 == queries {
let reconstructed = query_compiled_artifact(&artifact, &[(variable, value)])?;
witnesses_valid &= reconstructed.is_some() == baseline_sat;
witnesses_valid &= reconstructed.as_ref().is_none_or(|assignment| {
assignment[variable] == value && satisfies(&clauses, assignment)
});
}
}
let compiled_query_ns = direct_query_ns + reopened_query_ns;
let removed = vars - artifact.core_vars;
let path_text = path.to_string_lossy().replace(',', "%2C");
rows.push(format!(
"{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{:.6},{},{},{},ok",
path_text,
vars,
clauses.len(),
candidate_count,
artifact.seeds.len(),
candidate_count.saturating_sub(artifact.seeds.len()),
removed,
removed as f64 / vars as f64,
compile_ns,
artifact_bytes,
artifact
.seeds
.iter()
.map(|seed| seed.manager.nodes.len())
.sum::<usize>(),
baseline_setup_ns,
compiled_setup_ns,
queries,
direct_queries,
reopened_queries,
baseline_query_ns,
direct_query_ns,
reopened_query_ns,
compiled_query_ns,
compiled_query_ns as f64 / baseline_query_ns.max(1) as f64,
(compile_ns + compiled_setup_ns + compiled_query_ns) as f64
/ (baseline_setup_ns + baseline_query_ns).max(1) as f64,
removed * 10 >= vars * 3,
all_agree,
witnesses_valid
));
}
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create {}: {error}", parent.display()))?;
}
fs::write(output_path, rows.join("\n") + "\n")
.map_err(|error| format!("write {}: {error}", output_path.display()))?;
println!(
"benchmarked formulas={} queries_per_formula={} output={}",
rows.len() - 1,
queries,
output_path.display()
);
Ok(())
}
fn benchmark_corpus_isolated(
root: &Path,
output_path: &Path,
queries: usize,
timeout_seconds: u64,
) -> Result<(), String> {
let mut paths = Vec::new();
find_dimacs_files(root, &mut paths)?;
paths.sort();
if paths.is_empty() {
return Err(format!(
"no .cnf or .dimacs files found under {}",
root.display()
));
}
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("create {}: {error}", parent.display()))?;
}
let mut completed = BTreeSet::new();
if output_path.exists() {
let body = fs::read_to_string(output_path)
.map_err(|error| format!("read {}: {error}", output_path.display()))?;
for line in body.lines().skip(1) {
if let Some(path) = line.split(',').next() {
completed.insert(path.replace("%2C", ","));
}
}
} else {
fs::write(output_path, format!("{CORPUS_HEADER}\n"))
.map_err(|error| format!("write {}: {error}", output_path.display()))?;
}
let executable =
env::current_exe().map_err(|error| format!("locate current executable: {error}"))?;
let mut attempted = 0usize;
for (index, path) in paths.iter().enumerate() {
let path_text = path.to_string_lossy().to_string();
if completed.contains(&path_text) {
continue;
}
attempted += 1;
let temporary = std::env::temp_dir().join(format!(
"layered-sat-isolated-{}-{index}.csv",
std::process::id()
));
let mut child = Command::new(&executable)
.arg("benchmark-single")
.arg(path)
.arg(&temporary)
.arg(queries.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|error| format!("spawn benchmark for {}: {error}", path.display()))?;
let start = Instant::now();
let status = loop {
if let Some(status) = child
.try_wait()
.map_err(|error| format!("wait for {}: {error}", path.display()))?
{
break if status.success() {
"ok"
} else {
"child-error"
};
}
if start.elapsed() >= std::time::Duration::from_secs(timeout_seconds) {
child
.kill()
.map_err(|error| format!("kill timed out {}: {error}", path.display()))?;
child
.wait()
.map_err(|error| format!("reap {}: {error}", path.display()))?;
break "timeout";
}
thread::sleep(std::time::Duration::from_millis(100));
};
let row = if status == "ok" {
let body = fs::read_to_string(&temporary).map_err(|error| {
format!("read isolated result {}: {error}", temporary.display())
})?;
body.lines()
.nth(1)
.ok_or_else(|| format!("missing isolated row for {}", path.display()))?
.to_string()
} else {
format!(
"{}{}{}",
path_text.replace(',', "%2C"),
",".repeat(25),
status
)
};
let mut output = fs::OpenOptions::new()
.append(true)
.open(output_path)
.map_err(|error| format!("append {}: {error}", output_path.display()))?;
writeln!(output, "{row}")
.map_err(|error| format!("append {}: {error}", output_path.display()))?;
output
.flush()
.map_err(|error| format!("flush {}: {error}", output_path.display()))?;
let _ = fs::remove_file(&temporary);
println!(
"[{}/{}] {} status={}",
index + 1,
paths.len(),
path.display(),
status
);
}
println!(
"corpus complete discovered={} attempted={} output={}",
paths.len(),
attempted,
output_path.display()
);
Ok(())
}
fn predicate_cli_contract_line() -> String {
format!(
"predicate_cli_version={PREDICATE_CLI_CONTRACT_VERSION} certificate_versions={PREDICATE_CERTIFICATE_VERSION},{PREDICATE_CERTIFICATE_V2_VERSION} portfolio_certificate_version={PREDICATE_CERTIFICATE_VERSION} proof_format={PREDICATE_CERTIFICATE_V2_PROOF_FORMAT} min_relevant_inputs={PREDICATE_INTERFACE_MIN_INPUTS} max_relevant_inputs={PREDICATE_INTERFACE_MAX_INPUTS} max_latches={PREDICATE_INTERFACE_MAX_LATCHES} max_horizon={INTERFACE_QUOTIENT_MAX_HORIZON} max_certificate_v2_bytes={PREDICATE_CERTIFICATE_V2_MAX_BYTES} max_proof_bytes={PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES} max_total_proof_bytes={PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES}"
)
}
fn event_contract_cli_contract_line() -> String {
format!(
"event_contract_cli_version={EVENT_CONTRACT_CLI_CONTRACT_VERSION} certificate_version={EVENT_CONTRACT_CERTIFICATE_VERSION} portfolio_version={EVENT_CONTRACT_PORTFOLIO_VERSION} semantics={EVENT_CONTRACT_CERTIFICATE_SEMANTICS} proof_format={PREDICATE_CERTIFICATE_V2_PROOF_FORMAT} min_relevant_inputs={PREDICATE_INTERFACE_MIN_INPUTS} max_relevant_inputs={PREDICATE_INTERFACE_MAX_INPUTS} max_latches={PREDICATE_INTERFACE_MAX_LATCHES} max_horizon={INTERFACE_QUOTIENT_MAX_HORIZON} max_contract_bytes={EVENT_CONTRACT_MAX_BYTES} max_certificate_bytes={EVENT_CONTRACT_CERTIFICATE_MAX_BYTES} max_proof_bytes={PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES} max_total_proof_bytes={PREDICATE_CERTIFICATE_V2_MAX_TOTAL_PROOF_BYTES}"
)
}
fn read_bounded_regular_file(path: &Path, limit: usize, label: &str) -> Result<Vec<u8>, String> {
let mut options = fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
options.custom_flags(libc::O_NOFOLLOW);
let file = options
.open(path)
.map_err(|error| format!("open {label} {}: {error}", path.display()))?;
let metadata = file
.metadata()
.map_err(|error| format!("inspect open {label} {}: {error}", path.display()))?;
if !metadata.file_type().is_file() {
return Err(format!("{label} must be a regular non-symlink file"));
}
if metadata.len() > limit as u64 {
return Err(format!("{label} exceeds {limit} bytes"));
}
let mut bytes = Vec::new();
file.take((limit + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|error| format!("read {label} {}: {error}", path.display()))?;
if bytes.len() > limit {
return Err(format!("{label} exceeds {limit} bytes"));
}
Ok(bytes)
}
fn parse_component_batch_manifest(
manifest_path: &Path,
) -> Result<Vec<ComponentBatchManifestMember>, String> {
use std::path::Component;
let bytes = read_bounded_regular_file(
manifest_path,
BTOR2_COMPONENT_BATCH_MANIFEST_MAX_BYTES,
"component batch manifest",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "component batch manifest is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err("component batch manifest must be canonical LF text without NUL".to_string());
}
let mut lines = text.lines();
let mut take = |key: &str| -> Result<&str, String> {
lines
.next()
.and_then(|line| line.strip_prefix(&format!("{key}=")))
.ok_or_else(|| format!("component batch manifest expected {key}"))
};
let version = take("component_batch_manifest_version")?
.parse::<u32>()
.map_err(|_| "component batch manifest version is invalid".to_string())?;
if version != BTOR2_COMPONENT_BATCH_MANIFEST_VERSION {
return Err("unsupported component batch manifest version".to_string());
}
let count_text = take("member_count")?;
let count = count_text
.parse::<usize>()
.map_err(|_| "component batch member count is invalid".to_string())?;
if count.to_string() != count_text
|| count == 0
|| count > btor2_component::MAX_COMPONENT_BATCH_MEMBERS
{
return Err("component batch member count is outside limit or noncanonical".to_string());
}
let base = manifest_path.parent().unwrap_or_else(|| Path::new("."));
let mut members = Vec::with_capacity(count);
let mut canonical = format!(
"component_batch_manifest_version={BTOR2_COMPONENT_BATCH_MANIFEST_VERSION}\nmember_count={count}\n"
);
for _ in 0..count {
let plant = take("plant_path")?;
let contract = take("contract_path")?;
let horizon_text = take("horizon")?;
let horizon = horizon_text
.parse::<u32>()
.map_err(|_| "component batch horizon is invalid".to_string())?;
if horizon.to_string() != horizon_text {
return Err("component batch horizon is noncanonical".to_string());
}
let validate_path = |value: &str, label: &str| -> Result<PathBuf, String> {
if value.is_empty() || value.len() > 4096 {
return Err(format!("component batch {label} path length is invalid"));
}
let path = Path::new(value);
if path.is_absolute()
|| path
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(format!(
"component batch {label} path must be a normalized relative path"
));
}
Ok(base.join(path))
};
members.push(ComponentBatchManifestMember {
plant_path: validate_path(plant, "plant")?,
contract_path: validate_path(contract, "contract")?,
horizon,
});
canonical.push_str(&format!(
"plant_path={plant}\ncontract_path={contract}\nhorizon={horizon}\n"
));
}
if take("status")? != "complete" || lines.next().is_some() {
return Err("component batch manifest is incomplete or has trailing fields".to_string());
}
canonical.push_str("status=complete\n");
if canonical != text {
return Err("component batch manifest is not canonical".to_string());
}
Ok(members)
}
fn parse_source_model_provenance_manifest(
manifest_path: &Path,
) -> Result<SourceModelProvenanceManifest, String> {
use std::path::Component;
let bytes = read_bounded_regular_file(
manifest_path,
SOURCE_MODEL_PROVENANCE_MANIFEST_MAX_BYTES,
"source-model provenance manifest",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "source-model provenance manifest is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"source-model provenance manifest must be canonical LF text without NUL".to_string(),
);
}
let mut lines = text.lines();
let mut take = |key: &str| -> Result<&str, String> {
lines
.next()
.and_then(|line| line.strip_prefix(&format!("{key}=")))
.ok_or_else(|| format!("source-model provenance manifest expected {key}"))
};
let version = take("source_model_provenance_manifest_version")?;
if version != SOURCE_MODEL_PROVENANCE_MANIFEST_VERSION.to_string() {
return Err("unsupported source-model provenance manifest version".to_string());
}
let tool = take("tool")?;
if tool != "yosys" {
return Err("source-model provenance manifest tool is unsupported".to_string());
}
let tool_revision = take("tool_revision")?;
if tool_revision.len() != 40
|| !tool_revision
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err("source-model provenance manifest tool revision is invalid".to_string());
}
let count_text = take("member_count")?;
let count = count_text
.parse::<usize>()
.map_err(|_| "source-model provenance member count is invalid".to_string())?;
if count.to_string() != count_text
|| count == 0
|| count > source_model_attestation::MAX_ATTESTATION_MEMBERS
{
return Err("source-model provenance member count is outside limits".to_string());
}
let base = fs::canonicalize(manifest_path.parent().unwrap_or_else(|| Path::new(".")))
.map_err(|error| format!("resolve source-model provenance directory: {error}"))?;
let resolve = |workdir: &str, value: &str, label: &str| -> Result<PathBuf, String> {
if value.is_empty() || value.len() > 4096 || workdir.is_empty() || workdir.len() > 4096 {
return Err(format!(
"source-model provenance {label} path length is invalid"
));
}
let workdir_path = Path::new(workdir);
let value_path = Path::new(value);
let workdir_valid = workdir == "."
|| (!workdir_path.is_absolute()
&& workdir_path
.components()
.all(|component| matches!(component, Component::Normal(_))));
if !workdir_valid
|| value_path.is_absolute()
|| value_path
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(format!(
"source-model provenance {label} path must be normalized and relative"
));
}
let mut resolved = base.clone();
if workdir != "." {
resolved.push(workdir_path);
}
resolved.push(value_path);
let relative = resolved
.strip_prefix(&base)
.map_err(|_| format!("source-model provenance {label} escapes its directory"))?;
let mut checked = base.clone();
for component in relative.components() {
let Component::Normal(component) = component else {
return Err(format!("source-model provenance {label} path is invalid"));
};
checked.push(component);
let metadata = fs::symlink_metadata(&checked).map_err(|error| {
format!(
"inspect source-model provenance {label} path {}: {error}",
checked.display()
)
})?;
if metadata.file_type().is_symlink() {
return Err(format!(
"source-model provenance {label} path must not contain symlinks"
));
}
}
Ok(checked)
};
let mut canonical = format!(
"source_model_provenance_manifest_version={version}\ntool={tool}\ntool_revision={tool_revision}\nmember_count={count_text}\n"
);
let mut members = Vec::with_capacity(count);
for _ in 0..count {
let workdir = take("workdir")?;
let source = take("source_path")?;
let recipe = take("recipe_path")?;
let model = take("model_path")?;
members.push(SourceModelProvenanceMember {
source_path: resolve(workdir, source, "source")?,
recipe_path: resolve(workdir, recipe, "recipe")?,
model_path: resolve(workdir, model, "model")?,
});
canonical.push_str(&format!(
"workdir={workdir}\nsource_path={source}\nrecipe_path={recipe}\nmodel_path={model}\n"
));
}
if take("status")? != "complete" || lines.next().is_some() {
return Err(
"source-model provenance manifest is incomplete or has trailing fields".to_string(),
);
}
canonical.push_str("status=complete\n");
if canonical != text {
return Err("source-model provenance manifest is not canonical".to_string());
}
Ok(SourceModelProvenanceManifest {
tool: tool.to_string(),
tool_revision: tool_revision.to_string(),
members,
})
}
fn verify_bound_source_model_provenance(
query: &ControllerMtbddPlantManifest,
loaded: &LoadedSourceModelSnapshot,
provenance_path: &Path,
evidence_path: &Path,
) -> Result<source_model_attestation::SourceModelAttestationSummary, String> {
let provenance = parse_source_model_provenance_manifest(provenance_path)?;
let mut expected_pairs = vec![(
query.controller_source_path.clone(),
query.controller_aiger_path.clone(),
)];
for member in &query.members {
let pair = (
member.plant_source_path.clone(),
member.plant_aiger_path.clone(),
);
if !expected_pairs.contains(&pair) {
expected_pairs.push(pair);
}
}
if loaded.subjects.len() != expected_pairs.len()
|| loaded
.subjects
.iter()
.zip(&expected_pairs)
.any(|(subject, expected)| {
subject.source_path != expected.0 || subject.model_path != expected.1
})
|| provenance.members.len() != expected_pairs.len()
|| provenance
.members
.iter()
.zip(&expected_pairs)
.any(|(member, expected)| {
member.source_path != expected.0 || member.model_path != expected.1
})
{
return Err(
"source-model provenance subjects do not exactly match the portfolio query".to_string(),
);
}
let mut owned = Vec::with_capacity(provenance.members.len());
for member in &provenance.members {
owned.push((
read_bounded_regular_file(
&member.source_path,
AAG_INPUT_LIMIT_BYTES as usize,
"attested source",
)?,
read_bounded_regular_file(
&member.recipe_path,
SOURCE_MODEL_PROVENANCE_MANIFEST_MAX_BYTES,
"attested synthesis recipe",
)?,
read_bounded_regular_file(
&member.model_path,
AAG_INPUT_LIMIT_BYTES as usize,
"attested model",
)?,
));
}
let inputs = owned
.iter()
.map(|(source, recipe, model)| SourceModelBindingInput {
source,
recipe,
model,
})
.collect::<Vec<_>>();
verify_loaded_source_model_snapshot(&owned, loaded)?;
let evidence = read_bounded_regular_file(
evidence_path,
source_model_attestation::MAX_ATTESTATION_BYTES,
"source-model attestation evidence",
)?;
let summary = source_model_attestation::verify_source_model_attestation(&evidence, &inputs)
.map_err(|error| error.to_string())?;
if summary.tool != provenance.tool || summary.tool_revision != provenance.tool_revision {
return Err(
"source-model attestation tool identity does not match its provenance manifest"
.to_string(),
);
}
Ok(summary)
}
fn verify_loaded_source_model_snapshot(
subjects: &[(Vec<u8>, Vec<u8>, Vec<u8>)],
loaded: &LoadedSourceModelSnapshot,
) -> Result<(), String> {
if subjects.len() != loaded.subjects.len() {
return Err("source-model subject count changed after the query snapshot".to_string());
}
for (index, ((source, _, model), snapshot)) in subjects.iter().zip(&loaded.subjects).enumerate()
{
if <[u8; 32]>::from(Sha256::digest(source)) != snapshot.source_sha256
|| <[u8; 32]>::from(Sha256::digest(model)) != snapshot.model_sha256
{
return Err(format!(
"source-model provenance subject {index} changed after the query snapshot"
));
}
}
Ok(())
}
fn parse_controller_mtbdd_plant_manifest(
manifest_path: &Path,
max_controller_inputs: usize,
max_controller_outputs: usize,
) -> Result<ControllerMtbddPlantManifest, String> {
use std::path::Component;
let bytes = read_bounded_regular_file(
manifest_path,
CONTROLLER_MTBDD_PLANT_MANIFEST_MAX_BYTES,
"controller MTBDD plant manifest",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "controller MTBDD plant manifest is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"controller MTBDD plant manifest must be canonical LF text without NUL".to_string(),
);
}
let mut lines = text.lines();
let mut take = |key: &str| -> Result<&str, String> {
lines
.next()
.and_then(|line| line.strip_prefix(&format!("{key}=")))
.ok_or_else(|| format!("controller MTBDD plant manifest expected {key}"))
};
let version_text = take("controller_mtbdd_plant_manifest_version")?;
if version_text != CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION.to_string() {
return Err("unsupported controller MTBDD plant manifest version".to_string());
}
let base = fs::canonicalize(manifest_path.parent().unwrap_or_else(|| Path::new(".")))
.map_err(|error| format!("resolve controller MTBDD manifest directory: {error}"))?;
let normalized_path = |value: &str, label: &str| -> Result<PathBuf, String> {
if value.is_empty() || value.len() > 4096 {
return Err(format!("controller MTBDD {label} path length is invalid"));
}
let path = Path::new(value);
if path.is_absolute()
|| path
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(format!(
"controller MTBDD {label} path must be a normalized relative path"
));
}
let mut resolved = base.clone();
for component in path.components() {
let Component::Normal(component) = component else {
unreachable!("path components were validated above");
};
resolved.push(component);
let metadata = fs::symlink_metadata(&resolved).map_err(|error| {
format!(
"inspect controller MTBDD {label} path {}: {error}",
resolved.display()
)
})?;
if metadata.file_type().is_symlink() {
return Err(format!(
"controller MTBDD {label} path must not contain symlinks"
));
}
}
Ok(resolved)
};
let parse_vector = |value: &str, label: &str, maximum: usize| -> Result<Vec<usize>, String> {
if value.is_empty() {
return Err(format!("controller MTBDD {label} vector is empty"));
}
let values = value
.split(',')
.map(|item| {
let parsed = item
.parse::<usize>()
.map_err(|_| format!("controller MTBDD {label} index is invalid"))?;
if parsed.to_string() != item {
return Err(format!("controller MTBDD {label} index is noncanonical"));
}
Ok(parsed)
})
.collect::<Result<Vec<_>, _>>()?;
if values.len() > maximum || values.windows(2).any(|pair| pair[0] >= pair[1]) {
return Err(format!(
"controller MTBDD {label} vector is outside limits or not strictly sorted"
));
}
Ok(values)
};
let parse_usize = |value: &str, label: &str| -> Result<usize, String> {
let parsed = value
.parse::<usize>()
.map_err(|_| format!("controller MTBDD {label} is invalid"))?;
if parsed.to_string() != value {
return Err(format!("controller MTBDD {label} is noncanonical"));
}
Ok(parsed)
};
let controller_source_text = take("controller_source_path")?;
let controller_source_path = normalized_path(controller_source_text, "controller source")?;
let controller_aiger_text = take("controller_aiger_path")?;
let controller_aiger_path = normalized_path(controller_aiger_text, "controller AIGER")?;
let relevant_text = take("relevant_inputs")?;
let relevant_inputs = parse_vector(relevant_text, "relevant inputs", max_controller_inputs)?;
let observed_text = take("observed_outputs")?;
let observed_outputs = parse_vector(observed_text, "observed outputs", max_controller_outputs)?;
let count_text = take("member_count")?;
let count = parse_usize(count_text, "member count")?;
if count == 0 || count > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS {
return Err("controller MTBDD plant member count is outside limit".to_string());
}
let mut canonical = format!(
"controller_mtbdd_plant_manifest_version={version_text}\ncontroller_source_path={controller_source_text}\ncontroller_aiger_path={controller_aiger_text}\nrelevant_inputs={relevant_text}\nobserved_outputs={observed_text}\nmember_count={count}\n"
);
let mut members = Vec::with_capacity(count);
for _ in 0..count {
let plant_source_text = take("plant_source_path")?;
let plant_source_path = normalized_path(plant_source_text, "plant source")?;
let plant_aiger_text = take("plant_aiger_path")?;
let plant_aiger_path = normalized_path(plant_aiger_text, "plant AIGER")?;
let controller_sensor_text = take("controller_sensor_inputs")?;
let controller_sensor_inputs = parse_vector(
controller_sensor_text,
"controller sensor inputs",
max_controller_inputs,
)?;
let controller_action_text = take("controller_action_outputs")?;
let controller_action_outputs = parse_vector(
controller_action_text,
"controller action outputs",
max_controller_outputs,
)?;
let plant_sensor_text = take("plant_sensor_outputs")?;
let plant_sensor_outputs = parse_vector(
plant_sensor_text,
"plant sensor outputs",
max_controller_inputs,
)?;
let plant_action_text = take("plant_action_inputs")?;
let plant_action_inputs = parse_vector(
plant_action_text,
"plant action inputs",
max_controller_outputs,
)?;
let initial_controller_text = take("initial_controller_state")?;
let initial_controller_state =
parse_usize(initial_controller_text, "initial controller state")?;
let initial_plant_text = take("initial_plant_state")?;
let initial_plant_state = parse_usize(initial_plant_text, "initial plant state")?;
let bad_output_text = take("bad_plant_output")?;
let bad_plant_output = parse_usize(bad_output_text, "bad plant output")?;
let horizon_text = take("horizon")?;
let horizon = parse_usize(horizon_text, "horizon")?;
canonical.push_str(&format!(
"plant_source_path={plant_source_text}\nplant_aiger_path={plant_aiger_text}\ncontroller_sensor_inputs={controller_sensor_text}\ncontroller_action_outputs={controller_action_text}\nplant_sensor_outputs={plant_sensor_text}\nplant_action_inputs={plant_action_text}\ninitial_controller_state={initial_controller_text}\ninitial_plant_state={initial_plant_text}\nbad_plant_output={bad_output_text}\nhorizon={horizon_text}\n"
));
members.push(ControllerMtbddPlantManifestMember {
plant_source_path,
plant_aiger_path,
wiring: ControllerPlantWiring {
controller_sensor_inputs,
controller_action_outputs,
plant_sensor_outputs,
plant_action_inputs,
},
initial_controller_state,
initial_plant_state,
bad_plant_output,
horizon,
});
}
if take("status")? != "complete" || lines.next().is_some() {
return Err(
"controller MTBDD plant manifest is incomplete or has trailing fields".to_string(),
);
}
canonical.push_str("status=complete\n");
if canonical != text {
return Err("controller MTBDD plant manifest is not canonical".to_string());
}
Ok(ControllerMtbddPlantManifest {
controller_source_path,
controller_aiger_path,
relevant_inputs,
observed_outputs,
members,
})
}
fn parse_controller_plant_resource_policy(
path: &Path,
) -> Result<ControllerPlantResourcePolicy, String> {
let bytes = read_bounded_regular_file(
path,
CONTROLLER_PLANT_RESOURCE_POLICY_MAX_BYTES,
"controller plant resource policy",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "controller plant resource policy is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"controller plant resource policy must be canonical LF text without NUL".to_string(),
);
}
let mut lines = text.lines();
let mut take = |key: &str| -> Result<&str, String> {
lines
.next()
.and_then(|line| line.strip_prefix(&format!("{key}=")))
.ok_or_else(|| format!("controller plant resource policy expected {key}"))
};
let version = take("controller_plant_resource_policy_version")?;
if version != CONTROLLER_PLANT_RESOURCE_POLICY_VERSION.to_string() {
return Err("unsupported controller plant resource policy version".to_string());
}
let parse = |value: &str, label: &str| -> Result<usize, String> {
let parsed = value
.parse::<usize>()
.map_err(|_| format!("controller plant resource policy {label} is invalid"))?;
if parsed.to_string() != value {
return Err(format!(
"controller plant resource policy {label} is noncanonical"
));
}
Ok(parsed)
};
let artifact_text = take("max_artifact_bytes")?;
let artifact_bytes = parse(artifact_text, "artifact bytes")?;
let members_text = take("max_members")?;
let members = parse(members_text, "members")?;
let horizon_text = take("max_member_horizon")?;
let horizon = parse(horizon_text, "member horizon")?;
let states_text = take("max_product_states_per_member")?;
let states = parse(states_text, "product states")?;
let transitions_text = take("max_transition_evaluations")?;
let transitions = parse(transitions_text, "transition evaluations")?;
if take("status")? != "complete" || lines.next().is_some() {
return Err(
"controller plant resource policy is incomplete or has trailing fields".to_string(),
);
}
let canonical = format!(
"controller_plant_resource_policy_version={version}\nmax_artifact_bytes={artifact_text}\nmax_members={members_text}\nmax_member_horizon={horizon_text}\nmax_product_states_per_member={states_text}\nmax_transition_evaluations={transitions_text}\nstatus=complete\n"
);
if canonical != text {
return Err("controller plant resource policy is not canonical".to_string());
}
let envelope = controller_plant_artifact::ControllerPlantResourceEnvelope::new(
artifact_bytes,
members,
horizon,
states,
transitions,
)
.map_err(|error| error.to_string())?;
Ok(ControllerPlantResourcePolicy { envelope })
}
fn parse_controller_proof_mtbdd_resource_policy(
path: &Path,
) -> Result<ControllerProofMtbddResourcePolicy, String> {
let bytes = read_bounded_regular_file(
path,
CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_MAX_BYTES,
"controller proof MTBDD resource policy",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "controller proof MTBDD resource policy is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"controller proof MTBDD resource policy must be canonical LF text without NUL"
.to_string(),
);
}
let mut lines = text.lines();
let mut take = |key: &str| -> Result<&str, String> {
lines
.next()
.and_then(|line| line.strip_prefix(&format!("{key}=")))
.ok_or_else(|| format!("controller proof MTBDD resource policy expected {key}"))
};
let version = take("controller_proof_mtbdd_resource_policy_version")?;
if version != CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_VERSION.to_string() {
return Err("unsupported controller proof MTBDD resource policy version".to_string());
}
let parse = |value: &str, label: &str| -> Result<usize, String> {
let parsed = value
.parse::<usize>()
.map_err(|_| format!("controller proof MTBDD resource policy {label} is invalid"))?;
if parsed.to_string() != value {
return Err(format!(
"controller proof MTBDD resource policy {label} is noncanonical"
));
}
Ok(parsed)
};
let artifact_text = take("max_artifact_bytes")?;
let equivalence_text = take("max_equivalence_artifact_bytes")?;
let proof_text = take("max_unsat_proof_bytes")?;
let members_text = take("max_members")?;
let horizon_text = take("max_member_horizon")?;
let states_text = take("max_product_states_per_member")?;
let transitions_text = take("max_transition_evaluations")?;
if take("status")? != "complete" || lines.next().is_some() {
return Err(
"controller proof MTBDD resource policy is incomplete or has trailing fields"
.to_string(),
);
}
let canonical = format!(
"controller_proof_mtbdd_resource_policy_version={version}\nmax_artifact_bytes={artifact_text}\nmax_equivalence_artifact_bytes={equivalence_text}\nmax_unsat_proof_bytes={proof_text}\nmax_members={members_text}\nmax_member_horizon={horizon_text}\nmax_product_states_per_member={states_text}\nmax_transition_evaluations={transitions_text}\nstatus=complete\n"
);
if canonical != text {
return Err("controller proof MTBDD resource policy is not canonical".to_string());
}
let composition = controller_plant_artifact::ControllerPlantResourceEnvelope::new(
parse(artifact_text, "artifact bytes")?,
parse(members_text, "members")?,
parse(horizon_text, "member horizon")?,
parse(states_text, "product states")?,
parse(transitions_text, "transition evaluations")?,
)
.map_err(|error| error.to_string())?;
let envelope = controller_plant_artifact::ControllerProofMtbddResourceEnvelope::new(
composition,
parse(equivalence_text, "equivalence artifact bytes")?,
parse(proof_text, "UNSAT proof bytes")?,
)
.map_err(|error| error.to_string())?;
Ok(ControllerProofMtbddResourcePolicy { envelope })
}
fn parse_controller_split_resource_policy(
path: &Path,
) -> Result<ControllerSplitResourcePolicy, String> {
let bytes = read_bounded_regular_file(
path,
CONTROLLER_SPLIT_RESOURCE_POLICY_MAX_BYTES,
"controller split resource policy",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "controller split resource policy is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"controller split resource policy must be canonical LF text without NUL".to_string(),
);
}
let mut lines = text.lines();
let mut take = |key: &str| -> Result<&str, String> {
lines
.next()
.and_then(|line| line.strip_prefix(&format!("{key}=")))
.ok_or_else(|| format!("controller split resource policy expected {key}"))
};
let version = take("controller_split_resource_policy_version")?;
if version != CONTROLLER_SPLIT_RESOURCE_POLICY_VERSION.to_string() {
return Err("unsupported controller split resource policy version".to_string());
}
let parse = |value: &str, label: &str| -> Result<usize, String> {
let parsed = value
.parse::<usize>()
.map_err(|_| format!("controller split resource policy {label} is invalid"))?;
if parsed.to_string() != value {
return Err(format!(
"controller split resource policy {label} is noncanonical"
));
}
Ok(parsed)
};
let controller_artifact_text = take("max_controller_artifact_bytes")?;
let proof_text = take("max_unsat_proof_bytes")?;
let batches_text = take("max_batches")?;
let plant_artifact_text = take("max_plant_artifact_bytes_per_batch")?;
let members_text = take("max_members_per_batch")?;
let horizon_text = take("max_member_horizon")?;
let states_text = take("max_product_states_per_member")?;
let transitions_text = take("max_transition_evaluations_per_batch")?;
let total_artifact_text = take("max_total_plant_artifact_bytes")?;
let total_members_text = take("max_total_members")?;
let total_transitions_text = take("max_total_transition_evaluations")?;
if take("status")? != "complete" || lines.next().is_some() {
return Err(
"controller split resource policy is incomplete or has trailing fields".to_string(),
);
}
let canonical = format!(
"controller_split_resource_policy_version={version}\nmax_controller_artifact_bytes={controller_artifact_text}\nmax_unsat_proof_bytes={proof_text}\nmax_batches={batches_text}\nmax_plant_artifact_bytes_per_batch={plant_artifact_text}\nmax_members_per_batch={members_text}\nmax_member_horizon={horizon_text}\nmax_product_states_per_member={states_text}\nmax_transition_evaluations_per_batch={transitions_text}\nmax_total_plant_artifact_bytes={total_artifact_text}\nmax_total_members={total_members_text}\nmax_total_transition_evaluations={total_transitions_text}\nstatus=complete\n"
);
if canonical != text {
return Err("controller split resource policy is not canonical".to_string());
}
let controller_artifact_bytes = parse(controller_artifact_text, "controller artifact bytes")?;
let proof_bytes = parse(proof_text, "UNSAT proof bytes")?;
let max_batches = parse(batches_text, "batches")?;
let plant_artifact_bytes = parse(plant_artifact_text, "plant artifact bytes")?;
let max_members = parse(members_text, "members")?;
let max_horizon = parse(horizon_text, "member horizon")?;
let max_states = parse(states_text, "product states")?;
let max_transitions = parse(transitions_text, "transition evaluations")?;
let max_total_plant_artifact_bytes = parse(total_artifact_text, "total plant artifact bytes")?;
let max_total_members = parse(total_members_text, "total members")?;
let max_total_transition_evaluations =
parse(total_transitions_text, "total transition evaluations")?;
if max_batches == 0
|| max_batches > controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
|| max_total_plant_artifact_bytes == 0
|| max_total_members == 0
|| max_total_transition_evaluations == 0
|| plant_artifact_bytes
.checked_mul(max_batches)
.is_some_and(|maximum| max_total_plant_artifact_bytes > maximum)
|| max_members
.checked_mul(max_batches)
.is_some_and(|maximum| max_total_members > maximum)
|| max_transitions
.checked_mul(max_batches)
.is_some_and(|maximum| max_total_transition_evaluations > maximum)
{
return Err(
"controller split resource policy totals are outside static limits".to_string(),
);
}
let controller = controller_plant_artifact::ControllerProofEvidenceResourceEnvelope::new(
controller_artifact_bytes,
proof_bytes,
)
.map_err(|error| error.to_string())?;
let plant = controller_plant_artifact::ControllerPlantResourceEnvelope::new(
plant_artifact_bytes,
max_members,
max_horizon,
max_states,
max_transitions,
)
.map_err(|error| error.to_string())?;
Ok(ControllerSplitResourcePolicy {
controller,
plant,
max_batches,
max_total_plant_artifact_bytes,
max_total_members,
max_total_transition_evaluations,
})
}
fn controller_proof_mtbdd_resource_refusal(error: &str) -> Option<&'static str> {
if error.contains("resource artifact-byte limit exceeded")
|| (error.starts_with("proof-carrying controller MTBDD governed artifact exceeds ")
&& error.ends_with(" bytes"))
|| (error.starts_with("proof-carrying controller MTBDD governed portfolio exceeds ")
&& error.ends_with(" bytes"))
{
Some("artifact-bytes")
} else if error.contains("resource equivalence-artifact limit exceeded") {
Some("equivalence-artifact-bytes")
} else if error.contains("resource UNSAT-proof limit exceeded") {
Some("unsat-proof-bytes")
} else if error.contains("resource member limit exceeded") {
Some("members")
} else if error.contains("resource horizon limit exceeded") {
Some("horizon")
} else if error.contains("resource product-state limit exceeded") {
Some("product-states")
} else if error.contains("resource transition limit exceeded") {
Some("transition-evaluations")
} else {
None
}
}
fn classify_controller_proof_mtbdd_resource_error(error: String) -> String {
controller_proof_mtbdd_resource_refusal(&error).map_or(error, |reason| {
format!("controller-proof-mtbdd-resource refusal={reason} result=none")
})
}
fn controller_plant_resource_refusal(error: &str) -> Option<&'static str> {
if error.contains("resource envelope artifact-byte limit exceeded")
|| (error.starts_with("controller plant governed portfolio exceeds ")
&& error.ends_with(" bytes"))
{
Some("artifact-bytes")
} else if error.contains("resource envelope member limit exceeded") {
Some("members")
} else if error.contains("resource envelope horizon limit exceeded") {
Some("horizon")
} else if error.contains("resource envelope product-state limit exceeded") {
Some("product-states")
} else if error.contains("resource envelope transition limit exceeded") {
Some("transition-evaluations")
} else {
None
}
}
fn classify_controller_plant_resource_error(error: String) -> String {
controller_plant_resource_refusal(&error).map_or(error, |reason| {
format!("controller-plant-resource refusal={reason} result=none")
})
}
fn controller_split_resource_refusal(error: &str) -> Option<&'static str> {
if error.contains("controller proof evidence resource artifact-byte limit exceeded") {
Some("controller-artifact-bytes")
} else if error.contains("controller proof evidence resource UNSAT-proof limit exceeded") {
Some("unsat-proof-bytes")
} else if error == "controller split resource batch limit exceeded" {
Some("batches")
} else if error.contains("bound plant result resource artifact-byte limit exceeded") {
Some("plant-artifact-bytes")
} else if error.contains("bound plant result resource member limit exceeded") {
Some("members-per-batch")
} else if error.contains("bound plant result resource horizon limit exceeded") {
Some("horizon")
} else if error.contains("bound plant result resource product-state limit exceeded") {
Some("product-states")
} else if error.contains("bound plant result resource transition limit exceeded") {
Some("transitions-per-batch")
} else if error == "controller split resource total plant artifact-byte limit exceeded" {
Some("total-plant-artifact-bytes")
} else if error == "controller split resource total member limit exceeded" {
Some("total-members")
} else if error == "controller split resource total transition limit exceeded" {
Some("total-transition-evaluations")
} else {
None
}
}
fn classify_controller_split_resource_error(error: String) -> String {
controller_split_resource_refusal(&error).map_or(error, |reason| {
format!("controller-split-resource refusal={reason} result=none")
})
}
type LoadedControllerPlantManifest = (
ControllerMtbddPlantManifest,
AigerTransition,
[u8; 32],
Vec<AigerTransition>,
Vec<[u8; 32]>,
LoadedSourceModelSnapshot,
);
fn load_controller_plant_manifest(
path: &Path,
max_controller_inputs: usize,
max_controller_outputs: usize,
) -> Result<LoadedControllerPlantManifest, String> {
let manifest =
parse_controller_mtbdd_plant_manifest(path, max_controller_inputs, max_controller_outputs)?;
let controller_source = read_bounded_regular_file(
&manifest.controller_source_path,
AAG_INPUT_LIMIT_BYTES as usize,
"controller source",
)?;
let controller_aiger = read_bounded_regular_file(
&manifest.controller_aiger_path,
AAG_INPUT_LIMIT_BYTES as usize,
"controller AIGER model",
)?;
let controller = aiger_obligation::parse_ascii_aiger_transition(&controller_aiger)
.map_err(|error| error.to_string())?;
let controller_digest: [u8; 32] = Sha256::digest(&controller_source).into();
let mut source_model_subjects = vec![LoadedSourceModelSubject {
source_path: manifest.controller_source_path.clone(),
model_path: manifest.controller_aiger_path.clone(),
source_sha256: controller_digest,
model_sha256: Sha256::digest(&controller_aiger).into(),
}];
let mut plant_digests = Vec::with_capacity(manifest.members.len());
let mut plants = Vec::with_capacity(manifest.members.len());
for member in &manifest.members {
let plant_source = read_bounded_regular_file(
&member.plant_source_path,
AAG_INPUT_LIMIT_BYTES as usize,
"plant source",
)?;
let plant_aiger = read_bounded_regular_file(
&member.plant_aiger_path,
AAG_INPUT_LIMIT_BYTES as usize,
"plant AIGER model",
)?;
plant_digests.push(<[u8; 32]>::from(Sha256::digest(&plant_source)));
if !source_model_subjects.iter().any(|subject| {
subject.source_path == member.plant_source_path
&& subject.model_path == member.plant_aiger_path
}) {
source_model_subjects.push(LoadedSourceModelSubject {
source_path: member.plant_source_path.clone(),
model_path: member.plant_aiger_path.clone(),
source_sha256: Sha256::digest(&plant_source).into(),
model_sha256: Sha256::digest(&plant_aiger).into(),
});
}
plants.push(
aiger_obligation::parse_ascii_aiger_transition(&plant_aiger)
.map_err(|error| error.to_string())?,
);
}
Ok((
manifest,
controller,
controller_digest,
plants,
plant_digests,
LoadedSourceModelSnapshot {
subjects: source_model_subjects,
},
))
}
fn parse_btor2_phase_specs(raw: &str) -> Result<Vec<btor2_phase::PhaseSpec>, String> {
let raw_phases = raw.split(',').collect::<Vec<_>>();
if raw_phases.is_empty() || raw_phases.len() > btor2_phase::MAX_PHASES {
return Err("PHASES count is outside the supported limit".to_string());
}
raw_phases
.iter()
.enumerate()
.map(|(index, raw)| {
let (input, length) = raw
.split_once(':')
.ok_or_else(|| format!("phase {index} must use INPUT:LENGTH syntax"))?;
let input = match input {
"0" => false,
"1" => true,
_ => return Err(format!("phase {index} input must be 0 or 1")),
};
let length = length
.parse::<u64>()
.map_err(|_| format!("phase {index} length is invalid"))?;
Ok(btor2_phase::PhaseSpec { input, length })
})
.collect()
}
fn write_new_certificate(output: &Path, encoded: &[u8]) -> Result<(), String> {
write_new_certificate_with(output, |file| file.write_all(encoded))
}
fn write_new_certificate_with<F>(output: &Path, write: F) -> Result<(), String>
where
F: FnOnce(&mut fs::File) -> io::Result<()>,
{
if fs::symlink_metadata(output).is_ok() {
return Err(format!(
"create certificate {}: output already exists",
output.display()
));
}
let parent = output.parent().unwrap_or_else(|| Path::new("."));
let name = output
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| "certificate output filename is invalid".to_string())?;
let sequence = CERTIFICATE_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let temporary = parent.join(format!(
".{name}.gcc-certificate-{}-{sequence}.tmp",
std::process::id()
));
let result = (|| {
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
options.mode(0o600);
let mut file = options.open(&temporary).map_err(|error| {
format!(
"create temporary certificate {}: {error}",
temporary.display()
)
})?;
write(&mut file)
.and_then(|_| file.sync_all())
.map_err(|error| {
format!(
"write temporary certificate {}: {error}",
temporary.display()
)
})?;
drop(file);
fs::hard_link(&temporary, output)
.map_err(|error| format!("publish certificate {}: {error}", output.display()))?;
#[cfg(unix)]
fs::File::open(parent)
.and_then(|directory| directory.sync_all())
.map_err(|error| format!("sync certificate directory {}: {error}", parent.display()))?;
Ok(())
})();
let _ = fs::remove_file(&temporary);
result
}
fn parse_btor2_property_set(value: &str) -> Result<Vec<btor2::NodeId>, String> {
let properties = value
.split(',')
.map(|item| {
item.parse::<u64>()
.map_err(|_| "BAD_PROPERTIES must be comma-separated unsigned node identifiers")
})
.collect::<Result<Vec<_>, _>>()?;
if properties.is_empty()
|| properties.len() > btor2_predicate_set::MAX_PREDICATE_SET_MEMBERS
|| !properties.windows(2).all(|pair| pair[0] < pair[1])
{
return Err(
"BAD_PROPERTIES must contain 1..=64 strictly increasing node identifiers".to_string(),
);
}
Ok(properties)
}
fn parse_revision_output_nodes(value: &str, label: &str) -> Result<Vec<btor2::NodeId>, String> {
let nodes = value
.split(',')
.map(|item| {
item.parse::<btor2::NodeId>()
.ok()
.filter(|node| *node != 0)
.ok_or_else(|| format!("{label} must be comma-separated nonzero node identifiers"))
})
.collect::<Result<Vec<_>, _>>()?;
if nodes.is_empty() || nodes.windows(2).any(|pair| pair[0] >= pair[1]) {
return Err(format!(
"{label} must be nonempty, unique, and strictly increasing"
));
}
Ok(nodes)
}
fn parse_revision_impact_queries(path: &Path) -> Result<Vec<revision_local::BoundedQuery>, String> {
let bytes = read_bounded_regular_file(
path,
REVISION_IMPACT_QUERY_MANIFEST_MAX_BYTES,
"revision impact query manifest",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "revision impact query manifest is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"revision impact query manifest must be canonical LF text without NUL".to_string(),
);
}
let mut lines = text.lines();
if lines.next() != Some("gcc-btor2-revision-impact-queries-v1") {
return Err("revision impact query manifest has an unsupported header".to_string());
}
let mut queries = Vec::new();
let mut keys = Vec::new();
for (index, line) in lines.enumerate() {
let fields = line.split(',').collect::<Vec<_>>();
if fields.len() != 3 || fields.iter().any(|field| field.is_empty()) {
return Err(format!(
"revision impact query {} must use HORIZON,BAD_SIDE,BAD_OUTPUT syntax",
index + 1
));
}
let horizon = fields[0]
.parse::<u32>()
.map_err(|_| format!("revision impact query {} has an invalid horizon", index + 1))?;
let (bad_side, side_key) = match fields[1] {
"left" => (revision_local::ComponentSide::Left, 0_u8),
"right" => (revision_local::ComponentSide::Right, 1_u8),
_ => {
return Err(format!(
"revision impact query {} side must be left or right",
index + 1
));
}
};
let bad_output = fields[2]
.parse::<btor2::NodeId>()
.ok()
.filter(|node| *node != 0)
.ok_or_else(|| {
format!(
"revision impact query {} bad output must be nonzero",
index + 1
)
})?;
let key = (horizon, side_key, bad_output);
if keys.last().is_some_and(|previous| *previous >= key) {
return Err(
"revision impact queries must be unique and strictly ordered by horizon, side, and output"
.to_string(),
);
}
keys.push(key);
queries.push(revision_local::BoundedQuery {
horizon,
bad_side,
bad_output,
});
}
if queries.is_empty() || queries.len() > revision_impact::MAX_IMPACT_QUERIES {
return Err(format!(
"revision impact query manifest must contain 1..={} queries",
revision_impact::MAX_IMPACT_QUERIES
));
}
Ok(queries)
}
fn parse_canonical_usize(value: &str, label: &str) -> Result<usize, String> {
let parsed = value
.parse::<usize>()
.map_err(|_| format!("{label} is invalid"))?;
if parsed.to_string() != value {
return Err(format!("{label} is noncanonical"));
}
Ok(parsed)
}
fn parse_canonical_u64(value: &str, label: &str) -> Result<u64, String> {
let parsed = value
.parse::<u64>()
.map_err(|_| format!("{label} is invalid"))?;
if parsed.to_string() != value {
return Err(format!("{label} is noncanonical"));
}
Ok(parsed)
}
fn parse_btor2_channel_property_query_manifest(
path: &Path,
) -> Result<Btor2ChannelPropertyQueryManifest, String> {
let bytes = read_bounded_regular_file(
path,
BTOR2_CHANNEL_PROPERTY_QUERY_MANIFEST_MAX_BYTES,
"BTOR2 channel property query manifest",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "BTOR2 channel property query manifest is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"BTOR2 channel property query manifest must be canonical LF text without NUL"
.to_string(),
);
}
let mut lines = text.lines();
if lines.next() != Some("gcc-btor2-channel-properties-v1") {
return Err("BTOR2 channel property query manifest has an unsupported header".to_string());
}
let channels_line = lines
.next()
.and_then(|line| line.strip_prefix("channels="))
.ok_or_else(|| "BTOR2 channel property query manifest expected channels".to_string())?;
let expected_channels = parse_canonical_usize(channels_line, "channel count")?;
if !(1..=btor2_region_extract::MAX_REGION_CHANNELS).contains(&expected_channels) {
return Err("BTOR2 channel property channel count is outside policy".to_string());
}
let roots_line = lines
.next()
.and_then(|line| line.strip_prefix("semantic_roots="))
.ok_or_else(|| {
"BTOR2 channel property query manifest expected semantic_roots".to_string()
})?;
let semantic_roots = roots_line
.split(',')
.map(|value| {
parse_canonical_u64(value, "semantic root").and_then(|root| {
(root != 0)
.then_some(root)
.ok_or_else(|| "semantic root must be nonzero".to_string())
})
})
.collect::<Result<Vec<_>, _>>()?;
if semantic_roots.is_empty()
|| semantic_roots.len() > btor2_region_property::MAX_CHANNEL_PROPERTY_QUERIES
|| semantic_roots.windows(2).any(|pair| pair[0] >= pair[1])
{
return Err("semantic roots must be nonempty, unique, and strictly increasing".to_string());
}
let mut queries = Vec::new();
let mut canonical = format!(
"gcc-btor2-channel-properties-v1\nchannels={expected_channels}\nsemantic_roots={roots_line}\n"
);
for (index, line) in lines.by_ref().enumerate() {
if line == "status=complete" {
if lines.next().is_some() {
return Err("BTOR2 channel property query manifest has trailing fields".to_string());
}
canonical.push_str("status=complete\n");
break;
}
let fields = line
.strip_prefix("query=")
.ok_or_else(|| format!("BTOR2 channel property query {} is malformed", index + 1))?
.split(',')
.collect::<Vec<_>>();
if fields.len() != 4 || fields.iter().any(|field| field.is_empty()) {
return Err(format!(
"BTOR2 channel property query {} must use ID,CHANNEL,PROPERTY,HORIZON syntax",
index + 1
));
}
let query_id_u64 = parse_canonical_u64(fields[0], "query identifier")?;
let query_id = u32::try_from(query_id_u64)
.map_err(|_| "query identifier exceeds range".to_string())?;
let channel_index = parse_canonical_usize(fields[1], "query channel")?;
let property = match fields[2] {
"output-high" => btor2_region_property::Btor2ChannelProperty::OutputHigh,
"output-low" => btor2_region_property::Btor2ChannelProperty::OutputLow,
_ => return Err("query property must be output-high or output-low".to_string()),
};
let horizon_u64 = parse_canonical_u64(fields[3], "query horizon")?;
let horizon =
u32::try_from(horizon_u64).map_err(|_| "query horizon exceeds range".to_string())?;
if channel_index >= expected_channels
|| horizon > btor2_search::MAX_SEARCH_HORIZON
|| queries.last().is_some_and(
|previous: &btor2_region_property::Btor2ChannelPropertyQuery| {
previous.query_id >= query_id
},
)
{
return Err("BTOR2 channel property query is outside policy or order".to_string());
}
queries.push(btor2_region_property::Btor2ChannelPropertyQuery {
query_id,
channel_index,
property,
horizon,
});
canonical.push_str(line);
canonical.push('\n');
}
if !canonical.ends_with("status=complete\n")
|| queries.is_empty()
|| queries.len() > btor2_region_property::MAX_CHANNEL_PROPERTY_QUERIES
|| canonical != text
{
return Err(
"BTOR2 channel property query manifest is incomplete or noncanonical".to_string(),
);
}
Ok(Btor2ChannelPropertyQueryManifest {
expected_channels,
semantic_roots,
queries,
})
}
fn parse_btor2_channel_property_policy(
path: &Path,
) -> Result<btor2_region_property::Btor2ChannelPropertyProductionPolicy, String> {
let bytes = read_bounded_regular_file(
path,
BTOR2_CHANNEL_PROPERTY_POLICY_MAX_BYTES,
"BTOR2 channel property policy",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "BTOR2 channel property policy is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"BTOR2 channel property policy must be canonical LF text without NUL".to_string(),
);
}
let mut lines = text.lines();
let mut take = |key: &str| -> Result<&str, String> {
lines
.next()
.and_then(|line| line.strip_prefix(&format!("{key}=")))
.ok_or_else(|| format!("BTOR2 channel property policy expected {key}"))
};
let version = take("channel_property_policy_version")?;
if version != BTOR2_CHANNEL_PROPERTY_POLICY_VERSION.to_string() {
return Err("unsupported BTOR2 channel property policy version".to_string());
}
let max_queries_text = take("max_queries")?;
let max_members_text = take("max_members")?;
let max_evidence_bytes_text = take("max_evidence_bytes")?;
let max_artifact_bytes_text = take("max_artifact_bytes")?;
let max_projected_work_text = take("max_projected_work")?;
if take("status")? != "complete" || lines.next().is_some() {
return Err(
"BTOR2 channel property policy is incomplete or has trailing fields".to_string(),
);
}
let canonical = format!(
"channel_property_policy_version={version}\nmax_queries={max_queries_text}\nmax_members={max_members_text}\nmax_evidence_bytes={max_evidence_bytes_text}\nmax_artifact_bytes={max_artifact_bytes_text}\nmax_projected_work={max_projected_work_text}\nstatus=complete\n"
);
if canonical != text {
return Err("BTOR2 channel property policy is not canonical".to_string());
}
let artifact = btor2_region_property::Btor2ChannelPropertyProofPolicy::new(
parse_canonical_usize(max_queries_text, "maximum query count")?,
parse_canonical_usize(max_members_text, "maximum member count")?,
parse_canonical_usize(max_evidence_bytes_text, "maximum evidence bytes")?,
parse_canonical_usize(max_artifact_bytes_text, "maximum artifact bytes")?,
)
.map_err(|error| error.to_string())?;
btor2_region_property::Btor2ChannelPropertyProductionPolicy::new(
artifact,
parse_canonical_u64(max_projected_work_text, "maximum projected work")?,
)
.map_err(|error| error.to_string())
}
fn map_btor2_channel_property_production_error(
error: btor2_region_extract::Btor2RegionError,
) -> String {
let message = error.to_string();
let reason = if message.contains("production query count exceeds policy") {
Some("query-count")
} else if message.contains("production member count exceeds policy") {
Some("member-count")
} else if message.contains("aggregate projected work exceeds policy") {
Some("projected-work")
} else if message.contains("evidence exceeds policy") {
Some("evidence-bytes")
} else if message.contains("artifact exceeds byte policy")
|| message.contains("artifact is outside policy")
{
Some("artifact-bytes")
} else {
None
};
reason.map_or(message, |reason| {
format!("btor2-channel-property-resource refusal={reason} result=none")
})
}
fn parse_btor2_channel_trace_query_manifest(
path: &Path,
) -> Result<Btor2ChannelTraceQueryManifest, String> {
let bytes = read_bounded_regular_file(
path,
BTOR2_CHANNEL_TRACE_QUERY_MANIFEST_MAX_BYTES,
"BTOR2 channel trace query manifest",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "BTOR2 channel trace query manifest is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"BTOR2 channel trace query manifest must be canonical LF text without NUL".to_string(),
);
}
let mut lines = text.lines();
if lines.next() != Some("gcc-btor2-channel-traces-v1") {
return Err("BTOR2 channel trace query manifest has an unsupported header".to_string());
}
let channels_text = lines
.next()
.and_then(|line| line.strip_prefix("channels="))
.ok_or_else(|| "BTOR2 channel trace query manifest expected channels".to_string())?;
let expected_channels = parse_canonical_usize(channels_text, "channel count")?;
if !(1..=btor2_region_extract::MAX_REGION_CHANNELS).contains(&expected_channels) {
return Err("BTOR2 channel trace channel count is outside policy".to_string());
}
let roots_text = lines
.next()
.and_then(|line| line.strip_prefix("semantic_roots="))
.ok_or_else(|| "BTOR2 channel trace query manifest expected semantic_roots".to_string())?;
let semantic_roots = roots_text
.split(',')
.map(|value| {
parse_canonical_u64(value, "semantic root").and_then(|root| {
(root != 0)
.then_some(root)
.ok_or_else(|| "semantic root must be nonzero".to_string())
})
})
.collect::<Result<Vec<_>, _>>()?;
if semantic_roots.is_empty()
|| semantic_roots.len() > btor2_region_property::MAX_CHANNEL_TRACE_QUERIES
|| semantic_roots.windows(2).any(|pair| pair[0] >= pair[1])
{
return Err("semantic roots must be nonempty, unique, and strictly increasing".to_string());
}
let mut queries = Vec::new();
let mut canonical = format!(
"gcc-btor2-channel-traces-v1\nchannels={expected_channels}\nsemantic_roots={roots_text}\n"
);
for (index, line) in lines.by_ref().enumerate() {
if line == "status=complete" {
if lines.next().is_some() {
return Err("BTOR2 channel trace query manifest has trailing fields".to_string());
}
canonical.push_str("status=complete\n");
break;
}
let fields = line
.strip_prefix("query=")
.ok_or_else(|| format!("BTOR2 channel trace query {} is malformed", index + 1))?
.split(',')
.collect::<Vec<_>>();
if fields.len() != 6 || fields.iter().any(|field| field.is_empty()) {
return Err(format!(
"BTOR2 channel trace query {} must use ID,CHANNEL,LENGTH,MASK,VALUE,HORIZON syntax",
index + 1
));
}
let query_id = u32::try_from(parse_canonical_u64(fields[0], "query identifier")?)
.map_err(|_| "query identifier exceeds range".to_string())?;
let channel_index = parse_canonical_usize(fields[1], "query channel")?;
let length = u8::try_from(parse_canonical_u64(fields[2], "trace length")?)
.map_err(|_| "trace length exceeds range".to_string())?;
let mask = u8::try_from(parse_canonical_u64(fields[3], "trace mask")?)
.map_err(|_| "trace mask exceeds range".to_string())?;
let value = u8::try_from(parse_canonical_u64(fields[4], "trace value")?)
.map_err(|_| "trace value exceeds range".to_string())?;
let horizon = u32::try_from(parse_canonical_u64(fields[5], "query horizon")?)
.map_err(|_| "query horizon exceeds range".to_string())?;
let pattern = btor2_region_property::Btor2ChannelTracePattern::new(length, mask, value)
.map_err(|error| error.to_string())?;
if channel_index >= expected_channels
|| horizon > btor2_bitblast::MAX_BITBLAST_HORIZON
|| queries.last().is_some_and(
|previous: &btor2_region_property::Btor2ChannelTraceQuery| {
previous.query_id >= query_id
},
)
{
return Err("BTOR2 channel trace query is outside policy or order".to_string());
}
queries.push(btor2_region_property::Btor2ChannelTraceQuery {
query_id,
channel_index,
pattern,
horizon,
});
canonical.push_str(line);
canonical.push('\n');
}
if !canonical.ends_with("status=complete\n")
|| queries.is_empty()
|| queries.len() > btor2_region_property::MAX_CHANNEL_TRACE_QUERIES
|| canonical != text
{
return Err("BTOR2 channel trace query manifest is incomplete or noncanonical".to_string());
}
Ok(Btor2ChannelTraceQueryManifest {
expected_channels,
semantic_roots,
queries,
})
}
fn parse_btor2_channel_pair_trace_query_manifest(
path: &Path,
) -> Result<Btor2ChannelPairTraceQueryManifest, String> {
let bytes = read_bounded_regular_file(
path,
BTOR2_CHANNEL_TRACE_QUERY_MANIFEST_MAX_BYTES,
"BTOR2 channel-pair trace query manifest",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "BTOR2 channel-pair trace query manifest is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err(
"BTOR2 channel-pair trace query manifest must be canonical LF text without NUL"
.to_string(),
);
}
let mut lines = text.lines();
if lines.next() != Some("gcc-btor2-channel-pair-traces-v1") {
return Err(
"BTOR2 channel-pair trace query manifest has an unsupported header".to_string(),
);
}
let channels_text = lines
.next()
.and_then(|line| line.strip_prefix("channels="))
.ok_or_else(|| "BTOR2 channel-pair trace query manifest expected channels".to_string())?;
let expected_channels = parse_canonical_usize(channels_text, "channel count")?;
if !(2..=btor2_region_extract::MAX_REGION_CHANNELS).contains(&expected_channels) {
return Err("BTOR2 channel-pair trace channel count is outside policy".to_string());
}
let roots_text = lines
.next()
.and_then(|line| line.strip_prefix("semantic_roots="))
.ok_or_else(|| {
"BTOR2 channel-pair trace query manifest expected semantic_roots".to_string()
})?;
let semantic_roots = roots_text
.split(',')
.map(|value| {
parse_canonical_u64(value, "semantic root").and_then(|root| {
(root != 0)
.then_some(root)
.ok_or_else(|| "semantic root must be nonzero".to_string())
})
})
.collect::<Result<Vec<_>, _>>()?;
if semantic_roots.is_empty()
|| semantic_roots.len() > btor2_region_property::MAX_CHANNEL_TRACE_QUERIES
|| semantic_roots.windows(2).any(|pair| pair[0] >= pair[1])
{
return Err("semantic roots must be nonempty, unique, and strictly increasing".to_string());
}
let mut queries = Vec::new();
let mut canonical = format!(
"gcc-btor2-channel-pair-traces-v1\nchannels={expected_channels}\nsemantic_roots={roots_text}\n"
);
for (index, line) in lines.by_ref().enumerate() {
if line == "status=complete" {
if lines.next().is_some() {
return Err(
"BTOR2 channel-pair trace query manifest has trailing fields".to_string(),
);
}
canonical.push_str("status=complete\n");
break;
}
let fields = line
.strip_prefix("query=")
.ok_or_else(|| format!("BTOR2 channel-pair trace query {} is malformed", index + 1))?
.split(',')
.collect::<Vec<_>>();
if fields.len() != 8 || fields.iter().any(|field| field.is_empty()) {
return Err(format!(
"BTOR2 channel-pair trace query {} must use ID,LEFT,RIGHT,RELATION,LENGTH,MASK,VALUE,HORIZON syntax",
index + 1
));
}
let query_id = u32::try_from(parse_canonical_u64(fields[0], "query identifier")?)
.map_err(|_| "query identifier exceeds range".to_string())?;
let left_channel_index = parse_canonical_usize(fields[1], "left query channel")?;
let right_channel_index = parse_canonical_usize(fields[2], "right query channel")?;
let relation = match fields[3] {
"equal" => btor2_region_property::Btor2ChannelPairRelation::Equal,
"different" => btor2_region_property::Btor2ChannelPairRelation::Different,
_ => return Err("channel-pair relation must be equal or different".to_string()),
};
let length = u8::try_from(parse_canonical_u64(fields[4], "trace length")?)
.map_err(|_| "trace length exceeds range".to_string())?;
let mask = u8::try_from(parse_canonical_u64(fields[5], "trace mask")?)
.map_err(|_| "trace mask exceeds range".to_string())?;
let value = u8::try_from(parse_canonical_u64(fields[6], "trace value")?)
.map_err(|_| "trace value exceeds range".to_string())?;
let horizon = u32::try_from(parse_canonical_u64(fields[7], "query horizon")?)
.map_err(|_| "query horizon exceeds range".to_string())?;
let pattern = btor2_region_property::Btor2ChannelTracePattern::new(length, mask, value)
.map_err(|error| error.to_string())?;
if left_channel_index >= expected_channels
|| right_channel_index >= expected_channels
|| left_channel_index == right_channel_index
|| horizon > btor2_bitblast::MAX_BITBLAST_HORIZON
|| queries.last().is_some_and(
|previous: &btor2_region_property::Btor2ChannelPairTraceQuery| {
previous.query_id >= query_id
},
)
{
return Err("BTOR2 channel-pair trace query is outside policy or order".to_string());
}
queries.push(btor2_region_property::Btor2ChannelPairTraceQuery {
query_id,
left_channel_index,
right_channel_index,
relation,
pattern,
horizon,
});
canonical.push_str(line);
canonical.push('\n');
}
if !canonical.ends_with("status=complete\n")
|| queries.is_empty()
|| queries.len() > btor2_region_property::MAX_CHANNEL_TRACE_QUERIES
|| canonical != text
{
return Err(
"BTOR2 channel-pair trace query manifest is incomplete or noncanonical".to_string(),
);
}
Ok(Btor2ChannelPairTraceQueryManifest {
expected_channels,
semantic_roots,
queries,
})
}
fn parse_btor2_channel_trace_policy(
path: &Path,
) -> Result<btor2_region_property::Btor2ChannelTraceProductionPolicy, String> {
let bytes = read_bounded_regular_file(
path,
BTOR2_CHANNEL_TRACE_POLICY_MAX_BYTES,
"BTOR2 channel trace policy",
)?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| "BTOR2 channel trace policy is not UTF-8".to_string())?;
if bytes.contains(&0) || text.contains('\r') || !text.ends_with('\n') {
return Err("BTOR2 channel trace policy must be canonical LF text without NUL".to_string());
}
let mut lines = text.lines();
let mut take = |key: &str| -> Result<&str, String> {
lines
.next()
.and_then(|line| line.strip_prefix(&format!("{key}=")))
.ok_or_else(|| format!("BTOR2 channel trace policy expected {key}"))
};
let version = take("channel_trace_policy_version")?;
if version != BTOR2_CHANNEL_TRACE_POLICY_VERSION.to_string() {
return Err("unsupported BTOR2 channel trace policy version".to_string());
}
let max_queries = take("max_queries")?;
let max_members = take("max_members")?;
let max_evidence_bytes = take("max_evidence_bytes")?;
let max_artifact_bytes = take("max_artifact_bytes")?;
let max_projected_work = take("max_projected_work")?;
if take("status")? != "complete" || lines.next().is_some() {
return Err("BTOR2 channel trace policy is incomplete or has trailing fields".to_string());
}
let canonical = format!(
"channel_trace_policy_version={version}\nmax_queries={max_queries}\nmax_members={max_members}\nmax_evidence_bytes={max_evidence_bytes}\nmax_artifact_bytes={max_artifact_bytes}\nmax_projected_work={max_projected_work}\nstatus=complete\n"
);
if canonical != text {
return Err("BTOR2 channel trace policy is not canonical".to_string());
}
let artifact = btor2_region_property::Btor2ChannelTraceProofPolicy::new(
parse_canonical_usize(max_queries, "maximum query count")?,
parse_canonical_usize(max_members, "maximum member count")?,
parse_canonical_usize(max_evidence_bytes, "maximum evidence bytes")?,
parse_canonical_usize(max_artifact_bytes, "maximum artifact bytes")?,
)
.map_err(|error| error.to_string())?;
btor2_region_property::Btor2ChannelTraceProductionPolicy::new(
artifact,
parse_canonical_u64(max_projected_work, "maximum projected work")?,
)
.map_err(|error| error.to_string())
}
fn map_btor2_channel_trace_production_error(
error: btor2_region_extract::Btor2RegionError,
) -> String {
let message = error.to_string();
let reason = if message.contains("query count") && message.contains("policy") {
Some("query-count")
} else if message.contains("member count") && message.contains("policy") {
Some("member-count")
} else if message.contains("aggregate projected work exceeds policy") {
Some("projected-work")
} else if message.contains("evidence exceeds policy") {
Some("evidence-bytes")
} else if message.contains("artifact exceeds byte policy")
|| message.contains("artifact is outside policy")
{
Some("artifact-bytes")
} else {
None
};
reason.map_or(message, |reason| {
format!("btor2-channel-trace-resource refusal={reason} result=none")
})
}
fn map_btor2_channel_pair_trace_production_error(
error: btor2_region_extract::Btor2RegionError,
) -> String {
map_btor2_channel_trace_production_error(error).replacen(
"btor2-channel-trace-resource",
"btor2-channel-pair-trace-resource",
1,
)
}
fn run_artifact_cli(args: &[String]) -> Result<bool, String> {
let Some(command) = args.first().map(String::as_str) else {
return Ok(false);
};
match command {
"btor2-channel-pair-trace-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-channel-pair-trace-cli-version"
.to_string(),
);
}
println!(
"btor2_channel_pair_trace_cli_version={BTOR2_CHANNEL_PAIR_TRACE_CLI_VERSION} artifact_version={} query_manifest_version={BTOR2_CHANNEL_PAIR_TRACE_QUERY_MANIFEST_VERSION} policy_version={BTOR2_CHANNEL_TRACE_POLICY_VERSION} max_query_manifest_bytes={BTOR2_CHANNEL_TRACE_QUERY_MANIFEST_MAX_BYTES} max_policy_bytes={BTOR2_CHANNEL_TRACE_POLICY_MAX_BYTES} max_model_bytes={} max_channels={} max_queries={} max_pattern_length={} max_horizon={} max_evidence_bytes={} max_artifact_bytes={} max_projected_work={} refusal_exit=3 routing=structural-constant-or-static-exact fallback=exact result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed verification=source-replay-and-shortest-frame-proof publication=create-new",
btor2_region_property::BTOR2_CHANNEL_PAIR_TRACE_PROOF_VERSION,
btor2::MAX_BTOR2_BYTES,
btor2_region_extract::MAX_REGION_CHANNELS,
btor2_region_property::MAX_CHANNEL_TRACE_QUERIES,
btor2_region_property::MAX_CHANNEL_TRACE_PATTERN_LENGTH,
btor2_bitblast::MAX_BITBLAST_HORIZON,
btor2_region_property::MAX_CHANNEL_TRACE_EVIDENCE_BYTES,
btor2_region_property::MAX_CHANNEL_TRACE_ARTIFACT_BYTES,
btor2_region_property::MAX_CHANNEL_TRACE_PROJECTED_WORK,
);
Ok(true)
}
"certify-btor2-channel-pair-traces" | "verify-btor2-channel-pair-traces" => {
let certify = command.starts_with("certify-");
if args.len() != 5 {
return Err(format!(
"usage: guarded-continuation-checker {command} MODEL.btor2 QUERIES.txt POLICY.txt {}",
if certify {
"OUTPUT.channel-pair-traces"
} else {
"INPUT.channel-pair-traces"
}
));
}
let started = Instant::now();
let model = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 channel-pair trace model",
)?;
let manifest = parse_btor2_channel_pair_trace_query_manifest(Path::new(&args[2]))?;
let production_policy = parse_btor2_channel_trace_policy(Path::new(&args[3]))?;
let region_policy = btor2_region_extract::Btor2RegionPolicy::default();
let (encoded, plan) = if certify {
let structural =
btor2_region_equivalence::encode_btor2_region_equivalence_artifact(
&btor2_region_equivalence::produce_btor2_region_equivalence_artifact(
&model,
&manifest.semantic_roots,
manifest.expected_channels,
region_policy,
)
.map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())?;
let (plan, encoded) =
btor2_region_property::produce_btor2_channel_pair_trace_proof_bytes(
&model,
&structural,
&manifest.queries,
region_policy,
production_policy,
)
.map_err(map_btor2_channel_pair_trace_production_error)?;
(encoded, Some(plan))
} else {
(
read_bounded_regular_file(
Path::new(&args[4]),
production_policy.artifact().max_artifact_bytes(),
"BTOR2 channel-pair trace artifact",
)?,
None,
)
};
let artifact = btor2_region_property::decode_btor2_channel_pair_trace_proof_artifact(
&encoded,
production_policy.artifact(),
)
.map_err(|error| error.to_string())?;
let structural = btor2_region_equivalence::decode_btor2_region_equivalence_artifact(
&artifact.structural_admission,
)
.map_err(|error| error.to_string())?;
if structural.expected_channels != manifest.expected_channels
|| structural.semantic_roots != manifest.semantic_roots
{
return Err(
"BTOR2 channel-pair trace manifest does not match structural admission"
.to_string(),
);
}
let summary = btor2_region_property::verify_btor2_channel_pair_trace_proof(
&model,
&manifest.queries,
&artifact,
region_policy,
production_policy.artifact(),
)
.map_err(|error| error.to_string())?;
if certify {
write_new_certificate(Path::new(&args[4]), &encoded)?;
}
println!(
"btor2-channel-pair-traces status={} cli_version={BTOR2_CHANNEL_PAIR_TRACE_CLI_VERSION} artifact_version={} channels={} logical_queries={} proof_members={} reused_queries={} structural_members={} explicit_members={} bitblast_members={} evidence_bytes={} artifact_bytes={} projected_work={} elapsed_micros={}",
if certify { "CREATED" } else { "VERIFIED" },
btor2_region_property::BTOR2_CHANNEL_PAIR_TRACE_PROOF_VERSION,
manifest.expected_channels,
summary.metrics.logical_queries,
summary.metrics.proof_members,
summary.metrics.reused_logical_queries,
summary.metrics.structural_constant_members,
summary.metrics.explicit_state_members,
summary.metrics.bitblast_members,
summary.metrics.evidence_bytes,
encoded.len(),
plan.map_or_else(
|| "not-applied".to_string(),
|plan| plan.projected_work.to_string()
),
started.elapsed().as_micros(),
);
for (index, result) in summary.results.iter().enumerate() {
let answer = match result.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let relation = match result.query.relation {
btor2_region_property::Btor2ChannelPairRelation::Equal => "equal",
btor2_region_property::Btor2ChannelPairRelation::Different => "different",
};
let backend = match result.backend {
btor2_region_property::Btor2ChannelTraceBackend::RepresentativeClass => {
"representative-class"
}
btor2_region_property::Btor2ChannelTraceBackend::DirectExact => "direct-exact",
};
let solver = match result.solver {
btor2_region_property::Btor2ChannelPairTraceSolver::StructuralConstant => {
"structural-constant"
}
btor2_region_property::Btor2ChannelPairTraceSolver::ExplicitState => {
"explicit-state"
}
btor2_region_property::Btor2ChannelPairTraceSolver::BitblastCnf => {
"bitblast-cnf"
}
};
println!(
"btor2-channel-pair-trace index={index} query_id={} left_channel={} right_channel={} relation={relation} length={} mask={} value={} horizon={} answer={answer} bad_frame={} backend={backend} solver={solver} representative_left_channel={} representative_right_channel={} witness_valuations={}",
result.query.query_id,
result.query.left_channel_index,
result.query.right_channel_index,
result.query.pattern.length(),
result.query.pattern.mask(),
result.query.pattern.value(),
result.query.horizon,
result
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
result.representative_left_channel,
result.representative_right_channel,
result.witness_valuations.len(),
);
}
Ok(true)
}
"btor2-channel-trace-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-channel-trace-cli-version"
.to_string(),
);
}
println!(
"btor2_channel_trace_cli_version={BTOR2_CHANNEL_TRACE_CLI_VERSION} artifact_version={} query_manifest_version={BTOR2_CHANNEL_TRACE_QUERY_MANIFEST_VERSION} policy_version={BTOR2_CHANNEL_TRACE_POLICY_VERSION} max_query_manifest_bytes={BTOR2_CHANNEL_TRACE_QUERY_MANIFEST_MAX_BYTES} max_policy_bytes={BTOR2_CHANNEL_TRACE_POLICY_MAX_BYTES} max_model_bytes={} max_channels={} max_queries={} max_pattern_length={} max_horizon={} max_evidence_bytes={} max_artifact_bytes={} max_projected_work={} refusal_exit=3 routing=static-explicit-or-bitblast fallback=exact result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed verification=source-replay-and-shortest-frame-proof publication=create-new",
btor2_region_property::BTOR2_CHANNEL_TRACE_PROOF_VERSION,
btor2::MAX_BTOR2_BYTES,
btor2_region_extract::MAX_REGION_CHANNELS,
btor2_region_property::MAX_CHANNEL_TRACE_QUERIES,
btor2_region_property::MAX_CHANNEL_TRACE_PATTERN_LENGTH,
btor2_bitblast::MAX_BITBLAST_HORIZON,
btor2_region_property::MAX_CHANNEL_TRACE_EVIDENCE_BYTES,
btor2_region_property::MAX_CHANNEL_TRACE_ARTIFACT_BYTES,
btor2_region_property::MAX_CHANNEL_TRACE_PROJECTED_WORK,
);
Ok(true)
}
"certify-btor2-channel-traces" | "verify-btor2-channel-traces" => {
let certify = command.starts_with("certify-");
if args.len() != 5 {
return Err(format!(
"usage: guarded-continuation-checker {command} MODEL.btor2 QUERIES.txt POLICY.txt {}",
if certify {
"OUTPUT.channel-traces"
} else {
"INPUT.channel-traces"
}
));
}
let started = Instant::now();
let model = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 channel trace model",
)?;
let manifest = parse_btor2_channel_trace_query_manifest(Path::new(&args[2]))?;
let production_policy = parse_btor2_channel_trace_policy(Path::new(&args[3]))?;
let region_policy = btor2_region_extract::Btor2RegionPolicy::default();
let (encoded, plan) = if certify {
let structural =
btor2_region_equivalence::encode_btor2_region_equivalence_artifact(
&btor2_region_equivalence::produce_btor2_region_equivalence_artifact(
&model,
&manifest.semantic_roots,
manifest.expected_channels,
region_policy,
)
.map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())?;
let (plan, encoded) =
btor2_region_property::produce_btor2_channel_trace_proof_bytes(
&model,
&structural,
&manifest.queries,
region_policy,
production_policy,
)
.map_err(map_btor2_channel_trace_production_error)?;
(encoded, Some(plan))
} else {
(
read_bounded_regular_file(
Path::new(&args[4]),
production_policy.artifact().max_artifact_bytes(),
"BTOR2 channel trace artifact",
)?,
None,
)
};
let artifact = btor2_region_property::decode_btor2_channel_trace_proof_artifact(
&encoded,
production_policy.artifact(),
)
.map_err(|error| error.to_string())?;
let structural = btor2_region_equivalence::decode_btor2_region_equivalence_artifact(
&artifact.structural_admission,
)
.map_err(|error| error.to_string())?;
if structural.expected_channels != manifest.expected_channels
|| structural.semantic_roots != manifest.semantic_roots
{
return Err(
"BTOR2 channel trace manifest does not match structural admission".to_string(),
);
}
let summary = btor2_region_property::verify_btor2_channel_trace_proof(
&model,
&manifest.queries,
&artifact,
region_policy,
production_policy.artifact(),
)
.map_err(|error| error.to_string())?;
if certify {
write_new_certificate(Path::new(&args[4]), &encoded)?;
}
println!(
"btor2-channel-traces status={} cli_version={BTOR2_CHANNEL_TRACE_CLI_VERSION} artifact_version={} channels={} logical_queries={} proof_members={} reused_queries={} explicit_members={} bitblast_members={} evidence_bytes={} artifact_bytes={} projected_work={} elapsed_micros={}",
if certify { "CREATED" } else { "VERIFIED" },
btor2_region_property::BTOR2_CHANNEL_TRACE_PROOF_VERSION,
manifest.expected_channels,
summary.metrics.logical_queries,
summary.metrics.proof_members,
summary.metrics.reused_logical_queries,
summary.metrics.explicit_state_members,
summary.metrics.bitblast_members,
summary.metrics.evidence_bytes,
encoded.len(),
plan.map_or_else(
|| "not-applied".to_string(),
|plan| plan.projected_work.to_string()
),
started.elapsed().as_micros(),
);
for (index, result) in summary.results.iter().enumerate() {
let answer = match result.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let backend = match result.backend {
btor2_region_property::Btor2ChannelTraceBackend::RepresentativeClass => {
"representative-class"
}
btor2_region_property::Btor2ChannelTraceBackend::DirectExact => "direct-exact",
};
let solver = match result.solver {
btor2_region_property::Btor2ChannelTraceSolver::ExplicitState => {
"explicit-state"
}
btor2_region_property::Btor2ChannelTraceSolver::BitblastCnf => "bitblast-cnf",
};
println!(
"btor2-channel-trace index={index} query_id={} channel={} length={} mask={} value={} horizon={} answer={answer} bad_frame={} backend={backend} solver={solver} representative_channel={} witness_valuations={}",
result.query.query_id,
result.query.channel_index,
result.query.pattern.length(),
result.query.pattern.mask(),
result.query.pattern.value(),
result.query.horizon,
result
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
result.representative_channel,
result.witness_valuations.len(),
);
}
Ok(true)
}
"btor2-channel-property-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-channel-property-cli-version"
.to_string(),
);
}
println!(
"btor2_channel_property_cli_version={BTOR2_CHANNEL_PROPERTY_CLI_VERSION} artifact_version={} query_manifest_version={BTOR2_CHANNEL_PROPERTY_QUERY_MANIFEST_VERSION} policy_version={BTOR2_CHANNEL_PROPERTY_POLICY_VERSION} max_query_manifest_bytes={BTOR2_CHANNEL_PROPERTY_QUERY_MANIFEST_MAX_BYTES} max_policy_bytes={BTOR2_CHANNEL_PROPERTY_POLICY_MAX_BYTES} max_model_bytes={} max_channels={} max_queries={} max_evidence_bytes={} max_artifact_bytes={} max_projected_work={} refusal_exit=3 routing=static-explicit-or-bitblast fallback=exact result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed verification=source-replay",
btor2_region_property::BTOR2_CHANNEL_PROPERTY_PROOF_VERSION,
btor2::MAX_BTOR2_BYTES,
btor2_region_extract::MAX_REGION_CHANNELS,
btor2_region_property::MAX_CHANNEL_PROPERTY_QUERIES,
btor2_region_property::MAX_CHANNEL_PROPERTY_EVIDENCE_BYTES,
btor2_region_property::MAX_CHANNEL_PROPERTY_ARTIFACT_BYTES,
btor2_region_property::MAX_CHANNEL_PROPERTY_PROJECTED_WORK,
);
Ok(true)
}
"btor2-channel-property-observability-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-channel-property-observability-cli-version"
.to_string(),
);
}
println!(
"btor2_channel_property_observability_cli_version={BTOR2_CHANNEL_PROPERTY_OBSERVABILITY_CLI_VERSION} base_cli_version={BTOR2_CHANNEL_PROPERTY_CLI_VERSION} phase_metrics_version={BTOR2_CHANNEL_PROPERTY_PHASE_METRICS_VERSION} phases=input,structural-admission,preflight,proof-construction,encoding,artifact-decode,source-replay,publication timing_calibration=none correctness_dependency=none partial_metrics_on_failure=none unsupported=fail-closed"
);
Ok(true)
}
"certify-btor2-channel-properties"
| "verify-btor2-channel-properties"
| "certify-btor2-channel-properties-observed"
| "verify-btor2-channel-properties-observed" => {
let certify = command.starts_with("certify-");
let observed = command.ends_with("-observed");
if args.len() != 5 {
return Err(format!(
"usage: guarded-continuation-checker {command} MODEL.btor2 QUERIES.txt POLICY.txt {}",
if certify {
"OUTPUT.channel-properties"
} else {
"INPUT.channel-properties"
}
));
}
let total_started = Instant::now();
let input_started = Instant::now();
let model = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 channel property model",
)?;
let manifest = parse_btor2_channel_property_query_manifest(Path::new(&args[2]))?;
let production_policy = parse_btor2_channel_property_policy(Path::new(&args[3]))?;
let mut input_micros = input_started.elapsed().as_micros();
let region_policy = btor2_region_extract::Btor2RegionPolicy::default();
let started = Instant::now();
let mut structural_admission_micros = 0;
let mut preflight_micros = 0;
let mut proof_construction_micros = 0;
let mut encoding_micros = 0;
let (encoded, plan) = if certify {
let structural_started = Instant::now();
let structural =
btor2_region_equivalence::encode_btor2_region_equivalence_artifact(
&btor2_region_equivalence::produce_btor2_region_equivalence_artifact(
&model,
&manifest.semantic_roots,
manifest.expected_channels,
region_policy,
)
.map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())?;
structural_admission_micros = structural_started.elapsed().as_micros();
let (plan, encoded, phases) =
btor2_region_property::produce_btor2_channel_property_proof_bytes_phase_observed(
&model,
&structural,
&manifest.queries,
region_policy,
production_policy,
)
.map_err(map_btor2_channel_property_production_error)?;
preflight_micros = phases.preflight_micros;
proof_construction_micros = phases.proof_construction_micros;
encoding_micros = phases.encoding_micros;
(encoded, Some(plan))
} else {
let artifact_input_started = Instant::now();
let encoded = read_bounded_regular_file(
Path::new(&args[4]),
production_policy.artifact().max_artifact_bytes(),
"BTOR2 channel property artifact",
)?;
input_micros += artifact_input_started.elapsed().as_micros();
(encoded, None)
};
let artifact_decode_started = Instant::now();
let artifact = btor2_region_property::decode_btor2_channel_property_proof_artifact(
&encoded,
production_policy.artifact(),
)
.map_err(|error| error.to_string())?;
let artifact_decode_micros = artifact_decode_started.elapsed().as_micros();
let source_replay_started = Instant::now();
let structural = btor2_region_equivalence::decode_btor2_region_equivalence_artifact(
&artifact.structural_admission,
)
.map_err(|error| error.to_string())?;
if structural.expected_channels != manifest.expected_channels
|| structural.semantic_roots != manifest.semantic_roots
{
return Err(
"BTOR2 channel property manifest does not match structural admission"
.to_string(),
);
}
let summary = btor2_region_property::verify_btor2_channel_property_proof(
&model,
&manifest.queries,
&artifact,
region_policy,
)
.map_err(|error| error.to_string())?;
let source_replay_micros = source_replay_started.elapsed().as_micros();
let publication_started = Instant::now();
if certify {
write_new_certificate(Path::new(&args[4]), &encoded)?;
}
let publication_micros = publication_started.elapsed().as_micros();
let status = if certify { "CREATED" } else { "VERIFIED" };
println!(
"btor2-channel-properties status={status} cli_version={BTOR2_CHANNEL_PROPERTY_CLI_VERSION} artifact_version={} channels={} logical_queries={} proof_members={} reused_queries={} explicit_members={} bitblast_members={} evidence_bytes={} artifact_bytes={} projected_work={} elapsed_micros={}",
btor2_region_property::BTOR2_CHANNEL_PROPERTY_PROOF_VERSION,
manifest.expected_channels,
summary.metrics.logical_queries,
summary.metrics.proof_members,
summary.metrics.reused_logical_queries,
summary.metrics.explicit_state_members,
summary.metrics.bitblast_members,
summary.metrics.evidence_bytes,
encoded.len(),
plan.map_or_else(
|| "not-applied".to_string(),
|plan| plan.projected_work.to_string()
),
started.elapsed().as_micros(),
);
for (index, result) in summary.results.iter().enumerate() {
let property = match result.query.property {
btor2_region_property::Btor2ChannelProperty::OutputHigh => "output-high",
btor2_region_property::Btor2ChannelProperty::OutputLow => "output-low",
};
let answer = match result.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let backend = match result.backend {
btor2_region_property::Btor2ChannelPropertyBackend::RepresentativeClass => {
"representative-class"
}
btor2_region_property::Btor2ChannelPropertyBackend::DirectExact => {
"direct-exact"
}
};
let solver = match result.solver {
btor2_region_property::Btor2ChannelPropertySolver::ExplicitState => {
"explicit-state"
}
btor2_region_property::Btor2ChannelPropertySolver::BitblastCnf => {
"bitblast-cnf"
}
};
let bad_frame = result
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-channel-property index={index} query_id={} channel={} property={property} horizon={} answer={answer} bad_frame={bad_frame} backend={backend} solver={solver} representative_channel={} witness_valuations={}",
result.query.query_id,
result.query.channel_index,
result.query.horizon,
result.representative_channel,
result.witness_valuations.len(),
);
}
if observed {
println!(
"btor2-channel-property-phases status=MEASURED observability_cli_version={BTOR2_CHANNEL_PROPERTY_OBSERVABILITY_CLI_VERSION} phase_metrics_version={BTOR2_CHANNEL_PROPERTY_PHASE_METRICS_VERSION} operation={} input_micros={input_micros} structural_admission_micros={structural_admission_micros} preflight_micros={preflight_micros} proof_construction_micros={proof_construction_micros} encoding_micros={encoding_micros} artifact_decode_micros={artifact_decode_micros} source_replay_micros={source_replay_micros} publication_micros={publication_micros} total_micros={} timing_calibration=none correctness_dependency=none",
if certify { "certify" } else { "verify" },
total_started.elapsed().as_micros(),
);
}
Ok(true)
}
"btor2-revision-impact-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-revision-impact-cli-version"
.to_string(),
);
}
let policy = revision_impact::RevisionImpactPolicy::default();
println!(
"revision_impact_cli_version={REVISION_IMPACT_CLI_VERSION} impact_version={} query_manifest_version={REVISION_IMPACT_QUERY_MANIFEST_VERSION} max_query_manifest_bytes={REVISION_IMPACT_QUERY_MANIFEST_MAX_BYTES} max_input_bytes={} max_evidence_bytes={} max_bundle_bytes={} max_atoms={} max_combinations={} max_queries={} semantics=exact-counterfactual-v1 work_schema=verification-v1 query_schema=transition-semantic-set-v1 routing=none fallback=none unsupported=fail-closed",
revision_impact::REVISION_IMPACT_CERTIFICATE_VERSION,
policy.max_input_bytes,
policy.max_evidence_bytes,
policy.max_bundle_bytes,
revision_impact::MAX_IMPACT_ATOMS,
policy.max_combinations,
policy.max_queries,
);
Ok(true)
}
"composed-witness-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker composed-witness-cli-version".to_string(),
);
}
println!(
"composed_witness_cli_version={} format=ascii-aiger-1.9 scope=safety baseline=fm-2026 max_witnesses={} max_input_bytes={} liveness=unsupported comment_mapping=unsupported unsupported=fail-closed",
composed_witness::COMPOSED_WITNESS_BASELINE_VERSION,
composed_witness::MAX_COMPOSED_WITNESSES,
composed_witness::MAX_COMPOSED_AIGER_BYTES,
);
Ok(true)
}
"compose-safety-witnesses-v1" => {
if args.len() < 5 || args.len() - 3 > composed_witness::MAX_COMPOSED_WITNESSES {
return Err("usage: guarded-continuation-checker compose-safety-witnesses-v1 MODEL.aag OUTPUT.aag WITNESS.aag WITNESS.aag [...]".to_string());
}
let model = read_bounded_regular_file(
Path::new(&args[1]),
composed_witness::MAX_COMPOSED_AIGER_BYTES,
"composed-witness model",
)?;
let witnesses = args[3..]
.iter()
.map(|path| {
read_bounded_regular_file(
Path::new(path),
composed_witness::MAX_COMPOSED_AIGER_BYTES,
"composed-witness member",
)
})
.collect::<Result<Vec<_>, _>>()?;
let references = witnesses.iter().map(Vec::as_slice).collect::<Vec<_>>();
let encoded = composed_witness::compose_safety_witnesses_v1(&model, &references)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[2]), &encoded)?;
println!(
"composed-witness status=CREATED cli_version={} baseline=fm-2026 witnesses={} artifact_bytes={} output={}",
composed_witness::COMPOSED_WITNESS_BASELINE_VERSION,
references.len(),
encoded.len(),
Path::new(&args[2]).display(),
);
Ok(true)
}
"controller-mtbdd-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker controller-mtbdd-cli-version".to_string(),
);
}
println!(
"controller_mtbdd_cli_version={CONTROLLER_MTBDD_CLI_VERSION} mtbdd_version={} plant_artifact_version={} manifest_version={CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION} max_manifest_bytes={CONTROLLER_MTBDD_PLANT_MANIFEST_MAX_BYTES} max_artifact_bytes={} max_members={} max_state_bits={} max_inputs={} max_outputs={} max_nodes={} max_terminals={} max_assignments={} max_horizon={} unsupported=fail-closed",
controller_mtbdd::CONTROLLER_MTBDD_VERSION,
controller_plant_artifact::MTBDD_PLANT_ARTIFACT_VERSION,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
controller_mtbdd::MAX_MTBDD_STATE_BITS,
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
controller_mtbdd::MAX_MTBDD_NODES,
controller_mtbdd::MAX_MTBDD_TERMINALS,
controller_mtbdd::MAX_MTBDD_ASSIGNMENTS,
guarded_continuation_checker::controller_plant::MAX_COMPOSITION_HORIZON,
);
Ok(true)
}
"controller-proof-mtbdd-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker controller-proof-mtbdd-cli-version"
.to_string(),
);
}
println!(
"controller_proof_mtbdd_cli_version={CONTROLLER_PROOF_MTBDD_CLI_VERSION} mtbdd_version={} equivalence_proof_version={} plant_artifact_version={} manifest_version={CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION} max_manifest_bytes={CONTROLLER_MTBDD_PLANT_MANIFEST_MAX_BYTES} max_artifact_bytes={} max_equivalence_artifact_bytes={} max_unsat_proof_bytes={} max_members={} max_state_bits={} max_inputs={} max_outputs={} max_nodes={} max_terminals={} max_horizon={} verification=unsat-miter exhaustive_replay=no unsupported=fail-closed",
controller_mtbdd::CONTROLLER_MTBDD_VERSION,
guarded_continuation_checker::controller_mtbdd_proof::CONTROLLER_MTBDD_EQUIVALENCE_VERSION,
controller_plant_artifact::PROOF_MTBDD_PLANT_ARTIFACT_VERSION,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
guarded_continuation_checker::controller_mtbdd_proof::MAX_EQUIVALENCE_ARTIFACT_BYTES,
guarded_continuation_checker::unsat_proof::MAX_UNSAT_PROOF_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
controller_mtbdd::MAX_MTBDD_STATE_BITS,
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
controller_mtbdd::MAX_MTBDD_NODES,
controller_mtbdd::MAX_MTBDD_TERMINALS,
guarded_continuation_checker::controller_plant::MAX_COMPOSITION_HORIZON,
);
Ok(true)
}
"controller-split-evidence-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker controller-split-evidence-cli-version"
.to_string(),
);
}
println!(
"controller_split_evidence_cli_version={CONTROLLER_SPLIT_EVIDENCE_CLI_VERSION} controller_artifact_version={} plant_artifact_version={} manifest_version={CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION} max_manifest_bytes={CONTROLLER_MTBDD_PLANT_MANIFEST_MAX_BYTES} max_artifact_bytes={} max_batches={} admission=once verification=unsat-miter exhaustive_replay=no source_binding=sha256 obligation_binding=complete-ordered unsupported=fail-closed",
controller_plant_artifact::CONTROLLER_PROOF_EVIDENCE_VERSION,
controller_plant_artifact::BOUND_PLANT_RESULTS_VERSION,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
);
Ok(true)
}
"controller-split-resource-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker controller-split-resource-cli-version"
.to_string(),
);
}
println!("{}", controller_split_resource_capability_line());
Ok(true)
}
"controller-split-observability-cli-version" => {
if args.len() != 1 {
return Err("usage: guarded-continuation-checker controller-split-observability-cli-version".to_string());
}
println!("{}", controller_split_resource_capability_line());
println!("{}", controller_split_observability_capability_line());
Ok(true)
}
"controller-split-allocation-observability-cli-version" => {
if args.len() != 1 {
return Err("usage: guarded-continuation-checker controller-split-allocation-observability-cli-version".to_string());
}
println!("{}", controller_split_resource_capability_line());
println!("{}", controller_split_observability_capability_line());
println!(
"{}",
controller_split_allocation_observability_capability_line()
);
Ok(true)
}
"controller-split-cache-observability-cli-version" => {
if args.len() != 1 {
return Err("usage: guarded-continuation-checker controller-split-cache-observability-cli-version".to_string());
}
println!("{}", controller_split_resource_capability_line());
println!("{}", controller_split_observability_capability_line());
println!(
"{}",
controller_split_allocation_observability_capability_line()
);
println!("{}", controller_split_cache_observability_capability_line());
Ok(true)
}
"certify-controller-proof-evidence-v1" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker certify-controller-proof-evidence-v1 MANIFEST.txt OUTPUT.controller-evidence".to_string());
}
let started = Instant::now();
let (manifest, controller, controller_digest, _plants, _plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
let mtbdd = controller_mtbdd::produce_controller_mtbdd(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
)
.map_err(|error| error.to_string())?;
let artifact = controller_plant_artifact::produce_controller_proof_evidence_artifact(
&controller,
controller_digest,
&mtbdd,
)
.map_err(|error| error.to_string())?;
let encoded =
controller_plant_artifact::encode_controller_proof_evidence_artifact(&artifact)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[2]), &encoded)?;
println!(
"controller-split-evidence status=CREATED cli_version={CONTROLLER_SPLIT_EVIDENCE_CLI_VERSION} artifact_version={} mtbdd_nodes={} mtbdd_terminals={} artifact_bytes={} elapsed_micros={} output={}",
controller_plant_artifact::CONTROLLER_PROOF_EVIDENCE_VERSION,
mtbdd.nodes.len(),
mtbdd.terminals.len(),
encoded.len(),
started.elapsed().as_micros(),
Path::new(&args[2]).display(),
);
Ok(true)
}
"certify-bound-plant-results-v1" => {
if args.len() != 4 {
return Err("usage: guarded-continuation-checker certify-bound-plant-results-v1 MANIFEST.txt INPUT.controller-evidence OUTPUT.plant-results".to_string());
}
let started = Instant::now();
let (manifest, controller, controller_digest, plants, plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
let evidence = read_bounded_regular_file(
Path::new(&args[2]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"controller proof evidence",
)?;
let admitted = controller_plant_artifact::admit_controller_proof_evidence(
&controller,
controller_digest,
&evidence,
)
.map_err(|error| error.to_string())?;
if admitted.relevant_inputs() != manifest.relevant_inputs
|| admitted.observed_outputs() != manifest.observed_outputs
{
return Err(
"controller proof evidence does not match manifest boundary".to_string()
);
}
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let artifact =
controller_plant_artifact::produce_bound_plant_results_with_admitted_controller(
&admitted, &inputs,
)
.map_err(|error| error.to_string())?;
let encoded = controller_plant_artifact::encode_bound_plant_results_artifact(&artifact)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[3]), &encoded)?;
println!(
"controller-split-plant status=CREATED cli_version={CONTROLLER_SPLIT_EVIDENCE_CLI_VERSION} artifact_version={} members={} artifact_bytes={} elapsed_micros={} output={}",
controller_plant_artifact::BOUND_PLANT_RESULTS_VERSION,
inputs.len(),
encoded.len(),
started.elapsed().as_micros(),
Path::new(&args[3]).display(),
);
Ok(true)
}
"verify-bound-plant-result-set-v1" => {
if args.len() < 4
|| !(args.len() - 2).is_multiple_of(2)
|| (args.len() - 2) / 2
> controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS
{
return Err("usage: guarded-continuation-checker verify-bound-plant-result-set-v1 INPUT.controller-evidence MANIFEST.txt INPUT.plant-results [MANIFEST.txt INPUT.plant-results ...]".to_string());
}
let started = Instant::now();
let evidence = read_bounded_regular_file(
Path::new(&args[1]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"controller proof evidence",
)?;
let (_, admitted_controller, admitted_digest, _, _, _) =
load_controller_plant_manifest(
Path::new(&args[2]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
let admission_started = Instant::now();
let admitted = controller_plant_artifact::admit_controller_proof_evidence(
&admitted_controller,
admitted_digest,
&evidence,
)
.map_err(|error| error.to_string())?;
let admission_micros = admission_started.elapsed().as_micros();
let mut total_members = 0usize;
let mut total_safe = 0usize;
let mut total_unsafe = 0usize;
let mut total_reachable = 0usize;
let mut total_transitions = 0usize;
for (batch_index, pair) in args[2..].chunks_exact(2).enumerate() {
let (manifest, controller, controller_digest, plants, plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&pair[0]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
if controller != admitted_controller
|| controller_digest != admitted_digest
|| admitted.relevant_inputs() != manifest.relevant_inputs
|| admitted.observed_outputs() != manifest.observed_outputs
{
return Err(format!(
"controller split evidence batch {batch_index} does not match admitted controller"
));
}
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let encoded = read_bounded_regular_file(
Path::new(&pair[1]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"bound plant results",
)?;
let batch_started = Instant::now();
let summary =
controller_plant_artifact::verify_bound_plant_results_with_admitted_controller(
&admitted, &inputs, &encoded,
)
.map_err(|error| error.to_string())?;
println!(
"controller-split-batch index={batch_index} status=VERIFIED members={} safe={} unsafe={} reachable_product_states={} explored_transitions={} artifact_bytes={} verification_micros={}",
summary.members.len(),
summary.safe,
summary.unsafe_count,
summary.reachable_product_states,
summary.explored_transitions,
encoded.len(),
batch_started.elapsed().as_micros(),
);
total_members = total_members
.checked_add(summary.members.len())
.ok_or_else(|| "controller split member count overflow".to_string())?;
total_safe = total_safe
.checked_add(summary.safe)
.ok_or_else(|| "controller split SAFE count overflow".to_string())?;
total_unsafe = total_unsafe
.checked_add(summary.unsafe_count)
.ok_or_else(|| "controller split UNSAFE count overflow".to_string())?;
total_reachable = total_reachable
.checked_add(summary.reachable_product_states)
.ok_or_else(|| "controller split reachable count overflow".to_string())?;
total_transitions = total_transitions
.checked_add(summary.explored_transitions)
.ok_or_else(|| "controller split transition count overflow".to_string())?;
}
println!(
"controller-split-set status=VERIFIED cli_version={CONTROLLER_SPLIT_EVIDENCE_CLI_VERSION} controller_admissions=1 batches={} members={total_members} safe={total_safe} unsafe={total_unsafe} reachable_product_states={total_reachable} explored_transitions={total_transitions} controller_evidence_bytes={} admission_micros={admission_micros} elapsed_micros={}",
(args.len() - 2) / 2,
evidence.len(),
started.elapsed().as_micros(),
);
Ok(true)
}
"verify-bound-plant-result-set-with-resources-v1"
| "verify-bound-plant-result-set-with-resources-observed-v1"
| "verify-bound-plant-result-set-with-resources-allocation-observed-v1"
| "verify-bound-plant-result-set-with-resources-cache-observed-v1" => {
if args.len() < 5 || !(args.len() - 3).is_multiple_of(2) {
return Err(format!(
"usage: guarded-continuation-checker {} INPUT.controller-evidence POLICY.txt MANIFEST.txt INPUT.plant-results [MANIFEST.txt INPUT.plant-results ...]",
args[0]
));
}
let emit_cache_observability =
args[0] == "verify-bound-plant-result-set-with-resources-cache-observed-v1";
let emit_allocation_observability = emit_cache_observability
|| args[0] == "verify-bound-plant-result-set-with-resources-allocation-observed-v1";
let emit_observability = emit_allocation_observability
|| args[0] == "verify-bound-plant-result-set-with-resources-observed-v1";
let allocation_observation = emit_allocation_observability
.then(observed_allocator::AllocationObservationGuard::start)
.transpose()?;
let started = Instant::now();
let mut manifest_loads = 0usize;
let mut plant_artifact_reads = 0usize;
let mut resource_assessments = 0usize;
let mut batch_verifications = 0usize;
let mut buffered_result_rows = 0usize;
let mut prepared_batch_count = 0usize;
let mut cache_lookups = 0usize;
let mut cache_hits = 0usize;
let mut cache_misses = 0usize;
let policy = parse_controller_split_resource_policy(Path::new(&args[2]))?;
let batch_count = (args.len() - 3) / 2;
if batch_count > policy.max_batches {
return Err(classify_controller_split_resource_error(
"controller split resource batch limit exceeded".to_string(),
));
}
let evidence = read_bounded_regular_file(
Path::new(&args[1]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"controller proof evidence",
)?;
let (_, admitted_controller, admitted_digest, _, _, _) =
load_controller_plant_manifest(
Path::new(&args[3]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
increment_controller_split_observation(&mut manifest_loads, "manifest-load")?;
let policy_and_input_micros = started.elapsed().as_micros();
let admission_started = Instant::now();
let governed_admission =
controller_plant_artifact::admit_controller_proof_evidence_with_resources(
&admitted_controller,
admitted_digest,
&evidence,
policy.controller,
)
.map_err(|error| classify_controller_split_resource_error(error.to_string()))?;
let admission_micros = admission_started.elapsed().as_micros();
let mut total_plant_artifact_bytes = 0usize;
let mut total_members = 0usize;
let mut total_transition_bound = 0usize;
struct PreparedSplitBatch {
manifest: ControllerMtbddPlantManifest,
snapshot: LoadedSourceModelSnapshot,
resources: controller_plant_artifact::ControllerPlantResourceAssessment,
results_sha256: [u8; 32],
}
let mut prepared_batches = Vec::with_capacity(batch_count);
let preflight_started = Instant::now();
for (batch_index, pair) in args[3..].chunks_exact(2).enumerate() {
let (manifest, controller, controller_digest, plants, plant_digests, snapshot) =
load_controller_plant_manifest(
Path::new(&pair[0]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
increment_controller_split_observation(&mut manifest_loads, "manifest-load")?;
if controller != admitted_controller
|| controller_digest != admitted_digest
|| governed_admission.admitted.relevant_inputs() != manifest.relevant_inputs
|| governed_admission.admitted.observed_outputs() != manifest.observed_outputs
{
return Err(format!(
"controller split resource batch {batch_index} does not match admitted controller"
));
}
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let encoded = read_bounded_regular_file(
Path::new(&pair[1]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"bound plant results",
)?;
increment_controller_split_observation(&mut plant_artifact_reads, "artifact-read")?;
let resources = controller_plant_artifact::assess_bound_plant_results_resources(
&controller,
&inputs,
&encoded,
policy.plant,
)
.map_err(|error| classify_controller_split_resource_error(error.to_string()))?;
increment_controller_split_observation(
&mut resource_assessments,
"resource-assessment",
)?;
total_plant_artifact_bytes = total_plant_artifact_bytes
.checked_add(resources.artifact_bytes)
.ok_or_else(|| {
classify_controller_split_resource_error(
"controller split resource total plant artifact-byte limit exceeded"
.to_string(),
)
})?;
total_members = total_members
.checked_add(resources.members)
.ok_or_else(|| {
classify_controller_split_resource_error(
"controller split resource total member limit exceeded".to_string(),
)
})?;
total_transition_bound = total_transition_bound
.checked_add(resources.transition_evaluation_bound)
.ok_or_else(|| {
classify_controller_split_resource_error(
"controller split resource total transition limit exceeded".to_string(),
)
})?;
if total_plant_artifact_bytes > policy.max_total_plant_artifact_bytes {
return Err(classify_controller_split_resource_error(
"controller split resource total plant artifact-byte limit exceeded"
.to_string(),
));
}
if total_members > policy.max_total_members {
return Err(classify_controller_split_resource_error(
"controller split resource total member limit exceeded".to_string(),
));
}
if total_transition_bound > policy.max_total_transition_evaluations {
return Err(classify_controller_split_resource_error(
"controller split resource total transition limit exceeded".to_string(),
));
}
prepared_batches.push(PreparedSplitBatch {
manifest,
snapshot,
resources,
results_sha256: Sha256::digest(&encoded).into(),
});
increment_controller_split_observation(
&mut prepared_batch_count,
"prepared-batch",
)?;
}
let complete_set_preflight_micros = preflight_started.elapsed().as_micros();
let mut total_safe = 0usize;
let mut total_unsafe = 0usize;
let mut total_reachable = 0usize;
let mut total_explored = 0usize;
let mut verified_batches = Vec::with_capacity(batch_count);
struct VerifiedSplitCacheEntry {
manifest: ControllerMtbddPlantManifest,
snapshot: LoadedSourceModelSnapshot,
resources: controller_plant_artifact::ControllerPlantResourceAssessment,
results_sha256: [u8; 32],
verification: controller_plant_artifact::ControllerMtbddPlantBatchSummary,
}
let cache_capacity = if emit_cache_observability {
batch_count
} else {
0
};
let mut verification_cache =
Vec::<VerifiedSplitCacheEntry>::with_capacity(cache_capacity);
let replay_started = Instant::now();
for (batch_index, (pair, prepared)) in
args[3..].chunks_exact(2).zip(&prepared_batches).enumerate()
{
let (manifest, controller, controller_digest, plants, plant_digests, snapshot) =
load_controller_plant_manifest(
Path::new(&pair[0]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
increment_controller_split_observation(&mut manifest_loads, "manifest-load")?;
if manifest != prepared.manifest
|| snapshot != prepared.snapshot
|| controller != admitted_controller
|| controller_digest != admitted_digest
{
return Err(format!(
"controller split resource batch {batch_index} changed after preflight"
));
}
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let encoded = read_bounded_regular_file(
Path::new(&pair[1]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"bound plant results",
)?;
increment_controller_split_observation(&mut plant_artifact_reads, "artifact-read")?;
if <[u8; 32]>::from(Sha256::digest(&encoded)) != prepared.results_sha256 {
return Err(format!(
"controller split resource batch {batch_index} changed after preflight"
));
}
let resources = controller_plant_artifact::assess_bound_plant_results_resources(
&controller,
&inputs,
&encoded,
policy.plant,
)
.map_err(|error| classify_controller_split_resource_error(error.to_string()))?;
increment_controller_split_observation(
&mut resource_assessments,
"resource-assessment",
)?;
if resources != prepared.resources {
return Err(format!(
"controller split resource batch {batch_index} changed after preflight"
));
}
let batch_started = Instant::now();
let verification = if emit_cache_observability {
increment_controller_split_observation(&mut cache_lookups, "cache-lookup")?;
let cached = verification_cache.iter().find(|entry| {
entry.manifest == manifest
&& entry.snapshot == snapshot
&& entry.resources == resources
&& entry.results_sha256 == prepared.results_sha256
});
if let Some(entry) = cached {
increment_controller_split_observation(&mut cache_hits, "cache-hit")?;
entry.verification.clone()
} else {
increment_controller_split_observation(&mut cache_misses, "cache-miss")?;
let verification = controller_plant_artifact::verify_bound_plant_results_with_admitted_controller(
&governed_admission.admitted,
&inputs,
&encoded,
)
.map_err(|error| error.to_string())?;
verification_cache.push(VerifiedSplitCacheEntry {
manifest: manifest.clone(),
snapshot: snapshot.clone(),
resources: resources.clone(),
results_sha256: prepared.results_sha256,
verification: verification.clone(),
});
verification
}
} else {
controller_plant_artifact::verify_bound_plant_results_with_admitted_controller(
&governed_admission.admitted,
&inputs,
&encoded,
)
.map_err(|error| error.to_string())?
};
increment_controller_split_observation(
&mut batch_verifications,
"batch-verification",
)?;
let verification_micros = batch_started.elapsed().as_micros();
total_safe = total_safe
.checked_add(verification.safe)
.ok_or_else(|| "controller split resource SAFE count overflow".to_string())?;
total_unsafe = total_unsafe
.checked_add(verification.unsafe_count)
.ok_or_else(|| "controller split resource UNSAFE count overflow".to_string())?;
total_reachable = total_reachable
.checked_add(verification.reachable_product_states)
.ok_or_else(|| {
"controller split resource reachable count overflow".to_string()
})?;
total_explored = total_explored
.checked_add(verification.explored_transitions)
.ok_or_else(|| {
"controller split resource transition count overflow".to_string()
})?;
verified_batches.push((
batch_index,
controller_plant_artifact::GovernedBoundPlantResultsSummary {
resources,
verification,
},
verification_micros,
));
increment_controller_split_observation(
&mut buffered_result_rows,
"buffered-result-row",
)?;
}
let semantic_replay_micros = replay_started.elapsed().as_micros();
let total_micros = started.elapsed().as_micros();
increment_controller_split_observation(
&mut buffered_result_rows,
"buffered-result-row",
)?;
let allocation_observation = allocation_observation
.map(observed_allocator::AllocationObservationGuard::finish)
.transpose()?;
for (batch_index, governed, verification_micros) in &verified_batches {
println!(
"controller-split-resource-batch index={batch_index} status=VERIFIED policy_version={CONTROLLER_SPLIT_RESOURCE_POLICY_VERSION} envelope_version={} artifact_version={} members={} maximum_member_horizon={} maximum_product_states={} transition_evaluation_bound={} safe={} unsafe={} reachable_product_states={} explored_transitions={} artifact_bytes={} verification_micros={verification_micros}",
governed.resources.version,
controller_plant_artifact::BOUND_PLANT_RESULTS_VERSION,
governed.resources.members,
governed.resources.maximum_member_horizon,
governed.resources.maximum_product_states,
governed.resources.transition_evaluation_bound,
governed.verification.safe,
governed.verification.unsafe_count,
governed.verification.reachable_product_states,
governed.verification.explored_transitions,
governed.resources.artifact_bytes,
);
}
println!(
"controller-split-resource-set status=VERIFIED cli_version={CONTROLLER_SPLIT_RESOURCE_CLI_VERSION} policy_version={CONTROLLER_SPLIT_RESOURCE_POLICY_VERSION} controller_envelope_version={} plant_envelope_version={} controller_admissions=1 batches={batch_count} members={total_members} safe={total_safe} unsafe={total_unsafe} reachable_product_states={total_reachable} explored_transitions={total_explored} controller_evidence_bytes={} controller_mtbdd_bytes={} equivalence_artifact_bytes={} unsat_proof_bytes={} total_plant_artifact_bytes={total_plant_artifact_bytes} total_transition_evaluation_bound={total_transition_bound} admission_micros={admission_micros} elapsed_micros={}",
governed_admission.resources.version,
controller_plant_artifact::CONTROLLER_PLANT_RESOURCE_ENVELOPE_VERSION,
governed_admission.resources.artifact_bytes,
governed_admission.resources.mtbdd_bytes,
governed_admission.resources.equivalence_artifact_bytes,
governed_admission.resources.unsat_proof_bytes,
total_micros,
);
if emit_observability {
println!(
"controller-split-resource-observability status=MEASURED cli_version={CONTROLLER_SPLIT_OBSERVABILITY_CLI_VERSION} phase_metrics_version={CONTROLLER_SPLIT_PHASE_METRICS_VERSION} policy_and_input_micros={policy_and_input_micros} controller_admission_micros={admission_micros} complete_set_preflight_micros={complete_set_preflight_micros} semantic_replay_micros={semantic_replay_micros} total_micros={total_micros} controller_admissions=1 manifest_loads={manifest_loads} plant_artifact_reads={plant_artifact_reads} resource_assessments={resource_assessments} batch_verifications={batch_verifications} buffered_result_rows={buffered_result_rows} prepared_batches={prepared_batch_count} prepared_members={total_members} controller_evidence_bytes={} total_plant_artifact_bytes={total_plant_artifact_bytes} total_transition_evaluation_bound={total_transition_bound} timing_calibration=none",
governed_admission.resources.artifact_bytes,
);
}
if let Some(allocation) = allocation_observation {
println!(
"controller-split-allocation-observability status=MEASURED cli_version={CONTROLLER_SPLIT_ALLOCATION_OBSERVABILITY_CLI_VERSION} allocator=system scope=policy-through-replay allocation_calls={} allocated_bytes={} deallocation_calls={} deallocated_bytes={} reallocation_calls={} reallocated_bytes={} overflow=none timing_calibration=none",
allocation.allocation_calls,
allocation.allocated_bytes,
allocation.deallocation_calls,
allocation.deallocated_bytes,
allocation.reallocation_calls,
allocation.reallocated_bytes,
);
}
if emit_cache_observability {
println!(
"controller-split-cache-observability status=MEASURED cli_version={CONTROLLER_SPLIT_CACHE_OBSERVABILITY_CLI_VERSION} scope=semantic-replay key=manifest-snapshot,resource-assessment,result-sha256 lookups={cache_lookups} hits={cache_hits} misses={cache_misses} entries={} integrity_preflight=required overflow=none timing_calibration=none",
verification_cache.len(),
);
}
Ok(true)
}
"controller-plant-portfolio-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker controller-plant-portfolio-cli-version"
.to_string(),
);
}
println!(
"controller_plant_portfolio_cli_version={CONTROLLER_PLANT_PORTFOLIO_CLI_VERSION} artifact_version={} manifest_version={CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION} max_manifest_bytes={CONTROLLER_MTBDD_PLANT_MANIFEST_MAX_BYTES} max_artifact_bytes={} max_members={} backends=mtbdd,direct-exact routing=static fallback=exact unsupported=fail-closed",
controller_plant_artifact::MTBDD_PLANT_PORTFOLIO_VERSION,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
);
Ok(true)
}
"controller-plant-resource-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker controller-plant-resource-cli-version"
.to_string(),
);
}
println!(
"controller_plant_resource_cli_version={CONTROLLER_PLANT_RESOURCE_CLI_VERSION} policy_version={CONTROLLER_PLANT_RESOURCE_POLICY_VERSION} envelope_version={} manifest_version={CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION} portfolio_artifact_version={} max_policy_bytes={CONTROLLER_PLANT_RESOURCE_POLICY_MAX_BYTES} max_artifact_bytes={} max_members={} max_horizon={} max_product_states={} refusal_exit=3 accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=reason-v1 unsupported=fail-closed",
controller_plant_artifact::CONTROLLER_PLANT_RESOURCE_ENVELOPE_VERSION,
controller_plant_artifact::MTBDD_PLANT_PORTFOLIO_VERSION,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
guarded_continuation_checker::controller_plant::MAX_COMPOSITION_HORIZON,
guarded_continuation_checker::controller_plant::MAX_PRODUCT_STATES,
);
Ok(true)
}
"controller-proof-mtbdd-resource-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker controller-proof-mtbdd-resource-cli-version"
.to_string(),
);
}
println!(
"controller_proof_mtbdd_resource_cli_version={CONTROLLER_PROOF_MTBDD_RESOURCE_CLI_VERSION} policy_version={CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_VERSION} envelope_version={} manifest_version={CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION} artifact_version={} max_policy_bytes={CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_MAX_BYTES} max_artifact_bytes={} max_equivalence_artifact_bytes={} max_unsat_proof_bytes={} max_members={} max_horizon={} max_product_states={} refusal_exit=3 verification=unsat-miter exhaustive_replay=no accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=proof-reason-v1 unsupported=fail-closed",
controller_plant_artifact::CONTROLLER_PROOF_MTBDD_RESOURCE_ENVELOPE_VERSION,
controller_plant_artifact::PROOF_MTBDD_PLANT_ARTIFACT_VERSION,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
guarded_continuation_checker::controller_mtbdd_proof::MAX_EQUIVALENCE_ARTIFACT_BYTES,
guarded_continuation_checker::unsat_proof::MAX_UNSAT_PROOF_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
guarded_continuation_checker::controller_plant::MAX_COMPOSITION_HORIZON,
guarded_continuation_checker::controller_plant::MAX_PRODUCT_STATES,
);
Ok(true)
}
"controller-proof-mtbdd-portfolio-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker controller-proof-mtbdd-portfolio-cli-version"
.to_string(),
);
}
println!(
"controller_proof_mtbdd_portfolio_cli_version={CONTROLLER_PROOF_MTBDD_PORTFOLIO_CLI_VERSION} policy_version={CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_VERSION} envelope_version={} artifact_version={} proof_artifact_version={} direct_artifact_version={} manifest_version={CONTROLLER_MTBDD_PLANT_MANIFEST_VERSION} source_model_attestation_version={} max_policy_bytes={CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_MAX_BYTES} max_artifact_bytes={} max_equivalence_artifact_bytes={} max_unsat_proof_bytes={} max_members={} max_horizon={} max_product_states={} max_attestation_bytes={} refusal_exit=3 backends=proof-mtbdd,direct-exact routing=static fallback=exact proof_failure=fail-closed attested_verification=required accounting=conservative-static timing_calibration=none result_on_refusal=none refusal_schema=proof-reason-v1 unsupported=fail-closed",
controller_plant_artifact::CONTROLLER_PROOF_MTBDD_RESOURCE_ENVELOPE_VERSION,
controller_plant_artifact::PROOF_MTBDD_PLANT_PORTFOLIO_VERSION,
controller_plant_artifact::PROOF_MTBDD_PLANT_ARTIFACT_VERSION,
controller_plant_artifact::DIRECT_PLANT_ARTIFACT_VERSION,
source_model_attestation::SOURCE_MODEL_ATTESTATION_VERSION,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
guarded_continuation_checker::controller_mtbdd_proof::MAX_EQUIVALENCE_ARTIFACT_BYTES,
guarded_continuation_checker::unsat_proof::MAX_UNSAT_PROOF_BYTES,
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_MEMBERS,
guarded_continuation_checker::controller_plant::MAX_COMPOSITION_HORIZON,
guarded_continuation_checker::controller_plant::MAX_PRODUCT_STATES,
source_model_attestation::MAX_ATTESTATION_BYTES,
);
Ok(true)
}
"certify-controller-proof-mtbdd-portfolio" | "verify-controller-proof-mtbdd-portfolio" => {
if args.len() != 3 {
return Err(format!(
"usage: guarded-continuation-checker {command} MANIFEST.txt {}",
if command == "certify-controller-proof-mtbdd-portfolio" {
"OUTPUT.proof-mtbdd-portfolio"
} else {
"INPUT.proof-mtbdd-portfolio"
}
));
}
let started = Instant::now();
let (manifest, controller, controller_digest, plants, plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let (encoded, action) = if command == "certify-controller-proof-mtbdd-portfolio" {
let encoded =
controller_plant_artifact::produce_controller_proof_mtbdd_plant_portfolio(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
&inputs,
)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[2]), &encoded)?;
(encoded, "CREATED")
} else {
(
read_bounded_regular_file(
Path::new(&args[2]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"proof-carrying controller MTBDD portfolio",
)?,
"VERIFIED",
)
};
let summary = controller_plant_artifact::verify_controller_proof_mtbdd_plant_portfolio(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
&inputs,
&encoded,
)
.map_err(|error| error.to_string())?;
println!(
"controller-proof-mtbdd-portfolio status={action} cli_version={CONTROLLER_PROOF_MTBDD_PORTFOLIO_CLI_VERSION} artifact_version={} backend={} reason={} members={} safe={} unsafe={} reachable_product_states={} explored_transitions={} assignments_checked={} artifact_bytes={} elapsed_micros={}{}",
controller_plant_artifact::PROOF_MTBDD_PLANT_PORTFOLIO_VERSION,
match summary.backend {
controller_plant_artifact::ControllerProofMtbddPlantPortfolioBackend::ProofMtbdd => "PROOF_MTBDD",
controller_plant_artifact::ControllerProofMtbddPlantPortfolioBackend::DirectExact => "DIRECT_EXACT",
},
match summary.reason {
controller_plant_artifact::ControllerMtbddPlantSelectionReason::MtbddAdmitted => "MTBDD_ADMITTED",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::BoundaryLimit => "BOUNDARY_LIMIT",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::TerminalLimit => "TERMINAL_LIMIT",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::NodeLimit => "NODE_LIMIT",
},
summary.members.len(),
summary.safe,
summary.unsafe_count,
summary.reachable_product_states,
summary.explored_transitions,
summary.assignments_checked,
encoded.len(),
started.elapsed().as_micros(),
if action == "CREATED" { format!(" output={}", args[2]) } else { String::new() },
);
for (index, member) in summary.members.iter().enumerate() {
println!(
"controller-proof-mtbdd-portfolio-member index={index} answer={} horizon={} bad_frame={} trace_steps={} reachable_product_states={} explored_transitions={}",
match member.answer {
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Safe => "SAFE",
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Unsafe => "UNSAFE",
},
member.horizon,
member.bad_frame.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
member.trace.len(),
member.reachable_product_states,
member.explored_transitions,
);
}
Ok(true)
}
"verify-controller-proof-mtbdd-portfolio-resources"
| "verify-controller-proof-mtbdd-portfolio-resources-attested" => {
let attested = command.ends_with("-attested");
let expected_args = if attested { 6 } else { 4 };
if args.len() != expected_args {
return Err(if attested {
"usage: guarded-continuation-checker verify-controller-proof-mtbdd-portfolio-resources-attested MANIFEST.txt POLICY.txt INPUT.proof-mtbdd-portfolio PROVENANCE.txt ATTESTATION.csv"
.to_string()
} else {
"usage: guarded-continuation-checker verify-controller-proof-mtbdd-portfolio-resources MANIFEST.txt POLICY.txt INPUT.proof-mtbdd-portfolio"
.to_string()
});
}
let invocation_started = Instant::now();
let policy = parse_controller_proof_mtbdd_resource_policy(Path::new(&args[2]))?;
let (manifest, controller, controller_digest, plants, plant_digests, snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
let provenance = if attested {
Some(verify_bound_source_model_provenance(
&manifest,
&snapshot,
Path::new(&args[4]),
Path::new(&args[5]),
)?)
} else {
None
};
let load_micros = invocation_started.elapsed().as_micros();
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let artifact_started = Instant::now();
let encoded = read_bounded_regular_file(
Path::new(&args[3]),
policy.envelope.composition().max_artifact_bytes(),
"proof-carrying controller MTBDD governed portfolio",
)
.map_err(classify_controller_proof_mtbdd_resource_error)?;
let artifact_micros = artifact_started.elapsed().as_micros();
let verification_started = Instant::now();
let governed = controller_plant_artifact::verify_controller_proof_mtbdd_plant_portfolio_with_resources(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
&inputs,
&encoded,
policy.envelope,
)
.map_err(|error| classify_controller_proof_mtbdd_resource_error(error.to_string()))?;
let verification_micros = verification_started.elapsed().as_micros();
let resources = governed.resources;
let summary = governed.verification;
println!(
"controller-proof-mtbdd-portfolio-resource status=VERIFIED cli_version={CONTROLLER_PROOF_MTBDD_PORTFOLIO_CLI_VERSION} policy_version={CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_VERSION} envelope_version={} artifact_version={} backend={} reason={} members={} maximum_member_horizon={} maximum_product_states={} transition_evaluation_bound={} equivalence_artifact_bytes={} unsat_proof_bytes={} safe={} unsafe={} reachable_product_states={} explored_transitions={} artifact_bytes={} assignments_checked={} load_micros={load_micros} artifact_micros={artifact_micros} verification_micros={verification_micros} elapsed_micros={}{}",
resources.version,
controller_plant_artifact::PROOF_MTBDD_PLANT_PORTFOLIO_VERSION,
match summary.backend {
controller_plant_artifact::ControllerProofMtbddPlantPortfolioBackend::ProofMtbdd => "PROOF_MTBDD",
controller_plant_artifact::ControllerProofMtbddPlantPortfolioBackend::DirectExact => "DIRECT_EXACT",
},
match summary.reason {
controller_plant_artifact::ControllerMtbddPlantSelectionReason::MtbddAdmitted => "MTBDD_ADMITTED",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::BoundaryLimit => "BOUNDARY_LIMIT",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::TerminalLimit => "TERMINAL_LIMIT",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::NodeLimit => "NODE_LIMIT",
},
resources.members,
resources.maximum_member_horizon,
resources.maximum_product_states,
resources.transition_evaluation_bound,
resources.equivalence_artifact_bytes,
resources.unsat_proof_bytes,
summary.safe,
summary.unsafe_count,
summary.reachable_product_states,
summary.explored_transitions,
resources.artifact_bytes,
summary.assignments_checked,
invocation_started.elapsed().as_micros(),
provenance.map_or_else(String::new, |summary| format!(
" source_model_attestation_version={} source_model_members={} source_model_tool={} source_model_tool_revision={} provenance=BOUND",
summary.version,
summary.member_count,
summary.tool,
summary.tool_revision,
)),
);
for (index, member) in summary.members.iter().enumerate() {
println!(
"controller-proof-mtbdd-portfolio-resource-member index={index} answer={} horizon={} bad_frame={} trace_steps={} reachable_product_states={} explored_transitions={}",
match member.answer {
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Safe => "SAFE",
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Unsafe => "UNSAFE",
},
member.horizon,
member.bad_frame.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
member.trace.len(),
member.reachable_product_states,
member.explored_transitions,
);
}
Ok(true)
}
"verify-controller-proof-mtbdd-plant-resources" => {
if args.len() != 4 {
return Err(
"usage: guarded-continuation-checker verify-controller-proof-mtbdd-plant-resources MANIFEST.txt POLICY.txt INPUT.proof-mtbdd-plant"
.to_string(),
);
}
let invocation_started = Instant::now();
let policy = parse_controller_proof_mtbdd_resource_policy(Path::new(&args[2]))?;
let (manifest, controller, controller_digest, plants, plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
let load_micros = invocation_started.elapsed().as_micros();
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let artifact_started = Instant::now();
let encoded = read_bounded_regular_file(
Path::new(&args[3]),
policy.envelope.composition().max_artifact_bytes(),
"proof-carrying controller MTBDD governed artifact",
)
.map_err(classify_controller_proof_mtbdd_resource_error)?;
let artifact_micros = artifact_started.elapsed().as_micros();
let verification_started = Instant::now();
let governed = controller_plant_artifact::verify_controller_proof_mtbdd_plant_artifact_with_resources(
&controller,
controller_digest,
&inputs,
&encoded,
policy.envelope,
)
.map_err(|error| classify_controller_proof_mtbdd_resource_error(error.to_string()))?;
let verification_micros = verification_started.elapsed().as_micros();
let resources = governed.resources;
let summary = governed.verification;
println!(
"controller-proof-mtbdd-resource status=VERIFIED cli_version={CONTROLLER_PROOF_MTBDD_RESOURCE_CLI_VERSION} policy_version={CONTROLLER_PROOF_MTBDD_RESOURCE_POLICY_VERSION} envelope_version={} artifact_version={} members={} maximum_member_horizon={} maximum_product_states={} transition_evaluation_bound={} equivalence_artifact_bytes={} unsat_proof_bytes={} safe={} unsafe={} reachable_product_states={} explored_transitions={} artifact_bytes={} assignments_checked={} load_micros={load_micros} artifact_micros={artifact_micros} verification_micros={verification_micros} elapsed_micros={}",
resources.version,
controller_plant_artifact::PROOF_MTBDD_PLANT_ARTIFACT_VERSION,
resources.members,
resources.maximum_member_horizon,
resources.maximum_product_states,
resources.transition_evaluation_bound,
resources.equivalence_artifact_bytes,
resources.unsat_proof_bytes,
summary.safe,
summary.unsafe_count,
summary.reachable_product_states,
summary.explored_transitions,
resources.artifact_bytes,
summary.assignments_checked,
invocation_started.elapsed().as_micros(),
);
for (index, member) in summary.members.iter().enumerate() {
println!(
"controller-proof-mtbdd-resource-member index={index} answer={} horizon={} bad_frame={} trace_steps={} reachable_product_states={} explored_transitions={}",
match member.answer {
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Safe => "SAFE",
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Unsafe => "UNSAFE",
},
member.horizon,
member.bad_frame.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
member.trace.len(),
member.reachable_product_states,
member.explored_transitions,
);
}
Ok(true)
}
"verify-controller-plant-portfolio-resources" => {
if args.len() != 4 {
return Err(
"usage: guarded-continuation-checker verify-controller-plant-portfolio-resources MANIFEST.txt POLICY.txt INPUT.controller-plant"
.to_string(),
);
}
let invocation_started = Instant::now();
let policy = parse_controller_plant_resource_policy(Path::new(&args[2]))?;
let (manifest, controller, controller_digest, plants, plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
guarded_continuation_checker::controller_plant::MAX_DIRECT_CONTROLLER_INPUTS,
guarded_continuation_checker::controller_plant::MAX_PLANT_INPUTS,
)?;
let load_micros = invocation_started.elapsed().as_micros();
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let artifact_started = Instant::now();
let encoded = read_bounded_regular_file(
Path::new(&args[3]),
policy.envelope.max_artifact_bytes(),
"controller plant governed portfolio",
)
.map_err(classify_controller_plant_resource_error)?;
let artifact_micros = artifact_started.elapsed().as_micros();
let verification_started = Instant::now();
let governed =
controller_plant_artifact::verify_controller_mtbdd_plant_portfolio_with_resources(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
&inputs,
&encoded,
policy.envelope,
)
.map_err(|error| classify_controller_plant_resource_error(error.to_string()))?;
let verification_micros = verification_started.elapsed().as_micros();
let resources = governed.resources;
let summary = governed.verification;
println!(
"controller-plant-resource status=VERIFIED cli_version={CONTROLLER_PLANT_RESOURCE_CLI_VERSION} policy_version={CONTROLLER_PLANT_RESOURCE_POLICY_VERSION} envelope_version={} artifact_version={} backend={} members={} maximum_member_horizon={} maximum_product_states={} transition_evaluation_bound={} safe={} unsafe={} reachable_product_states={} explored_transitions={} artifact_bytes={} load_micros={load_micros} artifact_micros={artifact_micros} verification_micros={verification_micros} elapsed_micros={}",
resources.version,
controller_plant_artifact::MTBDD_PLANT_PORTFOLIO_VERSION,
match resources.backend {
controller_plant_artifact::ControllerMtbddPlantPortfolioBackend::Mtbdd =>
"MTBDD",
controller_plant_artifact::ControllerMtbddPlantPortfolioBackend::DirectExact =>
"DIRECT_EXACT",
},
resources.members,
resources.maximum_member_horizon,
resources.maximum_product_states,
resources.transition_evaluation_bound,
summary.safe,
summary.unsafe_count,
summary.reachable_product_states,
summary.explored_transitions,
resources.artifact_bytes,
invocation_started.elapsed().as_micros(),
);
for (index, member) in summary.members.iter().enumerate() {
println!(
"controller-plant-resource-member index={index} answer={} horizon={} bad_frame={} trace_steps={} reachable_product_states={} explored_transitions={}",
match member.answer {
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Safe =>
"SAFE",
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Unsafe =>
"UNSAFE",
},
member.horizon,
member.bad_frame.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
member.trace.len(),
member.reachable_product_states,
member.explored_transitions,
);
}
Ok(true)
}
"certify-controller-plant-portfolio" | "verify-controller-plant-portfolio" => {
if args.len() != 3 {
return Err(format!(
"usage: guarded-continuation-checker {command} MANIFEST.txt {}",
if command == "certify-controller-plant-portfolio" {
"OUTPUT.controller-plant"
} else {
"INPUT.controller-plant"
}
));
}
let invocation_started = Instant::now();
let (manifest, controller, controller_digest, plants, plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
guarded_continuation_checker::controller_plant::MAX_DIRECT_CONTROLLER_INPUTS,
guarded_continuation_checker::controller_plant::MAX_PLANT_INPUTS,
)?;
let load_micros = invocation_started.elapsed().as_micros();
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let artifact_started = Instant::now();
let (encoded, action, write_micros) = if command == "certify-controller-plant-portfolio"
{
let encoded = controller_plant_artifact::produce_controller_mtbdd_plant_portfolio(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
&inputs,
)
.map_err(|error| error.to_string())?;
let write_started = Instant::now();
write_new_certificate(Path::new(&args[2]), &encoded)?;
(encoded, "CREATED", write_started.elapsed().as_micros())
} else {
(
read_bounded_regular_file(
Path::new(&args[2]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"controller plant portfolio",
)?,
"VERIFIED",
0,
)
};
let artifact_micros = artifact_started
.elapsed()
.as_micros()
.saturating_sub(write_micros);
let verification_started = Instant::now();
let summary = controller_plant_artifact::verify_controller_mtbdd_plant_portfolio(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
&inputs,
&encoded,
)
.map_err(|error| classify_controller_plant_resource_error(error.to_string()))?;
let verification_micros = verification_started.elapsed().as_micros();
println!(
"controller-plant-portfolio status={action} cli_version={CONTROLLER_PLANT_PORTFOLIO_CLI_VERSION} artifact_version={} backend={} reason={} members={} safe={} unsafe={} reachable_product_states={} explored_transitions={} artifact_bytes={} load_micros={load_micros} artifact_micros={artifact_micros} verification_micros={verification_micros} write_micros={write_micros} elapsed_micros={}{}",
controller_plant_artifact::MTBDD_PLANT_PORTFOLIO_VERSION,
match summary.backend {
controller_plant_artifact::ControllerMtbddPlantPortfolioBackend::Mtbdd =>
"MTBDD",
controller_plant_artifact::ControllerMtbddPlantPortfolioBackend::DirectExact =>
"DIRECT_EXACT",
},
match summary.reason {
controller_plant_artifact::ControllerMtbddPlantSelectionReason::MtbddAdmitted =>
"mtbdd-admitted",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::BoundaryLimit =>
"boundary-limit",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::TerminalLimit =>
"terminal-limit",
controller_plant_artifact::ControllerMtbddPlantSelectionReason::NodeLimit =>
"node-limit",
},
summary.members.len(),
summary.safe,
summary.unsafe_count,
summary.reachable_product_states,
summary.explored_transitions,
encoded.len(),
invocation_started.elapsed().as_micros(),
if action == "CREATED" {
format!(" output={}", args[2])
} else {
String::new()
}
);
for (index, member) in summary.members.iter().enumerate() {
println!(
"controller-plant-portfolio-member index={index} answer={} horizon={} bad_frame={} trace_steps={} reachable_product_states={} explored_transitions={}",
match member.answer {
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Safe =>
"SAFE",
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Unsafe =>
"UNSAFE",
},
member.horizon,
member.bad_frame.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
member.trace.len(),
member.reachable_product_states,
member.explored_transitions,
);
}
Ok(true)
}
"certify-controller-proof-mtbdd-plant-batch"
| "verify-controller-proof-mtbdd-plant-batch" => {
if args.len() != 3 {
return Err(format!(
"usage: guarded-continuation-checker {command} MANIFEST.txt {}",
if command == "certify-controller-proof-mtbdd-plant-batch" {
"OUTPUT.proof-mtbdd-plant"
} else {
"INPUT.proof-mtbdd-plant"
}
));
}
let (manifest, controller, controller_digest, plants, plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
let source_pairs = plants
.iter()
.zip(&plant_digests)
.map(|(plant, &digest)| (plant, digest))
.collect::<Vec<_>>();
let started = Instant::now();
let (encoded, action) = if command == "certify-controller-proof-mtbdd-plant-batch" {
let mtbdd = controller_mtbdd::produce_controller_mtbdd(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
)
.map_err(|error| error.to_string())?;
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let artifact =
controller_plant_artifact::produce_controller_proof_mtbdd_plant_artifact(
&controller,
controller_digest,
&mtbdd,
&inputs,
)
.map_err(|error| error.to_string())?;
let encoded =
controller_plant_artifact::encode_controller_proof_mtbdd_plant_artifact(
&artifact,
)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[2]), &encoded)?;
(encoded, "CREATED")
} else {
(
read_bounded_regular_file(
Path::new(&args[2]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"proof-carrying controller MTBDD plant artifact",
)?,
"VERIFIED",
)
};
let outer =
controller_plant_artifact::decode_controller_proof_mtbdd_plant_artifact(&encoded)
.map_err(|error| error.to_string())?;
let inner = controller_plant_artifact::decode_controller_mtbdd_plant_artifact(
&outer.controller_mtbdd_plant,
)
.map_err(|error| error.to_string())?;
let decoded_mtbdd = controller_mtbdd::decode_controller_mtbdd(&inner.controller_mtbdd)
.map_err(|error| error.to_string())?;
if decoded_mtbdd.relevant_inputs != manifest.relevant_inputs
|| decoded_mtbdd.observed_outputs != manifest.observed_outputs
|| inner.members.len() != manifest.members.len()
{
return Err(
"proof-carrying controller MTBDD plant artifact does not match manifest boundary"
.to_string(),
);
}
for ((claimed, expected), expected_digest) in inner
.members
.iter()
.zip(&manifest.members)
.zip(&plant_digests)
{
if claimed.plant_source_sha256 != *expected_digest
|| claimed.wiring != expected.wiring
|| claimed.initial_controller_state != expected.initial_controller_state
|| claimed.initial_plant_state != expected.initial_plant_state
|| claimed.bad_plant_output != expected.bad_plant_output
|| claimed.horizon != expected.horizon
{
return Err(
"proof-carrying controller MTBDD plant artifact does not match manifest member"
.to_string(),
);
}
}
let summary = controller_plant_artifact::verify_controller_proof_mtbdd_plant_artifact(
&controller,
controller_digest,
&source_pairs,
&encoded,
)
.map_err(|error| error.to_string())?;
println!(
"controller-proof-mtbdd-plant-batch status={action} cli_version={CONTROLLER_PROOF_MTBDD_CLI_VERSION} artifact_version={} members={} safe={} unsafe={} mtbdd_nodes={} mtbdd_terminals={} assignments_checked={} reachable_product_states={} explored_transitions={} artifact_bytes={} elapsed_micros={}{}",
controller_plant_artifact::PROOF_MTBDD_PLANT_ARTIFACT_VERSION,
summary.members.len(),
summary.safe,
summary.unsafe_count,
summary.mtbdd_nodes,
summary.mtbdd_terminals,
summary.assignments_checked,
summary.reachable_product_states,
summary.explored_transitions,
encoded.len(),
started.elapsed().as_micros(),
if action == "CREATED" {
format!(" output={}", args[2])
} else {
String::new()
}
);
for (index, member) in summary.members.iter().enumerate() {
println!(
"controller-proof-mtbdd-plant-member index={index} answer={} horizon={} bad_frame={} trace_steps={} reachable_product_states={} explored_transitions={}",
match member.answer {
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Safe =>
"SAFE",
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Unsafe =>
"UNSAFE",
},
member.horizon,
member
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
member.trace.len(),
member.reachable_product_states,
member.explored_transitions,
);
}
Ok(true)
}
"certify-controller-mtbdd-plant-batch" | "verify-controller-mtbdd-plant-batch" => {
if args.len() != 3 {
return Err(format!(
"usage: guarded-continuation-checker {command} MANIFEST.txt {}",
if command == "certify-controller-mtbdd-plant-batch" {
"OUTPUT.mtbdd-plant"
} else {
"INPUT.mtbdd-plant"
}
));
}
let (manifest, controller, controller_digest, plants, plant_digests, _snapshot) =
load_controller_plant_manifest(
Path::new(&args[1]),
controller_mtbdd::MAX_MTBDD_INPUTS,
controller_mtbdd::MAX_MTBDD_OUTPUTS,
)?;
let source_pairs = plants
.iter()
.zip(&plant_digests)
.map(|(plant, &digest)| (plant, digest))
.collect::<Vec<_>>();
let started = Instant::now();
let (encoded, action) = if command == "certify-controller-mtbdd-plant-batch" {
let mtbdd = controller_mtbdd::produce_controller_mtbdd(
&controller,
controller_digest,
&manifest.relevant_inputs,
&manifest.observed_outputs,
)
.map_err(|error| error.to_string())?;
let inputs = manifest
.members
.iter()
.enumerate()
.map(|(index, member)| ControllerPlantArtifactInput {
plant: &plants[index],
plant_source_sha256: plant_digests[index],
wiring: &member.wiring,
initial_controller_state: member.initial_controller_state,
initial_plant_state: member.initial_plant_state,
bad_plant_output: member.bad_plant_output,
horizon: member.horizon,
})
.collect::<Vec<_>>();
let artifact = controller_plant_artifact::produce_controller_mtbdd_plant_artifact(
&controller,
controller_digest,
&mtbdd,
&inputs,
)
.map_err(|error| error.to_string())?;
let encoded =
controller_plant_artifact::encode_controller_mtbdd_plant_artifact(&artifact)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[2]), &encoded)?;
(encoded, "CREATED")
} else {
(
read_bounded_regular_file(
Path::new(&args[2]),
controller_plant_artifact::MAX_CONTROLLER_PLANT_ARTIFACT_BYTES,
"controller MTBDD plant artifact",
)?,
"VERIFIED",
)
};
let decoded =
controller_plant_artifact::decode_controller_mtbdd_plant_artifact(&encoded)
.map_err(|error| error.to_string())?;
let decoded_mtbdd =
controller_mtbdd::decode_controller_mtbdd(&decoded.controller_mtbdd)
.map_err(|error| error.to_string())?;
if decoded_mtbdd.relevant_inputs != manifest.relevant_inputs
|| decoded_mtbdd.observed_outputs != manifest.observed_outputs
|| decoded.members.len() != manifest.members.len()
{
return Err(
"controller MTBDD plant artifact does not match manifest boundary".to_string(),
);
}
for ((claimed, expected), expected_digest) in decoded
.members
.iter()
.zip(&manifest.members)
.zip(&plant_digests)
{
if claimed.plant_source_sha256 != *expected_digest
|| claimed.wiring != expected.wiring
|| claimed.initial_controller_state != expected.initial_controller_state
|| claimed.initial_plant_state != expected.initial_plant_state
|| claimed.bad_plant_output != expected.bad_plant_output
|| claimed.horizon != expected.horizon
{
return Err(
"controller MTBDD plant artifact does not match manifest member"
.to_string(),
);
}
}
let summary = controller_plant_artifact::verify_controller_mtbdd_plant_artifact(
&controller,
controller_digest,
&source_pairs,
&encoded,
)
.map_err(|error| error.to_string())?;
println!(
"controller-mtbdd-plant-batch status={action} cli_version={CONTROLLER_MTBDD_CLI_VERSION} artifact_version={} members={} safe={} unsafe={} mtbdd_nodes={} mtbdd_terminals={} assignments_checked={} reachable_product_states={} explored_transitions={} artifact_bytes={} elapsed_micros={}{}",
controller_plant_artifact::MTBDD_PLANT_ARTIFACT_VERSION,
summary.members.len(),
summary.safe,
summary.unsafe_count,
summary.mtbdd_nodes,
summary.mtbdd_terminals,
summary.assignments_checked,
summary.reachable_product_states,
summary.explored_transitions,
encoded.len(),
started.elapsed().as_micros(),
if action == "CREATED" {
format!(" output={}", args[2])
} else {
String::new()
}
);
for (index, member) in summary.members.iter().enumerate() {
println!(
"controller-mtbdd-plant-member index={index} answer={} horizon={} bad_frame={} trace_steps={} reachable_product_states={} explored_transitions={}",
match member.answer {
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Safe =>
"SAFE",
guarded_continuation_checker::controller_plant::ControllerPlantAnswer::Unsafe =>
"UNSAFE",
},
member.horizon,
member
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string()),
member.trace.len(),
member.reachable_product_states,
member.explored_transitions,
);
}
Ok(true)
}
"btor2-cli-version" => {
if args.len() != 1 {
return Err("usage: guarded-continuation-checker btor2-cli-version".to_string());
}
println!(
"btor2_cli_version={} phase_certificate_version={} replay_certificate_version={} search_certificate_version={} region_certificate_version={} motion_certificate_version={} braking_certificate_version={} component_contract_version={} component_certificate_version={} controller_obligation_version={} reusable_component_batch_version={} component_batch_portfolio_version={} component_batch_manifest_version={} bounded_portfolio_version={} max_bytes={} max_lines={} max_nodes={} max_bit_width={} max_phase_certificate_bytes={} max_phases={} max_phase_horizon={} max_replay_horizon={} max_replay_node_steps={} max_search_horizon={} max_search_states_per_layer={} max_search_total_states={} max_search_node_steps={} max_search_certificate_bytes={} max_region_horizon={} max_region_certificate_bytes={} max_motion_horizon={} max_motion_certificate_bytes={} max_braking_horizon={} max_braking_certificate_bytes={} max_component_contract_bytes={} max_controller_obligation_bytes={} max_component_phase_horizon={} max_component_search_horizon={} max_component_certificate_bytes={} max_component_batch_members={} max_reusable_component_batch_bytes={} max_component_batch_portfolio_bytes={} max_component_batch_manifest_bytes={} arrays=unsupported liveness=unsupported unsupported=fail-closed",
btor2::BTOR2_CORE_VERSION,
btor2_phase::PHASE_CERTIFICATE_VERSION,
btor2_phase::REPLAY_CERTIFICATE_VERSION,
btor2_search::SEARCH_CERTIFICATE_VERSION,
btor2_region::REGION_CERTIFICATE_VERSION,
btor2_motion::MOTION_CERTIFICATE_VERSION,
btor2_braking::BRAKING_CERTIFICATE_VERSION,
btor2_component::COMPONENT_CONTRACT_VERSION,
btor2_component::COMPONENT_CERTIFICATE_VERSION,
btor2_component::CONTROLLER_OBLIGATION_VERSION,
btor2_component::REUSABLE_COMPONENT_BATCH_VERSION,
btor2_component::COMPONENT_BATCH_PORTFOLIO_VERSION,
BTOR2_COMPONENT_BATCH_MANIFEST_VERSION,
btor2_bounded::BOUNDED_PORTFOLIO_VERSION,
btor2::MAX_BTOR2_BYTES,
btor2::MAX_BTOR2_LINES,
btor2::MAX_BTOR2_NODES,
btor2::MAX_BIT_WIDTH,
btor2_phase::MAX_CERTIFICATE_BYTES,
btor2_phase::MAX_PHASES,
btor2_phase::MAX_HORIZON,
btor2_phase::MAX_REPLAY_HORIZON,
btor2_phase::MAX_REPLAY_NODE_STEPS,
btor2_search::MAX_SEARCH_HORIZON,
btor2_search::MAX_STATES_PER_LAYER,
btor2_search::MAX_TOTAL_STATES,
btor2_search::MAX_SEARCH_NODE_STEPS,
btor2_search::MAX_SEARCH_CERTIFICATE_BYTES,
btor2_region::MAX_REGION_HORIZON,
btor2_region::MAX_REGION_CERTIFICATE_BYTES,
btor2_motion::MAX_MOTION_HORIZON,
btor2_motion::MAX_MOTION_CERTIFICATE_BYTES,
btor2_braking::MAX_BRAKING_HORIZON,
btor2_braking::MAX_BRAKING_CERTIFICATE_BYTES,
btor2_component::MAX_COMPONENT_CONTRACT_BYTES,
btor2_component::MAX_CONTROLLER_OBLIGATION_BYTES,
btor2_component::MAX_COMPONENT_PHASE_HORIZON,
btor2_component::MAX_COMPONENT_SEARCH_HORIZON,
btor2_component::MAX_COMPONENT_CERTIFICATE_BYTES,
btor2_component::MAX_COMPONENT_BATCH_MEMBERS,
btor2_component::MAX_REUSABLE_COMPONENT_BATCH_BYTES,
btor2_component::MAX_COMPONENT_BATCH_PORTFOLIO_BYTES,
BTOR2_COMPONENT_BATCH_MANIFEST_MAX_BYTES,
);
Ok(true)
}
"btor2-search-v5-capabilities" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-search-v5-capabilities".to_string(),
);
}
println!(
"btor2_search_capability_version=1 search_certificate_version={} min_inputs=1 max_inputs={} max_input_width={} max_total_input_bits={} max_horizon={} max_states_per_layer={} max_total_states={} max_node_steps={} max_certificate_bytes={} constraints=exact-all-frame-ordered-or-none valuation_order=input-node-ascending-then-lsb-first terminal_valuation=distinct dead_end_layers=empty-with-constraints unsafe=admissible-trace safe=complete-admissible-layers work_accounting=all-valuations resource_refusal=no-answer unsupported=fail-closed",
btor2_search::SEARCH_CERTIFICATE_VERSION,
btor2_search::MAX_SEARCH_INPUTS,
btor2_search::MAX_SEARCH_INPUT_BITS,
btor2_search::MAX_SEARCH_INPUT_BITS,
btor2_search::MAX_SEARCH_HORIZON,
btor2_search::MAX_STATES_PER_LAYER,
btor2_search::MAX_TOTAL_STATES,
btor2_search::MAX_SEARCH_NODE_STEPS,
btor2_search::MAX_SEARCH_CERTIFICATE_BYTES,
);
Ok(true)
}
"btor2-search-v3-capabilities" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-search-v3-capabilities".to_string(),
);
}
println!(
"btor2_search_capability_version=1 search_certificate_version={} min_inputs=1 max_inputs={} input_width=1 max_horizon={} max_states_per_layer={} max_total_states={} max_node_steps={} max_certificate_bytes={} constraints=unsupported valuation_order=input-node-ascending valuation_bit=i-maps-input-i terminal_valuation=distinct unsafe=trace safe=complete-layers resource_refusal=no-answer unsupported=fail-closed",
btor2_search::SEARCH_CERTIFICATE_V3_VERSION,
btor2_search::MAX_SEARCH_INPUTS,
btor2_search::MAX_SEARCH_HORIZON,
btor2_search::MAX_STATES_PER_LAYER,
btor2_search::MAX_TOTAL_STATES,
btor2_search::MAX_SEARCH_NODE_STEPS,
btor2_search::MAX_SEARCH_CERTIFICATE_BYTES,
);
Ok(true)
}
"btor2-search-v4-capabilities" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-search-v4-capabilities".to_string(),
);
}
println!(
"btor2_search_capability_version=1 search_certificate_version={} min_inputs=1 max_inputs={} input_width=1 min_constraints=1 max_horizon={} max_states_per_layer={} max_total_states={} max_node_steps={} max_certificate_bytes={} constraints=exact-all-frame-ordered valuation_order=input-node-ascending valuation_bit=i-maps-input-i terminal_valuation=distinct dead_end_layers=empty unsafe=admissible-trace safe=complete-admissible-layers work_accounting=all-valuations resource_refusal=no-answer unsupported=fail-closed",
btor2_search::SEARCH_CERTIFICATE_V4_VERSION,
btor2_search::MAX_SEARCH_INPUTS,
btor2_search::MAX_SEARCH_HORIZON,
btor2_search::MAX_STATES_PER_LAYER,
btor2_search::MAX_TOTAL_STATES,
btor2_search::MAX_SEARCH_NODE_STEPS,
btor2_search::MAX_SEARCH_CERTIFICATE_BYTES,
);
Ok(true)
}
"certify-btor2-controller-obligation" => {
if args.len() != 4 {
return Err("usage: guarded-continuation-checker certify-btor2-controller-obligation CONTROLLER.btor2 CONTRACT.txt OUTPUT.controller-obligation".to_string());
}
let controller = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"controller BTOR2 input",
)?;
let contract = read_bounded_regular_file(
Path::new(&args[2]),
btor2_component::MAX_COMPONENT_CONTRACT_BYTES,
"component contract",
)?;
let started = Instant::now();
let obligation = btor2_component::produce_controller_obligation(&controller, &contract)
.map_err(|error| error.to_string())?;
let encoded = btor2_component::encode_controller_obligation(&obligation)
.map_err(|error| error.to_string())?;
btor2_component::verify_controller_obligation(&controller, &obligation)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[3]), encoded.as_bytes())?;
println!(
"btor2-controller-obligation status=CREATED obligation_version={} velocity_width={} brake_velocity={} obligation_bytes={} elapsed_micros={} output={}",
btor2_component::CONTROLLER_OBLIGATION_VERSION,
obligation.velocity_width,
obligation.brake_velocity,
encoded.len(),
started.elapsed().as_micros(),
args[3]
);
Ok(true)
}
"verify-btor2-controller-obligation" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-btor2-controller-obligation CONTROLLER.btor2 INPUT.controller-obligation".to_string());
}
let controller = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"controller BTOR2 input",
)?;
let encoded = read_bounded_regular_file(
Path::new(&args[2]),
btor2_component::MAX_CONTROLLER_OBLIGATION_BYTES,
"controller obligation",
)?;
let obligation = btor2_component::decode_controller_obligation(&encoded)
.map_err(|error| error.to_string())?;
let started = Instant::now();
btor2_component::verify_controller_obligation(&controller, &obligation)
.map_err(|error| error.to_string())?;
println!(
"btor2-controller-obligation status=VERIFIED obligation_version={} velocity_width={} brake_velocity={} obligation_bytes={} elapsed_micros={}",
btor2_component::CONTROLLER_OBLIGATION_VERSION,
obligation.velocity_width,
obligation.brake_velocity,
encoded.len(),
started.elapsed().as_micros()
);
Ok(true)
}
"check-btor2-revision-portfolio" | "verify-btor2-revision-portfolio" => {
if args.len() != 10 {
return Err(format!(
"usage: guarded-continuation-checker {command} LEFT.btor2 LEFT_OUTPUTS RIGHT.btor2 RIGHT_OUTPUTS INTERFACE.txt HORIZON BAD_SIDE BAD_OUTPUT {}",
if command == "check-btor2-revision-portfolio" {
"OUTPUT.revision-proof"
} else {
"INPUT.revision-proof"
}
));
}
let left = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"left BTOR2 input",
)?;
let left_outputs = parse_revision_output_nodes(&args[2], "LEFT_OUTPUTS")?;
let right = read_bounded_regular_file(
Path::new(&args[3]),
btor2::MAX_BTOR2_BYTES,
"right BTOR2 input",
)?;
let right_outputs = parse_revision_output_nodes(&args[4], "RIGHT_OUTPUTS")?;
let interface = read_bounded_regular_file(
Path::new(&args[5]),
revision_local::MAX_WORD_INTERFACE_CONTRACT_BYTES,
"word interface contract",
)?;
let horizon = args[6]
.parse::<u32>()
.map_err(|_| "HORIZON must be an unsigned integer".to_string())?;
let bad_side = match args[7].as_str() {
"left" => revision_local::ComponentSide::Left,
"right" => revision_local::ComponentSide::Right,
_ => return Err("BAD_SIDE must be left or right".to_string()),
};
let bad_output = args[8]
.parse::<btor2::NodeId>()
.ok()
.filter(|node| *node != 0)
.ok_or_else(|| "BAD_OUTPUT must be a nonzero node identifier".to_string())?;
let query = revision_local::BoundedQuery {
horizon,
bad_side,
bad_output,
};
let started = Instant::now();
let (production, encoded) = if command == "check-btor2-revision-portfolio" {
let production = revision_local::produce_revision_portfolio(
&left,
&left_outputs,
&right,
&right_outputs,
&interface,
&query,
)
.map_err(|error| error.to_string())?;
let encoded = revision_local::encode_revision_portfolio(&production)
.map_err(|error| error.to_string())?;
(production, encoded)
} else {
let encoded = read_bounded_regular_file(
Path::new(&args[9]),
revision_local::MAX_REVISION_PORTFOLIO_BYTES,
"revision proof portfolio",
)?;
let production = revision_local::decode_revision_portfolio(&encoded)
.map_err(|error| error.to_string())?;
(production, encoded)
};
let embedded_query = match &production.certificate {
revision_local::RevisionPortfolioCertificate::RevisionLocal(certificate) => {
revision_local::decode_bounded_answer_certificate(&certificate.final_evidence)
.map_err(|error| error.to_string())?
.query
}
revision_local::RevisionPortfolioCertificate::DirectExact(certificate) => {
certificate.query.clone()
}
};
if embedded_query != query {
return Err("revision proof query does not match CLI query".to_string());
}
let summary = revision_local::verify_revision_portfolio(
&left,
&left_outputs,
&right,
&right_outputs,
&interface,
&production,
)
.map_err(|error| error.to_string())?;
if command == "check-btor2-revision-portfolio" {
write_new_certificate(Path::new(&args[9]), &encoded)?;
}
let status = if command == "check-btor2-revision-portfolio" {
"CREATED"
} else {
"VERIFIED"
};
let backend = match summary.backend {
revision_local::RevisionPortfolioBackend::RevisionLocal => "revision-local",
revision_local::RevisionPortfolioBackend::DirectExact => "direct-exact",
};
let result = match summary.result {
revision_local::BoundedResult::Safe => "SAFE",
revision_local::BoundedResult::Unsafe => "UNSAFE",
};
let bad_frame = summary
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-revision-portfolio status={status} portfolio_version={} backend={backend} reason={} result={result} horizon={horizon} bad_frame={bad_frame} certificate_bytes={} elapsed_micros={}{}",
revision_local::REVISION_PORTFOLIO_CERTIFICATE_VERSION,
summary.reason.as_str(),
encoded.len(),
started.elapsed().as_micros(),
if command == "check-btor2-revision-portfolio" {
format!(" output={}", args[9])
} else {
String::new()
}
);
Ok(true)
}
"check-btor2-revision-impact" | "verify-btor2-revision-impact" => {
if args.len() != 11 {
return Err(format!(
"usage: guarded-continuation-checker {command} LEFT_OLD.btor2 LEFT_NEW.btor2 LEFT_OUTPUTS RIGHT_OLD.btor2 RIGHT_NEW.btor2 RIGHT_OUTPUTS INTERFACE_OLD.txt INTERFACE_NEW.txt QUERIES.txt {}",
if command == "check-btor2-revision-impact" {
"OUTPUT.revision-impact"
} else {
"INPUT.revision-impact"
}
));
}
let left_old = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"old left BTOR2 input",
)?;
let left_new = read_bounded_regular_file(
Path::new(&args[2]),
btor2::MAX_BTOR2_BYTES,
"new left BTOR2 input",
)?;
let left_outputs = parse_revision_output_nodes(&args[3], "LEFT_OUTPUTS")?;
let right_old = read_bounded_regular_file(
Path::new(&args[4]),
btor2::MAX_BTOR2_BYTES,
"old right BTOR2 input",
)?;
let right_new = read_bounded_regular_file(
Path::new(&args[5]),
btor2::MAX_BTOR2_BYTES,
"new right BTOR2 input",
)?;
let right_outputs = parse_revision_output_nodes(&args[6], "RIGHT_OUTPUTS")?;
let interface_old = read_bounded_regular_file(
Path::new(&args[7]),
revision_local::MAX_WORD_INTERFACE_CONTRACT_BYTES,
"old word interface contract",
)?;
let interface_new = read_bounded_regular_file(
Path::new(&args[8]),
revision_local::MAX_WORD_INTERFACE_CONTRACT_BYTES,
"new word interface contract",
)?;
let queries = parse_revision_impact_queries(Path::new(&args[9]))?;
let input = revision_impact::TwoComponentRevisionImpactInput {
left_old: &left_old,
left_new: &left_new,
left_outputs: &left_outputs,
right_old: &right_old,
right_new: &right_new,
right_outputs: &right_outputs,
interface_old: &interface_old,
interface_new: &interface_new,
queries: &queries,
};
let policy = revision_impact::RevisionImpactPolicy::default();
let started = Instant::now();
let (bundle, encoded) = if command == "check-btor2-revision-impact" {
let bundle = revision_impact::produce_two_component_revision_impact_with_policy(
&input, policy,
)
.map_err(|error| error.to_string())?;
let encoded =
revision_impact::encode_two_component_revision_impact_bundle(&bundle, policy)
.map_err(|error| error.to_string())?;
(bundle, encoded)
} else {
let encoded = read_bounded_regular_file(
Path::new(&args[10]),
policy.max_bundle_bytes,
"revision impact bundle",
)?;
let bundle =
revision_impact::decode_two_component_revision_impact_bundle(&encoded, policy)
.map_err(|error| error.to_string())?;
(bundle, encoded)
};
let (summary, work) =
revision_impact::verify_two_component_revision_impact_observed_with_policy(
&input, &bundle, policy,
)
.map_err(|error| error.to_string())?;
let semantic_sets =
revision_impact::derive_minimal_semantic_change_sets(&bundle.impact)
.map_err(|error| error.to_string())?;
if semantic_sets.len() != summary.minimal_semantic_change_sets {
return Err("revision impact semantic-change count mismatch".to_string());
}
if command == "check-btor2-revision-impact" {
write_new_certificate(Path::new(&args[10]), &encoded)?;
}
let status = if command == "check-btor2-revision-impact" {
"CREATED"
} else {
"VERIFIED"
};
println!(
"btor2-revision-impact status={status} impact_version={} atoms={} queries={} combinations={} reusable_observations={} invalidated_observations={} minimal_invalidating_sets={} minimal_semantic_change_sets={} evidence_members={} certificate_bytes={} parsed_evidence_bytes={} semantic_replays={} component_validations={} composed_pair_checks={} final_transition_checks={} result_comparisons={} elapsed_micros={}",
revision_impact::REVISION_IMPACT_CERTIFICATE_VERSION,
summary.atoms,
summary.queries,
summary.combinations,
summary.reusable_observations,
summary.invalidated_observations,
summary.minimal_invalidating_sets,
summary.minimal_semantic_change_sets,
bundle.revision_evidence.len(),
encoded.len(),
work.parsed_evidence_bytes,
work.semantic_replays,
work.component_validations,
work.composed_pair_checks,
work.final_transition_checks,
work.result_comparisons,
started.elapsed().as_micros(),
);
let new_mask = summary.combinations - 1;
let result = |value| match value {
revision_local::BoundedResult::Safe => "SAFE",
revision_local::BoundedResult::Unsafe => "UNSAFE",
};
for (index, query) in queries.iter().enumerate() {
let old = bundle.impact.observations[index];
let new = bundle.impact.observations[new_mask * summary.queries + index];
let side = match query.bad_side {
revision_local::ComponentSide::Left => "left",
revision_local::ComponentSide::Right => "right",
};
println!(
"btor2-revision-impact-query index={index} horizon={} bad_side={side} bad_output={} old_result={} new_result={}",
query.horizon,
query.bad_output,
result(old.result),
result(new.result),
);
}
for set in semantic_sets {
println!(
"btor2-revision-impact-semantic-set query_index={} changed_mask={} baseline_result={} changed_result={}",
set.query_index,
set.changed_mask,
result(set.baseline_result),
result(set.changed_result),
);
}
Ok(true)
}
"check-btor2-revision-retained-left" => {
if args.len() != 10 {
return Err("usage: guarded-continuation-checker check-btor2-revision-retained-left LEFT.btor2 PREVIOUS.revision-proof RIGHT.btor2 RIGHT_OUTPUTS INTERFACE.txt HORIZON BAD_SIDE BAD_OUTPUT OUTPUT.revision-proof".to_string());
}
let left = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"retained left BTOR2 input",
)?;
let previous_bytes = read_bounded_regular_file(
Path::new(&args[2]),
revision_local::MAX_REVISION_PORTFOLIO_BYTES,
"previous revision proof portfolio",
)?;
let previous = revision_local::decode_revision_portfolio(&previous_bytes)
.map_err(|error| error.to_string())?;
let previous_local = match &previous.certificate {
revision_local::RevisionPortfolioCertificate::RevisionLocal(certificate) => {
certificate
}
revision_local::RevisionPortfolioCertificate::DirectExact(_) => {
return Err(
"previous revision proof does not contain reusable local evidence"
.to_string(),
);
}
};
let retained = revision_local::validate_local_artifact(
&left,
&previous_local.left.evidence,
revision_local::EvidenceSection::Left,
)
.map_err(|error| error.to_string())?;
let right = read_bounded_regular_file(
Path::new(&args[3]),
btor2::MAX_BTOR2_BYTES,
"changed right BTOR2 input",
)?;
let right_outputs = parse_revision_output_nodes(&args[4], "RIGHT_OUTPUTS")?;
let interface = read_bounded_regular_file(
Path::new(&args[5]),
revision_local::MAX_WORD_INTERFACE_CONTRACT_BYTES,
"word interface contract",
)?;
let horizon = args[6]
.parse::<u32>()
.map_err(|_| "HORIZON must be an unsigned integer".to_string())?;
let bad_side = match args[7].as_str() {
"left" => revision_local::ComponentSide::Left,
"right" => revision_local::ComponentSide::Right,
_ => return Err("BAD_SIDE must be left or right".to_string()),
};
let bad_output = args[8]
.parse::<btor2::NodeId>()
.ok()
.filter(|node| *node != 0)
.ok_or_else(|| "BAD_OUTPUT must be a nonzero node identifier".to_string())?;
let query = revision_local::BoundedQuery {
horizon,
bad_side,
bad_output,
};
let started = Instant::now();
let (certificate, produced, production_work) =
revision_local::produce_revision_with_retained_left(
&retained,
&right,
&right_outputs,
&interface,
&query,
)
.map_err(|error| error.to_string())?;
let production = revision_local::RevisionPortfolioProduction {
certificate: revision_local::RevisionPortfolioCertificate::RevisionLocal(
certificate,
),
backend: revision_local::RevisionPortfolioBackend::RevisionLocal,
reason: revision_local::RevisionSelectionReason::ExactLocalRelationAdmitted,
};
let encoded = revision_local::encode_revision_portfolio(&production)
.map_err(|error| error.to_string())?;
let certificate = match &production.certificate {
revision_local::RevisionPortfolioCertificate::RevisionLocal(certificate) => {
certificate
}
revision_local::RevisionPortfolioCertificate::DirectExact(_) => unreachable!(),
};
let (_, verification_work) = revision_local::verify_revision_with_retained_left(
&retained,
&right,
&interface,
certificate,
)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[9]), &encoded)?;
let result = match produced.answer.result {
revision_local::BoundedResult::Safe => "SAFE",
revision_local::BoundedResult::Unsafe => "UNSAFE",
};
let bad_frame = produced
.answer
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-revision-retained-left status=CREATED portfolio_version={} backend=revision-local reason=exact-local-relation-admitted result={result} horizon={horizon} bad_frame={bad_frame} produced_local_sections={} production_reused_local_sections={} changed_candidate_valuations={} verified_local_sections={} verification_reused_local_sections={} composed_pair_checks={} final_transition_checks={} certificate_bytes={} elapsed_micros={} output={}",
revision_local::REVISION_PORTFOLIO_CERTIFICATE_VERSION,
production_work.produced_local_sections,
production_work.reused_local_sections,
production_work.changed_candidate_valuations,
verification_work.semantically_verified_local_sections,
verification_work.reused_local_sections,
production_work.composed_pair_checks,
production_work.final_transition_checks,
encoded.len(),
started.elapsed().as_micros(),
args[9],
);
Ok(true)
}
"verify-btor2-revision-retained-left" => {
if args.len() != 9 {
return Err("usage: guarded-continuation-checker verify-btor2-revision-retained-left LEFT.btor2 PREVIOUS.revision-proof RIGHT.btor2 INTERFACE.txt HORIZON BAD_SIDE BAD_OUTPUT INPUT.revision-proof".to_string());
}
let left = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"retained left BTOR2 input",
)?;
let previous_bytes = read_bounded_regular_file(
Path::new(&args[2]),
revision_local::MAX_REVISION_PORTFOLIO_BYTES,
"previous revision proof portfolio",
)?;
let previous = revision_local::decode_revision_portfolio(&previous_bytes)
.map_err(|error| error.to_string())?;
let previous_local = match &previous.certificate {
revision_local::RevisionPortfolioCertificate::RevisionLocal(certificate) => {
certificate
}
revision_local::RevisionPortfolioCertificate::DirectExact(_) => {
return Err(
"previous revision proof does not contain reusable local evidence"
.to_string(),
);
}
};
let retained = revision_local::validate_local_artifact(
&left,
&previous_local.left.evidence,
revision_local::EvidenceSection::Left,
)
.map_err(|error| error.to_string())?;
let right = read_bounded_regular_file(
Path::new(&args[3]),
btor2::MAX_BTOR2_BYTES,
"changed right BTOR2 input",
)?;
let interface = read_bounded_regular_file(
Path::new(&args[4]),
revision_local::MAX_WORD_INTERFACE_CONTRACT_BYTES,
"word interface contract",
)?;
let horizon = args[5]
.parse::<u32>()
.map_err(|_| "HORIZON must be an unsigned integer".to_string())?;
let bad_side = match args[6].as_str() {
"left" => revision_local::ComponentSide::Left,
"right" => revision_local::ComponentSide::Right,
_ => return Err("BAD_SIDE must be left or right".to_string()),
};
let bad_output = args[7]
.parse::<btor2::NodeId>()
.ok()
.filter(|node| *node != 0)
.ok_or_else(|| "BAD_OUTPUT must be a nonzero node identifier".to_string())?;
let query = revision_local::BoundedQuery {
horizon,
bad_side,
bad_output,
};
let encoded = read_bounded_regular_file(
Path::new(&args[8]),
revision_local::MAX_REVISION_PORTFOLIO_BYTES,
"revision proof portfolio",
)?;
let production = revision_local::decode_revision_portfolio(&encoded)
.map_err(|error| error.to_string())?;
let certificate = match &production.certificate {
revision_local::RevisionPortfolioCertificate::RevisionLocal(certificate) => {
certificate
}
revision_local::RevisionPortfolioCertificate::DirectExact(_) => {
return Err(
"revision proof does not contain reusable local evidence".to_string()
);
}
};
let embedded_query =
revision_local::decode_bounded_answer_certificate(&certificate.final_evidence)
.map_err(|error| error.to_string())?
.query;
if embedded_query != query {
return Err("revision proof query does not match CLI query".to_string());
}
let started = Instant::now();
let (summary, work) = revision_local::verify_revision_with_retained_left(
&retained,
&right,
&interface,
certificate,
)
.map_err(|error| error.to_string())?;
let result = match summary.answer.result {
revision_local::BoundedResult::Safe => "SAFE",
revision_local::BoundedResult::Unsafe => "UNSAFE",
};
let bad_frame = summary
.answer
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-revision-retained-left status=VERIFIED portfolio_version={} backend=revision-local reason=exact-local-relation-admitted result={result} horizon={horizon} bad_frame={bad_frame} verified_local_sections={} reused_local_sections={} composed_pair_checks={} final_transition_checks={} certificate_bytes={} elapsed_micros={}",
revision_local::REVISION_PORTFOLIO_CERTIFICATE_VERSION,
work.semantically_verified_local_sections,
work.reused_local_sections,
work.composed_pair_checks,
work.final_transition_checks,
encoded.len(),
started.elapsed().as_micros(),
);
Ok(true)
}
"check-btor2-components" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker check-btor2-components CONTROLLER.btor2 PLANT.btor2 CONTRACT.txt HORIZON OUTPUT.component-cert".to_string());
}
let controller = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"controller BTOR2 input",
)?;
let plant = read_bounded_regular_file(
Path::new(&args[2]),
btor2::MAX_BTOR2_BYTES,
"plant BTOR2 input",
)?;
let contract = read_bounded_regular_file(
Path::new(&args[3]),
btor2_component::MAX_COMPONENT_CONTRACT_BYTES,
"component contract",
)?;
let horizon = args[4]
.parse::<u32>()
.map_err(|_| "HORIZON must be an unsigned integer".to_string())?;
let started = Instant::now();
let production = btor2_component::produce(&controller, &plant, &contract, horizon)
.map_err(|error| error.to_string())?;
let reason = production.selection_reason;
let encoded = btor2_component::encode(&production.certificate)
.map_err(|error| error.to_string())?;
let summary =
btor2_component::verify(&controller, &plant, &contract, &production.certificate)
.map_err(|error| error.to_string())?;
write_new_certificate(Path::new(&args[5]), encoded.as_bytes())?;
let backend = match summary.backend {
btor2_component::ComponentBackend::PhaseContract => "phase-contract",
btor2_component::ComponentBackend::ComposedSearch => "composed-search",
};
let result = match summary.result {
btor2_component::ComponentResult::Safe => "SAFE",
btor2_component::ComponentResult::Unsafe => "UNSAFE",
};
let bad_frame = summary
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-components status=CREATED certificate_version={} backend={backend} reason={} result={result} horizon={} bad_frame={bad_frame} logical_reachable_states={} certificate_bytes={} elapsed_micros={} output={}",
btor2_component::COMPONENT_CERTIFICATE_VERSION,
reason.as_str(),
summary.query_horizon,
summary.logical_reachable_states,
encoded.len(),
started.elapsed().as_micros(),
args[5]
);
Ok(true)
}
"verify-btor2-components" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker verify-btor2-components CONTROLLER.btor2 PLANT.btor2 CONTRACT.txt CERTIFICATE.component-cert".to_string());
}
let controller = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"controller BTOR2 input",
)?;
let plant = read_bounded_regular_file(
Path::new(&args[2]),
btor2::MAX_BTOR2_BYTES,
"plant BTOR2 input",
)?;
let contract = read_bounded_regular_file(
Path::new(&args[3]),
btor2_component::MAX_COMPONENT_CONTRACT_BYTES,
"component contract",
)?;
let encoded = read_bounded_regular_file(
Path::new(&args[4]),
btor2_component::MAX_COMPONENT_CERTIFICATE_BYTES,
"component certificate",
)?;
let certificate =
btor2_component::decode(&encoded).map_err(|error| error.to_string())?;
let started = Instant::now();
let summary = btor2_component::verify(&controller, &plant, &contract, &certificate)
.map_err(|error| error.to_string())?;
let backend = match summary.backend {
btor2_component::ComponentBackend::PhaseContract => "phase-contract",
btor2_component::ComponentBackend::ComposedSearch => "composed-search",
};
let result = match summary.result {
btor2_component::ComponentResult::Safe => "SAFE",
btor2_component::ComponentResult::Unsafe => "UNSAFE",
};
let bad_frame = summary
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-components status=VERIFIED certificate_version={} backend={backend} result={result} horizon={} bad_frame={bad_frame} logical_reachable_states={} certificate_bytes={} elapsed_micros={}",
btor2_component::COMPONENT_CERTIFICATE_VERSION,
summary.query_horizon,
summary.logical_reachable_states,
encoded.len(),
started.elapsed().as_micros()
);
Ok(true)
}
"check-btor2-component-batch" | "verify-btor2-component-batch" => {
if args.len() != 4 {
return Err(format!(
"usage: guarded-continuation-checker {command} CONTROLLER.btor2 MANIFEST.txt {}",
if command == "check-btor2-component-batch" {
"OUTPUT.component-batch"
} else {
"INPUT.component-batch"
}
));
}
let controller = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"controller BTOR2 input",
)?;
let manifest_path = Path::new(&args[2]);
let manifest = parse_component_batch_manifest(manifest_path)?;
let mut plants = Vec::with_capacity(manifest.len());
let mut contracts = Vec::with_capacity(manifest.len());
for member in &manifest {
plants.push(read_bounded_regular_file(
&member.plant_path,
btor2::MAX_BTOR2_BYTES,
"plant BTOR2 input",
)?);
contracts.push(read_bounded_regular_file(
&member.contract_path,
btor2_component::MAX_COMPONENT_CONTRACT_BYTES,
"component contract",
)?);
}
let inputs = manifest
.iter()
.enumerate()
.map(|(index, member)| btor2_component::ComponentBatchInput {
plant_source: plants[index].as_slice(),
contract_source: contracts[index].as_slice(),
horizon: member.horizon,
})
.collect::<Vec<_>>();
let started = Instant::now();
let (certificate, encoded, action, reason) = if command == "check-btor2-component-batch"
{
let production =
btor2_component::produce_component_batch_portfolio(&controller, &inputs)
.map_err(|error| error.to_string())?;
let reason = production.selection_reason.as_str();
let encoded =
btor2_component::encode_component_batch_portfolio(&production.certificate)
.map_err(|error| error.to_string())?;
(production.certificate, encoded, "CREATED", reason)
} else {
let encoded = read_bounded_regular_file(
Path::new(&args[3]),
btor2_component::MAX_COMPONENT_BATCH_PORTFOLIO_BYTES,
"component batch portfolio",
)?;
let certificate = btor2_component::decode_component_batch_portfolio(&encoded)
.map_err(|error| error.to_string())?;
let reason = match &certificate {
btor2_component::ComponentBatchPortfolioCertificate::Reusable(_) => {
"fully-admitted-reuse"
}
btor2_component::ComponentBatchPortfolioCertificate::Ordinary(_) => {
"singleton-or-exact-fallback"
}
};
(
certificate,
String::from_utf8(encoded)
.map_err(|_| "component batch portfolio is not UTF-8".to_string())?,
"VERIFIED",
reason,
)
};
let summary = btor2_component::verify_component_batch_portfolio(
&controller,
&inputs,
&certificate,
)
.map_err(|error| error.to_string())?;
let (route, members, reused, fallback) = match &certificate {
btor2_component::ComponentBatchPortfolioCertificate::Reusable(certificate) => (
"reusable",
certificate.members.len(),
certificate.members.len(),
0,
),
btor2_component::ComponentBatchPortfolioCertificate::Ordinary(certificate) => {
let fallback = certificate
.members
.iter()
.filter(|member| {
matches!(member, btor2_component::ComponentCertificate::Search(_))
})
.count();
("ordinary", certificate.members.len(), 0, fallback)
}
};
if command == "check-btor2-component-batch" {
write_new_certificate(Path::new(&args[3]), encoded.as_bytes())?;
}
println!(
"btor2-component-batch status={action} portfolio_version={} route={route} reason={reason} members={} reused_phase={} exact_fallback={} safe={} unsafe={} certificate_bytes={} elapsed_micros={}{}",
btor2_component::COMPONENT_BATCH_PORTFOLIO_VERSION,
members,
reused,
fallback,
summary.safe,
summary.unsafe_count,
encoded.len(),
started.elapsed().as_micros(),
if command == "check-btor2-component-batch" {
format!(" output={}", args[3])
} else {
String::new()
}
);
Ok(true)
}
"inspect-btor2" => {
if args.len() != 2 {
return Err(
"usage: guarded-continuation-checker inspect-btor2 INPUT.btor2".to_string(),
);
}
let path = Path::new(&args[1]);
let mut options = fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
options.custom_flags(libc::O_NOFOLLOW);
let file = options
.open(path)
.map_err(|error| format!("open BTOR2 {}: {error}", path.display()))?;
let metadata = file
.metadata()
.map_err(|error| format!("inspect open BTOR2 {}: {error}", path.display()))?;
if !metadata.file_type().is_file() {
return Err("BTOR2 input must be a regular non-symlink file".to_string());
}
if metadata.len() > btor2::MAX_BTOR2_BYTES as u64 {
return Err(format!(
"BTOR2 input exceeds {} bytes",
btor2::MAX_BTOR2_BYTES
));
}
let mut bytes = Vec::new();
file.take((btor2::MAX_BTOR2_BYTES + 1) as u64)
.read_to_end(&mut bytes)
.map_err(|error| format!("read BTOR2 {}: {error}", path.display()))?;
if bytes.len() > btor2::MAX_BTOR2_BYTES {
return Err(format!(
"BTOR2 input exceeds {} bytes",
btor2::MAX_BTOR2_BYTES
));
}
let model = btor2::parse_bytes(&bytes).map_err(|error| error.to_string())?;
let digest = Sha256::digest(&bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
println!(
"btor2-inspect status=VALID core_version={} sha256={digest} nodes={} inputs={} states={} bad={} constraints={} max_width={} word_semantics=preserved",
btor2::BTOR2_CORE_VERSION,
model.nodes().len(),
model.inputs().len(),
model.states().len(),
model.bad_properties().len(),
model.constraints().len(),
model.max_width(),
);
Ok(true)
}
"certify-btor2-counter-phase" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker certify-btor2-counter-phase INPUT.btor2 BAD_PROPERTY PHASES OUTPUT.cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let bad_property = args[2]
.parse::<u64>()
.map_err(|_| "BAD_PROPERTY must be an unsigned node identifier".to_string())?;
let phases = parse_btor2_phase_specs(&args[3])?;
let certificate = btor2_phase::produce(&source, &phases, bad_property)
.map_err(|error| error.to_string())?;
let encoded = btor2_phase::encode(&certificate).map_err(|error| error.to_string())?;
let output = Path::new(&args[4]);
write_new_certificate(output, encoded.as_bytes())?;
println!(
"btor2-phase-certificate status=CREATED version={} horizon={} phases={} final_state={} bad_property={} output={}",
btor2_phase::PHASE_CERTIFICATE_VERSION,
certificate.horizon,
certificate.phases.len(),
certificate.final_state,
certificate.bad_property,
output.display()
);
Ok(true)
}
"verify-btor2-counter-phase" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-btor2-counter-phase INPUT.btor2 CERTIFICATE.cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let encoded = read_bounded_regular_file(
Path::new(&args[2]),
btor2_phase::MAX_CERTIFICATE_BYTES,
"BTOR2 phase certificate",
)?;
let certificate = btor2_phase::decode(&encoded).map_err(|error| error.to_string())?;
let summary =
btor2_phase::verify(&source, &certificate).map_err(|error| error.to_string())?;
println!(
"btor2-phase-certificate status=VERIFIED version={} horizon={} phases={} final_state={} bad_property={}",
btor2_phase::PHASE_CERTIFICATE_VERSION,
summary.horizon,
summary.phases,
summary.final_state,
summary.bad_property
);
Ok(true)
}
"certify-btor2-counter-trace" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker certify-btor2-counter-trace INPUT.btor2 BAD_PROPERTY PHASES OUTPUT.cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let bad_property = args[2]
.parse::<u64>()
.map_err(|_| "BAD_PROPERTY must be an unsigned node identifier".to_string())?;
let phases = parse_btor2_phase_specs(&args[3])?;
let (backend, encoded, horizon, final_state) = match btor2_phase::produce(
&source,
&phases,
bad_property,
) {
Ok(certificate) => (
"closed-form-phase",
btor2_phase::encode(&certificate).map_err(|error| error.to_string())?,
certificate.horizon,
certificate.final_state,
),
Err(admission_error) => {
let certificate = btor2_phase::produce_replay(
&source,
&phases,
bad_property,
)
.map_err(|fallback_error| {
format!(
"phase backend rejected: {admission_error}; exact replay rejected: {fallback_error}"
)
})?;
let final_state = certificate
.final_states
.first()
.map_or(0, |(_, value)| *value);
(
"exact-replay",
btor2_phase::encode_replay(&certificate)
.map_err(|error| error.to_string())?,
certificate.horizon,
final_state,
)
}
};
let output = Path::new(&args[4]);
write_new_certificate(output, encoded.as_bytes())?;
println!(
"btor2-counter-trace status=CREATED backend={backend} horizon={horizon} phases={} final_state={final_state} bad_property={bad_property} output={}",
phases.len(),
output.display()
);
Ok(true)
}
"verify-btor2-counter-trace" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-btor2-counter-trace INPUT.btor2 CERTIFICATE.cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let encoded = read_bounded_regular_file(
Path::new(&args[2]),
btor2_phase::MAX_CERTIFICATE_BYTES,
"BTOR2 counter trace certificate",
)?;
let (backend, summary) = if encoded.starts_with(b"phase_certificate_version=") {
let certificate =
btor2_phase::decode(&encoded).map_err(|error| error.to_string())?;
(
"closed-form-phase",
btor2_phase::verify(&source, &certificate)
.map_err(|error| error.to_string())?,
)
} else if encoded.starts_with(b"replay_certificate_version=") {
let certificate =
btor2_phase::decode_replay(&encoded).map_err(|error| error.to_string())?;
(
"exact-replay",
btor2_phase::verify_replay(&source, &certificate)
.map_err(|error| error.to_string())?,
)
} else {
return Err("unknown BTOR2 counter trace certificate format".to_string());
};
println!(
"btor2-counter-trace status=VERIFIED backend={backend} horizon={} phases={} final_state={} bad_property={}",
summary.horizon, summary.phases, summary.final_state, summary.bad_property
);
Ok(true)
}
"search-btor2" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker search-btor2 INPUT.btor2 BAD_PROPERTY HORIZON OUTPUT.search-cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let bad_property = args[2]
.parse::<u64>()
.map_err(|_| "BAD_PROPERTY must be an unsigned node identifier".to_string())?;
let horizon = args[3]
.parse::<u32>()
.map_err(|_| "HORIZON must be an unsigned integer".to_string())?;
let certificate = btor2_search::produce(&source, bad_property, horizon)
.map_err(|error| error.to_string())?;
let encoded = btor2_search::encode(&certificate).map_err(|error| error.to_string())?;
let output = Path::new(&args[4]);
write_new_certificate(output, encoded.as_bytes())?;
let result = match certificate.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let bad_frame = certificate
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
if matches!(
certificate.certificate_version,
btor2_search::SEARCH_CERTIFICATE_V3_VERSION
| btor2_search::SEARCH_CERTIFICATE_V4_VERSION
| btor2_search::SEARCH_CERTIFICATE_VERSION
) {
println!(
"btor2-search status=CREATED version={} result={result} horizon={} bad_frame={bad_frame} layers={} witness_valuations={} output={}",
certificate.certificate_version,
certificate.query_horizon,
certificate.layers.len(),
certificate.witness_valuations.len(),
output.display()
);
} else {
println!(
"btor2-search status=CREATED version={} result={result} horizon={} bad_frame={bad_frame} layers={} witness_inputs={} output={}",
certificate.certificate_version,
certificate.query_horizon,
certificate.layers.len(),
certificate.witness_inputs.len(),
output.display()
);
}
Ok(true)
}
"verify-btor2-search" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-btor2-search INPUT.btor2 CERTIFICATE.search-cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let encoded = read_bounded_regular_file(
Path::new(&args[2]),
btor2_search::MAX_SEARCH_CERTIFICATE_BYTES,
"BTOR2 search certificate",
)?;
let certificate = btor2_search::decode(&encoded).map_err(|error| error.to_string())?;
let started = Instant::now();
let summary =
btor2_search::verify(&source, &certificate).map_err(|error| error.to_string())?;
let result = match summary.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let bad_frame = summary
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-search status=VERIFIED version={} result={result} horizon={} bad_frame={bad_frame} reachable_states={} certificate_bytes={} elapsed_micros={}",
certificate.certificate_version,
summary.query_horizon,
summary.reachable_states,
encoded.len(),
started.elapsed().as_micros()
);
Ok(true)
}
"check-btor2-bounded" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker check-btor2-bounded INPUT.btor2 BAD_PROPERTY HORIZON OUTPUT.btor2-cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let bad_property = args[2]
.parse::<u64>()
.map_err(|_| "BAD_PROPERTY must be an unsigned node identifier".to_string())?;
let horizon = args[3]
.parse::<u32>()
.map_err(|_| "HORIZON must be an unsigned integer".to_string())?;
let started = Instant::now();
let production =
btor2_bounded::produce_with_observation(&source, bad_property, horizon)
.map_err(|error| error.to_string())?;
let selection_reason = production.selection_reason;
let certificate = production.certificate;
let encoded = btor2_bounded::encode(&certificate).map_err(|error| error.to_string())?;
let summary =
btor2_bounded::verify(&source, &certificate).map_err(|error| error.to_string())?;
let output = Path::new(&args[4]);
write_new_certificate(output, encoded.as_bytes())?;
let backend = match summary.backend {
btor2_bounded::BoundedBackend::BrakingPhases => "braking-phases",
btor2_bounded::BoundedBackend::MotionCurve => "motion-curve",
btor2_bounded::BoundedBackend::WordRegion => "word-region",
btor2_bounded::BoundedBackend::ExplicitSearch => "explicit-search",
};
let result = match summary.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let bad_frame = summary
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-bounded status=CREATED portfolio_version={} backend={backend} reason={} result={result} horizon={} bad_frame={bad_frame} logical_reachable_states={} certificate_bytes={} elapsed_micros={} output={}",
btor2_bounded::BOUNDED_PORTFOLIO_VERSION,
selection_reason.as_str(),
summary.query_horizon,
summary.logical_reachable_states,
encoded.len(),
started.elapsed().as_micros(),
output.display()
);
Ok(true)
}
"verify-btor2-bounded" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-btor2-bounded INPUT.btor2 CERTIFICATE.btor2-cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let encoded = read_bounded_regular_file(
Path::new(&args[2]),
btor2_search::MAX_SEARCH_CERTIFICATE_BYTES,
"bounded BTOR2 certificate",
)?;
let certificate = btor2_bounded::decode(&encoded).map_err(|error| error.to_string())?;
let started = Instant::now();
let summary =
btor2_bounded::verify(&source, &certificate).map_err(|error| error.to_string())?;
let backend = match summary.backend {
btor2_bounded::BoundedBackend::BrakingPhases => "braking-phases",
btor2_bounded::BoundedBackend::MotionCurve => "motion-curve",
btor2_bounded::BoundedBackend::WordRegion => "word-region",
btor2_bounded::BoundedBackend::ExplicitSearch => "explicit-search",
};
let result = match summary.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let bad_frame = summary
.bad_frame
.map_or_else(|| "none".to_string(), |frame| frame.to_string());
println!(
"btor2-bounded status=VERIFIED portfolio_version={} backend={backend} result={result} horizon={} bad_frame={bad_frame} logical_reachable_states={} certificate_bytes={} elapsed_micros={}",
btor2_bounded::BOUNDED_PORTFOLIO_VERSION,
summary.query_horizon,
summary.logical_reachable_states,
encoded.len(),
started.elapsed().as_micros()
);
Ok(true)
}
"btor2-predicate-set-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker btor2-predicate-set-version".to_string(),
);
}
println!(
"btor2_predicate_set_cli_version={} certificate_versions={},{},{} portfolio_versions={},{},{} current_certificate_version={} current_portfolio_version={} max_members={} max_chain_states={} max_horizon={} max_source_bytes={} max_certificate_bytes={} property_order=strictly-increasing",
btor2_predicate_set::PREDICATE_SET_CLI_VERSION,
btor2_predicate_set::PREDICATE_SET_CERTIFICATE_V1_VERSION,
btor2_predicate_set::PREDICATE_SET_CERTIFICATE_V2_VERSION,
btor2_predicate_set::PREDICATE_SET_CERTIFICATE_VERSION,
btor2_predicate_set::PREDICATE_SET_PORTFOLIO_V1_VERSION,
btor2_predicate_set::PREDICATE_SET_PORTFOLIO_V2_VERSION,
btor2_predicate_set::PREDICATE_SET_PORTFOLIO_VERSION,
btor2_predicate_set::PREDICATE_SET_CERTIFICATE_VERSION,
btor2_predicate_set::PREDICATE_SET_PORTFOLIO_VERSION,
btor2_predicate_set::MAX_PREDICATE_SET_MEMBERS,
btor2_invariant_chain::MAX_CHAIN_STATES,
btor2_region::MAX_REGION_HORIZON,
btor2::MAX_BTOR2_BYTES,
btor2_predicate_set::MAX_PREDICATE_SET_CERTIFICATE_BYTES,
);
Ok(true)
}
"check-btor2-predicate-set" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker check-btor2-predicate-set INPUT.btor2 BAD_PROPERTIES HORIZON OUTPUT.btor2-set-cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let properties = parse_btor2_property_set(&args[2])?;
let horizon = args[3]
.parse::<u32>()
.map_err(|_| "HORIZON must be an unsigned integer".to_string())?;
let started = Instant::now();
let production = btor2_predicate_set::produce(&source, &properties, horizon)
.map_err(|error| error.to_string())?;
let reason = production.selection_reason;
let certificate = production.certificate;
let encoded =
btor2_predicate_set::encode(&certificate).map_err(|error| error.to_string())?;
let summary = btor2_predicate_set::verify(&source, &properties, horizon, &certificate)
.map_err(|error| error.to_string())?;
let output = Path::new(&args[4]);
write_new_certificate(output, encoded.as_bytes())?;
let answers = summary
.members
.iter()
.map(|member| {
let result = match member.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let frame = member
.bad_frame
.map_or_else(|| "none".to_string(), |value| value.to_string());
format!("{}:{result}:{frame}", member.bad_property)
})
.collect::<Vec<_>>()
.join(",");
println!(
"btor2-predicate-set status=CREATED certificate_version={} portfolio_version={} route={} reason={} horizon={} members={} safe={} unsafe={} answers={} logical_reachable_states={} certificate_bytes={} elapsed_micros={} output={}",
btor2_predicate_set::certificate_version(&certificate),
btor2_predicate_set::portfolio_version(&certificate),
summary.route.as_str(),
reason.as_str(),
summary.query_horizon,
summary.members.len(),
summary.safe,
summary.unsafe_count,
answers,
summary.logical_reachable_states,
encoded.len(),
started.elapsed().as_micros(),
output.display()
);
Ok(true)
}
"verify-btor2-predicate-set" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker verify-btor2-predicate-set INPUT.btor2 BAD_PROPERTIES HORIZON CERTIFICATE.btor2-set-cert".to_string());
}
let source = read_bounded_regular_file(
Path::new(&args[1]),
btor2::MAX_BTOR2_BYTES,
"BTOR2 input",
)?;
let properties = parse_btor2_property_set(&args[2])?;
let horizon = args[3]
.parse::<u32>()
.map_err(|_| "HORIZON must be an unsigned integer".to_string())?;
let encoded = read_bounded_regular_file(
Path::new(&args[4]),
btor2_predicate_set::MAX_PREDICATE_SET_CERTIFICATE_BYTES,
"BTOR2 predicate-set certificate",
)?;
let certificate =
btor2_predicate_set::decode(&encoded).map_err(|error| error.to_string())?;
let started = Instant::now();
let summary = btor2_predicate_set::verify(&source, &properties, horizon, &certificate)
.map_err(|error| error.to_string())?;
let answers = summary
.members
.iter()
.map(|member| {
let result = match member.result {
btor2_search::SearchResult::Safe => "SAFE",
btor2_search::SearchResult::Unsafe => "UNSAFE",
};
let frame = member
.bad_frame
.map_or_else(|| "none".to_string(), |value| value.to_string());
format!("{}:{result}:{frame}", member.bad_property)
})
.collect::<Vec<_>>()
.join(",");
println!(
"btor2-predicate-set status=VERIFIED certificate_version={} portfolio_version={} route={} horizon={} members={} safe={} unsafe={} answers={} logical_reachable_states={} certificate_bytes={} elapsed_micros={}",
btor2_predicate_set::certificate_version(&certificate),
btor2_predicate_set::portfolio_version(&certificate),
summary.route.as_str(),
summary.query_horizon,
summary.members.len(),
summary.safe,
summary.unsafe_count,
answers,
summary.logical_reachable_states,
encoded.len(),
started.elapsed().as_micros()
);
Ok(true)
}
"predicate-cli-version" => {
if args.len() != 1 {
return Err("usage: guarded-continuation-checker predicate-cli-version".to_string());
}
println!("{}", predicate_cli_contract_line());
Ok(true)
}
"event-contract-cli-version" => {
if args.len() != 1 {
return Err(
"usage: guarded-continuation-checker event-contract-cli-version".to_string(),
);
}
println!("{}", event_contract_cli_contract_line());
Ok(true)
}
"explain-aiger-counterexample" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker explain-aiger-counterexample INPUT.aag HORIZON MAX_BOUND_BITS OUTPUT_DIR".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid causal analysis horizon".to_string())?;
if horizon == 0 {
return Err("causal analysis horizon must be at least one".to_string());
}
let max_bound_bits = args[3]
.parse::<usize>()
.map_err(|_| "invalid causal CQ frontier bound".to_string())?;
if max_bound_bits > 20 {
return Err("causal CQ frontier bound must not exceed 20 bits".to_string());
}
explain_aiger_counterexample(
Path::new(&args[1]),
horizon,
max_bound_bits,
Path::new(&args[4]),
)?;
Ok(true)
}
"verify-aiger-causal-bundle" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-aiger-causal-bundle INPUT.aag OUTPUT_DIR".to_string());
}
verify_causal_bundle(Path::new(&args[1]), Path::new(&args[2]))?;
Ok(true)
}
"verify-aiger-causal-certificate" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-aiger-causal-certificate INPUT.aag CERTIFICATE.cert".to_string());
}
verify_causal_certificate(Path::new(&args[1]), Path::new(&args[2]))?;
Ok(true)
}
"benchmark-aiger-causal-strategies" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker benchmark-aiger-causal-strategies INPUT.aag HORIZON MAX_BOUND_BITS OUTPUT.csv".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid causal comparison horizon".to_string())?;
if horizon == 0 {
return Err("causal comparison horizon must be at least one".to_string());
}
let max_bound_bits = args[3]
.parse::<usize>()
.map_err(|_| "invalid causal comparison CQ frontier bound".to_string())?;
if max_bound_bits > 20 {
return Err(
"causal comparison CQ frontier bound must not exceed 20 bits".to_string(),
);
}
benchmark_aiger_causal_strategies(
Path::new(&args[1]),
horizon,
max_bound_bits,
Path::new(&args[4]),
)?;
Ok(true)
}
"benchmark-aiger-causal-batch" => {
if args.len() != 7 {
return Err("usage: guarded-continuation-checker benchmark-aiger-causal-batch INPUT.aag|INPUT.aig HORIZON MAX_BOUND_BITS MAX_CAUSES REPEATS OUTPUT.csv".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid causal batch horizon".to_string())?;
if horizon == 0 {
return Err("causal batch horizon must be at least one".to_string());
}
let max_bound_bits = args[3]
.parse::<usize>()
.map_err(|_| "invalid causal batch CQ frontier bound".to_string())?;
if max_bound_bits > 20 {
return Err("causal batch CQ frontier bound must not exceed 20 bits".to_string());
}
let max_causes = args[4]
.parse::<usize>()
.map_err(|_| "invalid causal batch cause limit".to_string())?;
let repeats = args[5]
.parse::<usize>()
.map_err(|_| "invalid causal batch repeat count".to_string())?;
benchmark_aiger_causal_batch(
Path::new(&args[1]),
horizon,
max_bound_bits,
max_causes,
repeats,
Path::new(&args[6]),
)?;
Ok(true)
}
"verify-aiger-causal-batch" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-aiger-causal-batch INPUT.aag|INPUT.aig OUTPUT.csv".to_string());
}
verify_aiger_causal_batch(Path::new(&args[1]), Path::new(&args[2]))?;
Ok(true)
}
"benchmark-aiger-interface-quotient" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker benchmark-aiger-interface-quotient INPUT.aag|INPUT.aig HORIZON MAX_CAUSES REPEATS OUTPUT.csv".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid interface quotient horizon".to_string())?;
let max_causes = args[3]
.parse::<usize>()
.map_err(|_| "invalid interface quotient cause limit".to_string())?;
let repeats = args[4]
.parse::<usize>()
.map_err(|_| "invalid interface quotient repeat count".to_string())?;
benchmark_aiger_interface_quotient(
Path::new(&args[1]),
horizon,
max_causes,
repeats,
Path::new(&args[5]),
)?;
Ok(true)
}
"verify-aiger-interface-quotient" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-aiger-interface-quotient INPUT.aag|INPUT.aig OUTPUT.csv".to_string());
}
verify_aiger_interface_quotient(Path::new(&args[1]), Path::new(&args[2]))?;
Ok(true)
}
"benchmark-aiger-predicate-interface" => {
if args.len() != 4 {
return Err("usage: guarded-continuation-checker benchmark-aiger-predicate-interface INPUT.aag|INPUT.aig REPEATS OUTPUT.csv".to_string());
}
let repeats = args[2]
.parse::<usize>()
.map_err(|_| "invalid predicate interface repeat count".to_string())?;
benchmark_aiger_predicate_interface(Path::new(&args[1]), repeats, Path::new(&args[3]))?;
Ok(true)
}
"benchmark-aiger-event-contract" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker benchmark-aiger-event-contract INPUT.aag|INPUT.aig OUTPUT_INDEX CONTRACT.txt REPEATS OUTPUT.csv".to_string());
}
let bad_output = args[2]
.parse::<usize>()
.map_err(|_| "invalid event contract output index".to_string())?;
let repeats = args[4]
.parse::<usize>()
.map_err(|_| "invalid event contract repeat count".to_string())?;
benchmark_aiger_event_contract(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
repeats,
Path::new(&args[5]),
)?;
Ok(true)
}
"benchmark-aiger-event-contract-proofs" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker benchmark-aiger-event-contract-proofs INPUT.aag|INPUT.aig OUTPUT_INDEX CONTRACT.txt REPEATS OUTPUT.csv".to_string());
}
let bad_output = args[2]
.parse::<usize>()
.map_err(|_| "invalid event contract proof output index".to_string())?;
let repeats = args[4]
.parse::<usize>()
.map_err(|_| "invalid event contract proof repeat count".to_string())?;
benchmark_aiger_event_contract_proofs(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
repeats,
Path::new(&args[5]),
)?;
Ok(true)
}
"certify-aiger-event-contract-v3" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker certify-aiger-event-contract-v3 INPUT.aag|INPUT.aig OUTPUT_INDEX CONTRACT.txt CERTIFICATE.cert3".to_string());
}
let bad_output = args[2]
.parse::<usize>()
.map_err(|_| "invalid event contract certificate v3 output index".to_string())?;
certify_aiger_event_contract_v3(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
Path::new(&args[4]),
)?;
Ok(true)
}
"verify-aiger-event-contract-certificate-v3" => {
if args.len() != 4 {
return Err("usage: guarded-continuation-checker verify-aiger-event-contract-certificate-v3 INPUT.aag|INPUT.aig CONTRACT.txt CERTIFICATE.cert3".to_string());
}
verify_aiger_event_contract_certificate_v3(
Path::new(&args[1]),
Path::new(&args[2]),
Path::new(&args[3]),
)?;
Ok(true)
}
"export-aiger-event-contract-v3-obligations" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker export-aiger-event-contract-v3-obligations INPUT.aag|INPUT.aig CONTRACT.txt CERTIFICATE.cert3 OUTPUT_DIR".to_string());
}
export_aiger_event_contract_v3_obligations(
Path::new(&args[1]),
Path::new(&args[2]),
Path::new(&args[3]),
Path::new(&args[4]),
)?;
Ok(true)
}
"verify-aiger-event-contract-portfolio" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker verify-aiger-event-contract-portfolio INPUT.aag|INPUT.aig OUTPUT_INDEX CONTRACT.txt REPORT.txt CERTIFICATE.cert3".to_string());
}
let bad_output = args[2]
.parse::<usize>()
.map_err(|_| "invalid event contract portfolio output index".to_string())?;
verify_aiger_event_contract_portfolio(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
Path::new(&args[4]),
Path::new(&args[5]),
)?;
Ok(true)
}
"verify-aiger-event-contract-portfolio-report" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker verify-aiger-event-contract-portfolio-report INPUT.aag|INPUT.aig OUTPUT_INDEX CONTRACT.txt REPORT.txt CERTIFICATE.cert3".to_string());
}
let bad_output = args[2]
.parse::<usize>()
.map_err(|_| "invalid event contract portfolio report output index".to_string())?;
verify_aiger_event_contract_portfolio_report(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
Path::new(&args[4]),
Path::new(&args[5]),
)?;
Ok(true)
}
"benchmark-aiger-event-contract-certificate-v3-cost" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker benchmark-aiger-event-contract-certificate-v3-cost INPUT.aag|INPUT.aig OUTPUT_INDEX CONTRACT.txt REPEATS OUTPUT.csv".to_string());
}
let bad_output = args[2].parse::<usize>().map_err(|_| {
"invalid event contract certificate v3 cost output index".to_string()
})?;
let repeats = args[4].parse::<usize>().map_err(|_| {
"invalid event contract certificate v3 cost repeat count".to_string()
})?;
benchmark_aiger_event_contract_certificate_v3_cost(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
repeats,
Path::new(&args[5]),
)?;
Ok(true)
}
"query-aiger-predicate-quotient" => {
if args.len() != 4 {
return Err("usage: guarded-continuation-checker query-aiger-predicate-quotient INPUT.aag|INPUT.aig HORIZON OUTPUT_INDEX".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid predicate quotient horizon".to_string())?;
let output = args[3]
.parse::<usize>()
.map_err(|_| "invalid predicate quotient output index".to_string())?;
query_aiger_predicate_quotient(Path::new(&args[1]), horizon, output)?;
Ok(true)
}
"certify-aiger-predicate" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker certify-aiger-predicate INPUT.aag|INPUT.aig OUTPUT_INDEX TRANSCRIPT.txt CERTIFICATE.cert".to_string());
}
let output = args[2]
.parse::<usize>()
.map_err(|_| "invalid predicate certificate output index".to_string())?;
certify_aiger_predicate(
Path::new(&args[1]),
output,
Path::new(&args[3]),
Path::new(&args[4]),
)?;
Ok(true)
}
"verify-aiger-predicate-certificate" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-aiger-predicate-certificate INPUT.aag|INPUT.aig CERTIFICATE.cert".to_string());
}
verify_aiger_predicate_certificate(Path::new(&args[1]), Path::new(&args[2]))?;
Ok(true)
}
"certify-aiger-predicate-v2" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker certify-aiger-predicate-v2 INPUT.aag|INPUT.aig OUTPUT_INDEX TRANSCRIPT.txt CERTIFICATE.cert2".to_string());
}
let output = args[2]
.parse::<usize>()
.map_err(|_| "invalid predicate certificate v2 output index".to_string())?;
certify_aiger_predicate_v2(
Path::new(&args[1]),
output,
Path::new(&args[3]),
Path::new(&args[4]),
)?;
Ok(true)
}
"verify-aiger-predicate-certificate-v2" => {
if args.len() != 3 {
return Err("usage: guarded-continuation-checker verify-aiger-predicate-certificate-v2 INPUT.aag|INPUT.aig CERTIFICATE.cert2".to_string());
}
verify_aiger_predicate_certificate_v2(Path::new(&args[1]), Path::new(&args[2]))?;
Ok(true)
}
"export-aiger-predicate-v2-obligations" => {
if args.len() != 4 {
return Err("usage: guarded-continuation-checker export-aiger-predicate-v2-obligations INPUT.aag|INPUT.aig CERTIFICATE.cert2 OUTPUT_DIR".to_string());
}
export_aiger_predicate_v2_obligations(
Path::new(&args[1]),
Path::new(&args[2]),
Path::new(&args[3]),
)?;
Ok(true)
}
"verify-aiger-counterfactual" => {
if args.len() != 7 {
return Err("usage: guarded-continuation-checker verify-aiger-counterfactual INPUT.aag|INPUT.aig OUTPUT_INDEX TRANSCRIPT.txt EXPECTED_QUERIES REPORT.txt CERTIFICATE.cert".to_string());
}
let output = args[2]
.parse::<usize>()
.map_err(|_| "invalid counterfactual output index".to_string())?;
let expected_queries = args[4]
.parse::<usize>()
.map_err(|_| "invalid counterfactual expected query count".to_string())?;
verify_aiger_counterfactual_portfolio(
Path::new(&args[1]),
output,
Path::new(&args[3]),
expected_queries,
Path::new(&args[5]),
Path::new(&args[6]),
)?;
Ok(true)
}
"verify-aiger-counterfactual-report" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker verify-aiger-counterfactual-report INPUT.aag|INPUT.aig TRANSCRIPT.txt REPORT.txt CERTIFICATE.cert".to_string());
}
verify_aiger_counterfactual_report(
Path::new(&args[1]),
Path::new(&args[2]),
Path::new(&args[3]),
Path::new(&args[4]),
)?;
Ok(true)
}
"benchmark-aiger-predicate-certificate-cost" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker benchmark-aiger-predicate-certificate-cost INPUT.aag|INPUT.aig OUTPUT_INDEX TRANSCRIPT.txt REPEATS OUTPUT.csv".to_string());
}
let bad_output = args[2]
.parse::<usize>()
.map_err(|_| "invalid predicate certificate cost output index".to_string())?;
let repeats = args[4]
.parse::<usize>()
.map_err(|_| "invalid predicate certificate cost repeats".to_string())?;
benchmark_aiger_predicate_certificate_cost(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
repeats,
Path::new(&args[5]),
)?;
Ok(true)
}
"benchmark-aiger-predicate-certificate-v2-cost" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker benchmark-aiger-predicate-certificate-v2-cost INPUT.aag|INPUT.aig OUTPUT_INDEX TRANSCRIPT.txt REPEATS OUTPUT.csv".to_string());
}
let bad_output = args[2]
.parse::<usize>()
.map_err(|_| "invalid predicate certificate v2 cost output index".to_string())?;
let repeats = args[4]
.parse::<usize>()
.map_err(|_| "invalid predicate certificate v2 cost repeats".to_string())?;
benchmark_aiger_predicate_certificate_v2_cost(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
repeats,
Path::new(&args[5]),
)?;
Ok(true)
}
"benchmark-aiger-predicate-proof-relation" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker benchmark-aiger-predicate-proof-relation INPUT.aag|INPUT.aig TRANSCRIPT.txt REPEATS OUTPUT.csv".to_string());
}
let repeats = args[3]
.parse::<usize>()
.map_err(|_| "invalid predicate proof relation repeats".to_string())?;
benchmark_aiger_predicate_proof_relation(
Path::new(&args[1]),
Path::new(&args[2]),
repeats,
Path::new(&args[4]),
)?;
Ok(true)
}
"benchmark-aiger-predicate-proof-terminal" => {
if args.len() != 6 {
return Err("usage: guarded-continuation-checker benchmark-aiger-predicate-proof-terminal INPUT.aag|INPUT.aig OUTPUT_INDEX TRANSCRIPT.txt REPEATS OUTPUT.csv".to_string());
}
let bad_output = args[2]
.parse::<usize>()
.map_err(|_| "invalid predicate proof terminal output index".to_string())?;
let repeats = args[4]
.parse::<usize>()
.map_err(|_| "invalid predicate proof terminal repeats".to_string())?;
benchmark_aiger_predicate_proof_terminal(
Path::new(&args[1]),
bad_output,
Path::new(&args[3]),
repeats,
Path::new(&args[5]),
)?;
Ok(true)
}
"benchmark-aiger-predicate-symbolic" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker benchmark-aiger-predicate-symbolic INPUT.aag|INPUT.aig HORIZON REPEATS OUTPUT.csv".to_string());
}
let horizon = args[2]
.parse::<usize>()
.map_err(|_| "invalid predicate symbolic horizon".to_string())?;
let repeats = args[3]
.parse::<usize>()
.map_err(|_| "invalid predicate symbolic repeat count".to_string())?;
benchmark_aiger_predicate_symbolic(
Path::new(&args[1]),
horizon,
repeats,
Path::new(&args[4]),
)?;
Ok(true)
}
"benchmark-continuation-quotients" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-continuation-quotients FAMILY VARS RATIO SEED STRATEGIES OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid quotient variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid quotient ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid quotient seed".to_string())?;
let strategies = args[5]
.parse::<usize>()
.map_err(|_| "invalid quotient strategy count".to_string())?
.max(1);
benchmark_continuation_quotients(
&args[1],
vars,
ratio,
seed,
strategies,
None,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-continuation-gate" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-continuation-gate FAMILY VARS RATIO SEED MAX_BOUND_BITS OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid gated quotient variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid gated quotient ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid gated quotient seed".to_string())?;
let limit = args[5]
.parse::<usize>()
.map_err(|_| "invalid continuation gate bound".to_string())?;
benchmark_continuation_quotients(
&args[1],
vars,
ratio,
seed,
1,
Some(limit),
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-continuation-reuse" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-continuation-reuse FAMILY VARS RATIO SEED QUERIES OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid continuation reuse variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid continuation reuse ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid continuation reuse seed".to_string())?;
let queries = args[5]
.parse::<usize>()
.map_err(|_| "invalid continuation reuse query count".to_string())?
.max(1);
benchmark_continuation_reuse(
&args[1],
vars,
ratio,
seed,
queries,
3,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-continuation-dimacs" => {
if args.len() != 5 {
return Err("usage: layered-sat benchmark-continuation-dimacs INPUT.cnf QUERIES MAX_ASSUMPTIONS OUTPUT.csv".to_string());
}
let queries = args[2]
.parse::<usize>()
.map_err(|_| "invalid DIMACS continuation query count".to_string())?
.max(1);
let max_assumptions = args[3]
.parse::<usize>()
.map_err(|_| "invalid DIMACS maximum assumptions".to_string())?
.max(1);
benchmark_continuation_dimacs(
Path::new(&args[1]),
queries,
max_assumptions,
Path::new(&args[4]),
)?;
Ok(true)
}
"benchmark-continuation-reuse-stress" => {
if args.len() != 8 {
return Err("usage: layered-sat benchmark-continuation-reuse-stress FAMILY VARS RATIO SEED QUERIES MAX_ASSUMPTIONS OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid continuation stress variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid continuation stress ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid continuation stress seed".to_string())?;
let queries = args[5]
.parse::<usize>()
.map_err(|_| "invalid continuation stress query count".to_string())?
.max(1);
let max_assumptions = args[6]
.parse::<usize>()
.map_err(|_| "invalid maximum assumption count".to_string())?
.clamp(1, vars.max(1));
benchmark_continuation_reuse(
&args[1],
vars,
ratio,
seed,
queries,
max_assumptions,
Path::new(&args[7]),
)?;
Ok(true)
}
"benchmark-continuation-temporal-phase" => {
if args.len() != 7 {
return Err("usage: guarded-continuation-checker benchmark-continuation-temporal-phase WIDTHS HORIZONS QUERIES MAX_BOUND_BITS SEED OUTPUT.csv (WIDTHS/HORIZONS are comma-separated)".to_string());
}
let widths = parse_size_grid(&args[1], "width")?;
let horizons = parse_size_grid(&args[2], "horizon")?;
let queries = args[3]
.parse::<usize>()
.map_err(|_| "invalid temporal query count".to_string())?
.max(1);
let max_bound_bits = args[4]
.parse::<usize>()
.map_err(|_| "invalid temporal bound".to_string())?;
let seed = args[5]
.parse::<u64>()
.map_err(|_| "invalid temporal seed".to_string())?;
benchmark_continuation_temporal_phase(
&widths,
&horizons,
queries,
max_bound_bits,
seed,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-temporal-vocabulary" => {
if args.len() != 8 {
return Err("usage: guarded-continuation-checker benchmark-temporal-vocabulary KINDS WIDTHS HORIZONS QUERIES MAX_WIDTH SEED OUTPUT.csv (grids are comma-separated)".to_string());
}
let kinds: Vec<_> = args[1].split(',').filter(|kind| !kind.is_empty()).collect();
if kinds.is_empty() {
return Err("transition kind grid must not be empty".to_string());
}
let widths = parse_size_grid(&args[2], "width")?;
let horizons = parse_size_grid(&args[3], "horizon")?;
let queries = args[4]
.parse::<usize>()
.map_err(|_| "invalid vocabulary query count".to_string())?
.max(1);
let max_width = args[5]
.parse::<usize>()
.map_err(|_| "invalid vocabulary width gate".to_string())?;
let seed = args[6]
.parse::<u64>()
.map_err(|_| "invalid vocabulary seed".to_string())?;
benchmark_temporal_vocabulary(
&kinds,
&widths,
&horizons,
queries,
max_width,
seed,
Path::new(&args[7]),
)?;
Ok(true)
}
"benchmark-temporal-compositions"
| "benchmark-local-temporal-compositions"
| "benchmark-symbolic-temporal-compositions" => {
if args.len() != 8 {
return Err("usage: guarded-continuation-checker benchmark-{local-}temporal-compositions KINDS WIDTHS HORIZONS QUERIES MAX_WIDTH SEED OUTPUT.csv (grids are comma-separated)".to_string());
}
let kinds: Vec<_> = args[1].split(',').filter(|kind| !kind.is_empty()).collect();
if kinds.is_empty() {
return Err("composition kind grid must not be empty".to_string());
}
let widths = parse_size_grid(&args[2], "width")?;
let horizons = parse_size_grid(&args[3], "horizon")?;
let queries = args[4]
.parse::<usize>()
.map_err(|_| "invalid composition query count".to_string())?
.max(1);
let max_width = args[5]
.parse::<usize>()
.map_err(|_| "invalid composition width gate".to_string())?;
let seed = args[6]
.parse::<u64>()
.map_err(|_| "invalid composition seed".to_string())?;
if args[0] == "benchmark-symbolic-temporal-compositions" {
benchmark_symbolic_temporal_compositions(
&kinds,
&widths,
&horizons,
queries,
max_width,
seed,
Path::new(&args[7]),
)?;
} else {
benchmark_temporal_compositions(
&kinds,
&widths,
&horizons,
queries,
max_width,
seed,
Path::new(&args[7]),
args[0] == "benchmark-local-temporal-compositions",
)?;
}
Ok(true)
}
"benchmark-symbolic-preimages" => {
if args.len() != 9 {
return Err("usage: guarded-continuation-checker benchmark-symbolic-preimages KINDS WIDTHS HORIZONS QUERIES NODE_LIMIT ORDER SEED OUTPUT.csv".to_string());
}
let kinds: Vec<_> = args[1].split(',').filter(|kind| !kind.is_empty()).collect();
let widths = parse_size_grid(&args[2], "width")?;
let horizons = parse_size_grid(&args[3], "horizon")?;
let queries = args[4]
.parse::<usize>()
.map_err(|_| "invalid preimage query count".to_string())?
.max(1);
let node_limit = args[5]
.parse::<usize>()
.map_err(|_| "invalid preimage node limit".to_string())?;
let seed = args[7]
.parse::<u64>()
.map_err(|_| "invalid preimage seed".to_string())?;
benchmark_symbolic_preimages(
&kinds,
&widths,
&horizons,
queries,
node_limit,
seed,
Path::new(&args[8]),
&args[6],
)?;
Ok(true)
}
"benchmark-checkpoint-cdcl" | "benchmark-checkpoint-aig" | "benchmark-checkpoint-lazy" => {
if args.len() != 9 {
return Err("usage: guarded-continuation-checker benchmark-checkpoint-cdcl KIND WIDTHS HORIZONS QUERIES CHECKPOINT NODE_LIMIT SEED OUTPUT.csv".to_string());
}
let widths = parse_size_grid(&args[2], "width")?;
let horizons = parse_size_grid(&args[3], "horizon")?;
let queries = args[4]
.parse::<usize>()
.map_err(|_| "invalid checkpoint query count".to_string())?
.max(1);
let checkpoint = args[5]
.parse::<usize>()
.map_err(|_| "invalid checkpoint frame".to_string())?;
let node_limit = args[6]
.parse::<usize>()
.map_err(|_| "invalid checkpoint node limit".to_string())?;
let seed = args[7]
.parse::<u64>()
.map_err(|_| "invalid checkpoint seed".to_string())?;
benchmark_checkpoint_cdcl(
&args[1],
&widths,
&horizons,
queries,
checkpoint,
node_limit,
seed,
Path::new(&args[8]),
match args[0].as_str() {
"benchmark-checkpoint-aig" => "aig",
"benchmark-checkpoint-lazy" => "lazy-bdd",
_ => "bdd",
},
)?;
Ok(true)
}
"benchmark-native-bdd-theory" => {
if args.len() != 9 {
return Err("usage: guarded-continuation-checker benchmark-native-bdd-theory KIND WIDTHS HORIZONS QUERIES CHECKPOINT NODE_LIMIT SEED OUTPUT.csv".to_string());
}
benchmark_native_bdd_theory(
&args[1],
&parse_size_grid(&args[2], "width")?,
&parse_size_grid(&args[3], "horizon")?,
args[4]
.parse::<usize>()
.map_err(|_| "invalid theory query count".to_string())?
.max(1),
args[5]
.parse::<usize>()
.map_err(|_| "invalid theory checkpoint".to_string())?,
args[6]
.parse::<usize>()
.map_err(|_| "invalid theory node limit".to_string())?,
args[7]
.parse::<u64>()
.map_err(|_| "invalid theory seed".to_string())?,
Path::new(&args[8]),
)?;
Ok(true)
}
"benchmark-cq-portfolio" => {
if args.len() != 9 {
return Err("usage: guarded-continuation-checker benchmark-cq-portfolio KIND WIDTHS HORIZONS QUERIES CHECKPOINT NODE_LIMIT SEED OUTPUT.csv".to_string());
}
benchmark_cq_portfolio(
&args[1],
&parse_size_grid(&args[2], "width")?,
&parse_size_grid(&args[3], "horizon")?,
args[4]
.parse::<usize>()
.map_err(|_| "invalid portfolio query count".to_string())?
.max(1),
args[5]
.parse::<usize>()
.map_err(|_| "invalid portfolio checkpoint".to_string())?,
args[6]
.parse::<usize>()
.map_err(|_| "invalid portfolio node limit".to_string())?,
args[7]
.parse::<u64>()
.map_err(|_| "invalid portfolio seed".to_string())?,
Path::new(&args[8]),
)?;
Ok(true)
}
"benchmark-cq-aiger" => {
if args.len() != 8 {
return Err("usage: guarded-continuation-checker benchmark-cq-aiger INPUT.aag HORIZON QUERIES CHECKPOINT NODE_LIMIT SEED OUTPUT.csv".to_string());
}
benchmark_cq_aiger(
Path::new(&args[1]),
args[2]
.parse::<usize>()
.map_err(|_| "invalid AIGER horizon".to_string())?
.max(1),
args[3]
.parse::<usize>()
.map_err(|_| "invalid AIGER query count".to_string())?
.max(1),
args[4]
.parse::<usize>()
.map_err(|_| "invalid AIGER checkpoint".to_string())?,
args[5]
.parse::<usize>()
.map_err(|_| "invalid AIGER node limit".to_string())?,
args[6]
.parse::<u64>()
.map_err(|_| "invalid AIGER seed".to_string())?,
Path::new(&args[7]),
)?;
Ok(true)
}
"benchmark-aiger-query-reuse" => {
if args.len() != 5 {
return Err("usage: guarded-continuation-checker benchmark-aiger-query-reuse INPUT.aag HORIZONS REPEATS OUTPUT.csv".to_string());
}
benchmark_aiger_query_reuse(
Path::new(&args[1]),
&parse_size_grid(&args[2], "horizon")?,
args[3]
.parse::<usize>()
.map_err(|_| "invalid AIGER reuse repeat count".to_string())?,
Path::new(&args[4]),
)?;
Ok(true)
}
"verify-cq-aiger" => {
if args.len() != 7 {
return Err("usage: guarded-continuation-checker verify-cq-aiger INPUT.aag HORIZON CHECKPOINT NODE_LIMIT OUTPUT.csv SAFETY_RESULT.txt".to_string());
}
verify_cq_aiger(
Path::new(&args[1]),
args[2]
.parse::<usize>()
.map_err(|_| "invalid AIGER horizon".to_string())?
.max(1),
args[3]
.parse::<usize>()
.map_err(|_| "invalid AIGER checkpoint".to_string())?,
args[4]
.parse::<usize>()
.map_err(|_| "invalid AIGER node limit".to_string())?,
Path::new(&args[5]),
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-continuation-repairs" => {
if args.len() != 8 {
return Err("usage: layered-sat benchmark-continuation-repairs FAMILY VARS RATIO SEED UPDATES QUERIES_PER_UPDATE OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid continuation repair variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid continuation repair ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid continuation repair seed".to_string())?;
let updates = args[5]
.parse::<usize>()
.map_err(|_| "invalid continuation repair update count".to_string())?
.max(1);
let queries = args[6]
.parse::<usize>()
.map_err(|_| "invalid continuation repair query count".to_string())?
.max(1);
benchmark_continuation_repairs(
&args[1],
vars,
ratio,
seed,
updates,
queries,
Path::new(&args[7]),
)?;
Ok(true)
}
"benchmark-continuation-hybrid" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-continuation-hybrid FAMILY VARS RATIO SEED PHASES OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid continuation hybrid variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid continuation hybrid ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid continuation hybrid seed".to_string())?;
let phases = args[5]
.parse::<usize>()
.map_err(|_| "invalid continuation hybrid phase count".to_string())?
.max(1);
benchmark_continuation_hybrid(
&args[1],
vars,
ratio,
seed,
phases,
1,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-continuation-hybrid-stable" => {
if args.len() != 8 {
return Err("usage: layered-sat benchmark-continuation-hybrid-stable FAMILY VARS RATIO SEED PHASES STABLE_SPAN OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid stable hybrid variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid stable hybrid ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid stable hybrid seed".to_string())?;
let phases = args[5]
.parse::<usize>()
.map_err(|_| "invalid stable hybrid phase count".to_string())?
.max(1);
let stable_span = args[6]
.parse::<usize>()
.map_err(|_| "invalid declared stable span".to_string())?
.max(1);
benchmark_continuation_hybrid(
&args[1],
vars,
ratio,
seed,
phases,
stable_span,
Path::new(&args[7]),
)?;
Ok(true)
}
"benchmark-holographic-network-cost" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-holographic-network-cost FAMILY VARS RATIO SEED STRATEGY OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid network-cost variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid network-cost ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid network-cost seed".to_string())?;
let strategy = args[5]
.parse::<usize>()
.map_err(|_| "invalid network-cost strategy".to_string())?;
benchmark_holographic_network_cost(
&args[1],
vars,
ratio,
seed,
strategy,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-holographic-tensor-strategies" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-holographic-tensor-strategies FAMILY VARS RATIO SEED STRATEGIES OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid tensor variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid tensor ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid tensor seed".to_string())?;
let strategies = args[5]
.parse::<usize>()
.map_err(|_| "invalid tensor strategy count".to_string())?
.max(1);
benchmark_holographic_tensor_strategies(
&args[1],
vars,
ratio,
seed,
strategies,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-affine-basis-strategies" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-affine-basis-strategies FAMILY VARS RATIO SEED STRATEGIES OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid affine variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid affine ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid affine seed".to_string())?;
let strategies = args[5]
.parse::<usize>()
.map_err(|_| "invalid affine strategy count".to_string())?
.max(1);
benchmark_affine_basis_strategies(
&args[1],
vars,
ratio,
seed,
strategies,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-finite-domain-groupings" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-finite-domain-groupings FAMILY VARS RATIO SEED STRATEGIES OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid grouping variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid grouping ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid grouping seed".to_string())?;
let strategies = args[5]
.parse::<usize>()
.map_err(|_| "invalid grouping strategy count".to_string())?
.max(1);
benchmark_finite_domain_groupings(
&args[1],
vars,
ratio,
seed,
strategies,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-direct-bdd-network-expansion" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-direct-bdd-network-expansion FAMILY VARS RATIO SEED RANDOM-ORDERS OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid direct network variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid direct network ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid direct network seed".to_string())?;
let random_orders = args[5]
.parse::<usize>()
.map_err(|_| "invalid direct network random order count".to_string())?;
benchmark_direct_bdd_network_expansion(
&args[1],
vars,
ratio,
seed,
random_orders,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-bdd-network-expansion" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-bdd-network-expansion FAMILY VARS RATIO SEED RANDOM-ORDERS OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid network variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid network ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid network seed".to_string())?;
let random_orders = args[5]
.parse::<usize>()
.map_err(|_| "invalid network random order count".to_string())?;
benchmark_bdd_network_expansion(
&args[1],
vars,
ratio,
seed,
random_orders,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-frontier-width-strategies" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-frontier-width-strategies FAMILY VARS RATIO SEED RANDOM-ORDERS OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid frontier variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid frontier ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid frontier seed".to_string())?;
let random_orders = args[5]
.parse::<usize>()
.map_err(|_| "invalid random order count".to_string())?;
benchmark_frontier_width_strategies(
&args[1],
vars,
ratio,
seed,
random_orders,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-frozen-width-strategy" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-frozen-width-strategy FAMILY VARS RATIO START-SEED TRIALS OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid frozen variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid frozen ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid frozen seed".to_string())?;
let trials = args[5]
.parse::<usize>()
.map_err(|_| "invalid frozen trial count".to_string())?
.max(1);
benchmark_frozen_width_strategy(
&args[1],
vars,
ratio,
seed,
trials,
Path::new(&args[6]),
)?;
Ok(true)
}
"benchmark-width-strategies" => {
if args.len() != 6 {
return Err("usage: layered-sat benchmark-width-strategies FAMILY VARS RATIO SEED OUTPUT.csv".to_string());
}
let vars = args[2]
.parse::<usize>()
.map_err(|_| "invalid strategy variable count".to_string())?;
let ratio = args[3]
.parse::<usize>()
.map_err(|_| "invalid strategy ratio".to_string())?;
let seed = args[4]
.parse::<u64>()
.map_err(|_| "invalid strategy seed".to_string())?;
benchmark_width_strategy_search(&args[1], vars, ratio, seed, Path::new(&args[5]))?;
Ok(true)
}
"benchmark-query-calibrated" => {
if args.len() != 7 {
return Err("usage: layered-sat benchmark-query-calibrated INPUT.cnf OUTPUT.csv CALIBRATION-QUERIES EVALUATION-QUERIES DEADLINE-MS GATE".to_string());
}
let calibration = args[3]
.parse::<usize>()
.map_err(|_| "invalid calibration query count".to_string())?
.max(1);
let evaluation = args[4]
.parse::<usize>()
.map_err(|_| "invalid evaluation query count".to_string())?
.max(1);
let deadline_ms = args[5]
.parse::<u64>()
.map_err(|_| "invalid calibrated deadline".to_string())?
.max(1);
benchmark_query_calibrated(
Path::new(&args[1]),
Path::new(&args[2]),
calibration,
evaluation,
std::time::Duration::from_millis(deadline_ms),
&args[6],
)?;
Ok(true)
}
"benchmark-query-portfolio" => {
if args.len() < 5 {
return Err("usage: layered-sat benchmark-query-portfolio INPUT.cnf OUTPUT.csv QUERIES DEADLINE-MS [GATE ...]".to_string());
}
let queries = args[3]
.parse::<usize>()
.map_err(|_| "invalid portfolio query count".to_string())?
.max(1);
let deadline_ms = args[4]
.parse::<u64>()
.map_err(|_| "invalid portfolio deadline".to_string())?
.max(1);
benchmark_query_portfolio(
Path::new(&args[1]),
Path::new(&args[2]),
0,
queries,
queries,
std::time::Duration::from_millis(deadline_ms),
&args[5..],
)?;
Ok(true)
}
"benchmark-query-micro" => {
if args.len() < 5 {
return Err("usage: layered-sat benchmark-query-micro INPUT.cnf OUTPUT.csv QUERIES TIMEOUT-MS [GATE]".to_string());
}
let queries = args[3]
.parse::<usize>()
.map_err(|_| "invalid micro query count".to_string())?
.max(1);
let timeout_ms = args[4]
.parse::<u64>()
.map_err(|_| "invalid micro timeout".to_string())?
.max(1);
let gate = args.get(5).map(String::as_str).unwrap_or("all");
let indexed_gate = gate
.strip_prefix("balanced-only-")
.or_else(|| gate.strip_prefix("balanced-prefix-"))
.is_some_and(|value| value.parse::<usize>().is_ok());
if !matches!(
gate,
"all" | "balanced" | "strict" | "none" | "learned" | "topology"
) && !indexed_gate
{
return Err(format!("unknown solver gate: {gate}"));
}
benchmark_query_race(
Path::new(&args[1]),
Path::new(&args[2]),
queries,
std::time::Duration::from_millis(timeout_ms),
gate,
)?;
Ok(true)
}
"export-query-candidate-features" => {
if args.len() < 4 {
return Err("usage: layered-sat export-query-candidate-features INPUT.cnf OUTPUT.csv SIGNED-LITERAL ...".to_string());
}
let queries = args[3..]
.iter()
.map(|value| {
let literal: isize = value
.parse()
.map_err(|_| format!("invalid signed literal: {value}"))?;
if literal == 0 {
return Err("literal zero is not a query".to_string());
}
Ok((literal.unsigned_abs() - 1, literal > 0))
})
.collect::<Result<Vec<_>, String>>()?;
export_query_candidate_features(Path::new(&args[1]), Path::new(&args[2]), &queries)?;
Ok(true)
}
"export-balanced-candidates" => {
if args.len() != 3 {
return Err(
"usage: layered-sat export-balanced-candidates INPUT.cnf OUTPUT.csv"
.to_string(),
);
}
export_balanced_candidates(Path::new(&args[1]), Path::new(&args[2]))?;
Ok(true)
}
"benchmark-query-race" => {
if args.len() < 3 {
return Err("usage: layered-sat benchmark-query-race INPUT.cnf OUTPUT.csv [queries] [timeout-seconds] [all|balanced|strict|none|learned|topology|balanced-only-N|balanced-prefix-N]".to_string());
}
let queries = args
.get(3)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize)
.max(1);
let timeout_seconds = args
.get(4)
.and_then(|value| value.parse().ok())
.unwrap_or(10u64)
.max(1);
let gate = args.get(5).map(String::as_str).unwrap_or("all");
let indexed_gate = gate
.strip_prefix("balanced-only-")
.or_else(|| gate.strip_prefix("balanced-prefix-"))
.is_some_and(|value| value.parse::<usize>().is_ok());
if !matches!(
gate,
"all" | "balanced" | "strict" | "none" | "learned" | "topology"
) && !indexed_gate
{
return Err(format!("unknown solver gate: {gate}"));
}
benchmark_query_race(
Path::new(&args[1]),
Path::new(&args[2]),
queries,
std::time::Duration::from_secs(timeout_seconds),
gate,
)?;
Ok(true)
}
"query-race-worker" => {
if args.len() != 6 {
return Err("internal query-race-worker usage error".to_string());
}
let variable = args[3]
.parse()
.map_err(|_| "invalid worker variable".to_string())?;
let value = args[4]
.parse()
.map_err(|_| "invalid worker value".to_string())?;
query_race_worker(
&args[1],
Path::new(&args[2]),
variable,
value,
Path::new(&args[5]),
)?;
Ok(true)
}
"profile-corpus" => {
if args.len() < 3 {
return Err(
"usage: layered-sat profile-corpus INPUT_DIR OUTPUT.csv [timeout-seconds]"
.to_string(),
);
}
let timeout_seconds = args
.get(3)
.and_then(|value| value.parse().ok())
.unwrap_or(10u64)
.max(1);
profile_corpus_isolated(Path::new(&args[1]), Path::new(&args[2]), timeout_seconds)?;
Ok(true)
}
"profile-single" => {
if args.len() != 3 {
return Err("internal profile-single usage error".to_string());
}
profile_single_formula(Path::new(&args[1]), Path::new(&args[2]))?;
Ok(true)
}
"benchmark-corpus" => {
if args.len() < 3 {
return Err(
"usage: layered-sat benchmark-corpus INPUT_DIR OUTPUT.csv [queries]"
.to_string(),
);
}
let queries = args
.get(3)
.and_then(|value| value.parse().ok())
.unwrap_or(100_000usize)
.max(1);
let timeout_seconds = args
.get(4)
.and_then(|value| value.parse().ok())
.unwrap_or(600u64)
.max(1);
benchmark_corpus_isolated(
Path::new(&args[1]),
Path::new(&args[2]),
queries,
timeout_seconds,
)?;
Ok(true)
}
"benchmark-single" => {
if args.len() != 4 {
return Err("internal benchmark-single usage error".to_string());
}
let queries = args[3]
.parse()
.map_err(|_| "invalid benchmark query count".to_string())?;
benchmark_corpus_inner(Path::new(&args[1]), Path::new(&args[2]), queries)?;
Ok(true)
}
"compile" => {
if args.len() < 3 {
return Err("usage: layered-sat compile INPUT.cnf OUTPUT.lsat [branch-cap] [node-limit] [time-ms]".to_string());
}
let branch_cap = args
.get(3)
.and_then(|value| value.parse().ok())
.unwrap_or(64usize)
.min(64);
let node_limit = args
.get(4)
.and_then(|value| value.parse().ok())
.unwrap_or(100_000usize);
let time_ms = args
.get(5)
.and_then(|value| value.parse().ok())
.unwrap_or(100u64);
let (vars, clauses) = parse_dimacs(Path::new(&args[1]))?;
let start = Instant::now();
let artifact = compile_safe_artifact(vars, &clauses, branch_cap, node_limit, time_ms);
save_compiled_artifact(Path::new(&args[2]), &artifact)?;
println!(
"compiled vars={} core_vars={} removed={} seeds={} clauses={} core_clauses={} elapsed_ms={:.3}",
vars,
artifact.core_vars,
vars - artifact.core_vars,
artifact.seeds.len(),
clauses.len(),
artifact.core_clauses.len(),
start.elapsed().as_secs_f64() * 1000.0
);
Ok(true)
}
"inspect" => {
if args.len() != 2 {
return Err("usage: layered-sat inspect MODEL.lsat".to_string());
}
let artifact = load_compiled_artifact(Path::new(&args[1]))?;
println!(
"original_vars={} core_vars={} removed={} removed_fraction={:.6} original_clauses={} core_clauses={} seeds={} bdd_nodes={}",
artifact.original_vars,
artifact.core_vars,
artifact.original_vars - artifact.core_vars,
(artifact.original_vars - artifact.core_vars) as f64
/ artifact.original_vars.max(1) as f64,
artifact.original_clauses.len(),
artifact.core_clauses.len(),
artifact.seeds.len(),
artifact
.seeds
.iter()
.map(|seed| seed.manager.nodes.len())
.sum::<usize>()
);
print!("retained_original_variables=");
for (index, variable) in artifact.core_to_original.iter().enumerate() {
if index > 0 {
print!(",");
}
print!("{}", variable + 1);
}
println!();
Ok(true)
}
"query" => {
if args.len() < 2 {
return Err("usage: layered-sat query MODEL.lsat [SIGNED-LITERAL ...]".to_string());
}
let artifact = load_compiled_artifact(Path::new(&args[1]))?;
let assumptions = args[2..]
.iter()
.map(|value| {
let literal: isize = value
.parse()
.map_err(|_| format!("invalid signed literal: {value}"))?;
if literal == 0 {
return Err("literal zero is not an assumption".to_string());
}
Ok((literal.unsigned_abs() - 1, literal > 0))
})
.collect::<Result<Vec<_>, String>>()?;
match query_compiled_artifact(&artifact, &assumptions)? {
None => println!("s UNSATISFIABLE"),
Some(assignment) => {
println!("s SATISFIABLE");
print!("v");
for (variable, value) in assignment.iter().enumerate() {
let literal = variable as isize + 1;
print!(" {}", if *value { literal } else { -literal });
}
println!(" 0");
}
}
Ok(true)
}
_ => Ok(false),
}
}
fn parallel_map<T: Sync, R: Send, F: Fn(&T) -> R + Sync>(items: &[T], function: F) -> Vec<R> {
let workers = std::thread::available_parallelism()
.map(|count| count.get())
.unwrap_or(1)
.min(items.len().max(1));
let chunk_size = items.len().div_ceil(workers);
std::thread::scope(|scope| {
let handles: Vec<_> = items
.chunks(chunk_size.max(1))
.map(|chunk| {
let function = &function;
scope.spawn(move || chunk.iter().map(function).collect::<Vec<_>>())
})
.collect();
handles
.into_iter()
.flat_map(|handle| handle.join().expect("experiment worker panicked"))
.collect()
})
}
fn encode_cached_formula(record: &CachedHelperFormula) -> String {
let clauses = record
.clauses
.iter()
.map(|clause| {
clause
.0
.iter()
.map(|&(variable, sign)| format!("{variable}{}", if sign { '+' } else { '-' }))
.collect::<Vec<_>>()
.join(",")
})
.collect::<Vec<_>>()
.join(";");
format!(
"{}|{}|{}|{}|{}|{}|{}|{}",
record.vars,
record.ratio,
record.family,
record.seed,
record.aligned_nodes,
record.order_nodes,
record.helper_nodes,
clauses
)
}
fn decode_cached_formula(line: &str) -> Option<CachedHelperFormula> {
let mut fields = line.splitn(8, '|');
let vars = fields.next()?.parse().ok()?;
let ratio = fields.next()?.parse().ok()?;
let family = fields.next()?.to_string();
let seed = fields.next()?.parse().ok()?;
let aligned_nodes = fields.next()?.parse().ok()?;
let order_nodes = fields.next()?.parse().ok()?;
let helper_nodes = fields.next()?.parse().ok()?;
let clauses = fields
.next()?
.split(';')
.filter(|clause| !clause.is_empty())
.map(|clause| {
Clause(
clause
.split(',')
.map(|literal| {
let (variable, sign) = literal.split_at(literal.len() - 1);
Some((variable.parse().ok()?, sign == "+"))
})
.collect::<Option<Vec<_>>>()?,
)
.into()
})
.collect::<Option<Vec<_>>>()?;
Some(CachedHelperFormula {
vars,
ratio,
family,
seed,
clauses,
aligned_nodes,
order_nodes,
helper_nodes,
})
}
fn helper_cache_path(vars: usize, trials: usize, budget: usize) -> PathBuf {
PathBuf::from("target/experiment-cache")
.join(format!("helper-v1-n{vars}-t{trials}-b{budget}.txt"))
}
fn load_or_build_helper_cache(
vars: usize,
trials: usize,
budget: usize,
) -> (Vec<CachedHelperFormula>, bool, u128) {
let start = Instant::now();
let path = helper_cache_path(vars, trials, budget);
if let Ok(contents) = fs::read_to_string(&path) {
let records: Vec<_> = contents.lines().filter_map(decode_cached_formula).collect();
if records.len() == 30 * trials {
return (records, true, start.elapsed().as_micros());
}
}
let sizes = [vars.saturating_sub(6).max(6), vars, vars + 6];
let mut tasks = Vec::new();
for training_vars in sizes {
for ratio in 2..=6 {
for family in ["random", "banded"] {
for training_seed in 1..=trials {
tasks.push((training_vars, ratio, family, training_seed));
}
}
}
}
let records = parallel_map(&tasks, |&(formula_vars, ratio, family, training_seed)| {
let seed = training_seed as u64
+ formula_vars as u64 * 1_000
+ ratio as u64 * 100
+ u64::from(family == "banded") * 50_000;
let clauses = generate_formula(family, formula_vars, ratio, seed);
let order = min_fill_order(formula_vars, &clauses);
let aligned = eliminate_with_bdds_ordered(formula_vars, &clauses, &order, &order);
let rule = ScentRule {
length: 9,
hops: 4,
strongest_first: true,
};
let scent_order = apply_scent_rule(formula_vars, &clauses, &order, rule);
let order_result =
eliminate_with_bdds_ordered(formula_vars, &clauses, &order, &scent_order);
let (expanded_vars, expanded, _, _) = scent_batch_expand(formula_vars, &clauses, budget, 4);
let expanded_elimination = min_fill_order(expanded_vars, &expanded);
let expanded_order =
apply_scent_rule(expanded_vars, &expanded, &expanded_elimination, rule);
let helper_result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&expanded_elimination,
&expanded_order,
);
CachedHelperFormula {
vars: formula_vars,
ratio,
family: family.to_string(),
seed,
clauses,
aligned_nodes: aligned.allocated_nodes,
order_nodes: order_result.allocated_nodes,
helper_nodes: helper_result.allocated_nodes,
}
});
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create experiment cache directory");
}
let body = records
.iter()
.map(encode_cached_formula)
.collect::<Vec<_>>()
.join("\n");
fs::write(&path, body).expect("write experiment cache");
(records, false, start.elapsed().as_micros())
}
fn main() {
let args: Vec<_> = env::args().collect();
match run_firmware_gate_cli(&args[1..]) {
Ok(Some(true)) => return,
Ok(Some(false)) => std::process::exit(1),
Ok(None) => {}
Err(error) => {
eprintln!("error: {error}");
std::process::exit(2);
}
}
if cfg!(feature = "production-firmware") {
eprintln!("error: command is outside production support profile firmware-rtl-v1");
std::process::exit(2);
}
match compiled_mmio_file::run_compiled_mmio_file_cli(&args[1..]) {
Ok(true) => return,
Ok(false) => {}
Err(error) => {
eprintln!("error: {error}");
std::process::exit(2);
}
}
match run_artifact_cli(&args[1..]) {
Ok(true) => return,
Ok(false) => {}
Err(error) => {
eprintln!("error: {error}");
std::process::exit(
if error.starts_with("controller-plant-resource refusal=")
|| error.starts_with("controller-proof-mtbdd-resource refusal=")
|| error.starts_with("controller-split-resource refusal=")
|| error.starts_with("btor2-channel-property-resource refusal=")
|| error.starts_with("btor2-channel-trace-resource refusal=")
|| error.starts_with("btor2-channel-pair-trace-resource refusal=")
{
3
} else {
2
},
);
}
}
let vars = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(16);
let ratio: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(4);
let trials: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(20);
let family = args.get(4).map(String::as_str).unwrap_or("random");
let order_name = args.get(5).map(String::as_str).unwrap_or("min-fill");
let engine = args.get(6).map(String::as_str).unwrap_or("compare");
let optional_budget = args.get(7).and_then(|value| value.parse().ok());
let helper_budget = optional_budget.unwrap_or(vars / 2);
let sift_passes = optional_budget.unwrap_or(4);
let sift_trials = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(usize::MAX);
assert!(vars > 0, "vars must be positive");
if engine == "compare" {
assert!(
vars < usize::BITS as usize,
"compare mode requires vars to fit a bit mask"
);
}
if engine == "bdd-only" {
println!(
"family,order,vars,clauses,seed,sat,bdd_solver_us,bdd_allocated_nodes,witness_valid"
);
} else if engine == "expand-bdd" {
println!(
"family,order,vars,clauses,seed,helpers,expanded_vars,expanded_clauses,original_us,expanded_us,original_nodes,expanded_nodes,node_ratio,sat_equivalent,projected_witness_valid"
);
} else if engine == "greedy-expand-bdd" || engine == "aligned-greedy-expand" {
println!(
"family,order,vars,clauses,seed,budget,accepted,recursive_accepted,candidates_tested,beneficial_trials,expanded_vars,expanded_clauses,original_nodes,greedy_nodes,node_ratio,original_us,final_us,search_us,sat_equivalent,projected_witness_valid"
);
} else if engine == "feedback-expand-bdd" || engine == "shortlist-expand-bdd" {
println!(
"family,order,vars,clauses,seed,budget,accepted,recursive_accepted,candidates_tested,beneficial_trials,expanded_vars,expanded_clauses,original_nodes,feedback_nodes,node_ratio,original_us,final_us,search_us,sat_equivalent,projected_witness_valid"
);
} else if engine == "bdd-frontier-expand" {
println!(
"family,order,vars,clauses,seed,budget,accepted,recursive_accepted,candidates_tested,beneficial_trials,expanded_vars,expanded_clauses,original_nodes,frontier_nodes,node_ratio,original_us,final_us,search_us,sat_equivalent,projected_witness_valid"
);
} else if engine == "bdd-order-sweep" {
println!(
"family,elimination_order,bdd_order,vars,clauses,seed,sat,bdd_us,allocated_nodes,witness_valid"
);
} else if engine == "music-order-sweep" {
println!(
"family,elimination_order,musical_characteristic,vars,clauses,seed,sat,bdd_us,allocated_nodes,node_ratio_vs_aligned,witness_valid"
);
} else if engine == "learned-phrase" {
println!(
"family,vars,clauses,training_trials,test_seed,phrase_length,rule,training_mean_ratio,test_nodes,aligned_nodes,test_ratio,witness_valid"
);
} else if engine == "learned-scent" {
println!(
"family,vars,clauses,training_trials,test_seed,phrase_length,hops,direction,training_mean_ratio,test_nodes,aligned_nodes,test_ratio,witness_valid"
);
} else if engine == "scent-universal" {
println!(
"test_family,train_vars,test_vars,density,test_seed,variant,phrase_length,hops,direction,training_mean_ratio,test_nodes,aligned_nodes,test_ratio,witness_valid"
);
} else if engine == "scent-gate" {
println!(
"family,train_vars,test_vars,density,test_seed,selector,predicted_log_ratio,learned_threshold,oof_ratio,oof_applied,applied,training_samples,gate_us,aligned_nodes,scent_nodes,final_nodes,final_ratio,oracle_applied,witness_valid"
);
} else if engine == "scent-helper" {
println!(
"family,train_vars,test_vars,density,test_seed,selector,gate_applied,predicted_log_ratio,threshold,budget,accepted,candidates_scored,discovery_us,final_us,baseline_nodes,final_nodes,node_ratio,sat_equivalent,witness_valid"
);
} else if engine == "scent-helper-gate" {
println!(
"family,train_vars,test_vars,density,test_seed,selector,order_gate,helper_prediction,helper_threshold,helper_oof_ratio,helper_oof_applied,helper_applied,budget,accepted,feature_us,baseline_nodes,order_nodes,helper_nodes,final_nodes,final_ratio,witness_valid"
);
} else if engine == "helper-graph-memory" {
println!(
"family,train_vars,test_vars,density,test_seed,selector,order_gate,prediction,threshold,oof_ratio,oof_applied,helper_applied,budget,feature_us,baseline_nodes,order_nodes,helper_nodes,final_nodes,final_ratio,witness_valid"
);
} else if engine == "learned-message" {
println!(
"family,train_vars,test_vars,density,test_seed,selector,order_gate,self_weight,clause_weight,variable_weight,candidate_weight,memory_retention,prediction,threshold,oof_ratio,oof_applied,helper_applied,budget,feature_us,baseline_nodes,order_nodes,helper_nodes,final_nodes,final_ratio,witness_valid"
);
} else if engine == "math-tricks" {
println!(
"family,vars,input_clauses,seed,output_clauses,tautologies,subsumed,consensus_pairs,passes,preprocess_us,baseline_us,processed_us,baseline_nodes,processed_nodes,node_ratio,sat_equivalent,baseline_witness_valid,processed_witness_valid"
);
} else if engine == "incremental-pinch" {
println!(
"family,vars,clauses,seed,changed_clause,earliest_layer,reused_layers,recomputed_layers,checkpoint_factors,cache_build_us,incremental_us,full_us,speedup,incremental_new_nodes,full_nodes,node_work_ratio,sat_equivalent,incremental_witness_valid,full_witness_valid"
);
} else if engine == "checkpoint-compression" {
println!(
"family,vars,clauses,seed,stride,checkpoints,checkpoint_factors,memory_ratio_vs_dense,earliest_layer,restored_layer,replayed_layers,recomputed_layers,cache_build_us,incremental_us,full_us,speedup,new_nodes,full_nodes,node_work_ratio,sat_equivalent,witness_valid"
);
} else if engine == "branch-reuse" {
println!(
"family,vars,clauses,seed,branch_percent,branch_level,branch_variable,stride,checkpoint_factors,false_sat,true_sat,cache_false_us,incremental_true_us,fresh_false_us,fresh_true_us,pair_speedup,sibling_speedup,new_nodes,fresh_true_nodes,node_work_ratio,true_equivalent,false_witness_valid,true_witness_valid"
);
} else if engine == "joint-branch" {
println!(
"family,vars,clauses,test_seed,selector,learned_alpha,branch_level,branch_variable,branches,sat,reuse_us,fresh_us,time_speedup,reuse_nodes,fresh_nodes,node_ratio,valid"
);
} else if engine == "supervised-branch" {
println!(
"family,vars,clauses,test_seed,selector,branch_level,branch_variable,predicted_log_work,inference_us,branches,sat,reuse_us,fresh_us,time_speedup,reuse_nodes,oracle_nodes,work_vs_oracle,fresh_nodes,node_ratio,valid"
);
} else if engine == "branch-portfolio"
|| engine == "cheap-branch-portfolio"
|| engine == "three-action-portfolio"
|| engine == "portfolio-generalization"
{
println!(
"family,vars,clauses,test_seed,selector,structure_prediction,policy,branch_level,branch_variable,branches,sat,decision_us,solve_us,total_us,direct_us,speed_vs_direct,work_nodes,direct_nodes,work_vs_direct,oracle_nodes,work_vs_oracle,valid"
);
} else if engine == "robust-reuse-portfolio"
|| engine == "external-reuse-portfolio"
|| engine == "external-ablation"
{
println!(
"family,vars,clauses,test_seed,selector,predicted_log_ratio,support_distance,prediction_threshold,distance_threshold,policy,branches,sat,decision_us,solve_us,total_us,direct_us,speed_vs_direct,work_nodes,direct_nodes,work_vs_direct,valid"
);
} else if engine == "replay-metrics" {
println!(
"family,vars,clauses,test_seed,sat,first_sat,branches,direct_us,reuse_us,speedup,cache_build_us,sibling_us,cache_nodes,sibling_new_nodes,checkpoints,restored_layer,replayed_layers,direct_nodes,reuse_nodes,node_ratio,valid"
);
} else if engine == "direct-provenance-portfolio" {
println!(
"family,vars,clauses,test_seed,selector,predicted_log_ratio,policy,sat,decision_us,solve_us,total_us,direct_us,speed_vs_direct,nodes,direct_nodes,node_ratio,valid"
);
} else if engine == "direct-layout-ablation" {
println!(
"family,vars,clauses,test_seed,selector,solve_us,direct_us,speed_vs_direct,nodes,direct_nodes,node_ratio,sat,valid"
);
} else if engine == "kernel-scaling" {
println!("family,vars,clauses,seed,kernel,solve_us,nodes,sat,valid");
} else if engine == "structure-scaling" {
println!("family,vars,clauses,seed,min_fill_width,estimated_work,solve_us,nodes,sat,valid");
} else if engine == "helper-width-scaling" {
println!(
"family,original_vars,original_clauses,seed,variant,budget,accepted,expanded_vars,expanded_clauses,min_fill_width,estimated_work,solve_us,nodes,sat,sat_equivalent,witness_valid"
);
} else if engine == "exact-width-helper" {
println!(
"family,vars,clauses,seed,selector,candidates,helper_added,original_treewidth,final_treewidth,width_change,original_nodes,final_nodes,node_ratio,sat_equivalent,witness_valid"
);
} else if engine == "coordinated-width-helper" {
println!(
"family,vars,clauses,seed,selector,first_candidates,pairs_tested,helpers_added,original_treewidth,final_treewidth,width_change,original_nodes,final_nodes,node_ratio,sat_equivalent,witness_valid"
);
} else if engine == "shake-inverse-width" {
println!(
"family,vars,clauses,seed,selector,leaf_richness,gate_applied,inverse_depth,removed,core_vars,core_clauses,probes,inverse_forced,original_treewidth,core_treewidth,width_change,original_nodes,core_nodes,node_ratio,sat,sat_equivalent,reconstruction_valid"
);
} else if engine == "seed-branch-width" {
println!(
"family,vars,clauses,seed,boundary,interior,local_clauses,summary_clauses,recipe_entries,recipe_bits,compilation_trials,original_treewidth,core_treewidth,width_change,original_nodes,core_nodes,node_ratio,sat,sat_equivalent,reconstruction_valid"
);
} else if engine == "seed-bdd-width" {
println!(
"family,vars,clauses,seed,seed_order,boundary,interior,local_clauses,summary_clauses,seed_live_nodes,seed_allocated_nodes,compile_us,original_solve_us,core_solve_us,end_to_end_time_ratio,original_treewidth,core_treewidth,width_change,original_nodes,core_nodes,node_ratio,stored_charged_ratio,allocated_charged_ratio,sat,sat_equivalent,reconstruction_valid"
);
} else if engine == "seed-bdd-reuse" {
println!(
"family,vars,clauses,base_seed,update,interior,cold_allocated,warm_new_allocated,allocation_ratio,cold_us,warm_us,time_ratio,shared_manager_nodes,cache_cap,cache_policy,peak_shared_nodes,compactions,evictions,hot_roots,seed_live_nodes,original_treewidth,core_treewidth,width_change,original_nodes,core_nodes,node_ratio,sat_equivalent,reconstruction_valid"
);
} else if engine == "multi-seed-width" {
println!(
"family,vars,clauses,seed,selector,branch_cap,max_seeds,seeds,removed,boundary_sum,seed_live_nodes,seed_allocated_nodes,compile_us,final_vars,final_clauses,original_treewidth,final_treewidth,width_change,original_nodes,final_nodes,node_ratio,stored_charged_ratio,original_solve_us,final_solve_us,end_to_end_time_ratio,sat_equivalent,reconstruction_valid"
);
} else if engine == "incremental-payback" {
println!(
"family,vars,clauses,seed,query,cold_us,incremental_setup_us,incremental_query_us,incremental_cumulative_ratio,seed_compile_us,seed_solve_us,seed_cumulative_ratio,seeds,removed,seed_cache_nodes,cold_sat,incremental_agrees,seed_agrees,incremental_witness_valid,seed_witness_valid"
);
} else if engine == "assumption-payback" {
println!(
"family,vars,clauses,seed,query,assumed_original,assumed_value,cold_ns,incremental_setup_ns,incremental_query_ns,incremental_cumulative_ratio,seed_setup_ns,seed_query_ns,seed_cumulative_ratio,seeds,removed,core_vars,seed_live_nodes,cold_sat,incremental_agrees,seed_agrees,incremental_witness_valid,seed_witness_valid"
);
} else if engine == "deployment-gate" {
println!(
"family,vars,clauses,seed,selector,predicted_log_ratio,threshold,applied,actual_ratio,policy_ratio,seeds,removed,live_nodes,incremental_ns,seeded_ns,training_rows,oof_applied,oof_policy_ratio,sat_valid"
);
} else if engine == "crossover-gate" {
println!(
"family,vars,clauses,seed,selector,predicted_query_ratio,query_margin,predicted_setup_queries,setup_margin,predicted_crossover,query_horizon,applied,actual_query_ratio,actual_crossover,actual_ratio,policy_ratio,seeds,removed,incremental_setup_ns,seeded_setup_ns,incremental_query_ns,seeded_query_ns,training_rows,sat_valid"
);
} else if engine == "speculative-compile" {
println!(
"family,vars,clauses,seed,selector,predicted_query_ratio,query_margin,budget_ns,compile_ns,calibration_ns,aborted,deployed,seeds,removed,incremental_ns,seeded_ns,policy_ns,policy_ratio,actual_query_ratio,training_rows,sat_valid"
);
} else if engine == "offline-scaling" {
println!(
"family,vars,clauses,seed,horizon,seeds,removed,core_vars,seed_live_nodes,offline_compile_ns,incremental_setup_ns,incremental_query_ns,seeded_query_ns,online_query_ratio,amortized_ratio,incremental_qps,seeded_qps,reconstruction_ns_per_sample,reconstruction_samples,crossover_queries,sat_valid"
);
} else if engine == "batch-branch-discovery" {
println!(
"family,vars,clauses,seed,branch_cap,candidates,removed,removed_fraction,boundary_sum,local_clause_touches,discovery_us,target_met,integrity_valid"
);
} else if engine == "batch-seed-compile" {
println!(
"family,vars,clauses,seed,branch_cap,candidates,removed,removed_fraction,discovery_us,compile_us,seed_live_nodes,seed_allocated_nodes,core_vars,core_clauses,original_sat,core_sat,sat_equivalent,reconstruction_valid,target_met"
);
} else if engine == "batch-offline-service" {
println!(
"family,vars,clauses,seed,horizon,branch_cap,seeds,removed,removed_fraction,core_vars,discovery_ns,compile_ns,incremental_setup_ns,seeded_solver_setup_ns,incremental_query_ns,seeded_query_ns,online_query_ratio,amortized_ratio,crossover_queries,incremental_qps,seeded_qps,seed_live_nodes,reconstruction_ns,reconstruction_samples,sat_valid"
);
} else if engine == "renaming-control" {
println!(
"family,vars,clauses,seed,renaming,branch_cap,candidates,removed,removed_fraction,discovery_us,compile_us,seed_live_nodes,seed_allocated_nodes,core_vars,sat_equivalent,reconstruction_valid,target_met"
);
} else if engine == "safe-compiler-control" {
println!(
"family,layout,vars,clauses,seed,branch_cap,node_limit,time_limit_ms,candidates,accepted,node_rejected,time_rejected,removed,removed_fraction,discovery_us,compile_us,attempt_nodes,seed_live_nodes,core_vars,sat_equivalent,reconstruction_valid,target_met"
);
} else if engine == "bdd-sift" || engine == "bdd-sift-control" {
println!(
"family,elimination_order,start_order,vars,clauses,seed,passes,swaps_tested,swaps_accepted,start_nodes,final_nodes,node_ratio,start_live,final_live,live_ratio,search_us,final_us,total_vs_start_solves,witness_valid"
);
} else if engine == "flower-ring-states" {
println!(
"family,vars,clauses,seed,direction,ring,processed,residual_states,best_possible_c6_orbits,total_bdd_nodes"
);
} else if engine == "joint-predict" {
println!(
"family,vars,clauses,seed,selector,budget,accepted,recursive_accepted,candidates_scored,natural_nodes,aligned_nodes,final_nodes,final_vs_natural,final_vs_aligned,predict_us,final_us,initial_width,final_width,estimated_work_ratio,sat_equivalent,witness_valid"
);
} else if engine == "learned-joint" {
println!(
"family,vars,clauses,test_seed,selector,budget,accepted,training_samples,training_us,inference_us,final_us,natural_nodes,aligned_nodes,final_nodes,final_vs_natural,final_vs_aligned,sat_equivalent,witness_valid"
);
} else {
assert_eq!(
engine, "compare",
"engine must be compare, bdd-only, expand-bdd, greedy-expand-bdd, aligned-greedy-expand, feedback-expand-bdd, shortlist-expand-bdd, bdd-frontier-expand, bdd-order-sweep, music-order-sweep, learned-phrase, learned-scent, scent-universal, scent-gate, scent-helper, scent-helper-gate, helper-graph-memory, learned-message, math-tricks, incremental-pinch, checkpoint-compression, branch-reuse, joint-branch, supervised-branch, branch-portfolio, cheap-branch-portfolio, three-action-portfolio, portfolio-generalization, robust-reuse-portfolio, external-reuse-portfolio, external-ablation, replay-metrics, direct-provenance-portfolio, direct-layout-ablation, kernel-scaling, structure-scaling, helper-width-scaling, exact-width-helper, coordinated-width-helper, shake-inverse-width, seed-branch-width, seed-bdd-width, seed-bdd-reuse, multi-seed-width, incremental-payback, assumption-payback, deployment-gate, crossover-gate, speculative-compile, offline-scaling, batch-branch-discovery, batch-seed-compile, bdd-sift, bdd-sift-control, flower-ring-states, joint-predict, or learned-joint"
);
println!(
"family,order,vars,clauses,seed,sat,peak_boundary,peak_entries,stored_entries,peak_bdd_nodes,stored_bdd_nodes,bdd_ratio,layered_us,bdd_solver_us,bdd_allocated_nodes,bdd_agrees,brute_us,agrees"
);
}
let mut histogram = HashMap::new();
if engine == "learned-joint" {
assert_eq!(
family, "random",
"learned-joint currently trains on random 3-SAT"
);
let training_start = Instant::now();
let mut samples = Vec::new();
let mut formula_training = Vec::new();
for training_seed in 1..=trials {
let formula = random_3sat(vars, vars * ratio, training_seed as u64);
let order = min_fill_order(vars, &formula);
let baseline = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
for (pair, frequency) in recurring_pair_candidates(&formula).into_iter().take(12) {
let Some(candidate) = add_pair_helper(vars, &formula, pair) else {
continue;
};
let candidate_order = min_fill_order(vars + 1, &candidate);
let candidate_result = eliminate_with_bdds_ordered(
vars + 1,
&candidate,
&candidate_order,
&candidate_order,
);
let features = helper_features(
vars,
&formula,
pair,
frequency,
&order,
&baseline.interaction_candidates,
);
let label = candidate_result.allocated_nodes as f64
/ baseline.allocated_nodes.max(1) as f64
- 1.0;
samples.push((features, label));
}
let (frequency_vars, frequency_formula, _) =
expand_recurring_pairs(vars, &formula, helper_budget);
let frequency_order = min_fill_order(frequency_vars, &frequency_formula);
let frequency_result = eliminate_with_bdds_ordered(
frequency_vars,
&frequency_formula,
&frequency_order,
&frequency_order,
);
formula_training.push((
formula_features(vars, &formula, &baseline),
frequency_result.allocated_nodes as f64 / baseline.allocated_nodes.max(1) as f64
- 1.0,
));
}
let weights = fit_ridge(&samples, 0.1);
let mut ranked_training: Vec<_> = samples
.iter()
.map(|(features, label)| (predict(&weights, features), *label))
.collect();
ranked_training.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut cumulative = 0.0;
let mut best_cumulative = 0.0;
let mut learned_threshold = f64::NEG_INFINITY;
for (prediction, label) in ranked_training {
cumulative += label;
if cumulative < best_cumulative {
best_cumulative = cumulative;
learned_threshold = prediction + f64::EPSILON;
}
}
let training_us = training_start.elapsed().as_micros();
for offset in 1..=trials {
let test_seed = trials + offset;
let formula = random_3sat(vars, vars * ratio, test_seed as u64);
let order = min_fill_order(vars, &formula);
let natural = eliminate_with_bdds(vars, &formula, &order);
let aligned = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
for selector in ["gated", "learned", "knn", "frequency", "oracle"] {
let inference_start = Instant::now();
let (expanded_vars, expanded, accepted) = match selector {
"gated" => {
let features = formula_features(vars, &formula, &aligned);
if formula_knn_prediction(&formula_training, &features, 5) < 0.0 {
expand_recurring_pairs(vars, &formula, helper_budget)
} else {
(vars, formula.clone(), 0)
}
}
"learned" => {
let (v, f, accepted, _) = learned_batch_expand(
vars,
&formula,
helper_budget,
&weights,
&aligned,
learned_threshold,
);
(v, f, accepted)
}
"knn" => {
let (v, f, accepted, _) =
knn_batch_expand(vars, &formula, helper_budget, &samples, &aligned, 7);
(v, f, accepted)
}
"frequency" => expand_recurring_pairs(vars, &formula, helper_budget),
_ => {
let oracle = greedy_expand(
vars,
&formula,
helper_budget,
12,
"min-fill",
test_seed as u64,
true,
);
(oracle.vars, oracle.clauses, oracle.accepted)
}
};
let inference_us = inference_start.elapsed().as_micros();
let final_order = min_fill_order(expanded_vars, &expanded);
let final_start = Instant::now();
let result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&final_order,
&final_order,
);
let final_us = final_start.elapsed().as_micros();
let equivalent = result.assignment.is_some() == natural.assignment.is_some();
let valid = result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&formula, &assignment[..vars]) && satisfies(&expanded, assignment)
});
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{:.6},{},{}",
family,
vars,
formula.len(),
test_seed,
selector,
helper_budget,
accepted,
samples.len(),
training_us,
inference_us,
final_us,
natural.allocated_nodes,
aligned.allocated_nodes,
result.allocated_nodes,
result.allocated_nodes as f64 / natural.allocated_nodes.max(1) as f64,
result.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64,
equivalent,
valid
);
}
}
return;
}
if engine == "learned-phrase" {
let rules = phrase_rules();
let mut log_ratios = vec![0.0f64; rules.len()];
for training_seed in 1..=trials {
let formula = generate_formula(family, vars, ratio, training_seed as u64);
let order = min_fill_order(vars, &formula);
let aligned = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
for (index, &rule) in rules.iter().enumerate() {
let candidate_order = apply_phrase_rule(vars, &formula, &order, rule);
let candidate =
eliminate_with_bdds_ordered(vars, &formula, &order, &candidate_order);
let ratio =
candidate.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64;
log_ratios[index] += ratio.ln();
}
}
let (best_index, &best_log_sum) = log_ratios
.iter()
.enumerate()
.min_by(|a, b| a.1.total_cmp(b.1))
.unwrap();
let selected = rules[best_index];
let training_mean = (best_log_sum / trials.max(1) as f64).exp();
for test_index in 1..=trials {
let test_seed = 10_000 + test_index as u64;
let formula = generate_formula(family, vars, ratio, test_seed);
let order = min_fill_order(vars, &formula);
let aligned = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
let candidate_order = apply_phrase_rule(vars, &formula, &order, selected);
let candidate = eliminate_with_bdds_ordered(vars, &formula, &order, &candidate_order);
let valid = candidate
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&formula, assignment));
println!(
"{},{},{},{},{},{},{},{:.6},{},{},{:.6},{}",
family,
vars,
formula.len(),
trials,
test_seed,
selected.length,
phrase_rule_name(selected),
training_mean,
candidate.allocated_nodes,
aligned.allocated_nodes,
candidate.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64,
valid
);
}
return;
}
if engine == "learned-scent" {
let rules = scent_rules();
let mut log_ratios = vec![0.0f64; rules.len()];
for training_seed in 1..=trials {
let formula = generate_formula(family, vars, ratio, training_seed as u64);
let order = min_fill_order(vars, &formula);
let aligned = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
for (index, &rule) in rules.iter().enumerate() {
let candidate_order = apply_scent_rule(vars, &formula, &order, rule);
let candidate =
eliminate_with_bdds_ordered(vars, &formula, &order, &candidate_order);
let ratio =
candidate.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64;
log_ratios[index] += ratio.ln();
}
}
let (best_index, &best_log_sum) = log_ratios
.iter()
.enumerate()
.min_by(|a, b| a.1.total_cmp(b.1))
.unwrap();
let selected = rules[best_index];
let training_mean = (best_log_sum / trials.max(1) as f64).exp();
for test_index in 1..=trials {
let test_seed = 10_000 + test_index as u64;
let formula = generate_formula(family, vars, ratio, test_seed);
let order = min_fill_order(vars, &formula);
let aligned = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
let candidate_order = apply_scent_rule(vars, &formula, &order, selected);
let candidate = eliminate_with_bdds_ordered(vars, &formula, &order, &candidate_order);
let valid = candidate
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&formula, assignment));
let direction = if selected.hops == 0 {
"no-op"
} else if selected.strongest_first {
"strongest-first"
} else {
"weakest-first"
};
println!(
"{},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{}",
family,
vars,
formula.len(),
trials,
test_seed,
selected.length,
selected.hops,
direction,
training_mean,
candidate.allocated_nodes,
aligned.allocated_nodes,
candidate.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64,
valid
);
}
return;
}
if engine == "scent-universal" {
let rules = scent_rules();
let mut log_ratios = vec![0.0f64; rules.len()];
let mut training_formulas = 0usize;
for training_ratio in 3..=5 {
for training_seed in 1..=trials {
let formula = random_3sat(vars, vars * training_ratio, training_seed as u64);
let order = min_fill_order(vars, &formula);
let aligned = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
for (index, &rule) in rules.iter().enumerate() {
let candidate_order = apply_scent_rule(vars, &formula, &order, rule);
let candidate =
eliminate_with_bdds_ordered(vars, &formula, &order, &candidate_order);
let node_ratio =
candidate.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64;
log_ratios[index] += node_ratio.ln();
}
training_formulas += 1;
}
}
let (best_index, &best_log_sum) = log_ratios
.iter()
.enumerate()
.min_by(|a, b| a.1.total_cmp(b.1))
.unwrap();
let selected = rules[best_index];
let training_mean = (best_log_sum / training_formulas.max(1) as f64).exp();
let sizes = [vars.saturating_sub(6).max(6), vars, vars + 6];
for &test_vars in &sizes {
for test_ratio in 2..=6 {
for test_family in ["random", "banded"] {
for test_index in 1..=trials {
let test_seed = 10_000
+ test_index as u64
+ test_vars as u64 * 1_000
+ test_ratio as u64 * 100
+ u64::from(test_family == "banded") * 50_000;
let formula =
generate_formula(test_family, test_vars, test_ratio, test_seed);
let order = min_fill_order(test_vars, &formula);
let aligned =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &order);
let variants = [
("full", selected, 0u8),
(
"no-diffusion",
ScentRule {
hops: 0,
..selected
},
0,
),
("degree-only", selected, 1),
("polarity-only", selected, 2),
("random-control", selected, 3),
];
for (variant_name, rule, signal_variant) in variants {
let candidate_order = apply_scent_rule_variant(
test_vars,
&formula,
&order,
rule,
signal_variant,
);
let candidate = eliminate_with_bdds_ordered(
test_vars,
&formula,
&order,
&candidate_order,
);
let valid = candidate
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&formula, assignment));
let direction = if selected.strongest_first {
"strongest-first"
} else {
"weakest-first"
};
println!(
"{},{},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{}",
test_family,
vars,
test_vars,
test_ratio,
test_seed,
variant_name,
selected.length,
rule.hops,
direction,
training_mean,
candidate.allocated_nodes,
aligned.allocated_nodes,
candidate.allocated_nodes as f64
/ aligned.allocated_nodes.max(1) as f64,
valid
);
}
}
}
}
}
return;
}
if engine == "scent-gate" {
let fixed_rule = ScentRule {
length: 9,
hops: 4,
strongest_first: true,
};
let sizes = [vars.saturating_sub(6).max(6), vars, vars + 6];
let mut training = Vec::new();
for &training_vars in &sizes {
for training_ratio in 2..=6 {
for training_family in ["random", "banded"] {
for training_seed in 1..=trials {
let seed = training_seed as u64
+ training_vars as u64 * 1_000
+ training_ratio as u64 * 100
+ u64::from(training_family == "banded") * 50_000;
let formula =
generate_formula(training_family, training_vars, training_ratio, seed);
let order = min_fill_order(training_vars, &formula);
let aligned =
eliminate_with_bdds_ordered(training_vars, &formula, &order, &order);
let scent_order =
apply_scent_rule(training_vars, &formula, &order, fixed_rule);
let scent = eliminate_with_bdds_ordered(
training_vars,
&formula,
&order,
&scent_order,
);
training.push((
scent_gate_features(training_vars, &formula),
(scent.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64)
.ln(),
));
}
}
}
}
let (learned_threshold, oof_ratio, oof_applied) = learn_scent_gate_threshold(&training, 11);
for &test_vars in &sizes {
for test_ratio in 2..=6 {
for test_family in ["random", "banded"] {
for test_index in 1..=trials {
let test_seed = 100_000
+ test_index as u64
+ test_vars as u64 * 1_000
+ test_ratio as u64 * 100
+ u64::from(test_family == "banded") * 50_000;
let formula =
generate_formula(test_family, test_vars, test_ratio, test_seed);
let order = min_fill_order(test_vars, &formula);
let aligned =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &order);
let scent_order = apply_scent_rule(test_vars, &formula, &order, fixed_rule);
let scent =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &scent_order);
let start = Instant::now();
let features = scent_gate_features(test_vars, &formula);
let prediction = scent_gate_predict(&training, &features, 11);
let gate_us = start.elapsed().as_micros();
let oracle_applied = scent.allocated_nodes < aligned.allocated_nodes;
for selector in ["always", "zero-gate", "oof-gate", "oracle", "off"] {
let applied = match selector {
"always" => true,
"zero-gate" => prediction < 0.0,
"oof-gate" => prediction < learned_threshold,
"oracle" => oracle_applied,
"off" => false,
_ => unreachable!(),
};
let final_result = if applied { &scent } else { &aligned };
let valid = final_result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&formula, assignment));
println!(
"{},{},{},{},{},{},{:.6},{:.6},{:.6},{},{},{},{},{},{},{},{:.6},{},{}",
test_family,
vars,
test_vars,
test_ratio,
test_seed,
selector,
prediction,
learned_threshold,
oof_ratio,
oof_applied,
applied,
training.len(),
gate_us,
aligned.allocated_nodes,
scent.allocated_nodes,
final_result.allocated_nodes,
final_result.allocated_nodes as f64
/ aligned.allocated_nodes.max(1) as f64,
oracle_applied,
valid
);
}
}
}
}
}
return;
}
if engine == "learned-message" {
let fixed_rule = ScentRule {
length: 9,
hops: 4,
strongest_first: true,
};
let budget = optional_budget.unwrap_or(3).min(6);
let sizes = [vars.saturating_sub(6).max(6), vars, vars + 6];
let (cached, cache_hit, cache_us) = load_or_build_helper_cache(vars, trials, budget);
eprintln!("training_cache_hit={cache_hit},training_cache_us={cache_us}");
let order_training: Vec<_> = cached
.iter()
.map(|record| {
(
scent_gate_features(record.vars, &record.clauses),
(record.order_nodes as f64 / record.aligned_nodes.max(1) as f64).ln(),
)
})
.collect();
let labeled_formulas: Vec<_> = cached
.into_iter()
.map(|record| {
(
record.vars,
record.clauses,
(record.helper_nodes as f64 / record.order_nodes.max(1) as f64).ln(),
)
})
.collect();
let (order_threshold, _, _) = learn_scent_gate_threshold(&order_training, 11);
let mut models = Vec::new();
for mode in 0..=1u8 {
let mut best: Option<(MessageParams, Vec<(Vec<f64>, f64)>, (f64, f64, usize))> = None;
let mut screened: Vec<_> = message_parameter_candidates(mode == 1)
.into_iter()
.map(|params| {
let samples: Vec<_> = labeled_formulas
.iter()
.take(60)
.map(|(formula_vars, formula, label)| {
(
helper_graph_features_with_params(
*formula_vars,
formula,
4,
12,
mode,
params,
),
*label,
)
})
.collect();
let score = learn_vector_threshold(&samples, 7).1;
(score, params)
})
.collect();
screened.sort_by(|a, b| a.0.total_cmp(&b.0));
screened.truncate(4);
for (_, params) in screened {
let samples: Vec<_> = labeled_formulas
.iter()
.map(|(formula_vars, formula, label)| {
(
helper_graph_features_with_params(
*formula_vars,
formula,
4,
12,
mode,
params,
),
*label,
)
})
.collect();
let threshold = learn_vector_threshold(&samples, 11);
if best.as_ref().is_none_or(|current| {
threshold.1 < current.2.1
|| (threshold.1 == current.2.1 && threshold.2 < current.2.2)
}) {
best = Some((params, samples, threshold));
}
}
let learned = best.unwrap();
let fixed_samples: Vec<_> = labeled_formulas
.iter()
.map(|(formula_vars, formula, label)| {
(
helper_graph_features_with_params(
*formula_vars,
formula,
4,
12,
mode,
DEFAULT_MESSAGE_PARAMS,
),
*label,
)
})
.collect();
let fixed_threshold = learn_vector_threshold(&fixed_samples, 11);
models.push((mode, DEFAULT_MESSAGE_PARAMS, fixed_samples, fixed_threshold));
models.push((mode, learned.0, learned.1, learned.2));
}
for &test_vars in &sizes {
for test_ratio in 2..=6 {
for test_family in ["random", "banded"] {
for test_index in 1..=sift_trials.min(trials) {
let test_seed = 500_000
+ test_index as u64
+ test_vars as u64 * 1_000
+ test_ratio as u64 * 100
+ u64::from(test_family == "banded") * 50_000;
let formula =
generate_formula(test_family, test_vars, test_ratio, test_seed);
let order = min_fill_order(test_vars, &formula);
let baseline =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &order);
let scent_order = apply_scent_rule(test_vars, &formula, &order, fixed_rule);
let order_result =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &scent_order);
let order_gate = scent_gate_predict(
&order_training,
&scent_gate_features(test_vars, &formula),
11,
) < order_threshold;
let (expanded_vars, expanded, _, _) =
scent_batch_expand(test_vars, &formula, budget, 4);
let expanded_elimination = min_fill_order(expanded_vars, &expanded);
let expanded_order = apply_scent_rule(
expanded_vars,
&expanded,
&expanded_elimination,
fixed_rule,
);
let helper_result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&expanded_elimination,
&expanded_order,
);
let feature_start = Instant::now();
let evaluated: Vec<_> = models
.iter()
.map(|(mode, params, samples, threshold)| {
let features = helper_graph_features_with_params(
test_vars, &formula, 4, 12, *mode, *params,
);
(vector_knn_predict(samples, &features, 11), *threshold)
})
.collect();
let feature_us = feature_start.elapsed().as_micros();
let beneficial =
helper_result.allocated_nodes < order_result.allocated_nodes;
for selector in [
"fixed-full",
"learned-full",
"fixed-ghost",
"learned-ghost",
"order-only",
"unconditional",
"oracle",
] {
let model_index = match selector {
"fixed-full" => Some(0usize),
"learned-full" => Some(1usize),
"fixed-ghost" => Some(2usize),
"learned-ghost" => Some(3usize),
_ => None,
};
let (params, prediction, threshold) = if let Some(index) = model_index {
(models[index].1, evaluated[index].0, evaluated[index].1.0)
} else {
(DEFAULT_MESSAGE_PARAMS, 0.0, 0.0)
};
let helper_applied = order_gate
&& match selector {
"order-only" => false,
"unconditional" => true,
"oracle" => beneficial,
_ => prediction < threshold,
};
let final_result = if !order_gate {
&baseline
} else if helper_applied {
&helper_result
} else {
&order_result
};
let threshold_info = model_index
.map(|index| models[index].3)
.unwrap_or((0.0, 1.0, 0));
let valid = final_result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&formula, &assignment[..test_vars])
});
println!(
"{},{},{},{},{},{},{},{:.3},{:.3},{:.3},{:.3},{:.3},{:.6},{:.6},{:.6},{},{},{},{},{},{},{},{},{:.6},{}",
test_family,
vars,
test_vars,
test_ratio,
test_seed,
selector,
order_gate,
params.self_weight,
params.clause_weight,
params.variable_weight,
params.candidate_weight,
params.memory_retention,
prediction,
threshold,
threshold_info.1,
threshold_info.2,
helper_applied,
budget,
feature_us,
baseline.allocated_nodes,
order_result.allocated_nodes,
helper_result.allocated_nodes,
final_result.allocated_nodes,
final_result.allocated_nodes as f64
/ baseline.allocated_nodes.max(1) as f64,
valid
);
}
}
}
}
}
return;
}
if engine == "helper-graph-memory" {
let fixed_rule = ScentRule {
length: 9,
hops: 4,
strongest_first: true,
};
let budget = optional_budget.unwrap_or(3).min(6);
let sizes = [vars.saturating_sub(6).max(6), vars, vars + 6];
let mut order_training = Vec::new();
let mut graph_training: [Vec<(Vec<f64>, f64)>; 3] = std::array::from_fn(|_| Vec::new());
for &training_vars in &sizes {
for training_ratio in 2..=6 {
for training_family in ["random", "banded"] {
for training_seed in 1..=trials {
let seed = training_seed as u64
+ training_vars as u64 * 1_000
+ training_ratio as u64 * 100
+ u64::from(training_family == "banded") * 50_000;
let formula =
generate_formula(training_family, training_vars, training_ratio, seed);
let order = min_fill_order(training_vars, &formula);
let aligned =
eliminate_with_bdds_ordered(training_vars, &formula, &order, &order);
let scent_order =
apply_scent_rule(training_vars, &formula, &order, fixed_rule);
let order_result = eliminate_with_bdds_ordered(
training_vars,
&formula,
&order,
&scent_order,
);
order_training.push((
scent_gate_features(training_vars, &formula),
(order_result.allocated_nodes as f64
/ aligned.allocated_nodes.max(1) as f64)
.ln(),
));
let (expanded_vars, expanded, _, _) =
scent_batch_expand(training_vars, &formula, budget, fixed_rule.hops);
let expanded_elimination = min_fill_order(expanded_vars, &expanded);
let expanded_order = apply_scent_rule(
expanded_vars,
&expanded,
&expanded_elimination,
fixed_rule,
);
let helper_result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&expanded_elimination,
&expanded_order,
);
let label = (helper_result.allocated_nodes as f64
/ order_result.allocated_nodes.max(1) as f64)
.ln();
for mode in 0..3 {
graph_training[mode].push((
helper_graph_features(
training_vars,
&formula,
fixed_rule.hops,
12,
mode as u8,
),
label,
));
}
}
}
}
}
let (order_threshold, _, _) = learn_scent_gate_threshold(&order_training, 11);
let graph_thresholds: Vec<_> = graph_training
.iter()
.map(|training| learn_vector_threshold(training, 11))
.collect();
for &test_vars in &sizes {
for test_ratio in 2..=6 {
for test_family in ["random", "banded"] {
for test_index in 1..=trials {
let test_seed = 400_000
+ test_index as u64
+ test_vars as u64 * 1_000
+ test_ratio as u64 * 100
+ u64::from(test_family == "banded") * 50_000;
let formula =
generate_formula(test_family, test_vars, test_ratio, test_seed);
let order = min_fill_order(test_vars, &formula);
let baseline =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &order);
let scent_order = apply_scent_rule(test_vars, &formula, &order, fixed_rule);
let order_result =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &scent_order);
let order_prediction = scent_gate_predict(
&order_training,
&scent_gate_features(test_vars, &formula),
11,
);
let order_gate = order_prediction < order_threshold;
let (expanded_vars, expanded, _, _) =
scent_batch_expand(test_vars, &formula, budget, fixed_rule.hops);
let expanded_elimination = min_fill_order(expanded_vars, &expanded);
let expanded_order = apply_scent_rule(
expanded_vars,
&expanded,
&expanded_elimination,
fixed_rule,
);
let helper_result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&expanded_elimination,
&expanded_order,
);
let feature_start = Instant::now();
let graph_features: Vec<_> = (0..3)
.map(|mode| {
helper_graph_features(
test_vars,
&formula,
fixed_rule.hops,
12,
mode,
)
})
.collect();
let predictions: Vec<_> = (0..3)
.map(|mode| {
vector_knn_predict(&graph_training[mode], &graph_features[mode], 11)
})
.collect();
let feature_us = feature_start.elapsed().as_micros();
let beneficial =
helper_result.allocated_nodes < order_result.allocated_nodes;
for selector in [
"order-only",
"full-graph",
"ghost-memory",
"forget-pruned",
"unconditional",
"oracle",
] {
let mode = match selector {
"full-graph" => Some(0usize),
"ghost-memory" => Some(1usize),
"forget-pruned" => Some(2usize),
_ => None,
};
let prediction = mode.map(|index| predictions[index]).unwrap_or(0.0);
let (threshold, oof_ratio, oof_applied) = mode
.map(|index| graph_thresholds[index])
.unwrap_or((0.0, 1.0, 0));
let helper_applied = order_gate
&& match selector {
"order-only" => false,
"unconditional" => true,
"oracle" => beneficial,
_ => prediction < threshold,
};
let final_result = if !order_gate {
&baseline
} else if helper_applied {
&helper_result
} else {
&order_result
};
let valid = final_result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&formula, &assignment[..test_vars])
});
println!(
"{},{},{},{},{},{},{},{:.6},{:.6},{:.6},{},{},{},{},{},{},{},{},{:.6},{}",
test_family,
vars,
test_vars,
test_ratio,
test_seed,
selector,
order_gate,
prediction,
threshold,
oof_ratio,
oof_applied,
helper_applied,
budget,
feature_us,
baseline.allocated_nodes,
order_result.allocated_nodes,
helper_result.allocated_nodes,
final_result.allocated_nodes,
final_result.allocated_nodes as f64
/ baseline.allocated_nodes.max(1) as f64,
valid
);
}
}
}
}
}
return;
}
if engine == "scent-helper-gate" {
let fixed_rule = ScentRule {
length: 9,
hops: 4,
strongest_first: true,
};
let budget = optional_budget.unwrap_or(3).min(6);
let sizes = [vars.saturating_sub(6).max(6), vars, vars + 6];
let mut order_training = Vec::new();
let mut helper_training = Vec::new();
for &training_vars in &sizes {
for training_ratio in 2..=6 {
for training_family in ["random", "banded"] {
for training_seed in 1..=trials {
let seed = training_seed as u64
+ training_vars as u64 * 1_000
+ training_ratio as u64 * 100
+ u64::from(training_family == "banded") * 50_000;
let formula =
generate_formula(training_family, training_vars, training_ratio, seed);
let order = min_fill_order(training_vars, &formula);
let aligned =
eliminate_with_bdds_ordered(training_vars, &formula, &order, &order);
let scent_order =
apply_scent_rule(training_vars, &formula, &order, fixed_rule);
let order_result = eliminate_with_bdds_ordered(
training_vars,
&formula,
&order,
&scent_order,
);
order_training.push((
scent_gate_features(training_vars, &formula),
(order_result.allocated_nodes as f64
/ aligned.allocated_nodes.max(1) as f64)
.ln(),
));
let (expanded_vars, expanded, _, _) =
scent_batch_expand(training_vars, &formula, budget, fixed_rule.hops);
let expanded_elimination = min_fill_order(expanded_vars, &expanded);
let expanded_order = apply_scent_rule(
expanded_vars,
&expanded,
&expanded_elimination,
fixed_rule,
);
let helper_result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&expanded_elimination,
&expanded_order,
);
helper_training.push((
helper_gate_features(training_vars, &formula, fixed_rule.hops, budget),
(helper_result.allocated_nodes as f64
/ order_result.allocated_nodes.max(1) as f64)
.ln(),
));
}
}
}
}
let (order_threshold, _, _) = learn_scent_gate_threshold(&order_training, 11);
let (helper_threshold, helper_oof_ratio, helper_oof_applied) =
learn_helper_gate_threshold(&helper_training, 11);
for &test_vars in &sizes {
for test_ratio in 2..=6 {
for test_family in ["random", "banded"] {
for test_index in 1..=trials {
let test_seed = 300_000
+ test_index as u64
+ test_vars as u64 * 1_000
+ test_ratio as u64 * 100
+ u64::from(test_family == "banded") * 50_000;
let formula =
generate_formula(test_family, test_vars, test_ratio, test_seed);
let order = min_fill_order(test_vars, &formula);
let baseline =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &order);
let scent_order = apply_scent_rule(test_vars, &formula, &order, fixed_rule);
let order_result =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &scent_order);
let order_prediction = scent_gate_predict(
&order_training,
&scent_gate_features(test_vars, &formula),
11,
);
let order_gate = order_prediction < order_threshold;
let feature_start = Instant::now();
let helper_features =
helper_gate_features(test_vars, &formula, fixed_rule.hops, budget);
let helper_prediction =
helper_gate_predict(&helper_training, &helper_features, 11);
let feature_us = feature_start.elapsed().as_micros();
let (expanded_vars, expanded, accepted, _) =
scent_batch_expand(test_vars, &formula, budget, fixed_rule.hops);
let expanded_elimination = min_fill_order(expanded_vars, &expanded);
let expanded_order = apply_scent_rule(
expanded_vars,
&expanded,
&expanded_elimination,
fixed_rule,
);
let helper_result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&expanded_elimination,
&expanded_order,
);
let helper_beneficial =
helper_result.allocated_nodes < order_result.allocated_nodes;
for selector in [
"order-only",
"unconditional-helper",
"helper-gate",
"helper-oracle",
] {
let helper_applied = order_gate
&& match selector {
"order-only" => false,
"unconditional-helper" => true,
"helper-gate" => helper_prediction < helper_threshold,
"helper-oracle" => helper_beneficial,
_ => unreachable!(),
};
let final_result = if !order_gate {
&baseline
} else if helper_applied {
&helper_result
} else {
&order_result
};
let valid = final_result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&formula, &assignment[..test_vars])
});
println!(
"{},{},{},{},{},{},{},{:.6},{:.6},{:.6},{},{},{},{},{},{},{},{},{},{:.6},{}",
test_family,
vars,
test_vars,
test_ratio,
test_seed,
selector,
order_gate,
helper_prediction,
helper_threshold,
helper_oof_ratio,
helper_oof_applied,
helper_applied,
budget,
if helper_applied { accepted } else { 0 },
feature_us,
baseline.allocated_nodes,
order_result.allocated_nodes,
helper_result.allocated_nodes,
final_result.allocated_nodes,
final_result.allocated_nodes as f64
/ baseline.allocated_nodes.max(1) as f64,
valid
);
}
}
}
}
}
return;
}
if engine == "scent-helper" {
let fixed_rule = ScentRule {
length: 9,
hops: 4,
strongest_first: true,
};
let budget = optional_budget.unwrap_or(3).min(6);
let sizes = [vars.saturating_sub(6).max(6), vars, vars + 6];
let mut training = Vec::new();
for &training_vars in &sizes {
for training_ratio in 2..=6 {
for training_family in ["random", "banded"] {
for training_seed in 1..=trials {
let seed = training_seed as u64
+ training_vars as u64 * 1_000
+ training_ratio as u64 * 100
+ u64::from(training_family == "banded") * 50_000;
let formula =
generate_formula(training_family, training_vars, training_ratio, seed);
let order = min_fill_order(training_vars, &formula);
let aligned =
eliminate_with_bdds_ordered(training_vars, &formula, &order, &order);
let scent_order =
apply_scent_rule(training_vars, &formula, &order, fixed_rule);
let scent = eliminate_with_bdds_ordered(
training_vars,
&formula,
&order,
&scent_order,
);
training.push((
scent_gate_features(training_vars, &formula),
(scent.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64)
.ln(),
));
}
}
}
}
let (threshold, _, _) = learn_scent_gate_threshold(&training, 11);
for &test_vars in &sizes {
for test_ratio in 2..=6 {
for test_family in ["random", "banded"] {
for test_index in 1..=trials {
let test_seed = 200_000
+ test_index as u64
+ test_vars as u64 * 1_000
+ test_ratio as u64 * 100
+ u64::from(test_family == "banded") * 50_000;
let formula =
generate_formula(test_family, test_vars, test_ratio, test_seed);
let order = min_fill_order(test_vars, &formula);
let baseline =
eliminate_with_bdds_ordered(test_vars, &formula, &order, &order);
let features = scent_gate_features(test_vars, &formula);
let prediction = scent_gate_predict(&training, &features, 11);
let gate_applied = prediction < threshold;
for selector in [
"order-only",
"frequency-helper",
"scent-helper",
"greedy-oracle",
] {
let discovery_start = Instant::now();
let (expanded_vars, expanded, accepted, scored) = if !gate_applied
|| selector == "order-only"
{
(test_vars, formula.clone(), 0, 0)
} else if selector == "frequency-helper" {
let (v, c, a) = expand_recurring_pairs(test_vars, &formula, budget);
(v, c, a, recurring_pair_candidates(&formula).len())
} else if selector == "scent-helper" {
scent_batch_expand(test_vars, &formula, budget, fixed_rule.hops)
} else {
let greedy = greedy_expand(
test_vars, &formula, budget, 12, "min-fill", test_seed, true,
);
(
greedy.vars,
greedy.clauses,
greedy.accepted,
greedy.candidates_tested,
)
};
let discovery_us = discovery_start.elapsed().as_micros();
let expanded_elimination = min_fill_order(expanded_vars, &expanded);
let expanded_bdd_order = if gate_applied {
apply_scent_rule(
expanded_vars,
&expanded,
&expanded_elimination,
fixed_rule,
)
} else {
expanded_elimination.clone()
};
let final_start = Instant::now();
let final_result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&expanded_elimination,
&expanded_bdd_order,
);
let final_us = final_start.elapsed().as_micros();
let equivalent =
final_result.assignment.is_some() == baseline.assignment.is_some();
let valid = final_result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&formula, &assignment[..test_vars])
&& satisfies(&expanded, assignment)
});
println!(
"{},{},{},{},{},{},{},{:.6},{:.6},{},{},{},{},{},{},{},{:.6},{},{}",
test_family,
vars,
test_vars,
test_ratio,
test_seed,
selector,
gate_applied,
prediction,
threshold,
budget,
accepted,
scored,
discovery_us,
final_us,
baseline.allocated_nodes,
final_result.allocated_nodes,
final_result.allocated_nodes as f64
/ baseline.allocated_nodes.max(1) as f64,
equivalent,
valid
);
}
}
}
}
}
return;
}
if engine == "coordinated-width-helper" {
assert!(
vars <= 12,
"coordinated-width-helper supports at most 12 original variables"
);
let candidate_limit = optional_budget.unwrap_or(6);
for seed in 1..=trials {
let formula = generate_formula(family, vars, ratio, 110_000 + seed as u64);
let original_treewidth = exact_treewidth(vars, &formula);
let original_order = min_fill_order(vars, &formula);
let mut original_rank = vec![0usize; vars];
for (level, &variable) in original_order.iter().enumerate() {
original_rank[variable] = level;
}
let original_mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(v, s)| (original_rank[v], s))
.collect(),
)
})
.collect();
let original_result = solve_tuple_natural(vars, &original_mapped);
let first_candidates: Vec<_> = recurring_pair_candidates(&formula)
.into_iter()
.take(candidate_limit)
.collect();
let mut pairs_tested = 0usize;
let mut oracle: Option<(usize, Vec<Clause>, usize)> = None;
for &(first_pair, _) in &first_candidates {
let Some(first_formula) = add_pair_helper(vars, &formula, first_pair) else {
continue;
};
let second_candidates: Vec<_> = recurring_pair_candidates(&first_formula)
.into_iter()
.take(candidate_limit)
.collect();
for &(second_pair, _) in &second_candidates {
let Some(second_formula) =
add_pair_helper(vars + 1, &first_formula, second_pair)
else {
continue;
};
pairs_tested += 1;
let width = exact_treewidth(vars + 2, &second_formula);
if oracle.as_ref().is_none_or(|candidate| width < candidate.2) {
oracle = Some((vars + 2, second_formula, width));
}
}
}
let (frequency_vars, frequency_formula, frequency_added) =
expand_two_greedy(vars, &formula, candidate_limit, false);
let (warp_vars, warp_formula, warp_added) =
expand_two_greedy(vars, &formula, candidate_limit, true);
let variants = [
("original", vars, formula.clone(), 0, original_treewidth),
(
"frequency-two",
frequency_vars,
frequency_formula,
frequency_added,
0,
),
("warp-two", warp_vars, warp_formula, warp_added, 0),
oracle.map_or_else(
|| ("oracle-two", vars, formula.clone(), 0, original_treewidth),
|(oracle_vars, oracle_formula, width)| {
("oracle-two", oracle_vars, oracle_formula, 2, width)
},
),
];
for (selector, expanded_vars, expanded, added, known_width) in variants {
let final_treewidth = if known_width > 0 {
known_width
} else {
exact_treewidth(expanded_vars, &expanded)
};
let order = min_fill_order(expanded_vars, &expanded);
let mut rank = vec![0usize; expanded_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = expanded
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let result = solve_tuple_natural(expanded_vars, &mapped);
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{}",
family,
vars,
formula.len(),
110_000 + seed as u64,
selector,
first_candidates.len(),
pairs_tested,
added,
original_treewidth,
final_treewidth,
final_treewidth as isize - original_treewidth as isize,
original_result.nodes,
result.nodes,
result.nodes as f64 / original_result.nodes.max(1) as f64,
result.assignment.is_some() == original_result.assignment.is_some(),
result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment))
);
}
}
return;
}
if engine == "shake-inverse-width" {
assert!(
vars <= 16,
"shake-inverse-width supports at most 16 variables"
);
for seed in 1..=trials {
let formula_seed = 120_000 + seed as u64;
let formula = generate_formula(family, vars, ratio, formula_seed);
let original_treewidth = exact_treewidth(vars, &formula);
let original_order = min_fill_order(vars, &formula);
let mut original_rank = vec![0usize; vars];
for (level, &variable) in original_order.iter().enumerate() {
original_rank[variable] = level;
}
let original_mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(v, s)| (original_rank[v], s))
.collect(),
)
})
.collect();
let original_result = solve_tuple_natural(vars, &original_mapped);
let original_sat = original_result.assignment.is_some();
let richness = leaf_richness(vars, &formula);
for (selector, gated, inverse_depth, probe_limit) in [
("shake", false, 0, 0),
("gated-shake", true, 0, 0),
("gated-depth-two", true, 2, 4),
] {
let gate_applied = !gated || richness > 0;
let shaken = if gate_applied {
shake_formula(vars, &formula, inverse_depth, probe_limit)
} else {
ShakenFormula {
vars,
clauses: formula.clone(),
core_to_original: (0..vars).collect(),
fixed: vec![None; vars],
removed: 0,
probes: 0,
inverse_forced: 0,
contradiction: false,
}
};
let core_treewidth = if shaken.contradiction {
0
} else {
exact_treewidth(shaken.vars, &shaken.clauses)
};
let (core_nodes, core_assignment, core_rank) = if shaken.contradiction {
(0usize, None, Vec::new())
} else {
let order = min_fill_order(shaken.vars, &shaken.clauses);
let mut rank = vec![0usize; shaken.vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = shaken
.clauses
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let result = solve_tuple_natural(shaken.vars, &mapped);
(result.nodes, result.assignment, rank)
};
let core_sat = core_assignment.is_some();
let reconstruction_valid = if let Some(core_assignment) = core_assignment.as_ref() {
let mut reconstructed = vec![false; vars];
for (variable, value) in shaken.fixed.iter().enumerate() {
if let Some(value) = value {
reconstructed[variable] = *value;
}
}
for (core_variable, &original_variable) in
shaken.core_to_original.iter().enumerate()
{
reconstructed[original_variable] =
core_assignment[core_rank[core_variable]];
}
satisfies(&formula, &reconstructed)
} else {
!original_sat
};
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{}",
family,
vars,
formula.len(),
formula_seed,
selector,
richness,
gate_applied,
inverse_depth,
shaken.removed,
shaken.vars,
shaken.clauses.len(),
shaken.probes,
shaken.inverse_forced,
original_treewidth,
core_treewidth,
core_treewidth as isize - original_treewidth as isize,
original_result.nodes,
core_nodes,
core_nodes as f64 / original_result.nodes.max(1) as f64,
core_sat,
core_sat == original_sat,
reconstruction_valid
);
}
}
return;
}
if engine == "seed-branch-width" {
assert!(
vars <= 16,
"seed-branch-width supports at most 16 variables"
);
let max_interior = optional_budget.unwrap_or(8).min(12);
for seed in 1..=trials {
let formula_seed = 130_000 + seed as u64;
let formula = generate_formula(family, vars, ratio, formula_seed);
let original_treewidth = exact_treewidth(vars, &formula);
let original_order = min_fill_order(vars, &formula);
let mut original_rank = vec![0usize; vars];
for (level, &variable) in original_order.iter().enumerate() {
original_rank[variable] = level;
}
let original_mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(v, s)| (original_rank[v], s))
.collect(),
)
})
.collect();
let original_result = solve_tuple_natural(vars, &original_mapped);
let original_sat = original_result.assignment.is_some();
let seeded = seed_detachable_branch(vars, &formula, max_interior);
let core_treewidth = exact_treewidth(seeded.vars, &seeded.clauses);
let core_order = min_fill_order(seeded.vars, &seeded.clauses);
let mut core_rank = vec![0usize; seeded.vars];
for (level, &variable) in core_order.iter().enumerate() {
core_rank[variable] = level;
}
let core_mapped: Vec<_> = seeded
.clauses
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (core_rank[v], s)).collect()))
.collect();
let core_result = solve_tuple_natural(seeded.vars, &core_mapped);
let core_sat = core_result.assignment.is_some();
let reconstruction_valid = if let Some(core_assignment) = &core_result.assignment {
let mut reconstructed = vec![false; vars];
for (core_variable, &original_variable) in
seeded.core_to_original.iter().enumerate()
{
reconstructed[original_variable] = core_assignment[core_rank[core_variable]];
}
let mut boundary_bits = 0usize;
for (index, &variable) in seeded.boundary.iter().enumerate() {
if reconstructed[variable] {
boundary_bits |= 1usize << index;
}
}
if let Some(Some(values)) = seeded.witnesses.get(boundary_bits) {
for (index, &variable) in seeded.interior.iter().enumerate() {
reconstructed[variable] = values[index];
}
satisfies(&formula, &reconstructed)
} else {
seeded.interior.is_empty() && satisfies(&formula, &reconstructed)
}
} else {
!original_sat
};
let recipe_entries = seeded.witnesses.iter().flatten().count();
let recipe_bits = recipe_entries * seeded.interior.len();
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{}",
family,
vars,
formula.len(),
formula_seed,
seeded.boundary.len(),
seeded.interior.len(),
seeded.local_clauses,
seeded.summary_clauses,
recipe_entries,
recipe_bits,
seeded.compilation_trials,
original_treewidth,
core_treewidth,
core_treewidth as isize - original_treewidth as isize,
original_result.nodes,
core_result.nodes,
core_result.nodes as f64 / original_result.nodes.max(1) as f64,
core_sat,
core_sat == original_sat,
reconstruction_valid
);
}
return;
}
if engine == "seed-bdd-width" {
assert!(vars <= 16, "seed-bdd-width supports at most 16 variables");
let max_interior = optional_budget.unwrap_or(8).min(14);
for seed in 1..=trials {
let formula_seed = 140_000 + seed as u64;
let formula = generate_formula(family, vars, ratio, formula_seed);
let original_treewidth = exact_treewidth(vars, &formula);
let original_order = min_fill_order(vars, &formula);
let mut original_rank = vec![0usize; vars];
for (level, &variable) in original_order.iter().enumerate() {
original_rank[variable] = level;
}
let original_mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(v, s)| (original_rank[v], s))
.collect(),
)
})
.collect();
let original_start = Instant::now();
let original_result = solve_tuple_natural(vars, &original_mapped);
let original_solve_us = original_start.elapsed().as_micros();
let original_sat = original_result.assignment.is_some();
let compile_start = Instant::now();
let seeded = seed_detachable_branch_bdd(vars, &formula, max_interior, order_name);
let compile_us = compile_start.elapsed().as_micros();
let core_treewidth = exact_treewidth(seeded.vars, &seeded.clauses);
let core_order = min_fill_order(seeded.vars, &seeded.clauses);
let mut core_rank = vec![0usize; seeded.vars];
for (level, &variable) in core_order.iter().enumerate() {
core_rank[variable] = level;
}
let core_mapped: Vec<_> = seeded
.clauses
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (core_rank[v], s)).collect()))
.collect();
let core_start = Instant::now();
let core_result = solve_tuple_natural(seeded.vars, &core_mapped);
let core_solve_us = core_start.elapsed().as_micros();
let core_sat = core_result.assignment.is_some();
let reconstruction_valid = if let Some(core_assignment) = &core_result.assignment {
let mut reconstructed = vec![false; vars];
for (core_variable, &original_variable) in
seeded.core_to_original.iter().enumerate()
{
reconstructed[original_variable] = core_assignment[core_rank[core_variable]];
}
if let Some(values) = regrow_bdd_seed(&seeded, &reconstructed) {
for (index, &variable) in seeded.interior.iter().enumerate() {
reconstructed[variable] = values[index];
}
satisfies(&formula, &reconstructed)
} else {
false
}
} else {
!original_sat
};
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{:.6},{:.6},{:.6},{},{},{}",
family,
vars,
formula.len(),
formula_seed,
order_name,
seeded.boundary.len(),
seeded.interior.len(),
seeded.local_clauses,
seeded.summary_clauses,
seeded.live_nodes,
seeded.allocated_nodes,
compile_us,
original_solve_us,
core_solve_us,
(compile_us + core_solve_us) as f64 / original_solve_us.max(1) as f64,
original_treewidth,
core_treewidth,
core_treewidth as isize - original_treewidth as isize,
original_result.nodes,
core_result.nodes,
core_result.nodes as f64 / original_result.nodes.max(1) as f64,
(core_result.nodes + seeded.live_nodes) as f64
/ original_result.nodes.max(1) as f64,
(core_result.nodes + seeded.allocated_nodes) as f64
/ original_result.nodes.max(1) as f64,
core_sat,
core_sat == original_sat,
reconstruction_valid
);
}
return;
}
if engine == "seed-bdd-reuse" {
assert!(vars <= 16, "seed-bdd-reuse supports at most 16 variables");
let max_interior = optional_budget.unwrap_or(8).min(14);
let updates = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize)
.max(1);
let cache_cap = args
.get(9)
.and_then(|value| value.parse().ok())
.unwrap_or(usize::MAX);
let cache_policy = args.get(10).map(String::as_str).unwrap_or("roots");
for seed in 1..=trials {
let formula_seed = 150_000 + seed as u64;
let base = generate_formula(family, vars, ratio, formula_seed);
let mut shared_manager = BddManager::default();
let mut cache_roots = Vec::new();
let mut evictions = 0usize;
let mut compactions = 0usize;
let mut peak_shared_nodes = 0usize;
let mut hot_roots = 0usize;
for update in 0..updates {
let mut formula = base.clone();
if update > 0 && !formula.is_empty() {
let clause_index = (update - 1) % formula.len();
if !formula[clause_index].0.is_empty() {
let literal_index = (update - 1) % formula[clause_index].0.len();
formula[clause_index].0[literal_index].1 =
!formula[clause_index].0[literal_index].1;
}
}
let cold_start = Instant::now();
let cold = seed_detachable_branch_bdd(vars, &formula, max_interior, "natural");
let cold_us = cold_start.elapsed().as_micros();
let warm_start = Instant::now();
let warm = seed_detachable_branch_bdd_in(
vars,
&formula,
max_interior,
"natural",
&mut shared_manager,
);
let warm_us = warm_start.elapsed().as_micros();
cache_roots.push(warm.cache_root);
peak_shared_nodes = peak_shared_nodes.max(shared_manager.nodes.len());
if shared_manager.nodes.len() > cache_cap {
let seed_root_count = cache_roots.len();
let mut retained_roots = cache_roots.clone();
if cache_policy == "hits" {
let mut hottest: Vec<_> = shared_manager
.node_hits
.iter()
.copied()
.enumerate()
.filter(|(index, hits)| {
*hits > 0 && !cache_roots.contains(&(index + 2))
})
.collect();
hottest.sort_by_key(|&(index, hits)| (std::cmp::Reverse(hits), index));
retained_roots
.extend(hottest.into_iter().take(128).map(|(index, _)| index + 2));
}
loop {
let (compacted, mapped_roots) =
compact_bdd_roots(&shared_manager, &retained_roots);
compactions += 1;
shared_manager = compacted;
retained_roots = mapped_roots;
if shared_manager.nodes.len() <= cache_cap {
break;
}
if retained_roots.len() > seed_root_count {
let hot_count = retained_roots.len() - seed_root_count;
retained_roots.truncate(retained_roots.len() - hot_count.div_ceil(2));
} else if seed_root_count > 1 {
retained_roots.remove(0);
evictions += 1;
} else {
break;
}
}
let retained_seed_count = seed_root_count.min(retained_roots.len());
cache_roots = retained_roots[..retained_seed_count].to_vec();
hot_roots = retained_roots.len() - retained_seed_count;
}
let original_treewidth = exact_treewidth(vars, &formula);
let core_treewidth = exact_treewidth(warm.vars, &warm.clauses);
let original_order = min_fill_order(vars, &formula);
let mut original_rank = vec![0usize; vars];
for (level, &variable) in original_order.iter().enumerate() {
original_rank[variable] = level;
}
let original_mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(v, s)| (original_rank[v], s))
.collect(),
)
})
.collect();
let original_result = solve_tuple_natural(vars, &original_mapped);
let core_order = min_fill_order(warm.vars, &warm.clauses);
let mut core_rank = vec![0usize; warm.vars];
for (level, &variable) in core_order.iter().enumerate() {
core_rank[variable] = level;
}
let core_mapped: Vec<_> = warm
.clauses
.iter()
.map(|clause| {
Clause(clause.0.iter().map(|&(v, s)| (core_rank[v], s)).collect())
})
.collect();
let core_result = solve_tuple_natural(warm.vars, &core_mapped);
let equivalent =
core_result.assignment.is_some() == original_result.assignment.is_some();
let valid = if let Some(core_assignment) = &core_result.assignment {
let mut reconstructed = vec![false; vars];
for (core_variable, &original_variable) in
warm.core_to_original.iter().enumerate()
{
reconstructed[original_variable] =
core_assignment[core_rank[core_variable]];
}
regrow_bdd_seed(&warm, &reconstructed).is_some_and(|values| {
for (index, &variable) in warm.interior.iter().enumerate() {
reconstructed[variable] = values[index];
}
satisfies(&formula, &reconstructed)
})
} else {
original_result.assignment.is_none()
};
println!(
"{},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{}",
family,
vars,
formula.len(),
formula_seed,
update,
warm.interior.len(),
cold.allocated_nodes,
warm.allocated_nodes,
warm.allocated_nodes as f64 / cold.allocated_nodes.max(1) as f64,
cold_us,
warm_us,
warm_us as f64 / cold_us.max(1) as f64,
shared_manager.nodes.len(),
cache_cap,
cache_policy,
peak_shared_nodes,
compactions,
evictions,
hot_roots,
warm.live_nodes,
original_treewidth,
core_treewidth,
core_treewidth as isize - original_treewidth as isize,
original_result.nodes,
core_result.nodes,
core_result.nodes as f64 / original_result.nodes.max(1) as f64,
equivalent,
valid
);
}
}
return;
}
if engine == "multi-seed-width" {
assert!(vars <= 16, "multi-seed-width supports at most 16 variables");
let branch_cap = optional_budget.unwrap_or(4).min(10);
let max_seeds = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize);
for seed in 1..=trials {
let formula_seed = 160_000 + seed as u64;
let formula = generate_formula(family, vars, ratio, formula_seed);
let original_treewidth = exact_treewidth(vars, &formula);
let original_order = min_fill_order(vars, &formula);
let mut original_rank = vec![0usize; vars];
for (level, &variable) in original_order.iter().enumerate() {
original_rank[variable] = level;
}
let original_mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(v, s)| (original_rank[v], s))
.collect(),
)
})
.collect();
let original_start = Instant::now();
let original_result = solve_tuple_natural(vars, &original_mapped);
let original_solve_us = original_start.elapsed().as_micros();
for (selector, cap, limit) in [
("original", 0usize, 0usize),
("single-giant", vars.saturating_sub(2), 1usize),
("multi-small", branch_cap, max_seeds),
] {
let compile_start = Instant::now();
let mut current_vars = vars;
let mut current_clauses = formula.clone();
let mut seeds = Vec::new();
for _ in 0..limit {
let compiled =
seed_detachable_branch_bdd(current_vars, ¤t_clauses, cap, "natural");
if compiled.interior.is_empty() {
break;
}
current_vars = compiled.vars;
current_clauses = compiled.clauses.clone();
seeds.push(compiled);
}
let compile_us = compile_start.elapsed().as_micros();
let final_treewidth = exact_treewidth(current_vars, ¤t_clauses);
let final_order = min_fill_order(current_vars, ¤t_clauses);
let mut final_rank = vec![0usize; current_vars];
for (level, &variable) in final_order.iter().enumerate() {
final_rank[variable] = level;
}
let final_mapped: Vec<_> = current_clauses
.iter()
.map(|clause| {
Clause(clause.0.iter().map(|&(v, s)| (final_rank[v], s)).collect())
})
.collect();
let final_start = Instant::now();
let final_result = solve_tuple_natural(current_vars, &final_mapped);
let final_solve_us = final_start.elapsed().as_micros();
let reconstructed = final_result.assignment.as_ref().and_then(|assignment| {
let core_assignment: Vec<_> = (0..current_vars)
.map(|variable| assignment[final_rank[variable]])
.collect();
regrow_seed_chain(&seeds, &core_assignment)
});
let equivalent =
final_result.assignment.is_some() == original_result.assignment.is_some();
let valid = reconstructed
.as_ref()
.is_none_or(|assignment| satisfies(&formula, assignment))
&& (reconstructed.is_some() == original_result.assignment.is_some());
let removed: usize = seeds.iter().map(|seed| seed.interior.len()).sum();
let boundary_sum: usize = seeds.iter().map(|seed| seed.boundary.len()).sum();
let live_nodes: usize = seeds.iter().map(|seed| seed.live_nodes).sum();
let allocated_nodes: usize = seeds.iter().map(|seed| seed.allocated_nodes).sum();
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{:.6},{},{},{:.6},{},{}",
family,
vars,
formula.len(),
formula_seed,
selector,
cap,
limit,
seeds.len(),
removed,
boundary_sum,
live_nodes,
allocated_nodes,
compile_us,
current_vars,
current_clauses.len(),
original_treewidth,
final_treewidth,
final_treewidth as isize - original_treewidth as isize,
original_result.nodes,
final_result.nodes,
final_result.nodes as f64 / original_result.nodes.max(1) as f64,
(final_result.nodes + live_nodes) as f64 / original_result.nodes.max(1) as f64,
original_solve_us,
final_solve_us,
(compile_us + final_solve_us) as f64 / original_solve_us.max(1) as f64,
equivalent,
valid
);
}
}
return;
}
if engine == "incremental-payback" {
let branch_cap = optional_budget.unwrap_or(8).min(12);
let queries = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(32usize)
.max(2);
let max_seeds = args
.get(9)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize);
for seed in 1..=trials {
let formula_seed = 170_000 + seed as u64;
let base = generate_formula(family, vars, ratio, formula_seed);
let mut formulas = Vec::with_capacity(queries);
formulas.push(base.clone());
for query in 1..queries {
let mut formula = base.clone();
if !formula.is_empty() {
let clause_index = (query - 1) % formula.len();
if !formula[clause_index].0.is_empty() {
let literal_index = (query - 1) % formula[clause_index].0.len();
formula[clause_index].0[literal_index].1 =
!formula[clause_index].0[literal_index].1;
}
}
formulas.push(formula);
}
let setup_start = Instant::now();
let mut incremental = Solver::new();
let selector_count = queries - 1;
for (clause_index, clause) in base.iter().enumerate() {
let mut literals: Vec<_> = clause
.0
.iter()
.map(|&(variable, positive)| Lit::from_var(Var::from_index(variable), positive))
.collect();
for query in 1..queries {
if (query - 1) % base.len() == clause_index {
literals.push(Lit::from_var(Var::from_index(vars + query - 1), true));
}
}
incremental.add_clause(&literals);
}
for query in 1..queries {
let clause_index = (query - 1) % base.len();
let mut literals: Vec<_> = formulas[query][clause_index]
.0
.iter()
.map(|&(variable, positive)| Lit::from_var(Var::from_index(variable), positive))
.collect();
literals.push(Lit::from_var(Var::from_index(vars + query - 1), false));
incremental.add_clause(&literals);
}
let setup_us = setup_start.elapsed().as_micros();
let mut shared_seed_manager = BddManager::default();
let mut cumulative_cold = 0u128;
let mut cumulative_incremental = setup_us;
let mut cumulative_seed = 0u128;
for (query, formula) in formulas.iter().enumerate() {
let cold_start = Instant::now();
let cold_assignment = solve_with_varisat(vars, formula);
let cold_us = cold_start.elapsed().as_micros();
cumulative_cold += cold_us;
let assumptions: Vec<_> = (0..selector_count)
.map(|selector| {
Lit::from_var(
Var::from_index(vars + selector),
query > 0 && selector == query - 1,
)
})
.collect();
incremental.assume(&assumptions);
let incremental_start = Instant::now();
let incremental_sat = incremental.solve().expect("incremental Varisat solve");
let incremental_us = incremental_start.elapsed().as_micros();
cumulative_incremental += incremental_us;
let mut incremental_assignment = vec![false; vars];
if incremental_sat {
for literal in incremental.model().expect("incremental model") {
if literal.var().index() < vars {
incremental_assignment[literal.var().index()] = literal.is_positive();
}
}
}
let seed_compile_start = Instant::now();
let mut current_vars = vars;
let mut current_clauses = formula.clone();
let mut seeds = Vec::new();
for _ in 0..max_seeds {
let compiled = seed_detachable_branch_bdd_in(
current_vars,
¤t_clauses,
branch_cap,
"natural",
&mut shared_seed_manager,
);
if compiled.interior.is_empty() {
break;
}
current_vars = compiled.vars;
current_clauses = compiled.clauses.clone();
seeds.push(compiled);
}
let seed_compile_us = seed_compile_start.elapsed().as_micros();
let seed_solve_start = Instant::now();
let core_assignment = solve_with_varisat(current_vars, ¤t_clauses);
let seed_solve_us = seed_solve_start.elapsed().as_micros();
cumulative_seed += seed_compile_us + seed_solve_us;
let seed_assignment = core_assignment
.as_ref()
.and_then(|assignment| regrow_seed_chain(&seeds, assignment));
let cold_sat = cold_assignment.is_some();
let incremental_valid =
!incremental_sat || satisfies(formula, &incremental_assignment);
let seed_valid = seed_assignment
.as_ref()
.is_none_or(|assignment| satisfies(formula, assignment));
let removed: usize = seeds.iter().map(|seed| seed.interior.len()).sum();
println!(
"{},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{},{},{},{},{},{},{},{}",
family,
vars,
formula.len(),
formula_seed,
query,
cold_us,
setup_us,
incremental_us,
cumulative_incremental as f64 / cumulative_cold.max(1) as f64,
seed_compile_us,
seed_solve_us,
cumulative_seed as f64 / cumulative_cold.max(1) as f64,
seeds.len(),
removed,
shared_seed_manager.nodes.len(),
cold_sat,
incremental_sat == cold_sat,
seed_assignment.is_some() == cold_sat,
incremental_valid,
seed_valid
);
}
}
return;
}
if engine == "safe-compiler-control" {
let branch_cap = optional_budget.unwrap_or(64).min(64);
let node_limit = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(100_000usize);
let time_limit_ms = args
.get(9)
.and_then(|value| value.parse().ok())
.unwrap_or(100u64);
let sizes: Vec<_> = [500usize, 1000]
.into_iter()
.filter(|&size| size <= vars)
.collect();
assert!(!sizes.is_empty(), "safe compiler requires vars >= 500");
for &scale_vars in &sizes {
for trial in 0..trials {
let formula_seed = 1_700_000 + scale_vars as u64 * 100 + trial as u64;
let original = generate_formula(family, scale_vars, ratio, formula_seed);
for layout in ["original", "renamed"] {
let mut permutation: Vec<_> = (0..scale_vars).collect();
if layout == "renamed" {
Rng(formula_seed ^ 0x517c_c1b7).shuffle(&mut permutation);
}
let formula: Vec<_> = original
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(variable, value)| (permutation[variable], value))
.collect(),
)
})
.collect();
let discovery_start = Instant::now();
let candidates =
fast_detachable_branch_candidates(scale_vars, &formula, branch_cap);
let discovery_us = discovery_start.elapsed().as_micros();
let compile_start = Instant::now();
let mut seeds = Vec::new();
let mut node_rejected = 0usize;
let mut time_rejected = 0usize;
let mut attempt_nodes = 0usize;
for (interior, boundary) in candidates.iter().cloned() {
let attempt = try_seed_bdd_candidate(
scale_vars,
&formula,
interior,
boundary,
"min-fill",
node_limit,
std::time::Duration::from_millis(time_limit_ms),
);
attempt_nodes += attempt.nodes;
node_rejected += usize::from(attempt.node_exceeded);
time_rejected += usize::from(attempt.time_exceeded);
if let Some(seed) = attempt.seed {
seeds.push(seed);
}
}
let compile_us = compile_start.elapsed().as_micros();
let all_interior: BTreeSet<_> = seeds
.iter()
.flat_map(|seed| seed.interior.iter().copied())
.collect();
let mut core_clauses: Vec<_> = formula
.iter()
.filter(|clause| {
!clause
.0
.iter()
.any(|(variable, _)| all_interior.contains(variable))
})
.cloned()
.collect();
for seed in &seeds {
core_clauses.extend(seed.summary.iter().cloned());
}
let core_to_original: Vec<_> = (0..scale_vars)
.filter(|variable| !all_interior.contains(variable))
.collect();
let mut original_to_core = vec![usize::MAX; scale_vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
for clause in &mut core_clauses {
for (variable, _) in &mut clause.0 {
*variable = original_to_core[*variable];
}
}
let original_assignment = solve_with_varisat(scale_vars, &formula);
let core_assignment = solve_with_varisat(core_to_original.len(), &core_clauses);
let mut reconstructed = core_assignment.as_ref().map(|core| {
let mut assignment = vec![false; scale_vars];
for (index, &original) in core_to_original.iter().enumerate() {
assignment[original] = core[index];
}
assignment
});
if let Some(assignment) = &mut reconstructed {
for seed in &seeds {
let Some(values) = regrow_bdd_seed(seed, assignment) else {
reconstructed = None;
break;
};
for (index, &variable) in seed.interior.iter().enumerate() {
assignment[variable] = values[index];
}
}
}
let removed = all_interior.len();
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{},{},{}",
family,
layout,
scale_vars,
formula.len(),
formula_seed,
branch_cap,
node_limit,
time_limit_ms,
candidates.len(),
seeds.len(),
node_rejected,
time_rejected,
removed,
removed as f64 / scale_vars as f64,
discovery_us,
compile_us,
attempt_nodes,
seeds.iter().map(|seed| seed.live_nodes).sum::<usize>(),
core_to_original.len(),
original_assignment.is_some() == core_assignment.is_some(),
original_assignment.is_none()
|| reconstructed
.as_ref()
.is_some_and(|assignment| satisfies(&formula, assignment)),
removed * 10 >= scale_vars * 3
);
}
}
}
return;
}
if engine == "renaming-control" {
let branch_cap = optional_budget.unwrap_or(64).min(64);
let renamings = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(3usize);
let sizes: Vec<_> = [500usize, 1000]
.into_iter()
.filter(|&size| size <= vars)
.collect();
assert!(!sizes.is_empty(), "renaming control requires vars >= 500");
for &scale_vars in &sizes {
for trial in 0..trials {
let formula_seed = 1_500_000 + scale_vars as u64 * 100 + trial as u64;
let original = generate_formula(family, scale_vars, ratio, formula_seed);
for renaming in 0..=renamings {
let mut permutation: Vec<_> = (0..scale_vars).collect();
if renaming > 0 {
Rng(formula_seed ^ (renaming as u64).wrapping_mul(0x9e37_79b9))
.shuffle(&mut permutation);
}
let formula: Vec<_> = original
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(variable, value)| (permutation[variable], value))
.collect(),
)
})
.collect();
let discovery_start = Instant::now();
let candidates =
fast_detachable_branch_candidates(scale_vars, &formula, branch_cap);
let discovery_us = discovery_start.elapsed().as_micros();
let compile_start = Instant::now();
let mut seeds = Vec::new();
for (mut interior, boundary) in candidates.iter().cloned() {
let mut manager = BddManager::default();
seeds.push(seed_bdd_candidate_in(
scale_vars,
&formula,
&mut interior,
boundary,
"min-fill",
&mut manager,
));
}
let all_interior: BTreeSet<_> = seeds
.iter()
.flat_map(|seed| seed.interior.iter().copied())
.collect();
let mut core_clauses: Vec<_> = formula
.iter()
.filter(|clause| {
!clause
.0
.iter()
.any(|(variable, _)| all_interior.contains(variable))
})
.cloned()
.collect();
for seed in &seeds {
core_clauses.extend(seed.summary.iter().cloned());
}
let core_to_original: Vec<_> = (0..scale_vars)
.filter(|variable| !all_interior.contains(variable))
.collect();
let mut original_to_core = vec![usize::MAX; scale_vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
for clause in &mut core_clauses {
for (variable, _) in &mut clause.0 {
*variable = original_to_core[*variable];
}
}
let compile_us = compile_start.elapsed().as_micros();
let original_assignment = solve_with_varisat(scale_vars, &formula);
let core_assignment = solve_with_varisat(core_to_original.len(), &core_clauses);
let mut reconstructed = core_assignment.as_ref().map(|core| {
let mut assignment = vec![false; scale_vars];
for (index, &original) in core_to_original.iter().enumerate() {
assignment[original] = core[index];
}
assignment
});
if let Some(assignment) = &mut reconstructed {
for seed in &seeds {
let Some(values) = regrow_bdd_seed(seed, assignment) else {
reconstructed = None;
break;
};
for (index, &variable) in seed.interior.iter().enumerate() {
assignment[variable] = values[index];
}
}
}
let removed = all_interior.len();
println!(
"{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{},{},{}",
family,
scale_vars,
formula.len(),
formula_seed,
renaming,
branch_cap,
candidates.len(),
removed,
removed as f64 / scale_vars as f64,
discovery_us,
compile_us,
seeds.iter().map(|seed| seed.live_nodes).sum::<usize>(),
seeds.iter().map(|seed| seed.allocated_nodes).sum::<usize>(),
core_to_original.len(),
original_assignment.is_some() == core_assignment.is_some(),
reconstructed
.as_ref()
.is_some_and(|assignment| satisfies(&formula, assignment)),
removed * 10 >= scale_vars * 3
);
}
}
}
return;
}
if engine == "batch-offline-service" {
let branch_cap = optional_budget.unwrap_or(64).min(64);
let horizon = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(1_000_000usize)
.max(2);
let sizes: Vec<_> = [500usize, 1000]
.into_iter()
.filter(|&size| size <= vars)
.collect();
assert!(!sizes.is_empty(), "batch service requires vars >= 500");
for &scale_vars in &sizes {
for trial in 0..trials {
let formula_seed = 1_300_000 + scale_vars as u64 * 100 + trial as u64;
let mut formula = generate_formula(family, scale_vars, ratio, formula_seed);
if order_name.contains("renamed") {
let mut permutation: Vec<_> = (0..scale_vars).collect();
Rng(formula_seed ^ 0x9e37_79b9).shuffle(&mut permutation);
for clause in &mut formula {
for (variable, _) in &mut clause.0 {
*variable = permutation[*variable];
}
}
}
let discovery_start = Instant::now();
let candidates =
fast_detachable_branch_candidates(scale_vars, &formula, branch_cap);
let discovery_ns = discovery_start.elapsed().as_nanos();
let compile_start = Instant::now();
let mut seeds = Vec::new();
for (mut interior, boundary) in candidates {
let strategy = if order_name.contains("renamed") {
"min-fill"
} else {
"natural"
};
if order_name.starts_with("safe") {
let attempt = try_seed_bdd_candidate(
scale_vars,
&formula,
interior,
boundary,
strategy,
100_000,
std::time::Duration::from_millis(100),
);
if let Some(seed) = attempt.seed {
seeds.push(seed);
}
} else {
let mut manager = BddManager::default();
seeds.push(seed_bdd_candidate_in(
scale_vars,
&formula,
&mut interior,
boundary,
strategy,
&mut manager,
));
}
}
let all_interior: BTreeSet<_> = seeds
.iter()
.flat_map(|seed| seed.interior.iter().copied())
.collect();
let mut core_clauses: Vec<_> = formula
.iter()
.filter(|clause| {
!clause
.0
.iter()
.any(|(variable, _)| all_interior.contains(variable))
})
.cloned()
.collect();
for seed in &seeds {
core_clauses.extend(seed.summary.iter().cloned());
}
let core_to_original: Vec<_> = (0..scale_vars)
.filter(|variable| !all_interior.contains(variable))
.collect();
let mut original_to_core = vec![usize::MAX; scale_vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
for clause in &mut core_clauses {
for (variable, _) in &mut clause.0 {
*variable = original_to_core[*variable];
}
}
let compile_ns = compile_start.elapsed().as_nanos();
let incremental_setup_start = Instant::now();
let mut incremental = Solver::new();
add_to_varisat(&mut incremental, &formula);
let incremental_setup_ns = incremental_setup_start.elapsed().as_nanos();
let seeded_setup_start = Instant::now();
let mut seeded = Solver::new();
add_to_varisat(&mut seeded, &core_clauses);
let seeded_solver_setup_ns = seeded_setup_start.elapsed().as_nanos();
let mut incremental_query_ns = 0u128;
let mut seeded_query_ns = 0u128;
let mut reconstruction_ns = 0u128;
let mut reconstruction_samples = 0usize;
let mut valid = !core_to_original.is_empty();
for query in 0..128 + horizon {
let core_variable = query % core_to_original.len();
let original_variable = core_to_original[core_variable];
let value = (query / core_to_original.len() + query) % 2 == 0;
incremental.assume(&[Lit::from_var(Var::from_index(original_variable), value)]);
let start = Instant::now();
let incremental_sat = incremental.solve().expect("batch incremental solve");
if query >= 128 {
incremental_query_ns += start.elapsed().as_nanos();
}
seeded.assume(&[Lit::from_var(Var::from_index(core_variable), value)]);
let start = Instant::now();
let seeded_sat = seeded.solve().expect("batch seeded solve");
if query >= 128 {
seeded_query_ns += start.elapsed().as_nanos();
}
valid &= incremental_sat == seeded_sat;
if seeded_sat && query >= 128 && (query < 132 || query + 1 == 128 + horizon) {
let start = Instant::now();
let mut assignment = vec![false; scale_vars];
for literal in seeded.model().expect("batch seeded model") {
if literal.var().index() < core_to_original.len() {
assignment[core_to_original[literal.var().index()]] =
literal.is_positive();
}
}
for seed in &seeds {
let Some(values) = regrow_bdd_seed(seed, &assignment) else {
valid = false;
break;
};
for (index, &variable) in seed.interior.iter().enumerate() {
assignment[variable] = values[index];
}
}
valid &= assignment[original_variable] == value
&& satisfies(&formula, &assignment);
reconstruction_ns += start.elapsed().as_nanos();
reconstruction_samples += 1;
}
}
let incremental_total = incremental_setup_ns + incremental_query_ns;
let seeded_total =
discovery_ns + compile_ns + seeded_solver_setup_ns + seeded_query_ns;
let incremental_per = incremental_query_ns as f64 / horizon as f64;
let seeded_per = seeded_query_ns as f64 / horizon as f64;
let crossover = if seeded_per < incremental_per {
(discovery_ns + compile_ns + seeded_solver_setup_ns)
.saturating_sub(incremental_setup_ns) as f64
/ (incremental_per - seeded_per)
} else {
f64::INFINITY
};
println!(
"{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{},{},{:.6},{:.6},{:.1},{:.1},{:.1},{},{},{},{}",
family,
scale_vars,
formula.len(),
formula_seed,
horizon,
branch_cap,
seeds.len(),
all_interior.len(),
all_interior.len() as f64 / scale_vars as f64,
core_to_original.len(),
discovery_ns,
compile_ns,
incremental_setup_ns,
seeded_solver_setup_ns,
incremental_query_ns,
seeded_query_ns,
seeded_query_ns as f64 / incremental_query_ns.max(1) as f64,
seeded_total as f64 / incremental_total.max(1) as f64,
crossover,
horizon as f64 * 1e9 / incremental_query_ns.max(1) as f64,
horizon as f64 * 1e9 / seeded_query_ns.max(1) as f64,
seeds.iter().map(|seed| seed.live_nodes).sum::<usize>(),
reconstruction_ns,
reconstruction_samples,
valid
);
}
}
return;
}
if engine == "batch-seed-compile" {
let branch_cap = optional_budget.unwrap_or(48).min(64);
let sizes: Vec<_> = [500usize, 1000]
.into_iter()
.filter(|&size| size <= vars)
.collect();
assert!(!sizes.is_empty(), "batch compilation requires vars >= 500");
for &scale_vars in &sizes {
for trial in 0..trials {
let formula_seed = 1_100_000 + scale_vars as u64 * 100 + trial as u64;
let formula = generate_formula(family, scale_vars, ratio, formula_seed);
let discovery_start = Instant::now();
let candidates =
fast_detachable_branch_candidates(scale_vars, &formula, branch_cap);
let discovery_us = discovery_start.elapsed().as_micros();
let compile_start = Instant::now();
let mut seeds = Vec::new();
for (mut interior, boundary) in candidates.iter().cloned() {
let mut manager = BddManager::default();
seeds.push(seed_bdd_candidate_in(
scale_vars,
&formula,
&mut interior,
boundary,
"natural",
&mut manager,
));
}
let all_interior: BTreeSet<_> = seeds
.iter()
.flat_map(|seed| seed.interior.iter().copied())
.collect();
let mut core_clauses: Vec<_> = formula
.iter()
.filter(|clause| {
!clause
.0
.iter()
.any(|(variable, _)| all_interior.contains(variable))
})
.cloned()
.collect();
for seed in &seeds {
core_clauses.extend(seed.summary.iter().cloned());
}
let core_to_original: Vec<_> = (0..scale_vars)
.filter(|variable| !all_interior.contains(variable))
.collect();
let mut original_to_core = vec![usize::MAX; scale_vars];
for (core, &original) in core_to_original.iter().enumerate() {
original_to_core[original] = core;
}
for clause in &mut core_clauses {
for (variable, _) in &mut clause.0 {
*variable = original_to_core[*variable];
}
}
let compile_us = compile_start.elapsed().as_micros();
let original_assignment = solve_with_varisat(scale_vars, &formula);
let core_assignment = solve_with_varisat(core_to_original.len(), &core_clauses);
let mut reconstructed = core_assignment.as_ref().map(|core| {
let mut assignment = vec![false; scale_vars];
for (index, &original) in core_to_original.iter().enumerate() {
assignment[original] = core[index];
}
assignment
});
if let Some(assignment) = &mut reconstructed {
for seed in &seeds {
let Some(values) = regrow_bdd_seed(seed, assignment) else {
reconstructed = None;
break;
};
for (index, &variable) in seed.interior.iter().enumerate() {
assignment[variable] = values[index];
}
}
}
let removed = all_interior.len();
println!(
"{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{},{},{},{},{},{}",
family,
scale_vars,
formula.len(),
formula_seed,
branch_cap,
seeds.len(),
removed,
removed as f64 / scale_vars as f64,
discovery_us,
compile_us,
seeds.iter().map(|seed| seed.live_nodes).sum::<usize>(),
seeds.iter().map(|seed| seed.allocated_nodes).sum::<usize>(),
core_to_original.len(),
core_clauses.len(),
original_assignment.is_some(),
core_assignment.is_some(),
original_assignment.is_some() == core_assignment.is_some(),
reconstructed
.as_ref()
.is_some_and(|assignment| satisfies(&formula, assignment)),
removed * 10 >= scale_vars * 3
);
}
}
return;
}
if engine == "batch-branch-discovery" {
let branch_cap = optional_budget.unwrap_or(8).min(64);
let sizes: Vec<_> = [100usize, 250, 500, 1000]
.into_iter()
.filter(|&size| size <= vars)
.collect();
assert!(!sizes.is_empty(), "batch discovery requires vars >= 100");
for &scale_vars in &sizes {
for trial in 0..trials {
let formula_seed = 900_000 + scale_vars as u64 * 100 + trial as u64;
let formula = generate_formula(family, scale_vars, ratio, formula_seed);
let start = Instant::now();
let candidates =
fast_detachable_branch_candidates(scale_vars, &formula, branch_cap);
let discovery_us = start.elapsed().as_micros();
let graph = primal_graph(scale_vars, &formula);
let mut all_interior = BTreeSet::new();
let mut integrity_valid = true;
let mut local_clause_touches = 0usize;
for (interior, boundary) in &candidates {
let interior_set: BTreeSet<_> = interior.iter().copied().collect();
let actual_boundary: BTreeSet<_> = interior
.iter()
.flat_map(|&variable| graph[variable].iter().copied())
.filter(|variable| !interior_set.contains(variable))
.collect();
integrity_valid &= actual_boundary.iter().copied().eq(boundary.iter().copied());
integrity_valid &= interior
.iter()
.all(|variable| all_interior.insert(*variable));
local_clause_touches += formula
.iter()
.filter(|clause| {
clause
.0
.iter()
.any(|(variable, _)| interior_set.contains(variable))
})
.count();
}
let removed = all_interior.len();
println!(
"{},{},{},{},{},{},{},{:.6},{},{},{},{},{}",
family,
scale_vars,
formula.len(),
formula_seed,
branch_cap,
candidates.len(),
removed,
removed as f64 / scale_vars as f64,
candidates
.iter()
.map(|candidate| candidate.1.len())
.sum::<usize>(),
local_clause_touches,
discovery_us,
removed * 10 >= scale_vars * 3,
integrity_valid
);
}
}
return;
}
if engine == "offline-scaling" {
let branch_cap = optional_budget.unwrap_or(8).min(12);
let max_seeds = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize);
let sizes: Vec<_> = [100usize, 250, 500, 1000]
.into_iter()
.filter(|&size| size <= vars)
.collect();
assert!(!sizes.is_empty(), "offline-scaling requires vars >= 100");
let horizons = [10_000usize, 25_000, 50_000, 100_000, 1_000_000];
for &scale_vars in &sizes {
for trial in 0..trials {
let formula_seed = 790_000 + scale_vars as u64 * 100 + trial as u64;
let formula = generate_formula(family, scale_vars, ratio, formula_seed);
for &horizon in &horizons {
let measured = measure_assumption_service_warm(
scale_vars, &formula, branch_cap, max_seeds, 128, horizon,
);
let incremental_per_query =
measured.incremental_query_ns as f64 / horizon as f64;
let seeded_per_query = measured.seeded_query_ns as f64 / horizon as f64;
let crossover = if seeded_per_query < incremental_per_query {
measured
.seeded_setup_ns
.saturating_sub(measured.incremental_setup_ns)
as f64
/ (incremental_per_query - seeded_per_query)
} else {
f64::INFINITY
};
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{:.6},{:.1},{:.1},{:.1},{},{:.1},{}",
family,
scale_vars,
formula.len(),
formula_seed,
horizon,
measured.seeds,
measured.removed,
scale_vars.saturating_sub(measured.removed),
measured.live_nodes,
measured.seeded_setup_ns,
measured.incremental_setup_ns,
measured.incremental_query_ns,
measured.seeded_query_ns,
measured.seeded_query_ns as f64
/ measured.incremental_query_ns.max(1) as f64,
measured.seeded_ns as f64 / measured.incremental_ns.max(1) as f64,
horizon as f64 * 1e9 / measured.incremental_query_ns.max(1) as f64,
horizon as f64 * 1e9 / measured.seeded_query_ns.max(1) as f64,
measured.reconstruction_ns as f64
/ measured.reconstruction_samples.max(1) as f64,
measured.reconstruction_samples,
crossover,
measured.valid
);
}
}
}
return;
}
if engine == "speculative-compile" {
let branch_cap = optional_budget.unwrap_or(8).min(12);
let queries = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(4096usize)
.max(2);
let max_seeds = args
.get(9)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize);
let warmup = 128usize;
let calibration_queries = 128usize;
let families = ["random", "banded", "stacked-flower"];
let sizes = [vars.saturating_sub(10).max(10), vars];
let densities = [2usize, 4usize];
let mut query_training = Vec::new();
for (family_index, training_family) in families.iter().enumerate() {
for &training_vars in &sizes {
for &training_ratio in &densities {
for trial in 0..trials {
let formula_seed = 590_000
+ family_index as u64 * 10_000
+ training_vars as u64 * 100
+ training_ratio as u64 * 10
+ trial as u64;
let formula = generate_formula(
training_family,
training_vars,
training_ratio,
formula_seed,
);
let features = deployment_features(training_vars, &formula, branch_cap);
let measured = measure_assumption_service_warm(
training_vars,
&formula,
branch_cap,
max_seeds,
warmup,
queries,
);
let query_ratio = measured.seeded_query_ns as f64
/ measured.incremental_query_ns.max(1) as f64;
query_training.push((features, query_ratio.max(1e-12).ln()));
}
}
}
}
let query_margin = upper_error_margin(&query_training);
for (family_index, test_family) in families.iter().enumerate() {
for &test_vars in &sizes {
for &test_ratio in &densities {
for trial in 0..trials {
let formula_seed = 690_000
+ family_index as u64 * 10_000
+ test_vars as u64 * 100
+ test_ratio as u64 * 10
+ trial as u64;
let formula =
generate_formula(test_family, test_vars, test_ratio, formula_seed);
let features = deployment_features(test_vars, &formula, branch_cap);
let predicted_query_ratio =
(deployment_knn(&query_training, &features, None) + query_margin).exp();
let mut calibration_solver = Solver::new();
add_to_varisat(&mut calibration_solver, &formula);
let calibration_start = Instant::now();
for query in 0..calibration_queries {
calibration_solver.assume(&[Lit::from_var(
Var::from_index(query % test_vars),
query % 2 == 0,
)]);
calibration_solver
.solve()
.expect("speculative calibration solve");
}
let calibration_ns = calibration_start.elapsed().as_nanos();
let incremental_per_query =
calibration_ns as f64 / calibration_queries as f64;
let budget_ns = if predicted_query_ratio < 1.0 {
(queries as f64 * incremental_per_query * (1.0 - predicted_query_ratio))
as u128
} else {
0
};
let compile_start = Instant::now();
let mut current_vars = test_vars;
let mut current_clauses = formula.clone();
let mut seeds = 0usize;
let mut removed = 0usize;
let mut aborted = budget_ns == 0;
if !aborted {
for _ in 0..max_seeds {
let compiled = seed_detachable_branch_bdd(
current_vars,
¤t_clauses,
branch_cap,
"natural",
);
if compiled.interior.is_empty() {
break;
}
removed += compiled.interior.len();
seeds += 1;
current_vars = compiled.vars;
current_clauses = compiled.clauses;
if compile_start.elapsed().as_nanos() > budget_ns {
aborted = true;
break;
}
}
}
let compile_ns = compile_start.elapsed().as_nanos();
let deployed = !aborted && seeds > 0 && compile_ns <= budget_ns;
let measured = measure_assumption_service_warm(
test_vars, &formula, branch_cap, max_seeds, warmup, queries,
);
let actual_query_ratio = measured.seeded_query_ns as f64
/ measured.incremental_query_ns.max(1) as f64;
for selector in ["off", "always", "speculative", "oracle"] {
let selected = match selector {
"off" => false,
"always" => true,
"speculative" => deployed,
"oracle" => measured.seeded_ns < measured.incremental_ns,
_ => unreachable!(),
};
let policy_ns = match selector {
"speculative" if !deployed => {
measured.incremental_ns + calibration_ns + compile_ns
}
"speculative" => measured.seeded_ns + calibration_ns,
_ if selected => measured.seeded_ns,
_ => measured.incremental_ns,
};
println!(
"{},{},{},{},{},{:.6},{:.6},{},{},{},{},{},{},{},{},{},{},{:.6},{:.6},{},{}",
test_family,
test_vars,
formula.len(),
formula_seed,
selector,
predicted_query_ratio,
query_margin,
budget_ns,
compile_ns,
calibration_ns,
aborted,
deployed,
seeds,
removed,
measured.incremental_ns,
measured.seeded_ns,
policy_ns,
policy_ns as f64 / measured.incremental_ns.max(1) as f64,
actual_query_ratio,
query_training.len(),
measured.valid
);
}
}
}
}
}
return;
}
if engine == "crossover-gate" {
let branch_cap = optional_budget.unwrap_or(8).min(12);
let queries = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(4096usize)
.max(2);
let max_seeds = args
.get(9)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize);
let warmup = 128usize;
let families = ["random", "banded", "stacked-flower"];
let sizes = [vars.saturating_sub(10).max(10), vars];
let densities = [2usize, 4usize];
let mut query_training = Vec::new();
let mut setup_training = Vec::new();
let mut end_training = Vec::new();
let mut end_ratios = Vec::new();
for (family_index, training_family) in families.iter().enumerate() {
for &training_vars in &sizes {
for &training_ratio in &densities {
for trial in 0..trials {
let formula_seed = 390_000
+ family_index as u64 * 10_000
+ training_vars as u64 * 100
+ training_ratio as u64 * 10
+ trial as u64;
let formula = generate_formula(
training_family,
training_vars,
training_ratio,
formula_seed,
);
let features = deployment_features(training_vars, &formula, branch_cap);
let measured = measure_assumption_service_warm(
training_vars,
&formula,
branch_cap,
max_seeds,
warmup,
queries,
);
let query_ratio = measured.seeded_query_ns as f64
/ measured.incremental_query_ns.max(1) as f64;
let incremental_per_query =
measured.incremental_query_ns as f64 / queries as f64;
let setup_queries = measured
.seeded_setup_ns
.saturating_sub(measured.incremental_setup_ns)
as f64
/ incremental_per_query.max(1.0);
let end_ratio =
measured.seeded_ns as f64 / measured.incremental_ns.max(1) as f64;
query_training.push((features.clone(), query_ratio.max(1e-12).ln()));
setup_training.push((features.clone(), setup_queries.ln_1p()));
end_training.push((features, end_ratio.max(1e-12).ln()));
end_ratios.push(end_ratio);
}
}
}
}
let query_margin = upper_error_margin(&query_training);
let setup_margin = upper_error_margin(&setup_training);
let end_predictions: Vec<_> = end_training
.iter()
.enumerate()
.map(|(index, item)| deployment_knn(&end_training, &item.0, Some(index)))
.collect();
let mut thresholds = end_predictions.clone();
thresholds.extend([f64::NEG_INFINITY, f64::INFINITY]);
let end_threshold = thresholds
.into_iter()
.min_by(|left, right| {
let cost = |threshold: f64| {
end_predictions
.iter()
.zip(&end_ratios)
.map(
|(&prediction, &actual)| {
if prediction < threshold { actual } else { 1.0 }
},
)
.sum::<f64>()
};
cost(*left).total_cmp(&cost(*right))
})
.expect("crossover end threshold");
for (family_index, test_family) in families.iter().enumerate() {
for &test_vars in &sizes {
for &test_ratio in &densities {
for trial in 0..trials {
let formula_seed = 490_000
+ family_index as u64 * 10_000
+ test_vars as u64 * 100
+ test_ratio as u64 * 10
+ trial as u64;
let formula =
generate_formula(test_family, test_vars, test_ratio, formula_seed);
let features = deployment_features(test_vars, &formula, branch_cap);
let predicted_query_ratio =
(deployment_knn(&query_training, &features, None) + query_margin).exp();
let predicted_setup_queries =
(deployment_knn(&setup_training, &features, None) + setup_margin).exp()
- 1.0;
let predicted_crossover = if predicted_query_ratio < 1.0 {
predicted_setup_queries / (1.0 - predicted_query_ratio)
} else {
f64::INFINITY
};
let end_prediction = deployment_knn(&end_training, &features, None);
let measured = measure_assumption_service_warm(
test_vars, &formula, branch_cap, max_seeds, warmup, queries,
);
let actual_query_ratio = measured.seeded_query_ns as f64
/ measured.incremental_query_ns.max(1) as f64;
let incremental_per_query =
measured.incremental_query_ns as f64 / queries as f64;
let seeded_per_query = measured.seeded_query_ns as f64 / queries as f64;
let actual_crossover = if seeded_per_query < incremental_per_query {
measured
.seeded_setup_ns
.saturating_sub(measured.incremental_setup_ns)
as f64
/ (incremental_per_query - seeded_per_query)
} else {
f64::INFINITY
};
let actual_ratio =
measured.seeded_ns as f64 / measured.incremental_ns.max(1) as f64;
for selector in [
"off",
"always",
"end-to-end-gate",
"crossover-gate",
"oracle",
] {
let applied = match selector {
"off" => false,
"always" => true,
"end-to-end-gate" => end_prediction < end_threshold,
"crossover-gate" => predicted_crossover < queries as f64,
"oracle" => actual_ratio < 1.0,
_ => unreachable!(),
};
println!(
"{},{},{},{},{},{:.6},{:.6},{:.3},{:.6},{:.3},{},{},{:.6},{:.3},{:.6},{:.6},{},{},{},{},{},{},{},{}",
test_family,
test_vars,
formula.len(),
formula_seed,
selector,
predicted_query_ratio,
query_margin,
predicted_setup_queries,
setup_margin,
predicted_crossover,
queries,
applied,
actual_query_ratio,
actual_crossover,
actual_ratio,
if applied { actual_ratio } else { 1.0 },
measured.seeds,
measured.removed,
measured.incremental_setup_ns,
measured.seeded_setup_ns,
measured.incremental_query_ns,
measured.seeded_query_ns,
query_training.len(),
measured.valid
);
}
}
}
}
}
return;
}
if engine == "deployment-gate" {
let branch_cap = optional_budget.unwrap_or(8).min(12);
let queries = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(4096usize)
.max(2);
let max_seeds = args
.get(9)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize);
let families = ["random", "banded", "stacked-flower"];
let sizes = [vars.saturating_sub(10).max(10), vars];
let densities = [2usize, 4usize];
let mut training = Vec::new();
let mut training_ratios = Vec::new();
for (family_index, training_family) in families.iter().enumerate() {
for &training_vars in &sizes {
for &training_ratio in &densities {
for trial in 0..trials {
let formula_seed = 190_000
+ family_index as u64 * 10_000
+ training_vars as u64 * 100
+ training_ratio as u64 * 10
+ trial as u64;
let formula = generate_formula(
training_family,
training_vars,
training_ratio,
formula_seed,
);
let features = deployment_features(training_vars, &formula, branch_cap);
let measurement = measure_assumption_service(
training_vars,
&formula,
branch_cap,
max_seeds,
queries,
);
let ratio =
measurement.seeded_ns as f64 / measurement.incremental_ns.max(1) as f64;
training.push((features, ratio.max(1e-12).ln()));
training_ratios.push(ratio);
}
}
}
}
let predictions: Vec<_> = training
.iter()
.enumerate()
.map(|(index, item)| deployment_knn(&training, &item.0, Some(index)))
.collect();
let mut thresholds = predictions.clone();
thresholds.push(f64::NEG_INFINITY);
thresholds.push(f64::INFINITY);
let (threshold, oof_policy_ratio) = thresholds
.into_iter()
.map(|candidate| {
let cost = predictions
.iter()
.zip(&training_ratios)
.map(|(&prediction, &actual)| if prediction < candidate { actual } else { 1.0 })
.sum::<f64>()
/ training.len().max(1) as f64;
(candidate, cost)
})
.min_by(|left, right| left.1.total_cmp(&right.1))
.expect("deployment threshold candidates");
let oof_applied = predictions
.iter()
.filter(|&&prediction| prediction < threshold)
.count();
for (family_index, test_family) in families.iter().enumerate() {
for &test_vars in &sizes {
for &test_ratio in &densities {
for trial in 0..trials {
let formula_seed = 290_000
+ family_index as u64 * 10_000
+ test_vars as u64 * 100
+ test_ratio as u64 * 10
+ trial as u64;
let formula =
generate_formula(test_family, test_vars, test_ratio, formula_seed);
let features = deployment_features(test_vars, &formula, branch_cap);
let prediction = deployment_knn(&training, &features, None);
let measurement = measure_assumption_service(
test_vars, &formula, branch_cap, max_seeds, queries,
);
let actual =
measurement.seeded_ns as f64 / measurement.incremental_ns.max(1) as f64;
for selector in ["off", "always", "oof-gate", "oracle"] {
let applied = match selector {
"off" => false,
"always" => true,
"oof-gate" => prediction < threshold,
"oracle" => actual < 1.0,
_ => unreachable!(),
};
let policy = if applied { actual } else { 1.0 };
println!(
"{},{},{},{},{},{:.6},{:.6},{},{:.6},{:.6},{},{},{},{},{},{},{},{:.6},{}",
test_family,
test_vars,
formula.len(),
formula_seed,
selector,
prediction,
threshold,
applied,
actual,
policy,
measurement.seeds,
measurement.removed,
measurement.live_nodes,
measurement.incremental_ns,
measurement.seeded_ns,
training.len(),
oof_applied,
oof_policy_ratio,
measurement.valid
);
}
}
}
}
}
return;
}
if engine == "assumption-payback" {
let branch_cap = optional_budget.unwrap_or(8).min(12);
let queries = args
.get(8)
.and_then(|value| value.parse().ok())
.unwrap_or(128usize)
.max(2);
let max_seeds = args
.get(9)
.and_then(|value| value.parse().ok())
.unwrap_or(8usize);
for seed in 1..=trials {
let formula_seed = 180_000 + seed as u64;
let formula = generate_formula(family, vars, ratio, formula_seed);
let incremental_setup_start = Instant::now();
let mut incremental = Solver::new();
add_to_varisat(&mut incremental, &formula);
let incremental_setup_us = incremental_setup_start.elapsed().as_nanos();
let seed_setup_start = Instant::now();
let mut current_vars = vars;
let mut current_clauses = formula.clone();
let mut current_to_original: Vec<_> = (0..vars).collect();
let mut seeds = Vec::new();
for _ in 0..max_seeds {
let compiled = seed_detachable_branch_bdd(
current_vars,
¤t_clauses,
branch_cap,
"natural",
);
if compiled.interior.is_empty() {
break;
}
current_to_original = compiled
.core_to_original
.iter()
.map(|&previous| current_to_original[previous])
.collect();
current_vars = compiled.vars;
current_clauses = compiled.clauses.clone();
seeds.push(compiled);
}
assert!(
current_vars > 0,
"assumption benchmark requires a nonempty core"
);
let mut seed_solver = Solver::new();
add_to_varisat(&mut seed_solver, ¤t_clauses);
let seed_setup_us = seed_setup_start.elapsed().as_nanos();
let removed: usize = seeds.iter().map(|seed| seed.interior.len()).sum();
let live_nodes: usize = seeds.iter().map(|seed| seed.live_nodes).sum();
let mut cumulative_cold = 0u128;
let mut cumulative_incremental = incremental_setup_us;
let mut cumulative_seed = seed_setup_us;
for query in 0..queries {
let core_variable = query % current_vars;
let original_variable = current_to_original[core_variable];
let value = (query / current_vars + query) % 2 == 0;
let mut cold_formula = formula.clone();
cold_formula.push(Clause(vec![(original_variable, value)]));
let cold_start = Instant::now();
let cold_assignment = solve_with_varisat(vars, &cold_formula);
let cold_us = cold_start.elapsed().as_nanos();
cumulative_cold += cold_us;
incremental.assume(&[Lit::from_var(Var::from_index(original_variable), value)]);
let incremental_start = Instant::now();
let incremental_sat = incremental.solve().expect("incremental assumption solve");
let incremental_query_us = incremental_start.elapsed().as_nanos();
cumulative_incremental += incremental_query_us;
let mut incremental_assignment = vec![false; vars];
if incremental_sat {
for literal in incremental.model().expect("incremental assumption model") {
if literal.var().index() < vars {
incremental_assignment[literal.var().index()] = literal.is_positive();
}
}
}
seed_solver.assume(&[Lit::from_var(Var::from_index(core_variable), value)]);
let seed_query_start = Instant::now();
let seed_sat = seed_solver.solve().expect("seed core assumption solve");
let seed_query_us = seed_query_start.elapsed().as_nanos();
cumulative_seed += seed_query_us;
let mut core_assignment = vec![false; current_vars];
if seed_sat {
for literal in seed_solver.model().expect("seed assumption model") {
if literal.var().index() < current_vars {
core_assignment[literal.var().index()] = literal.is_positive();
}
}
}
let seed_assignment = seed_sat
.then(|| regrow_seed_chain(&seeds, &core_assignment))
.flatten();
let cold_sat = cold_assignment.is_some();
let incremental_valid = !incremental_sat
|| (incremental_assignment[original_variable] == value
&& satisfies(&formula, &incremental_assignment));
let seed_valid = seed_assignment.as_ref().is_none_or(|assignment| {
assignment[original_variable] == value && satisfies(&formula, assignment)
});
println!(
"{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{},{},{},{},{},{},{},{},{}",
family,
vars,
formula.len(),
formula_seed,
query,
original_variable,
value,
cold_us,
incremental_setup_us,
incremental_query_us,
cumulative_incremental as f64 / cumulative_cold.max(1) as f64,
seed_setup_us,
seed_query_us,
cumulative_seed as f64 / cumulative_cold.max(1) as f64,
seeds.len(),
removed,
current_vars,
live_nodes,
cold_sat,
incremental_sat == cold_sat,
seed_sat == cold_sat,
incremental_valid,
seed_valid
);
}
}
return;
}
if engine == "exact-width-helper" {
assert!(
vars <= 16,
"exact-width-helper supports at most 16 original variables"
);
let candidate_limit = optional_budget.unwrap_or(12);
for seed in 1..=trials {
let formula = generate_formula(family, vars, ratio, 100_000 + seed as u64);
let original_treewidth = exact_treewidth(vars, &formula);
let original_order = min_fill_order(vars, &formula);
let mut original_rank = vec![0usize; vars];
for (level, &variable) in original_order.iter().enumerate() {
original_rank[variable] = level;
}
let original_mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(v, s)| (original_rank[v], s))
.collect(),
)
})
.collect();
let original_result = solve_tuple_natural(vars, &original_mapped);
let candidates: Vec<_> = recurring_pair_candidates(&formula)
.into_iter()
.take(candidate_limit)
.filter_map(|(pair, frequency)| {
add_pair_helper(vars, &formula, pair).map(|expanded| {
let width = exact_treewidth(vars + 1, &expanded);
let osmotic = osmotic_helper_score(vars, &formula, pair, frequency);
(pair, frequency, osmotic, width, expanded)
})
})
.collect();
let frequency_choice = candidates
.iter()
.max_by_key(|candidate| candidate.1)
.map(|candidate| candidate.0);
let osmotic_choice = candidates
.iter()
.max_by(|a, b| a.2.total_cmp(&b.2))
.map(|candidate| candidate.0);
let oracle_choice = candidates
.iter()
.min_by_key(|candidate| candidate.3)
.map(|candidate| candidate.0);
for (selector, choice) in [
("original", None),
("frequency", frequency_choice),
("osmosis", osmotic_choice),
("oracle", oracle_choice),
] {
let selected =
choice.and_then(|pair| candidates.iter().find(|candidate| candidate.0 == pair));
let (expanded_vars, expanded, final_treewidth) = selected.map_or_else(
|| (vars, formula.clone(), original_treewidth),
|candidate| (vars + 1, candidate.4.clone(), candidate.3),
);
let order = min_fill_order(expanded_vars, &expanded);
let mut rank = vec![0usize; expanded_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = expanded
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let result = solve_tuple_natural(expanded_vars, &mapped);
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{}",
family,
vars,
formula.len(),
100_000 + seed as u64,
selector,
candidates.len(),
selected.is_some(),
original_treewidth,
final_treewidth,
final_treewidth as isize - original_treewidth as isize,
original_result.nodes,
result.nodes,
result.nodes as f64 / original_result.nodes.max(1) as f64,
result.assignment.is_some() == original_result.assignment.is_some(),
result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment))
);
}
}
return;
}
if engine == "helper-width-scaling" {
let budget = optional_budget.unwrap_or((vars / 10).max(1));
for seed in 1..=trials {
let formula = generate_formula(family, vars, ratio, 90_000 + seed as u64);
let original_order = min_fill_order(vars, &formula);
let mut original_rank = vec![0usize; vars];
for (level, &variable) in original_order.iter().enumerate() {
original_rank[variable] = level;
}
let original_mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(v, s)| (original_rank[v], s))
.collect(),
)
})
.collect();
let original_result = solve_tuple_natural(vars, &original_mapped);
let (frequency_vars, frequency_formula, frequency_accepted) =
expand_recurring_pairs(vars, &formula, budget);
let (scent_vars, scent_formula, scent_accepted, _) =
scent_batch_expand(vars, &formula, budget, 4);
for (variant, expanded_vars, expanded, accepted) in [
("original", vars, formula.clone(), 0),
(
"frequency",
frequency_vars,
frequency_formula,
frequency_accepted,
),
("scent", scent_vars, scent_formula, scent_accepted),
] {
let order = min_fill_order(expanded_vars, &expanded);
let (width, estimated_work) = elimination_cost(expanded_vars, &expanded, &order);
let mut rank = vec![0usize; expanded_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = expanded
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let start = Instant::now();
let result = solve_tuple_natural(expanded_vars, &mapped);
let solve_us = start.elapsed().as_micros();
println!(
"{},{},{},{},{},{},{},{},{},{},{:.3},{},{},{},{},{}",
family,
vars,
formula.len(),
90_000 + seed as u64,
variant,
budget,
accepted,
expanded_vars,
expanded.len(),
width,
estimated_work,
solve_us,
result.nodes,
result.assignment.is_some(),
result.assignment.is_some() == original_result.assignment.is_some(),
result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment))
);
}
}
return;
}
if engine == "structure-scaling" {
for seed in 1..=trials {
let formula = generate_formula(family, vars, ratio, 80_000 + seed as u64);
let order = min_fill_order(vars, &formula);
let (width, estimated_work) = elimination_cost(vars, &formula, &order);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let start = Instant::now();
let result = solve_tuple_natural(vars, &mapped);
let solve_us = start.elapsed().as_micros();
let valid = result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment));
println!(
"{},{},{},{},{},{:.3},{},{},{},{}",
family,
vars,
mapped.len(),
80_000 + seed as u64,
width,
estimated_work,
solve_us,
result.nodes,
result.assignment.is_some(),
valid
);
}
return;
}
if engine == "kernel-scaling" {
for test_family in ["random", "banded"] {
for seed in 1..=trials {
let formula = generate_formula(test_family, vars, ratio, 70_000 + seed as u64);
let order = min_fill_order(vars, &formula);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let natural: Vec<_> = (0..vars).collect();
let run_ordinary = || {
let start = Instant::now();
let result = eliminate_with_bdds(vars, &mapped, &natural);
(start.elapsed().as_micros(), result)
};
let run_specialized = || {
let start = Instant::now();
let result = solve_tuple_natural(vars, &mapped);
(start.elapsed().as_micros(), result)
};
let (ordinary_us, ordinary, specialized_us, specialized) = if seed % 2 == 0 {
let (specialized_us, specialized) = run_specialized();
let (ordinary_us, ordinary) = run_ordinary();
(ordinary_us, ordinary, specialized_us, specialized)
} else {
let (ordinary_us, ordinary) = run_ordinary();
let (specialized_us, specialized) = run_specialized();
(ordinary_us, ordinary, specialized_us, specialized)
};
let agrees = ordinary.assignment.is_some() == specialized.assignment.is_some();
for (kernel, solve_us, nodes, assignment) in [
(
"runtime-order",
ordinary_us,
ordinary.allocated_nodes,
ordinary.assignment.as_ref(),
),
(
"natural-specialized",
specialized_us,
specialized.nodes,
specialized.assignment.as_ref(),
),
] {
println!(
"{},{},{},{},{},{},{},{},{}",
test_family,
vars,
mapped.len(),
70_000 + seed as u64,
kernel,
solve_us,
nodes,
assignment.is_some(),
agrees
&& assignment.is_none_or(|item| satisfies(&mapped, item))
&& nodes == ordinary.allocated_nodes
);
}
}
}
return;
}
if engine == "direct-layout-ablation" {
let cases: Vec<(String, usize, Vec<Clause>)> = if family == "external" {
let directory = args
.get(7)
.map(String::as_str)
.unwrap_or("benchmarks/satlib");
let mut paths: Vec<_> = fs::read_dir(directory)
.expect("read external benchmark directory")
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|extension| extension == "cnf"))
.collect();
paths.sort();
paths
.into_iter()
.map(|path| {
let (case_vars, formula) = parse_dimacs(&path).unwrap();
(
path.file_name().unwrap().to_string_lossy().into_owned(),
case_vars,
formula,
)
})
.collect()
} else {
(1..=trials)
.map(|seed| {
(
family.to_string(),
vars,
generate_formula(family, vars, ratio, 60_000 + seed as u64),
)
})
.collect()
};
let names = [
"ordinary",
"tuple-natural",
"layout",
"provenance",
"checkpoints",
"full",
];
for (index, (case_name, case_vars, formula)) in cases.into_iter().enumerate() {
let order = min_fill_order(case_vars, &formula);
let mut rank = vec![0usize; case_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let natural: Vec<_> = (0..case_vars).collect();
let mut results: Vec<Option<(u128, DirectVariantResult)>> =
(0..names.len()).map(|_| None).collect();
for offset in 0..names.len() {
let variant = (index + offset) % names.len();
let start = Instant::now();
let result = if variant == 0 {
let ordinary = eliminate_with_bdds(case_vars, &mapped, &natural);
DirectVariantResult {
assignment: ordinary.assignment,
nodes: ordinary.allocated_nodes,
}
} else if variant == 1 {
solve_tuple_natural(case_vars, &mapped)
} else {
solve_provenance_variant(
case_vars,
&mapped,
variant == 3 || variant == 5,
variant == 4 || variant == 5,
4,
)
};
results[variant] = Some((start.elapsed().as_micros(), result));
}
let direct_us = results[0].as_ref().unwrap().0;
let direct_nodes = results[0].as_ref().unwrap().1.nodes;
let direct_sat = results[0].as_ref().unwrap().1.assignment.is_some();
for (variant, name) in names.iter().enumerate() {
let (solve_us, result) = results[variant].as_ref().unwrap();
let valid = result.assignment.is_some() == direct_sat
&& result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment));
println!(
"{},{},{},{},{},{},{},{:.6},{},{},{:.6},{},{}",
case_name,
case_vars,
mapped.len(),
index + 1,
name,
solve_us,
direct_us,
direct_us as f64 / (*solve_us).max(1) as f64,
result.nodes,
direct_nodes,
result.nodes as f64 / direct_nodes.max(1) as f64,
result.assignment.is_some(),
valid
);
}
}
return;
}
if engine == "direct-provenance-portfolio" {
let training_trials = (trials / 2).max(2);
let mut training = Vec::new();
for training_vars in [24, 30, 36] {
for training_ratio in [3, 4, 5] {
for training_family in ["random", "banded"] {
for training_seed in 1..=training_trials {
let formula = generate_formula(
training_family,
training_vars,
training_ratio,
training_seed as u64,
);
let order = min_fill_order(training_vars, &formula);
let mut rank = vec![0usize; training_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect())
})
.collect();
let natural: Vec<_> = (0..training_vars).collect();
let (direct_us, provenance_us) = if training_seed % 2 == 0 {
let provenance_start = Instant::now();
let _provenance =
build_incremental_bdd_cache_with_stride(training_vars, &mapped, 4);
let provenance_us = provenance_start.elapsed().as_micros();
let direct_start = Instant::now();
let _direct = eliminate_with_bdds(training_vars, &mapped, &natural);
(direct_start.elapsed().as_micros().max(1), provenance_us)
} else {
let direct_start = Instant::now();
let _direct = eliminate_with_bdds(training_vars, &mapped, &natural);
let direct_us = direct_start.elapsed().as_micros().max(1);
let provenance_start = Instant::now();
let _provenance =
build_incremental_bdd_cache_with_stride(training_vars, &mapped, 4);
(direct_us, provenance_start.elapsed().as_micros())
};
training.push((
cheap_structure_features(training_vars, &formula),
(provenance_us as f64 / direct_us as f64).max(1e-9).ln(),
));
}
}
}
}
let cases: Vec<(String, usize, Vec<Clause>)> = if family == "external" {
let directory = args
.get(7)
.map(String::as_str)
.unwrap_or("benchmarks/satlib");
let mut paths: Vec<_> = fs::read_dir(directory)
.expect("read external benchmark directory")
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|extension| extension == "cnf"))
.collect();
paths.sort();
paths
.into_iter()
.map(|path| {
let (case_vars, formula) = parse_dimacs(&path).unwrap();
(
path.file_name().unwrap().to_string_lossy().into_owned(),
case_vars,
formula,
)
})
.collect()
} else {
(1..=(trials - training_trials).max(1))
.map(|seed| {
(
family.to_string(),
vars,
generate_formula(family, vars, ratio, 50_000 + seed as u64),
)
})
.collect()
};
for (index, (case_name, case_vars, formula)) in cases.into_iter().enumerate() {
let order = min_fill_order(case_vars, &formula);
let mut rank = vec![0usize; case_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let natural: Vec<_> = (0..case_vars).collect();
let (direct, direct_us, provenance, provenance_us) = if index % 2 == 0 {
let provenance_start = Instant::now();
let provenance = build_incremental_bdd_cache_with_stride(case_vars, &mapped, 4);
let provenance_us = provenance_start.elapsed().as_micros();
let direct_start = Instant::now();
let direct = eliminate_with_bdds(case_vars, &mapped, &natural);
(
direct,
direct_start.elapsed().as_micros(),
provenance,
provenance_us,
)
} else {
let direct_start = Instant::now();
let direct = eliminate_with_bdds(case_vars, &mapped, &natural);
let direct_us = direct_start.elapsed().as_micros();
let provenance_start = Instant::now();
let provenance = build_incremental_bdd_cache_with_stride(case_vars, &mapped, 4);
(
direct,
direct_us,
provenance,
provenance_start.elapsed().as_micros(),
)
};
let decision_start = Instant::now();
let prediction = scent_gate_predict(
&training,
&cheap_structure_features(case_vars, &formula),
11,
);
let decision_us = decision_start.elapsed().as_micros();
for selector in ["direct", "provenance", "cost-gate"] {
let apply =
selector == "provenance" || (selector == "cost-gate" && prediction < -0.10);
let policy = if apply { "provenance" } else { "direct" };
let (sat, solve_us, nodes, valid) = if apply {
(
provenance.assignment.is_some(),
provenance_us,
provenance.manager.nodes.len(),
provenance
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment))
&& provenance.assignment.is_some() == direct.assignment.is_some(),
)
} else {
(
direct.assignment.is_some(),
direct_us,
direct.allocated_nodes,
direct
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment)),
)
};
let charged_decision = if selector == "cost-gate" {
decision_us
} else {
0
};
let total_us = charged_decision + solve_us;
println!(
"{},{},{},{},{},{:.6},{},{},{},{},{},{},{:.6},{},{},{:.6},{}",
case_name,
case_vars,
mapped.len(),
index + 1,
selector,
prediction,
policy,
sat,
charged_decision,
solve_us,
total_us,
direct_us,
direct_us as f64 / total_us.max(1) as f64,
nodes,
direct.allocated_nodes,
nodes as f64 / direct.allocated_nodes.max(1) as f64,
valid
);
}
}
return;
}
if engine == "replay-metrics" {
let cases: Vec<(String, usize, Vec<Clause>)> = if family == "external" {
let directory = args
.get(7)
.map(String::as_str)
.unwrap_or("benchmarks/satlib");
let mut paths: Vec<_> = fs::read_dir(directory)
.expect("read external benchmark directory")
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|extension| extension == "cnf"))
.collect();
paths.sort();
paths
.into_iter()
.map(|path| {
let (case_vars, formula) = parse_dimacs(&path).unwrap();
(
path.file_name().unwrap().to_string_lossy().into_owned(),
case_vars,
formula,
)
})
.collect()
} else {
(1..=trials)
.map(|seed| {
(
family.to_string(),
vars,
generate_formula(family, vars, ratio, seed as u64),
)
})
.collect()
};
for (index, (case_name, case_vars, formula)) in cases.into_iter().enumerate() {
let order = min_fill_order(case_vars, &formula);
let mut rank = vec![0usize; case_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let natural: Vec<_> = (0..case_vars).collect();
let direct_start = Instant::now();
let direct = eliminate_with_bdds(case_vars, &mapped, &natural);
let direct_us = direct_start.elapsed().as_micros();
let reuse = evaluate_branch_choice(case_vars, &mapped, case_vars - 1, 4);
println!(
"{},{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{},{},{},{},{:.6},{}",
case_name,
case_vars,
mapped.len(),
index + 1,
reuse.satisfiable,
reuse.first_satisfiable,
reuse.branches,
direct_us,
reuse.reuse_us,
direct_us as f64 / reuse.reuse_us.max(1) as f64,
reuse.cache_build_us,
reuse.sibling_us,
reuse.cache_nodes,
reuse.sibling_new_nodes,
reuse.checkpoint_count,
reuse.restored_layer,
reuse.replayed_layers,
direct.allocated_nodes,
reuse.reuse_nodes,
reuse.reuse_nodes as f64 / direct.allocated_nodes.max(1) as f64,
reuse.valid && reuse.satisfiable == direct.assignment.is_some()
);
}
return;
}
if engine == "robust-reuse-portfolio"
|| engine == "external-reuse-portfolio"
|| engine == "external-ablation"
{
let ablation = engine == "external-ablation";
let external = engine == "external-reuse-portfolio" || ablation;
let training_trials = (trials / 2).max(2);
let test_trials = (trials - training_trials).max(1);
let mut records = Vec::new();
let mut regime = 0usize;
for training_vars in [24, 30, 36] {
for training_ratio in [3, 4, 5] {
for training_family in ["random", "banded"] {
for training_seed in 1..=training_trials {
let formula = generate_formula(
training_family,
training_vars,
training_ratio,
training_seed as u64,
);
let order = min_fill_order(training_vars, &formula);
let mut rank = vec![0usize; training_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect())
})
.collect();
let natural: Vec<_> = (0..training_vars).collect();
let direct_start = Instant::now();
let _direct = eliminate_with_bdds(training_vars, &mapped, &natural);
let direct_us = direct_start.elapsed().as_micros().max(1);
let reuse =
evaluate_branch_choice(training_vars, &mapped, training_vars - 1, 4);
records.push((
cheap_structure_features(training_vars, &formula),
(reuse.reuse_us as f64 / direct_us as f64).max(1e-9).ln(),
regime,
));
}
regime += 1;
}
}
}
let training: Vec<_> = records.iter().map(|item| (item.0, item.1)).collect();
let (prediction_threshold, distance_threshold, _, _) = learn_regime_rejection(&records, 11);
let cases: Vec<(String, usize, Vec<Clause>)> = if external {
let directory = args
.get(7)
.map(String::as_str)
.unwrap_or("benchmarks/satlib");
let mut paths: Vec<_> = fs::read_dir(directory)
.expect("read external benchmark directory")
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|extension| extension == "cnf"))
.collect();
paths.sort();
paths
.into_iter()
.map(|path| {
let (case_vars, formula) =
parse_dimacs(&path).unwrap_or_else(|error| panic!("{error}"));
(
path.file_name().unwrap().to_string_lossy().into_owned(),
case_vars,
formula,
)
})
.collect()
} else {
(1..=test_trials)
.map(|test_index| {
let test_seed = 40_000 + test_index as u64;
(
family.to_string(),
vars,
generate_formula(family, vars, ratio, test_seed),
)
})
.collect()
};
for (test_index, (case_name, vars, formula)) in cases.into_iter().enumerate() {
let test_seed = 40_001 + test_index as u64;
let family = case_name.as_str();
let order = min_fill_order(vars, &formula);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let natural: Vec<_> = (0..vars).collect();
let direct_start = Instant::now();
let direct = eliminate_with_bdds(vars, &mapped, &natural);
let direct_us = direct_start.elapsed().as_micros();
let decision_start = Instant::now();
let features = cheap_structure_features(vars, &formula);
let prediction = scent_gate_predict(&training, &features, 11);
let distance = support_distance(&training, &features);
let decision_us = decision_start.elapsed().as_micros();
let (reuse, forced) = if ablation && test_index % 2 == 0 {
let forced = evaluate_forced_branch_pair(vars, &mapped, vars - 1, 4);
let reuse = evaluate_branch_choice(vars, &mapped, vars - 1, 4);
(reuse, Some(forced))
} else {
let reuse = evaluate_branch_choice(vars, &mapped, vars - 1, 4);
let forced =
ablation.then(|| evaluate_forced_branch_pair(vars, &mapped, vars - 1, 4));
(reuse, forced)
};
let portfolio_reuse =
prediction < prediction_threshold && distance <= distance_threshold;
let selectors: &[&str] = if ablation {
&[
"direct",
"complete",
"no-gate",
"no-reuse",
"fresh-no-gate",
"no-early-stop",
"forced-no-gate",
"cost-gate",
]
} else {
&["direct", "reuse", "portfolio", "cost-gate"]
};
for &selector in selectors {
let apply_reuse = selector == "reuse"
|| selector == "no-gate"
|| (selector == "cost-gate" && prediction < prediction_threshold)
|| ((selector == "portfolio" || selector == "complete") && portfolio_reuse);
let apply_fresh =
(selector == "no-reuse" && portfolio_reuse) || selector == "fresh-no-gate";
let apply_forced = (selector == "no-early-stop" && portfolio_reuse)
|| selector == "forced-no-gate";
let policy = if apply_reuse {
"reuse"
} else if apply_fresh {
"fresh-branch"
} else if apply_forced {
"forced-pair"
} else {
"direct"
};
let (branches, satisfiable, solve_us, work_nodes, valid) = if apply_reuse {
(
reuse.branches,
reuse.satisfiable,
reuse.reuse_us,
reuse.reuse_nodes,
reuse.valid && reuse.satisfiable == direct.assignment.is_some(),
)
} else if apply_fresh {
(
reuse.branches,
reuse.satisfiable,
reuse.fresh_us,
reuse.fresh_nodes,
reuse.valid && reuse.satisfiable == direct.assignment.is_some(),
)
} else if apply_forced {
let forced = forced.as_ref().unwrap();
(
forced.branches,
forced.satisfiable,
forced.reuse_us,
forced.reuse_nodes,
forced.valid && forced.satisfiable == direct.assignment.is_some(),
)
} else {
(
1,
direct.assignment.is_some(),
direct_us,
direct.allocated_nodes,
direct
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment)),
)
};
let charged_decision = if matches!(
selector,
"portfolio" | "complete" | "no-reuse" | "no-early-stop" | "cost-gate"
) {
decision_us
} else {
0
};
let total_us = charged_decision + solve_us;
println!(
"{},{},{},{},{},{:.6},{:.6},{:.6},{:.6},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{}",
family,
vars,
mapped.len(),
test_seed,
selector,
prediction,
distance,
prediction_threshold,
distance_threshold,
policy,
branches,
satisfiable,
charged_decision,
solve_us,
total_us,
direct_us,
direct_us as f64 / total_us.max(1) as f64,
work_nodes,
direct.allocated_nodes,
work_nodes as f64 / direct.allocated_nodes.max(1) as f64,
valid
);
}
}
return;
}
if engine == "branch-portfolio"
|| engine == "cheap-branch-portfolio"
|| engine == "three-action-portfolio"
|| engine == "portfolio-generalization"
{
let generalization = engine == "portfolio-generalization";
let three_action = engine == "three-action-portfolio" || generalization;
let cheap_gate = engine != "branch-portfolio";
let model_vars = if generalization { 30 } else { vars };
let model_ratio = if generalization { 4 } else { ratio };
let training_trials = (trials / 2).max(1);
let test_trials = (trials - training_trials).max(1);
let mut branch_training = Vec::new();
let mut structure_training = Vec::new();
for training_seed in 1..=training_trials {
for training_family in ["random", "banded"] {
let formula = generate_formula(
training_family,
model_vars,
model_ratio,
training_seed as u64,
);
let structure_features = if cheap_gate {
cheap_structure_features(model_vars, &formula)
} else {
scent_gate_features(model_vars, &formula)
};
structure_training
.push((structure_features, f64::from(training_family == "banded")));
if training_family == "random" {
let order = min_fill_order(model_vars, &formula);
let mut rank = vec![0usize; model_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let feature_matrix = supervised_branch_feature_matrix(model_vars, &mapped);
for variable in 0..model_vars {
let result = evaluate_branch_choice(model_vars, &mapped, variable, 4);
branch_training.push((
feature_matrix[variable].clone(),
(result.reuse_nodes as f64 + 1.0).ln(),
));
}
}
}
}
let mut benefit_training = Vec::new();
let mut reuse_benefit_training = Vec::new();
if cheap_gate {
for training_seed in 1..=training_trials {
for &training_family in if three_action {
&["random", "banded"][..]
} else {
&["random"][..]
} {
let formula = generate_formula(
training_family,
model_vars,
model_ratio,
training_seed as u64,
);
let order = min_fill_order(model_vars, &formula);
let mut rank = vec![0usize; model_vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let natural: Vec<_> = (0..model_vars).collect();
let direct_start = Instant::now();
let _direct = eliminate_with_bdds(model_vars, &mapped, &natural);
let direct_us = direct_start.elapsed().as_micros().max(1);
let inference_start = Instant::now();
let matrix = supervised_branch_feature_matrix(model_vars, &mapped);
let variable = (0..model_vars)
.min_by(|&a, &b| {
vector_knn_predict(&branch_training, &matrix[a], 15)
.total_cmp(&vector_knn_predict(&branch_training, &matrix[b], 15))
})
.unwrap();
let inference_us = inference_start.elapsed().as_micros();
let branch = evaluate_branch_choice(model_vars, &mapped, variable, 4);
let ratio = (inference_us + branch.reuse_us) as f64 / direct_us as f64;
benefit_training.push((
cheap_structure_features(model_vars, &formula),
ratio.max(1e-9).ln(),
));
let reuse = evaluate_branch_choice(model_vars, &mapped, model_vars - 1, 4);
reuse_benefit_training.push((
cheap_structure_features(model_vars, &formula),
(reuse.reuse_us as f64 / direct_us as f64).max(1e-9).ln(),
));
}
}
}
let (benefit_threshold, _, _) = if cheap_gate {
learn_scent_gate_threshold(&benefit_training, 7)
} else {
(f64::NEG_INFINITY, 1.0, 0)
};
let (reuse_threshold, _, _) = if three_action {
learn_scent_gate_threshold(&reuse_benefit_training, 7)
} else {
(f64::NEG_INFINITY, 1.0, 0)
};
let test_families: Vec<&str> = if generalization {
vec![family]
} else {
vec!["random", "banded"]
};
for test_family in test_families {
for test_index in 1..=test_trials {
let test_seed = 30_000 + test_index as u64;
let formula = generate_formula(test_family, vars, ratio, test_seed);
let order = min_fill_order(vars, &formula);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let natural: Vec<_> = (0..vars).collect();
let direct_start = Instant::now();
let direct = eliminate_with_bdds(vars, &mapped, &natural);
let direct_us = direct_start.elapsed().as_micros();
let structure_start = Instant::now();
let structure_features = if cheap_gate {
cheap_structure_features(vars, &formula)
} else {
scent_gate_features(vars, &formula)
};
let structure_prediction = if three_action {
0.0
} else {
scent_gate_predict(&structure_training, &structure_features, 7)
};
let benefit_prediction = if cheap_gate {
scent_gate_predict(&benefit_training, &structure_features, 7)
} else {
0.0
};
let reuse_prediction = if three_action {
scent_gate_predict(&reuse_benefit_training, &structure_features, 7)
} else {
0.0
};
let structure_us = structure_start.elapsed().as_micros();
let branch_start = Instant::now();
let feature_matrix = supervised_branch_feature_matrix(vars, &mapped);
let predictions: Vec<_> = (0..vars)
.map(|variable| {
vector_knn_predict(&branch_training, &feature_matrix[variable], 15)
})
.collect();
let branch_us = branch_start.elapsed().as_micros();
let supervised_variable = (0..vars)
.min_by(|&a, &b| predictions[a].total_cmp(&predictions[b]))
.unwrap();
let impact_scores = branching_scores(vars, &mapped, 0.0);
let impact_variable = (0..vars)
.max_by(|&a, &b| impact_scores[a].total_cmp(&impact_scores[b]))
.unwrap();
let results: Vec<_> = (0..vars)
.map(|variable| evaluate_branch_choice(vars, &mapped, variable, 4))
.collect();
let oracle_variable = (0..vars)
.min_by_key(|&variable| results[variable].reuse_nodes)
.unwrap();
let oracle_nodes = direct
.allocated_nodes
.min(results[oracle_variable].reuse_nodes);
let portfolio_policy = if three_action {
let supervised_allowed = benefit_prediction < benefit_threshold;
let reuse_allowed = reuse_prediction < reuse_threshold;
if supervised_allowed
&& (!reuse_allowed || benefit_prediction < reuse_prediction)
{
"supervised"
} else if reuse_allowed {
"reuse"
} else {
"direct"
}
} else if cheap_gate {
if structure_prediction < 0.5 && benefit_prediction < benefit_threshold {
"supervised"
} else {
"direct"
}
} else if structure_prediction < 0.25 {
"supervised"
} else if structure_prediction > 0.75 {
"reuse"
} else {
"direct"
};
for selector in [
"direct",
"impact",
"reuse",
"supervised",
"portfolio",
"oracle",
] {
let policy = match selector {
"portfolio" => portfolio_policy,
"oracle" => {
if direct.allocated_nodes <= results[oracle_variable].reuse_nodes {
"direct"
} else {
"oracle-branch"
}
}
other => other,
};
let variable = match policy {
"impact" => Some(impact_variable),
"reuse" => Some(vars - 1),
"supervised" => Some(supervised_variable),
"oracle-branch" => Some(oracle_variable),
_ => None,
};
let decision_us = match selector {
"supervised" => branch_us,
"portfolio" => {
structure_us + if policy == "supervised" { branch_us } else { 0 }
}
_ => 0,
};
let (branches, satisfiable, solve_us, work_nodes, valid) =
if let Some(variable) = variable {
let result = &results[variable];
(
result.branches,
result.satisfiable,
result.reuse_us,
result.reuse_nodes,
result.valid && result.satisfiable == direct.assignment.is_some(),
)
} else {
(
1,
direct.assignment.is_some(),
direct_us,
direct.allocated_nodes,
direct
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment)),
)
};
let total_us = decision_us + solve_us;
let branch_level = variable.map_or(usize::MAX, |item| item);
let branch_variable = variable.map_or(usize::MAX, |item| order[item]);
println!(
"{},{},{},{},{},{:.6},{},{},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{},{:.6},{}",
test_family,
vars,
mapped.len(),
test_seed,
selector,
if three_action {
benefit_prediction.min(reuse_prediction)
} else if cheap_gate {
benefit_prediction
} else {
structure_prediction
},
policy,
branch_level,
branch_variable,
branches,
satisfiable,
decision_us,
solve_us,
total_us,
direct_us,
direct_us as f64 / total_us.max(1) as f64,
work_nodes,
direct.allocated_nodes,
work_nodes as f64 / direct.allocated_nodes.max(1) as f64,
oracle_nodes,
work_nodes as f64 / oracle_nodes.max(1) as f64,
valid
);
}
}
}
return;
}
if engine == "supervised-branch" {
let training_trials = (trials / 2).max(1);
let test_trials = (trials - training_trials).max(1);
let mut training = Vec::new();
for training_seed in 1..=training_trials {
let formula = generate_formula(family, vars, ratio, training_seed as u64);
let order = min_fill_order(vars, &formula);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let feature_matrix = supervised_branch_feature_matrix(vars, &mapped);
for variable in 0..vars {
let result = evaluate_branch_choice(vars, &mapped, variable, 4);
training.push((
feature_matrix[variable].clone(),
(result.reuse_nodes as f64 + 1.0).ln(),
));
}
}
for test_index in 1..=test_trials {
let test_seed = 20_000 + test_index as u64;
let formula = generate_formula(family, vars, ratio, test_seed);
let order = min_fill_order(vars, &formula);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let inference_start = Instant::now();
let feature_matrix = supervised_branch_feature_matrix(vars, &mapped);
let predictions: Vec<_> = (0..vars)
.map(|variable| vector_knn_predict(&training, &feature_matrix[variable], 15))
.collect();
let inference_us = inference_start.elapsed().as_micros();
let supervised_variable = (0..vars)
.min_by(|&a, &b| predictions[a].total_cmp(&predictions[b]))
.unwrap();
let impact_scores = branching_scores(vars, &mapped, 0.0);
let impact_variable = (0..vars)
.max_by(|&a, &b| impact_scores[a].total_cmp(&impact_scores[b]))
.unwrap();
let random_variable = Rng(test_seed).below(vars);
let results: Vec<_> = (0..vars)
.map(|variable| evaluate_branch_choice(vars, &mapped, variable, 4))
.collect();
let oracle_variable = (0..vars)
.min_by_key(|&variable| results[variable].reuse_nodes)
.unwrap();
let oracle_nodes = results[oracle_variable].reuse_nodes;
for (selector, variable) in [
("supervised", supervised_variable),
("impact", impact_variable),
("reuse", vars - 1),
("random", random_variable),
("oracle", oracle_variable),
] {
let result = &results[variable];
println!(
"{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{:.6},{},{},{:.6},{},{:.6},{}",
family,
vars,
mapped.len(),
test_seed,
selector,
variable,
order[variable],
predictions[variable],
inference_us,
result.branches,
result.satisfiable,
result.reuse_us,
result.fresh_us,
result.fresh_us as f64 / result.reuse_us.max(1) as f64,
result.reuse_nodes,
oracle_nodes,
result.reuse_nodes as f64 / oracle_nodes.max(1) as f64,
result.fresh_nodes,
result.reuse_nodes as f64 / result.fresh_nodes.max(1) as f64,
result.valid
);
}
}
return;
}
if engine == "joint-branch" {
let training_trials = (trials / 2).max(1);
let test_trials = (trials - training_trials).max(1);
let alphas = [0.0f64, 0.25, 0.5, 1.0, 2.0, 4.0];
let mut training_work = vec![0usize; alphas.len()];
for training_seed in 1..=training_trials {
let formula = generate_formula(family, vars, ratio, training_seed as u64);
let order = min_fill_order(vars, &formula);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
for (index, &alpha) in alphas.iter().enumerate() {
let scores = branching_scores(vars, &mapped, alpha);
let variable = (0..vars)
.max_by(|&a, &b| scores[a].total_cmp(&scores[b]))
.unwrap();
training_work[index] +=
evaluate_branch_choice(vars, &mapped, variable, 4).reuse_nodes;
}
}
let best_alpha_index = (0..alphas.len())
.min_by_key(|&index| training_work[index])
.unwrap();
let learned_alpha = alphas[best_alpha_index];
for test_index in 1..=test_trials {
let test_seed = 10_000 + test_index as u64;
let formula = generate_formula(family, vars, ratio, test_seed);
let order = min_fill_order(vars, &formula);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let impact_scores = branching_scores(vars, &mapped, 0.0);
let joint_scores = branching_scores(vars, &mapped, learned_alpha);
let impact_variable = (0..vars)
.max_by(|&a, &b| impact_scores[a].total_cmp(&impact_scores[b]))
.unwrap();
let joint_variable = (0..vars)
.max_by(|&a, &b| joint_scores[a].total_cmp(&joint_scores[b]))
.unwrap();
let random_variable = Rng(test_seed).below(vars);
let mut oracle_results: Vec<_> = (0..vars)
.map(|variable| (variable, evaluate_branch_choice(vars, &mapped, variable, 4)))
.collect();
let oracle_index = (0..oracle_results.len())
.min_by_key(|&index| oracle_results[index].1.reuse_nodes)
.unwrap();
let (oracle_variable, oracle_result) = oracle_results.swap_remove(oracle_index);
let strategies = [
("impact", impact_variable),
("reuse", vars - 1),
("joint", joint_variable),
("random", random_variable),
];
for (selector, variable) in strategies {
let result = evaluate_branch_choice(vars, &mapped, variable, 4);
println!(
"{},{},{},{},{},{:.2},{},{},{},{},{},{},{:.6},{},{},{:.6},{}",
family,
vars,
mapped.len(),
test_seed,
selector,
learned_alpha,
variable,
order[variable],
result.branches,
result.satisfiable,
result.reuse_us,
result.fresh_us,
result.fresh_us as f64 / result.reuse_us.max(1) as f64,
result.reuse_nodes,
result.fresh_nodes,
result.reuse_nodes as f64 / result.fresh_nodes.max(1) as f64,
result.valid
);
}
println!(
"{},{},{},{},oracle,{:.2},{},{},{},{},{},{},{:.6},{},{},{:.6},{}",
family,
vars,
mapped.len(),
test_seed,
learned_alpha,
oracle_variable,
order[oracle_variable],
oracle_result.branches,
oracle_result.satisfiable,
oracle_result.reuse_us,
oracle_result.fresh_us,
oracle_result.fresh_us as f64 / oracle_result.reuse_us.max(1) as f64,
oracle_result.reuse_nodes,
oracle_result.fresh_nodes,
oracle_result.reuse_nodes as f64 / oracle_result.fresh_nodes.max(1) as f64,
oracle_result.valid
);
}
return;
}
for seed in 1..=trials {
let formula = generate_formula(family, vars, ratio, seed as u64);
let order = choose_order(order_name, vars, &formula, seed as u64);
if engine == "branch-reuse" {
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(variable, sign)| (rank[variable], sign))
.collect(),
)
})
.collect();
let natural: Vec<_> = (0..vars).collect();
for branch_percent in [0usize, 25, 50, 75, 90] {
let branch_level = (vars - 1) * branch_percent / 100;
let branch_variable = order[branch_level];
let mut false_formula = mapped.clone();
false_formula.push(Clause(vec![(branch_level, false)]));
let branch_clause = false_formula.len() - 1;
let true_clause = Clause(vec![(branch_level, true)]);
let mut true_formula = false_formula.clone();
true_formula[branch_clause] = true_clause.clone();
let cache_start = Instant::now();
let mut cache = build_incremental_bdd_cache_with_stride(vars, &false_formula, 4);
let cache_false_us = cache_start.elapsed().as_micros();
let checkpoint_factors: usize = cache.checkpoints.iter().map(Vec::len).sum();
let false_sat = cache.assignment.is_some();
let false_valid = cache
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&false_formula, assignment));
let incremental_start = Instant::now();
let (true_assignment, new_nodes, _, _) =
incremental_clause_update(&mut cache, branch_clause, &true_clause);
let incremental_true_us = incremental_start.elapsed().as_micros();
let true_valid = true_assignment
.as_ref()
.is_none_or(|assignment| satisfies(&true_formula, assignment));
let fresh_false_start = Instant::now();
let fresh_false = eliminate_with_bdds(vars, &false_formula, &natural);
let fresh_false_us = fresh_false_start.elapsed().as_micros();
assert_eq!(false_sat, fresh_false.assignment.is_some());
let fresh_true_start = Instant::now();
let fresh_true = eliminate_with_bdds(vars, &true_formula, &natural);
let fresh_true_us = fresh_true_start.elapsed().as_micros();
let true_equivalent = true_assignment.is_some() == fresh_true.assignment.is_some();
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{:.6},{},{},{:.6},{},{},{}",
family,
vars,
mapped.len(),
seed,
branch_percent,
branch_level,
branch_variable,
4,
checkpoint_factors,
false_sat,
true_assignment.is_some(),
cache_false_us,
incremental_true_us,
fresh_false_us,
fresh_true_us,
(fresh_false_us + fresh_true_us) as f64
/ (cache_false_us + incremental_true_us).max(1) as f64,
fresh_true_us as f64 / incremental_true_us.max(1) as f64,
new_nodes,
fresh_true.allocated_nodes,
new_nodes as f64 / fresh_true.allocated_nodes.max(1) as f64,
true_equivalent,
false_valid,
true_valid
);
}
continue;
}
if engine == "checkpoint-compression" {
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let changed_clause = seed % mapped.len();
let mut replacement = mapped[changed_clause].clone();
replacement.0[0].1 = !replacement.0[0].1;
let mut updated = mapped.clone();
updated[changed_clause] = replacement.clone();
let natural: Vec<_> = (0..vars).collect();
let full_start = Instant::now();
let full = eliminate_with_bdds(vars, &updated, &natural);
let full_us = full_start.elapsed().as_micros();
let dense = build_incremental_bdd_cache_with_stride(vars, &mapped, 1);
let dense_factors: usize = dense.checkpoints.iter().map(Vec::len).sum();
for stride in [1usize, 2, 4, 8, 16] {
let cache_start = Instant::now();
let mut cache = build_incremental_bdd_cache_with_stride(vars, &mapped, stride);
let cache_build_us = cache_start.elapsed().as_micros();
let checkpoint_factors: usize = cache.checkpoints.iter().map(Vec::len).sum();
let update_start = Instant::now();
let (assignment, new_nodes, earliest, restored) =
incremental_clause_update(&mut cache, changed_clause, &replacement);
let incremental_us = update_start.elapsed().as_micros();
let valid = assignment
.as_ref()
.is_none_or(|candidate| satisfies(&updated, candidate));
println!(
"{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{},{}",
family,
vars,
mapped.len(),
seed,
stride,
cache.checkpoints.len(),
checkpoint_factors,
checkpoint_factors as f64 / dense_factors.max(1) as f64,
earliest,
restored,
earliest - restored,
vars - earliest,
cache_build_us,
incremental_us,
full_us,
full_us as f64 / incremental_us.max(1) as f64,
new_nodes,
full.allocated_nodes,
new_nodes as f64 / full.allocated_nodes.max(1) as f64,
assignment.is_some() == full.assignment.is_some(),
valid
);
}
continue;
}
if engine == "incremental-pinch" {
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mapped: Vec<_> = formula
.iter()
.map(|clause| {
Clause(
clause
.0
.iter()
.map(|&(variable, sign)| (rank[variable], sign))
.collect(),
)
})
.collect();
let changed_clause = seed % mapped.len();
let mut replacement = mapped[changed_clause].clone();
replacement.0[0].1 = !replacement.0[0].1;
let mut updated_mapped = mapped.clone();
updated_mapped[changed_clause] = replacement.clone();
let mut updated_original = formula.clone();
updated_original[changed_clause].0[0].1 = !updated_original[changed_clause].0[0].1;
let cache_start = Instant::now();
let mut cache = build_incremental_bdd_cache(vars, &mapped);
let cache_build_us = cache_start.elapsed().as_micros();
let checkpoint_factors: usize = cache.checkpoints.iter().map(Vec::len).sum();
let incremental_start = Instant::now();
let (mapped_assignment, new_nodes, earliest, _) =
incremental_clause_update(&mut cache, changed_clause, &replacement);
let incremental_us = incremental_start.elapsed().as_micros();
let full_start = Instant::now();
let natural_order: Vec<_> = (0..vars).collect();
let full = eliminate_with_bdds(vars, &updated_mapped, &natural_order);
let full_us = full_start.elapsed().as_micros();
let incremental_assignment = mapped_assignment.map(|assignment| {
let mut original = vec![false; vars];
for level in 0..vars {
original[order[level]] = assignment[level];
}
original
});
let full_assignment = full.assignment.as_ref().map(|assignment| {
let mut original = vec![false; vars];
for level in 0..vars {
original[order[level]] = assignment[level];
}
original
});
let equivalent = incremental_assignment.is_some() == full_assignment.is_some();
let incremental_valid = incremental_assignment
.as_ref()
.is_none_or(|assignment| satisfies(&updated_original, assignment));
let full_valid = full_assignment
.as_ref()
.is_none_or(|assignment| satisfies(&updated_original, assignment));
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{},{},{}",
family,
vars,
formula.len(),
seed,
changed_clause,
earliest,
earliest,
vars - earliest,
checkpoint_factors,
cache_build_us,
incremental_us,
full_us,
full_us as f64 / incremental_us.max(1) as f64,
new_nodes,
full.allocated_nodes,
new_nodes as f64 / full.allocated_nodes.max(1) as f64,
equivalent,
incremental_valid,
full_valid
);
continue;
}
if engine == "math-tricks" {
let preprocess_start = Instant::now();
let (processed, stats) = mathematical_identity_preprocess(&formula);
let preprocess_us = preprocess_start.elapsed().as_micros();
let baseline_start = Instant::now();
let baseline = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
let baseline_us = baseline_start.elapsed().as_micros();
let processed_order = min_fill_order(vars, &processed);
let processed_start = Instant::now();
let processed_result =
eliminate_with_bdds_ordered(vars, &processed, &processed_order, &processed_order);
let processed_us = processed_start.elapsed().as_micros();
let equivalent = baseline.assignment.is_some() == processed_result.assignment.is_some();
let baseline_valid = baseline
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&formula, assignment));
let processed_valid = processed_result
.assignment
.as_ref()
.is_none_or(|assignment| {
satisfies(&processed, assignment) && satisfies(&formula, assignment)
});
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{}",
family,
vars,
formula.len(),
seed,
processed.len(),
stats.tautologies,
stats.subsumed,
stats.consensus_pairs,
stats.passes,
preprocess_us,
baseline_us,
processed_us,
baseline.allocated_nodes,
processed_result.allocated_nodes,
processed_result.allocated_nodes as f64 / baseline.allocated_nodes.max(1) as f64,
equivalent,
baseline_valid,
processed_valid
);
continue;
}
if engine == "joint-predict" {
let natural = eliminate_with_bdds(vars, &formula, &order);
let aligned = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
for selector in ["graph", "semantic", "frequency"] {
let start = Instant::now();
let (
expanded_vars,
expanded,
accepted,
recursive,
scored,
initial_width,
final_width,
work_ratio,
) = if selector == "graph" {
let predicted = predicted_joint_expand(vars, &formula, helper_budget, 24);
let work_ratio = predicted.final_estimated_work
/ predicted.initial_estimated_work.max(f64::MIN_POSITIVE);
(
predicted.vars,
predicted.clauses,
predicted.accepted,
predicted.recursive_accepted,
predicted.candidates_scored,
predicted.initial_width,
predicted.final_width,
work_ratio,
)
} else if selector == "semantic" {
let (expanded_vars, expanded, accepted, scored) = semantic_batch_expand(
vars,
&formula,
helper_budget,
&aligned.interaction_candidates,
);
let initial_order = min_fill_order(vars, &formula);
let expanded_order = min_fill_order(expanded_vars, &expanded);
let initial = elimination_cost(vars, &formula, &initial_order);
let final_cost = elimination_cost(expanded_vars, &expanded, &expanded_order);
(
expanded_vars,
expanded,
accepted,
0,
scored,
initial.0,
final_cost.0,
final_cost.1 / initial.1.max(f64::MIN_POSITIVE),
)
} else {
let (expanded_vars, expanded, accepted) =
expand_recurring_pairs(vars, &formula, helper_budget);
let initial_order = min_fill_order(vars, &formula);
let expanded_order = min_fill_order(expanded_vars, &expanded);
let initial = elimination_cost(vars, &formula, &initial_order);
let final_cost = elimination_cost(expanded_vars, &expanded, &expanded_order);
(
expanded_vars,
expanded,
accepted,
0,
accepted,
initial.0,
final_cost.0,
final_cost.1 / initial.1.max(f64::MIN_POSITIVE),
)
};
let predict_us = start.elapsed().as_micros();
let expanded_order = min_fill_order(expanded_vars, &expanded);
let start = Instant::now();
let result = eliminate_with_bdds_ordered(
expanded_vars,
&expanded,
&expanded_order,
&expanded_order,
);
let final_us = start.elapsed().as_micros();
let equivalent = result.assignment.is_some() == natural.assignment.is_some();
let valid = result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&formula, &assignment[..vars]) && satisfies(&expanded, assignment)
});
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{:.6},{},{},{},{},{:.6},{},{}",
family,
vars,
formula.len(),
seed,
selector,
helper_budget,
accepted,
recursive,
scored,
natural.allocated_nodes,
aligned.allocated_nodes,
result.allocated_nodes,
result.allocated_nodes as f64 / natural.allocated_nodes.max(1) as f64,
result.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64,
predict_us,
final_us,
initial_width,
final_width,
work_ratio,
equivalent,
valid
);
}
continue;
}
if engine == "flower-ring-states" {
assert!(
family == "flower" || family == "flower-planted" || family == "flower-symmetric",
"flower-ring-states requires a flat flower family"
);
for (direction, outside_in) in [("inside-out", false), ("outside-in", true)] {
let (total_nodes, profile) = flower_ring_state_profile(vars, &formula, outside_in);
for point in profile {
println!(
"{},{},{},{},{},{},{},{},{},{}",
family,
vars,
formula.len(),
seed,
direction,
point.ring,
point.processed,
point.residual_states,
point.residual_states.div_ceil(6),
total_nodes
);
}
}
continue;
}
if engine == "bdd-sift" || engine == "bdd-sift-control" {
let natural: Vec<_> = (0..vars).collect();
for (start_name, initial) in [("natural", natural), ("elimination", order.clone())] {
let start = Instant::now();
let baseline = eliminate_with_bdds_ordered(vars, &formula, &order, &initial);
let baseline_us = start.elapsed().as_micros().max(1);
let start = Instant::now();
let sifted = sift_bdd_order(
vars,
&formula,
&order,
&initial,
sift_passes,
sift_trials,
engine == "bdd-sift",
seed as u64,
);
let search_us = start.elapsed().as_micros();
let start = Instant::now();
let final_check =
eliminate_with_bdds_ordered(vars, &formula, &order, &sifted.order);
let final_us = start.elapsed().as_micros();
assert_eq!(final_check.allocated_nodes, sifted.result.allocated_nodes);
let valid = final_check
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&formula, assignment));
println!(
"{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{:.6},{},{},{:.3},{}",
family,
order_name,
start_name,
vars,
formula.len(),
seed,
sifted.passes,
sifted.swaps_tested,
sifted.swaps_accepted,
baseline.allocated_nodes,
sifted.result.allocated_nodes,
sifted.result.allocated_nodes as f64 / baseline.allocated_nodes.max(1) as f64,
baseline.live_nodes,
sifted.result.live_nodes,
sifted.result.live_nodes as f64 / baseline.live_nodes.max(1) as f64,
search_us,
final_us,
(search_us + final_us) as f64 / baseline_us as f64,
valid
);
}
continue;
}
if engine == "bdd-order-sweep" {
let natural: Vec<_> = (0..vars).collect();
let mut reverse_elimination = order.clone();
reverse_elimination.reverse();
let configurations = [
("natural", natural),
("elimination", order.clone()),
("reverse-elimination", reverse_elimination),
("frequent-first", occurrence_order(vars, &formula, true)),
("rare-first", occurrence_order(vars, &formula, false)),
];
for (bdd_order_name, bdd_order) in configurations {
let start = Instant::now();
let result = eliminate_with_bdds_ordered(vars, &formula, &order, &bdd_order);
let elapsed = start.elapsed().as_micros();
let valid = result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&formula, assignment));
println!(
"{},{},{},{},{},{},{},{},{},{}",
family,
order_name,
bdd_order_name,
vars,
formula.len(),
seed,
result.assignment.is_some(),
elapsed,
result.allocated_nodes,
valid
);
}
continue;
}
if engine == "music-order-sweep" {
let aligned = eliminate_with_bdds_ordered(vars, &formula, &order, &order);
let configurations = vec![
("aligned-melody", order.clone()),
("rhythm-2", metrical_order(&order, 2)),
("rhythm-3", metrical_order(&order, 3)),
("rhythm-4", metrical_order(&order, 4)),
("rhythm-5", metrical_order(&order, 5)),
("rhythm-7", metrical_order(&order, 7)),
("phrasing-4", phrase_order(&order, 4)),
("phrasing-8", phrase_order(&order, 8)),
("counterpoint", counterpoint_order(&order)),
("motif-polarity-degree", motif_order(vars, &formula, &order)),
(
"dynamics-crescendo",
occurrence_order(vars, &formula, false),
),
(
"dynamics-decrescendo",
occurrence_order(vars, &formula, true),
),
(
"harmony-voice-leading",
harmonic_order(vars, &formula, &order),
),
(
"tension-then-resolution",
tension_order(vars, &formula, true),
),
(
"resolution-then-tension",
tension_order(vars, &formula, false),
),
];
for (name, bdd_order) in configurations {
let start = Instant::now();
let result = eliminate_with_bdds_ordered(vars, &formula, &order, &bdd_order);
let elapsed = start.elapsed().as_micros();
let valid = result
.assignment
.as_ref()
.is_none_or(|a| satisfies(&formula, a));
println!(
"{},{},{},{},{},{},{},{},{},{:.6},{}",
family,
order_name,
name,
vars,
formula.len(),
seed,
result.assignment.is_some(),
elapsed,
result.allocated_nodes,
result.allocated_nodes as f64 / aligned.allocated_nodes.max(1) as f64,
valid
);
}
continue;
}
if engine == "bdd-frontier-expand" {
let start = Instant::now();
let original = eliminate_with_bdds(vars, &formula, &order);
let original_us = start.elapsed().as_micros();
let start = Instant::now();
let frontier =
bdd_frontier_expand(vars, &formula, helper_budget, 24, order_name, seed as u64);
let search_us = start.elapsed().as_micros();
let final_order =
choose_order(order_name, frontier.vars, &frontier.clauses, seed as u64);
let start = Instant::now();
let final_check = eliminate_with_bdds(frontier.vars, &frontier.clauses, &final_order);
let final_us = start.elapsed().as_micros();
assert_eq!(final_check.allocated_nodes, frontier.result.allocated_nodes);
let equivalent = original.assignment.is_some() == frontier.result.assignment.is_some();
let projected_valid = frontier
.result
.assignment
.as_ref()
.is_none_or(|assignment| {
satisfies(&formula, &assignment[..vars])
&& satisfies(&frontier.clauses, assignment)
});
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{}",
family,
order_name,
vars,
formula.len(),
seed,
helper_budget,
frontier.accepted,
frontier.recursive_accepted,
frontier.candidates_tested,
frontier.beneficial_trials,
frontier.vars,
frontier.clauses.len(),
original.allocated_nodes,
frontier.result.allocated_nodes,
frontier.result.allocated_nodes as f64 / original.allocated_nodes.max(1) as f64,
original_us,
final_us,
search_us,
equivalent,
projected_valid
);
continue;
}
if engine == "feedback-expand-bdd" || engine == "shortlist-expand-bdd" {
let start = Instant::now();
let original = eliminate_with_bdds(vars, &formula, &order);
let original_us = start.elapsed().as_micros();
let start = Instant::now();
let feedback = feedback_expand(
vars,
&formula,
helper_budget,
24,
6,
order_name,
seed as u64,
engine == "feedback-expand-bdd",
);
let search_us = start.elapsed().as_micros();
let final_order =
choose_order(order_name, feedback.vars, &feedback.clauses, seed as u64);
let start = Instant::now();
let final_check = eliminate_with_bdds(feedback.vars, &feedback.clauses, &final_order);
let final_us = start.elapsed().as_micros();
assert_eq!(final_check.allocated_nodes, feedback.result.allocated_nodes);
let equivalent = original.assignment.is_some() == feedback.result.assignment.is_some();
let projected_valid = feedback
.result
.assignment
.as_ref()
.is_none_or(|assignment| {
satisfies(&formula, &assignment[..vars])
&& satisfies(&feedback.clauses, assignment)
});
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{}",
family,
order_name,
vars,
formula.len(),
seed,
helper_budget,
feedback.accepted,
feedback.recursive_accepted,
feedback.candidates_tested,
feedback.beneficial_trials,
feedback.vars,
feedback.clauses.len(),
original.allocated_nodes,
feedback.result.allocated_nodes,
feedback.result.allocated_nodes as f64 / original.allocated_nodes.max(1) as f64,
original_us,
final_us,
search_us,
equivalent,
projected_valid
);
continue;
}
if engine == "greedy-expand-bdd" || engine == "aligned-greedy-expand" {
let aligned = engine == "aligned-greedy-expand";
let start = Instant::now();
let original = solve_bdd_strategy(vars, &formula, order_name, seed as u64, aligned);
let original_us = start.elapsed().as_micros();
let start = Instant::now();
let greedy = greedy_expand(
vars,
&formula,
helper_budget,
24,
order_name,
seed as u64,
aligned,
);
let search_us = start.elapsed().as_micros();
let start = Instant::now();
let final_check = solve_bdd_strategy(
greedy.vars,
&greedy.clauses,
order_name,
seed as u64,
aligned,
);
let final_us = start.elapsed().as_micros();
assert_eq!(final_check.allocated_nodes, greedy.result.allocated_nodes);
let equivalent = original.assignment.is_some() == greedy.result.assignment.is_some();
let projected_valid = greedy.result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&formula, &assignment[..vars]) && satisfies(&greedy.clauses, assignment)
});
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{}",
family,
order_name,
vars,
formula.len(),
seed,
helper_budget,
greedy.accepted,
greedy.recursive_accepted,
greedy.candidates_tested,
greedy.beneficial_trials,
greedy.vars,
greedy.clauses.len(),
original.allocated_nodes,
greedy.result.allocated_nodes,
greedy.result.allocated_nodes as f64 / original.allocated_nodes.max(1) as f64,
original_us,
final_us,
search_us,
equivalent,
projected_valid
);
continue;
}
if engine == "expand-bdd" {
let start = Instant::now();
let original = eliminate_with_bdds(vars, &formula, &order);
let original_us = start.elapsed().as_micros();
let (expanded_vars, expanded, helpers) =
expand_recurring_pairs(vars, &formula, helper_budget);
let expanded_order = choose_order(order_name, expanded_vars, &expanded, seed as u64);
let start = Instant::now();
let expanded_result = eliminate_with_bdds(expanded_vars, &expanded, &expanded_order);
let expanded_us = start.elapsed().as_micros();
let equivalent = original.assignment.is_some() == expanded_result.assignment.is_some();
let projected_valid = expanded_result
.assignment
.as_ref()
.is_none_or(|assignment| {
satisfies(&formula, &assignment[..vars]) && satisfies(&expanded, assignment)
});
println!(
"{},{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{}",
family,
order_name,
vars,
formula.len(),
seed,
helpers,
expanded_vars,
expanded.len(),
original_us,
expanded_us,
original.allocated_nodes,
expanded_result.allocated_nodes,
expanded_result.allocated_nodes as f64 / original.allocated_nodes.max(1) as f64,
equivalent,
projected_valid
);
continue;
}
if engine == "bdd-only" {
let start = Instant::now();
let result = eliminate_with_bdds(vars, &formula, &order);
let elapsed = start.elapsed().as_micros();
let valid = result
.assignment
.as_ref()
.is_none_or(|a| satisfies(&formula, a));
println!(
"{},{},{},{},{},{},{},{},{}",
family,
order_name,
vars,
formula.len(),
seed,
result.assignment.is_some(),
elapsed,
result.allocated_nodes,
valid
);
continue;
}
let start = Instant::now();
let result = eliminate(vars, &formula, &order);
let layered_us = start.elapsed().as_micros();
let start = Instant::now();
let bdd_result = eliminate_with_bdds(vars, &formula, &order);
let bdd_solver_us = start.elapsed().as_micros();
let bdd_valid = bdd_result
.assignment
.as_ref()
.is_none_or(|a| satisfies(&formula, a));
let bdd_agrees =
bdd_result.assignment.is_some() == result.assignment.is_some() && bdd_valid;
let start = Instant::now();
let brute = (vars <= 24).then(|| brute_force(vars, &formula));
let brute_us = start.elapsed().as_micros();
let valid = result
.assignment
.as_ref()
.is_none_or(|a| satisfies(&formula, a));
let agrees = match brute {
Some(brute) if result.assignment.is_some() == brute.is_some() && valid => "true",
Some(_) => "false",
None if valid => "unchecked",
None => "invalid-witness",
};
*histogram.entry(result.peak_boundary).or_insert(0usize) += 1;
let stored_entries: usize = result.layers.iter().map(|l| l.witness.len()).sum();
let stored_bdd_nodes: usize = result.layers.iter().map(|l| l.bdd_nodes).sum();
let bdd_ratio = stored_bdd_nodes as f64 / stored_entries as f64;
println!(
"{},{},{},{},{},{},{},{},{},{},{},{:.6},{},{},{},{},{},{}",
family,
order_name,
vars,
formula.len(),
seed,
result.assignment.is_some(),
result.peak_boundary,
result.peak_entries,
stored_entries,
result.peak_bdd_nodes,
stored_bdd_nodes,
bdd_ratio,
layered_us,
bdd_solver_us,
bdd_result.allocated_nodes,
bdd_agrees,
brute_us,
agrees
);
}
if engine == "compare" {
eprintln!("peak-boundary histogram: {histogram:?}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layered_solver_matches_brute_force() {
for vars in 3..=10 {
for seed in 1..=40 {
let clauses = random_3sat(vars, vars * 4, seed);
let result = eliminate(vars, &clauses, &min_degree_order(vars, &clauses));
let brute = brute_force(vars, &clauses);
assert_eq!(
result.assignment.is_some(),
brute.is_some(),
"vars={vars}, seed={seed}"
);
assert!(
result
.assignment
.as_ref()
.is_none_or(|a| satisfies(&clauses, a))
);
}
}
}
#[test]
fn reduced_bdd_merges_repeated_subfunctions() {
assert_eq!(reduced_bdd_nodes(&[false, true, true, false], 2), 3); // xor
assert_eq!(reduced_bdd_nodes(&[false, true, false, true], 2), 1); // x
assert_eq!(reduced_bdd_nodes(&[true; 8], 3), 0); // constant
}
#[test]
fn native_bdd_solver_matches_brute_force() {
for vars in 3..=10 {
for seed in 1..=40 {
let clauses = random_3sat(vars, vars * 4, seed);
let order = min_fill_order(vars, &clauses);
let bdd = eliminate_with_bdds(vars, &clauses, &order);
let brute = brute_force(vars, &clauses);
assert_eq!(bdd.assignment.is_some(), brute.is_some());
assert!(
bdd.assignment
.as_ref()
.is_none_or(|a| satisfies(&clauses, a))
);
}
}
}
#[test]
fn pair_expansion_preserves_sat_and_witnesses() {
for vars in 4..=10 {
for seed in 1..=30 {
let clauses = random_3sat(vars, vars * 4, seed);
let (expanded_vars, expanded, _) = expand_recurring_pairs(vars, &clauses, vars / 2);
let original = brute_force(vars, &clauses);
let result = eliminate_with_bdds(
expanded_vars,
&expanded,
&min_fill_order(expanded_vars, &expanded),
);
assert_eq!(original.is_some(), result.assignment.is_some());
assert!(result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&clauses, &assignment[..vars]) && satisfies(&expanded, assignment)
}));
}
}
}
#[test]
fn greedy_expansion_is_monotonic_and_equivalent() {
for seed in 1..=20 {
let vars = 10;
let clauses = random_3sat(vars, vars * 6, seed);
let baseline = eliminate_with_bdds(vars, &clauses, &min_fill_order(vars, &clauses));
let greedy = greedy_expand(vars, &clauses, 3, 12, "min-fill", seed, false);
assert!(greedy.result.allocated_nodes <= baseline.allocated_nodes);
assert_eq!(
baseline.assignment.is_some(),
greedy.result.assignment.is_some()
);
assert!(greedy.result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&clauses, &assignment[..vars]) && satisfies(&greedy.clauses, assignment)
}));
}
}
#[test]
fn feedback_expansion_is_monotonic_and_equivalent() {
for seed in 1..=20 {
let vars = 10;
let clauses = random_3sat(vars, vars * 6, seed);
let baseline = eliminate_with_bdds(vars, &clauses, &min_fill_order(vars, &clauses));
let feedback = feedback_expand(vars, &clauses, 3, 12, 4, "min-fill", seed, true);
assert!(feedback.result.allocated_nodes <= baseline.allocated_nodes);
assert_eq!(
baseline.assignment.is_some(),
feedback.result.assignment.is_some()
);
assert!(
feedback
.result
.assignment
.as_ref()
.is_none_or(|assignment| {
satisfies(&clauses, &assignment[..vars])
&& satisfies(&feedback.clauses, assignment)
})
);
}
}
#[test]
fn bdd_frontier_expansion_is_monotonic_and_equivalent() {
for seed in 1..=15 {
let vars = 10;
let clauses = random_3sat(vars, vars * 6, seed);
let baseline = eliminate_with_bdds(vars, &clauses, &min_fill_order(vars, &clauses));
let frontier = bdd_frontier_expand(vars, &clauses, 3, 12, "min-fill", seed);
assert!(frontier.result.allocated_nodes <= baseline.allocated_nodes);
assert_eq!(
baseline.assignment.is_some(),
frontier.result.assignment.is_some()
);
assert!(
frontier
.result
.assignment
.as_ref()
.is_none_or(|assignment| {
satisfies(&clauses, &assignment[..vars])
&& satisfies(&frontier.clauses, assignment)
})
);
}
}
#[test]
fn bdd_variable_order_preserves_answers_and_witnesses() {
for seed in 1..=25 {
let vars = 10;
let clauses = random_3sat(vars, vars * 4, seed);
let elimination = min_fill_order(vars, &clauses);
let mut reversed = elimination.clone();
reversed.reverse();
for bdd_order in [elimination.clone(), reversed] {
let result = eliminate_with_bdds_ordered(vars, &clauses, &elimination, &bdd_order);
let brute = brute_force(vars, &clauses);
assert_eq!(result.assignment.is_some(), brute.is_some());
assert!(
result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&clauses, assignment))
);
}
}
}
#[test]
fn bdd_sifting_is_monotonic_and_preserves_witnesses() {
for seed in 1..=15 {
let vars = 10;
let clauses = random_3sat(vars, vars * 5, seed);
let elimination = min_fill_order(vars, &clauses);
let natural: Vec<_> = (0..vars).collect();
let baseline = eliminate_with_bdds_ordered(vars, &clauses, &elimination, &natural);
let sifted = sift_bdd_order(
vars,
&clauses,
&elimination,
&natural,
3,
usize::MAX,
true,
seed,
);
assert!(sifted.result.allocated_nodes <= baseline.allocated_nodes);
assert_eq!(
sifted.result.assignment.is_some(),
brute_force(vars, &clauses).is_some()
);
assert!(
sifted
.result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&clauses, assignment))
);
}
}
#[test]
fn flower_geometries_match_brute_force() {
for vars in 5..=10 {
for seed in 1..=15 {
for clauses in [
flower_3sat(vars, vars * 4, seed),
stacked_flower_3sat(vars, vars * 4, seed),
] {
let order = min_fill_order(vars, &clauses);
let result = eliminate_with_bdds(vars, &clauses, &order);
assert_eq!(
result.assignment.is_some(),
brute_force(vars, &clauses).is_some()
);
assert!(
result
.assignment
.as_ref()
.is_none_or(|assignment| satisfies(&clauses, assignment))
);
}
}
}
}
#[test]
fn planted_flowers_are_satisfiable_and_have_ring_profiles() {
for vars in [7, 19, 25] {
for seed in 1..=10 {
let clauses = planted_flower_3sat(vars, vars * 4, seed);
let planted = planted_assignment(vars, seed);
assert!(satisfies(&clauses, &planted));
for outside_in in [false, true] {
let (_, profile) = flower_ring_state_profile(vars, &clauses, outside_in);
assert!(!profile.is_empty());
assert_eq!(profile.last().unwrap().processed, vars);
assert!((1..=2).contains(&profile.last().unwrap().residual_states));
}
}
}
}
#[test]
fn symmetric_flower_is_rotation_invariant_and_satisfiable() {
for vars in [7, 19, 37] {
let clauses = symmetric_flower_3sat(vars, vars * 4);
assert!(satisfies(&clauses, &vec![true; vars]));
let coordinates = flower_coordinates(vars);
let index: HashMap<_, _> = coordinates
.iter()
.copied()
.enumerate()
.map(|(variable, coordinate)| (coordinate, variable))
.collect();
let original: BTreeSet<_> = clauses.iter().map(|clause| clause.0.clone()).collect();
let rotated: BTreeSet<_> = clauses
.iter()
.map(|clause| {
let mut literals: Vec<_> = clause
.0
.iter()
.map(|&(variable, sign)| {
let (q, r) = coordinates[variable];
(index[&(-r, q + r)], sign)
})
.collect();
literals.sort_unstable();
literals
})
.collect();
assert_eq!(original, rotated);
}
}
#[test]
fn joint_predictor_improves_its_proxy_and_preserves_answers() {
for seed in 1..=20 {
let vars = 10;
let clauses = random_3sat(vars, vars * 6, seed);
let predicted = predicted_joint_expand(vars, &clauses, 4, 16);
assert!(
predicted.final_width < predicted.initial_width
|| (predicted.final_width == predicted.initial_width
&& predicted.final_estimated_work <= predicted.initial_estimated_work)
);
let order = min_fill_order(predicted.vars, &predicted.clauses);
let result =
eliminate_with_bdds_ordered(predicted.vars, &predicted.clauses, &order, &order);
assert_eq!(
result.assignment.is_some(),
brute_force(vars, &clauses).is_some()
);
assert!(result.assignment.as_ref().is_none_or(|assignment| {
satisfies(&clauses, &assignment[..vars])
&& satisfies(&predicted.clauses, assignment)
}));
}
}
#[test]
fn mathematical_identity_preprocessing_is_equivalent() {
for seed in 1..=20 {
for clauses in [random_3sat(8, 32, seed), identity_expanded_sat(8, 4, seed)] {
let (processed, _) = mathematical_identity_preprocess(&clauses);
for bits in 0..(1usize << 8) {
let assignment: Vec<_> = (0..8)
.map(|variable| ((bits >> variable) & 1) == 1)
.collect();
assert_eq!(
satisfies(&clauses, &assignment),
satisfies(&processed, &assignment)
);
}
}
}
}
#[test]
fn incremental_clause_updates_match_full_resolves() {
for seed in 1..=20 {
let vars = 10;
let clauses = random_3sat(vars, 40, seed);
let order = min_fill_order(vars, &clauses);
let mut rank = vec![0usize; vars];
for (level, &variable) in order.iter().enumerate() {
rank[variable] = level;
}
let mut mapped: Vec<_> = clauses
.iter()
.map(|clause| Clause(clause.0.iter().map(|&(v, s)| (rank[v], s)).collect()))
.collect();
let changed = seed as usize % mapped.len();
let mut replacement = mapped[changed].clone();
replacement.0[0].1 = !replacement.0[0].1;
let mut cache = build_incremental_bdd_cache(vars, &mapped);
let (incremental, _, _, _) =
incremental_clause_update(&mut cache, changed, &replacement);
mapped[changed] = replacement;
let natural: Vec<_> = (0..vars).collect();
let full = eliminate_with_bdds(vars, &mapped, &natural);
assert_eq!(incremental.is_some(), full.assignment.is_some());
assert!(
incremental
.as_ref()
.is_none_or(|assignment| satisfies(&mapped, assignment))
);
}
}
#[test]
fn exact_treewidth_matches_path_and_clique() {
let path = vec![
Clause(vec![(0, true), (1, true)]),
Clause(vec![(1, true), (2, true)]),
Clause(vec![(2, true), (3, true)]),
];
assert_eq!(exact_treewidth(4, &path), 1);
assert_eq!(
exact_weighted_treewidth(&[1, 1, 1, 1], &primal_graph(4, &path)),
1
);
assert_eq!(greedy_clique_lower_bound(4, &path), 1);
assert_eq!(minor_min_width_lower_bound(4, &path), 1);
let mut clique = Vec::new();
for left in 0..4 {
for right in left + 1..4 {
clique.push(Clause(vec![(left, true), (right, true)]));
}
}
assert_eq!(exact_treewidth(4, &clique), 3);
assert_eq!(
exact_weighted_treewidth(&[1, 1, 1, 1], &primal_graph(4, &clique)),
3
);
assert_eq!(greedy_clique_lower_bound(4, &clique), 3);
assert_eq!(minor_min_width_lower_bound(4, &clique), 3);
}
#[test]
fn shaking_and_inverse_probing_preserve_sat_and_reconstruct_witnesses() {
for seed in 1..=40 {
let vars = 9;
let clauses = random_3sat(vars, vars * 4, seed);
let expected = brute_force(vars, &clauses).is_some();
for inverse in [false, true] {
let shaken = shake_formula(vars, &clauses, usize::from(inverse), vars);
if shaken.contradiction {
assert!(!expected);
continue;
}
let core = brute_force(shaken.vars, &shaken.clauses);
assert_eq!(core.is_some(), expected);
if let Some(core_assignment) = core {
let mut reconstructed = vec![false; vars];
for (variable, value) in shaken.fixed.iter().enumerate() {
if let Some(value) = value {
reconstructed[variable] = *value;
}
}
for (core_variable, &original_variable) in
shaken.core_to_original.iter().enumerate()
{
reconstructed[original_variable] = core_assignment[core_variable];
}
assert!(satisfies(&clauses, &reconstructed));
}
}
}
}
#[test]
fn depth_two_probe_finds_a_forcing_invisible_to_unit_propagation() {
let clauses = vec![
Clause(vec![(0, true), (1, true), (2, true)]),
Clause(vec![(0, true), (1, true), (2, false)]),
Clause(vec![(0, true), (1, false), (2, true)]),
Clause(vec![(0, true), (1, false), (2, false)]),
Clause(vec![(0, false), (3, true), (4, true)]),
Clause(vec![(0, false), (3, false), (4, true)]),
Clause(vec![(0, false), (3, true), (4, false)]),
];
let shallow = shake_formula(5, &clauses, 1, 5);
let deep = shake_formula(5, &clauses, 2, 5);
assert_eq!(shallow.inverse_forced, 0);
assert_eq!(deep.fixed[0], Some(true));
assert!(deep.inverse_forced >= 1);
assert!(deep.clauses.is_empty());
}
#[test]
fn global_articulation_discovery_finds_small_branches_once() {
// Triangle 0-1-2 is the core; articulation 2 owns the path 3-4-5.
let clauses = vec![
Clause(vec![(0, true), (1, true)]),
Clause(vec![(1, true), (2, true)]),
Clause(vec![(2, true), (0, true)]),
Clause(vec![(2, true), (3, true)]),
Clause(vec![(3, true), (4, true)]),
Clause(vec![(4, true), (5, true)]),
];
let graph = compact_primal_graph(6, &clauses);
let candidates = global_small_separator_candidates(&graph, 3);
assert!(
candidates
.iter()
.any(|(interior, boundary)| interior == &[3, 4, 5] && boundary == &[2])
);
for (interior, boundary) in candidates {
let interior: BTreeSet<_> = interior.into_iter().collect();
let actual: BTreeSet<_> = interior
.iter()
.flat_map(|&v| graph[v].iter().copied())
.filter(|v| !interior.contains(v))
.collect();
assert_eq!(actual.into_iter().collect::<Vec<_>>(), boundary);
}
}
#[test]
fn global_discovery_finds_two_vertex_separator() {
let clauses = vec![
Clause(vec![(0, true), (1, true)]),
Clause(vec![(1, true), (2, true)]),
Clause(vec![(2, true), (0, true)]),
Clause(vec![(1, true), (3, true)]),
Clause(vec![(2, true), (3, true)]),
Clause(vec![(3, true), (4, true)]),
];
let graph = compact_primal_graph(5, &clauses);
let candidates = global_small_separator_candidates(&graph, 2);
assert!(candidates.iter().any(|(interior, boundary)| {
interior.iter().copied().collect::<BTreeSet<_>>() == BTreeSet::from([3, 4])
&& boundary == &[1, 2]
}));
}
#[test]
fn seeded_branches_preserve_sat_and_regrow_witnesses() {
for seed in 1..=30 {
let vars = 9;
let clauses = banded_3sat(vars, vars * 2, seed, 3);
let seeded = seed_detachable_branch(vars, &clauses, 6);
let expected = brute_force(vars, &clauses).is_some();
let core = brute_force(seeded.vars, &seeded.clauses);
assert_eq!(core.is_some(), expected);
if let Some(core_assignment) = core {
let mut reconstructed = vec![false; vars];
for (core, &original) in seeded.core_to_original.iter().enumerate() {
reconstructed[original] = core_assignment[core];
}
let mut bits = 0usize;
for (index, &variable) in seeded.boundary.iter().enumerate() {
if reconstructed[variable] {
bits |= 1usize << index;
}
}
if !seeded.interior.is_empty() {
let witness = seeded.witnesses[bits].as_ref().unwrap();
for (index, &variable) in seeded.interior.iter().enumerate() {
reconstructed[variable] = witness[index];
}
}
assert!(satisfies(&clauses, &reconstructed));
}
}
}
#[test]
fn shared_bdd_seeds_preserve_sat_and_regrow_witnesses() {
for seed in 1..=30 {
let vars = 9;
let clauses = banded_3sat(vars, vars * 2, seed, 3);
for strategy in [
"natural",
"min-degree",
"min-fill",
"boundary-min-degree",
"boundary-min-fill",
] {
let seeded = seed_detachable_branch_bdd(vars, &clauses, 8, strategy);
let expected = brute_force(vars, &clauses).is_some();
let core = brute_force(seeded.vars, &seeded.clauses);
assert_eq!(core.is_some(), expected);
if let Some(core_assignment) = core {
let mut reconstructed = vec![false; vars];
for (core, &original) in seeded.core_to_original.iter().enumerate() {
reconstructed[original] = core_assignment[core];
}
let values = regrow_bdd_seed(&seeded, &reconstructed).unwrap();
for (index, &variable) in seeded.interior.iter().enumerate() {
reconstructed[variable] = values[index];
}
assert!(satisfies(&clauses, &reconstructed));
}
}
}
}
#[test]
fn multiple_small_seeds_regrow_in_reverse() {
for seed in 1..=20 {
let vars = 10;
let clauses = banded_3sat(vars, vars * 2, seed, 3);
let mut current_vars = vars;
let mut current_clauses = clauses.clone();
let mut seeds = Vec::new();
for _ in 0..6 {
let compiled =
seed_detachable_branch_bdd(current_vars, ¤t_clauses, 4, "natural");
if compiled.interior.is_empty() {
break;
}
current_vars = compiled.vars;
current_clauses = compiled.clauses.clone();
seeds.push(compiled);
}
let expected = brute_force(vars, &clauses).is_some();
let core = brute_force(current_vars, ¤t_clauses);
assert_eq!(core.is_some(), expected);
if let Some(core_assignment) = core {
let reconstructed = regrow_seed_chain(&seeds, &core_assignment).unwrap();
assert!(satisfies(&clauses, &reconstructed));
}
}
}
#[test]
fn bounded_seed_compiler_rejects_without_using_partial_summary() {
let vars = 9;
let clauses: Vec<_> = (0..8)
.map(|variable| {
Clause(vec![
(variable, true),
((variable + 1) % 8, false),
(8, variable % 2 == 0),
])
})
.collect();
let rejected = try_seed_bdd_candidate(
vars,
&clauses,
(0..8).collect(),
vec![8],
"min-fill",
1,
std::time::Duration::from_secs(1),
);
assert!(rejected.seed.is_none());
assert!(rejected.node_exceeded);
let accepted = try_seed_bdd_candidate(
vars,
&clauses,
(0..8).collect(),
vec![8],
"min-fill",
100_000,
std::time::Duration::from_secs(1),
);
assert!(accepted.seed.is_some());
}
#[test]
fn compiled_artifact_round_trip_queries_and_reconstructs() {
let vars = 24;
let clauses = banded_3sat(vars, vars * 2, 44, 3);
let artifact = compile_safe_artifact(vars, &clauses, 16, 100_000, 1_000);
let path = std::env::temp_dir().join(format!(
"layered-sat-round-trip-{}-{}.lsat",
std::process::id(),
artifact.core_vars
));
save_compiled_artifact(&path, &artifact).unwrap();
let loaded = load_compiled_artifact(&path).unwrap();
std::fs::remove_file(path).unwrap();
assert_eq!(loaded.original_vars, vars);
assert_eq!(loaded.core_vars, artifact.core_vars);
let assumed = loaded.core_to_original[0];
let assignment = query_compiled_artifact(&loaded, &[(assumed, true)])
.unwrap()
.unwrap();
assert!(assignment[assumed]);
assert!(satisfies(&clauses, &assignment));
let removed = loaded.seeds[0].interior[0];
let reopened = query_compiled_artifact(&loaded, &[(removed, assignment[removed])])
.unwrap()
.unwrap();
assert_eq!(reopened[removed], assignment[removed]);
assert!(satisfies(&clauses, &reopened));
}
#[test]
fn solver_aware_gates_preserve_answers_and_witnesses() {
let vars = 18;
let clauses = banded_3sat(vars, vars * 2, 91, 3);
for gate in ["all", "balanced", "strict"] {
let artifact =
compile_safe_artifact_with_gate(vars, &clauses, 16, 100_000, 1_000, gate);
for variable in [0, vars / 2, vars - 1] {
for value in [false, true] {
let expected = {
let mut constrained = clauses.clone();
constrained.push(Clause(vec![(variable, value)]));
brute_force(vars, &constrained).is_some()
};
let actual = query_compiled_artifact(&artifact, &[(variable, value)]).unwrap();
assert_eq!(actual.is_some(), expected, "gate={gate}");
if let Some(assignment) = actual {
assert!(satisfies(&clauses, &assignment));
assert_eq!(assignment[variable], value);
}
}
}
}
}
#[test]
fn continuation_repairs_match_full_recompilation() {
let vars = 12;
let base_formula = banded_3sat(vars, vars * 3, 707, 3);
let order: Vec<_> = (0..vars).collect();
let base = compile_continuation(&base_formula, &order);
let changes = [
(Clause(vec![(3, true), (7, false), (10, true)]), true),
(base_formula[5].clone(), false),
];
for (changed, insertion) in changes {
let mut updated = base_formula.clone();
if insertion {
updated.push(changed.clone());
} else {
updated.remove(5);
}
let repaired = repair_continuation(&base, &changed, insertion);
let full = compile_continuation(&updated, &order);
let mut repaired_scratch = ContinuationScratch::new(&repaired);
let mut full_scratch = ContinuationScratch::new(&full);
for variable in 0..vars {
for value in [false, true] {
let mut assumptions = vec![None; vars];
assumptions[variable] = Some(value);
let repaired_answer =
query_continuation(&repaired, &assumptions, &mut repaired_scratch);
let full_answer = query_continuation(&full, &assumptions, &mut full_scratch);
assert_eq!(repaired_answer.is_some(), full_answer.is_some());
if let Some(assignment) = repaired_answer {
assert!(satisfies(&updated, &assignment));
assert_eq!(assignment[variable], value);
}
}
}
}
}
#[test]
fn cumulative_continuation_repairs_preserve_prefix_residuals() {
let vars = 12;
let mut formula = banded_3sat(vars, vars * 3, 808, 3);
let order: Vec<_> = (0..vars).collect();
let base = compile_continuation(&formula, &order);
let inserted = Clause(vec![(8, true), (9, false), (11, true)]);
formula.push(inserted.clone());
let after_insert = repair_continuation(&base, &inserted, true);
let deleted_index = formula
.iter()
.position(|clause| clause.0.iter().any(|&(variable, _)| variable < 8))
.unwrap();
let deleted = formula.remove(deleted_index);
let cumulative = repair_continuation(&after_insert, &deleted, false);
let full = compile_continuation(&formula, &order);
let mut cumulative_scratch = ContinuationScratch::new(&cumulative);
let mut full_scratch = ContinuationScratch::new(&full);
for variable in 0..vars {
for value in [false, true] {
let mut assumptions = vec![None; vars];
assumptions[variable] = Some(value);
let cumulative_answer =
query_continuation(&cumulative, &assumptions, &mut cumulative_scratch);
let full_answer = query_continuation(&full, &assumptions, &mut full_scratch);
assert_eq!(cumulative_answer.is_some(), full_answer.is_some());
if let Some(assignment) = cumulative_answer {
assert!(satisfies(&formula, &assignment));
}
}
}
}
#[test]
fn temporal_memory_queries_preserve_state_across_horizon() {
let width = 3;
let horizon = 5;
let (vars, formula) = temporal_memory_formula(width, horizon);
let order: Vec<_> = (0..vars).collect();
let compiled = compile_continuation(&formula, &order);
let summarized = compile_temporal_memory_continuation(width, horizon);
let mut scratch = ContinuationScratch::new(&compiled);
let mut summarized_scratch = ContinuationScratch::new(&summarized);
let mut consistent = vec![None; vars];
consistent[1] = Some(true);
consistent[horizon * width + 1] = Some(true);
let assignment = query_continuation(&compiled, &consistent, &mut scratch).unwrap();
let summarized_assignment =
query_continuation(&summarized, &consistent, &mut summarized_scratch).unwrap();
let kernel_assignment = query_temporal_memory_kernel(width, horizon, &consistent).unwrap();
assert!(satisfies(&formula, &assignment));
assert!(satisfies(&formula, &summarized_assignment));
assert!(satisfies(&formula, &kernel_assignment));
assert!(assignment[1]);
assert!(assignment[horizon * width + 1]);
let mut conflicting = consistent;
conflicting[horizon * width + 1] = Some(false);
assert!(query_continuation(&compiled, &conflicting, &mut scratch).is_none());
assert!(query_continuation(&summarized, &conflicting, &mut summarized_scratch).is_none());
assert!(query_temporal_memory_kernel(width, horizon, &conflicting).is_none());
}
#[test]
fn temporal_kernel_matches_generic_continuations_on_small_grid() {
let mut rng = Rng(0x5eed_2024);
for width in 1..=4 {
for horizon in 1..=4 {
let (vars, formula) = temporal_memory_formula(width, horizon);
let order: Vec<_> = (0..vars).collect();
let generic = compile_continuation(&formula, &order);
let mut scratch = ContinuationScratch::new(&generic);
for _ in 0..100 {
let mut assumptions = vec![None; vars];
for _ in 0..4 {
let variable = rng.below(vars);
assumptions[variable] = Some(rng.next() & 1 == 1);
}
let generic_answer = query_continuation(&generic, &assumptions, &mut scratch);
let kernel_answer = query_temporal_memory_kernel(width, horizon, &assumptions);
assert_eq!(generic_answer.is_some(), kernel_answer.is_some());
if let Some(assignment) = kernel_answer {
assert!(satisfies(&formula, &assignment));
assert!(assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
}));
}
}
}
}
}
#[test]
fn temporal_vocabulary_is_recognized_from_cnf_and_matches_varisat() {
let mut rng = Rng(0xdec0_de24);
for kind in ["copy", "negate", "permute", "xor", "circuit"] {
let width = 4;
let horizon = 7;
let (vars, formula) = temporal_vocabulary_formula(kind, width, horizon).unwrap();
let kernel = RecognizedTemporalKernel::recognize(&formula, width, horizon).unwrap();
for _ in 0..100 {
let mut assumptions = vec![None; vars];
for _ in 0..5 {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
let kernel_answer = kernel.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(assumptions.iter().enumerate().filter_map(
|(variable, value)| value.map(|value| Clause(vec![(variable, value)])),
));
let varisat_answer = solve_with_varisat(vars, &constrained);
assert_eq!(kernel_answer.is_some(), varisat_answer.is_some(), "{kind}");
if let Some(assignment) = kernel_answer {
assert!(satisfies(&formula, &assignment), "{kind}");
assert!(assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
}));
}
}
}
}
#[test]
fn temporal_recognizer_rejects_a_changed_transition_template() {
let width = 4;
let horizon = 3;
let (_, mut formula) = temporal_vocabulary_formula("copy", width, horizon).unwrap();
let second_step_clause = 2 * width;
formula[second_step_clause].0[0].1 = !formula[second_step_clause].0[0].1;
assert!(RecognizedTemporalKernel::recognize(&formula, width, horizon).is_err());
}
#[test]
fn exact_composition_recognizer_handles_functions_outside_fixed_vocabulary() {
let mut rng = Rng(0xc0de_0517);
for kind in ["majority3", "mux3", "mixed3", "cascade4"] {
let width = 4;
let horizon = 6;
let (vars, formula) = temporal_composition_formula(kind, width, horizon).unwrap();
assert!(RecognizedTemporalKernel::recognize(&formula, width, horizon).is_err());
let kernel =
RecognizedTemporalKernel::recognize_exact_composition(&formula, width, horizon)
.unwrap();
for _ in 0..50 {
let mut assumptions = vec![None; vars];
for _ in 0..5 {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
let kernel_answer = kernel.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(assumptions.iter().enumerate().filter_map(
|(variable, value)| value.map(|value| Clause(vec![(variable, value)])),
));
let varisat_answer = solve_with_varisat(vars, &constrained);
assert_eq!(kernel_answer.is_some(), varisat_answer.is_some(), "{kind}");
if let Some(assignment) = kernel_answer {
assert!(satisfies(&formula, &assignment), "{kind}");
}
}
}
}
#[test]
fn exact_composition_recognizer_rejects_nondeterminism() {
let width = 4;
let horizon = 3;
let (_, mut formula) = temporal_composition_formula("majority3", width, horizon).unwrap();
let unconstrained_outputs: BTreeSet<_> = (1..=horizon).map(|time| time * width).collect();
formula.retain(|clause| {
!clause
.0
.iter()
.any(|(variable, _)| unconstrained_outputs.contains(variable))
});
assert!(
RecognizedTemporalKernel::recognize_exact_composition(&formula, width, horizon)
.is_err()
);
}
#[test]
fn local_composition_recognizer_matches_exact_without_fixed_vocabulary() {
let mut rng = Rng(0x10ca_1c0e);
for kind in ["majority3", "mux3", "mixed3", "cascade4"] {
let width = 7;
let horizon = 9;
let (vars, formula) = temporal_composition_formula(kind, width, horizon).unwrap();
let local =
RecognizedTemporalKernel::recognize_local_composition(&formula, width, horizon)
.unwrap();
let exact =
RecognizedTemporalKernel::recognize_exact_composition(&formula, width, horizon)
.unwrap();
assert_eq!(local.jumps[0], exact.jumps[0], "{kind}");
for _ in 0..50 {
let mut assumptions = vec![None; vars];
for _ in 0..6 {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
assert_eq!(
local.query(&assumptions).is_some(),
exact.query(&assumptions).is_some(),
"{kind}"
);
if let Some(assignment) = local.query(&assumptions) {
assert!(satisfies(&formula, &assignment), "{kind}");
}
}
}
}
#[test]
fn local_composition_recognizer_rejects_cross_output_clauses() {
let width = 4;
let horizon = 2;
let (_, mut formula) = temporal_composition_formula("mixed3", width, horizon).unwrap();
formula[0].0.push((width + 1, true));
formula[0].0.sort_unstable();
assert!(
RecognizedTemporalKernel::recognize_local_composition(&formula, width, horizon)
.is_err()
);
}
#[test]
fn symbolic_transition_replays_without_exponential_state_table() {
let width = 24;
let horizon = 40;
let (vars, formula) = temporal_composition_formula("cascade4", width, horizon).unwrap();
let transition = SymbolicTemporalTransition::recognize(&formula, width, horizon).unwrap();
assert_eq!(transition.representation_entries(), width * (4 + 16));
let mut assumptions = vec![None; vars];
for (bit, value) in assumptions.iter_mut().take(width).enumerate() {
*value = Some(bit % 3 == 0);
}
let assignment = transition.query(&assumptions).unwrap();
assert!(satisfies(&formula, &assignment));
}
#[test]
fn symbolic_transition_matches_varisat_with_observations() {
let width = 7;
let horizon = 12;
let (vars, formula) = temporal_composition_formula("mux3", width, horizon).unwrap();
let transition = SymbolicTemporalTransition::recognize(&formula, width, horizon).unwrap();
let mut rng = Rng(0x5a8b_011c);
for _ in 0..50 {
let mut assumptions = vec![None; vars];
for value in assumptions.iter_mut().take(width) {
*value = Some(rng.next() & 1 == 1);
}
for _ in 0..4 {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
let symbolic = transition.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Clause(vec![(variable, value)]))
}),
);
let varisat = solve_with_varisat(vars, &constrained);
assert_eq!(symbolic.is_some(), varisat.is_some());
if let Some(assignment) = symbolic {
assert!(satisfies(&formula, &assignment));
}
}
}
#[test]
fn symbolic_preimage_finds_partial_initial_state_and_witness() {
let width = 6;
let horizon = 5;
let (vars, formula) = temporal_composition_formula("mixed3", width, horizon).unwrap();
let mut preimage = SymbolicPreimageTransition::recognize_ordered(
&formula, width, horizon, 100_000, "natural",
)
.unwrap();
let mut rng = Rng(0xbdd5_ea2c);
for _ in 0..40 {
let mut assumptions = vec![None; vars];
for _ in 0..8 {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
let answer = preimage.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Clause(vec![(variable, value)]))
}),
);
let varisat = solve_with_varisat(vars, &constrained);
assert_eq!(answer.is_some(), varisat.is_some());
if let Some(assignment) = answer {
assert!(satisfies(&formula, &assignment));
assert!(assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
}));
}
}
}
#[test]
fn symbolic_preimage_node_gate_rejects_growth_exactly() {
let width = 10;
let horizon = 20;
let (_, formula) = temporal_composition_formula("cascade4", width, horizon).unwrap();
assert!(
SymbolicPreimageTransition::recognize_ordered(&formula, width, horizon, 10, "natural")
.is_err()
);
}
#[test]
fn asymmetric_preimages_preserve_answers_across_fixed_orders() {
let mut rng = Rng(0xa51e_77ac);
for kind in ["hub3", "tree3", "irregular3"] {
let width = 7;
let horizon = 6;
let (vars, formula) = temporal_composition_formula(kind, width, horizon).unwrap();
for order in ["natural", "reverse", "evenodd", "dependency"] {
let mut preimage = SymbolicPreimageTransition::recognize_ordered(
&formula, width, horizon, 200_000, order,
)
.unwrap();
for _ in 0..12 {
let mut assumptions = vec![None; vars];
for _ in 0..6 {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
let answer = preimage.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(assumptions.iter().enumerate().filter_map(
|(variable, value)| value.map(|value| Clause(vec![(variable, value)])),
));
assert_eq!(
answer.is_some(),
solve_with_varisat(vars, &constrained).is_some(),
"{kind}/{order}"
);
if let Some(assignment) = answer {
assert!(satisfies(&formula, &assignment));
}
}
}
}
}
#[test]
fn symbolic_preimage_reuses_exact_frame_cycles() {
let width = 6;
let horizon = 200;
let (vars, formula) = temporal_composition_formula("majority3", width, horizon).unwrap();
let mut preimage = SymbolicPreimageTransition::recognize_ordered(
&formula,
width,
horizon,
200_000,
"dependency",
)
.unwrap();
assert!(preimage.cycle().is_some());
assert!(preimage.compiled_frames() < horizon / 4);
let mut assumptions = vec![None; vars];
assumptions[0] = Some(true);
assumptions[137 * width + 3] = Some(false);
let answer = preimage.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| value.map(|value| Clause(vec![(variable, value)]))),
);
assert_eq!(
answer.is_some(),
solve_with_varisat(vars, &constrained).is_some()
);
if let Some(assignment) = answer {
assert!(satisfies(&formula, &assignment));
}
}
#[test]
fn preimage_growth_guard_rejects_early_without_approximating() {
let width = 9;
let horizon = 137;
let (_, explosive) = temporal_composition_formula("cascade4", width, horizon).unwrap();
let error = SymbolicPreimageTransition::recognize_ordered(
&explosive,
width,
horizon,
200_000,
"dependency-guard",
)
.err()
.unwrap();
assert!(error.contains("growth guard"));
let (_, compact) = temporal_composition_formula("majority3", width, horizon).unwrap();
let guarded = SymbolicPreimageTransition::recognize_ordered(
&compact,
width,
horizon,
200_000,
"dependency-guard",
)
.unwrap();
assert!(guarded.cycle().is_some());
}
#[test]
fn hybrid_preimage_falls_back_exactly_on_bdd_growth() {
let width = 9;
let horizon = 137;
let (vars, formula) = temporal_composition_formula("cascade4", width, horizon).unwrap();
let (mut hybrid, backend, reason) =
HybridTemporalPreimage::recognize(&formula, width, horizon, 200_000).unwrap();
assert_eq!(backend, "cdcl-fallback");
assert!(reason.unwrap().contains("growth guard"));
let mut assumptions = vec![None; vars];
assumptions[3] = Some(true);
assumptions[91 * width + 4] = Some(false);
let answer = hybrid.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| value.map(|value| Clause(vec![(variable, value)]))),
);
assert_eq!(
answer.is_some(),
solve_with_varisat(vars, &constrained).is_some()
);
if let Some(assignment) = answer {
assert!(satisfies(&formula, &assignment));
}
}
#[test]
fn checkpoint_cdcl_reuses_bdd_prefix_exactly() {
let width = 9;
let horizon = 137;
let (vars, formula) = temporal_composition_formula("cascade4", width, horizon).unwrap();
let mut checkpoint = CheckpointCdclPreimage::recognize_with_encoding(
&formula, width, horizon, 20, 200_000, "bdd",
)
.unwrap();
assert_eq!(checkpoint.checkpoint, 20);
assert!(checkpoint.bdd_nodes > 0);
let mut rng = Rng(0xc1ac_0170);
for _ in 0..3 {
let mut assumptions = vec![None; vars];
for _ in 0..7 {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
let answer = checkpoint.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Clause(vec![(variable, value)]))
}),
);
assert_eq!(
answer.is_some(),
solve_with_varisat(vars, &constrained).is_some()
);
if let Some(assignment) = answer {
assert!(satisfies(&formula, &assignment));
}
}
}
#[test]
fn aig_checkpoint_preserves_answers_and_witnesses() {
let width = 5;
let horizon = 23;
let (vars, formula) = temporal_composition_formula("cascade4", width, horizon).unwrap();
let mut checkpoint = CheckpointCdclPreimage::recognize_with_encoding(
&formula, width, horizon, 7, 50_000, "aig",
)
.unwrap();
assert!(checkpoint.encoding_nodes > 0);
assert_eq!(checkpoint.encoding_clauses, checkpoint.encoding_nodes * 3);
let mut assumptions = vec![None; vars];
assumptions[2] = Some(true);
assumptions[width * 11 + 3] = Some(false);
let answer = checkpoint.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| value.map(|value| Clause(vec![(variable, value)]))),
);
assert_eq!(
answer.is_some(),
solve_with_varisat(vars, &constrained).is_some()
);
if let Some(assignment) = answer {
assert!(satisfies(&formula, &assignment));
}
}
#[test]
fn lazy_checkpoint_adds_observed_prefix_cones_exactly() {
let width = 5;
let horizon = 31;
let checkpoint_frame = 9;
let (vars, formula) = temporal_composition_formula("cascade4", width, horizon).unwrap();
let mut checkpoint = CheckpointCdclPreimage::recognize_with_encoding(
&formula,
width,
horizon,
checkpoint_frame,
50_000,
"lazy-bdd",
)
.unwrap();
let initial_nodes = checkpoint.encoding_nodes;
assert!(initial_nodes < checkpoint.bdd_nodes);
let mut assumptions = vec![None; vars];
assumptions[width * 3 + 1] = Some(true);
assumptions[width * 7 + 4] = Some(false);
assumptions[width * 19 + 2] = Some(true);
let answer = checkpoint.query(&assumptions);
assert!(checkpoint.encoding_nodes >= initial_nodes);
let mut constrained = formula.clone();
constrained.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| value.map(|value| Clause(vec![(variable, value)]))),
);
assert_eq!(
answer.is_some(),
solve_with_varisat(vars, &constrained).is_some()
);
if let Some(assignment) = answer {
assert!(satisfies(&formula, &assignment));
assert!(assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
}));
}
}
#[test]
fn native_bdd_theory_reconciles_checkpoint_models_exactly() {
let width = 5;
let horizon = 31;
let (vars, formula) = temporal_composition_formula("cascade4", width, horizon).unwrap();
let mut engine =
NativeBddTheoryPreimage::recognize(&formula, width, horizon, 9, 50_000).unwrap();
let mut rng = Rng(0x07e0_f1e5);
for _ in 0..5 {
let mut assumptions = vec![None; vars];
for _ in 0..6 {
assumptions[rng.below(vars)] = Some(rng.next() & 1 == 1);
}
let answer = engine.query(&assumptions);
let mut constrained = formula.clone();
constrained.extend(
assumptions
.iter()
.enumerate()
.filter_map(|(variable, value)| {
value.map(|value| Clause(vec![(variable, value)]))
}),
);
assert_eq!(
answer.is_some(),
solve_with_varisat(vars, &constrained).is_some()
);
if let Some(assignment) = answer {
assert!(satisfies(&formula, &assignment));
assert!(assumptions.iter().enumerate().all(|(variable, required)| {
required.is_none_or(|value| assignment[variable] == value)
}));
}
}
}
#[test]
fn cq_portfolio_gate_is_static_and_explainable() {
let (_, cascade) = temporal_composition_formula("cascade4", 9, 137).unwrap();
let dense = cq_portfolio_decision(&cascade, 9, 137, 50, 5.0).unwrap();
assert!(dense.specialized);
assert_eq!(dense.reason, "dense-transition");
assert!(
!cq_portfolio_decision(&cascade, 9, 137, 7, 5.0)
.unwrap()
.specialized
);
assert!(
!cq_portfolio_decision(&cascade, 9, 137, 50, 10.0)
.unwrap()
.specialized
);
let (_, wide_cascade) = temporal_composition_formula("cascade4", 10, 137).unwrap();
assert!(
!cq_portfolio_decision(&wide_cascade, 10, 137, 50, 5.0)
.unwrap()
.specialized
);
let (_, hub) = temporal_composition_formula("hub3", 7, 137).unwrap();
let narrow_hub = cq_portfolio_decision(&hub, 7, 137, 128, 5.0).unwrap();
assert!(narrow_hub.specialized);
assert_eq!(narrow_hub.reason, "narrow-hub");
assert!(
!cq_portfolio_decision(&hub, 7, 137, 127, 5.0)
.unwrap()
.specialized
);
let (_, tree_short) = temporal_composition_formula("tree3", 9, 137).unwrap();
let fallback = cq_portfolio_decision(&tree_short, 9, 137, 50, 5.0).unwrap();
assert!(!fallback.specialized);
assert_eq!(fallback.reason, "cdcl-fallback");
let (_, tree_long) = temporal_composition_formula("tree3", 11, 1_333).unwrap();
let conservative_fallback = cq_portfolio_decision(&tree_long, 11, 1_333, 50, 5.0).unwrap();
assert!(!conservative_fallback.specialized);
assert_eq!(conservative_fallback.reason, "cdcl-fallback");
let (_, watchdog) = temporal_composition_formula("watchdog4", 9, 137).unwrap();
assert_eq!(
cq_portfolio_decision(&watchdog, 9, 137, 50, 5.0)
.unwrap()
.reason,
"dense-transition"
);
let (_, sensor_vote) = temporal_composition_formula("sensor-vote3", 8, 257).unwrap();
assert_eq!(
cq_portfolio_decision(&sensor_vote, 8, 257, 50, 5.0)
.unwrap()
.reason,
"cdcl-fallback"
);
}
#[test]
fn ascii_aiger_import_preserves_transition_and_initial_state() {
let path =
std::env::temp_dir().join(format!("cq-sat-aiger-import-{}.aag", std::process::id()));
fs::write(
&path,
"aag 5 0 4 1 1\n2 4\n4 6\n6 8\n8 10\n8\n10 2 5\nc\nsmall closed sequential model\n",
)
.unwrap();
let model = parse_aag(&path).unwrap();
std::fs::remove_file(path).unwrap();
assert_eq!(model.latches.len(), 4);
assert_eq!(model.outputs, vec![8]);
let (variables, formula, initial) = aag_temporal_formula(&model, 3).unwrap();
assert_eq!(variables, 16);
assert_eq!(initial, vec![Some(false); 4]);
assert!(
cq_portfolio_decision(&formula, 4, 3, 8, 2.0)
.unwrap()
.specialized
);
let transition = SymbolicTemporalTransition::recognize(&formula, 4, 3).unwrap();
let mut assumptions = vec![None; variables];
assumptions[..4].copy_from_slice(&initial);
let assignment = transition.query(&assumptions).unwrap();
assert!(assignment.iter().all(|value| !value));
assert!(satisfies(&formula, &assignment));
}
fn binary_aiger_fixture() -> Vec<u8> {
let mut bytes = b"aig 4 2 1 1 1\n6 0\n8\n".to_vec();
// lhs=8, rhs0=4, rhs1=2; binary AIGER stores the two deltas.
bytes.extend([4, 2]);
bytes.extend_from_slice(
b"i0 left_request\ni1 right_request\nl0 state\no0 simultaneous_request\nc\nfixture\n",
);
bytes
}
fn wide_sparse_aiger_fixture() -> Vec<u8> {
let mut source = String::from("aag 18 16 1 1 1\n");
for variable in 1..=16 {
source.push_str(&format!("{}\n", variable * 2));
}
source.push_str("34 2 0\n36\n36 4 2\n");
for input in 0..16 {
source.push_str(&format!("i{input} sensor_{input}\n"));
}
source.push_str("l0 mode\no0 conflicting_primary_sensors\nc\nwide sparse fixture\n");
source.into_bytes()
}
fn wide_dense_aiger_fixture() -> Vec<u8> {
dense_support_aiger_fixture(9)
}
fn dense_support_aiger_fixture(relevant: usize) -> Vec<u8> {
assert!((2..=16).contains(&relevant));
let gates = relevant - 1;
let maximum = 17 + gates;
let mut source = format!("aag {maximum} 16 1 1 {gates}\n");
for variable in 1..=16 {
source.push_str(&format!("{}\n", variable * 2));
}
source.push_str(&format!("34 2 0\n{}\n", maximum * 2));
let mut accumulated = 2usize;
for (gate, input) in (1..relevant).enumerate() {
let output = (18 + gate) * 2;
source.push_str(&format!("{output} {accumulated} {}\n", (input + 1) * 2));
accumulated = output;
}
source.extend(std::iter::once('c'));
source.push_str("\nwide dense fixture\n");
source.into_bytes()
}
#[test]
fn binary_aiger_import_matches_ascii_semantics() {
let ascii = b"aag 4 2 1 1 1\n2\n4\n6 6 0\n8\n8 4 2\ni0 left_request\ni1 right_request\nl0 state\no0 simultaneous_request\nc\nfixture\n";
let expected = parse_aiger_bytes(ascii).unwrap();
let actual = parse_aiger_bytes(&binary_aiger_fixture()).unwrap();
assert_eq!(actual, expected);
let encoding = aag_bmc_encoding(&actual, 2).unwrap();
let run = run_aag_bmc(&actual, 2, &encoding).unwrap();
assert!(run.first_sat_query.is_some());
assert!(run.witnesses_valid);
}
#[test]
fn interface_quotient_repairs_locally_and_recovers_an_avoiding_trace() {
let model = parse_aiger_bytes(&binary_aiger_fixture()).unwrap();
let table = AagInterfaceTable::compile(&model).unwrap();
let mut tree = InterfaceQuotientTree::new(&table, 1).unwrap();
let mut constraints = vec![vec![None; 2]; 2];
constraints[1] = vec![Some(true), Some(true)];
assert!(
tree.query_avoiding(&model, 0, &constraints, true)
.unwrap()
.witness
.is_none()
);
constraints[1][1] = None;
let recovered_outcome = tree.query_avoiding(&model, 0, &constraints, true).unwrap();
let recovered = recovered_outcome.witness.unwrap();
assert_eq!(recovered.states, vec![0, 0]);
assert_eq!(recovered.inputs[1] & 1, 1);
assert_eq!(recovered.inputs[1] >> 1 & 1, 0);
assert_eq!(recovered_outcome.repaired_leaves, 0); // terminal-only change
assert_eq!(recovered_outcome.repaired_internal_nodes, 0);
constraints[0][0] = Some(true);
let repaired_outcome = tree.query_avoiding(&model, 0, &constraints, true).unwrap();
let repaired = repaired_outcome.witness.unwrap();
// This input does not affect the latch transition, so the semantic leaf
// quotient is unchanged and no ancestor is needlessly recomposed.
assert_eq!(repaired_outcome.repaired_leaves, 0);
assert_eq!(repaired_outcome.repaired_internal_nodes, 0);
for (input, constraint) in repaired.inputs.iter().zip(&constraints).take(2) {
assert!(table.input_allowed(*input, constraint));
}
assert_eq!(
table.next[repaired.states[0]][repaired.inputs[0]],
repaired.states[1]
);
assert_eq!(
table.bad_masks[repaired.states[1]][repaired.inputs[1]] & 1,
0
);
let input_driven = parse_aiger_bytes(b"aag 2 1 1 1 0\n2\n4 2 0\n4\n").unwrap();
let driven_table = AagInterfaceTable::compile(&input_driven).unwrap();
let mut driven_tree = InterfaceQuotientTree::new(&driven_table, 1).unwrap();
let driven_constraints = vec![vec![Some(true)], vec![None]];
let driven = driven_tree
.query_avoiding(&input_driven, 0, &driven_constraints, true)
.unwrap();
assert_eq!(driven.repaired_leaves, 1);
assert_eq!(driven.repaired_internal_nodes, 1);
}
#[test]
fn interface_projection_is_exact_for_sixteen_declared_inputs() {
let model = parse_aiger_bytes(&wide_sparse_aiger_fixture()).unwrap();
let table = AagInterfaceTable::compile(&model).unwrap();
assert_eq!(table.declared_input_count, 16);
assert_eq!(table.projected_inputs, vec![0, 1]);
assert_eq!(table.input_count, 2);
for state in 0..2usize {
for declared in 0..=u16::MAX {
let declared = u64::from(declared);
let projected = table.project_input(declared);
assert_eq!(table.next[state][projected], usize::from(declared & 1 == 1));
assert_eq!(
table.bad_masks[state][projected] & 1 != 0,
declared & 0b11 == 0b11
);
}
}
let constraints = vec![vec![None; 2]; 2];
let mut tree = InterfaceQuotientTree::new(&table, 1).unwrap();
let witness = tree
.query_avoiding(&model, 0, &constraints, true)
.unwrap()
.witness
.unwrap();
assert!(validate_interface_witness(
&model,
&table,
0,
&constraints,
&witness
));
assert!(witness.declared_inputs.iter().all(|input| input >> 2 == 0));
}
#[test]
fn interface_projection_rejects_support_wider_than_eight() {
let model = parse_aiger_bytes(&wide_dense_aiger_fixture()).unwrap();
let error = AagInterfaceTable::compile(&model).unwrap_err();
assert!(error.contains("projected support"));
assert!(error.contains("found 9"));
}
#[test]
fn predicate_interface_handles_nine_relevant_inputs_without_enumeration() {
let model = parse_aiger_bytes(&wide_dense_aiger_fixture()).unwrap();
let mut predicate = AagPredicateInterface::compile(&model).unwrap();
assert_eq!(predicate.projected_inputs, (0..9).collect::<Vec<_>>());
assert!(predicate.manager.nodes.len() < 200);
let unconstrained = vec![None; 9];
let relation = predicate.relation(&unconstrained).unwrap();
for source in 0..2 {
assert!(relation.contains(source, 0));
assert!(relation.contains(source, 1));
}
let mut input_zero_true = vec![None; 9];
input_zero_true[0] = Some(true);
let relation = predicate.relation(&input_zero_true).unwrap();
for source in 0..2 {
assert!(!relation.contains(source, 0));
assert!(relation.contains(source, 1));
}
let all_true = vec![Some(true); 9];
assert!(
predicate
.witness_input(0, None, Some(0), &all_true)
.unwrap()
.is_none()
);
let mut avoidable = all_true;
avoidable[8] = Some(false);
let witness = predicate
.witness_input(0, None, Some(0), &avoidable)
.unwrap()
.unwrap();
assert_eq!(witness >> 8 & 1, 0);
}
#[test]
fn predicate_interface_scales_compactly_through_sixteen_relevant_inputs() {
for relevant in 9..=16 {
let model = parse_aiger_bytes(&dense_support_aiger_fixture(relevant)).unwrap();
let mut predicate = AagPredicateInterface::compile(&model).unwrap();
assert_eq!(predicate.projected_inputs.len(), relevant);
assert!(predicate.manager.nodes.len() < relevant * 20);
let all_true = vec![Some(true); relevant];
assert!(
predicate
.witness_input(0, None, Some(0), &all_true)
.unwrap()
.is_none()
);
for released in 0..relevant {
let mut avoiding = all_true.clone();
avoiding[released] = Some(false);
let witness = predicate
.witness_input(0, None, Some(0), &avoiding)
.unwrap()
.unwrap();
assert_eq!(witness >> released & 1, 0);
}
}
}
#[test]
fn predicate_quotient_powers_dense_relations_and_recovers_exact_trace() {
let model = parse_aiger_bytes(&dense_support_aiger_fixture(16)).unwrap();
let mut quotient = PredicateQuotient::new(&model).unwrap();
let horizon = 32;
let transition_constraints = vec![Some(true); 16];
let mut constraints = vec![transition_constraints; horizon + 1];
constraints[horizon][15] = Some(false);
let witness = quotient.query(0, 0, &constraints).unwrap().unwrap();
assert_eq!(witness.states.len(), horizon + 1);
assert_eq!(witness.declared_inputs.len(), horizon + 1);
assert_eq!(witness.states[0], 0);
assert!(witness.states.iter().skip(1).all(|state| *state == 1));
assert_eq!(witness.declared_inputs[horizon] >> 15 & 1, 0);
assert!(validate_predicate_witness(
&model,
"ient.interface.projected_inputs,
0,
&constraints,
&witness
));
assert!(
quotient
.power_cache
.contains_key(&(vec![Some(true); 16], horizon))
);
constraints[horizon][15] = Some(true);
assert!(quotient.query(0, 0, &constraints).unwrap().is_none());
}
#[test]
fn predicate_symbolic_baseline_agrees_when_yosys_is_available() {
if Command::new("yosys").arg("-V").output().is_err() {
eprintln!("skipping predicate symbolic baseline because Yosys is unavailable");
return;
}
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/mobile-robot/firmware/dense-sensor-consensus.aag");
let output = std::env::temp_dir().join(format!(
"cq-sat-predicate-symbolic-{}.csv",
std::process::id()
));
benchmark_aiger_predicate_symbolic(&input, 2, 1, &output).unwrap();
let report = fs::read_to_string(&output).unwrap();
fs::remove_file(output).unwrap();
assert_eq!(report.lines().count(), 2);
assert!(report.contains(",true,true,false,ok\n"));
assert!(report.lines().all(|line| line.split(',').count() == 22));
}
#[test]
fn dense_product_fixtures_have_exact_width_and_state_dependent_properties() {
let cases = [
(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
10,
9,
2,
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
13,
12,
3,
),
(
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
17,
16,
4,
),
];
for (relative, declared, relevant, latches) in cases {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative);
let model = parse_aag(&path).unwrap();
let predicate = AagPredicateInterface::compile(&model).unwrap();
assert_eq!(model.inputs.len(), declared);
assert_eq!(predicate.projected_inputs.len(), relevant);
assert_eq!(model.latches.len(), latches);
assert!(predicate.manager.nodes.len() < 500);
let state_mask = (1usize << latches) - 1;
let mut state_observable = false;
for declared_input in 0..(1u64 << declared) {
let evaluate = |state: usize| {
let mut values = vec![false; model.max_variable + 1];
for (bit, latch) in model.latches.iter().enumerate() {
values[latch.current / 2] = state >> bit & 1 == 1;
}
for (input, literal) in model.inputs.iter().enumerate() {
values[literal / 2] = declared_input >> input & 1 == 1;
}
for gate in &model.ands {
values[gate.output / 2] = evaluate_aag_literal(gate.left, &values)
&& evaluate_aag_literal(gate.right, &values);
}
evaluate_aag_literal(model.outputs[0], &values)
};
if evaluate(0) != evaluate(state_mask) {
state_observable = true;
break;
}
}
assert!(
state_observable,
"property is not state dependent: {relative}"
);
}
}
#[test]
fn dense_product_fixed_bad_traces_are_unavoidable_across_backends() {
let fixtures = [
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
];
let horizon = 4;
let yosys_available = Command::new("yosys").arg("-V").output().is_ok();
for relative in fixtures {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative);
let model = parse_aag(&path).unwrap();
let encoding = aag_bmc_encoding(&model, horizon).unwrap();
let query = encoding
.queries
.iter()
.find(|query| query.frame == horizon && query.output == 0)
.unwrap();
let mut unsafe_solver = Solver::new();
add_to_varisat(&mut unsafe_solver, &encoding.clauses);
let unsafe_trace =
causal_query_witness(&mut unsafe_solver, encoding.variables, query.assumption)
.unwrap()
.unwrap();
let mut quotient = PredicateQuotient::new(&model).unwrap();
let mut constraints =
vec![vec![None; quotient.interface.projected_inputs.len()]; horizon + 1];
let mut assumptions = vec![None; encoding.variables];
for frame in 0..=horizon {
for (projected, input) in quotient.interface.projected_inputs.iter().enumerate() {
let variable = frame * model.max_variable + model.inputs[*input] / 2 - 1;
let value = unsafe_trace[variable];
constraints[frame][projected] = Some(value);
assumptions[variable] = Some(value);
}
}
let initial_state = model
.latches
.iter()
.enumerate()
.fold(0usize, |state, (bit, latch)| {
state | (usize::from(latch.initial == Some(true)) << bit)
});
assert!(
quotient
.query(initial_state, 0, &constraints)
.unwrap()
.is_none()
);
assert!(
!add_causal_counterfactual_assumption(&mut assumptions, query.assumption).unwrap()
);
let mut avoiding_solver = Solver::new();
add_to_varisat(&mut avoiding_solver, &encoding.clauses);
assert!(!solve_causal_persistent(&mut avoiding_solver, &assumptions).unwrap());
if yosys_available {
let mut script = format!(
"read_aiger {}; hierarchy -auto-top; sat -seq {}",
path.display(),
horizon + 1
);
for (frame, frame_constraints) in constraints.iter().enumerate() {
for (projected, required) in frame_constraints.iter().enumerate() {
let input = quotient.interface.projected_inputs[projected];
script.push_str(&format!(
" -set-at {} {} {}",
frame + 1,
model.input_names[input],
usize::from(required.unwrap())
));
}
}
script.push_str(&format!(
" -set-at {} {} 0;",
horizon + 1,
model.output_names[0]
));
let external = Command::new("yosys")
.args(["-Q", "-p", &script])
.output()
.unwrap();
assert!(external.status.success());
assert!(
String::from_utf8_lossy(&external.stdout)
.contains("SAT solving finished - no model found.")
);
}
}
}
#[test]
fn predicate_quotient_fails_closed_outside_static_resource_bounds() {
let too_narrow = parse_aiger_bytes(&dense_support_aiger_fixture(8)).unwrap();
assert!(
AagPredicateInterface::compile(&too_narrow)
.err()
.unwrap()
.contains("requires 9..=16 relevant inputs")
);
let mut five_latches = String::from("aag 22 9 5 1 8\n");
for variable in 1..=9 {
five_latches.push_str(&format!("{}\n", variable * 2));
}
for variable in 10..=14 {
five_latches.push_str(&format!("{} 2 0\n", variable * 2));
}
five_latches.push_str("44\n");
let mut accumulated = 2;
for (gate, input) in (2..=9).enumerate() {
let output = (15 + gate) * 2;
five_latches.push_str(&format!("{output} {accumulated} {}\n", input * 2));
accumulated = output;
}
let too_stateful = parse_aiger_bytes(five_latches.as_bytes()).unwrap();
assert!(
AagPredicateInterface::compile(&too_stateful)
.err()
.unwrap()
.contains("requires 1..=4 latches")
);
assert!(
benchmark_aiger_predicate_symbolic(
Path::new("unused"),
INTERFACE_QUOTIENT_MAX_HORIZON + 1,
1,
Path::new("unused")
)
.unwrap_err()
.contains("horizon exceeds")
);
}
#[test]
fn predicate_quotient_static_gate_excludes_observed_loss_regimes() {
assert!(!predicate_quotient_admitted(9, 2, 8, 99));
assert!(predicate_quotient_admitted(9, 2, 8, 100));
assert!(!predicate_quotient_admitted(12, 3, 8, 100));
assert!(predicate_quotient_admitted(12, 3, 16, 100));
assert!(!predicate_quotient_admitted(16, 4, 16, 100));
assert!(predicate_quotient_admitted(16, 4, 32, 100));
assert!(!predicate_quotient_admitted(8, 2, 64, 1000));
assert!(!predicate_quotient_admitted(16, 5, 64, 1000));
}
#[test]
fn independent_predicate_checker_matches_producer_relations_and_terminal_sets() {
let fixtures = [
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
];
for relative in fixtures {
let model = parse_aag(&Path::new(env!("CARGO_MANIFEST_DIR")).join(relative)).unwrap();
let mut producer = PredicateQuotient::new(&model).unwrap();
let mut checker = IndependentPredicateChecker::new(&model).unwrap();
assert_eq!(checker.relevant_inputs, producer.interface.projected_inputs);
let width = checker.relevant_inputs.len();
let mut released = vec![Some(true); width];
released[width - 1] = Some(false);
for constraints in [vec![None; width], vec![Some(true); width], released] {
let expected = producer.relation(&constraints).unwrap();
let actual = checker.one_step_relation(&constraints).unwrap();
assert_eq!(
actual,
expected
.rows()
.iter()
.map(|row| row[0] as u16)
.collect::<Vec<_>>()
);
for length in [1, 2, 7] {
let expected = producer.relation_power(&constraints, length).unwrap();
let actual = IndependentPredicateChecker::power(&actual, length).unwrap();
assert_eq!(
actual,
expected
.rows()
.iter()
.map(|row| row[0] as u16)
.collect::<Vec<_>>()
);
}
let safe = checker.terminal_safe_states(0, &constraints).unwrap();
for state in 0..checker.states {
assert_eq!(
safe >> state & 1 == 1,
producer
.interface
.witness_input(state, None, Some(0), &constraints)
.unwrap()
.is_some()
);
}
}
}
}
#[test]
fn predicate_relation_proofs_replace_input_enumeration_exactly() {
let fixtures = [
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
];
for relative in fixtures {
let model = parse_aag(&Path::new(env!("CARGO_MANIFEST_DIR")).join(relative)).unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let constraints = vec![None; relevant.len()];
let proof = predicate_proof_relation_experiment(&model, &constraints).unwrap();
let mut exhaustive = IndependentPredicateChecker::new(&model).unwrap();
assert_eq!(
proof.relation,
exhaustive.one_step_relation(&constraints).unwrap()
);
assert!(proof.witness_count >= proof.relation.len());
assert!(proof.proof_bytes > 0);
assert!(proof.generation_ns > 0);
assert!(proof.verification_ns > 0);
}
let model =
parse_aag(&Path::new(env!("CARGO_MANIFEST_DIR")).join(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
))
.unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let constraints = vec![None; relevant.len()];
let mut quotient = PredicateQuotient::new(&model).unwrap();
let row = quotient.relation(&constraints).unwrap().rows()[0][0] as u16;
let clauses =
predicate_relation_completeness_clauses(&model, &relevant, 0, &constraints, row)
.unwrap();
let proof = generate_varisat_unsat_proof(&clauses).unwrap();
verify_varisat_unsat_proof(&clauses, &proof).unwrap();
assert!(verify_varisat_unsat_proof(&clauses, &proof[..proof.len() - 1]).is_err());
let omitted_target = row & (row - 1);
let incomplete = predicate_relation_completeness_clauses(
&model,
&relevant,
0,
&constraints,
omitted_target,
)
.unwrap();
assert!(generate_varisat_unsat_proof(&incomplete).is_err());
let stem = std::env::temp_dir().join(format!(
"cq-sat-predicate-proof-relation-{}",
std::process::id()
));
let transcript = stem.with_extension("transcript");
let output = stem.with_extension("csv");
fs::write(&transcript, "xxxxxxxxx\n").unwrap();
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
benchmark_aiger_predicate_proof_relation(&input, &transcript, 1, &output).unwrap();
let body = fs::read_to_string(&output).unwrap();
assert_eq!(body.lines().count(), 2);
assert_eq!(body.lines().nth(1).unwrap().split(',').count(), 15);
assert!(body.lines().nth(1).unwrap().ends_with(",true,ok"));
assert!(
benchmark_aiger_predicate_proof_relation(&input, &transcript, 1, &output)
.unwrap_err()
.contains("overwrite")
);
fs::remove_file(transcript).unwrap();
fs::remove_file(output).unwrap();
}
#[test]
fn predicate_terminal_proofs_replace_input_enumeration_exactly() {
let fixtures = [
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
];
for relative in fixtures {
let model = parse_aag(&Path::new(env!("CARGO_MANIFEST_DIR")).join(relative)).unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
for constraints in [vec![None; relevant.len()], vec![Some(true); relevant.len()]] {
let proof = predicate_proof_terminal_experiment(&model, &constraints, 0).unwrap();
let mut exhaustive = IndependentPredicateChecker::new(&model).unwrap();
assert_eq!(
proof.safe_states,
exhaustive.terminal_safe_states(0, &constraints).unwrap()
);
assert_eq!(proof.witness_count, proof.safe_states.count_ones() as usize);
assert!(proof.proof_bytes > 0);
assert!(proof.producer_ns > 0);
assert!(proof.generation_ns > 0);
assert!(proof.verification_ns > 0);
}
}
let model =
parse_aag(&Path::new(env!("CARGO_MANIFEST_DIR")).join(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
))
.unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let constraints = vec![Some(true); relevant.len()];
let terminal = predicate_proof_terminal_experiment(&model, &constraints, 0).unwrap();
assert_ne!(terminal.safe_states, 0);
let omitted = terminal.safe_states & (terminal.safe_states - 1);
let incomplete =
predicate_terminal_completeness_clauses(&model, &relevant, &constraints, 0, omitted)
.unwrap();
assert!(generate_varisat_unsat_proof(&incomplete).is_err());
let stem = std::env::temp_dir().join(format!(
"cq-sat-predicate-proof-terminal-{}",
std::process::id()
));
let transcript = stem.with_extension("transcript");
let output = stem.with_extension("csv");
fs::write(&transcript, "xxxxxxxxxxxx\n").unwrap();
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/actuator-controller/firmware/dense-actuator-interlock.aag");
benchmark_aiger_predicate_proof_terminal(&input, 0, &transcript, 1, &output).unwrap();
let body = fs::read_to_string(&output).unwrap();
assert_eq!(body.lines().count(), 2);
assert_eq!(body.lines().nth(1).unwrap().split(',').count(), 17);
assert!(body.lines().nth(1).unwrap().ends_with(",true,ok"));
assert!(
benchmark_aiger_predicate_proof_terminal(&input, 0, &transcript, 1, &output,)
.unwrap_err()
.contains("overwrite")
);
fs::remove_file(transcript).unwrap();
fs::remove_file(output).unwrap();
}
#[test]
fn predicate_certificate_round_trip_and_tamper_rejection() {
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/mobile-robot/firmware/dense-sensor-fusion.aag");
let stem = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-{}",
std::process::id()
));
let transcript = stem.with_extension("transcript");
let certificate_path = stem.with_extension("cert");
let mut lines = vec!["1".repeat(16); 9];
lines[8].replace_range(15..16, "0");
fs::write(&transcript, lines.join("\n") + "\n").unwrap();
certify_aiger_predicate(&input, 0, &transcript, &certificate_path).unwrap();
verify_aiger_predicate_certificate(&input, &certificate_path).unwrap();
let certificate = parse_predicate_certificate(&certificate_path).unwrap();
assert!(certificate.avoidable);
let write_tamper = |suffix: &str, body: String| {
let path = stem.with_extension(format!("{suffix}.cert"));
fs::write(&path, body).unwrap();
assert!(verify_aiger_predicate_certificate(&input, &path).is_err());
fs::remove_file(path).unwrap();
};
let mut tampered = certificate.clone();
tampered.input_sha256.replace_range(0..1, "0");
write_tamper("digest", predicate_certificate_body(&tampered));
let mut tampered = certificate.clone();
tampered.phases[0].rows[0] ^= 1;
write_tamper("relation", predicate_certificate_body(&tampered));
let mut tampered = certificate.clone();
tampered.terminal_safe_states ^= 1;
write_tamper("terminal", predicate_certificate_body(&tampered));
let mut tampered = certificate.clone();
tampered.inputs[0] ^= 1u64 << tampered.relevant_inputs[0];
write_tamper("witness", predicate_certificate_body(&tampered));
let body = predicate_certificate_body(&certificate);
write_tamper("unknown", body.clone() + "unknown=1\n");
write_tamper("truncated", body.trim_end_matches('\n').to_string());
write_tamper("crlf", body.replace('\n', "\r\n"));
write_tamper("number", body.replace("horizon=8", "horizon=+8"));
write_tamper(
"duplicate",
body.replace("horizon=8\n", "horizon=8\nhorizon=8\n"),
);
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let symlink_path = stem.with_extension("symlink.cert");
symlink(&certificate_path, &symlink_path).unwrap();
assert!(verify_aiger_predicate_certificate(&input, &symlink_path).is_err());
fs::remove_file(symlink_path).unwrap();
let transcript_symlink = stem.with_extension("symlink.transcript");
let rejected_certificate = stem.with_extension("symlink-output.cert");
symlink(&transcript, &transcript_symlink).unwrap();
assert!(
certify_aiger_predicate(&input, 0, &transcript_symlink, &rejected_certificate)
.is_err()
);
assert!(!rejected_certificate.exists());
fs::remove_file(transcript_symlink).unwrap();
}
fs::remove_file(transcript).unwrap();
fs::remove_file(certificate_path).unwrap();
}
#[test]
fn predicate_certificate_verifies_unavoidable_fixed_bad_trace() {
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/actuator-controller/firmware/dense-actuator-interlock.aag");
let model = parse_aag(&input).unwrap();
let horizon = 4;
let encoding = aag_bmc_encoding(&model, horizon).unwrap();
let query = encoding
.queries
.iter()
.find(|query| query.frame == horizon && query.output == 0)
.unwrap();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &encoding.clauses);
let trace = causal_query_witness(&mut solver, encoding.variables, query.assumption)
.unwrap()
.unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let transcript_body = (0..=horizon)
.map(|frame| {
relevant
.iter()
.map(|input| {
let variable = frame * model.max_variable + model.inputs[*input] / 2 - 1;
if trace[variable] { '1' } else { '0' }
})
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
+ "\n";
let stem = std::env::temp_dir().join(format!(
"cq-sat-predicate-unavoidable-{}",
std::process::id()
));
let transcript = stem.with_extension("transcript");
let certificate = stem.with_extension("cert");
fs::write(&transcript, transcript_body).unwrap();
certify_aiger_predicate(&input, 0, &transcript, &certificate).unwrap();
assert!(!parse_predicate_certificate(&certificate).unwrap().avoidable);
verify_aiger_predicate_certificate(&input, &certificate).unwrap();
fs::remove_file(transcript).unwrap();
fs::remove_file(&certificate).unwrap();
}
#[test]
fn predicate_certificate_v2_is_deterministic_exact_and_tamper_evident() {
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let transcript = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/predicate-certificate-cost/interrupt-h8-avoidable.transcript");
let stem = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-v2-{}",
std::process::id()
));
let certificate_path = stem.with_extension("cert2");
let duplicate_path = stem.with_extension("duplicate.cert2");
certify_aiger_predicate_v2(&input, 0, &transcript, &certificate_path).unwrap();
verify_aiger_predicate_certificate_v2(&input, &certificate_path).unwrap();
certify_aiger_predicate_v2(&input, 0, &transcript, &duplicate_path).unwrap();
assert_eq!(
fs::read(&certificate_path).unwrap(),
fs::read(&duplicate_path).unwrap()
);
let compatibility_bytes = fs::read(&certificate_path).unwrap();
assert_eq!(compatibility_bytes.len(), 9_706);
assert_eq!(
predicate_bytes_hex(&Sha256::digest(&compatibility_bytes)),
"6a1ffce05d42bfaa65a227da57647ba6abeac26c82b620219b84011d05ab1f6d"
);
assert!(
certify_aiger_predicate_v2(&input, 0, &transcript, &certificate_path)
.unwrap_err()
.contains("overwrite")
);
let certificate = parse_predicate_certificate_v2(&certificate_path).unwrap();
assert!(certificate.avoidable);
assert_eq!(certificate.phases.len(), 1);
assert_eq!(certificate.phases[0].proofs.len(), 4);
let write_tamper = |suffix: &str, certificate: &PredicateCertificateV2| {
let path = stem.with_extension(format!("{suffix}.cert2"));
fs::write(&path, predicate_certificate_v2_body(certificate)).unwrap();
assert!(
verify_aiger_predicate_certificate_v2(&input, &path).is_err(),
"v2 tamper unexpectedly verified: {suffix}"
);
fs::remove_file(path).unwrap();
};
let mut tampered = certificate.clone();
let replacement = if tampered.input_sha256.starts_with('0') {
"1"
} else {
"0"
};
tampered.input_sha256.replace_range(0..1, replacement);
write_tamper("digest", &tampered);
let mut tampered = certificate.clone();
tampered.phases[0].base_rows[0] ^= 1;
write_tamper("base", &tampered);
let mut tampered = certificate.clone();
tampered.phases[0].powered_rows[0] ^= 1;
write_tamper("power", &tampered);
let mut tampered = certificate.clone();
tampered.phases[0].edges[0].input |= 1u64 << tampered.declared_inputs;
write_tamper("edge", &tampered);
let mut tampered = certificate.clone();
tampered.phases[0].proofs[0].pop();
write_tamper("proof", &tampered);
let mut tampered = certificate.clone();
tampered.terminal_safe_states ^= 1;
write_tamper("terminal", &tampered);
let mut tampered = certificate.clone();
tampered.terminal_proof.pop();
write_tamper("terminal-proof", &tampered);
let mut tampered = certificate.clone();
tampered.inputs[0] |= 1u64 << tampered.declared_inputs;
write_tamper("trace", &tampered);
let canonical = predicate_certificate_v2_body(&certificate);
for (suffix, body) in [
("unknown", canonical.clone() + "unknown=1\n"),
("truncated", canonical.trim_end_matches('\n').to_string()),
("crlf", canonical.replace('\n', "\r\n")),
("number", canonical.replace("horizon=8", "horizon=08")),
(
"reordered",
canonical.replacen(
"semantics=bounded-terminal-bad-avoidance\nproof_format=varisat-native-0.2.2",
"proof_format=varisat-native-0.2.2\nsemantics=bounded-terminal-bad-avoidance",
1,
),
),
] {
let path = stem.with_extension(format!("{suffix}.cert2"));
fs::write(&path, body).unwrap();
assert!(parse_predicate_certificate_v2(&path).is_err());
fs::remove_file(path).unwrap();
}
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let symlink_path = stem.with_extension("symlink.cert2");
symlink(&certificate_path, &symlink_path).unwrap();
assert!(parse_predicate_certificate_v2(&symlink_path).is_err());
fs::remove_file(symlink_path).unwrap();
}
let unsafe_input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/actuator-controller/firmware/dense-actuator-interlock.aag");
let unsafe_transcript = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/predicate-certificate-cost/actuator-h1-unavoidable.transcript");
let unsafe_certificate = stem.with_extension("unsafe.cert2");
certify_aiger_predicate_v2(&unsafe_input, 0, &unsafe_transcript, &unsafe_certificate)
.unwrap();
assert!(
!parse_predicate_certificate_v2(&unsafe_certificate)
.unwrap()
.avoidable
);
verify_aiger_predicate_certificate_v2(&unsafe_input, &unsafe_certificate).unwrap();
fs::remove_file(certificate_path).unwrap();
fs::remove_file(duplicate_path).unwrap();
fs::remove_file(unsafe_certificate).unwrap();
}
#[test]
fn btor2_cli_v1_inspects_word_level_fixture_and_rejects_bad_arguments() {
assert!(run_artifact_cli(&["btor2-cli-version".to_string()]).unwrap());
assert!(run_artifact_cli(&["btor2-search-v3-capabilities".to_string()]).unwrap());
assert!(run_artifact_cli(&["btor2-search-v4-capabilities".to_string()]).unwrap());
assert!(run_artifact_cli(&["btor2-search-v5-capabilities".to_string()]).unwrap());
assert!(
run_artifact_cli(&[
"btor2-search-v3-capabilities".to_string(),
"unexpected".to_string(),
])
.is_err()
);
assert!(
run_artifact_cli(&[
"btor2-search-v4-capabilities".to_string(),
"unexpected".to_string(),
])
.is_err()
);
assert!(
run_artifact_cli(&[
"btor2-search-v5-capabilities".to_string(),
"unexpected".to_string(),
])
.is_err()
);
assert!(
run_artifact_cli(&["btor2-cli-version".to_string(), "unexpected".to_string(),])
.is_err()
);
let fixture =
Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/btor2/watchdog-counter-v1.btor2");
assert!(
run_artifact_cli(&["inspect-btor2".to_string(), fixture.display().to_string(),])
.unwrap()
);
let certificate = std::env::temp_dir().join(format!(
"gcc-btor2-phase-{}-{}.cert",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
assert!(
run_artifact_cli(&[
"certify-btor2-counter-phase".to_string(),
fixture.display().to_string(),
"13".to_string(),
"1:2,0:1000000003".to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
run_artifact_cli(&[
"verify-btor2-counter-phase".to_string(),
fixture.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
run_artifact_cli(&[
"certify-btor2-counter-phase".to_string(),
fixture.display().to_string(),
"13".to_string(),
"0:3".to_string(),
certificate.display().to_string(),
])
.unwrap_err()
.contains("create certificate")
);
fs::remove_file(&certificate).unwrap();
assert!(
run_artifact_cli(&[
"certify-btor2-counter-trace".to_string(),
fixture.display().to_string(),
"13".to_string(),
"0:3".to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
fs::read_to_string(&certificate)
.unwrap()
.starts_with("phase_certificate_version=1\n")
);
assert!(
run_artifact_cli(&[
"verify-btor2-counter-trace".to_string(),
fixture.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
fs::remove_file(&certificate).unwrap();
let saturating = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/btor2/saturating-timer-rejected-v1.btor2");
assert!(
run_artifact_cli(&[
"certify-btor2-counter-trace".to_string(),
saturating.display().to_string(),
"15".to_string(),
"0:255".to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
fs::read_to_string(&certificate)
.unwrap()
.starts_with("replay_certificate_version=1\n")
);
assert!(
run_artifact_cli(&[
"verify-btor2-counter-trace".to_string(),
saturating.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
fs::remove_file(&certificate).unwrap();
let word_source = certificate.with_extension("word-input.btor2");
fs::write(
&word_source,
b"1 sort bitvec 1\n2 sort bitvec 3\n3 input 2 command\n4 state 2 state\n5 zero 2\n6 init 2 4 5\n7 next 2 4 3\n8 constd 2 5\n9 eq 1 4 8\n10 bad 9 reached_five\n",
)
.unwrap();
assert!(
run_artifact_cli(&[
"search-btor2".to_string(),
word_source.display().to_string(),
"10".to_string(),
"1".to_string(),
certificate.display().to_string(),
])
.unwrap()
);
let word_certificate = fs::read(&certificate).unwrap();
assert!(word_certificate.starts_with(b"search_certificate_version=5\n"));
assert!(
word_certificate
.windows(b"input_widths=3\n".len())
.any(|window| window == b"input_widths=3\n")
);
assert!(
run_artifact_cli(&[
"verify-btor2-search".to_string(),
word_source.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
run_artifact_cli(&[
"search-btor2".to_string(),
word_source.display().to_string(),
"10".to_string(),
"1".to_string(),
certificate.display().to_string(),
])
.unwrap_err()
.contains("create certificate")
);
assert_eq!(fs::read(&certificate).unwrap(), word_certificate);
fs::remove_file(&certificate).unwrap();
fs::remove_file(word_source).unwrap();
for (horizon, expected) in [(2, "SAFE"), (3, "UNSAFE")] {
assert!(
run_artifact_cli(&[
"search-btor2".to_string(),
fixture.display().to_string(),
"13".to_string(),
horizon.to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
fs::read_to_string(&certificate)
.unwrap()
.contains(&format!("result={expected}\n"))
);
assert!(
run_artifact_cli(&[
"verify-btor2-search".to_string(),
fixture.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
fs::remove_file(&certificate).unwrap();
assert!(
run_artifact_cli(&[
"check-btor2-bounded".to_string(),
fixture.display().to_string(),
"13".to_string(),
horizon.to_string(),
certificate.display().to_string(),
])
.unwrap()
);
let expected_header = if horizon == 2 {
"region_certificate_version=1\n"
} else {
"search_certificate_version=1\n"
};
assert!(
fs::read_to_string(&certificate)
.unwrap()
.starts_with(expected_header)
);
assert!(
run_artifact_cli(&[
"verify-btor2-bounded".to_string(),
fixture.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
fs::remove_file(&certificate).unwrap();
}
let motion =
Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/btor2/motion-envelope-v1.btor2");
for (horizon, header) in [
(200, "motion_certificate_version=1\n"),
(201, "search_certificate_version=1\n"),
] {
assert!(
run_artifact_cli(&[
"check-btor2-bounded".to_string(),
motion.display().to_string(),
"21".to_string(),
horizon.to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
fs::read_to_string(&certificate)
.unwrap()
.starts_with(header)
);
assert!(
run_artifact_cli(&[
"verify-btor2-bounded".to_string(),
motion.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
fs::remove_file(&certificate).unwrap();
}
let braking = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/btor2/braking-controller-v1.btor2");
for (horizon, header) in [
(255, "braking_certificate_version=1\n"),
(256, "search_certificate_version=1\n"),
] {
assert!(
run_artifact_cli(&[
"check-btor2-bounded".to_string(),
braking.display().to_string(),
"31".to_string(),
horizon.to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
fs::read_to_string(&certificate)
.unwrap()
.starts_with(header)
);
assert!(
run_artifact_cli(&[
"verify-btor2-bounded".to_string(),
braking.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
fs::remove_file(&certificate).unwrap();
}
}
#[test]
fn btor2_component_cli_preserves_sources_and_exact_fallback() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let controller = root.join("examples/btor2/components/braking-controller-v1.btor2");
let plant = root.join("examples/btor2/components/motion-plant-v1.btor2");
let contract = root.join("examples/btor2/components/braking-motion-contract-v1.txt");
let certificate =
std::env::temp_dir().join(format!("gcc-btor2-components-{}.cert", std::process::id()));
let obligation = std::env::temp_dir().join(format!(
"gcc-btor2-controller-{}.obligation",
std::process::id()
));
assert!(
run_artifact_cli(&[
"certify-btor2-controller-obligation".to_string(),
controller.display().to_string(),
contract.display().to_string(),
obligation.display().to_string(),
])
.unwrap()
);
assert!(
fs::read_to_string(&obligation)
.unwrap()
.starts_with("controller_obligation_version=1\n")
);
assert!(
run_artifact_cli(&[
"verify-btor2-controller-obligation".to_string(),
controller.display().to_string(),
obligation.display().to_string(),
])
.unwrap()
);
fs::remove_file(&obligation).unwrap();
for (horizon, backend) in [
(255, "backend=phase-contract"),
(256, "backend=composed-search"),
] {
assert!(
run_artifact_cli(&[
"check-btor2-components".to_string(),
controller.display().to_string(),
plant.display().to_string(),
contract.display().to_string(),
horizon.to_string(),
certificate.display().to_string(),
])
.unwrap()
);
let text = fs::read_to_string(&certificate).unwrap();
assert!(text.starts_with("component_certificate_version=1\n"));
assert!(text.contains(backend));
assert!(
run_artifact_cli(&[
"verify-btor2-components".to_string(),
controller.display().to_string(),
plant.display().to_string(),
contract.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
fs::remove_file(&certificate).unwrap();
}
}
#[test]
fn revision_portfolio_cli_routes_verifies_and_rejects_query_drift() {
let scratch =
std::env::temp_dir().join(format!("gcc-revision-portfolio-cli-{}", std::process::id()));
fs::create_dir_all(&scratch).unwrap();
let left = scratch.join("left.btor2");
let right = scratch.join("right.btor2");
let interface = scratch.join("interface.txt");
let certificate = scratch.join("answer.revision-proof");
fs::write(
&left,
b"1 sort bitvec 1\n2 sort bitvec 2\n3 sort bitvec 9\n4 input 2 command\n5 state 3 wide_state\n6 zero 3\n7 init 3 5 6\n8 uext 3 4 7\n9 next 3 5 8\n10 zero 1\n11 bad 10 never\n",
)
.unwrap();
fs::write(
&right,
b"1 sort bitvec 1\n2 sort bitvec 2\n3 input 2 sensed\n4 state 2 state\n5 zero 2\n6 init 2 4 5\n7 add 2 4 3\n8 next 2 4 7\n9 constd 2 2\n10 eq 1 4 9\n11 bad 10 reached_two\n",
)
.unwrap();
fs::write(
&interface,
b"word_interface_version=1\nwire_count=1\nwire=left,4,3\nstatus=complete\n",
)
.unwrap();
let base = vec![
left.display().to_string(),
"4".to_string(),
right.display().to_string(),
"7,10".to_string(),
interface.display().to_string(),
"1".to_string(),
"right".to_string(),
"10".to_string(),
certificate.display().to_string(),
];
let mut check = vec!["check-btor2-revision-portfolio".to_string()];
check.extend(base.clone());
assert!(run_artifact_cli(&check).unwrap());
assert!(fs::read(&certificate).unwrap().starts_with(b"GCCRPF01"));
assert!(run_artifact_cli(&check).is_err());
let mut verify = vec!["verify-btor2-revision-portfolio".to_string()];
verify.extend(base.clone());
assert!(run_artifact_cli(&verify).unwrap());
verify[6] = "0".to_string();
assert_eq!(
run_artifact_cli(&verify).unwrap_err(),
"revision proof query does not match CLI query"
);
fs::remove_dir_all(&scratch).unwrap();
}
#[test]
fn revision_portfolio_cli_accepts_strict_property_free_components() {
let scratch = std::env::temp_dir().join(format!(
"gcc-revision-property-free-cli-{}",
std::process::id()
));
fs::create_dir_all(&scratch).unwrap();
let left = scratch.join("left.btor2");
let right = scratch.join("right.btor2");
let interface = scratch.join("interface.txt");
let certificate = scratch.join("answer.revision-proof");
fs::write(
&left,
b"1 sort bitvec 1\n2 sort bitvec 2\n3 input 2 command\n4 state 2 state\n5 zero 2\n6 init 2 4 5\n7 add 2 4 3\n8 next 2 4 7\n9 redor 1 7\n10 output 7 projected\n11 output 9 property\n",
)
.unwrap();
fs::write(
&right,
b"1 sort bitvec 1\n2 sort bitvec 2\n3 input 2 sensed\n4 state 2 state\n5 zero 2\n6 init 2 4 5\n7 add 2 4 3\n8 next 2 4 7\n9 output 7 projected\n",
)
.unwrap();
fs::write(
&interface,
b"word_interface_version=2\nwire_count=1\nwire=left,7,3\nexternal_count=1\nexternal=left,3\nstatus=complete\n",
)
.unwrap();
let arguments = vec![
left.display().to_string(),
"7,9".to_string(),
right.display().to_string(),
"7".to_string(),
interface.display().to_string(),
"1".to_string(),
"left".to_string(),
"9".to_string(),
certificate.display().to_string(),
];
let mut check = vec!["check-btor2-revision-portfolio".to_string()];
check.extend(arguments.clone());
assert!(run_artifact_cli(&check).unwrap());
let mut verify = vec!["verify-btor2-revision-portfolio".to_string()];
verify.extend(arguments);
assert!(run_artifact_cli(&verify).unwrap());
fs::remove_dir_all(&scratch).unwrap();
}
#[test]
fn revision_retained_cli_produces_only_changed_side_and_remains_ordinarily_verifiable() {
let scratch =
std::env::temp_dir().join(format!("gcc-revision-retained-cli-{}", std::process::id()));
fs::create_dir_all(&scratch).unwrap();
let left = scratch.join("left.btor2");
let right_v1 = scratch.join("right-v1.btor2");
let right_v2 = scratch.join("right-v2.btor2");
let interface = scratch.join("interface.txt");
let previous = scratch.join("previous.revision-proof");
let next = scratch.join("next.revision-proof");
fs::write(
&left,
b"1 sort bitvec 1\n2 sort bitvec 2\n3 input 2 command\n4 state 2 state\n5 zero 2\n6 init 2 4 5\n7 add 2 4 3\n8 next 2 4 7\n9 zero 1\n10 bad 9 never\n",
)
.unwrap();
fs::write(
&right_v1,
b"1 sort bitvec 1\n2 sort bitvec 2\n3 input 2 sensed\n4 state 2 state\n5 zero 2\n6 init 2 4 5\n7 add 2 4 3\n8 next 2 4 7\n9 constd 2 2\n10 eq 1 4 9\n11 bad 10 reached_two\n",
)
.unwrap();
fs::write(
&right_v2,
b"1 sort bitvec 1\n2 sort bitvec 2\n3 input 2 sensed\n4 state 2 state\n5 zero 2\n6 init 2 4 5\n7 xor 2 4 3\n8 next 2 4 7\n9 constd 2 2\n10 eq 1 4 9\n11 bad 10 reached_two\n",
)
.unwrap();
fs::write(
&interface,
b"word_interface_version=1\nwire_count=1\nwire=left,7,3\nstatus=complete\n",
)
.unwrap();
assert!(
run_artifact_cli(&[
"check-btor2-revision-portfolio".to_string(),
left.display().to_string(),
"7".to_string(),
right_v1.display().to_string(),
"7,10".to_string(),
interface.display().to_string(),
"1".to_string(),
"right".to_string(),
"10".to_string(),
previous.display().to_string(),
])
.unwrap()
);
assert!(
run_artifact_cli(&[
"check-btor2-revision-retained-left".to_string(),
left.display().to_string(),
previous.display().to_string(),
right_v2.display().to_string(),
"7,10".to_string(),
interface.display().to_string(),
"1".to_string(),
"right".to_string(),
"10".to_string(),
next.display().to_string(),
])
.unwrap()
);
assert!(
run_artifact_cli(&[
"verify-btor2-revision-portfolio".to_string(),
left.display().to_string(),
"7".to_string(),
right_v2.display().to_string(),
"7,10".to_string(),
interface.display().to_string(),
"1".to_string(),
"right".to_string(),
"10".to_string(),
next.display().to_string(),
])
.unwrap()
);
assert!(
run_artifact_cli(&[
"verify-btor2-revision-retained-left".to_string(),
left.display().to_string(),
previous.display().to_string(),
right_v2.display().to_string(),
interface.display().to_string(),
"1".to_string(),
"right".to_string(),
"10".to_string(),
next.display().to_string(),
])
.unwrap()
);
assert!(
run_artifact_cli(&[
"check-btor2-revision-retained-left".to_string(),
right_v1.display().to_string(),
previous.display().to_string(),
right_v2.display().to_string(),
"7,10".to_string(),
interface.display().to_string(),
"1".to_string(),
"right".to_string(),
"10".to_string(),
scratch.join("drift.revision-proof").display().to_string(),
])
.is_err()
);
assert!(
run_artifact_cli(&[
"check-btor2-revision-retained-left".to_string(),
left.display().to_string(),
previous.display().to_string(),
right_v2.display().to_string(),
"7,10".to_string(),
interface.display().to_string(),
"1".to_string(),
"right".to_string(),
"10".to_string(),
next.display().to_string(),
])
.is_err()
);
fs::remove_dir_all(&scratch).unwrap();
}
#[test]
fn btor2_component_batch_cli_is_manifest_bound_and_self_verifying() {
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/btor2/components");
let scratch =
std::env::temp_dir().join(format!("gcc-btor2-component-batch-{}", std::process::id()));
if scratch.exists() {
fs::remove_dir_all(&scratch).unwrap();
}
fs::create_dir(&scratch).unwrap();
for name in [
"braking-controller-v1.btor2",
"motion-plant-v1.btor2",
"semi-implicit-motion-plant-v1.btor2",
"braking-motion-contract-v1.txt",
] {
fs::copy(source.join(name), scratch.join(name)).unwrap();
}
let manifest = scratch.join("batch.txt");
fs::write(
&manifest,
"component_batch_manifest_version=1\nmember_count=3\nplant_path=motion-plant-v1.btor2\ncontract_path=braking-motion-contract-v1.txt\nhorizon=255\nplant_path=motion-plant-v1.btor2\ncontract_path=braking-motion-contract-v1.txt\nhorizon=256\nplant_path=semi-implicit-motion-plant-v1.btor2\ncontract_path=braking-motion-contract-v1.txt\nhorizon=127\nstatus=complete\n",
)
.unwrap();
let certificate = scratch.join("batch.component-batch");
assert!(
run_artifact_cli(&[
"check-btor2-component-batch".to_string(),
scratch
.join("braking-controller-v1.btor2")
.display()
.to_string(),
manifest.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
let encoded = fs::read_to_string(&certificate).unwrap();
assert!(encoded.starts_with("component_batch_portfolio_version=1\n"));
assert!(matches!(
btor2_component::decode_component_batch_portfolio(encoded.as_bytes()).unwrap(),
btor2_component::ComponentBatchPortfolioCertificate::Ordinary(_)
));
assert!(
run_artifact_cli(&[
"verify-btor2-component-batch".to_string(),
scratch
.join("braking-controller-v1.btor2")
.display()
.to_string(),
manifest.display().to_string(),
certificate.display().to_string(),
])
.unwrap()
);
fs::write(
&manifest,
"component_batch_manifest_version=1\nmember_count=1\nplant_path=../motion-plant-v1.btor2\ncontract_path=braking-motion-contract-v1.txt\nhorizon=255\nstatus=complete\n",
)
.unwrap();
assert!(parse_component_batch_manifest(&manifest).is_err());
fs::remove_dir_all(&scratch).unwrap();
}
#[test]
fn predicate_cli_v1_contract_is_machine_readable_and_strict() {
assert_eq!(
predicate_cli_contract_line(),
"predicate_cli_version=1 certificate_versions=1,2 portfolio_certificate_version=1 proof_format=varisat-native-0.2.2 min_relevant_inputs=9 max_relevant_inputs=16 max_latches=4 max_horizon=64 max_certificate_v2_bytes=16777216 max_proof_bytes=1048576 max_total_proof_bytes=8388608"
);
assert!(run_artifact_cli(&["predicate-cli-version".to_string()]).unwrap());
assert!(
run_artifact_cli(&[
"predicate-cli-version".to_string(),
"unexpected".to_string(),
])
.unwrap_err()
.contains("usage:")
);
}
#[test]
fn predicate_v2_obligation_export_is_deterministic_complete_and_unsat() {
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let transcript = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/predicate-certificate-cost/interrupt-h8-avoidable.transcript");
let scratch = std::env::temp_dir().join(format!(
"cq-sat-predicate-obligation-export-{}",
std::process::id()
));
fs::create_dir(&scratch).unwrap();
let certificate = scratch.join("input.cert2");
certify_aiger_predicate_v2(&input, 0, &transcript, &certificate).unwrap();
let first = scratch.join("first");
let second = scratch.join("second");
export_aiger_predicate_v2_obligations(&input, &certificate, &first).unwrap();
export_aiger_predicate_v2_obligations(&input, &certificate, &second).unwrap();
let first_manifest = fs::read(first.join("manifest.txt")).unwrap();
assert_eq!(
first_manifest,
fs::read(second.join("manifest.txt")).unwrap()
);
let manifest = String::from_utf8(first_manifest).unwrap();
let count = manifest
.lines()
.find_map(|line| line.strip_prefix("obligation_count="))
.unwrap()
.parse::<usize>()
.unwrap();
let certificate_body = parse_predicate_certificate_v2(&certificate).unwrap();
assert_eq!(
count,
certificate_body
.phases
.iter()
.map(|phase| phase.base_rows.len())
.sum::<usize>()
+ 1
);
for line in manifest.lines().filter(|line| {
line.starts_with("obligation_") && !line.starts_with("obligation_count=")
}) {
let filename = line.split_once('=').unwrap().1.split(',').next().unwrap();
assert_eq!(
fs::read(first.join(filename)).unwrap(),
fs::read(second.join(filename)).unwrap()
);
let (variables, clauses) = parse_dimacs(&first.join(filename)).unwrap();
assert!(solve_with_varisat(variables, &clauses).is_none());
}
assert_eq!(
fs::read(first.join("aggregate.cnf")).unwrap(),
fs::read(second.join("aggregate.cnf")).unwrap()
);
let (variables, clauses) = parse_dimacs(&first.join("aggregate.cnf")).unwrap();
assert!(solve_with_varisat(variables, &clauses).is_none());
assert!(
export_aiger_predicate_v2_obligations(&input, &certificate, &first)
.unwrap_err()
.contains("refuses to overwrite")
);
fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn selector_aggregate_is_unsat_exactly_when_every_obligation_is_unsat() {
let contradiction = vec![Clause(vec![(0, true)]), Clause(vec![(0, false)])];
let satisfiable = vec![Clause(vec![(0, true)])];
let (variables, all_unsat) =
aggregate_predicate_obligations(1, &[contradiction.clone(), contradiction.clone()])
.unwrap();
assert!(solve_with_varisat(variables, &all_unsat).is_none());
let (variables, one_sat) =
aggregate_predicate_obligations(1, &[contradiction, satisfiable]).unwrap();
assert!(solve_with_varisat(variables, &one_sat).is_some());
assert!(aggregate_predicate_obligations(1, &[]).is_err());
assert!(aggregate_predicate_obligations(0, &[Vec::new()]).is_err());
}
#[test]
fn predicate_certificate_v2_corrupt_inputs_are_bounded_and_process_safe() {
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let transcript = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/predicate-certificate-cost/interrupt-h8-avoidable.transcript");
let scratch = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-v2-reliability-{}",
std::process::id()
));
fs::create_dir(&scratch).unwrap();
let valid = scratch.join("valid.cert2");
certify_aiger_predicate_v2(&input, 0, &transcript, &valid).unwrap();
let seed = fs::read(&valid).unwrap();
let mutated = scratch.join("mutated.cert2");
for iteration in 0..5_000 {
fs::write(
&mutated,
deterministic_input_mutation(&seed, iteration ^ 0xc2c2),
)
.unwrap();
let _ = parse_predicate_certificate_v2(&mutated);
}
let invalid_utf8 = scratch.join("invalid-utf8.cert2");
fs::write(&invalid_utf8, [0xff, b'\n']).unwrap();
assert!(parse_predicate_certificate_v2(&invalid_utf8).is_err());
let oversized = scratch.join("oversized.cert2");
fs::File::create(&oversized)
.unwrap()
.set_len(PREDICATE_CERTIFICATE_V2_MAX_BYTES + 1)
.unwrap();
assert!(
parse_predicate_certificate_v2(&oversized)
.unwrap_err()
.contains("no larger")
);
let body = String::from_utf8(seed).unwrap();
let excessive_proof = scratch.join("excessive-proof.cert2");
let excessive_hex = "00".repeat(PREDICATE_CERTIFICATE_V2_MAX_PROOF_BYTES + 1);
let replaced = body
.lines()
.map(|line| {
if line.starts_with("phase_0_proof_0=") {
format!("phase_0_proof_0={excessive_hex}")
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
+ "\n";
fs::write(&excessive_proof, replaced).unwrap();
assert!(
parse_predicate_certificate_v2(&excessive_proof)
.unwrap_err()
.contains("bounded canonical byte hex")
);
let certificate = parse_predicate_certificate_v2(&valid).unwrap();
let mut rejected_proof_mutations = 0usize;
let mut contained_dependency_failures = 0usize;
for iteration in 0..128 {
let mut corrupted = certificate.clone();
let proof =
&mut corrupted.phases[0].proofs[iteration % certificate.phases[0].proofs.len()];
let byte = (iteration * 131 + 17) % proof.len();
proof[byte] ^= 1 << (iteration % 8);
let corrupted_path = scratch.join(format!("corrupt-proof-{iteration}.cert2"));
fs::write(&corrupted_path, predicate_certificate_v2_body(&corrupted)).unwrap();
if let Err(error) = verify_aiger_predicate_certificate_v2(&input, &corrupted_path) {
rejected_proof_mutations += 1;
if error.contains("malformed proof was rejected") {
contained_dependency_failures += 1;
}
}
}
assert!(rejected_proof_mutations > 0);
assert_eq!(contained_dependency_failures, 0);
fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn predicate_certificate_v2_covers_product_and_multiphase_contracts() {
let fixtures = [
(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/predicate-certificate-cost/interrupt-h8-avoidable.transcript",
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/predicate-certificate-cost/actuator-h16-avoidable.transcript",
),
(
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
"examples/predicate-certificate-cost/sensor-h32-avoidable.transcript",
),
];
for (index, (model, transcript)) in fixtures.into_iter().enumerate() {
let input = Path::new(env!("CARGO_MANIFEST_DIR")).join(model);
let transcript = Path::new(env!("CARGO_MANIFEST_DIR")).join(transcript);
let certificate = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-v2-cohort-{}-{index}.cert2",
std::process::id()
));
certify_aiger_predicate_v2(&input, 0, &transcript, &certificate).unwrap();
verify_aiger_predicate_certificate_v2(&input, &certificate).unwrap();
fs::remove_file(certificate).unwrap();
}
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let stem = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-v2-multiphase-{}",
std::process::id()
));
let transcript = stem.with_extension("transcript");
let certificate = stem.with_extension("cert2");
let frames = [
"xxxxxxxxx",
"xxxxxxxxx",
"111111111",
"111111111",
"xxxxxxxxx",
"xxxxxxxxx",
"xxxxxxxxx",
];
fs::write(&transcript, frames.join("\n") + "\n").unwrap();
certify_aiger_predicate_v2(&input, 0, &transcript, &certificate).unwrap();
let parsed = parse_predicate_certificate_v2(&certificate).unwrap();
assert_eq!(parsed.phases.len(), 3);
assert_eq!(
parsed
.phases
.iter()
.map(|phase| phase.length)
.sum::<usize>(),
6
);
verify_aiger_predicate_certificate_v2(&input, &certificate).unwrap();
fs::remove_file(transcript).unwrap();
fs::remove_file(certificate).unwrap();
}
#[test]
fn counterfactual_portfolio_admits_certificates_and_falls_back_exactly() {
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/mobile-robot/firmware/dense-sensor-fusion.aag");
let stem = std::env::temp_dir().join(format!(
"cq-sat-counterfactual-portfolio-{}",
std::process::id()
));
let admitted_transcript = stem.with_extension("admitted.transcript");
let admitted_report = stem.with_extension("admitted.report");
let admitted_certificate = stem.with_extension("admitted.cert");
let mut admitted_lines = vec!["1".repeat(16); 33];
admitted_lines[32].replace_range(15..16, "0");
fs::write(&admitted_transcript, admitted_lines.join("\n") + "\n").unwrap();
verify_aiger_counterfactual_portfolio(
&input,
0,
&admitted_transcript,
100,
&admitted_report,
&admitted_certificate,
)
.unwrap();
let report = fs::read_to_string(&admitted_report).unwrap();
assert!(report.contains("admitted=1\n"));
assert!(report.contains("backend=predicate-certificate\n"));
assert!(report.contains("result=avoidable\n"));
assert!(report.contains("certificate_verified=1\n"));
verify_aiger_predicate_certificate(&input, &admitted_certificate).unwrap();
verify_aiger_counterfactual_report(
&input,
&admitted_transcript,
&admitted_report,
&admitted_certificate,
)
.unwrap();
assert!(
verify_aiger_counterfactual_portfolio(
&input,
0,
&admitted_transcript,
100,
&admitted_report,
&admitted_certificate,
)
.unwrap_err()
.contains("overwrite")
);
let fallback_transcript = stem.with_extension("fallback.transcript");
let fallback_report = stem.with_extension("fallback.report");
let fallback_certificate = stem.with_extension("fallback.cert");
let mut fallback_lines = vec!["1".repeat(16); 9];
fallback_lines[8].replace_range(15..16, "0");
fs::write(&fallback_transcript, fallback_lines.join("\n") + "\n").unwrap();
verify_aiger_counterfactual_portfolio(
&input,
0,
&fallback_transcript,
100,
&fallback_report,
&fallback_certificate,
)
.unwrap();
let report = fs::read_to_string(&fallback_report).unwrap();
assert!(report.contains("admitted=0\n"));
assert!(report.contains("backend=persistent-cdcl\n"));
assert!(report.contains("reason=static-rejection\n"));
assert!(report.contains("result=avoidable\n"));
assert!(report.contains("certificate_verified=0\n"));
assert!(!fallback_certificate.exists());
verify_aiger_counterfactual_report(
&input,
&fallback_transcript,
&fallback_report,
&fallback_certificate,
)
.unwrap();
let forced_report = stem.with_extension("forced.report");
let forced_certificate = stem.with_extension("forced.cert");
verify_aiger_counterfactual_portfolio_with_node_limit(
&input,
0,
&admitted_transcript,
100,
&forced_report,
&forced_certificate,
1,
)
.unwrap();
let report = fs::read_to_string(&forced_report).unwrap();
assert!(report.contains("admitted=1\n"));
assert!(report.contains("backend=persistent-cdcl\n"));
assert!(report.contains("reason=predicate-resource-fallback\n"));
assert!(report.contains("result=avoidable\n"));
assert!(!forced_certificate.exists());
verify_aiger_counterfactual_report(
&input,
&admitted_transcript,
&forced_report,
&forced_certificate,
)
.unwrap();
let tampered_report = stem.with_extension("tampered.report");
fs::write(
&tampered_report,
fs::read_to_string(&admitted_report)
.unwrap()
.replace("result=avoidable", "result=unavoidable"),
)
.unwrap();
assert!(
verify_aiger_counterfactual_report(
&input,
&admitted_transcript,
&tampered_report,
&admitted_certificate,
)
.is_err()
);
for (suffix, body) in [
(
"unknown",
fs::read_to_string(&admitted_report).unwrap() + "unknown=1\n",
),
(
"crlf",
fs::read_to_string(&admitted_report)
.unwrap()
.replace('\n', "\r\n"),
),
(
"number",
fs::read_to_string(&admitted_report)
.unwrap()
.replace("horizon=32", "horizon=032"),
),
] {
let malformed = stem.with_extension(format!("{suffix}.report"));
fs::write(&malformed, body).unwrap();
assert!(parse_counterfactual_report(&malformed).is_err());
fs::remove_file(malformed).unwrap();
}
for path in [
admitted_transcript,
admitted_report,
admitted_certificate,
fallback_transcript,
fallback_report,
forced_report,
tampered_report,
] {
fs::remove_file(path).unwrap();
}
let unsafe_input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/actuator-controller/firmware/dense-actuator-interlock.aag");
let model = parse_aag(&unsafe_input).unwrap();
let horizon = 4;
let encoding = aag_bmc_encoding(&model, horizon).unwrap();
let query = encoding
.queries
.iter()
.find(|query| query.frame == horizon && query.output == 0)
.unwrap();
let mut solver = Solver::new();
add_to_varisat(&mut solver, &encoding.clauses);
let trace = causal_query_witness(&mut solver, encoding.variables, query.assumption)
.unwrap()
.unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let unsafe_transcript = stem.with_extension("unsafe.transcript");
let unsafe_report = stem.with_extension("unsafe.report");
let unsafe_certificate = stem.with_extension("unsafe.cert");
let transcript_body = (0..=horizon)
.map(|frame| {
relevant
.iter()
.map(|input| {
let variable = frame * model.max_variable + model.inputs[*input] / 2 - 1;
if trace[variable] { '1' } else { '0' }
})
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
+ "\n";
fs::write(&unsafe_transcript, transcript_body).unwrap();
verify_aiger_counterfactual_portfolio(
&unsafe_input,
0,
&unsafe_transcript,
100,
&unsafe_report,
&unsafe_certificate,
)
.unwrap();
let report = fs::read_to_string(&unsafe_report).unwrap();
assert!(report.contains("backend=persistent-cdcl\n"));
assert!(report.contains("result=unavoidable\n"));
assert!(!unsafe_certificate.exists());
verify_aiger_counterfactual_report(
&unsafe_input,
&unsafe_transcript,
&unsafe_report,
&unsafe_certificate,
)
.unwrap();
fs::remove_file(unsafe_transcript).unwrap();
fs::remove_file(unsafe_report).unwrap();
}
#[test]
fn counterfactual_portfolio_certificate_contract_covers_product_cohort() {
let fixtures = [
(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
9,
8,
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
12,
16,
),
(
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
16,
32,
),
];
for (index, (relative, width, horizon)) in fixtures.into_iter().enumerate() {
let input = Path::new(env!("CARGO_MANIFEST_DIR")).join(relative);
let stem = std::env::temp_dir().join(format!(
"cq-sat-counterfactual-compatibility-{}-{index}",
std::process::id()
));
let transcript = stem.with_extension("transcript");
let report = stem.with_extension("report");
let certificate = stem.with_extension("cert");
fs::write(
&transcript,
vec!["x".repeat(width); horizon + 1].join("\n") + "\n",
)
.unwrap();
verify_aiger_counterfactual_portfolio(
&input,
0,
&transcript,
100,
&report,
&certificate,
)
.unwrap();
assert!(
fs::read_to_string(&report)
.unwrap()
.contains("backend=predicate-certificate\n")
);
verify_aiger_counterfactual_report(&input, &transcript, &report, &certificate).unwrap();
fs::remove_file(transcript).unwrap();
fs::remove_file(report).unwrap();
fs::remove_file(certificate).unwrap();
}
}
#[test]
fn predicate_certificate_cost_benchmark_preserves_every_agreeing_trial() {
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let stem = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-cost-test-{}",
std::process::id()
));
let transcript = stem.with_extension("transcript");
let output = stem.with_extension("csv");
fs::write(&transcript, vec!["x".repeat(9); 9].join("\n") + "\n").unwrap();
benchmark_aiger_predicate_certificate_cost(&input, 0, &transcript, 2, &output).unwrap();
let body = fs::read_to_string(&output).unwrap();
let lines = body.lines().collect::<Vec<_>>();
assert_eq!(lines.len(), 3);
assert!(lines[0].starts_with("schema_version,input_sha256,transcript_sha256,"));
for (trial, row) in lines[1..].iter().enumerate() {
let fields = row.split(',').collect::<Vec<_>>();
assert_eq!(fields.len(), 16);
assert_eq!(fields[0], "1");
assert_eq!(fields[7], trial.to_string());
assert_eq!(&fields[14..], &["true", "ok"]);
}
assert!(
benchmark_aiger_predicate_certificate_cost(&input, 0, &transcript, 1, &output,)
.unwrap_err()
.contains("overwrite")
);
fs::remove_file(transcript).unwrap();
fs::remove_file(output).unwrap();
}
#[test]
fn predicate_certificate_v2_cost_benchmark_preserves_every_agreeing_trial() {
let input = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let stem = std::env::temp_dir().join(format!(
"cq-sat-predicate-certificate-v2-cost-test-{}",
std::process::id()
));
let transcript = stem.with_extension("transcript");
let output = stem.with_extension("csv");
fs::write(&transcript, vec!["x".repeat(9); 9].join("\n") + "\n").unwrap();
benchmark_aiger_predicate_certificate_v2_cost(&input, 0, &transcript, 2, &output).unwrap();
let body = fs::read_to_string(&output).unwrap();
let lines = body.lines().collect::<Vec<_>>();
assert_eq!(lines.len(), 3);
assert!(lines[0].starts_with("schema_version,input_sha256,transcript_sha256,"));
for (trial, row) in lines[1..].iter().enumerate() {
let fields = row.split(',').collect::<Vec<_>>();
assert_eq!(fields.len(), 18);
assert_eq!(fields[0], "1");
assert_eq!(fields[7], trial.to_string());
assert_eq!(&fields[16..], &["true", "ok"]);
}
assert!(
benchmark_aiger_predicate_certificate_v2_cost(&input, 0, &transcript, 1, &output)
.unwrap_err()
.contains("overwrite")
);
fs::remove_file(transcript).unwrap();
fs::remove_file(output).unwrap();
}
#[test]
fn interface_projection_handles_bit_sixty_three_and_rejects_sixty_five_inputs() {
let mut boundary = String::from("aag 66 64 1 1 1\n");
for variable in 1..=64 {
boundary.push_str(&format!("{}\n", variable * 2));
}
boundary.push_str("130 2 0\n132\n132 128 2\nc\n64-input boundary\n");
let model = parse_aiger_bytes(boundary.as_bytes()).unwrap();
let table = AagInterfaceTable::compile(&model).unwrap();
assert_eq!(table.declared_input_count, 64);
assert_eq!(table.projected_inputs, vec![0, 63]);
assert_eq!(table.project_input((1u64 << 63) | 1), 0b11);
assert_eq!(table.lift_input(0b10), 1u64 << 63);
let mut excessive = String::from("aag 66 65 1 1 0\n");
for variable in 1..=65 {
excessive.push_str(&format!("{}\n", variable * 2));
}
excessive.push_str("132 2 0\n2\nc\n65-input rejection\n");
let model = parse_aiger_bytes(excessive.as_bytes()).unwrap();
assert!(
AagInterfaceTable::compile(&model)
.unwrap_err()
.contains("1..=64 declared inputs")
);
}
#[test]
fn interface_relation_composition_is_exact() {
let mut left = InterfaceRelation::empty(4);
left.insert(0, 1);
left.insert(0, 2);
left.insert(3, 0);
let mut right = InterfaceRelation::empty(4);
right.insert(1, 3);
right.insert(2, 0);
right.insert(0, 2);
let composed = InterfaceRelation::compose(&left, &right).unwrap();
assert_eq!(composed.targets(0).collect::<Vec<_>>(), vec![0, 3]);
assert_eq!(composed.targets(3).collect::<Vec<_>>(), vec![2]);
assert_eq!(composed.pairs(), 3);
}
#[test]
fn binary_aiger_rejects_malformed_delta_streams() {
let mut truncated = b"aig 3 1 1 1 1\n2 0\n6\n".to_vec();
truncated.push(0x80);
assert!(
parse_aiger_bytes(&truncated)
.unwrap_err()
.contains("truncated binary AIGER delta")
);
let mut underflow = b"aig 3 1 1 1 1\n2 0\n6\n".to_vec();
underflow.extend([7, 0]);
assert!(
parse_aiger_bytes(&underflow)
.unwrap_err()
.contains("invalid binary AIGER first delta")
);
let mut overflow = b"aig 3 1 1 1 1\n2 0\n6\n".to_vec();
overflow.extend(std::iter::repeat_n(0xff, (usize::BITS as usize / 7) + 1));
assert!(
parse_aiger_bytes(&overflow)
.unwrap_err()
.contains("binary AIGER delta overflow")
);
}
#[test]
fn binary_aiger_fixture_is_accepted_by_yosys_when_available() {
if Command::new("yosys").arg("-V").output().is_err() {
eprintln!("skipping binary AIGER interoperability test because Yosys is unavailable");
return;
}
let path = std::env::temp_dir().join(format!(
"cq-sat-binary-aiger-yosys-{}.aig",
std::process::id()
));
fs::write(&path, binary_aiger_fixture()).unwrap();
let script = format!("read_aiger {}; hierarchy -check", path.display());
let output = Command::new("yosys")
.args(["-Q", "-q", "-p", &script])
.output()
.unwrap();
fs::remove_file(path).unwrap();
assert!(
output.status.success(),
"Yosys rejected binary fixture: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn external_aiger_counter_reports_exact_unsafe_trace() {
let input =
Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/aiger/counter-overflow-4.aag");
let output = std::env::temp_dir().join(format!(
"cq-sat-aiger-counter-{}-portfolio.csv",
std::process::id()
));
let safety = std::env::temp_dir().join(format!(
"cq-sat-aiger-counter-{}-safety.txt",
std::process::id()
));
verify_cq_aiger(&input, 20, 10, 200_000, &output, &safety).unwrap();
let portfolio = fs::read_to_string(&output).unwrap();
let result = fs::read_to_string(&safety).unwrap();
std::fs::remove_file(output).unwrap();
std::fs::remove_file(safety).unwrap();
assert!(portfolio.contains(",cdcl,cdcl-fallback,"));
assert!(portfolio.contains(",true,true,ok\n"));
assert!(result.starts_with("status=UNSAFE\n"));
assert!(result.contains("bad_frame=15\n"));
assert!(result.contains("15,1111\n"));
}
#[test]
fn constant_false_aiger_property_reports_safe_without_solving() {
let stem = format!("cq-sat-aiger-safe-{}", std::process::id());
let input = std::env::temp_dir().join(format!("{stem}.aag"));
let output = std::env::temp_dir().join(format!("{stem}.csv"));
let safety = std::env::temp_dir().join(format!("{stem}.txt"));
fs::write(&input, "aag 1 0 1 1 0\n2 2\n0\n").unwrap();
verify_cq_aiger(&input, 20, 10, 200_000, &output, &safety).unwrap();
let portfolio = fs::read_to_string(&output).unwrap();
let result = fs::read_to_string(&safety).unwrap();
std::fs::remove_file(input).unwrap();
std::fs::remove_file(output).unwrap();
std::fs::remove_file(safety).unwrap();
assert!(portfolio.contains(",static,constant-false-output,"));
assert!(portfolio.lines().all(|line| line.split(',').count() == 26));
assert!(result.starts_with("status=SAFE\n"));
assert!(result.contains("backend=static\n"));
}
#[test]
fn primary_input_aiger_uses_exact_cdcl_and_emits_input_trace() {
let stem = format!("cq-sat-aiger-input-{}", std::process::id());
let input = std::env::temp_dir().join(format!("{stem}.aag"));
let output = std::env::temp_dir().join(format!("{stem}.csv"));
let safety = std::env::temp_dir().join(format!("{stem}.txt"));
fs::write(&input, "aag 2 1 1 1 0\n2\n4 2\n4\n").unwrap();
verify_cq_aiger(&input, 2, 1, 200_000, &output, &safety).unwrap();
let portfolio = fs::read_to_string(&output).unwrap();
let result = fs::read_to_string(&safety).unwrap();
std::fs::remove_file(input).unwrap();
std::fs::remove_file(output).unwrap();
std::fs::remove_file(safety).unwrap();
assert!(portfolio.contains(",cdcl,aiger-primary-inputs,"));
assert!(portfolio.lines().all(|line| line.split(',').count() == 26));
assert!(result.starts_with("status=UNSAFE\n"));
assert!(result.contains("bad_frame=1\n"));
assert!(result.contains("frame,latch_bits_low_to_high,input_bits_low_to_high\n"));
assert!(result.contains("0,0,1\n"));
assert!(result.contains("1,1,"));
}
#[test]
fn causal_minimizer_returns_a_verified_one_minimal_cause() {
let stem = format!("cq-sat-causal-{}", std::process::id());
let input = std::env::temp_dir().join(format!("{stem}.aag"));
let bundle = std::env::temp_dir().join(format!("{stem}.bundle"));
let certificate = bundle.join("causal-certificate.txt");
let metrics = bundle.join("causal-metrics.csv");
fs::write(
&input,
"aag 4 2 1 1 1\n2\n4\n6 6 0\n8\n8 2 4\ni0 left_request\ni1 right_request\no0 simultaneous_request\n",
)
.unwrap();
explain_aiger_counterexample(&input, 1, 16, &bundle).unwrap();
verify_causal_bundle(&input, &bundle).unwrap();
verify_causal_certificate(&input, &certificate).unwrap();
let report = fs::read_to_string(&certificate).unwrap();
let measurements = fs::read_to_string(&metrics).unwrap();
assert!(report.contains("bad_output_name=simultaneous_request\n"));
assert!(report.contains("candidate_count=2\n"));
assert!(report.contains("cause_count=2\n"));
assert!(report.contains("minimality=1-minimal\n"));
assert!(measurements.contains(",true,true,ok\n"));
let wrong_name = report.replace(
"bad_output_name=simultaneous_request",
"bad_output_name=unrelated_output",
);
fs::write(&certificate, wrong_name).unwrap();
assert!(verify_causal_certificate(&input, &certificate).is_err());
fs::write(&certificate, &report).unwrap();
let mut tampered = report.replace("cause_1=1,0,0,1", "cause_1=1,0,0,0");
if tampered == report {
tampered = report.replace("cause_0=0,0,0,1", "cause_0=0,0,0,0");
}
fs::write(&certificate, tampered).unwrap();
assert!(verify_causal_certificate(&input, &certificate).is_err());
assert!(verify_causal_bundle(&input, &bundle).is_err());
std::fs::remove_file(input).unwrap();
std::fs::remove_dir_all(bundle).unwrap();
}
#[test]
fn causal_bundle_rejects_overwrite_and_tampered_metrics() {
let stem = format!("cq-sat-causal-bundle-{}", std::process::id());
let input = std::env::temp_dir().join(format!("{stem}.aag"));
let bundle = std::env::temp_dir().join(format!("{stem}.bundle"));
fs::write(
&input,
"aag 4 2 1 1 1\n2\n4\n6 6 0\n8\n8 2 4\ni0 left_request\ni1 right_request\no0 simultaneous_request\n",
)
.unwrap();
explain_aiger_counterexample(&input, 1, 16, &bundle).unwrap();
assert!(
explain_aiger_counterexample(&input, 1, 16, &bundle)
.unwrap_err()
.contains("already exists")
);
let original_metrics = fs::read_to_string(bundle.join("causal-metrics.csv")).unwrap();
fs::write(bundle.join("causal-metrics.csv"), "tampered\n").unwrap();
assert!(verify_causal_bundle(&input, &bundle).is_err());
fs::write(bundle.join("causal-metrics.csv"), original_metrics).unwrap();
assert!(verify_causal_bundle(&input, &bundle).is_ok());
fs::write(bundle.join("unexpected.txt"), "unexpected\n").unwrap();
assert!(verify_causal_bundle(&input, &bundle).is_err());
fs::remove_file(bundle.join("unexpected.txt")).unwrap();
let changed_input = std::env::temp_dir().join(format!("{stem}-changed.aag"));
fs::write(
&changed_input,
"aag 4 2 1 1 1\n2\n4\n6 6 0\n8\n8 2 4\ni0 left_request\ni1 right_request\no0 renamed_output\n",
)
.unwrap();
assert!(verify_causal_bundle(&changed_input, &bundle).is_err());
std::fs::remove_file(input).unwrap();
std::fs::remove_file(changed_input).unwrap();
std::fs::remove_dir_all(bundle).unwrap();
}
#[test]
fn causal_analysis_rejects_models_without_primary_inputs_without_publishing() {
let stem = format!("cq-sat-causal-no-input-{}", std::process::id());
let input = std::env::temp_dir().join(format!("{stem}.aag"));
let bundle = std::env::temp_dir().join(format!("{stem}.bundle"));
fs::write(&input, "aag 1 0 1 1 0\n2 2\n2\n").unwrap();
assert!(
explain_aiger_counterexample(&input, 1, 16, &bundle)
.unwrap_err()
.contains("at least one primary input")
);
assert!(!bundle.exists());
std::fs::remove_file(input).unwrap();
}
#[test]
fn causal_relative_output_uses_the_current_directory_as_its_parent() {
assert_eq!(
causal_output_parent(Path::new("causal-bundle")),
Path::new(".")
);
assert_eq!(
causal_output_parent(Path::new("nested/causal-bundle")),
Path::new("nested")
);
}
#[test]
fn causal_strategy_comparison_is_exact_strict_and_no_overwrite() {
let stem = format!("cq-sat-causal-strategies-{}", std::process::id());
let input = std::env::temp_dir().join(format!("{stem}.aag"));
let output = std::env::temp_dir().join(format!("{stem}.csv"));
fs::write(
&input,
"aag 4 2 1 1 1\n2\n4\n6 6 0\n8\n8 2 4\ni0 left_request\ni1 right_request\no0 simultaneous_request\n",
)
.unwrap();
benchmark_aiger_causal_strategies(&input, 1, 16, &output).unwrap();
let report = fs::read_to_string(&output).unwrap();
let rows = report.lines().collect::<Vec<_>>();
assert_eq!(rows.len(), 3);
assert_eq!(rows[0], CAUSAL_STRATEGY_HEADER);
assert!(rows[1].contains(",deletion,2,2,0;1,"));
assert!(rows[2].contains(",quickxplain,2,2,0;1,"));
assert!(rows.iter().all(|row| row.split(',').count() == 28));
assert!(
rows.iter()
.skip(1)
.all(|row| row.ends_with(",true,true,ok"))
);
assert!(
benchmark_aiger_causal_strategies(&input, 1, 16, &output)
.unwrap_err()
.contains("already exists")
);
fs::remove_file(input).unwrap();
fs::remove_file(output).unwrap();
}
#[test]
fn causal_subset_map_enumerates_distinct_minimal_causes() {
let result = enumerate_minimal_causal_sets(4, 8, |active| {
Ok((active[0] && active[1]) || (active[2] && active[3]))
})
.unwrap();
assert!(result.complete);
let causes = result
.causes
.iter()
.map(|cause| {
cause
.iter()
.enumerate()
.filter_map(|(index, selected)| selected.then_some(index))
.collect::<Vec<_>>()
})
.collect::<BTreeSet<_>>();
assert_eq!(causes, BTreeSet::from([vec![0, 1], vec![2, 3]]));
}
#[test]
fn causal_batch_compiles_once_and_explains_all_failing_targets() {
let stem = format!("cq-sat-causal-batch-{}", std::process::id());
let input = std::env::temp_dir().join(format!("{stem}.aig"));
let output = std::env::temp_dir().join(format!("{stem}.csv"));
fs::write(&input, binary_aiger_fixture()).unwrap();
benchmark_aiger_causal_batch(&input, 1, 16, 4, 2, &output).unwrap();
let report = fs::read_to_string(&output).unwrap();
let rows = report.lines().collect::<Vec<_>>();
assert_eq!(rows[0], CAUSAL_BATCH_HEADER);
assert_eq!(rows.len(), 7); // two failing frame/output targets, three vocabularies.
assert!(rows.iter().skip(1).all(|row| row.split(',').count() == 25));
assert!(
rows.iter()
.skip(1)
.all(|row| row.ends_with(",true,true,ok"))
);
assert!(rows.iter().skip(1).any(|row| row.contains(",segments,")));
assert!(rows.iter().skip(1).any(|row| row.contains(",points,")));
assert!(rows.iter().skip(1).any(|row| row.contains(",dyadic,")));
verify_aiger_causal_batch(&input, &output).unwrap();
assert!(
benchmark_aiger_causal_batch(&input, 1, 16, 4, 2, &output)
.unwrap_err()
.contains("already exists")
);
fs::remove_file(input).unwrap();
fs::remove_file(output).unwrap();
}
#[test]
fn interface_quotient_replays_causal_transcripts_against_cdcl() {
let stem = format!("cq-sat-interface-quotient-{}", std::process::id());
let input = std::env::temp_dir().join(format!("{stem}.aig"));
let output = std::env::temp_dir().join(format!("{stem}.csv"));
fs::write(&input, binary_aiger_fixture()).unwrap();
benchmark_aiger_interface_quotient(&input, 1, 4, 2, &output).unwrap();
let report = fs::read_to_string(&output).unwrap();
let rows = report.lines().collect::<Vec<_>>();
assert_eq!(rows[0], INTERFACE_QUOTIENT_HEADER);
assert_eq!(rows.len(), 3);
assert!(rows.iter().skip(1).all(|row| row.split(',').count() == 26));
assert!(
rows.iter()
.skip(1)
.all(|row| row.ends_with(",true,true,ok"))
);
verify_aiger_interface_quotient(&input, &output).unwrap();
let omitted = rows[..rows.len() - 1].join("\n") + "\n";
fs::write(&output, omitted).unwrap();
assert!(
verify_aiger_interface_quotient(&input, &output)
.unwrap_err()
.contains("omits a reachable target")
);
let mut tampered = report.clone();
let digest = report.lines().nth(1).unwrap().split(',').nth(1).unwrap();
tampered = tampered.replacen(digest, &"0".repeat(64), 1);
fs::write(&output, tampered).unwrap();
assert!(
verify_aiger_interface_quotient(&input, &output)
.unwrap_err()
.contains("invalid identity or status")
);
fs::write(&output, report).unwrap();
assert!(
benchmark_aiger_interface_quotient(&input, 1, 4, 2, &output)
.unwrap_err()
.contains("already exists")
);
fs::remove_file(input).unwrap();
fs::remove_file(output).unwrap();
let wide_input = std::env::temp_dir().join(format!("{stem}-wide.aag"));
let wide_output = std::env::temp_dir().join(format!("{stem}-wide.csv"));
fs::write(&wide_input, wide_sparse_aiger_fixture()).unwrap();
benchmark_aiger_interface_quotient(&wide_input, 1, 4, 2, &wide_output).unwrap();
let wide_report = fs::read_to_string(&wide_output).unwrap();
assert!(
wide_report
.lines()
.skip(1)
.all(|row| row.ends_with(",true,true,ok"))
);
verify_aiger_interface_quotient(&wide_input, &wide_output).unwrap();
fs::remove_file(wide_input).unwrap();
fs::remove_file(wide_output).unwrap();
}
#[test]
fn causal_minimizer_rejects_a_non_forcing_observation_set() {
let result = minimize_causal_events(2, |_| Ok(false));
assert!(
result
.unwrap_err()
.contains("do not force the target failure")
);
}
#[test]
fn quickxplain_and_deletion_are_one_minimal_for_all_small_monotone_oracles() {
for count in 1..=4usize {
let assignments = 1usize << count;
for table in 0usize..(1usize << assignments) {
let conflict = |active: &[bool]| {
let mask = active
.iter()
.enumerate()
.fold(0usize, |mask, (index, value)| {
mask | (usize::from(*value) << index)
});
(table >> mask) & 1 == 1
};
if !conflict(&vec![true; count]) {
continue;
}
let monotone = (0..assignments).all(|mask| {
if (table >> mask) & 1 == 0 {
return true;
}
(0..assignments)
.all(|superset| mask & superset != mask || (table >> superset) & 1 == 1)
});
if !monotone {
continue;
}
let deletion =
minimize_causal_events(count, |active| Ok(conflict(active))).unwrap();
validate_causal_selection(&deletion, |active| Ok(conflict(active))).unwrap();
let mut quick_queries = 0usize;
let quickxplain = quickxplain_causal_events(count, |active| {
quick_queries += 1;
Ok(conflict(active))
})
.unwrap();
validate_causal_selection(&quickxplain, |active| Ok(conflict(active))).unwrap();
assert!(quick_queries <= count.saturating_mul(2).saturating_add(1));
}
}
}
#[test]
fn independent_input_driven_aiger_models_match_known_safety_results() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/aiger");
for (name, horizon, expected) in [
("petersons-algorithm-2-threads-1-core.aag", 20, "SAFE"),
("spi-bus-receive-e-08-bits.aag", 16, "UNSAFE"),
] {
let stem = format!("cq-sat-aiger-known-{expected}-{}", std::process::id());
let output = std::env::temp_dir().join(format!("{stem}.csv"));
let safety = std::env::temp_dir().join(format!("{stem}.txt"));
verify_cq_aiger(&root.join(name), horizon, 10, 200_000, &output, &safety).unwrap();
let portfolio = fs::read_to_string(&output).unwrap();
let result = fs::read_to_string(&safety).unwrap();
std::fs::remove_file(output).unwrap();
std::fs::remove_file(safety).unwrap();
assert!(portfolio.contains(",cdcl,aiger-primary-inputs,"));
assert!(portfolio.contains(",true,true,ok\n"));
assert!(result.starts_with(&format!("status={expected}\n")));
if expected == "UNSAFE" {
assert!(result.contains("bad_frame=16\n"));
assert!(result.contains("input_bits_low_to_high\n"));
}
}
}
#[test]
fn firmware_safety_gate_distinguishes_safe_and_rejected_builds() {
let root =
Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/products/infusion-pump/firmware");
for (name, expected_safe, expected_status) in [
("safe-controller.aag", true, "status=SAFE\n"),
("door-interlock-regression.aag", false, "status=UNSAFE\n"),
] {
let artifacts = std::env::temp_dir().join(format!(
"cq-sat-firmware-gate-{name}-{}",
std::process::id()
));
let safe = firmware_safety_gate(&root.join(name), 8, &artifacts).unwrap();
assert_eq!(safe, expected_safe);
let report = fs::read_to_string(artifacts.join("safety-report.txt")).unwrap();
assert!(report.starts_with(expected_status));
assert!(artifacts.join("solver-metrics.csv").is_file());
if !expected_safe {
assert!(report.contains("bad_frame=1\n"));
assert!(report.contains("0,0,10\n"));
assert!(report.contains("1,1,01\n"));
}
std::fs::remove_dir_all(artifacts).unwrap();
}
}
#[test]
fn evidence_index_rejects_oversized_file_before_hashing() {
let root =
std::env::temp_dir().join(format!("cq-sat-oversized-evidence-{}", std::process::id()));
fs::create_dir(&root).unwrap();
let oversized = root.join("oversized.bin");
fs::File::create(&oversized)
.unwrap()
.set_len(YOSYS_FILE_LIMIT_BYTES + 1)
.unwrap();
fs::write(
root.join("evidence.sha256"),
format!("{} oversized.bin\n", "0".repeat(64)),
)
.unwrap();
let index_digest = sha256_file(&root.join("evidence.sha256")).unwrap();
assert!(
validate_evidence_index(&root, &index_digest)
.unwrap_err()
.contains("evidence file exceeds")
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn rtl_safety_gate_synthesizes_and_names_exact_traces_when_yosys_is_available() {
if Command::new("yosys").arg("-V").output().is_err() {
eprintln!("skipping RTL integration test because Yosys is unavailable");
return;
}
let root =
Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/products/infusion-pump/rtl");
for (name, expected_safe, expected_status) in [
("safe-controller.sv", true, "status=SAFE\n"),
("door-interlock-regression.sv", false, "status=UNSAFE\n"),
] {
let artifacts =
std::env::temp_dir().join(format!("cq-sat-rtl-gate-{name}-{}", std::process::id()));
let safe = firmware_rtl_safety_gate(
&root.join(name),
"infusion_pump_controller",
8,
&artifacts,
)
.unwrap();
assert_eq!(safe, expected_safe);
let report = fs::read_to_string(artifacts.join("safety-report.txt")).unwrap();
assert!(report.starts_with(expected_status));
assert!(report.contains("top=infusion_pump_controller\n"));
assert!(report.contains("generated_model=model.aag\n"));
assert!(artifacts.join("source.sv").is_file());
assert!(artifacts.join("signal.map").is_file());
assert!(artifacts.join("run-manifest.txt").is_file());
if !expected_safe {
assert!(report.contains("bad_frame=1\n"));
assert!(report.contains("bad_output_name=bad\n"));
assert!(
report.contains("named_frame,requested_motor_active,motor_request,door_open\n")
);
assert!(report.contains("0,0,1,0\n"));
assert!(report.contains("1,1,0,1\n"));
}
std::fs::remove_dir_all(artifacts).unwrap();
}
assert!(
firmware_rtl_safety_gate(
&root.join("safe-controller.sv"),
"bad; shell",
8,
&std::env::temp_dir().join("cq-sat-invalid-rtl-top")
)
.is_err()
);
#[cfg(unix)]
{
let real_artifacts =
std::env::temp_dir().join(format!("cq-sat-real-artifacts-{}", std::process::id()));
let linked_artifacts = std::env::temp_dir()
.join(format!("cq-sat-linked-artifacts-{}", std::process::id()));
fs::create_dir(&real_artifacts).unwrap();
std::os::unix::fs::symlink(&real_artifacts, &linked_artifacts).unwrap();
assert!(
firmware_rtl_safety_gate(
&root.join("safe-controller.sv"),
"infusion_pump_controller",
8,
&linked_artifacts,
)
.unwrap_err()
.contains("real directory")
);
fs::remove_file(linked_artifacts).unwrap();
fs::remove_dir(real_artifacts).unwrap();
}
let oversized =
std::env::temp_dir().join(format!("cq-sat-oversized-rtl-{}.sv", std::process::id()));
let oversized_file = fs::File::create(&oversized).unwrap();
oversized_file.set_len(10 * 1024 * 1024 + 1).unwrap();
assert!(
firmware_rtl_safety_gate(
&oversized,
"infusion_pump_controller",
8,
&std::env::temp_dir().join("cq-sat-oversized-rtl")
)
.is_err()
);
std::fs::remove_file(oversized).unwrap();
}
#[test]
fn hierarchical_rtl_and_bounded_query_reuse_agree_with_cold_bmc() {
if Command::new("yosys").arg("-V").output().is_err() {
eprintln!("skipping hierarchical RTL test because Yosys is unavailable");
return;
}
let root =
Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/products/infusion-pump/rtl");
let artifacts =
std::env::temp_dir().join(format!("cq-sat-hierarchical-rtl-{}", std::process::id()));
assert!(
firmware_rtl_safety_gate(
&root.join("multimodule-controller.sv"),
"infusion_pump_system",
8,
&artifacts,
)
.unwrap()
);
let script = fs::read_to_string(artifacts.join("synthesis.ys")).unwrap();
assert!(script.contains("flatten\n"));
assert!(script.contains("setundef -zero\n"));
let benchmark = artifacts.join("query-reuse.csv");
benchmark_aiger_query_reuse(&artifacts.join("model.aag"), &[4, 8], 2, &benchmark).unwrap();
let results = fs::read_to_string(benchmark).unwrap();
assert_eq!(results.lines().count(), 3);
assert!(results.lines().skip(1).all(|row| row.ends_with(",true,ok")));
assert!(results.lines().skip(1).all(|row| row.contains(",2,2,8,0,")));
assert!(aiger_reuse_gate(15_000, 2));
assert!(!aiger_reuse_gate(15_001, 2));
assert!(!aiger_reuse_gate(1_000, 1));
std::fs::remove_dir_all(artifacts).unwrap();
}
#[test]
fn rtl_project_gate_stages_multiple_sources_and_rejects_ambiguous_inputs() {
if Command::new("yosys").arg("-V").output().is_err() {
eprintln!("skipping RTL project test because Yosys is unavailable");
return;
}
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/infusion-pump/rtl/project");
let sources = vec![root.join("pump-components.sv"), root.join("pump-system.sv")];
let artifacts =
std::env::temp_dir().join(format!("cq-sat-rtl-project-{}", std::process::id()));
assert!(
firmware_rtl_project_safety_gate(&sources, "infusion_pump_system", 8, &artifacts,)
.unwrap()
);
assert!(artifacts.join("source-0000.sv").is_file());
assert!(artifacts.join("source-0001.sv").is_file());
assert!(!artifacts.join("source.sv").exists());
let synthesis = fs::read_to_string(artifacts.join("synthesis.ys")).unwrap();
assert!(synthesis.contains("source-0000.sv source-0001.sv"));
assert!(!synthesis.contains(&root.to_string_lossy().to_string()));
let manifest = fs::read_to_string(artifacts.join("run-manifest.txt")).unwrap();
assert!(manifest.contains("status=SAFE\n"));
assert!(manifest.contains("schema_version=4\n"));
assert!(manifest.contains("firmware_cli_version=2\n"));
assert!(manifest.contains("evidence_digest_algorithm=sha256\n"));
assert!(manifest.contains("evidence_index=evidence.sha256\n"));
assert!(manifest.contains("source_count=2\n"));
assert!(manifest.contains("source_0="));
assert!(manifest.contains("source_1="));
assert!(manifest.contains(&format!("containment_platform={}\n", std::env::consts::OS)));
assert!(manifest.contains("process_group_timeout_kill=true\n"));
assert!(manifest.contains(&format!(
"synthesis_memory_limit_kind={}\n",
synthesis_memory_limit_kind()
)));
assert!(manifest.contains(&format!(
"synthesis_memory_limit_bytes={}\n",
synthesis_memory_limit_bytes()
)));
assert!(manifest.contains("synthesis_file_limit_bytes=536870912\n"));
validate_rtl_artifact_bundle(&artifacts).unwrap();
let source_snapshot = artifacts.join("source-0000.sv");
let source_bytes = fs::read(&source_snapshot).unwrap();
fs::write(&source_snapshot, b"tampered RTL").unwrap();
assert!(
validate_rtl_artifact_bundle(&artifacts)
.unwrap_err()
.contains("evidence SHA-256 mismatch")
);
fs::write(&source_snapshot, &source_bytes).unwrap();
#[cfg(unix)]
{
let external = std::env::temp_dir().join(format!(
"cq-sat-evidence-symlink-target-{}",
std::process::id()
));
fs::write(&external, &source_bytes).unwrap();
fs::remove_file(&source_snapshot).unwrap();
std::os::unix::fs::symlink(&external, &source_snapshot).unwrap();
assert!(
validate_rtl_artifact_bundle(&artifacts)
.unwrap_err()
.contains("not a regular file")
);
fs::remove_file(&source_snapshot).unwrap();
fs::write(&source_snapshot, &source_bytes).unwrap();
fs::remove_file(external).unwrap();
}
validate_rtl_artifact_bundle(&artifacts).unwrap();
fs::write(
artifacts.join("run-manifest.txt"),
manifest.replacen("status=SAFE", "status=UNSAFE", 1),
)
.unwrap();
assert!(
validate_rtl_artifact_bundle(&artifacts)
.unwrap_err()
.contains("status disagrees")
);
fs::write(
artifacts.join("run-manifest.txt"),
format!("{manifest}unexpected_field=value\n"),
)
.unwrap();
assert!(
validate_rtl_artifact_bundle(&artifacts)
.unwrap_err()
.contains("fields or ordering")
);
fs::write(artifacts.join("run-manifest.txt"), &manifest).unwrap();
assert!(
firmware_rtl_safety_gate(
&root.parent().unwrap().join("safe-controller.sv"),
"infusion_pump_controller",
8,
&artifacts,
)
.unwrap()
);
assert!(artifacts.join("source.sv").is_file());
assert!(!artifacts.join("source-0000.sv").exists());
assert!(!artifacts.join("source-0001.sv").exists());
std::fs::remove_dir_all(artifacts).unwrap();
assert!(
firmware_rtl_project_safety_gate(
&[sources[0].clone(), sources[0].clone()],
"infusion_pump_system",
8,
&std::env::temp_dir().join("cq-sat-duplicate-rtl-project"),
)
.unwrap_err()
.contains("duplicate RTL source")
);
assert!(
firmware_rtl_project_safety_gate(
&vec![sources[0].clone(); 65],
"infusion_pump_system",
8,
&std::env::temp_dir().join("cq-sat-too-many-rtl-sources"),
)
.unwrap_err()
.contains("between 1 and 64")
);
}
#[test]
fn rtl_environment_assumptions_are_exact_named_all_frame_constraints() {
if Command::new("yosys").arg("-V").output().is_err() {
eprintln!("skipping RTL assumption test because Yosys is unavailable");
return;
}
let root =
Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/products/infusion-pump/rtl");
let source = root.join("door-interlock-regression.sv");
let assumptions = root.join("door-closed.assumptions");
let artifacts =
std::env::temp_dir().join(format!("cq-sat-rtl-assumptions-{}", std::process::id()));
assert!(
firmware_rtl_project_safety_gate_with_assumptions(
&[source],
"infusion_pump_controller",
8,
&artifacts,
Some(&assumptions),
None,
)
.unwrap()
);
assert_eq!(
fs::read_to_string(artifacts.join("assumptions.txt")).unwrap(),
fs::read_to_string(&assumptions).unwrap()
);
let report = fs::read_to_string(artifacts.join("safety-report.txt")).unwrap();
assert!(report.contains("assumption_count=1\n"));
assert!(report.contains("assumption_0=door_open=0\n"));
let manifest = fs::read_to_string(artifacts.join("run-manifest.txt")).unwrap();
assert!(manifest.contains("assumption_count=1\n"));
validate_rtl_artifact_bundle(&artifacts).unwrap();
let unknown = std::env::temp_dir().join(format!(
"cq-sat-unknown-assumption-{}.txt",
std::process::id()
));
fs::write(&unknown, "missing_input=0\n").unwrap();
let (constraints, _) = parse_environment_assumptions(&unknown).unwrap();
let model = parse_aag(&artifacts.join("model.aag")).unwrap();
assert!(
aag_bmc_encoding_with_constraints(&model, 8, &constraints)
.err()
.unwrap()
.contains("matched 0 synthesized inputs")
);
std::fs::remove_file(unknown).unwrap();
std::fs::remove_dir_all(artifacts).unwrap();
}
#[test]
fn rtl_project_config_v1_is_strict_and_path_safe() {
let scratch =
std::env::temp_dir().join(format!("cq-sat-project-config-{}", std::process::id()));
fs::create_dir_all(&scratch).unwrap();
let config_path = scratch.join("cq-project.conf");
fs::write(
&config_path,
"project_version=1\ntop=pump_top\nhorizon=32\nclock=clk:posedge\nreset=rst_n:deasserted-high\nsource=rtl/top.sv\nsource=rtl/memory.sv\ninclude_dir=rtl/include\nparameter=DEPTH:16\nassumptions=env.assumptions\n",
)
.unwrap();
let parsed = parse_rtl_project_config(&config_path).unwrap();
assert_eq!(parsed.version, 1);
assert_eq!(parsed.top, "pump_top");
assert_eq!(parsed.horizon, 32);
assert_eq!(parsed.sources.len(), 2);
assert_eq!(parsed.include_dirs, vec![PathBuf::from("rtl/include")]);
assert_eq!(
parsed.parameters,
vec![("DEPTH".to_string(), "16".to_string())]
);
assert_eq!(parsed.clock, ("clk".to_string(), "posedge".to_string()));
assert_eq!(
parsed.reset,
RtlResetPolicy::Deasserted {
signal: "rst_n".to_string(),
level: true
}
);
fs::write(
&config_path,
"project_version=2\ntop=pump_top\nhorizon=32\nclock=clk:posedge\nreset=rst_n:active-low:2\nsource=rtl/top.sv\n",
)
.unwrap();
let startup = parse_rtl_project_config(&config_path).unwrap();
assert_eq!(startup.version, 2);
assert_eq!(
startup.reset,
RtlResetPolicy::Startup {
signal: "rst_n".to_string(),
active_low: true,
asserted_frames: 2,
}
);
for (body, expected) in [
(
"project_version=1\ntop=x\nhorizon=1\nclock=c:posedge\nreset=none\nsource=../escape.sv\n",
"without traversal",
),
(
"project_version=1\ntop=x\ntop=y\nhorizon=1\nclock=c:posedge\nreset=none\nsource=x.sv\n",
"duplicate",
),
(
"project_version=1\ntop=x\nhorizon=1\nclock=c:posedge\nreset=none\nsource=x.sv\nshell=evil\n",
"unknown",
),
(
"project_version=1\ntop=x\nhorizon=2\nclock=c:posedge\nreset=rst_n:active-low:1\nsource=x.sv\n",
"require project_version=2",
),
] {
fs::write(&config_path, body).unwrap();
assert!(
parse_rtl_project_config(&config_path)
.unwrap_err()
.contains(expected)
);
}
fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn rtl_config_gate_snapshots_includes_applies_parameters_and_maps_memory() {
if Command::new("yosys").arg("-V").output().is_err() {
eprintln!("skipping RTL config test because Yosys is unavailable");
return;
}
let project = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples/products/infusion-pump/rtl/config-project");
let artifacts =
std::env::temp_dir().join(format!("cq-sat-config-project-{}", std::process::id()));
assert!(
firmware_rtl_config_safety_gate(&project.join("cq-project.conf"), &artifacts).unwrap()
);
validate_rtl_artifact_bundle(&artifacts).unwrap();
assert_eq!(
fs::read(artifacts.join("cq-project.conf")).unwrap(),
fs::read(project.join("cq-project.conf")).unwrap()
);
assert_eq!(
fs::read(artifacts.join("include-0000/pump-widths.svh")).unwrap(),
fs::read(project.join("include/pump-widths.svh")).unwrap()
);
let script = fs::read_to_string(artifacts.join("synthesis.ys")).unwrap();
assert!(script.contains("-Iinclude-0000"));
assert!(script.contains("chparam -set DEPTH 8 infusion_pump_memory"));
assert!(script.contains("select -assert-count 1 infusion_pump_memory/clk"));
assert!(script.contains("memory_map"));
let manifest = fs::read_to_string(artifacts.join("run-manifest.txt")).unwrap();
assert!(manifest.contains("schema_version=4\n"));
assert!(manifest.contains("firmware_cli_version=2\n"));
assert!(manifest.contains("clock_policy=clk:posedge\n"));
assert!(manifest.contains("reset_policy=rst_n:active-low:1\n"));
assert!(manifest.contains("parameters=DEPTH:8\n"));
let report = fs::read_to_string(artifacts.join("safety-report.txt")).unwrap();
assert!(
report.contains("assumption_0=rst_n=startup(asserted_frames=1,asserted_value=0)\n")
);
let config_snapshot = fs::read_to_string(artifacts.join("cq-project.conf")).unwrap();
fs::write(
artifacts.join("cq-project.conf"),
config_snapshot.replace("parameter=DEPTH:8", "parameter=DEPTH:7"),
)
.unwrap();
assert!(
validate_rtl_artifact_bundle(&artifacts)
.unwrap_err()
.contains("evidence SHA-256 mismatch")
);
fs::remove_dir_all(artifacts).unwrap();
}
#[test]
fn startup_reset_constraints_change_value_at_the_exact_frame_boundary() {
let model = AagModel {
max_variable: 2,
inputs: vec![2],
input_names: vec!["rst_n".to_string()],
latches: vec![AagLatch {
current: 4,
next: 4,
initial: Some(false),
}],
latch_names: vec!["state".to_string()],
outputs: vec![4],
output_names: vec!["bad".to_string()],
ands: Vec::new(),
};
let encoding = aag_bmc_encoding_with_constraints(
&model,
3,
&[AagInputConstraint {
name: "rst_n".to_string(),
pattern: AagInputConstraintPattern::StartupReset {
asserted_frames: 2,
asserted_value: false,
},
}],
)
.unwrap();
for (frame, expected) in [(0, false), (1, false), (2, true), (3, true)] {
let variable = frame * model.max_variable;
assert!(
encoding
.clauses
.iter()
.any(|clause| clause.0 == vec![(variable, expected)])
);
}
}
#[test]
fn public_rtl_corpus_accepts_parameterless_safe_and_unsafe_bundles() {
if Command::new("yosys").arg("-V").output().is_err() {
eprintln!("skipping public RTL corpus test because Yosys is unavailable");
return;
}
let corpus = Path::new(env!("CARGO_MANIFEST_DIR")).join("corpus/rtl/yosys-simple");
for (case, expected_safe) in [("always01-safe", true), ("always01-unsafe", false)] {
let artifacts = std::env::temp_dir()
.join(format!("cq-sat-public-rtl-{case}-{}", std::process::id()));
assert_eq!(
firmware_rtl_config_safety_gate(&corpus.join(format!("{case}.conf")), &artifacts,)
.unwrap(),
expected_safe
);
validate_rtl_artifact_bundle(&artifacts).unwrap();
let manifest = fs::read_to_string(artifacts.join("run-manifest.txt")).unwrap();
assert!(manifest.contains("parameter_count=0\nparameters=none\n"));
fs::remove_dir_all(artifacts).unwrap();
}
}
fn deterministic_input_mutation(seed: &[u8], iteration: usize) -> Vec<u8> {
let mut bytes = seed.to_vec();
if bytes.is_empty() {
bytes.push(0);
}
let mut rng =
Rng(0x9e37_79b9_7f4a_7c15 ^ iteration as u64 ^ (seed.len() as u64).rotate_left(23));
for _ in 0..=iteration % 8 {
match rng.below(4) {
0 => {
let index = rng.below(bytes.len());
bytes[index] ^= 1 << rng.below(8);
}
1 => bytes.truncate(rng.below(bytes.len() + 1)),
2 if bytes.len() < 16_384 => {
let index = rng.below(bytes.len() + 1);
bytes.insert(index, rng.below(256) as u8);
}
_ => {
let index = rng.below(bytes.len());
bytes[index] = rng.below(256) as u8;
}
}
if bytes.is_empty() {
bytes.push(rng.below(256) as u8);
}
}
bytes.truncate(16_384);
bytes
}
fn corpus_files(path: &Path) -> Vec<Vec<u8>> {
let mut paths = fs::read_dir(path)
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|entry| entry.is_file())
.collect::<Vec<_>>();
paths.sort();
paths.iter().map(|entry| fs::read(entry).unwrap()).collect()
}
#[test]
fn parser_mutation_regression_corpora_are_bounded_and_process_safe() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let scratch =
std::env::temp_dir().join(format!("cq-sat-parser-reliability-{}", std::process::id()));
fs::create_dir(&scratch).unwrap();
let mut aiger_seeds = corpus_files(&root.join("tests/fuzz-corpus/aiger"));
aiger_seeds.push(fs::read(root.join("examples/aiger/counter-overflow-4.aag")).unwrap());
let aiger_input = scratch.join("mutated.aag");
for iteration in 0..5_000 {
let bytes = deterministic_input_mutation(
&aiger_seeds[iteration % aiger_seeds.len()],
iteration,
);
fs::write(&aiger_input, bytes).unwrap();
let _ = parse_aag(&aiger_input);
}
let oversized_aiger = scratch.join("oversized.aag");
fs::File::create(&oversized_aiger)
.unwrap()
.set_len(AAG_INPUT_LIMIT_BYTES + 1)
.unwrap();
assert!(
parse_aag(&oversized_aiger)
.unwrap_err()
.contains("exceeds safety limit")
);
let mut assumption_seeds = corpus_files(&root.join("tests/fuzz-corpus/assumptions"));
assumption_seeds.push(
fs::read(root.join("examples/products/infusion-pump/rtl/door-closed.assumptions"))
.unwrap(),
);
let assumption_input = scratch.join("mutated.assumptions");
for iteration in 0..5_000 {
let bytes = deterministic_input_mutation(
&assumption_seeds[iteration % assumption_seeds.len()],
iteration ^ 0x5a5a,
);
fs::write(&assumption_input, bytes).unwrap();
let _ = parse_environment_assumptions(&assumption_input);
}
let config_seeds = corpus_files(&root.join("tests/fuzz-corpus/project-config"));
let config_input = scratch.join("mutated.conf");
for iteration in 0..5_000 {
let bytes = deterministic_input_mutation(
&config_seeds[iteration % config_seeds.len()],
iteration ^ 0x3c3c,
);
fs::write(&config_input, bytes).unwrap();
let _ = parse_rtl_project_config(&config_input);
}
let cli_seeds = corpus_files(&root.join("tests/fuzz-corpus/cli"));
let cli_artifacts = scratch.join("cli-artifacts");
for iteration in 0..10_000 {
let bytes = deterministic_input_mutation(
&cli_seeds[iteration % cli_seeds.len()],
iteration ^ 0xa5a5,
);
let text = String::from_utf8_lossy(&bytes);
for line in text.lines().take(32) {
let mut args = line
.split('\t')
.take(70)
.map(|argument| argument.chars().take(1_024).collect::<String>())
.collect::<Vec<_>>();
if args.first().map(String::as_str) == Some("firmware-safety-gate")
&& args.len() == 4
{
args[1] = scratch.join("missing.aag").to_string_lossy().to_string();
args[3] = cli_artifacts.to_string_lossy().to_string();
}
if args.first().map(String::as_str) == Some("firmware-cli-version")
&& args.len() == 1
{
args.push("fuzz-extra-argument".to_string());
}
let _ = run_firmware_gate_cli(&args);
}
}
std::fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn firmware_cli_contract_version_is_machine_readable_and_strict() {
assert_eq!(
run_firmware_gate_cli(&["firmware-cli-version".to_string()]).unwrap(),
Some(true)
);
assert!(
run_firmware_gate_cli(&["firmware-cli-version".to_string(), "unexpected".to_string(),])
.unwrap_err()
.contains("usage:")
);
#[cfg(feature = "production-firmware")]
assert_eq!(
run_firmware_gate_cli(&["production-profile-version".to_string()]).unwrap(),
Some(true)
);
#[cfg(not(feature = "production-firmware"))]
assert!(
run_firmware_gate_cli(&["production-profile-version".to_string()])
.unwrap_err()
.contains("not a production support-profile build")
);
assert!(
run_firmware_gate_cli(&[
"production-profile-version".to_string(),
"unexpected".to_string(),
])
.unwrap_err()
.contains("usage:")
);
}
#[test]
fn event_contract_predicates_express_non_cube_scheduler_rules() {
let at_most_one = InputPredicate {
clauses: vec![vec![(0, false), (1, false)]],
};
let allowed = (0..4)
.filter(|input| at_most_one.allows(*input))
.collect::<Vec<_>>();
assert_eq!(allowed, vec![0, 1, 2]);
assert!(!allowed.len().is_power_of_two());
}
#[test]
fn event_contract_constant_false_property_still_requires_an_admissible_trace() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut model =
parse_aag(&root.join(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
))
.unwrap();
model.outputs[0] = 0;
let relevant = IndependentPredicateChecker::support(&model).unwrap();
assert_eq!(relevant.len(), 9);
let contract = EventContract {
horizon: 1,
phases: vec![EventContractPhase {
start: 0,
length: 1,
predicate: InputPredicate {
clauses: vec![vec![(0, true)], vec![(0, false)]],
},
}],
terminal: InputPredicate { clauses: vec![] },
};
let mut quotient = PredicateQuotient::new(&model).unwrap();
assert!(
quotient
.query_event_contract(0, 0, &contract)
.unwrap()
.is_none()
);
assert!(!solve_event_contract_cdcl(&model, &relevant, 0, &contract).unwrap());
}
#[test]
fn event_contract_unavoidable_answer_agrees_with_exact_cdcl() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let model =
parse_aag(&root.join(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
))
.unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let contract = EventContract {
horizon: 1,
phases: vec![EventContractPhase {
start: 0,
length: 1,
predicate: InputPredicate { clauses: vec![] },
}],
terminal: InputPredicate {
clauses: vec![vec![(0, true)], vec![(0, false)]],
},
};
let mut quotient = PredicateQuotient::new(&model).unwrap();
assert!(
quotient
.query_event_contract(0, 0, &contract)
.unwrap()
.is_none()
);
assert!(!solve_event_contract_cdcl(&model, &relevant, 0, &contract).unwrap());
}
#[test]
fn event_contract_cnf_agrees_with_cdcl_across_product_cohort() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let fixtures = [
(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/event-contracts/interrupt-priority-v1.contract",
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/event-contracts/actuator-interlock-v1.contract",
),
(
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
"examples/event-contracts/robot-recovery-v1.contract",
),
];
for (index, (model, contract)) in fixtures.into_iter().enumerate() {
let output = std::env::temp_dir().join(format!(
"guarded-continuation-event-contract-{}-{index}.csv",
std::process::id()
));
let _ = fs::remove_file(&output);
benchmark_aiger_event_contract(&root.join(model), 0, &root.join(contract), 1, &output)
.unwrap();
let body = fs::read_to_string(&output).unwrap();
assert!(body.lines().nth(1).unwrap().ends_with(",true,true,ok"));
fs::remove_file(output).unwrap();
}
}
#[test]
fn event_contract_cnf_relations_have_independently_checked_proof_primitives() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
for (model_path, contract_path) in [
(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/event-contracts/interrupt-priority-v1.contract",
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/event-contracts/actuator-interlock-v1.contract",
),
(
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
"examples/event-contracts/robot-recovery-v1.contract",
),
] {
let model = parse_aag(&root.join(model_path)).unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let contract =
parse_event_contract(&root.join(contract_path), &model, &relevant).unwrap();
let metrics = event_contract_proof_experiment(&model, 0, &contract).unwrap();
assert_eq!(
metrics.obligations,
contract.phases.len() * (1usize << model.latches.len()) + 1
);
assert!(metrics.witnesses > 0);
assert!(metrics.proof_bytes > 0);
}
}
#[test]
fn event_contract_proof_rejects_an_omitted_relation_target() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let model =
parse_aag(&root.join(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
))
.unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let contract = parse_event_contract(
&root.join("examples/event-contracts/interrupt-priority-v1.contract"),
&model,
&relevant,
)
.unwrap();
let mut quotient = PredicateQuotient::new(&model).unwrap();
let relation = quotient
.interface
.relation_predicate(&contract.phases[0].predicate)
.unwrap();
let targets = relation.rows()[0][0] as u16;
let omitted = targets & !(1u16 << targets.trailing_zeros());
let clauses = predicate_relation_completeness_clauses_for_predicate(
&model,
&relevant,
0,
&contract.phases[0].predicate,
omitted,
)
.unwrap();
assert!(generate_varisat_unsat_proof(&clauses).is_err());
}
#[test]
fn event_contract_certificate_v3_is_deterministic_and_exact_across_products() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
for (index, (model, contract, expected_avoidable)) in [
(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/event-contracts/interrupt-priority-v1.contract",
true,
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/event-contracts/actuator-interlock-v1.contract",
true,
),
(
"examples/products/mobile-robot/firmware/dense-sensor-fusion.aag",
"examples/event-contracts/robot-recovery-v1.contract",
true,
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/event-contracts/actuator-h1-unavoidable-v1.contract",
false,
),
]
.into_iter()
.enumerate()
{
let first = std::env::temp_dir().join(format!(
"guarded-continuation-event-v3-{}-{index}-a.cert3",
std::process::id()
));
let second = std::env::temp_dir().join(format!(
"guarded-continuation-event-v3-{}-{index}-b.cert3",
std::process::id()
));
let _ = fs::remove_file(&first);
let _ = fs::remove_file(&second);
let model = root.join(model);
let contract = root.join(contract);
certify_aiger_event_contract_v3(&model, 0, &contract, &first).unwrap();
certify_aiger_event_contract_v3(&model, 0, &contract, &second).unwrap();
assert_eq!(fs::read(&first).unwrap(), fs::read(&second).unwrap());
verify_aiger_event_contract_certificate_v3(&model, &contract, &first).unwrap();
assert_eq!(
parse_event_contract_certificate_v3(&first)
.unwrap()
.avoidable,
expected_avoidable
);
fs::remove_file(first).unwrap();
fs::remove_file(second).unwrap();
}
}
#[test]
fn event_contract_certificate_v3_proves_unavoidable_and_rejects_tampering() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let model = root
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let stem = format!(
"guarded-continuation-event-v3-tamper-{}",
std::process::id()
);
let contract = std::env::temp_dir().join(format!("{stem}.contract"));
let certificate = std::env::temp_dir().join(format!("{stem}.cert3"));
let tampered = std::env::temp_dir().join(format!("{stem}-tampered.cert3"));
let _ = fs::remove_file(&contract);
let _ = fs::remove_file(&certificate);
let _ = fs::remove_file(&tampered);
fs::write(
&contract,
"event_contract_version=1\nhorizon=1\nphase_count=1\nphase_0=0,1\nphase_0_clause_count=0\nterminal_clause_count=2\nterminal_clause_0=irq[0]\nterminal_clause_1=!irq[0]\n",
)
.unwrap();
certify_aiger_event_contract_v3(&model, 0, &contract, &certificate).unwrap();
verify_aiger_event_contract_certificate_v3(&model, &contract, &certificate).unwrap();
let parsed = parse_event_contract_certificate_v3(&certificate).unwrap();
assert!(!parsed.avoidable);
assert!(parsed.states.is_empty());
let mut changed = parsed.clone();
changed.phases[0].powered_rows[0] ^= 1;
fs::write(&tampered, event_contract_certificate_v3_body(&changed)).unwrap();
assert!(verify_aiger_event_contract_certificate_v3(&model, &contract, &tampered).is_err());
fs::remove_file(&tampered).unwrap();
let mut changed = parsed;
changed.contract_sha256 = "0".repeat(64);
fs::write(&tampered, event_contract_certificate_v3_body(&changed)).unwrap();
assert!(verify_aiger_event_contract_certificate_v3(&model, &contract, &tampered).is_err());
fs::remove_file(contract).unwrap();
fs::remove_file(certificate).unwrap();
fs::remove_file(&tampered).unwrap();
}
#[test]
fn event_contract_certificate_v3_corrupt_inputs_are_bounded_and_fail_closed() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let input = root
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let contract = root.join("examples/event-contracts/interrupt-priority-v1.contract");
let scratch = std::env::temp_dir().join(format!(
"guarded-continuation-event-v3-reliability-{}",
std::process::id()
));
fs::create_dir(&scratch).unwrap();
let valid = scratch.join("valid.cert3");
certify_aiger_event_contract_v3(&input, 0, &contract, &valid).unwrap();
let seed = fs::read(&valid).unwrap();
let mutated = scratch.join("mutated.cert3");
for iteration in 0..1_000 {
fs::write(
&mutated,
deterministic_input_mutation(&seed, iteration ^ 0xe3e3),
)
.unwrap();
let _ = parse_event_contract_certificate_v3(&mutated);
}
let invalid_utf8 = scratch.join("invalid-utf8.cert3");
fs::write(&invalid_utf8, [0xff, b'\n']).unwrap();
assert!(parse_event_contract_certificate_v3(&invalid_utf8).is_err());
let oversized = scratch.join("oversized.cert3");
fs::File::create(&oversized)
.unwrap()
.set_len(EVENT_CONTRACT_CERTIFICATE_MAX_BYTES + 1)
.unwrap();
assert!(
parse_event_contract_certificate_v3(&oversized)
.unwrap_err()
.contains("no larger")
);
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let symlink_path = scratch.join("symlink.cert3");
symlink(&valid, &symlink_path).unwrap();
assert!(parse_event_contract_certificate_v3(&symlink_path).is_err());
}
let parsed = parse_event_contract_certificate_v3(&valid).unwrap();
let swapped = scratch.join("swapped-proof.cert3");
let mut swapped_body = parsed.clone();
swapped_body.phases[0].proofs.swap(0, 1);
fs::write(&swapped, event_contract_certificate_v3_body(&swapped_body)).unwrap();
assert!(verify_aiger_event_contract_certificate_v3(&input, &contract, &swapped).is_err());
let truncated = scratch.join("truncated-proof.cert3");
let mut truncated_body = parsed.clone();
truncated_body.phases[0].proofs[0].pop();
fs::write(
&truncated,
event_contract_certificate_v3_body(&truncated_body),
)
.unwrap();
assert!(verify_aiger_event_contract_certificate_v3(&input, &contract, &truncated).is_err());
fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn event_contract_certificate_v3_cost_preserves_exact_agreement() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let output = std::env::temp_dir().join(format!(
"guarded-continuation-event-v3-cost-test-{}.csv",
std::process::id()
));
let _ = fs::remove_file(&output);
benchmark_aiger_event_contract_certificate_v3_cost(
&root.join(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
),
0,
&root.join("examples/event-contracts/interrupt-priority-v1.contract"),
1,
&output,
)
.unwrap();
let body = fs::read_to_string(&output).unwrap();
let row = body.lines().nth(1).unwrap();
assert!(row.ends_with(",true,ok"));
assert_eq!(row.split(',').count(), 19);
fs::remove_file(output).unwrap();
}
#[test]
fn event_contract_v3_obligation_export_is_deterministic_complete_and_bound() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let scratch = std::env::temp_dir().join(format!(
"guarded-continuation-event-v3-obligations-{}",
std::process::id()
));
fs::create_dir(&scratch).unwrap();
for (index, (model, contract)) in [
(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/event-contracts/interrupt-priority-v1.contract",
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/event-contracts/actuator-h1-unavoidable-v1.contract",
),
]
.into_iter()
.enumerate()
{
let model = root.join(model);
let contract = root.join(contract);
let certificate = scratch.join(format!("input-{index}.cert3"));
certify_aiger_event_contract_v3(&model, 0, &contract, &certificate).unwrap();
let first = scratch.join(format!("first-{index}"));
let second = scratch.join(format!("second-{index}"));
export_aiger_event_contract_v3_obligations(&model, &contract, &certificate, &first)
.unwrap();
export_aiger_event_contract_v3_obligations(&model, &contract, &certificate, &second)
.unwrap();
let first_manifest = fs::read(first.join("manifest.txt")).unwrap();
assert_eq!(
first_manifest,
fs::read(second.join("manifest.txt")).unwrap()
);
let manifest = String::from_utf8(first_manifest).unwrap();
assert!(manifest.starts_with("event_contract_obligation_bundle_version=1\n"));
assert!(manifest.contains(&format!(
"contract_sha256={}\n",
sha256_file(&contract).unwrap()
)));
let count = manifest
.lines()
.find_map(|line| line.strip_prefix("obligation_count="))
.unwrap()
.parse::<usize>()
.unwrap();
let parsed = parse_event_contract_certificate_v3(&certificate).unwrap();
assert_eq!(
count,
parsed
.phases
.iter()
.map(|phase| phase.base_rows.len())
.sum::<usize>()
+ 1
);
for line in manifest.lines().filter(|line| {
line.starts_with("obligation_") && !line.starts_with("obligation_count=")
}) {
let filename = line.split_once('=').unwrap().1.split(',').next().unwrap();
assert_eq!(
fs::read(first.join(filename)).unwrap(),
fs::read(second.join(filename)).unwrap()
);
let (variables, clauses) = parse_dimacs(&first.join(filename)).unwrap();
assert!(solve_with_varisat(variables, &clauses).is_none());
}
assert_eq!(
fs::read(first.join("aggregate.cnf")).unwrap(),
fs::read(second.join("aggregate.cnf")).unwrap()
);
let (variables, clauses) = parse_dimacs(&first.join("aggregate.cnf")).unwrap();
assert!(solve_with_varisat(variables, &clauses).is_none());
assert!(
export_aiger_event_contract_v3_obligations(
&model,
&contract,
&certificate,
&first,
)
.unwrap_err()
.contains("refuses to overwrite")
);
}
let model = root
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let wrong_contract =
root.join("examples/event-contracts/actuator-h1-unavoidable-v1.contract");
let certificate = scratch.join("input-0.cert3");
assert!(
export_aiger_event_contract_v3_obligations(
&model,
&wrong_contract,
&certificate,
&scratch.join("wrong-source"),
)
.unwrap_err()
.contains("source binding mismatch")
);
let substituted_model = root
.join("examples/products/actuator-controller/firmware/dense-actuator-interlock.aag");
assert!(
export_aiger_event_contract_v3_obligations(
&substituted_model,
&root.join("examples/event-contracts/interrupt-priority-v1.contract"),
&certificate,
&scratch.join("wrong-model"),
)
.unwrap_err()
.contains("source binding mismatch")
);
fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn event_contract_cli_v1_contract_is_machine_readable_and_strict() {
assert_eq!(
event_contract_cli_contract_line(),
"event_contract_cli_version=1 certificate_version=3 portfolio_version=1 semantics=bounded-named-cnf-terminal-bad-avoidance proof_format=varisat-native-0.2.2 min_relevant_inputs=9 max_relevant_inputs=16 max_latches=4 max_horizon=64 max_contract_bytes=1048576 max_certificate_bytes=33554432 max_proof_bytes=1048576 max_total_proof_bytes=8388608"
);
assert!(run_artifact_cli(&["event-contract-cli-version".to_string()]).unwrap());
assert!(
run_artifact_cli(&[
"event-contract-cli-version".to_string(),
"unexpected".to_string(),
])
.unwrap_err()
.contains("usage:")
);
}
#[test]
fn event_contract_portfolio_admits_both_answers_and_verifies_reports() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let scratch = std::env::temp_dir().join(format!(
"guarded-continuation-event-portfolio-{}",
std::process::id()
));
fs::create_dir(&scratch).unwrap();
for (index, (model, contract, expected)) in [
(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
"examples/event-contracts/interrupt-priority-v1.contract",
true,
),
(
"examples/products/actuator-controller/firmware/dense-actuator-interlock.aag",
"examples/event-contracts/actuator-h1-unavoidable-v1.contract",
false,
),
]
.into_iter()
.enumerate()
{
let model = root.join(model);
let contract = root.join(contract);
let report = scratch.join(format!("report-{index}.txt"));
let certificate = scratch.join(format!("certificate-{index}.cert3"));
verify_aiger_event_contract_portfolio(&model, 0, &contract, &report, &certificate)
.unwrap();
let parsed = parse_event_contract_portfolio_report(&report).unwrap();
assert!(parsed.admitted);
assert_eq!(parsed.avoidable, expected);
assert_eq!(parsed.backend, "event-contract-certificate-v3");
assert!(parsed.certificate_verified);
verify_aiger_event_contract_portfolio_report(
&model,
0,
&contract,
&report,
&certificate,
)
.unwrap();
}
fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn event_contract_portfolio_falls_back_exactly_on_rejection_and_resource_limit() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let scratch = std::env::temp_dir().join(format!(
"guarded-continuation-event-portfolio-fallback-{}",
std::process::id()
));
fs::create_dir(&scratch).unwrap();
let rejected_model =
root.join("examples/products/infusion-pump/firmware/safe-controller.aag");
let rejected_contract = scratch.join("rejected.contract");
fs::write(
&rejected_contract,
"event_contract_version=1\nhorizon=1\nphase_count=1\nphase_0=0,1\nphase_0_clause_count=0\nterminal_clause_count=0\n",
)
.unwrap();
let rejected_report = scratch.join("rejected.report");
let rejected_certificate = scratch.join("rejected.cert3");
verify_aiger_event_contract_portfolio(
&rejected_model,
0,
&rejected_contract,
&rejected_report,
&rejected_certificate,
)
.unwrap();
let parsed = parse_event_contract_portfolio_report(&rejected_report).unwrap();
assert!(!parsed.admitted);
assert_eq!(parsed.backend, "persistent-cdcl");
assert_eq!(parsed.reason, "static-rejection");
assert!(!rejected_certificate.exists());
verify_aiger_event_contract_portfolio_report(
&rejected_model,
0,
&rejected_contract,
&rejected_report,
&rejected_certificate,
)
.unwrap();
let admitted_model = root
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let admitted_contract =
root.join("examples/event-contracts/interrupt-priority-v1.contract");
let fallback_report = scratch.join("resource.report");
let fallback_certificate = scratch.join("resource.cert3");
verify_aiger_event_contract_portfolio_with_node_limit(
&admitted_model,
0,
&admitted_contract,
&fallback_report,
&fallback_certificate,
1,
)
.unwrap();
let parsed = parse_event_contract_portfolio_report(&fallback_report).unwrap();
assert!(parsed.admitted);
assert_eq!(parsed.backend, "persistent-cdcl");
assert_eq!(parsed.reason, "event-contract-resource-fallback");
assert!(!fallback_certificate.exists());
verify_aiger_event_contract_portfolio_report(
&admitted_model,
0,
&admitted_contract,
&fallback_report,
&fallback_certificate,
)
.unwrap();
fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn event_contract_portfolio_report_rejects_hostile_and_tampered_inputs() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let model = root
.join("examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag");
let contract = root.join("examples/event-contracts/interrupt-priority-v1.contract");
let scratch = std::env::temp_dir().join(format!(
"guarded-continuation-event-report-reliability-{}",
std::process::id()
));
fs::create_dir(&scratch).unwrap();
let report = scratch.join("valid.report");
let certificate = scratch.join("valid.cert3");
verify_aiger_event_contract_portfolio(&model, 0, &contract, &report, &certificate).unwrap();
let seed = fs::read(&report).unwrap();
let mutated = scratch.join("mutated.report");
for iteration in 0..1_000 {
fs::write(
&mutated,
deterministic_input_mutation(&seed, iteration ^ 0xe4e4),
)
.unwrap();
let _ = parse_event_contract_portfolio_report(&mutated);
}
let invalid_utf8 = scratch.join("invalid-utf8.report");
fs::write(&invalid_utf8, [0xff, b'\n']).unwrap();
assert!(parse_event_contract_portfolio_report(&invalid_utf8).is_err());
let oversized = scratch.join("oversized.report");
fs::File::create(&oversized)
.unwrap()
.set_len(16 * 1024 + 1)
.unwrap();
assert!(parse_event_contract_portfolio_report(&oversized).is_err());
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let linked = scratch.join("linked.report");
symlink(&report, &linked).unwrap();
assert!(parse_event_contract_portfolio_report(&linked).is_err());
}
let tampered = scratch.join("tampered.report");
let body = String::from_utf8(seed)
.unwrap()
.replace("result=avoidable", "result=unavoidable");
fs::write(&tampered, body).unwrap();
assert!(
verify_aiger_event_contract_portfolio_report(
&model,
0,
&contract,
&tampered,
&certificate,
)
.is_err()
);
assert!(
verify_aiger_event_contract_portfolio_report(
&model,
1,
&contract,
&report,
&certificate,
)
.unwrap_err()
.contains("bad output mismatch or range error")
);
fs::remove_dir_all(scratch).unwrap();
}
#[test]
fn event_contract_parser_rejects_ambiguous_and_hostile_rules() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let model =
parse_aag(&root.join(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
))
.unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let path = std::env::temp_dir().join(format!(
"guarded-continuation-hostile-event-{}.contract",
std::process::id()
));
for (body, expected) in [
(
"event_contract_version=1\nhorizon=8\nphase_count=1\nphase_0=1,7\nphase_0_clause_count=0\nterminal_clause_count=0\n",
"contiguous bounded partition",
),
(
"event_contract_version=1\nhorizon=8\nphase_count=1\nphase_0=0,8\nphase_0_clause_count=1\nphase_0_clause_0=irq[0]|!irq[0]\nterminal_clause_count=0\n",
"tautological",
),
(
"event_contract_version=1\nhorizon=8\nphase_count=1\nphase_0=0,8\nphase_0_clause_count=1\nphase_0_clause_0=unknown_irq\nterminal_clause_count=0\n",
"unsupported input",
),
(
"event_contract_version=1\nhorizon=8\nphase_count=1\nphase_0=0,8\nphase_0_clause_count=1\nphase_0_clause_0=irq[0]|irq[0]\nterminal_clause_count=0\n",
"duplicates input",
),
(
"event_contract_version=1\nhorizon=8\nphase_count=1\nphase_0=0,8\nphase_0_clause_count=0\nterminal_clause_count=0\nunknown=value\n",
"unknown event contract field",
),
(
"event_contract_version=1\r\nhorizon=8\r\nphase_count=1\r\nphase_0=0,8\r\nphase_0_clause_count=0\r\nterminal_clause_count=0\r\n",
"canonical newline",
),
(
"event_contract_version=1\nhorizon=8\nphase_count=1\nphase_0=0,8\nphase_0_clause_count=0\nterminal_clause_count=0",
"canonical newline",
),
(
"event_contract_version=1\nhorizon=8\nphase_count=1\nphase_0=18446744073709551615,2\nphase_0_clause_count=0\nterminal_clause_count=0\n",
"exceeds the bounded horizon",
),
] {
fs::write(&path, body).unwrap();
assert!(
parse_event_contract(&path, &model, &relevant)
.unwrap_err()
.contains(expected)
);
}
fs::File::create(&path)
.unwrap()
.set_len(EVENT_CONTRACT_MAX_BYTES + 1)
.unwrap();
assert!(
parse_event_contract(&path, &model, &relevant)
.unwrap_err()
.contains("no larger")
);
fs::remove_file(path).unwrap();
}
#[cfg(unix)]
#[test]
fn event_contract_parser_rejects_symlinks() {
use std::os::unix::fs::symlink;
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let model =
parse_aag(&root.join(
"examples/products/interrupt-controller/firmware/dense-interrupt-arbiter.aag",
))
.unwrap();
let relevant = IndependentPredicateChecker::support(&model).unwrap();
let path = std::env::temp_dir().join(format!(
"guarded-continuation-event-symlink-{}.contract",
std::process::id()
));
let _ = fs::remove_file(&path);
symlink(
root.join("examples/event-contracts/interrupt-priority-v1.contract"),
&path,
)
.unwrap();
assert!(
parse_event_contract(&path, &model, &relevant)
.unwrap_err()
.contains("regular file")
);
fs::remove_file(path).unwrap();
}
#[cfg(unix)]
#[test]
fn contained_process_kills_descendants() {
let limited_file = std::env::temp_dir().join(format!(
"cq-sat-contained-output-{}.bin",
std::process::id()
));
let mut file_command = Command::new("sh");
file_command
.arg("-c")
.arg("dd if=/dev/zero of=\"$1\" bs=2048 count=1 2>/dev/null")
.arg("cq-sat-containment-test")
.arg(&limited_file)
.stderr(Stdio::null());
configure_contained_process(&mut file_command, YOSYS_MEMORY_LIMIT_BYTES, 1024).unwrap();
let mut file_child = file_command.spawn().unwrap();
let file_status = wait_for_contained_process(
&mut file_child,
std::time::Duration::from_secs(5),
"file-limit probe",
)
.unwrap()
.expect("file-limit probe timed out");
assert!(!file_status.success());
assert!(fs::metadata(&limited_file).unwrap().len() <= 1024);
std::fs::remove_file(limited_file).unwrap();
let pid_file = std::env::temp_dir().join(format!(
"cq-sat-contained-descendant-{}.pid",
std::process::id()
));
let mut tree_command = Command::new("sh");
tree_command
.arg("-c")
.arg("sleep 30 & child=$!; printf '%s' \"$child\" > \"$1\"; wait")
.arg("cq-sat-containment-test")
.arg(&pid_file);
configure_contained_process(
&mut tree_command,
YOSYS_MEMORY_LIMIT_BYTES,
YOSYS_FILE_LIMIT_BYTES,
)
.unwrap();
let mut tree_child = tree_command.spawn().unwrap();
assert!(
wait_for_contained_process(
&mut tree_child,
std::time::Duration::from_millis(250),
"process-tree probe",
)
.unwrap()
.is_none()
);
let descendant = fs::read_to_string(&pid_file)
.unwrap()
.parse::<i32>()
.unwrap();
let mut gone = false;
for _ in 0..100 {
// SAFETY: signal zero only queries whether the recorded child PID exists.
if unsafe { libc::kill(descendant, 0) } == -1
&& std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
{
gone = true;
break;
}
#[cfg(target_os = "linux")]
{
// A minimal container may have no init process that promptly reaps
// orphaned zombies. A zombie has terminated and cannot execute, so
// it satisfies the containment invariant even while its PID remains.
let stat = fs::read_to_string(format!("/proc/{descendant}/stat"));
if stat.is_ok_and(|body| {
body.rsplit_once(')')
.and_then(|(_, tail)| tail.split_whitespace().next())
.is_some_and(|state| matches!(state, "Z" | "X"))
}) {
gone = true;
break;
}
}
thread::sleep(std::time::Duration::from_millis(20));
}
assert!(
gone,
"contained descendant remained executable after timeout"
);
std::fs::remove_file(pid_file).unwrap();
}
#[cfg(all(unix, not(target_os = "macos")))]
#[test]
fn contained_process_enforces_address_space_limit() {
let mut memory_command = Command::new("python3");
memory_command
.arg("-c")
.arg("payload = bytearray(768 * 1024 * 1024); print(len(payload))");
configure_contained_process(&mut memory_command, 512 * 1024 * 1024, 1024 * 1024).unwrap();
let mut memory_child = memory_command.spawn().unwrap();
let memory_status = wait_for_contained_process(
&mut memory_child,
std::time::Duration::from_secs(10),
"memory-limit probe",
)
.unwrap()
.expect("memory-limit probe timed out");
assert!(!memory_status.success());
}
#[test]
fn controller_plant_resource_refusal_reasons_are_stable_and_narrow() {
for (message, reason) in [
(
"controller-plant resource envelope artifact-byte limit exceeded",
"artifact-bytes",
),
(
"controller-plant resource envelope member limit exceeded",
"members",
),
(
"controller-plant resource envelope horizon limit exceeded",
"horizon",
),
(
"controller-plant resource envelope product-state limit exceeded",
"product-states",
),
(
"controller-plant resource envelope transition limit exceeded",
"transition-evaluations",
),
] {
assert_eq!(controller_plant_resource_refusal(message), Some(reason));
assert_eq!(
classify_controller_plant_resource_error(message.to_string()),
format!("controller-plant-resource refusal={reason} result=none")
);
}
for invalid in [
"controller MTBDD portfolio integrity mismatch",
"controller-plant resource transition bound overflow",
"controller plant resource policy is not canonical",
] {
assert_eq!(controller_plant_resource_refusal(invalid), None);
assert_eq!(
classify_controller_plant_resource_error(invalid.to_string()),
invalid
);
}
}
#[test]
fn controller_proof_mtbdd_resource_refusal_reasons_are_stable_and_narrow() {
for (message, reason) in [
(
"proof-carrying controller MTBDD resource artifact-byte limit exceeded",
"artifact-bytes",
),
(
"proof-carrying controller MTBDD resource equivalence-artifact limit exceeded",
"equivalence-artifact-bytes",
),
(
"proof-carrying controller MTBDD resource UNSAT-proof limit exceeded",
"unsat-proof-bytes",
),
(
"proof-carrying controller MTBDD resource member limit exceeded",
"members",
),
(
"proof-carrying controller MTBDD resource horizon limit exceeded",
"horizon",
),
(
"proof-carrying controller MTBDD resource product-state limit exceeded",
"product-states",
),
(
"proof-carrying controller MTBDD resource transition limit exceeded",
"transition-evaluations",
),
] {
assert_eq!(
controller_proof_mtbdd_resource_refusal(message),
Some(reason)
);
assert_eq!(
classify_controller_proof_mtbdd_resource_error(message.to_string()),
format!("controller-proof-mtbdd-resource refusal={reason} result=none")
);
}
for invalid in [
"proof-carrying controller MTBDD plant artifact integrity mismatch",
"proof-carrying transition bound overflow",
"controller proof MTBDD resource policy is not canonical",
] {
assert_eq!(controller_proof_mtbdd_resource_refusal(invalid), None);
assert_eq!(
classify_controller_proof_mtbdd_resource_error(invalid.to_string()),
invalid
);
}
}
#[test]
fn controller_split_resource_refusal_reasons_are_stable_and_narrow() {
for (message, reason) in [
(
"controller proof evidence resource artifact-byte limit exceeded",
"controller-artifact-bytes",
),
(
"controller proof evidence resource UNSAT-proof limit exceeded",
"unsat-proof-bytes",
),
("controller split resource batch limit exceeded", "batches"),
(
"bound plant result resource artifact-byte limit exceeded",
"plant-artifact-bytes",
),
(
"bound plant result resource member limit exceeded",
"members-per-batch",
),
(
"bound plant result resource horizon limit exceeded",
"horizon",
),
(
"bound plant result resource product-state limit exceeded",
"product-states",
),
(
"bound plant result resource transition limit exceeded",
"transitions-per-batch",
),
(
"controller split resource total plant artifact-byte limit exceeded",
"total-plant-artifact-bytes",
),
(
"controller split resource total member limit exceeded",
"total-members",
),
(
"controller split resource total transition limit exceeded",
"total-transition-evaluations",
),
] {
assert_eq!(controller_split_resource_refusal(message), Some(reason));
assert_eq!(
classify_controller_split_resource_error(message.to_string()),
format!("controller-split-resource refusal={reason} result=none")
);
}
for invalid in [
"bound plant result mismatch",
"bound plant result resource transition bound overflow",
"controller split resource policy is not canonical",
] {
assert_eq!(controller_split_resource_refusal(invalid), None);
assert_eq!(
classify_controller_split_resource_error(invalid.to_string()),
invalid
);
}
}
#[test]
fn source_model_attestation_rejects_post_snapshot_replacement() {
let source = b"module controller; endmodule\n".to_vec();
let recipe = b"read_verilog controller.v\n".to_vec();
let model = b"aag 0 0 0 0 0\n".to_vec();
let snapshot = LoadedSourceModelSnapshot {
subjects: vec![LoadedSourceModelSubject {
source_path: PathBuf::from("controller.v"),
model_path: PathBuf::from("controller.aag"),
source_sha256: Sha256::digest(&source).into(),
model_sha256: Sha256::digest(&model).into(),
}],
};
let original = vec![(source.clone(), recipe.clone(), model.clone())];
verify_loaded_source_model_snapshot(&original, &snapshot).unwrap();
let replaced_source = vec![(b"replacement source\n".to_vec(), recipe.clone(), model)];
assert_eq!(
verify_loaded_source_model_snapshot(&replaced_source, &snapshot).unwrap_err(),
"source-model provenance subject 0 changed after the query snapshot"
);
let replaced_model = vec![(source, recipe, b"aag 1 0 0 0 0\n".to_vec())];
assert_eq!(
verify_loaded_source_model_snapshot(&replaced_model, &snapshot).unwrap_err(),
"source-model provenance subject 0 changed after the query snapshot"
);
assert_eq!(
verify_loaded_source_model_snapshot(&[], &snapshot).unwrap_err(),
"source-model subject count changed after the query snapshot"
);
}
#[test]
fn btor2_predicate_set_cli_preserves_query_binding_and_output_immutability() {
let source = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("corpus/rtl/opentitan-aon-timer/generated/watchdog-predicate-set-small.btor2");
let certificate = std::env::temp_dir().join(format!(
"gcc-btor2-predicate-set-{}.cert",
std::process::id()
));
let _ = fs::remove_file(&certificate);
assert!(run_artifact_cli(&["btor2-predicate-set-version".to_string()]).unwrap());
let check = vec![
"check-btor2-predicate-set".to_string(),
source.display().to_string(),
"18,22".to_string(),
"4".to_string(),
certificate.display().to_string(),
];
assert!(run_artifact_cli(&check).unwrap());
assert!(
fs::read_to_string(&certificate)
.unwrap()
.starts_with("predicate_set_certificate_version=2\nroute=shared_exact_region\n")
);
assert!(
run_artifact_cli(&check)
.unwrap_err()
.contains("create certificate")
);
assert!(
run_artifact_cli(&[
"verify-btor2-predicate-set".to_string(),
source.display().to_string(),
"18,22".to_string(),
"4".to_string(),
certificate.display().to_string(),
])
.unwrap()
);
assert!(
run_artifact_cli(&[
"verify-btor2-predicate-set".to_string(),
source.display().to_string(),
"18,22".to_string(),
"5".to_string(),
certificate.display().to_string(),
])
.unwrap_err()
.contains("query binding mismatch")
);
assert!(parse_btor2_property_set("22,18").is_err());
fs::remove_file(certificate).unwrap();
}
#[test]
fn certificate_publication_hides_and_cleans_a_failed_partial_write() {
let directory = std::env::temp_dir().join(format!(
"gcc-certificate-publication-failure-{}-{}",
std::process::id(),
CERTIFICATE_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed)
));
let _ = fs::remove_dir_all(&directory);
fs::create_dir(&directory).unwrap();
let output = directory.join("result.certificate");
let error = write_new_certificate_with(&output, |file| {
file.write_all(b"partial-certificate")?;
Err(io::Error::new(
io::ErrorKind::StorageFull,
"injected pre-publication failure",
))
})
.unwrap_err();
assert!(error.contains("injected pre-publication failure"));
assert!(!output.exists());
assert_eq!(fs::read_dir(&directory).unwrap().count(), 0);
fs::remove_dir(directory).unwrap();
}
}