• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

chrjabs / rustsat / 20344260102

18 Dec 2025 04:43PM UTC coverage: 60.722% (-0.09%) from 60.81%
20344260102

push

github

web-flow
feat(capi): export 2-product am1 encoding (#541)

0 of 32 new or added lines in 2 files covered. (0.0%)

13425 of 22109 relevant lines covered (60.72%)

133555.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/codegen/src/main.rs
1
use std::io::Write;
2

3
use minijinja::{context, Environment};
4
use similar::{ChangeTag, TextDiff};
5
use tempfile::NamedTempFile;
6

7
fn main() {
×
8
    // Check if current directory has a Cargo.toml with [workspace]
9
    let cargo_toml_path = std::env::current_dir().unwrap().join("Cargo.toml");
×
10
    let cargo_toml_content =
×
11
        std::fs::read_to_string(cargo_toml_path).expect("Failed to read Cargo.toml");
×
12
    if !cargo_toml_content.contains("[workspace]") {
×
13
        panic!(
×
14
            "Cargo.toml does not contain [workspace] (you must run codegen from the workspace root)"
×
15
        );
16
    }
×
17

18
    let do_check = std::env::args().any(|arg| arg == "--check");
×
19
    let mut has_changes = false;
×
20

21
    let templates = template_env();
×
22

23
    let am1_encs = [
×
24
        Am1 {
×
25
            name: "Pairwise",
×
26
            id: "pairwise",
×
27
            wrapped: false,
×
28
            n_vars: 4,
×
29
            n_clauses: 6,
×
30
        },
×
31
        Am1 {
×
32
            name: "Ladder",
×
33
            id: "ladder",
×
34
            wrapped: false,
×
35
            n_vars: 7,
×
36
            n_clauses: 8,
×
37
        },
×
38
        Am1 {
×
39
            name: "Bitwise",
×
40
            id: "bitwise",
×
41
            wrapped: false,
×
42
            n_vars: 6,
×
43
            n_clauses: 8,
×
44
        },
×
45
        Am1 {
×
46
            name: "Commander",
×
47
            id: "commander",
×
48
            wrapped: true,
×
49
            n_vars: 5,
×
50
            n_clauses: 10,
×
51
        },
×
52
        Am1 {
×
53
            name: "Bimander",
×
54
            id: "bimander",
×
55
            wrapped: true,
×
56
            n_vars: 5,
×
57
            n_clauses: 10,
×
58
        },
×
NEW
59
        Am1 {
×
NEW
60
            name: "TwoProduct",
×
NEW
61
            id: "twoproduct",
×
NEW
62
            wrapped: true,
×
NEW
63
            n_vars: 8,
×
NEW
64
            n_clauses: 10,
×
NEW
65
        },
×
66
    ];
×
67
    let path = "capi/src/encodings/am1.rs";
×
68
    let generated = rustfmt(capi_enc_bindings("capi-am1.rs.j2", &am1_encs, &templates));
×
69
    if do_check {
×
70
        has_changes |= diff(path, &generated);
×
71
    } else {
×
72
        write!(file(path), "{generated}").unwrap();
×
73
    }
×
74
    has_changes |= capi_tests("am1", &am1_encs, &templates, do_check);
×
75

76
    let card_encs = [Card {
×
77
        name: "Totalizer",
×
78
        id: "tot",
×
79
        ub: true,
×
80
        lb: true,
×
81
        n_vars: 12,
×
82
        n_clauses: 28,
×
83
    }];
×
84
    let path = "capi/src/encodings/card.rs";
×
85
    let generated = rustfmt(capi_enc_bindings("capi-card.rs.j2", &card_encs, &templates));
×
86
    if do_check {
×
87
        has_changes |= diff(path, &generated);
×
88
    } else {
×
89
        write!(file(path), "{generated}").unwrap();
×
90
    }
×
91
    has_changes |= capi_tests("card", &card_encs, &templates, do_check);
×
92

93
    let pb_encs = [
×
94
        Pb {
×
95
            name: "GeneralizedTotalizer",
×
96
            id: "gte",
×
97
            ub: true,
×
98
            lb: false,
×
99
            extend: true,
×
100
            n_vars: 24,
×
101
            n_vars_reserve: 24,
×
102
            n_clauses: 25,
×
103
            skip_reserve: false,
×
104
        },
×
105
        Pb {
×
106
            name: "BinaryAdder",
×
107
            id: "bin_adder",
×
108
            ub: true,
×
109
            lb: true,
×
110
            extend: true,
×
111
            n_vars: 20,
×
112
            n_vars_reserve: 20,
×
113
            n_clauses: 53,
×
114
            skip_reserve: true,
×
115
        },
×
116
        Pb {
×
117
            name: "DynamicPolyWatchdog",
×
118
            id: "dpw",
×
119
            ub: true,
×
120
            lb: false,
×
121
            extend: false,
×
122
            n_vars: 19,
×
123
            n_vars_reserve: 19,
×
124
            n_clauses: 21,
×
125
            skip_reserve: false,
×
126
        },
×
127
    ];
×
128
    let path = "capi/src/encodings/pb.rs";
×
129
    let generated = rustfmt(capi_enc_bindings("capi-pb.rs.j2", &pb_encs, &templates));
×
130
    if do_check {
×
131
        has_changes |= diff(path, &generated);
×
132
    } else {
×
133
        write!(file(path), "{generated}").unwrap();
×
134
    }
×
135
    has_changes |= capi_tests("pb", &pb_encs, &templates, do_check);
×
136

137
    has_changes |= capi_header(do_check);
×
138

139
    if has_changes && do_check {
×
140
        std::process::exit(1);
×
141
    }
×
142
}
×
143

144
fn template_env() -> Environment<'static> {
×
145
    let mut env = Environment::new();
×
146
    env.set_loader(minijinja::path_loader("codegen/templates"));
×
147
    env
×
148
}
×
149

