Skip to content

Commit 7752ea9

Browse files
authored
Report whether a file declared a c t track (#41)
`CnfMeta::mode` was a public field that resolved an absent `c t` line to `Mc`, so `c t mc` and a file with no header at all read back identically. A consumer that varies its own defaults between a competition instance and a plain CNF had nothing to key on — and the show set and the weights already answer the same question with an `Option`. The track is now stored as `Option<Mode>` behind two accessors: `CnfMeta::declared_track` reports the line the file carried, and `CnfMeta::mode` reports the track to read the file as, still resolving an absent line to `Mc`. `CnfMeta::from_parts` takes the option as well, so metadata assembled in process can state absence the way a parsed file does. This replaces the public field, so a caller reading a track moves to `mode()`, and one that needs to know whether the file said so moves to `declared_track()`. Inside the crate only mode detection in `resolve_mode` read it.
1 parent 0a5bf98 commit 7752ea9

9 files changed

Lines changed: 107 additions & 27 deletions

File tree

src/cnf/dimacs.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,10 @@ impl CnfFormula {
225225
let mut clauses = Vec::new();
226226
let mut current_clause: Vec<Literal> = Vec::new();
227227
let mut line_num = 0usize;
228-
let mut mode = Mode::default();
228+
// Stays `None` until a `c t` line names a track, so that `c t mc` and a
229+
// file carrying no such line remain distinguishable — see
230+
// [`CnfMeta::declared_track`].
231+
let mut track: Option<Mode> = None;
229232
// The show set and weight lines are collected as written and converted
230233
// to indexed form only once every id is known to fit the declared
231234
// count — both conversions subtract one from a written id, and the
@@ -260,9 +263,9 @@ impl CnfFormula {
260263
let toks: Vec<&str> = line.split_whitespace().collect();
261264
match toks.as_slice() {
262265
["c", "t", ty] => {
263-
mode = Mode::parse_track(ty).ok_or_else(|| {
266+
track = Some(Mode::parse_track(ty).ok_or_else(|| {
264267
format!("line {line_num}: unknown problem type: {ty}")
265-
})?;
268+
})?);
266269
}
267270
["c", "p", "show", rest @ ..] => {
268271
saw_show = true;
@@ -382,7 +385,7 @@ impl CnfFormula {
382385
)
383386
};
384387
let meta =
385-
CnfMeta::from_parts(num_vars, mode, show_vars, weights).map_err(|e| e.to_string())?;
388+
CnfMeta::from_parts(num_vars, track, show_vars, weights).map_err(|e| e.to_string())?;
386389

387390
Ok((CnfFormula { num_vars, clauses }, meta))
388391
}

src/cnf/mod.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,10 @@ impl Mode {
192192
/// preprocessing renumbers variables.
193193
#[derive(Clone, Debug, Default)]
194194
pub struct CnfMeta {
195-
/// The declared counting track; `Mode::Mc` if no `c t` line was seen.
196-
pub mode: Mode,
195+
/// The track a `c t` line named, read through [`CnfMeta::declared_track`],
196+
/// or resolved to `Mode::Mc` through [`CnfMeta::mode`]. `None` when no
197+
/// `c t` line is present.
198+
track: Option<Mode>,
197199
/// The show set for projected counting, read through
198200
/// [`CnfMeta::declared_show_vars`]; the projected-out set is
199201
/// `all_vars \ show_vars`. `None` when no `c p show` line is present.
@@ -211,8 +213,9 @@ impl CnfMeta {
211213
/// therefore contains 0-based ids. `weights` remains the sparse table of
212214
/// explicit signed 1-based DIMACS literal declarations; omitted literals
213215
/// are not materialized. `None` means the corresponding declaration was
214-
/// absent, while `Some(ShowSet::empty())` and `Some` of an empty
215-
/// [`WeightTable`] preserve explicit empty declarations.
216+
/// absent, for `track` as well as for the other two, while
217+
/// `Some(ShowSet::empty())` and `Some` of an empty [`WeightTable`] preserve
218+
/// explicit empty declarations.
216219
///
217220
/// Passing the formula's declared count is mandatory: metadata assembled
218221
/// in-process is range-checked exactly as metadata read from DIMACS is.
@@ -224,7 +227,7 @@ impl CnfMeta {
224227
/// space.
225228
pub fn from_parts(
226229
num_vars: u32,
227-
mode: Mode,
230+
track: Option<Mode>,
228231
show_vars: Option<ShowSet<Original>>,
229232
weights: Option<WeightTable>,
230233
) -> Result<Self, crate::error::VitriError> {
@@ -241,12 +244,31 @@ impl CnfMeta {
241244
weights.validate_num_vars(num_vars)?;
242245
}
243246
Ok(CnfMeta {
244-
mode,
247+
track,
245248
show_vars,
246249
weights,
247250
})
248251
}
249252

253+
/// The track this file's `c t` line named, or `None` when it carried no
254+
/// such line.
255+
///
256+
/// Declaring a track and counting under one are different questions, as
257+
/// they are for the show set and the weights below. [`Self::mode`] answers
258+
/// the second, resolving an absent line to [`Mode::Mc`]; this answers the
259+
/// first, and is the only thing that tells `c t mc` apart from a file
260+
/// carrying no header at all. A consumer whose own defaults differ between
261+
/// a competition instance and a plain CNF reads this one, not `mode`.
262+
pub fn declared_track(&self) -> Option<Mode> {
263+
self.track
264+
}
265+
266+
/// The counting track to read this file as: the one its `c t` line named,
267+
/// or [`Mode::Mc`] when it carried no such line.
268+
pub fn mode(&self) -> Mode {
269+
self.track.unwrap_or_default()
270+
}
271+
250272
/// The show set this file declares, or `None` when it carried no
251273
/// `c p show` line.
252274
///

src/config/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,11 +984,11 @@ impl RunConfig {
984984
/// instance.)
985985
pub fn resolve_mode(&self, meta: &crate::cnf::CnfMeta) -> Result<ResolvedMode, VitriError> {
986986
use crate::cnf::Mode;
987-
let declares_weights = meta.mode.is_weighted() || meta.declared_weights().is_some();
987+
let declares_weights = meta.mode().is_weighted() || meta.declared_weights().is_some();
988988
// Detection asks a wider question than "does the file carry a show set":
989989
// a `c t pmc` header asks for a projected count even before the
990990
// `c p show` line that must accompany it is read.
991-
let declares_show = meta.mode.is_projected() || meta.declared_show_vars().is_some();
991+
let declares_show = meta.mode().is_projected() || meta.declared_show_vars().is_some();
992992
let detected = match (declares_show, declares_weights) {
993993
(false, false) => Mode::Mc,
994994
(false, true) => Mode::Wmc,

src/tests/bundle/harness.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ impl RoundTrip {
204204
self.mode
205205
};
206206
assert_eq!(
207-
self.reparsed_meta.mode, expect_header,
207+
self.reparsed_meta.mode(),
208+
expect_header,
208209
"reduced.cnf's `c t` header must name the track the record does",
209210
);
210211
if self.mode == Mode::Compile {

src/tests/bundle/writer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ fn writer_round_trips_every_header() {
6262
.expect("write");
6363
let (reparsed, meta) = parse(&std::fs::read_to_string(&path).expect("read"));
6464
assert_eq!(reparsed, formula);
65-
assert_eq!(meta.mode, Mode::Pwmc);
65+
assert_eq!(meta.mode(), Mode::Pwmc);
6666
assert_eq!(
6767
meta.declared_show_vars().map(|s| s.to_dimacs()),
6868
Some(vec![1, 3]),

src/tests/cnf/meta_lines.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ fn test_parse_mcc_weighted_meta() {
8282
let (formula, meta) = CnfFormula::from_dimacs(&input[..]).unwrap();
8383
assert_eq!(formula.num_vars, 2);
8484
assert_eq!(formula.clauses.len(), 1);
85-
assert_eq!(meta.mode, Mode::Wmc);
85+
assert_eq!(meta.mode(), Mode::Wmc);
8686
let wt = meta.weights.expect("weights parsed");
8787
let resolved: Weights<Original> = wt.resolve(2); // (w_neg, w_pos) per var
8888
let r = |n: i64, d: i64| {
@@ -96,7 +96,7 @@ fn test_parse_mcc_weighted_meta() {
9696
fn test_parse_show_set() {
9797
let input = b"c t pmc\np cnf 4 1\nc p show 1 3 0\n1 -2 3 0\n";
9898
let (_f, meta) = CnfFormula::from_dimacs(&input[..]).unwrap();
99-
assert_eq!(meta.mode, Mode::Pmc);
99+
assert_eq!(meta.mode(), Mode::Pmc);
100100
assert_eq!(
101101
meta.declared_show_vars(),
102102
Some(&ShowSet::from_zero_based([0, 2]))
@@ -107,11 +107,45 @@ fn test_parse_show_set() {
107107
fn test_plain_mc_meta_default() {
108108
let input = b"p cnf 2 1\n1 2 0\n";
109109
let (_f, meta) = CnfFormula::from_dimacs(&input[..]).unwrap();
110-
assert_eq!(meta.mode, Mode::Mc);
110+
assert_eq!(meta.mode(), Mode::Mc);
111111
assert!(meta.declared_show_vars().is_none());
112112
assert!(meta.weights.is_none());
113113
}
114114

115+
/// A `c t` line is what makes a file DECLARE a track, and `c t mc` declares one
116+
/// as much as `c t pmc` does. `mode` cannot say so — it reads `Mc` for a file
117+
/// with no such line too — which is why the declaration is reported separately.
118+
#[test]
119+
fn declared_track_reports_the_line_not_the_resolved_mode() {
120+
let (_, projected) =
121+
CnfFormula::from_dimacs(&b"c t pmc\np cnf 6 1\nc p show 2 3 0\n1 -2 0\n"[..])
122+
.expect("must parse");
123+
assert_eq!(projected.declared_track(), Some(Mode::Pmc));
124+
assert_eq!(projected.mode(), Mode::Pmc);
125+
126+
let (_, plain) =
127+
CnfFormula::from_dimacs(&b"c t mc\np cnf 2 1\n1 2 0\n"[..]).expect("must parse");
128+
assert_eq!(
129+
plain.declared_track(),
130+
Some(Mode::Mc),
131+
"`c t mc` names a track, so the file declares one",
132+
);
133+
assert_eq!(plain.mode(), Mode::Mc);
134+
135+
let (_, bare) =
136+
CnfFormula::from_dimacs(&b"c comment\np cnf 3 2\n1 -2 0\n2 3 0\n"[..]).expect("must parse");
137+
assert_eq!(
138+
bare.declared_track(),
139+
None,
140+
"no `c t` line means no declaration, however `mode` resolves",
141+
);
142+
assert_eq!(
143+
bare.mode(),
144+
Mode::Mc,
145+
"an absent track still reads as plain model counting",
146+
);
147+
}
148+
115149
#[test]
116150
fn test_parse_c_p_show_accumulates_and_dedups() {
117151
let input = b"p cnf 4 1\nc p show 3 1 0\nc p show 1 2 0\n1 2 3 4 0\n";

src/tests/cnf/round_trip.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fn a_parsed_formula_reparses_from_the_dimacs_it_writes() {
4747
let declared = meta.weights.as_ref().expect("the fixture declares weights");
4848
let rows = declared_rows(declared);
4949
let header = DimacsHeader {
50-
track: Some(meta.mode.token()),
50+
track: meta.declared_track().map(Mode::token),
5151
show: meta.declared_show_vars(),
5252
weights: Some(&rows),
5353
};
@@ -56,7 +56,8 @@ fn a_parsed_formula_reparses_from_the_dimacs_it_writes() {
5656

5757
assert_eq!(again, formula, "the clause set changed through write→read");
5858
assert_eq!(
59-
again_meta.mode, meta.mode,
59+
again_meta.declared_track(),
60+
meta.declared_track(),
6061
"the track the file declared changed through write→read",
6162
);
6263
assert_eq!(
@@ -143,7 +144,8 @@ fn a_written_track_header_comes_back_as_the_mode_it_names() {
143144
};
144145
let (_, meta) = write_then_read("dimacs-track", &formula, &header);
145146
assert_eq!(
146-
meta.mode, mode,
147+
meta.declared_track(),
148+
Some(mode),
147149
"the `c t` line written for {mode:?} read back as something else",
148150
);
149151
}

src/tests/cnf/types.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,20 @@ fn a_programmatic_weight_table_is_sparse_and_the_last_duplicate_wins() {
4848

4949
/// Absence and an explicit empty declaration remain different values at the
5050
/// programmatic boundary, just as no `c p show` line differs from
51-
/// `c p show 0` in a file.
51+
/// `c p show 0` in a file. The track is the same kind of value: `None` is an
52+
/// undeclared track, not `mc`.
5253
#[test]
5354
fn programmatic_metadata_preserves_empty_declarations_and_absence() {
5455
let empty_weights = WeightTable::from_dimacs_pairs(Vec::new(), 3)
5556
.expect("an empty table has no out-of-range literal");
56-
let declared = CnfMeta::from_parts(3, Mode::Pwmc, Some(ShowSet::empty()), Some(empty_weights))
57-
.expect("empty declarations are valid");
57+
let declared = CnfMeta::from_parts(
58+
3,
59+
Some(Mode::Pwmc),
60+
Some(ShowSet::empty()),
61+
Some(empty_weights),
62+
)
63+
.expect("empty declarations are valid");
64+
assert_eq!(declared.declared_track(), Some(Mode::Pwmc));
5865
assert_eq!(declared.declared_show_vars(), Some(&ShowSet::empty()));
5966
assert_eq!(
6067
declared
@@ -64,8 +71,14 @@ fn programmatic_metadata_preserves_empty_declarations_and_absence() {
6471
Vec::new(),
6572
);
6673

67-
let absent = CnfMeta::from_parts(3, Mode::Pwmc, None, None)
74+
let absent = CnfMeta::from_parts(3, None, None, None)
6875
.expect("absent declarations carry no ids to validate");
76+
assert_eq!(absent.declared_track(), None);
77+
assert_eq!(
78+
absent.mode(),
79+
Mode::Mc,
80+
"an undeclared track still resolves to plain model counting",
81+
);
6982
assert!(absent.declared_show_vars().is_none());
7083
assert!(absent.declared_weights().is_none());
7184
}
@@ -103,8 +116,13 @@ fn zero_and_out_of_range_programmatic_show_ids_are_input_errors() {
103116
);
104117
assert!(zero.to_string().contains('0'), "{zero} must name zero");
105118

106-
let err = CnfMeta::from_parts(3, Mode::Pmc, Some(ShowSet::from_zero_based([3])), None)
107-
.expect_err("zero-based id 3 is DIMACS variable 4, above num_vars 3");
119+
let err = CnfMeta::from_parts(
120+
3,
121+
Some(Mode::Pmc),
122+
Some(ShowSet::from_zero_based([3])),
123+
None,
124+
)
125+
.expect_err("zero-based id 3 is DIMACS variable 4, above num_vars 3");
108126
assert!(
109127
matches!(err, crate::error::VitriError::Input { .. }),
110128
"malformed metadata is input, got {err:?}",

tests/cli/output_bytes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ fn reduced_cnf_alone_states_the_problem_it_belongs_to() {
200200
let (_, meta) =
201201
CnfFormula::from_dimacs(std::io::BufReader::new(file)).expect("the emitted CNF must parse");
202202

203-
assert_eq!(meta.mode.token(), record["mode"]);
203+
assert_eq!(meta.mode().token(), record["mode"]);
204204

205205
let show_from_cnf: Vec<u64> = meta
206206
.declared_show_vars()

0 commit comments

Comments
 (0)