Skip to main content

xlsynth_driver/
prover_config.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Prover configuration helpers.
4//!
5//! This module defines configuration structs for invoking xlsynth-driver
6//! subcommands programmatically. Each configuration translates to a complete
7//! command line for the driver, suitable for running in parallel processes.
8//!
9//! JSON DSL overview
10//! - A single task is an object with a `kind` discriminator (`ir-equiv`,
11//!   `dslx-equiv`, `prove-quickcheck`, `dslx-fn-prove-assertions`).
12//! - Tasks can be composed into groups with `kind` = `all` | `any` | `first`
13//!   and a `tasks` array.
14//!
15//! Example (single task):
16//!
17//! ```json
18//! { "kind": "ir-equiv", "lhs_ir_file": "lhs.ir", "rhs_ir_file": "rhs.ir", "top": "main" }
19//! ```
20//!
21//! See the driver README section "Prover configuration JSON (task-spec DSL)"
22//! for the full schema and examples.
23
24use std::path::PathBuf;
25use std::process::Command;
26
27use serde::{Deserialize, Serialize};
28use xlsynth_prover::prover::SolverChoice;
29use xlsynth_prover::prover::types::AssertionSemantics;
30use xlsynth_prover::prover::types::EquivParallelism;
31use xlsynth_prover::prover::types::QuickCheckAssertionSemantics;
32
33fn add_flag(cmd: &mut Command, name: &str, value: &str) {
34    cmd.arg(format!("--{}", name)).arg(value);
35}
36
37fn add_bool(cmd: &mut Command, name: &str, value: Option<bool>) {
38    if let Some(v) = value {
39        add_flag(cmd, name, if v { "true" } else { "false" });
40    }
41}
42
43fn resolve_driver_exe() -> std::path::PathBuf {
44    if let Ok(p) = std::env::var("CARGO_BIN_EXE_xlsynth-driver") {
45        return std::path::PathBuf::from(p);
46    }
47    let cur = std::env::current_exe().expect("resolve current exe");
48    if let Some(deps_dir) = cur.parent() {
49        if let Some(debug_dir) = deps_dir.parent() {
50            let candidate = debug_dir.join("xlsynth-driver");
51            if candidate.exists() {
52                return candidate;
53            }
54        }
55    }
56    cur
57}
58
59#[derive(Clone, Debug, Default, Serialize, Deserialize)]
60pub struct IrEquivConfig {
61    pub lhs_ir_file: PathBuf,
62    pub rhs_ir_file: PathBuf,
63    /// --top if both sides have same name, otherwise use lhs_ir_top/rhs_ir_top
64    pub top: Option<String>,
65    pub lhs_ir_top: Option<String>,
66    pub rhs_ir_top: Option<String>,
67
68    pub solver: Option<SolverChoice>,
69    pub flatten_aggregates: Option<bool>,
70    pub drop_params: Option<Vec<String>>, // comma joined
71    pub parallelism_strategy: Option<EquivParallelism>,
72    pub assertion_semantics: Option<AssertionSemantics>,
73    pub lhs_fixed_implicit_activation: Option<bool>,
74    pub rhs_fixed_implicit_activation: Option<bool>,
75    /// Include only assertions whose label matches this regex.
76    pub assert_label_filter: Option<String>,
77
78    pub json: Option<bool>,
79}
80
81#[derive(Clone, Debug, Default, Serialize, Deserialize)]
82pub struct DslxEquivConfig {
83    pub lhs_dslx_file: PathBuf,
84    pub rhs_dslx_file: PathBuf,
85
86    /// Either set `dslx_top` or both `lhs_dslx_top` and `rhs_dslx_top`.
87    pub dslx_top: Option<String>,
88    pub lhs_dslx_top: Option<String>,
89    pub rhs_dslx_top: Option<String>,
90
91    // Optional search paths
92    pub dslx_path: Option<Vec<PathBuf>>, // joined with ';'
93    pub dslx_stdlib_path: Option<PathBuf>,
94
95    // Behavior flags
96    pub solver: Option<SolverChoice>,
97    pub flatten_aggregates: Option<bool>,
98    pub drop_params: Option<Vec<String>>, // comma joined
99    pub parallelism_strategy: Option<EquivParallelism>,
100    pub assertion_semantics: Option<AssertionSemantics>,
101    pub lhs_fixed_implicit_activation: Option<bool>,
102    pub rhs_fixed_implicit_activation: Option<bool>,
103    pub assume_enum_in_bound: Option<bool>,
104    pub type_inference_v2: Option<bool>, // external toolchain only
105    /// Include only assertions whose label matches this regex.
106    pub assert_label_filter: Option<String>,
107
108    /// Treat DSLX function on the LHS/RHS as uninterpreted function (repeated).
109    /// Each entry is "func_name:uf_name".
110    pub lhs_uf: Option<Vec<String>>, // each entry is "func_name:uf_name"
111    pub rhs_uf: Option<Vec<String>>, // each entry is "func_name:uf_name"
112
113    pub json: Option<bool>,
114}
115
116#[derive(Clone, Debug, Default, Serialize, Deserialize)]
117pub struct ProveQuickcheckConfig {
118    pub dslx_input_file: PathBuf,
119    pub dslx_path: Option<Vec<PathBuf>>,
120    pub dslx_stdlib_path: Option<PathBuf>,
121    pub test_filter: Option<String>,
122    pub solver: Option<SolverChoice>,
123    pub assertion_semantics: Option<QuickCheckAssertionSemantics>,
124    /// Treat DSLX function as uninterpreted function (repeated), entries
125    /// "func_name:uf_name".
126    pub uf: Option<Vec<String>>,
127    /// Include only assertions whose label matches this regex.
128    pub assert_label_filter: Option<String>,
129    pub json: Option<bool>,
130}
131
132#[derive(Clone, Debug, Default, Serialize, Deserialize)]
133pub struct DslxFnProveAssertionsConfig {
134    pub dslx_input_file: PathBuf,
135    pub dslx_top: String,
136    pub dslx_path: Option<Vec<PathBuf>>,
137    pub dslx_stdlib_path: Option<PathBuf>,
138    pub solver: Option<SolverChoice>,
139    /// Include only assertions whose label matches this regex.
140    pub assert_label_filter: Option<String>,
141    pub assume_enum_in_bound: Option<bool>,
142    /// Treat DSLX function as uninterpreted function (repeated), entries
143    /// "func_name:uf_name".
144    pub uf: Option<Vec<String>>,
145}
146
147pub trait ToDriverCommand {
148    fn to_command(&self) -> Command;
149}
150
151impl ToDriverCommand for IrEquivConfig {
152    fn to_command(&self) -> Command {
153        let exe = resolve_driver_exe();
154        let mut cmd = Command::new(exe);
155        cmd.arg("ir-equiv");
156        cmd.arg(self.lhs_ir_file.as_os_str());
157        cmd.arg(self.rhs_ir_file.as_os_str());
158        if let Some(top) = &self.top {
159            add_flag(&mut cmd, "top", top);
160        }
161        if let Some(lhs_top) = &self.lhs_ir_top {
162            add_flag(&mut cmd, "lhs_ir_top", lhs_top);
163        }
164        if let Some(rhs_top) = &self.rhs_ir_top {
165            add_flag(&mut cmd, "rhs_ir_top", rhs_top);
166        }
167        if let Some(solver) = &self.solver {
168            add_flag(&mut cmd, "solver", &solver.to_string());
169        }
170        add_bool(&mut cmd, "flatten_aggregates", self.flatten_aggregates);
171        if let Some(drop) = &self.drop_params {
172            if !drop.is_empty() {
173                add_flag(&mut cmd, "drop_params", &drop.join(","));
174            }
175        }
176        if let Some(strategy) = &self.parallelism_strategy {
177            add_flag(&mut cmd, "parallelism-strategy", &strategy.to_string());
178        }
179        if let Some(sem) = &self.assertion_semantics {
180            add_flag(&mut cmd, "assertion-semantics", &sem.to_string());
181        }
182        if let Some(pat) = &self.assert_label_filter {
183            add_flag(&mut cmd, "assert-label-filter", pat);
184        }
185        add_bool(
186            &mut cmd,
187            "lhs_fixed_implicit_activation",
188            self.lhs_fixed_implicit_activation,
189        );
190        add_bool(
191            &mut cmd,
192            "rhs_fixed_implicit_activation",
193            self.rhs_fixed_implicit_activation,
194        );
195        add_bool(&mut cmd, "json", self.json);
196        cmd
197    }
198}
199
200impl ToDriverCommand for DslxEquivConfig {
201    fn to_command(&self) -> Command {
202        let exe = resolve_driver_exe();
203        let mut cmd = Command::new(exe);
204        cmd.arg("dslx-equiv");
205        cmd.arg(self.lhs_dslx_file.as_os_str());
206        cmd.arg(self.rhs_dslx_file.as_os_str());
207
208        if let Some(top) = &self.dslx_top {
209            add_flag(&mut cmd, "dslx_top", top);
210        }
211        if let Some(lhs_top) = &self.lhs_dslx_top {
212            add_flag(&mut cmd, "lhs_dslx_top", lhs_top);
213        }
214        if let Some(rhs_top) = &self.rhs_dslx_top {
215            add_flag(&mut cmd, "rhs_dslx_top", rhs_top);
216        }
217        if let Some(stdlib) = &self.dslx_stdlib_path {
218            add_flag(&mut cmd, "dslx_stdlib_path", &stdlib.display().to_string());
219        }
220        if let Some(paths) = &self.dslx_path {
221            if !paths.is_empty() {
222                let joined = paths
223                    .iter()
224                    .map(|p| p.display().to_string())
225                    .collect::<Vec<_>>()
226                    .join(";");
227                add_flag(&mut cmd, "dslx_path", &joined);
228            }
229        }
230
231        if let Some(solver) = &self.solver {
232            add_flag(&mut cmd, "solver", &solver.to_string());
233        }
234        add_bool(&mut cmd, "flatten_aggregates", self.flatten_aggregates);
235        if let Some(drop) = &self.drop_params {
236            if !drop.is_empty() {
237                add_flag(&mut cmd, "drop_params", &drop.join(","));
238            }
239        }
240        if let Some(strategy) = &self.parallelism_strategy {
241            add_flag(&mut cmd, "parallelism-strategy", &strategy.to_string());
242        }
243        if let Some(sem) = &self.assertion_semantics {
244            add_flag(&mut cmd, "assertion-semantics", &sem.to_string());
245        }
246        if let Some(pat) = &self.assert_label_filter {
247            add_flag(&mut cmd, "assert-label-filter", pat);
248        }
249        add_bool(
250            &mut cmd,
251            "lhs_fixed_implicit_activation",
252            self.lhs_fixed_implicit_activation,
253        );
254        add_bool(
255            &mut cmd,
256            "rhs_fixed_implicit_activation",
257            self.rhs_fixed_implicit_activation,
258        );
259        add_bool(&mut cmd, "assume-enum-in-bound", self.assume_enum_in_bound);
260        add_bool(&mut cmd, "type_inference_v2", self.type_inference_v2);
261
262        if let Some(list) = &self.lhs_uf {
263            for entry in list {
264                add_flag(&mut cmd, "lhs_uf", entry);
265            }
266        }
267        if let Some(list) = &self.rhs_uf {
268            for entry in list {
269                add_flag(&mut cmd, "rhs_uf", entry);
270            }
271        }
272
273        add_bool(&mut cmd, "json", self.json);
274        cmd
275    }
276}
277
278impl ToDriverCommand for ProveQuickcheckConfig {
279    fn to_command(&self) -> Command {
280        let exe = resolve_driver_exe();
281        let mut cmd = Command::new(exe);
282        cmd.arg("prove-quickcheck");
283        add_flag(
284            &mut cmd,
285            "dslx_input_file",
286            &self.dslx_input_file.display().to_string(),
287        );
288        if let Some(paths) = &self.dslx_path {
289            if !paths.is_empty() {
290                let joined = paths
291                    .iter()
292                    .map(|p| p.display().to_string())
293                    .collect::<Vec<_>>()
294                    .join(";");
295                add_flag(&mut cmd, "dslx_path", &joined);
296            }
297        }
298        if let Some(stdlib) = &self.dslx_stdlib_path {
299            add_flag(&mut cmd, "dslx_stdlib_path", &stdlib.display().to_string());
300        }
301        if let Some(filter) = &self.test_filter {
302            add_flag(&mut cmd, "test_filter", filter);
303        }
304        if let Some(solver) = &self.solver {
305            add_flag(&mut cmd, "solver", &solver.to_string());
306        }
307        if let Some(sem) = &self.assertion_semantics {
308            add_flag(&mut cmd, "assertion-semantics", &sem.to_string());
309        }
310        if let Some(list) = &self.uf {
311            for entry in list {
312                add_flag(&mut cmd, "uf", entry);
313            }
314        }
315        if let Some(pat) = &self.assert_label_filter {
316            add_flag(&mut cmd, "assert-label-filter", pat);
317        }
318        add_bool(&mut cmd, "json", self.json);
319        cmd
320    }
321}
322
323impl ToDriverCommand for DslxFnProveAssertionsConfig {
324    fn to_command(&self) -> Command {
325        let exe = resolve_driver_exe();
326        let mut cmd = Command::new(exe);
327        cmd.arg("dslx-fn-prove-assertions");
328        add_flag(
329            &mut cmd,
330            "dslx_input_file",
331            &self.dslx_input_file.display().to_string(),
332        );
333        add_flag(&mut cmd, "dslx_top", &self.dslx_top);
334        if let Some(paths) = &self.dslx_path {
335            if !paths.is_empty() {
336                let joined = paths
337                    .iter()
338                    .map(|p| p.display().to_string())
339                    .collect::<Vec<_>>()
340                    .join(";");
341                add_flag(&mut cmd, "dslx_path", &joined);
342            }
343        }
344        if let Some(stdlib) = &self.dslx_stdlib_path {
345            add_flag(&mut cmd, "dslx_stdlib_path", &stdlib.display().to_string());
346        }
347        if let Some(solver) = &self.solver {
348            add_flag(&mut cmd, "solver", &solver.to_string());
349        }
350        if let Some(pat) = &self.assert_label_filter {
351            add_flag(&mut cmd, "assert-label-filter", pat);
352        }
353        add_bool(&mut cmd, "assume-enum-in-bound", self.assume_enum_in_bound);
354        if let Some(list) = &self.uf {
355            for entry in list {
356                add_flag(&mut cmd, "uf", entry);
357            }
358        }
359        cmd
360    }
361}
362
363// -------------------------------
364// Fake task definition (available for fuzzing, tests, and local use)
365// -------------------------------
366#[cfg(feature = "enable-fake-task")]
367#[derive(Clone, Debug, Default, Serialize, Deserialize)]
368pub struct FakeTaskConfig {
369    /// Optional delay before the fake task reports a result.
370    /// - When `Some(d)`, sleeps for `d` milliseconds then exits and writes
371    ///   JSON.
372    /// - When `None`, sleeps indefinitely and never finishes (until
373    ///   canceled/timeout).
374    pub delay_ms: Option<u32>,
375    /// Whether the task should report success.
376    pub success: bool,
377    /// Number of bytes to write to stdout.
378    pub stdout_len: u16,
379    /// Number of bytes to write to stderr.
380    pub stderr_len: u16,
381}
382
383#[cfg(feature = "enable-fake-task")]
384impl ToDriverCommand for FakeTaskConfig {
385    fn to_command(&self) -> Command {
386        // Use a POSIX shell snippet to sleep and then locate the --output_json path
387        // from argv.
388        let script = r#"
389delay_ms="${FAKE_DELAY_MS:-}"
390# If delay_ms is empty, sleep indefinitely (never complete).
391if [ -z "$delay_ms" ]; then
392  # Sleep forever to simulate a non-terminating task; will be killed on timeout/cancel.
393  tail -f /dev/null
394fi
395# Otherwise, sleep for the requested milliseconds.
396secs=$(printf "%s" "$delay_ms" | awk '{ printf("%.3f", $1/1000.0) }')
397sleep "$secs"
398# Emit stdout noise
399ol=${FAKE_STDOUT_LEN:-0}
400if [ "$ol" -gt 0 ]; then head -c "$ol" < /dev/zero | tr '\0' 'X'; echo; fi
401# Emit stderr noise
402el=${FAKE_STDERR_LEN:-0}
403if [ "$el" -gt 0 ]; then (head -c "$el" < /dev/zero | tr '\0' 'Y'; echo) >&2; fi
404# Parse --output_json DEST from arguments
405out=""
406while [ $# -gt 0 ]; do
407  if [ "$1" = "--output_json" ]; then
408    shift
409    out="$1"
410    break
411  fi
412  shift
413done
414if [ -n "$out" ]; then
415  if [ "${FAKE_SUCCESS:-1}" -eq 1 ]; then
416    printf '{"success":true}\n' > "$out"
417  else
418    printf '{"success":false}\n' > "$out"
419  fi
420fi
421"#;
422        let mut cmd = Command::new("/bin/sh");
423        cmd.arg("-c").arg(script).arg("sh");
424        if let Some(ms) = self.delay_ms {
425            cmd.env("FAKE_DELAY_MS", ms.to_string());
426        }
427        cmd.env("FAKE_SUCCESS", if self.success { "1" } else { "0" });
428        cmd.env("FAKE_STDOUT_LEN", self.stdout_len.to_string());
429        cmd.env("FAKE_STDERR_LEN", self.stderr_len.to_string());
430        cmd
431    }
432}
433
434// -----------------------------------------------------------------------------
435// Unified enum for collections of prover tasks
436// -----------------------------------------------------------------------------
437
438#[derive(Clone, Debug, Serialize, Deserialize)]
439#[serde(tag = "kind", rename_all = "kebab-case")]
440pub enum ProverTask {
441    #[serde(rename = "ir-equiv")]
442    IrEquiv {
443        #[serde(flatten)]
444        config: IrEquivConfig,
445    },
446    #[serde(rename = "dslx-equiv")]
447    DslxEquiv {
448        #[serde(flatten)]
449        config: DslxEquivConfig,
450    },
451    #[serde(rename = "prove-quickcheck")]
452    ProveQuickcheck {
453        #[serde(flatten)]
454        config: ProveQuickcheckConfig,
455    },
456    #[serde(rename = "dslx-fn-prove-assertions")]
457    DslxFnProveAssertions {
458        #[serde(flatten)]
459        config: DslxFnProveAssertionsConfig,
460    },
461    #[cfg(feature = "enable-fake-task")]
462    #[serde(rename = "fake")]
463    Fake {
464        #[serde(flatten)]
465        config: FakeTaskConfig,
466    },
467}
468
469// No common fields on ProverTask variants; common per-task metadata lives on
470// the ProverPlan::Task node for consolidation.
471
472impl ToDriverCommand for ProverTask {
473    fn to_command(&self) -> Command {
474        match self {
475            ProverTask::IrEquiv { config, .. } => config.to_command(),
476            ProverTask::DslxEquiv { config, .. } => config.to_command(),
477            ProverTask::ProveQuickcheck { config, .. } => config.to_command(),
478            ProverTask::DslxFnProveAssertions { config, .. } => config.to_command(),
479            #[cfg(feature = "enable-fake-task")]
480            ProverTask::Fake { config, .. } => config.to_command(),
481        }
482    }
483}
484
485#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
486#[serde(rename_all = "kebab-case")]
487pub enum GroupKind {
488    All,
489    Any,
490    First,
491}
492
493#[derive(Clone, Debug, Serialize, Deserialize)]
494#[serde(untagged)]
495pub enum ProverPlan {
496    Task {
497        #[serde(flatten)]
498        task: ProverTask,
499        /// Optional timeout for this task node.
500        #[serde(default, skip_serializing_if = "Option::is_none")]
501        timeout_ms: Option<u64>,
502        /// Optional user-provided identifier for tracking/reporting.
503        #[serde(default, skip_serializing_if = "Option::is_none")]
504        task_id: Option<String>,
505    },
506    Group {
507        kind: GroupKind,
508        tasks: Vec<ProverPlan>,
509        // When true, do not cancel or prune sibling tasks upon this group's
510        // resolution; allow them to continue running until they naturally
511        // complete. Default is false.
512        #[serde(default)]
513        keep_running_till_finish: bool,
514    },
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    fn args_of(cmd: Command) -> Vec<String> {
522        cmd.get_args()
523            .map(|s| s.to_string_lossy().into_owned())
524            .collect()
525    }
526
527    fn head_of(plan: &ProverPlan) -> String {
528        match plan {
529            ProverPlan::Task { task, .. } => args_of(task.to_command())[0].clone(),
530            ProverPlan::Group { kind, .. } => match kind {
531                GroupKind::All => "all".to_string(),
532                GroupKind::Any => "any".to_string(),
533                GroupKind::First => "first".to_string(),
534            },
535        }
536    }
537
538    #[test]
539    fn test_ir_equiv_to_command_args() {
540        let cfg = IrEquivConfig {
541            lhs_ir_file: "lhs.ir".into(),
542            rhs_ir_file: "rhs.ir".into(),
543            top: Some("main".to_string()),
544            solver: Some(SolverChoice::Toolchain),
545            parallelism_strategy: Some(EquivParallelism::OutputBits),
546            assertion_semantics: Some(AssertionSemantics::Same),
547            flatten_aggregates: Some(true),
548            drop_params: Some(vec!["p0".into(), "p1".into()]),
549            json: Some(true),
550            ..Default::default()
551        };
552        let got = args_of(cfg.to_command());
553        assert_eq!(
554            got,
555            vec![
556                "ir-equiv",
557                "lhs.ir",
558                "rhs.ir",
559                "--top",
560                "main",
561                "--solver",
562                "toolchain",
563                "--flatten_aggregates",
564                "true",
565                "--drop_params",
566                "p0,p1",
567                "--parallelism-strategy",
568                "output-bits",
569                "--assertion-semantics",
570                "same",
571                "--json",
572                "true",
573            ]
574        );
575    }
576
577    #[test]
578    fn test_dslx_equiv_to_command_args() {
579        let cfg = DslxEquivConfig {
580            lhs_dslx_file: "lhs.x".into(),
581            rhs_dslx_file: "rhs.x".into(),
582            dslx_top: Some("foo".into()),
583            dslx_path: Some(vec!["a".into(), "b".into()]),
584            dslx_stdlib_path: Some("stdlib".into()),
585            solver: Some(SolverChoice::Toolchain),
586            assertion_semantics: Some(AssertionSemantics::Assume),
587            parallelism_strategy: Some(EquivParallelism::SingleThreaded),
588            flatten_aggregates: Some(false),
589            lhs_fixed_implicit_activation: Some(true),
590            rhs_fixed_implicit_activation: Some(false),
591            assume_enum_in_bound: Some(true),
592            json: Some(true),
593            ..Default::default()
594        };
595        let got = args_of(cfg.to_command());
596        assert_eq!(
597            got,
598            vec![
599                "dslx-equiv",
600                "lhs.x",
601                "rhs.x",
602                "--dslx_top",
603                "foo",
604                "--dslx_stdlib_path",
605                "stdlib",
606                "--dslx_path",
607                "a;b",
608                "--solver",
609                "toolchain",
610                "--flatten_aggregates",
611                "false",
612                "--parallelism-strategy",
613                "single-threaded",
614                "--assertion-semantics",
615                "assume",
616                "--lhs_fixed_implicit_activation",
617                "true",
618                "--rhs_fixed_implicit_activation",
619                "false",
620                "--assume-enum-in-bound",
621                "true",
622                "--json",
623                "true",
624            ]
625        );
626    }
627
628    #[test]
629    fn test_prove_quickcheck_to_command_args() {
630        let cfg = ProveQuickcheckConfig {
631            dslx_input_file: "qc.x".into(),
632            dslx_path: Some(vec!["a".into(), "b".into()]),
633            dslx_stdlib_path: Some("stdlib".into()),
634            test_filter: Some(".*mytest".into()),
635            solver: Some(SolverChoice::Toolchain),
636            assertion_semantics: Some(QuickCheckAssertionSemantics::Assume),
637            json: Some(true),
638            ..Default::default()
639        };
640        let got = args_of(cfg.to_command());
641        assert_eq!(
642            got,
643            vec![
644                "prove-quickcheck",
645                "--dslx_input_file",
646                "qc.x",
647                "--dslx_path",
648                "a;b",
649                "--dslx_stdlib_path",
650                "stdlib",
651                "--test_filter",
652                ".*mytest",
653                "--solver",
654                "toolchain",
655                "--assertion-semantics",
656                "assume",
657                "--json",
658                "true",
659            ]
660        );
661    }
662
663    #[test]
664    fn test_dslx_fn_prove_assertions_to_command_args() {
665        let cfg = DslxFnProveAssertionsConfig {
666            dslx_input_file: "assertions.x".into(),
667            dslx_top: "main".into(),
668            dslx_path: Some(vec!["a".into(), "b".into()]),
669            dslx_stdlib_path: Some("stdlib".into()),
670            solver: Some(SolverChoice::Bitwuzla),
671            assert_label_filter: Some("red|blue".into()),
672            assume_enum_in_bound: Some(false),
673            uf: Some(vec!["helper:F".into()]),
674        };
675        let got = args_of(cfg.to_command());
676        assert_eq!(
677            got,
678            vec![
679                "dslx-fn-prove-assertions",
680                "--dslx_input_file",
681                "assertions.x",
682                "--dslx_top",
683                "main",
684                "--dslx_path",
685                "a;b",
686                "--dslx_stdlib_path",
687                "stdlib",
688                "--solver",
689                "bitwuzla",
690                "--assert-label-filter",
691                "red|blue",
692                "--assume-enum-in-bound",
693                "false",
694                "--uf",
695                "helper:F",
696            ]
697        );
698    }
699
700    #[test]
701    fn test_serde_ir_equiv_parse() {
702        let json = r#"{
703            "lhs_ir_file": "lhs.ir",
704            "rhs_ir_file": "rhs.ir",
705            "top": "main",
706            "solver": "toolchain",
707            "parallelism_strategy": "input-bit-split",
708            "assertion_semantics": "never",
709            "json": true
710        }"#;
711        let cfg: IrEquivConfig = serde_json::from_str(json).unwrap();
712        assert_eq!(cfg.solver, Some(SolverChoice::Toolchain));
713        assert_eq!(
714            cfg.parallelism_strategy,
715            Some(EquivParallelism::InputBitSplit)
716        );
717        assert_eq!(cfg.assertion_semantics, Some(AssertionSemantics::Never));
718        assert_eq!(cfg.json, Some(true));
719    }
720
721    #[test]
722    fn test_prover_task_json_roundtrip() {
723        let json = r#"[
724          {
725            "kind": "ir-equiv",
726            "lhs_ir_file": "lhs.ir",
727            "rhs_ir_file": "rhs.ir",
728            "top": "main",
729            "solver": "toolchain",
730            "json": true
731          },
732          {
733            "kind": "dslx-equiv",
734            "lhs_dslx_file": "lhs.x",
735            "rhs_dslx_file": "rhs.x",
736            "dslx_top": "foo",
737            "solver": "toolchain",
738            "json": true
739          },
740          {
741            "kind": "prove-quickcheck",
742            "dslx_input_file": "qc.x",
743            "assertion_semantics": "assume",
744            "solver": "toolchain",
745            "json": true
746          },
747          {
748            "kind": "dslx-fn-prove-assertions",
749            "dslx_input_file": "assertions.x",
750            "dslx_top": "main",
751            "solver": "bitwuzla"
752          }
753        ]"#;
754        let tasks: Vec<ProverTask> = serde_json::from_str(json).unwrap();
755        assert_eq!(tasks.len(), 4);
756        // Smoke test command arg heads
757        let head: Vec<String> = args_of(tasks[0].to_command());
758        assert_eq!(head[0], "ir-equiv");
759        let head: Vec<String> = args_of(tasks[1].to_command());
760        assert_eq!(head[0], "dslx-equiv");
761        let head: Vec<String> = args_of(tasks[2].to_command());
762        assert_eq!(head[0], "prove-quickcheck");
763        let head: Vec<String> = args_of(tasks[3].to_command());
764        assert_eq!(head[0], "dslx-fn-prove-assertions");
765    }
766
767    // -----------------------
768    // ProverPlan tests
769    // -----------------------
770
771    #[test]
772    fn test_prover_plan_all_deserialize_and_heads() {
773        let json = r#"{
774          "kind": "all",
775          "tasks": [
776            { "kind": "ir-equiv", "lhs_ir_file": "lhs.ir", "rhs_ir_file": "rhs.ir" },
777            { "kind": "dslx-equiv", "lhs_dslx_file": "lhs.x", "rhs_dslx_file": "rhs.x", "dslx_top": "foo" },
778            { "kind": "prove-quickcheck", "dslx_input_file": "qc.x" },
779            { "kind": "dslx-fn-prove-assertions", "dslx_input_file": "assertions.x", "dslx_top": "main" }
780          ]
781        }"#;
782        let plan: ProverPlan = serde_json::from_str(json).unwrap();
783        match plan {
784            ProverPlan::Group {
785                kind: GroupKind::All,
786                tasks,
787                keep_running_till_finish,
788            } => {
789                assert_eq!(tasks.len(), 4);
790                let heads: Vec<String> = tasks.iter().map(head_of).collect();
791                assert_eq!(
792                    heads,
793                    vec![
794                        "ir-equiv",
795                        "dslx-equiv",
796                        "prove-quickcheck",
797                        "dslx-fn-prove-assertions"
798                    ]
799                );
800                assert!(!keep_running_till_finish);
801            }
802            _ => panic!("expected ProverPlan::Group(All)"),
803        }
804    }
805
806    #[test]
807    fn test_prover_plan_any_deserialize_and_heads() {
808        let json = r#"{
809          "kind": "any",
810          "tasks": [
811            { "kind": "ir-equiv", "lhs_ir_file": "lhs.ir", "rhs_ir_file": "rhs.ir" },
812            { "kind": "prove-quickcheck", "dslx_input_file": "qc.x" }
813          ],
814          "keep_running_till_finish": true
815        }"#;
816        let plan: ProverPlan = serde_json::from_str(json).unwrap();
817        match plan {
818            ProverPlan::Group {
819                kind: GroupKind::Any,
820                tasks,
821                keep_running_till_finish,
822            } => {
823                assert_eq!(tasks.len(), 2);
824                let heads: Vec<String> = tasks.iter().map(head_of).collect();
825                assert_eq!(heads, vec!["ir-equiv", "prove-quickcheck"]);
826                assert!(keep_running_till_finish);
827            }
828            _ => panic!("expected ProverPlan::Group(Any)"),
829        }
830    }
831
832    #[test]
833    fn test_prover_plan_first_deserialize_and_heads() {
834        let json = r#"{
835          "kind": "first",
836          "tasks": [
837            { "kind": "ir-equiv", "lhs_ir_file": "lhs.ir", "rhs_ir_file": "rhs.ir" },
838            { "kind": "dslx-equiv", "lhs_dslx_file": "lhs.x", "rhs_dslx_file": "rhs.x", "dslx_top": "foo" }
839          ]
840        }"#;
841        let plan: ProverPlan = serde_json::from_str(json).unwrap();
842        match plan {
843            ProverPlan::Group {
844                kind: GroupKind::First,
845                tasks,
846                ..
847            } => {
848                assert_eq!(tasks.len(), 2);
849                let heads: Vec<String> = tasks.iter().map(head_of).collect();
850                assert_eq!(heads, vec!["ir-equiv", "dslx-equiv"]);
851            }
852            _ => panic!("expected ProverPlan::Group(First)"),
853        }
854    }
855
856    #[test]
857    fn test_prover_plan_all_serialize_shape() {
858        let plan = ProverPlan::Group {
859            kind: GroupKind::All,
860            tasks: vec![ProverPlan::Task {
861                task: ProverTask::IrEquiv {
862                    config: IrEquivConfig {
863                        lhs_ir_file: "lhs.ir".into(),
864                        rhs_ir_file: "rhs.ir".into(),
865                        ..Default::default()
866                    },
867                },
868                timeout_ms: None,
869                task_id: None,
870            }],
871            keep_running_till_finish: false,
872        };
873        let v = serde_json::to_value(&plan).unwrap();
874        assert_eq!(v["kind"], "all");
875        assert!(v["tasks"].is_array());
876        assert_eq!(v["tasks"][0]["kind"], "ir-equiv");
877        assert_eq!(v["tasks"][0]["lhs_ir_file"], "lhs.ir");
878        assert_eq!(v["tasks"][0]["rhs_ir_file"], "rhs.ir");
879    }
880
881    #[test]
882    fn test_prover_plan_task_with_metadata_serde() {
883        // Build a task plan with metadata and ensure it serializes and parses
884        // correctly.
885        let plan = ProverPlan::Task {
886            task: ProverTask::IrEquiv {
887                config: IrEquivConfig {
888                    lhs_ir_file: "lhs.ir".into(),
889                    rhs_ir_file: "rhs.ir".into(),
890                    ..Default::default()
891                },
892            },
893            timeout_ms: Some(1234),
894            task_id: Some("task-xyz".to_string()),
895        };
896        let v = serde_json::to_value(&plan).unwrap();
897        assert_eq!(v["kind"], "ir-equiv");
898        assert_eq!(v["lhs_ir_file"], "lhs.ir");
899        assert_eq!(v["rhs_ir_file"], "rhs.ir");
900        assert_eq!(v["timeout_ms"], 1234);
901        assert_eq!(v["task_id"], "task-xyz");
902
903        // Round-trip back to ProverPlan and verify fields.
904        let parsed: ProverPlan = serde_json::from_value(v).unwrap();
905        match parsed {
906            ProverPlan::Task {
907                task: ProverTask::IrEquiv { .. },
908                timeout_ms,
909                task_id,
910            } => {
911                assert_eq!(timeout_ms, Some(1234));
912                assert_eq!(task_id.as_deref(), Some("task-xyz"));
913            }
914            _ => panic!("expected ProverPlan::Task::IrEquiv"),
915        }
916    }
917
918    #[test]
919    fn test_prover_plan_task_parse_metadata_from_json() {
920        let json = r#"{
921            "kind": "ir-equiv",
922            "lhs_ir_file": "lhs.ir",
923            "rhs_ir_file": "rhs.ir",
924            "timeout_ms": 5000,
925            "task_id": "tid-123"
926        }"#;
927        let plan: ProverPlan = serde_json::from_str(json).unwrap();
928        match plan {
929            ProverPlan::Task {
930                task: ProverTask::IrEquiv { .. },
931                timeout_ms,
932                task_id,
933            } => {
934                assert_eq!(timeout_ms, Some(5000));
935                assert_eq!(task_id.as_deref(), Some("tid-123"));
936            }
937            _ => panic!("expected ProverPlan::Task::IrEquiv"),
938        }
939    }
940
941    #[test]
942    fn test_prover_plan_task_serialize_and_parse_behavior() {
943        let plan = ProverPlan::Task {
944            task: ProverTask::IrEquiv {
945                config: IrEquivConfig {
946                    lhs_ir_file: "lhs.ir".into(),
947                    rhs_ir_file: "rhs.ir".into(),
948                    ..Default::default()
949                },
950            },
951            timeout_ms: None,
952            task_id: None,
953        };
954        let v = serde_json::to_value(&plan).unwrap();
955        // The inner task tag is used on serialization.
956        assert_eq!(v["kind"], "ir-equiv");
957        // It can be parsed as a ProverTask...
958        let task: ProverTask = serde_json::from_value(v.clone()).unwrap();
959        match task {
960            ProverTask::IrEquiv { .. } => {}
961            _ => panic!("expected ProverTask::IrEquiv"),
962        }
963        // ...and can be parsed back as a ProverPlan::Task as well.
964        let plan2: ProverPlan = serde_json::from_value(v.clone()).unwrap();
965        match plan2 {
966            ProverPlan::Task {
967                task: ProverTask::IrEquiv { .. },
968                ..
969            } => {}
970            _ => panic!("expected ProverPlan::Task::IrEquiv"),
971        }
972    }
973}