use std::io::{BufRead, Write};
use std::path::Path;
use num_bigint::BigInt;
use num_rational::BigRational;
use crate::error::VitriError;
use super::weights::LiteralWeight;
use super::{Clause, CnfFormula, CnfMeta, Literal, Mode, ShowSet, Space, WeightTable};
pub(crate) fn rational_string(r: &BigRational) -> String {
format!("{}/{}", r.numer(), r.denom())
}
pub fn parse_rational_weight(s: &str) -> Result<BigRational, VitriError> {
parse_weight(s).map_err(VitriError::input)
}
pub(crate) fn parse_weight(s: &str) -> Result<BigRational, String> {
let s = s.trim();
if let Some((n, d)) = s.split_once('/') {
let num: BigInt = n
.trim()
.parse()
.map_err(|_| format!("invalid weight numerator: {s}"))?;
let den: BigInt = d
.trim()
.parse()
.map_err(|_| format!("invalid weight denominator: {s}"))?;
if den.sign() == num_bigint::Sign::NoSign {
return Err(format!("zero denominator in weight: {s}"));
}
return Ok(BigRational::new(num, den));
}
let (mantissa, exp) = match s.split_once(['e', 'E']) {
Some((m, e)) => (
m,
e.trim()
.parse::<i64>()
.map_err(|_| format!("invalid weight exponent: {s}"))?,
),
None => (s, 0i64),
};
let neg = mantissa.starts_with('-');
let mant = mantissa.trim_start_matches(['+', '-']);
let (int_part, frac_part) = match mant.split_once('.') {
Some((i, f)) => (i, f),
None => (mant, ""),
};
let digits: String = format!("{int_part}{frac_part}");
let digits = if digits.is_empty() {
"0".to_string()
} else {
digits
};
let mut num: BigInt = digits
.parse()
.map_err(|_| format!("invalid weight value: {s}"))?;
if neg {
num = -num;
}
let scale = exp - frac_part.len() as i64;
let ten = || BigInt::from(10);
if scale >= 0 {
num *= num_traits::pow(ten(), scale as usize);
Ok(BigRational::from_integer(num))
} else {
let den = num_traits::pow(ten(), (-scale) as usize);
Ok(BigRational::new(num, den))
}
}
#[derive(Clone, Copy)]
struct WidestId {
kind: &'static str,
written: i64,
line: usize,
}
impl WidestId {
fn note(widest: &mut Option<WidestId>, kind: &'static str, written: i64, line: usize) {
let wider = match widest {
Some(w) => written.unsigned_abs() > w.written.unsigned_abs(),
None => true,
};
if wider {
*widest = Some(WidestId {
kind,
written,
line,
});
}
}
fn check(widest: Option<WidestId>, num_vars: u32) -> Result<(), String> {
match widest {
Some(w) if w.written.unsigned_abs() > u64::from(num_vars) => Err(format!(
"line {}: {} {} exceeds the declared variable count {num_vars}",
w.line, w.kind, w.written,
)),
_ => Ok(()),
}
}
}
fn close_clause(current_clause: &mut Vec<Literal>, clauses: &mut Vec<Clause>) {
current_clause.sort_by_key(|l| (l.var.0, !l.positive));
current_clause.dedup();
if current_clause.windows(2).any(|w| w[0].var == w[1].var) {
current_clause.clear();
} else {
clauses.push(Clause::new(std::mem::take(current_clause)));
}
}
impl CnfFormula {
pub fn from_dimacs<R: BufRead>(reader: R) -> Result<(Self, CnfMeta), VitriError> {
Self::parse_dimacs(reader).map_err(VitriError::input)
}
fn parse_dimacs<R: BufRead>(reader: R) -> Result<(Self, CnfMeta), String> {
let mut num_vars = 0u32;
let mut clauses = Vec::new();
let mut current_clause: Vec<Literal> = Vec::new();
let mut line_num = 0usize;
let mut track: Option<Mode> = None;
let mut show: Vec<i64> = Vec::new();
let mut saw_show = false;
let mut weight_lines: Vec<(i32, BigRational)> = Vec::new();
let mut widest: Option<WidestId> = None;
for line in reader.lines() {
let line = line.map_err(|e| e.to_string())?;
let line = line.trim();
line_num += 1;
if line.is_empty() {
continue;
}
match line.as_bytes()[0] {
b'c' => {
let toks: Vec<&str> = line.split_whitespace().collect();
match toks.as_slice() {
["c", "t", ty] => {
track = Some(Mode::parse_track(ty).ok_or_else(|| {
format!("line {line_num}: unknown problem type: {ty}")
})?);
}
["c", "p", "show", rest @ ..] => {
saw_show = true;
for t in rest {
let v: i64 = t.parse().map_err(|_| {
format!("line {line_num}: invalid show var: {t:?}")
})?;
if v == 0 {
break;
}
if v < 0 {
return Err(format!("line {line_num}: negative show var: {v}"));
}
WidestId::note(&mut widest, "show var", v, line_num);
show.push(v);
}
}
["c", "p", "weight", lit, w, ..] => {
let l: i32 = lit.parse().map_err(|_| {
format!("line {line_num}: invalid weight literal: {lit:?}")
})?;
if l == 0 {
return Err(format!("line {line_num}: weight on literal 0"));
}
let val =
parse_weight(w).map_err(|e| format!("line {line_num}: {e}"))?;
WidestId::note(&mut widest, "weight literal", i64::from(l), line_num);
weight_lines.push((l, val));
}
_ => {}
}
continue;
}
b'%' => break, b'w' => continue,
b'p' => {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 4 || parts[1] != "cnf" {
return Err(format!("line {}: invalid problem line: {}", line_num, line));
}
num_vars = parts[2].parse().map_err(|_| {
format!("line {}: invalid variable count: {}", line_num, parts[2])
})?;
let nc: usize = parts[3].parse().map_err(|_| {
format!("line {}: invalid clause count: {}", line_num, parts[3])
})?;
clauses.reserve(nc);
continue;
}
_ => {}
}
if let Some(first) = line.split_whitespace().next()
&& first.contains('.')
{
continue;
}
for token in line.split_whitespace() {
let val: i32 = token.parse().map_err(|_| {
format!(
"line {}: invalid token in clause data: {:?}",
line_num, token
)
})?;
if val == 0 {
close_clause(&mut current_clause, &mut clauses);
} else {
WidestId::note(&mut widest, "clause literal", i64::from(val), line_num);
current_clause.push(Literal::from(val)); }
}
}
if !current_clause.is_empty() {
close_clause(&mut current_clause, &mut clauses);
}
if num_vars == 0 && !clauses.is_empty() {
return Err("Missing problem line".to_string());
}
WidestId::check(widest, num_vars)?;
let show_vars = if saw_show {
let ids: Vec<u32> = show.into_iter().map(|v| v as u32).collect();
Some(ShowSet::from_dimacs_ids(&ids).map_err(|e| e.to_string())?)
} else {
None
};
let weights = if weight_lines.is_empty() {
None
} else {
Some(
WeightTable::from_dimacs_pairs(weight_lines, num_vars)
.map_err(|e| e.to_string())?,
)
};
let meta =
CnfMeta::from_parts(num_vars, track, show_vars, weights).map_err(|e| e.to_string())?;
Ok((CnfFormula { num_vars, clauses }, meta))
}
}
#[derive(Debug)]
pub(crate) struct DimacsHeader<'a, S: Space> {
pub track: Option<&'a str>,
pub show: Option<&'a ShowSet<S>>,
pub weights: Option<&'a [LiteralWeight]>,
}
impl<S: Space> Default for DimacsHeader<'_, S> {
fn default() -> Self {
Self {
track: None,
show: None,
weights: None,
}
}
}
pub(crate) fn write_dimacs<S: Space>(
formula: &CnfFormula,
header: &DimacsHeader<'_, S>,
path: &Path,
) -> Result<(), VitriError> {
debug_assert!(
!formula.is_refuted(),
"refusing to write the empty clause — DIMACS cannot express it, so the file \
would re-parse as satisfiable; emit an explicit contradiction instead",
);
emit_dimacs(formula, header, path).map_err(|e| VitriError::io(path, "write", &e))
}
fn emit_dimacs<S: Space>(
formula: &CnfFormula,
header: &DimacsHeader<'_, S>,
path: &Path,
) -> std::io::Result<()> {
let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
emit_problem_line(&mut f, formula)?;
emit_meta_lines(&mut f, header)?;
emit_clause_lines(&mut f, formula)?;
f.flush()?;
Ok(())
}
fn emit_problem_line<W: std::io::Write>(w: &mut W, formula: &CnfFormula) -> std::io::Result<()> {
writeln!(w, "p cnf {} {}", formula.num_vars, formula.clauses.len())
}
fn emit_meta_lines<W: std::io::Write, S: Space>(
w: &mut W,
header: &DimacsHeader<'_, S>,
) -> std::io::Result<()> {
if let Some(t) = header.track {
writeln!(w, "c t {t}")?;
}
if let Some(show) = header.show {
write!(w, "c p show")?;
for v in show.to_dimacs() {
write!(w, " {v}")?;
}
writeln!(w, " 0")?;
}
if let Some(weights) = header.weights {
for weight in weights {
writeln!(w, "c p weight {} {} 0", weight.literal, weight.weight)?;
}
}
Ok(())
}
fn emit_clause_lines<W: std::io::Write>(w: &mut W, formula: &CnfFormula) -> std::io::Result<()> {
for clause in &formula.clauses {
for (i, lit) in clause.literals.iter().enumerate() {
if i > 0 {
write!(w, " ")?;
}
write!(w, "{}", lit.to_dimacs())?;
}
writeln!(w, " 0")?;
}
Ok(())
}
impl CnfFormula {
pub fn write_dimacs<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
emit_problem_line(w, self)?;
emit_clause_lines(w, self)
}
pub fn write_dimacs_clauses<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
emit_clause_lines(w, self)
}
}