150
fn file(path: &str) -> impl std::io::Write {
×
151
    std::io::BufWriter::new(std::fs::File::create(path).expect("could not open file"))
×
152
}
×
153

154
/// Runs `rustfmt` on a generated string
155
fn rustfmt(generated: String) -> String {
×
156
    let mut fmt = std::process::Command::new("rustfmt")
×
157
        .stdin(std::process::Stdio::piped())
×
158
        .stdout(std::process::Stdio::piped())
×
159
        .spawn()
×
160
        .expect("Failed to spawn rustfmt");
×
161

162
    fmt.stdin
×
163
        .take()
×
164
        .expect("Failed to get stdin")
×
165
        .write_all(generated.as_bytes())
×
166
        .expect("Failed to write to rustfmt stdin");
×
167

168
    let formatted_output = fmt.wait_with_output().expect("Failed to wait for rustfmt");
×
169
    if !formatted_output.status.success() {
×
170
        eprintln!("rustfmt failed with exit code: {}", formatted_output.status);
×
171
        std::process::exit(1);
×
172
    }
×
173

174
    String::from_utf8(formatted_output.stdout).unwrap()
×
175
}
×
176

177
/// Runs `clang-format` on a generated string
178
fn clang_format(generated: String) -> String {
×
179
    let mut fmt = std::process::Command::new("clang-format")
×
180
        .stdin(std::process::Stdio::piped())
×
181
        .stdout(std::process::Stdio::piped())
×
182
        .spawn()
×
183
        .expect("Failed to spawn clang-format");
×
184

185
    fmt.stdin
×
186
        .take()
×
187
        .expect("Failed to get stdin")
×
188
        .write_all(generated.as_bytes())
×
189
        .expect("Failed to write to clang-format stdin");
×
190

191
    let formatted_output = fmt
×
192
        .wait_with_output()
×
193
        .expect("Failed to wait for clang-format");
×
194
    if !formatted_output.status.success() {
×
195
        eprintln!(
×
196
            "clang-format failed with exit code: {}",
×
197
            formatted_output.status
198
        );
199
        std::process::exit(1);
×
200
    }
×
201

202
    String::from_utf8(formatted_output.stdout).unwrap()
×
203
}
×
204

205
fn diff(path: &str, generated: &str) -> bool {
×
206
    let Ok(old) = std::fs::read(path) else {
×
207
        eprintln!("Would create {path}");
×
208
        return true;
×
209
    };
210
    let old = std::str::from_utf8(&old).unwrap();
×
211
    if old == generated {
×
212
        return false;
×
213
    }
×
214
    let diff = TextDiff::from_lines(old, generated);
×
215
    eprintln!("Diff for {path}:");
×
216
    for change in diff.iter_all_changes() {
×
217
        let sign = match change.tag() {
×
218
            ChangeTag::Delete => "-",
×
219
            ChangeTag::Insert => "+",
×
220
            ChangeTag::Equal => " ",
×
221
        };
222
        eprint!("{}{}", sign, change);
×
223
    }
224
    true
×
225
}
×
226

227
fn capi_enc_bindings<E: Enc>(
×
228
    template: &str,
×
229
    encs: &[E],
×
230
    templates: &Environment<'static>,
×
231
) -> String {
×
232
    let tmpl = templates.get_template(template).expect("missing template");
×
233
    let ub = encs.iter().any(|enc| enc.ub());
×
234
    let lb = encs.iter().any(|enc| enc.lb());
×
235
    tmpl.render(context!(encodings => encs, ub => ub, lb => lb))
×
236
        .expect("missing template context")
×
237
}
×
238

