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)]
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)]
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, Clone)]
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
133#[derive_group(Serializers)]
134#[derive(JsonSchema, Parser, Debug, Clone)]
135pub struct FStarOptions<E: Extension> {
136    /// Set the Z3 per-query resource limit
137    #[arg(long, default_value = "15")]
138    pub z3rlimit: u32,
139    /// Number of unrolling of recursive functions to try
140    #[arg(long, default_value = "0")]
141    pub fuel: u32,
142    /// Number of unrolling of inductive datatypes to try
143    #[arg(long, default_value = "1")]
144    pub ifuel: u32,
145    /// Modules for which Hax should extract interfaces (`*.fsti`
146    /// files) in supplement to implementations (`*.fst` files). By
147    /// default we extract no interface, only implementations. If a
148    /// item is signature only (see the `+:` prefix of the
149    /// `--include_namespaces` flag of the `into` subcommand), then
150    /// its namespace is extracted with an interface. This flag
151    /// expects a space-separated list of inclusion clauses. An
152    /// inclusion clause is a Rust path prefixed with `+`, `+!` or
153    /// `-`. `-` means implementation only, `+!` means interface only
154    /// and `+` means implementation and interface. Rust path chunks
155    /// can be either a concrete string, or a glob (just like bash
156    /// globs, but with Rust paths).
157    #[arg(
158        long,
159        value_parser = parse_inclusion_clause,
160        value_delimiter = ' ',
161        allow_hyphen_values(true)
162    )]
163    pub interfaces: Vec<InclusionClause>,
164
165    #[arg(long, default_value = "100", env = "HAX_FSTAR_LINE_WIDTH")]
166    pub line_width: u16,
167
168    #[group(flatten)]
169    pub cli_extension: E::FStarOptions,
170}
171
172#[derive_group(Serializers)]
173#[derive(JsonSchema, Subcommand, Debug, Clone)]
174pub enum Backend<E: Extension> {
175    /// Use the F* backend
176    Fstar(FStarOptions<E>),
177    /// Use the Coq backend
178    Coq,
179    /// Use the SSProve backend
180    Ssprove,
181    /// Use the EasyCrypt backend (warning: work in progress!)
182    Easycrypt,
183    /// Use the ProVerif backend (warning: work in progress!)
184    ProVerif(ProVerifOptions),
185    /// Use the Lean backend (warning: work in progress!)
186    #[clap(hide = true)]
187    Lean,
188    /// Extract `DefId`s of the crate as a Rust module tree.
189    /// This is a command that regenerates code for the rust engine.
190    #[clap(hide = true)]
191    GenerateRustEngineNames,
192}
193
194impl fmt::Display for Backend<()> {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        match self {
197            Backend::Fstar { .. } => write!(f, "fstar"),
198            Backend::Coq { .. } => write!(f, "coq"),
199            Backend::Ssprove { .. } => write!(f, "ssprove"),
200            Backend::Easycrypt { .. } => write!(f, "easycrypt"),
201            Backend::ProVerif { .. } => write!(f, "proverif"),
202            Backend::Lean { .. } => write!(f, "lean"),
203            Backend::GenerateRustEngineNames { .. } => write!(f, "generate_rust_engine_names"),
204        }
205    }
206}
207
208#[derive_group(Serializers)]
209#[derive(JsonSchema, Debug, Clone)]
210pub enum DepsKind {
211    Transitive,
212    Shallow,
213    None,
214}
215
216#[derive_group(Serializers)]
217#[derive(JsonSchema, Debug, Clone)]
218pub enum InclusionKind {
219    /// `+query` include the items selected by `query`
220    Included(DepsKind),
221    SignatureOnly,
222    Excluded,
223}
224
225#[derive_group(Serializers)]
226#[derive(JsonSchema, Debug, Clone)]
227pub struct InclusionClause {
228    pub kind: InclusionKind,
229    pub namespace: Namespace,
230}
231
232const PREFIX_INCLUDED_TRANSITIVE: &str = "+";
233const PREFIX_INCLUDED_SHALLOW: &str = "+~";
234const PREFIX_INCLUDED_NONE: &str = "+!";
235const PREFIX_SIGNATURE_ONLY: &str = "+:";
236const PREFIX_EXCLUDED: &str = "-";
237
238impl ToString for InclusionClause {
239    fn to_string(&self) -> String {
240        let kind = match self.kind {
241            InclusionKind::Included(DepsKind::Transitive) => PREFIX_INCLUDED_TRANSITIVE,
242            InclusionKind::Included(DepsKind::Shallow) => PREFIX_INCLUDED_SHALLOW,
243            InclusionKind::Included(DepsKind::None) => PREFIX_INCLUDED_NONE,
244            InclusionKind::SignatureOnly => PREFIX_SIGNATURE_ONLY,
245            InclusionKind::Excluded => PREFIX_EXCLUDED,
246        };
247        format!("{kind}{}", self.namespace.to_string())
248    }
249}
250
251pub fn parse_inclusion_clause(
252    s: &str,
253) -> Result<InclusionClause, Box<dyn std::error::Error + Send + Sync + 'static>> {
254    let s = s.trim();
255    if s.is_empty() {
256        Err("Expected `-` or `+`, got an empty string")?
257    }
258    let (prefix, namespace) = {
259        let f = |&c: &char| matches!(c, '+' | '-' | '~' | '!' | ':');
260        (
261            s.chars().take_while(f).into_iter().collect::<String>(),
262            s.chars().skip_while(f).into_iter().collect::<String>(),
263        )
264    };
265    let kind = match &prefix[..] {
266        PREFIX_INCLUDED_TRANSITIVE => InclusionKind::Included(DepsKind::Transitive),
267        PREFIX_INCLUDED_SHALLOW => InclusionKind::Included(DepsKind::Shallow),
268        PREFIX_INCLUDED_NONE => InclusionKind::Included(DepsKind::None),
269        PREFIX_SIGNATURE_ONLY => InclusionKind::SignatureOnly,
270        PREFIX_EXCLUDED => InclusionKind::Excluded,
271        prefix => Err(format!(
272            "Expected `+`, `+~`, `+!`, `+:` or `-`, got an `{prefix}`"
273        ))?,
274    };
275    Ok(InclusionClause {
276        kind,
277        namespace: namespace.to_string().into(),
278    })
279}
280
281#[derive_group(Serializers)]
282#[derive(JsonSchema, Parser, Debug, Clone)]
283pub struct TranslationOptions {
284    /// Controls which Rust item should be extracted or not.
285    ///
286    /// This is a space-separated list of patterns prefixed with a
287    /// modifier, read from the left to the right.
288    ///
289    /// A pattern is a Rust path (say `mycrate::mymod::myfn`) where
290    /// globs are allowed: `*` matches any name
291    /// (e.g. `mycrate::mymod::myfn` is matched by
292    /// `mycrate::*::myfn`), while `**` matches any subpath, empty
293    /// included (e.g. `mycrate::mymod::myfn` is matched by
294    /// `**::myfn`).
295
296    /// By default, hax includes all items. Then, the patterns
297    /// prefixed by modifiers are processed from left to right,
298    /// excluding or including items. Each pattern selects a number of
299    /// item. The modifiers are:
300
301    /// {n}{n} - `+`: includes the selected items with their
302    /// dependencies, transitively (e.g. if function `f` calls `g`
303    /// which in turn calls `h`, then `+k::f` includes `f`, `g` and
304    /// `h`)
305
306    /// {n} - `+~`: includes the selected items with their direct
307    /// dependencies only (following the previous example, `+~k::f`
308    /// would select `f` and `g`, but not `h`)
309
310    /// {n} - `+!`: includes the selected items, without their
311    /// dependencies (`+!k::f` would only select `f`)
312
313    /// {n} - `+:`: only includes the type of the selected items (no
314    /// dependencies). This includes full struct and enums, but only
315    /// the type signature of functions and trait impls (except when
316    /// they contain associated types), dropping their bodies.
317    #[arg(
318        value_parser = parse_inclusion_clause,
319        value_delimiter = ' ',
320    )]
321    #[arg(short, allow_hyphen_values(true))]
322    pub include_namespaces: Vec<InclusionClause>,
323}
324
325#[derive_group(Serializers)]
326#[derive(JsonSchema, Parser, Debug, Clone)]
327pub struct BackendOptions<E: Extension> {
328    #[command(subcommand)]
329    pub backend: Backend<E>,
330
331    /// Don't write anything on disk. Output everything as JSON to stdout
332    /// instead.
333    #[arg(long = "dry-run")]
334    pub dry_run: bool,
335
336    /// Verbose mode for the Hax engine. Set `-vv` for maximal verbosity.
337    #[arg(short, long, action = clap::ArgAction::Count)]
338    pub verbose: u8,
339
340    /// Prints statistics about how many items have been translated
341    /// successfully by the engine.
342    #[arg(long)]
343    pub stats: bool,
344
345    /// Enables profiling for the engine: for each phase of the
346    /// engine, time and memory usage are recorded and reported.
347    #[arg(long)]
348    pub profile: bool,
349
350    /// Enable engine debugging: dumps the AST at each phase.
351    ///
352    /// The value of `<DEBUG_ENGINE>` can be either:
353
354    /// {n}{n} - `interactive` (or `i`): enables debugging of the engine,
355    /// and visualize interactively in a webapp how a crate was
356    /// transformed by each phase, both in Rust-like syntax and
357    /// browsing directly the internal AST. By default, the webapp is
358    /// hosted on `http://localhost:8000`, the port can be override by
359    /// setting the `HAX_DEBUGGER_PORT` environment variable.
360
361    /// {n} - `<FILE>` or `file:<FILE>`: outputs the different AST as JSON
362    /// to `<FILE>`. `<FILE>` can be either [-] or a path.
363    #[arg(short, long = "debug-engine")]
364    pub debug_engine: Option<DebugEngineMode>,
365
366    /// Extract type aliases. This is disabled by default, since
367    /// extracted terms depends on expanded types rather than on type
368    /// aliases. Turning this option on is discouraged: Rust type
369    /// synonyms can ommit generic bounds, which are ususally
370    /// necessary in the hax backends, leading to typechecking
371    /// errors. For more details see
372    /// https://github.com/hacspec/hax/issues/708.
373    #[arg(long)]
374    pub extract_type_aliases: bool,
375
376    #[command(flatten)]
377    pub translation_options: TranslationOptions,
378
379    /// Where to put the output files resulting from the translation.
380    /// Defaults to "<crate folder>/proofs/<backend>/extraction".
381    #[arg(long)]
382    pub output_dir: Option<PathBuf>,
383
384    #[group(flatten)]
385    pub cli_extension: E::BackendOptions,
386}
387
388#[derive_group(Serializers)]
389#[derive(JsonSchema, Subcommand, Debug, Clone)]
390pub enum Command<E: Extension> {
391    /// Translate to a backend. The translated modules will be written
392    /// under the directory `<PKG>/proofs/<BACKEND>/extraction`, where
393    /// `<PKG>` is the translated cargo package name and `<BACKEND>`
394    /// the name of the backend.
395    #[clap(name = "into")]
396    Backend(BackendOptions<E>),
397
398    /// Export directly as a JSON file
399    JSON {
400        /// Path to the output JSON file, "-" denotes stdout.
401        #[arg(
402            short,
403            long = "output-file",
404            default_value = "hax_frontend_export.json"
405        )]
406        output_file: PathOrDash,
407        /// Whether the bodies are exported as THIR, built MIR, const
408        /// MIR, or a combination. Repeat this option to extract a
409        /// combination (e.g. `-k thir -k mir-built`). Pass `--kind`
410        /// alone with no value to disable body extraction.
411        #[arg(
412            value_enum,
413            short,
414            long = "kind",
415            num_args = 0..=3,
416            default_values_t = [ExportBodyKind::Thir]
417        )]
418        kind: Vec<ExportBodyKind>,
419
420        /// By default, `cargo hax json` outputs a JSON where every
421        /// piece of information is inlined. This however creates very
422        /// large JSON files. This flag enables the use of unique IDs
423        /// and outputs a map from IDs to actual objects.
424        #[arg(long)]
425        use_ids: bool,
426
427        /// Whether to include extra informations about `DefId`s.
428        #[arg(short = 'E', long = "include-extra", default_value = "false")]
429        include_extra: bool,
430    },
431
432    #[command(flatten)]
433    CliExtension(E::Command),
434}
435
436impl<E: Extension> Command<E> {
437    pub fn body_kinds(&self) -> Vec<ExportBodyKind> {
438        match self {
439            Command::JSON { kind, .. } => kind.clone(),
440            _ => vec![ExportBodyKind::Thir],
441        }
442    }
443}
444
445#[derive_group(Serializers)]
446#[derive(JsonSchema, ValueEnum, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
447pub enum ExportBodyKind {
448    Thir,
449    MirBuilt,
450}
451
452#[derive_group(Serializers)]
453#[derive(JsonSchema, Parser, Debug, Clone)]
454#[command(
455    author,
456    version = crate::HAX_VERSION,
457    long_version = concat!("\nversion=", env!("HAX_VERSION"), "\n", "commit=", env!("HAX_GIT_COMMIT_HASH")),
458    name = "hax",
459    about,
460    long_about = None
461)]
462pub struct ExtensibleOptions<E: Extension> {
463    /// Replace the expansion of each macro matching PATTERN by their
464    /// invocation. PATTERN denotes a rust path (i.e. `A::B::c`) in
465    /// which glob patterns are allowed. The glob pattern * matches
466    /// any name, the glob pattern ** matches zero, one or more
467    /// names. For instance, `A::B::C::D::X` and `A::E::F::D::Y`
468    /// matches `A::**::D::*`.
469    #[arg(
470        short = 'i',
471        long = "inline-macro-call",
472        value_name = "PATTERN",
473        value_parser,
474        value_delimiter = ',',
475        default_values = [
476            "hacspec_lib::array::array", "hacspec_lib::array::public_bytes", "hacspec_lib::array::bytes",
477            "hacspec_lib::math_integers::public_nat_mod", "hacspec_lib::math_integers::unsigned_public_integer",
478        ],
479    )]
480    pub inline_macro_calls: Vec<Namespace>,
481
482    /// Semi-colon terminated list of arguments to pass to the
483    /// `cargo build` invocation. For example, to apply this
484    /// program on a package `foo`, use `-C -p foo ;`. (make sure
485    /// to escape `;` correctly in your shell)
486    #[arg(default_values = Vec::<&str>::new(), short='C', allow_hyphen_values=true, num_args=1.., long="cargo-args", value_terminator=";")]
487    pub cargo_flags: Vec<String>,
488
489    #[command(subcommand)]
490    pub command: Command<E>,
491
492    /// `cargo` caching is enable by default, this flag disables it.
493    #[arg(long="disable-cargo-cache", action=clap::builder::ArgAction::SetFalse)]
494    pub force_cargo_build: ForceCargoBuild,
495
496    /// Apply the command to every local package of the dependency closure. By
497    /// default, the command is only applied to the primary packages (i.e. the
498    /// package(s) of the current directory, or the ones selected with cargo
499    /// options like `-C -p <PKG> ;`).
500    #[arg(long = "deps")]
501    pub deps: bool,
502
503    /// By default, hax uses `$CARGO_TARGET_DIR/hax` as target folder,
504    /// to avoid recompilation when working both with `cargo hax` and
505    /// `cargo build` (or, e.g. `rust-analyzer`). This option disables
506    /// this behavior.
507    #[arg(long)]
508    pub no_custom_target_directory: bool,
509
510    /// Diagnostic format. Sets `cargo`'s `--message-format` as well,
511    /// if not present.
512    #[arg(long, default_value = "human")]
513    pub message_format: MessageFormat,
514
515    #[group(flatten)]
516    pub extension: E::Options,
517}
518
519pub type Options = ExtensibleOptions<()>;
520
521#[derive_group(Serializers)]
522#[derive(JsonSchema, ValueEnum, Debug, Clone, Copy, Eq, PartialEq)]
523pub enum MessageFormat {
524    Human,
525    Json,
526}
527
528impl<E: Extension> NormalizePaths for Command<E> {
529    fn normalize_paths(&mut self) {
530        use Command::*;
531        match self {
532            JSON { output_file, .. } => output_file.normalize_paths(),
533            _ => (),
534        }
535    }
536}
537
538impl NormalizePaths for Options {
539    fn normalize_paths(&mut self) {
540        self.command.normalize_paths()
541    }
542}
543
544impl From<Options> for hax_frontend_exporter_options::Options {
545    fn from(_opts: Options) -> hax_frontend_exporter_options::Options {
546        hax_frontend_exporter_options::Options {
547            inline_anon_consts: true,
548            bounds_options: hax_frontend_exporter_options::BoundsOptions {
549                resolve_drop: false,
550                prune_sized: true,
551            },
552        }
553    }
554}
555
556pub const ENV_VAR_OPTIONS_FRONTEND: &str = "DRIVER_HAX_FRONTEND_OPTS";