use std::str::FromStr;
use clap::{Parser as ClapParser, Subcommand, ValueEnum};
#[derive(ClapParser)]
#[command(name = "aver", version, about = "The Aver language toolchain")]
pub(super) struct Cli {
#[command(subcommand)]
pub(super) command: Commands,
}
#[derive(Clone, Debug, Default, ValueEnum)]
pub(super) enum ProofBackend {
#[default]
#[value(name = "lean")]
Lean,
#[value(name = "dafny")]
Dafny,
}
#[derive(Clone, Debug, ValueEnum)]
pub(super) enum ProofVerifyMode {
#[value(name = "auto")]
Auto,
#[value(name = "sorry")]
Sorry,
#[value(name = "theorem-skeleton")]
TheoremSkeleton,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub(super) enum WasmBridge {
None,
#[value(name = "wasip1")]
Wasip1,
Fetch,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub(super) enum DeployPack {
None,
Cloudflare,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub(super) enum DeployPreset {
Cloudflare,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub(super) enum WasmRuntimeArtifact {
#[value(name = "runtime")]
Runtime,
#[value(name = "wasi-bridge")]
WasiBridge,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub(super) enum WasmOptMode {
#[value(name = "speed")]
O3,
#[value(name = "size")]
Oz,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub(super) enum CompileTarget {
#[default]
#[value(name = "rust")]
Rust,
#[value(name = "wasm")]
Wasm,
#[value(name = "edge-wasm")]
EdgeWasm,
#[value(name = "wasm-gc")]
WasmGc,
}
impl CompileTarget {
pub(super) fn needs_wasm_pipeline(self) -> bool {
matches!(
self,
CompileTarget::Wasm | CompileTarget::EdgeWasm | CompileTarget::WasmGc
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub(super) enum CompilePolicyMode {
#[value(name = "embed")]
Embed,
#[value(name = "runtime")]
Runtime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ContextDepth {
Auto,
Unlimited,
Limited(usize),
}
impl FromStr for ContextDepth {
type Err = String;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let value = input.trim().to_ascii_lowercase();
match value.as_str() {
"auto" => Ok(Self::Auto),
"unlimited" => Ok(Self::Unlimited),
_ => value
.parse::<usize>()
.map(Self::Limited)
.map_err(|_| "expected auto, unlimited, or a non-negative integer".to_string()),
}
}
}
pub(super) fn parse_context_budget(input: &str) -> Result<usize, String> {
let value = input.trim().to_ascii_lowercase();
let (number, multiplier) = if let Some(raw) = value.strip_suffix("kb") {
(raw.trim(), 1024usize)
} else if let Some(raw) = value.strip_suffix("mb") {
(raw.trim(), 1024usize * 1024)
} else if let Some(raw) = value.strip_suffix('b') {
(raw.trim(), 1usize)
} else {
(value.as_str(), 1usize)
};
let amount = number
.parse::<usize>()
.map_err(|_| "expected a byte size like 8192, 10kb, or 1mb".to_string())?;
amount
.checked_mul(multiplier)
.ok_or_else(|| "budget is too large".to_string())
}
#[derive(Subcommand)]
pub(super) enum Commands {
Run {
file: String,
#[arg(long)]
module_root: Option<String>,
#[arg(long)]
verify: bool,
#[arg(long)]
record: Option<String>,
#[arg(short = 'e', long = "expr", value_name = "CALL_EXPR")]
expr: Vec<String>,
#[arg(long = "input-file", value_name = "PATH", conflicts_with = "expr")]
input_file: Option<String>,
#[arg(long, conflicts_with = "profile")]
self_host: bool,
#[arg(long)]
profile: bool,
#[arg(long, conflicts_with_all = ["self_host", "profile", "wasm_gc"])]
wasm: bool,
#[arg(long = "wasm-gc", conflicts_with_all = ["self_host", "profile", "wasm"])]
wasm_gc: bool,
#[arg(last = true)]
program_args: Vec<String>,
},
Check {
file: String,
#[arg(long)]
module_root: Option<String>,
#[arg(long)]
deps: bool,
#[arg(long)]
verbose: bool,
#[arg(long)]
json: bool,
},
Verify {
file: String,
#[arg(long)]
module_root: Option<String>,
#[arg(long)]
deps: bool,
#[arg(long)]
verbose: bool,
#[arg(long)]
json: bool,
#[arg(long)]
hostile: bool,
},
Audit {
#[arg(default_value = ".")]
path: String,
#[arg(long)]
module_root: Option<String>,
#[arg(long)]
json: bool,
#[arg(long)]
hostile: bool,
},
Format {
#[arg(default_value = ".")]
path: String,
#[arg(long)]
check: bool,
#[arg(long)]
json: bool,
},
Replay {
recording: String,
#[arg(long)]
diff: bool,
#[arg(long)]
test: bool,
#[arg(long = "check-args")]
check_args: bool,
#[arg(long, conflicts_with = "wasm_gc")]
self_host: bool,
#[arg(long = "wasm-gc")]
wasm_gc: bool,
#[arg(long)]
json: bool,
},
Repl,
Context {
file: String,
#[arg(long)]
module_root: Option<String>,
#[arg(short = 'o', long)]
output: Option<String>,
#[arg(long)]
json: bool,
#[arg(long)]
decisions_only: bool,
#[arg(long)]
focus: Option<String>,
#[arg(long, default_value = "auto")]
depth: ContextDepth,
#[arg(long, default_value = "10kb", value_parser = parse_context_budget)]
budget: usize,
},
Compile {
file: String,
#[arg(short = 'o', long, default_value = "out")]
output: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
module_root: Option<String>,
#[arg(long, default_value = "rust")]
target: CompileTarget,
#[arg(long)]
with_replay: bool,
#[arg(long = "policy", value_enum)]
policy: Option<CompilePolicyMode>,
#[arg(long)]
guest_entry: Option<String>,
#[arg(long)]
with_self_host_support: bool,
#[arg(long, value_enum)]
bridge: Option<WasmBridge>,
#[arg(long, value_enum)]
pack: Option<DeployPack>,
#[arg(long, value_enum, conflicts_with_all = &["target", "bridge", "pack"])]
preset: Option<DeployPreset>,
#[arg(long)]
handler: Option<String>,
#[arg(long, value_enum)]
optimize: Option<WasmOptMode>,
#[arg(long, value_name = "PASS")]
emit_ir_after: Option<String>,
#[arg(long, default_value_t = false)]
explain_passes: bool,
#[arg(long, default_value_t = false)]
json: bool,
},
#[command(hide = true)]
WasmRuntime {
#[arg(short = 'o', long)]
output: String,
#[arg(long, value_enum, default_value = "runtime")]
artifact: WasmRuntimeArtifact,
#[arg(long, value_enum)]
optimize: Option<WasmOptMode>,
#[arg(long)]
wat: bool,
},
Why {
file: String,
#[arg(long)]
module_root: Option<String>,
#[arg(long)]
verbose: bool,
#[arg(long)]
json: bool,
},
Bench {
scenario: String,
#[arg(long, default_value = "vm")]
target: String,
#[arg(long, value_name = "N")]
iterations: Option<usize>,
#[arg(long, value_name = "N")]
warmup: Option<usize>,
#[arg(long)]
json: bool,
#[arg(long, value_name = "PATH")]
save_baseline: Option<String>,
#[arg(long, value_name = "PATH", conflicts_with = "baseline_dir")]
compare: Option<String>,
#[arg(long, value_name = "DIR", conflicts_with = "compare")]
baseline_dir: Option<String>,
#[arg(long)]
fail_on_regression: bool,
},
Proof {
file: String,
#[arg(short = 'o', long, default_value = "out")]
output: String,
#[arg(long)]
name: Option<String>,
#[arg(long)]
module_root: Option<String>,
#[arg(long, default_value = "lean")]
backend: ProofBackend,
#[arg(long, default_value = "auto")]
verify_mode: ProofVerifyMode,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_accepts_deps_flag() {
let cli = Cli::parse_from(["aver", "verify", "examples/modules/app.av", "--deps"]);
match cli.command {
Commands::Verify { file, deps, .. } => {
assert_eq!(file, "examples/modules/app.av");
assert!(deps);
}
_ => panic!("expected verify command"),
}
}
#[test]
fn run_accepts_self_host_flag() {
let cli = Cli::parse_from(["aver", "run", "examples/modules/app.av", "--self-host"]);
match cli.command {
Commands::Run { self_host, .. } => {
assert!(self_host);
}
_ => panic!("expected run command"),
}
}
#[test]
fn replay_accepts_self_host_flag() {
let cli = Cli::parse_from(["aver", "replay", "recordings", "--self-host"]);
match cli.command {
Commands::Replay { self_host, .. } => {
assert!(self_host);
}
_ => panic!("expected replay command"),
}
}
#[test]
fn compile_accepts_with_replay_and_guest_entry() {
let cli = Cli::parse_from([
"aver",
"compile",
"examples/modules/app.av",
"--with-replay",
"--guest-entry",
"runGuestProgram",
]);
match cli.command {
Commands::Compile {
with_replay,
policy,
guest_entry,
with_self_host_support,
..
} => {
assert!(with_replay);
assert_eq!(policy, None);
assert_eq!(guest_entry.as_deref(), Some("runGuestProgram"));
assert!(!with_self_host_support);
}
_ => panic!("expected compile command"),
}
}
#[test]
fn compile_accepts_optimize() {
let cli = Cli::parse_from([
"aver",
"compile",
"examples/core/hello.av",
"--target",
"wasm",
"--optimize",
"size",
]);
match cli.command {
Commands::Compile {
target, optimize, ..
} => {
assert_eq!(target, CompileTarget::Wasm);
assert_eq!(optimize, Some(WasmOptMode::Oz));
}
_ => panic!("expected compile command"),
}
}
#[test]
fn compile_accepts_explicit_self_host_support() {
let cli = Cli::parse_from([
"aver",
"compile",
"self_hosted/main.av",
"--with-self-host-support",
"--guest-entry",
"runGuestCliProgram",
"--policy",
"runtime",
]);
match cli.command {
Commands::Compile {
policy,
guest_entry,
with_self_host_support,
..
} => {
assert_eq!(policy, Some(CompilePolicyMode::Runtime));
assert_eq!(guest_entry.as_deref(), Some("runGuestCliProgram"));
assert!(with_self_host_support);
}
_ => panic!("expected compile command"),
}
}
#[test]
fn compile_accepts_explicit_runtime_policy() {
let cli = Cli::parse_from([
"aver",
"compile",
"examples/modules/app.av",
"--policy",
"runtime",
]);
match cli.command {
Commands::Compile { policy, .. } => {
assert_eq!(policy, Some(CompilePolicyMode::Runtime));
}
_ => panic!("expected compile command"),
}
}
#[test]
fn context_defaults_to_auto_depth_and_10kb_budget() {
let cli = Cli::parse_from(["aver", "context", "examples/modules/app.av"]);
match cli.command {
Commands::Context { depth, budget, .. } => {
assert_eq!(depth, ContextDepth::Auto);
assert_eq!(budget, 10 * 1024);
}
_ => panic!("expected context command"),
}
}
#[test]
fn context_accepts_unlimited_and_labeled_budget() {
let cli = Cli::parse_from([
"aver",
"context",
"examples/modules/app.av",
"--depth",
"unlimited",
"--budget",
"12kb",
]);
match cli.command {
Commands::Context { depth, budget, .. } => {
assert_eq!(depth, ContextDepth::Unlimited);
assert_eq!(budget, 12 * 1024);
}
_ => panic!("expected context command"),
}
}
#[test]
fn context_accepts_numeric_depth() {
let cli = Cli::parse_from(["aver", "context", "examples/modules/app.av", "--depth", "2"]);
match cli.command {
Commands::Context { depth, .. } => {
assert_eq!(depth, ContextDepth::Limited(2));
}
_ => panic!("expected context command"),
}
}
#[test]
fn context_accepts_focus_symbol() {
let cli = Cli::parse_from([
"aver",
"context",
"examples/modules/app.av",
"--focus",
"Json.fromString",
]);
match cli.command {
Commands::Context { focus, .. } => {
assert_eq!(focus.as_deref(), Some("Json.fromString"));
}
_ => panic!("expected context command"),
}
}
}