239
fn capi_tests<E: Enc>(
×
240
    id: &str,
×
241
    encs: &[E],
×
242
    templates: &Environment<'static>,
×
243
    do_check: bool,
×
244
) -> bool {
×
245
    let mut has_changes = false;
×
246
    for entry in std::fs::read_dir("codegen/templates/").expect("failed to iteratre over template")
×
247
    {
248
        let entry = entry.unwrap();
×
249
        let file_type = entry.file_type().unwrap();
×
250
        if file_type.is_file() {
×
251
            let filename = entry.file_name();
×
252
            let filename = filename.to_str().unwrap();
×
253
            if let Some(name) = filename.strip_prefix(&format!("capi-{id}-test-")) {
×
254
                let name = name.trim_end_matches(".j2");
×
255
                let tmpl = templates.get_template(filename).expect("missing template");
×
256
                for enc in encs {
×
257
                    if enc.skip(name) {
×
258
                        continue;
×
259
                    }
×
260
                    let path = format!("capi/tests/{}-{name}", enc.id());
×
261
                    let generated = clang_format(
×
262
                        tmpl.render(context!(enc => enc))
×
263
                            .expect("missing template context"),
×
264
                    );
265
                    if do_check {
×
266
                        has_changes |= diff(&path, &generated);
×
267
                    } else {
×
268
                        write!(file(&path), "{generated}").unwrap();
×
269
                    }
×
270
                }
271
            }
×
272
        }
×
273
    }
274
    has_changes
×
275
}
×
276

277
trait Enc: serde::Serialize {
278
    fn id(&self) -> &str;
279
    fn ub(&self) -> bool {
×
280
        false
×
281
    }
×
282
    fn lb(&self) -> bool {
×
283
        false
×
284
    }
×
285
    fn skip(&self, _key: &str) -> bool {
×
286
        false
×
287
    }
×
288
}
289

290
#[derive(serde::Serialize)]
291
struct Am1<'a> {
292
    name: &'a str,
293
    id: &'a str,
294
    wrapped: bool,
295
    n_vars: u32,
296
    n_clauses: usize,
297
}
298

299
impl Enc for Am1<'_> {
300
    fn id(&self) -> &str {
×
301
        self.id
×
302
    }
×
303
}
304

305
#[derive(serde::Serialize)]
306
struct Card<'a> {
307
    name: &'a str,
308
    id: &'a str,
309
    ub: bool,
310
    lb: bool,
311
    n_vars: u32,
312
    n_clauses: usize,
313
}
314

315
impl Enc for Card<'_> {
316
    fn id(&self) -> &str {
×
317
        self.id
×
318
    }
×
319
    fn ub(&self) -> bool {
×
320
        self.ub
×
321
    }
×
322
    fn lb(&self) -> bool {
×
323
        self.lb
×
324
    }
×
325
}
326

327
#[derive(serde::Serialize)]
328
struct Pb<'a> {
329
    name: &'a str,
330
    id: &'a str,
331
    ub: bool,
332
    lb: bool,
333
    extend: bool,
334
    n_vars: u32,
335
    n_vars_reserve: u32,
336
    n_clauses: usize,
337
    skip_reserve: bool,
338
}
339

340
impl Enc for Pb<'_> {
341
    fn id(&self) -> &str {
×
342
        self.id
×
343
    }
×
344
    fn ub(&self) -> bool {
×
345
        self.ub
×
346
    }
×
347
    fn lb(&self) -> bool {
×
348
        self.lb
×
349
    }
×
350
    fn skip(&self, key: &str) -> bool {
×
351
        if key == "reserve.c" {
×
352
            return self.skip_reserve;
×
353
        }
×
354
        false
×
355
    }
×
356
}
357

358
/// Generates the C-API header
359
fn capi_header(do_check: bool) -> bool {
×
360
    let mut temp_path = None;
×
361
    let path = if do_check {
×
362
        let path = NamedTempFile::new().unwrap().into_temp_path();
×
363
        std::fs::copy("capi/rustsat.h", &path).unwrap();
×
364
        temp_path = Some(path);
×
365
        temp_path.as_ref().unwrap().to_str().unwrap()
×
366
    } else {
367
        "capi/rustsat.h"
×
368
    };
369
    let changed = cbindgen::Builder::new()
×
370
        .with_config(
×
371
            cbindgen::Config::from_file("capi/cbindgen.toml")
×
372
                .expect("could not read cbindgen.toml"),
×
373
        )
×
374
        .with_crate("capi")
×
375
        .with_after_include(format!(
×
376
            r#"#define RUSTSAT_VERSION {version}
×
377
#define RUSTSAT_VERSION_MAJOR {major}
×
378
#define RUSTSAT_VERSION_MINOR {minor}
×
379
#define RUSTSAT_VERSION_PATCH {patch}"#,
×
380
            version = env!("CARGO_PKG_VERSION"),
×
381
            major = env!("CARGO_PKG_VERSION_MAJOR"),
×
382
            minor = env!("CARGO_PKG_VERSION_MINOR"),
×
383
            patch = env!("CARGO_PKG_VERSION_PATCH"),
×
384
        ))
×
385
        .generate()
×
386
        .expect("Unable to generate bindings")
×
387
        .write_to_file(path);
×
388
    if changed {
×
389
        let generated = std::fs::read(path).unwrap();
×
390
        let generated = std::str::from_utf8(&generated).unwrap();
×
391
        diff("capi/rustsat.h", generated);
×
392
    }
×
393
    drop(temp_path);
×
394
    changed
×
395
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc