#![doc = include_str!("../README.md")]
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use monty_types::TypeCheckingFormat;
#[cfg(feature = "standalone")]
mod run;
mod subprocess;
#[global_allocator]
static ALLOC: monty_alloc::LimitedAllocator = monty_alloc::LimitedAllocator;
#[derive(Parser)]
#[command(version)]
pub(crate) struct Cli {
#[arg(short = 'i', long = "interactive")]
interactive: bool,
#[arg(short = 't', long = "type-check")]
type_check: bool,
#[arg(long = "type-check-format", value_parser = TypeCheckingFormat::from_name, requires = "type_check")]
type_check_format: Option<TypeCheckingFormat>,
#[arg(short = 'c')]
command: Option<String>,
file: Option<String>,
#[arg(short = 'm', long = "mount")]
mounts: Vec<String>,
#[arg(long)]
max_duration: Option<f64>,
#[arg(long, value_parser = parse_memory_size)]
max_memory: Option<usize>,
#[arg(long)]
gc_interval: Option<usize>,
#[arg(long)]
max_recursion_depth: Option<usize>,
#[arg(long)]
max_suspensions: Option<usize>,
#[command(subcommand)]
subcommand: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Subprocess,
}
impl Cli {
fn subprocess_conflict(&self) -> Option<&'static str> {
if self.interactive {
Some("--interactive")
} else if self.command.is_some() {
Some("-c")
} else if self.file.is_some() {
Some("a file argument")
} else if self.type_check {
Some("--type-check")
} else if !self.mounts.is_empty() {
Some("--mount")
} else if self.any_resource_limit_flag() {
Some("a resource-limit flag")
} else {
None
}
}
fn any_resource_limit_flag(&self) -> bool {
self.max_duration.is_some()
|| self.max_memory.is_some()
|| self.gc_interval.is_some()
|| self.max_recursion_depth.is_some()
|| self.max_suspensions.is_some()
}
#[cfg(feature = "standalone")]
fn resource_limits(&self) -> Result<monty_types::ResourceLimits, String> {
let mut limits = monty_types::ResourceLimits::default();
if let Some(secs) = self.max_duration {
limits = limits.max_duration(
#[expect(clippy::absolute_paths)]
std::time::Duration::try_from_secs_f64(secs).map_err(|err| format!("invalid --max-duration: {err}"))?,
);
}
if let Some(bytes) = self.max_memory {
limits = limits.max_memory(bytes);
}
if let Some(interval) = self.gc_interval {
limits = limits.gc_interval(interval);
}
if let Some(depth) = self.max_recursion_depth {
limits = limits.max_recursion_depth(depth);
}
if let Some(max) = self.max_suspensions {
limits = limits.max_suspensions(max);
}
Ok(limits)
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
if let Some(Command::Subprocess) = cli.subcommand {
if let Some(flag) = cli.subprocess_conflict() {
eprintln!("error: `subprocess` cannot be combined with {flag}");
ExitCode::FAILURE
} else {
subprocess::run()
}
} else {
run_standalone(cli)
}
}
#[cfg(feature = "standalone")]
fn run_standalone(cli: Cli) -> ExitCode {
run::run(cli)
}
#[cfg(not(feature = "standalone"))]
fn run_standalone(_cli: Cli) -> ExitCode {
eprintln!("error: this build runs `monty subprocess` only — rebuild with the `standalone` feature for the CLI");
ExitCode::FAILURE
}
fn parse_memory_size(s: &str) -> Result<usize, String> {
let s = s.trim();
let (num_str, multiplier) = if let Some(n) = s.strip_suffix("GB").or_else(|| s.strip_suffix("gb")) {
(n.trim(), 1024 * 1024 * 1024)
} else if let Some(n) = s.strip_suffix("MB").or_else(|| s.strip_suffix("mb")) {
(n.trim(), 1024 * 1024)
} else if let Some(n) = s.strip_suffix("KB").or_else(|| s.strip_suffix("kb")) {
(n.trim(), 1024)
} else {
(s, 1)
};
let value: usize = num_str.parse().map_err(|e| format!("invalid memory size '{s}': {e}"))?;
value
.checked_mul(multiplier)
.ok_or_else(|| format!("memory size '{s}' overflows"))
}