hax_types/cli_options/
mod.rs

1use crate::prelude::*;
2
3use clap::{Parser, Subcommand, ValueEnum};
4use std::fmt;
5
6pub use hax_frontend_exporter_options::*;
7pub mod extension;
8use extension::Extension;
9
10#[derive_group(Serializers)]
11#[derive(JsonSchema, Debug, Clone, Eq, PartialEq)]
12pub enum DebugEngineMode {
13    File(PathOrDash),
14    Interactive,
15}
16
17impl std::convert::From<&str> for DebugEngineMode {
18    fn from(s: &str) -> Self {
19        match s {
20            "i" | "interactively" => DebugEngineMode::Interactive,
21            s => DebugEngineMode::File(s.strip_prefix("file:").unwrap_or(s).into()),
22        }
23    }
24}
25
26#[derive_group(Serializers)]
27#[derive(JsonSchema, Debug, Clone, Default)]
28pub struct ForceCargoBuild {
29    pub data: u64,
30}
31
32impl std::convert::From<&str> for ForceCargoBuild {
33    fn from(s: &str) -> Self {
34        use std::time::{SystemTime, UNIX_EPOCH};
35        if s == "false" {
36            let data = SystemTime::now()
37                .duration_since(UNIX_EPOCH)
38                .map(|r| r.as_millis())
39                .unwrap_or(0);
40            ForceCargoBuild { data: data as u64 }
41        } else {
42            ForceCargoBuild::default()
43        }
44    }
45}
46
47#[derive_group(Serializers)]
48#[derive(Debug, Clone, JsonSchema, Eq, PartialEq)]
49pub enum PathOrDash {
50    Dash,
51    Path(PathBuf),
52}
53
54impl std::convert::From<&str> for PathOrDash {
55    fn from(s: &str) -> Self {
56        match s {
57            "-" => PathOrDash::Dash,
58            _ => PathOrDash::Path(PathBuf::from(s)),
59        }
60    }
61}
62
63impl PathOrDash {
64    pub fn open_or_stdout(&self) -> Box<dyn std::io::Write> {
65        use std::io::BufWriter;
66        match self {
67            PathOrDash::Dash => Box::new(BufWriter::new(std::io::stdout())),
68            PathOrDash::Path(path) => {
69                Box::new(BufWriter::new(std::fs::File::create(&path).unwrap()))
70            }
71        }
72    }
73    pub fn map_path<F: FnOnce(&Path) -> PathBuf>(&self, f: F) -> Self {
74        match self {
75            PathOrDash::Path(path) => PathOrDash::Path(f(path)),
76            PathOrDash::Dash => PathOrDash::Dash,
77        }
78    }
79}
80
81fn absolute_path(path: impl AsRef<std::path::Path>) -> std::io::Result<std::path::PathBuf> {
82    use path_clean::PathClean;
83    let path = path.as_ref();
84
85    let absolute_path = if path.is_absolute() {
86        path.to_path_buf()
87    } else {
88        std::env::current_dir()?.join(path)
89    }
90    .clean();
91
92    Ok(absolute_path)
93}
94
95pub trait NormalizePaths {
96    fn normalize_paths(&mut self);
97}
98
99impl NormalizePaths for PathBuf {
100    fn normalize_paths(&mut self) {
101        *self = absolute_path(&self).unwrap();
102    }
103}
104impl NormalizePaths for PathOrDash {
105    fn normalize_paths(&mut self) {
106        match self {
107            PathOrDash::Path(p) => p.normalize_paths(),
108            PathOrDash::Dash => (),
109        }
110    }
111}
112
113#[derive_group(Serializers)]
114#[derive(JsonSchema, Parser, Debug, Hash, Clone, Eq, PartialEq)]
115pub struct ProVerifOptions {
116    /// Items for which hax should extract a default-valued process
117    /// macro with a corresponding type signature. This flag expects a
118    /// space-separated list of inclusion clauses. An inclusion clause
119    /// is a Rust path prefixed with `+`, `+!` or `-`. `-` means
120    /// implementation only, `+!` means interface only and `+` means
121    /// implementation and interface. Rust path chunks can be either a
122    /// concrete string, or a glob (just like bash globs, but with
123    /// Rust paths).
124    #[arg(
125        long,
126        value_parser = parse_inclusion_clause,
127        value_delimiter = ' ',
128        allow_hyphen_values(true)
129    )]
130    pub assume_items: Vec<InclusionClause>,
131}
132
133impl ProVerifOptions {
134    /// The flag rendering of these options, as `cargo hax extract
135    /// --dry-run` prints it. Lives next to the fields so a new or renamed
136    /// option is reflected here.
137    pub fn flags(&self) -> Vec<String> {
138        if self.assume_items.is_empty() {
139            return Vec::new();
140        }
141        std::iter::once("--assume-items".to_string())
142            .chain(self.assume_items.iter().map(ToString::to_string))
143            .collect()
144    }
145}
146
147/// The defaults of the F* flags below, shared with
148/// [`FStarOptions::defaults`] so the two cannot diverge.
149const FSTAR_DEFAULT_Z3RLIMIT: u32 = 15;
150const FSTAR_DEFAULT_FUEL: u32 = 0;
151const FSTAR_DEFAULT_IFUEL: u32 = 1;
152const FSTAR_DEFAULT_LINE_WIDTH: u16 = 100;
153
154#[derive_group(Serializers)]
155#[derive(JsonSchema, Parser, Debug, Hash, Clone, Eq, PartialEq)]
156pub struct FStarOptions {
157    /// Set the Z3 per-query resource limit
158    #[arg(long, default_value_t = FSTAR_DEFAULT_Z3RLIMIT)]
159    pub z3rlimit: u32,
160    /// Number of unrolling of recursive functions to try
161    #[arg(long, default_value_t = FSTAR_DEFAULT_FUEL)]
162    pub fuel: u32,
163    /// Number of unrolling of inductive datatypes to try
164    #[arg(long, default_value_t = FSTAR_DEFAULT_IFUEL)]
165    pub ifuel: u32,
166    /// Modules for which Hax should extract interfaces (`*.fsti`
167    /// files) in supplement to implementations (`*.fst` files). By
168    /// default we extract no interface, only implementations. If a
169    /// item is signature only (see the `+:` prefix of the
170    /// `--include_namespaces` flag of the `into` subcommand), then
171    /// its namespace is extracted with an interface. This flag
172    /// expects a space-separated list of inclusion clauses. An
173    /// inclusion clause is a Rust path prefixed with `+`, `+!` or
174    /// `-`. `-` means implementation only, `+!` means interface only
175    /// and `+` means implementation and interface. Rust path chunks
176    /// can be either a concrete string, or a glob (just like bash
177    /// globs, but with Rust paths).
178    #[arg(
179        long,
180        value_parser = parse_inclusion_clause,
181        value_delimiter = ' ',
182        allow_hyphen_values(true)
183    )]
184    pub interfaces: Vec<InclusionClause>,
185
186    #[arg(long, default_value_t = FSTAR_DEFAULT_LINE_WIDTH, env = "HAX_FSTAR_LINE_WIDTH")]
187    pub line_width: u16,
188}
189
190impl FStarOptions {
191    /// The flags' defaults. Proof scenarios resolve absent keys to these;
192    /// environment-supplied flag defaults (`HAX_FSTAR_LINE_WIDTH`)
193    /// deliberately do not apply to scenario runs.
194    pub fn defaults() -> Self {
195        Self {
196            z3rlimit: FSTAR_DEFAULT_Z3RLIMIT,
197            fuel: FSTAR_DEFAULT_FUEL,
198            ifuel: FSTAR_DEFAULT_IFUEL,
199            interfaces: Vec::new(),
200            line_width: FSTAR_DEFAULT_LINE_WIDTH,
201        }
202    }
203
204    /// The flag rendering of these options, as `cargo hax extract
205    /// --dry-run` prints it. Lives next to the fields so a new or renamed
206    /// option is reflected here.
207    pub fn flags(&self) -> Vec<String> {
208        let mut flags = vec![
209            format!("--z3rlimit={}", self.z3rlimit),
210            format!("--fuel={}", self.fuel),
211            format!("--ifuel={}", self.ifuel),
212            format!("--line-width={}", self.line_width),
213        ];
214        if !self.interfaces.is_empty() {
215            flags.push("--interfaces".to_string());
216            flags.extend(self.interfaces.iter().map(ToString::to_string));
217        }
218        flags
219    }
220}
221
222/// The inputs a proof scenario resolves for the Lean backend, carried
223/// through the `__json` re-entry rather than argv: verbatim argument
224/// arrays (no shell splitting), the compiled item selection, and the
225/// package-layout overrides. Empty on flag-driven `into` invocations.
226#[derive_group(Serializers)]
227#[derive(JsonSchema, Debug, Clone, Hash, Eq, PartialEq, Default)]
228pub struct LeanScenarioOptions {
229    /// The Lean package name, overriding the crate-name derivation.
230    pub package_name: Option<String>,
231    /// The scenario's `project-files` key, overriding the top-level key.
232    pub project_files: Option<bool>,
233    /// Charon name patterns compiled to `--start-from`.
234    pub include: Vec<String>,
235    /// Charon name patterns compiled to `--exclude`.
236    pub exclude: Vec<String>,
237    /// Charon name patterns compiled to `--opaque`, the default opaque
238    /// set already merged in.
239    pub opaque: Vec<String>,
240    /// Verbatim extra charon arguments, one element per process argument.
241    pub charon_args: Vec<String>,
242    /// Verbatim extra aeneas arguments, one element per process argument.
243    pub aeneas_args: Vec<String>,
244    /// Cargo arguments (feature selection) for the cargo invocation
245    /// charon drives.
246    pub cargo_args: Vec<String>,
247}
248
249impl LeanScenarioOptions {
250    /// The charon flags compiled from the unified item-selection keys.
251    /// Both the real charon invocation and the `--dry-run` display use
252    /// this compilation, so the two cannot diverge.
253    pub fn selection_flags(&self) -> Vec<String> {
254        [
255            ("start-from", &self.include),
256            ("exclude", &self.exclude),
257            ("opaque", &self.opaque),
258        ]
259        .into_iter()
260        .flat_map(|(flag, patterns)| {
261            patterns
262                .iter()
263                .map(move |pattern| format!("--{flag}={pattern}"))
264        })
265        .collect()
266    }
267}
268
269#[derive_group(Serializers)]
270#[derive(JsonSchema, Parser, Debug, Clone, Hash, Eq, PartialEq)]
271#[command(after_help = "\
272TOOLS:
273  This backend runs `charon`, then `aeneas`, and generates a Lean proof project.
274  Tool versions are managed by hax: each tool resolves through, in order, the
275  project's `hax.toml` (member crate, then workspace root) and the built-in
276  default version this release was tested with, and is downloaded into the
277  tool cache on demand with checksum verification.
278
279  Inspect the active versions and their sources with `cargo hax tools show`;
280  pre-install them with `cargo hax tools install`.
281
282INVOCATION:
283  The tools are run with some fixed flags (to which any --charon-args/--aeneas-args
284  are appended). Pass `-v` (`cargo hax into -v lean`) to print the exact command
285  before each tool runs.
286
287  Overriding a flag that controls where output is written (aeneas's -backend,
288  -dest, -subdir, or -split-files) may break the extraction or the generated
289  proof project. Charon's --dest-file is reserved and rejected: aeneas is always
290  run on the LLBC file hax chooses. Overriding -dest or -subdir additionally disables
291  the generation and checking of the Lean package files and the clearing of stale
292  extraction files, since hax no longer knows the package layout. Committing
293  `project-files = false` in `hax.toml` disables the package files for every
294  invocation.
295
296  To use a binary you built yourself, commit a `path` entry for the tool in
297  `hax.toml` instead of a version.")]
298pub struct LeanOptions {
299    /// Extra arguments forwarded to charon. Parsed with shell-style quoting,
300    /// so values containing spaces can be single- or double-quoted.
301    /// Example: --charon-args="--opaque '{impl Serialize for _}'"
302    #[arg(long)]
303    pub charon_args: Option<String>,
304
305    /// Extra arguments forwarded to aeneas. Parsed with shell-style quoting.
306    /// Example: --aeneas-args="-split-files"
307    #[arg(long)]
308    pub aeneas_args: Option<String>,
309
310    /// The scenario-resolved inputs; not settable from the command line.
311    #[clap(skip)]
312    pub scenario: LeanScenarioOptions,
313}
314
315#[derive_group(Serializers)]
316#[derive(JsonSchema, Subcommand, Debug, Clone, Hash, Eq, PartialEq)]
317pub enum Backend {
318    /// Use the F* backend
319    Fstar(FStarOptions),
320    /// Use the legacy Lean backend (warning: experimental)
321    LegacyLean,
322    /// Use the Lean backend (charon + aeneas pipeline)
323    Lean(LeanOptions),
324    /// Use the Coq backend
325    Coq,
326    /// Use the SSProve backend
327    Ssprove,
328    /// Use the EasyCrypt backend (warning: work in progress!)
329    Easycrypt,
330    /// Use the ProVerif backend (warning: work in progress!)
331    #[clap(alias("proverif"))]
332    ProVerif(ProVerifOptions),
333    /// Use the Rust backend (warning: work in progress!)
334    #[clap(hide = true)]
335    Rust,
336    /// Extract `DefId`s of the crate as a Rust module tree.
337    /// This is a command that regenerates code for the rust engine.
338    #[clap(hide = true)]
339    GenerateRustEngineNames,
340    /// A debugger for the Rust engine
341    Debugger {
342        #[arg(long, short)]
343        interactive: bool,
344    },
345}
346
347impl fmt::Display for Backend {
348    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349        BackendName::from(self).fmt(f)
350    }
351}
352
353#[derive_group(Serializers)]
354#[derive(JsonSchema, Debug, Hash, Clone, Eq, PartialEq)]
355pub enum DepsKind {
356    Transitive,
357    Shallow,
358    None,
359}
360
361#[derive_group(Serializers)]
362#[derive(JsonSchema, Debug, Hash, Clone, Eq, PartialEq)]
363pub enum InclusionKind {
364    /// `+query` include the items selected by `query`
365    Included(DepsKind),
366    SignatureOnly,
367    Excluded,
368}
369
370#[derive_group(Serializers)]
371#[derive(JsonSchema, Debug, Hash, Clone, Eq, PartialEq)]
372pub struct InclusionClause {
373    pub kind: InclusionKind,
374    pub namespace: Namespace,
375}
376
377const PREFIX_INCLUDED_TRANSITIVE: &str = "+";
378const PREFIX_INCLUDED_SHALLOW: &str = "+~";
379const PREFIX_INCLUDED_NONE: &str = "+!";
380const PREFIX_SIGNATURE_ONLY: &str = "+:";
381const PREFIX_EXCLUDED: &str = "-";
382
383impl ToString for InclusionClause {
384    fn to_string(&self) -> String {
385        let kind = match self.kind {
386            InclusionKind::Included(DepsKind::Transitive) => PREFIX_INCLUDED_TRANSITIVE,
387            InclusionKind::Included(DepsKind::Shallow) => PREFIX_INCLUDED_SHALLOW,
388            InclusionKind::Included(DepsKind::None) => PREFIX_INCLUDED_NONE,
389            InclusionKind::SignatureOnly => PREFIX_SIGNATURE_ONLY,
390            InclusionKind::Excluded => PREFIX_EXCLUDED,
391        };
392        format!("{kind}{}", self.namespace.to_string())
393    }
394}
395
396pub fn parse_inclusion_clause(
397    s: &str,
398) -> Result<InclusionClause, Box<dyn std::error::Error + Send + Sync + 'static>> {
399    let s = s.trim();
400    if s.is_empty() {
401        Err("Expected `-` or `+`, got an empty string")?
402    }
403    let (prefix, namespace) = {
404        let f = |&c: &char| matches!(c, '+' | '-' | '~' | '!' | ':');
405        (
406            s.chars().take_while(f).into_iter().collect::<String>(),
407            s.chars().skip_while(f).into_iter().collect::<String>(),
408        )
409    };
410    let kind = match &prefix[..] {
411        PREFIX_INCLUDED_TRANSITIVE => InclusionKind::Included(DepsKind::Transitive),
412        PREFIX_INCLUDED_SHALLOW => InclusionKind::Included(DepsKind::Shallow),
413        PREFIX_INCLUDED_NONE => InclusionKind::Included(DepsKind::None),
414        PREFIX_SIGNATURE_ONLY => InclusionKind::SignatureOnly,
415        PREFIX_EXCLUDED => InclusionKind::Excluded,
416        prefix => Err(format!(
417            "Expected `+`, `+~`, `+!`, `+:` or `-`, got an `{prefix}`"
418        ))?,
419    };
420    Ok(InclusionClause {
421        kind,
422        namespace: namespace.to_string().into(),
423    })
424}
425
426#[derive_group(Serializers)]
427#[derive(JsonSchema, Parser, Debug, Clone, Eq, PartialEq)]
428pub struct TranslationOptions {
429    /// Controls which Rust item should be extracted or not.
430    ///
431    /// This is a space-separated list of patterns prefixed with a
432    /// modifier, read from the left to the right.
433    ///
434    /// A pattern is a Rust path (say `mycrate::mymod::myfn`) where
435    /// globs are allowed: `*` matches any name
436    /// (e.g. `mycrate::mymod::myfn` is matched by
437    /// `mycrate::*::myfn`), while `**` matches any subpath, empty
438    /// included (e.g. `mycrate::mymod::myfn` is matched by
439    /// `**::myfn`).
440
441    /// By default, hax includes all items. Then, the patterns
442    /// prefixed by modifiers are processed from left to right,
443    /// excluding or including items. Each pattern selects a number of
444    /// item. The modifiers are:
445
446    /// {n}{n} - `+`: includes the selected items with their
447    /// dependencies, transitively (e.g. if function `f` calls `g`
448    /// which in turn calls `h`, then `+k::f` includes `f`, `g` and
449    /// `h`)
450
451    /// {n} - `+~`: includes the selected items with their direct
452    /// dependencies only (following the previous example, `+~k::f`
453    /// would select `f` and `g`, but not `h`)
454
455    /// {n} - `+!`: includes the selected items, without their
456    /// dependencies (`+!k::f` would only select `f`)
457
458    /// {n} - `+:`: only includes the type of the selected items (no
459    /// dependencies). This includes full struct and enums, but only
460    /// the type signature of functions and trait impls (except when
461    /// they contain associated types), dropping their bodies.
462    #[arg(
463        value_parser = parse_inclusion_clause,
464        value_delimiter = ' ',
465    )]
466    #[arg(short, allow_hyphen_values(true))]
467    pub include_namespaces: Vec<InclusionClause>,
468}
469
470#[derive_group(Serializers)]
471#[derive(JsonSchema, Parser, Debug, Clone, Eq, PartialEq)]
472pub struct BackendOptions<E: Extension> {
473    #[command(subcommand)]
474    pub backend: Backend,
475
476    /// Don't write anything on disk. Output everything as JSON to stdout
477    /// instead.
478    #[arg(long = "dry-run")]
479    pub dry_run: bool,
480
481    /// Verbose mode for the Hax engine. Set `-vv` for maximal verbosity.
482    #[arg(short, long, action = clap::ArgAction::Count)]
483    pub verbose: u8,
484
485    /// Prints statistics about how many items have been translated
486    /// successfully by the engine.
487    #[arg(long)]
488    pub stats: bool,
489
490    /// Enables profiling for the engine: for each phase of the
491    /// engine, time and memory usage are recorded and reported.
492    #[arg(long)]
493    pub profile: bool,
494
495    /// Prune Rust items that are not under the provided top-level module name.
496    /// This will effectively remove all items that don't match `*::<prune_haxmetadata>::**`.
497    /// This prunning occurs directly on the `haxmeta` file, in the frontend.
498    /// This is independent from any engine options.
499    #[arg(long)]
500    #[clap(hide = true)]
501    pub prune_haxmeta: Option<String>,
502
503    /// Enable engine debugging: dumps the AST at each phase.
504    ///
505    /// The value of `<DEBUG_ENGINE>` can be either:
506
507    /// {n}{n} - `interactive` (or `i`): enables debugging of the engine,
508    /// and visualize interactively in a webapp how a crate was
509    /// transformed by each phase, both in Rust-like syntax and
510    /// browsing directly the internal AST. By default, the webapp is
511    /// hosted on `http://localhost:8000`, the port can be override by
512    /// setting the `HAX_DEBUGGER_PORT` environment variable.
513
514    /// {n} - `<FILE>` or `file:<FILE>`: outputs the different AST as JSON
515    /// to `<FILE>`. `<FILE>` can be either [-] or a path.
516    #[arg(short, long = "debug-engine")]
517    pub debug_engine: Option<DebugEngineMode>,
518
519    /// Extract type aliases. This is disabled by default, since
520    /// extracted terms depends on expanded types rather than on type
521    /// aliases. Turning this option on is discouraged: Rust type
522    /// synonyms can ommit generic bounds, which are ususally
523    /// necessary in the hax backends, leading to typechecking
524    /// errors. For more details see
525    /// https://github.com/hacspec/hax/issues/708.
526    #[arg(long)]
527    pub extract_type_aliases: bool,
528
529    #[command(flatten)]
530    pub translation_options: TranslationOptions,
531
532    /// Where to put the output files resulting from the translation.
533    /// Defaults to "<crate folder>/proofs/<backend>/extraction".
534    #[arg(long)]
535    pub output_dir: Option<PathBuf>,
536
537    #[group(flatten)]
538    pub cli_extension: E::BackendOptions,
539}
540
541/// Cargo's hermeticity flags, applied to every cargo invocation a
542/// `cargo hax extract` run drives: project discovery, the frontend's
543/// `cargo check`, and the build charon drives.
544#[derive_group(Serializers)]
545#[derive(JsonSchema, Parser, Debug, Clone, Default, Eq, PartialEq)]
546pub struct CargoHermeticityOptions {
547    /// Assert that `Cargo.lock` will remain unchanged
548    #[arg(long)]
549    pub locked: bool,
550
551    /// Run without accessing the network
552    #[arg(long)]
553    pub offline: bool,
554
555    /// Equivalent to specifying both --locked and --offline
556    #[arg(long)]
557    pub frozen: bool,
558}
559
560impl CargoHermeticityOptions {
561    /// The cargo flags these options stand for, verbatim.
562    pub fn flags(&self) -> Vec<String> {
563        [
564            ("--locked", self.locked),
565            ("--offline", self.offline),
566            ("--frozen", self.frozen),
567        ]
568        .into_iter()
569        .filter(|(_, set)| *set)
570        .map(|(flag, _)| flag.to_string())
571        .collect()
572    }
573}
574
575#[derive_group(Serializers)]
576#[derive(JsonSchema, Subcommand, Debug, Clone, Eq, PartialEq)]
577pub enum Command<E: Extension> {
578    /// Translate to a backend. The translated modules will be written
579    /// under the directory `<PKG>/proofs/<BACKEND>/extraction`, where
580    /// `<PKG>` is the translated cargo package name and `<BACKEND>`
581    /// the name of the backend.
582    #[clap(name = "into")]
583    Backend(BackendOptions<E>),
584
585    /// Run the proof scenarios declared in `hax.toml` (`[scenario.<name>]`
586    /// tables): named, complete extraction configurations. Without names,
587    /// every scenario in scope runs. Scenarios run sequentially; a failing
588    /// scenario does not abort the run, failures are reported in a summary
589    /// and produce a non-zero exit code.
590    Extract {
591        /// The scenarios to run. Each name selects every scenario with
592        /// that name in scope. Absent, every scenario in scope runs.
593        names: Vec<String>,
594
595        /// Restrict the scope to the scenarios extracting the given
596        /// package. May be repeated.
597        #[arg(short = 'p', long = "package")]
598        packages: Vec<String>,
599
600        /// Print the resolved invocations, including the arguments
601        /// compiled from the scenarios, without running them.
602        #[arg(long)]
603        dry_run: bool,
604
605        /// Verbose mode, forwarded to each scenario run.
606        #[arg(short, long, action = clap::ArgAction::Count)]
607        verbose: u8,
608
609        #[command(flatten)]
610        hermeticity: CargoHermeticityOptions,
611    },
612
613    /// Export directly as a JSON file
614    JSON {
615        /// Path to the output JSON file, "-" denotes stdout.
616        #[arg(
617            short,
618            long = "output-file",
619            default_value = "hax_frontend_export.json"
620        )]
621        output_file: PathOrDash,
622        /// Whether the bodies are exported as THIR, built MIR, const
623        /// MIR, or a combination. Repeat this option to extract a
624        /// combination (e.g. `-k thir -k mir-built`). Pass `--kind`
625        /// alone with no value to disable body extraction.
626        #[arg(
627            value_enum,
628            short,
629            long = "kind",
630            num_args = 0..=3,
631            default_values_t = [ExportBodyKind::Thir]
632        )]
633        kind: Vec<ExportBodyKind>,
634
635        /// By default, `cargo hax json` outputs a JSON where every
636        /// piece of information is inlined. This however creates very
637        /// large JSON files. This flag enables the use of unique IDs
638        /// and outputs a map from IDs to actual objects.
639        #[arg(long)]
640        use_ids: bool,
641
642        /// Whether to include extra informations about `DefId`s.
643        #[arg(short = 'E', long = "include-extra", default_value = "false")]
644        include_extra: bool,
645    },
646
647    /// Serialize to a `haxmeta` file, the internal binary format used by hax to
648    /// store the ASTs produced by the hax exporter.
649    #[clap(hide = true)]
650    Serialize {
651        /// Whether the bodies are exported as THIR, built MIR, const
652        /// MIR, or a combination. Repeat this option to extract a
653        /// combination (e.g. `-k thir -k mir-built`). Pass `--kind`
654        /// alone with no value to disable body extraction.
655        #[arg(
656            value_enum,
657            short,
658            long = "kind",
659            num_args = 0..=3,
660            default_values_t = [ExportBodyKind::Thir]
661        )]
662        kind: Vec<ExportBodyKind>,
663
664        /// When extracting to a given backend, the exporter is called with different `cfg` options.
665        /// This option allows to set the same flags as `cargo hax into` would pick.
666        #[arg(short)]
667        backend: Option<BackendName>,
668    },
669
670    /// Manage the external tools hax depends on (e.g. charon and aeneas).
671    #[command(subcommand)]
672    Tools(ToolsCommand),
673
674    #[command(flatten)]
675    CliExtension(E::Command),
676}
677
678/// Subcommands of `cargo hax tools`.
679#[derive_group(Serializers)]
680#[derive(JsonSchema, Subcommand, Debug, Clone, Eq, PartialEq)]
681pub enum ToolsCommand {
682    /// Download and cache the tool versions the current project
683    /// resolves to, or a specific `<tool>@<version>`.
684    Install {
685        /// A `<tool>@<version>` specification (e.g.
686        /// `charon@nightly-2026.07.01`) to install into the
687        /// machine-wide cache. When absent, installs what the current
688        /// project's configuration resolves to.
689        spec: Option<String>,
690        /// Re-download and verify even if the version is already cached
691        /// (e.g. to verify a copy installed before its checksum shipped).
692        #[arg(long)]
693        force: bool,
694    },
695    /// List the tool versions this release of hax can install with
696    /// checksum verification.
697    List {
698        /// Restrict the listing to one tool.
699        tool: Option<String>,
700        /// Only show versions present in the local cache.
701        #[arg(long)]
702        installed: bool,
703        /// Show every version instead of only the most recent ones.
704        #[arg(long)]
705        all: bool,
706    },
707    /// Show which tool versions are active in the current project,
708    /// and where each one comes from.
709    Show,
710    /// Remove a tool version from the machine-wide cache.
711    Remove {
712        /// The `<tool>@<version>` specification (e.g.
713        /// `charon@nightly-2026.07.01`) to remove from the cache.
714        spec: String,
715    },
716    /// Delete the entire tool cache. Later runs download what they need
717    /// again.
718    Clean,
719    /// Write version pins into the project's `hax.toml`, creating the
720    /// file when missing.
721    Pin {
722        /// A `<name>@<version>` specification to write as one entry,
723        /// accepting managed tools (e.g. `charon@nightly-2026.07.01`)
724        /// and declared versions (e.g. `lean@leanprover/lean4:v4.31.0`).
725        /// When absent, pins the built-in defaults of this release.
726        spec: Option<String>,
727    },
728}
729
730impl<E: Extension> Command<E> {
731    pub fn body_kinds(&self) -> Vec<ExportBodyKind> {
732        match self {
733            Command::JSON { kind, .. } => kind.clone(),
734            Command::Serialize { kind, .. } => kind.clone(),
735            Command::Backend { .. }
736            | Command::Extract { .. }
737            | Command::Tools { .. }
738            | Command::CliExtension { .. } => {
739                vec![ExportBodyKind::Thir]
740            }
741        }
742    }
743    pub fn backend_name(&self) -> Option<BackendName> {
744        match self {
745            Command::Backend(backend_options) => Some((&backend_options.backend).into()),
746            Command::JSON { .. } => None,
747            Command::Serialize { backend, .. } => backend.clone(),
748            Command::Extract { .. } => None,
749            Command::Tools(_) => None,
750            Command::CliExtension(_) => None,
751        }
752    }
753}
754
755#[derive_group(Serializers)]
756#[derive(JsonSchema, ValueEnum, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
757pub enum ExportBodyKind {
758    Thir,
759    MirBuilt,
760}
761
762#[derive_group(Serializers)]
763#[derive(JsonSchema, Parser, Debug, Clone)]
764#[command(
765    author,
766    version = crate::HAX_VERSION,
767    long_version = concat!("\nversion=", env!("HAX_VERSION"), "\n", "commit=", env!("HAX_GIT_COMMIT_HASH")),
768    name = "hax",
769    about,
770    long_about = None
771)]
772pub struct ExtensibleOptions<E: Extension> {
773    /// Semi-colon terminated list of arguments to pass to the
774    /// `cargo build` invocation. For example, to apply this
775    /// program on a package `foo`, use `-C -p foo ;`. (make sure
776    /// to escape `;` correctly in your shell)
777    #[arg(default_values = Vec::<&str>::new(), short='C', allow_hyphen_values=true, num_args=1.., long="cargo-args", value_terminator=";")]
778    pub cargo_flags: Vec<String>,
779
780    #[command(subcommand)]
781    pub command: Command<E>,
782
783    /// `cargo` caching is enable by default, this flag disables it.
784    #[arg(long="disable-cargo-cache", action=clap::builder::ArgAction::SetFalse)]
785    pub force_cargo_build: ForceCargoBuild,
786
787    /// Apply the command to every local package of the dependency closure. By
788    /// default, the command is only applied to the primary packages (i.e. the
789    /// package(s) of the current directory, or the ones selected with cargo
790    /// options like `-C -p <PKG> ;`).
791    #[arg(long = "deps")]
792    pub deps: bool,
793
794    /// Provide a precomputed haxmeta file explicitly.
795    /// Setting this option bypasses rustc and the exporter altogether.
796    #[arg(long)]
797    #[clap(hide = true)]
798    pub haxmeta: Option<PathBuf>,
799
800    /// By default, hax uses `$CARGO_TARGET_DIR/hax` as target folder,
801    /// to avoid recompilation when working both with `cargo hax` and
802    /// `cargo build` (or, e.g. `rust-analyzer`). This option disables
803    /// this behavior.
804    #[arg(long)]
805    pub no_custom_target_directory: bool,
806
807    /// Diagnostic format. Sets `cargo`'s `--message-format` as well,
808    /// if not present.
809    #[arg(long, default_value = "human")]
810    pub message_format: MessageFormat,
811
812    /// Enables experimental FullDef format for items exported from the frontend
813    /// in the haxmeta file.
814    #[arg(long, env = "HAX_EXPERIMENTAL_FULL_DEF")]
815    pub experimental_full_def: bool,
816
817    #[group(flatten)]
818    pub extension: E::Options,
819}
820
821pub type Options = ExtensibleOptions<()>;
822
823#[derive_group(Serializers)]
824#[derive(JsonSchema, ValueEnum, Debug, Clone, Copy, Eq, PartialEq)]
825pub enum MessageFormat {
826    Human,
827    Json,
828}
829
830impl<E: Extension> NormalizePaths for Command<E> {
831    fn normalize_paths(&mut self) {
832        use Command::*;
833        match self {
834            JSON { output_file, .. } => output_file.normalize_paths(),
835            _ => (),
836        }
837    }
838}
839
840impl NormalizePaths for Options {
841    fn normalize_paths(&mut self) {
842        self.command.normalize_paths()
843    }
844}
845
846impl From<Options> for hax_frontend_exporter_options::Options {
847    fn from(_opts: Options) -> hax_frontend_exporter_options::Options {
848        hax_frontend_exporter_options::Options {
849            inline_anon_consts: true,
850            bounds_options: hax_frontend_exporter_options::BoundsOptions {
851                resolve_destruct: false,
852                prune_sized: true,
853            },
854            item_ref_use_concrete_impl: false,
855        }
856    }
857}
858
859/// The subset of `Options` the frontend is sensible to.
860#[derive_group(Serializers)]
861#[derive(JsonSchema, Debug, Clone)]
862pub struct ExporterOptions {
863    pub deps: bool,
864    pub force_cargo_build: ForceCargoBuild,
865    /// When exporting, the driver sets `--cfg hax_backend_{backkend}`, thus we need this information.
866    pub backend: Option<BackendName>,
867    pub body_kinds: Vec<ExportBodyKind>,
868    pub experimental_full_def: bool,
869}
870
871#[derive_group(Serializers)]
872#[derive(JsonSchema, ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Hash)]
873pub enum BackendName {
874    Fstar,
875    Coq,
876    Ssprove,
877    Easycrypt,
878    #[clap(alias("proverif"))]
879    ProVerif,
880    LegacyLean,
881    Lean,
882    Rust,
883    GenerateRustEngineNames,
884    Debugger,
885}
886
887impl BackendName {
888    /// The subdirectory holding the backend's extraction, below the output
889    /// directory. The engine backends keep an `extraction/` level; the Lean
890    /// backend writes its package at the top of the output directory.
891    pub fn output_subdir(self) -> Option<&'static str> {
892        match self {
893            Self::Lean => None,
894            _ => Some("extraction"),
895        }
896    }
897
898    pub fn iter() -> impl Iterator<Item = Self> {
899        [
900            Self::Fstar,
901            Self::Coq,
902            Self::Ssprove,
903            Self::Easycrypt,
904            Self::ProVerif,
905            Self::LegacyLean,
906            Self::Lean,
907            Self::Rust,
908            Self::GenerateRustEngineNames,
909        ]
910        .into_iter()
911    }
912}
913
914impl fmt::Display for BackendName {
915    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
916        let name = match self {
917            BackendName::Fstar => "fstar",
918            BackendName::Coq => "coq",
919            BackendName::Ssprove => "ssprove",
920            BackendName::Easycrypt => "easycrypt",
921            BackendName::ProVerif => "proverif",
922            BackendName::LegacyLean => "legacy-lean",
923            BackendName::Lean => "lean",
924            BackendName::Rust => "rust",
925            BackendName::GenerateRustEngineNames => "generate_rust_engine_names",
926            BackendName::Debugger => "debugger",
927        };
928        write!(f, "{name}")
929    }
930}
931
932impl From<&Options> for ExporterOptions {
933    fn from(options: &Options) -> Self {
934        ExporterOptions {
935            deps: options.deps,
936            force_cargo_build: options.force_cargo_build.clone(),
937            backend: options.command.backend_name(),
938            body_kinds: options.command.body_kinds(),
939            experimental_full_def: options.experimental_full_def,
940        }
941    }
942}
943
944impl From<&Backend> for BackendName {
945    fn from(backend: &Backend) -> Self {
946        match backend {
947            Backend::Fstar { .. } => BackendName::Fstar,
948            Backend::Coq { .. } => BackendName::Coq,
949            Backend::Ssprove { .. } => BackendName::Ssprove,
950            Backend::Easycrypt { .. } => BackendName::Easycrypt,
951            Backend::ProVerif { .. } => BackendName::ProVerif,
952            Backend::LegacyLean { .. } => BackendName::LegacyLean,
953            Backend::Lean { .. } => BackendName::Lean,
954            Backend::Rust { .. } => BackendName::Rust,
955            Backend::GenerateRustEngineNames { .. } => BackendName::GenerateRustEngineNames,
956            Backend::Debugger { .. } => BackendName::Debugger,
957        }
958    }
959}
960
961pub const ENV_VAR_OPTIONS_FRONTEND: &str = "DRIVER_HAX_FRONTEND_OPTS";
962pub const ENV_VAR_OPTIONS_FULL: &str = "DRIVER_HAX_FRONTEND_FULL_OPTS";
963
964#[cfg(test)]
965mod tests {
966    use super::*;
967
968    /// `flags()` renders the options for `extract --dry-run`. The
969    /// destructuring makes a new field a compile error here, and each field
970    /// is asserted to reach the rendering, which nothing else enforces.
971    #[test]
972    fn fstar_flags_render_every_field() {
973        let options = FStarOptions {
974            z3rlimit: 111,
975            fuel: 222,
976            ifuel: 333,
977            interfaces: vec![parse_inclusion_clause("+**::foo").unwrap()],
978            line_width: 444,
979        };
980        let FStarOptions {
981            z3rlimit,
982            fuel,
983            ifuel,
984            interfaces,
985            line_width,
986        } = &options;
987        let flags = options.flags().join(" ");
988        assert!(flags.contains(&format!("--z3rlimit={z3rlimit}")), "{flags}");
989        assert!(flags.contains(&format!("--fuel={fuel}")), "{flags}");
990        assert!(flags.contains(&format!("--ifuel={ifuel}")), "{flags}");
991        assert!(
992            flags.contains(&format!("--line-width={line_width}")),
993            "{flags}"
994        );
995        assert!(flags.contains("--interfaces"), "{flags}");
996        for clause in interfaces {
997            assert!(flags.contains(&clause.to_string()), "{flags}");
998        }
999    }
1000
1001    #[test]
1002    fn proverif_flags_render_every_field() {
1003        let options = ProVerifOptions {
1004            assume_items: vec![parse_inclusion_clause("+**::bar").unwrap()],
1005        };
1006        let ProVerifOptions { assume_items } = &options;
1007        let flags = options.flags().join(" ");
1008        assert!(flags.contains("--assume-items"), "{flags}");
1009        for clause in assume_items {
1010            assert!(flags.contains(&clause.to_string()), "{flags}");
1011        }
1012    }
1013
1014    /// The Lean backend writes its package at the top of the output
1015    /// directory; every engine backend keeps an `extraction/` level.
1016    #[test]
1017    fn only_the_lean_backend_has_no_extraction_subdir() {
1018        for backend in BackendName::iter() {
1019            let subdir = backend.output_subdir();
1020            if backend == BackendName::Lean {
1021                assert_eq!(subdir, None, "{backend}");
1022            } else {
1023                assert_eq!(subdir, Some("extraction"), "{backend}");
1024            }
1025        }
1026    }
1027}