#[cfg(feature = "telemetry")]
use std::env;
use std::{
fmt, fs, io,
process::ExitCode,
thread,
time::{Duration, Instant},
};
use anstream::{AutoStream, ColorChoice, eprintln};
use anstyle::{AnsiColor, Color, Style};
use monty::{
MontyRepl, MontyRun, ReplContinuationMode, ReplProgress, RunProgress, detect_repl_continuation_mode,
type_check_nesting_exception,
};
use monty_fs::{MountCallOutcome, MountMode, MountTable, OverlayState};
use monty_type_checking::{SourceFile, TypeChecker};
use monty_types::{
CallArgs, CompileOptions, DEFAULT_MAX_SUSPENSIONS, ExcType, ExtFunctionResult, MontyException, MontyObject,
NameLookupResult, OsFunctionCall, OsPolicy, PrintWriter, ResourceLimits, ResourceTracker, SOURCE_SCAN_THRESHOLD,
SleepMode, TypeCheckingConfig, memory_limit_with_headroom, validate_cwd,
};
use rustyline::{DefaultEditor, error::ReadlineError};
#[cfg(feature = "telemetry")]
use tracing::field::Empty;
use crate::Cli;
const DIM: Style = Style::new().dimmed();
const BOLD_RED: Style = Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::Red)));
const BOLD_GREEN: Style = Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::Green)));
const BOLD_CYAN: Style = Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
const ARROW: &str = "❯";
const EXT_FUNCTIONS: bool = false;
#[cfg(feature = "telemetry")]
pub(crate) fn run(cli: Cli) -> ExitCode {
let logfire = match configure_logfire() {
Ok(logfire) => logfire,
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: failed to configure telemetry: {err}");
return ExitCode::FAILURE;
}
};
let run_span = logfire.as_ref().map(|_| logfire::span!("run monty", success = Empty,));
let exit_code = run_cli(cli);
if let Some(run_span) = run_span {
run_span.record("success", exit_code == ExitCode::SUCCESS);
}
exit_code
}
#[cfg(not(feature = "telemetry"))]
pub(crate) fn run(cli: Cli) -> ExitCode {
run_cli(cli)
}
#[cfg(feature = "telemetry")]
fn configure_logfire() -> Result<Option<logfire::ShutdownGuard>, logfire::ConfigureError> {
if env::var_os("LOGFIRE_TOKEN").is_some() {
logfire::configure()
.with_service_name("monty")
.with_service_version(env!("CARGO_PKG_VERSION"))
.finish()
.map(logfire::Logfire::shutdown_guard)
.map(Some)
} else {
Ok(None)
}
}
fn run_cli(cli: Cli) -> ExitCode {
let type_check = cli.type_check.then(|| TypeCheckingConfig {
format: cli.type_check_format.unwrap_or_default(),
color: stderr_styled(),
});
let limits = match cli.resource_limits() {
Ok(limits) => limits,
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: {err}");
return ExitCode::FAILURE;
}
};
let hard_memory_limit = memory_limit_with_headroom(limits.max_memory, type_check.is_some());
monty_alloc::set_hard_limit(hard_memory_limit).expect("monty-runtime must install LimitedAllocator globally");
let (mounts, first_mount) = match build_mount_table(&cli.mounts) {
Ok(Some((mt, first_mount))) => (Some(mt), Some(first_mount)),
Ok(None) => (None, None),
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: {err}");
return ExitCode::FAILURE;
}
};
let max_sleep = match cli.max_sleep() {
Ok(max_sleep) => max_sleep,
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: {err}");
return ExitCode::FAILURE;
}
};
let host = HostOs {
mounts,
max_sleep,
sleep_budget: limits.max_total_sleep.map(SleepBudget::new),
};
let cwd = match sandbox_cwd(cli.cwd.as_deref(), first_mount) {
Ok(cwd) => cwd,
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: {err}");
return ExitCode::FAILURE;
}
};
if let Some(cmd) = cli.command {
if cli.file.is_some() {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: cannot specify both -c and a file");
return ExitCode::FAILURE;
}
return if cli.interactive {
dispatch_repl("<string>", &cmd, limits, host, &cwd)
} else {
dispatch_script("<string>", cmd, type_check, limits, host, &cwd)
};
}
if let Some(file_path) = cli.file.as_deref() {
let code = match read_file(file_path) {
Ok(code) => code,
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: {err}");
return ExitCode::FAILURE;
}
};
return if cli.interactive {
dispatch_repl(file_path, &code, limits, host, &cwd)
} else {
dispatch_script(file_path, code, type_check, limits, host, &cwd)
};
}
dispatch_repl("repl.py", "", limits, host, &cwd)
}
fn sandbox_cwd(cwd: Option<&str>, first_mount: Option<String>) -> Result<String, String> {
match cwd {
Some(cwd) => validate_cwd(cwd).map_err(|message| format!("--{message}")),
None => Ok(first_mount.unwrap_or_else(|| "/".to_owned())),
}
}
fn dispatch_script(
file_path: &str,
code: String,
type_check: Option<TypeCheckingConfig>,
limits: ResourceLimits,
host: HostOs,
cwd: &str,
) -> ExitCode {
run_script(file_path, code, type_check, ResourceTracker::new(limits), host, cwd)
}
fn dispatch_repl(file_path: &str, code: &str, limits: ResourceLimits, host: HostOs, cwd: &str) -> ExitCode {
run_repl(file_path, code, ResourceTracker::new(limits), host, cwd)
}
fn run_script(
file_path: &str,
code: String,
type_check: Option<TypeCheckingConfig>,
tracker: ResourceTracker,
mut host: HostOs,
cwd: &str,
) -> ExitCode {
if let Some(config) = type_check {
if let Err(err) = type_check_nesting_exception(&code, file_path, SOURCE_SCAN_THRESHOLD) {
eprintln!("{BOLD_RED}error{BOLD_RED:#}:\n{err}");
return ExitCode::FAILURE;
}
let start = Instant::now();
let mut checker = TypeChecker::default();
if let Some(failure) = checker.run(&SourceFile::new(&code, file_path), None, config).unwrap() {
let elapsed = start.elapsed();
eprintln!(
"{DIM}{}{DIM:#} {BOLD_CYAN}{ARROW}{BOLD_CYAN:#} {BOLD_RED}type check failed{BOLD_RED:#}:\n{failure}",
FormattedDuration(elapsed)
);
} else {
let elapsed = start.elapsed();
eprintln!(
"{DIM}{}{DIM:#} {BOLD_CYAN}{ARROW}{BOLD_CYAN:#} {BOLD_GREEN}type check passed{BOLD_GREEN:#}",
FormattedDuration(elapsed)
);
}
}
let input_names = vec![];
let inputs = vec![];
let mut runner = match MontyRun::new(code, file_path, input_names, CompileOptions::default()) {
Ok(ex) => ex.with_os_policy(host.os_policy()),
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}:\n{err}");
return ExitCode::FAILURE;
}
};
runner.set_cwd(cwd);
if EXT_FUNCTIONS || host.suspends() {
let start = Instant::now();
let progress = match runner.start(inputs, tracker, PrintWriter::Stdout) {
Ok(p) => p,
Err(err) => {
let elapsed = start.elapsed();
eprintln!(
"{DIM}{}{DIM:#} {BOLD_CYAN}{ARROW}{BOLD_CYAN:#} {BOLD_RED}error{BOLD_RED:#}: {err}",
FormattedDuration(elapsed)
);
return ExitCode::FAILURE;
}
};
let mut suspensions = SuspensionBudget::from_progress(&progress);
match run_until_complete(progress, &mut host, &mut suspensions) {
Ok(value) => {
let elapsed = start.elapsed();
eprintln!(
"{DIM}{}{DIM:#} {BOLD_CYAN}{ARROW}{BOLD_CYAN:#} {value}",
FormattedDuration(elapsed)
);
ExitCode::SUCCESS
}
Err(err) => {
let elapsed = start.elapsed();
eprintln!(
"{DIM}{}{DIM:#} {BOLD_CYAN}{ARROW}{BOLD_CYAN:#} {BOLD_RED}error{BOLD_RED:#}: {err}",
FormattedDuration(elapsed)
);
ExitCode::FAILURE
}
}
} else {
let start = Instant::now();
let value = match runner.run(inputs, tracker, PrintWriter::Stdout) {
Ok(p) => p,
Err(err) => {
let elapsed = start.elapsed();
eprintln!(
"{DIM}{}{DIM:#} {BOLD_CYAN}{ARROW}{BOLD_CYAN:#} {BOLD_RED}error{BOLD_RED:#}: {err}",
FormattedDuration(elapsed)
);
return ExitCode::FAILURE;
}
};
let elapsed = start.elapsed();
eprintln!(
"{DIM}{}{DIM:#} {BOLD_CYAN}{ARROW}{BOLD_CYAN:#} {value}",
FormattedDuration(elapsed)
);
ExitCode::SUCCESS
}
}
fn run_repl(file_path: &str, code: &str, tracker: ResourceTracker, mut host: HostOs, cwd: &str) -> ExitCode {
let mut suspensions = SuspensionBudget::new(&tracker);
let mut repl = MontyRepl::new(file_path, tracker, CompileOptions::default()).with_os_policy(host.os_policy());
repl.set_cwd(cwd);
let mut repl = Some(repl);
if !code.is_empty() {
execute_repl_snippet(&mut repl, code, &mut host, &mut suspensions);
}
eprintln!("Monty v{} REPL. Type `exit` to exit.", env!("CARGO_PKG_VERSION"));
let mut rl = match DefaultEditor::new() {
Ok(rl) => rl,
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#} initializing editor: {err}");
return ExitCode::FAILURE;
}
};
let mut pending_snippet = String::new();
let mut continuation_mode = ReplContinuationMode::Complete;
let statement_prompt = if stdout_styled() {
format!("{BOLD_CYAN}{ARROW}{BOLD_CYAN:#} ")
} else {
format!("{ARROW} ")
};
loop {
let prompt = if continuation_mode == ReplContinuationMode::Complete {
statement_prompt.as_str()
} else {
"… "
};
let line = match rl.readline(prompt) {
Ok(line) => line,
Err(ReadlineError::Eof) => return ExitCode::SUCCESS,
Err(ReadlineError::Interrupted) => {
pending_snippet.clear();
continuation_mode = ReplContinuationMode::Complete;
continue;
}
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#} reading input: {err}");
return ExitCode::FAILURE;
}
};
let snippet = line.trim_end();
if continuation_mode == ReplContinuationMode::Complete && snippet.is_empty() {
continue;
}
if continuation_mode == ReplContinuationMode::Complete && snippet == "exit" {
return ExitCode::SUCCESS;
}
pending_snippet.push_str(snippet);
pending_snippet.push('\n');
if continuation_mode == ReplContinuationMode::IncompleteBlock && snippet.is_empty() {
let _ = rl.add_history_entry(pending_snippet.trim_end());
execute_repl_snippet(&mut repl, &pending_snippet, &mut host, &mut suspensions);
pending_snippet.clear();
continuation_mode = ReplContinuationMode::Complete;
continue;
}
let detected_mode = detect_repl_continuation_mode(&pending_snippet);
match detected_mode {
ReplContinuationMode::Complete => {
if continuation_mode == ReplContinuationMode::IncompleteBlock {
continue;
}
let _ = rl.add_history_entry(pending_snippet.trim_end());
execute_repl_snippet(&mut repl, &pending_snippet, &mut host, &mut suspensions);
pending_snippet.clear();
continuation_mode = ReplContinuationMode::Complete;
}
ReplContinuationMode::IncompleteBlock => continuation_mode = ReplContinuationMode::IncompleteBlock,
ReplContinuationMode::IncompleteImplicit => {
if continuation_mode != ReplContinuationMode::IncompleteBlock {
continuation_mode = ReplContinuationMode::IncompleteImplicit;
}
}
}
}
}
fn execute_repl_snippet(
repl: &mut Option<MontyRepl>,
snippet: &str,
host: &mut HostOs,
suspensions: &mut SuspensionBudget,
) {
let r = repl.take().expect("repl must be present");
if host.suspends() {
match execute_repl_with_mounts(r, snippet, host, suspensions) {
Ok((returned_repl, output)) => {
if output != MontyObject::none() {
println!("{output}");
}
*repl = Some(returned_repl);
}
Err((returned_repl, err)) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: {err}");
*repl = Some(returned_repl);
}
}
} else {
let mut r = r;
match r.feed_run(snippet, vec![], PrintWriter::Stdout) {
Ok(output) => {
if output != MontyObject::none() {
println!("{output}");
}
}
Err(err) => {
eprintln!("{BOLD_RED}error{BOLD_RED:#}: {err}");
}
}
*repl = Some(r);
}
}
#[expect(clippy::result_large_err)]
fn execute_repl_with_mounts(
r: MontyRepl,
snippet: &str,
host: &mut HostOs,
suspensions: &mut SuspensionBudget,
) -> Result<(MontyRepl, MontyObject), (MontyRepl, String)> {
let mut progress = match r.feed_start(snippet, vec![], PrintWriter::Stdout) {
Ok(p) => p,
Err(err) => return Err((err.repl, format!("{}", err.error))),
};
loop {
if !matches!(progress, ReplProgress::Complete { .. })
&& let Some(exc) = suspensions.record_suspension()
{
let outcome = match progress {
ReplProgress::OsCall(call) => call.abort(exc, PrintWriter::Stdout),
ReplProgress::FunctionCall(call) => call.abort(exc, PrintWriter::Stdout),
ReplProgress::NameLookup(lookup) => lookup.abort(exc, PrintWriter::Stdout),
ReplProgress::ResolveFutures(state) => state.abort(exc, PrintWriter::Stdout),
ReplProgress::Complete { .. } => unreachable!("only suspensions are counted"),
};
match outcome {
Ok(p) => progress = p,
Err(err) => return Err((err.repl, format!("{}", err.error))),
}
continue;
}
match progress {
ReplProgress::Complete { repl, value } => return Ok((repl, value)),
ReplProgress::OsCall(call) => {
let outcome = match host.refuse_sleep(&call.function_call) {
Some(exc) => call.abort(exc, PrintWriter::Stdout),
None => call.resume_with(PrintWriter::Stdout, |fc| host.handle_os_call(fc)),
};
match outcome {
Ok(p) => progress = p,
Err(err) => return Err((err.repl, format!("{}", err.error))),
}
}
ReplProgress::FunctionCall(call) => {
return Err((
call.into_repl(),
"external function calls not supported in CLI".to_owned(),
));
}
ReplProgress::NameLookup(lookup) => match lookup.resume(NameLookupResult::Undefined, PrintWriter::Stdout) {
Ok(p) => progress = p,
Err(err) => return Err((err.repl, format!("{}", err.error))),
},
ReplProgress::ResolveFutures(state) => {
return Err((state.into_repl(), "async futures not supported in CLI".to_owned()));
}
}
}
}
fn run_until_complete(
mut progress: RunProgress,
host: &mut HostOs,
suspensions: &mut SuspensionBudget,
) -> Result<MontyObject, String> {
loop {
if !matches!(progress, RunProgress::Complete(_))
&& let Some(exc) = suspensions.record_suspension()
{
progress = match progress {
RunProgress::OsCall(call) => call.abort(exc, PrintWriter::Stdout),
RunProgress::FunctionCall(call) => call.abort(exc, PrintWriter::Stdout),
RunProgress::NameLookup(lookup) => lookup.abort(exc, PrintWriter::Stdout),
RunProgress::ResolveFutures(state) => state.abort(exc, PrintWriter::Stdout),
RunProgress::Complete(_) => unreachable!("only suspensions are counted"),
}
.map_err(|err| format!("{err}"))?;
continue;
}
match progress {
RunProgress::Complete(value) => return Ok(value),
RunProgress::FunctionCall(call) => {
let return_value = resolve_external_call(&call.function_name, &call.args)?;
progress = call
.resume(return_value, PrintWriter::Stdout)
.map_err(|err| format!("{err}"))?;
}
RunProgress::ResolveFutures(state) => {
return Err(format!(
"async futures not supported in CLI: {:?}",
state.pending_call_ids()
));
}
RunProgress::NameLookup(lookup) => {
let result = if lookup.name == "add_ints" {
NameLookupResult::from(MontyObject::function("add_ints".to_string(), None))
} else {
NameLookupResult::Undefined
};
progress = lookup
.resume(result, PrintWriter::Stdout)
.map_err(|err| format!("{err}"))?;
}
RunProgress::OsCall(call) => {
let outcome = match host.refuse_sleep(&call.function_call) {
Some(exc) => call.abort(exc, PrintWriter::Stdout),
None => call.resume_with(PrintWriter::Stdout, |fc| host.handle_os_call(fc)),
};
progress = outcome.map_err(|err| format!("{err}"))?;
}
}
}
}
struct SleepBudget {
limit: Duration,
used: Duration,
}
impl SleepBudget {
fn new(limit: Duration) -> Self {
Self {
limit,
used: Duration::ZERO,
}
}
fn charge(&mut self, delay: Duration) -> Option<MontyException> {
let total = self.used.saturating_add(delay);
if total > self.limit {
Some(MontyException::new(
ExcType::TimeoutError,
Some(format!("sleep limit exceeded: {total:?} > {:?}", self.limit)),
))
} else {
self.used = total;
None
}
}
}
struct SuspensionBudget {
limit: usize,
seen: usize,
}
impl SuspensionBudget {
fn new(tracker: &ResourceTracker) -> Self {
Self {
limit: tracker.max_suspensions(),
seen: 0,
}
}
fn from_progress(progress: &RunProgress) -> Self {
let limit = match progress {
RunProgress::FunctionCall(call) => call.tracker().max_suspensions(),
RunProgress::OsCall(call) => call.tracker().max_suspensions(),
RunProgress::NameLookup(lookup) => lookup.tracker().max_suspensions(),
RunProgress::ResolveFutures(state) => state.tracker().max_suspensions(),
RunProgress::Complete(_) => DEFAULT_MAX_SUSPENSIONS,
};
Self { limit, seen: 0 }
}
fn record_suspension(&mut self) -> Option<MontyException> {
self.seen += 1;
let limit = self.limit;
(self.seen > limit).then(|| {
MontyException::new(
ExcType::RuntimeError,
Some(format!("suspension limit {limit} exceeded")),
)
})
}
}
struct HostOs {
mounts: Option<MountTable>,
max_sleep: Duration,
sleep_budget: Option<SleepBudget>,
}
impl HostOs {
fn suspends(&self) -> bool {
self.mounts.is_some() || self.sleep_budget.is_some()
}
fn os_policy(&self) -> OsPolicy {
OsPolicy {
sleep: SleepMode::System(self.max_sleep),
..OsPolicy::default()
}
}
fn refuse_sleep(&mut self, call: &OsFunctionCall) -> Option<MontyException> {
match (call, self.sleep_budget.as_mut()) {
(OsFunctionCall::SystemSleep(delay) | OsFunctionCall::AsyncSystemSleep(delay), Some(budget)) => {
budget.charge((*delay).min(self.max_sleep))
}
_ => None,
}
}
fn handle_os_call(&mut self, call: OsFunctionCall) -> ExtFunctionResult {
if let OsFunctionCall::SystemSleep(delay) | OsFunctionCall::AsyncSystemSleep(delay) = call {
thread::sleep(delay.min(self.max_sleep));
return MontyObject::none().into();
}
match self.mounts.as_mut() {
Some(mounts) => match mounts.handle_os_call(call) {
MountCallOutcome::Handled(Ok(obj)) => obj.into(),
MountCallOutcome::Handled(Err(err)) => err.into_exception().into(),
MountCallOutcome::NotHandled(call) => call.on_no_handler().into(),
},
None => call.on_no_handler().into(),
}
}
}
fn resolve_external_call(function_name: &str, args: &CallArgs) -> Result<MontyObject, String> {
let rendered = || args.args().map(|arg| arg.py_repr()).collect::<Vec<_>>().join(", ");
if function_name != "add_ints" {
return Err(format!("unknown external function: {function_name}({})", rendered()));
}
if args.args().len() != 2 {
return Err(format!(
"add_ints requires exactly 2 arguments, got {}",
args.args().len()
));
}
match (
args.arg(0).and_then(|a| a.as_int()),
args.arg(1).and_then(|b| b.as_int()),
) {
(Some(a), Some(b)) => a
.checked_add(b)
.map(MontyObject::int)
.ok_or_else(|| format!("add_ints result is out of i64 range, got {}", rendered())),
_ => Err(format!("add_ints requires integer arguments, got {}", rendered())),
}
}
fn build_mount_table(mount_args: &[String]) -> Result<Option<(MountTable, String)>, String> {
if mount_args.is_empty() {
return Ok(None);
}
let mut table = MountTable::new();
let mut first_virtual_path = None;
for arg in mount_args {
let (host_path, virtual_path, mode, write_bytes_limit) = parse_mount(arg)?;
table
.mount(&virtual_path, &host_path, mode, write_bytes_limit)
.map_err(|e| format!("mount {arg}: {e}"))?;
first_virtual_path.get_or_insert(virtual_path);
}
Ok(first_virtual_path.map(|first| (table, first)))
}
fn parse_mount(spec: &str) -> Result<(String, String, MountMode, Option<u64>), String> {
let parts: Vec<&str> = spec.split("::").collect();
let (host_path, virtual_path, mode_str, limit_str) = match parts.len() {
2 => (parts[0], parts[1], "ro", None),
3 => (parts[0], parts[1], parts[2], None),
4 => (parts[0], parts[1], parts[2], Some(parts[3])),
_ => {
return Err(format!(
"invalid mount spec '{spec}': expected host_path::virtual_path[::mode[::write_limit_bytes]]"
));
}
};
if host_path.is_empty() || virtual_path.is_empty() {
return Err(format!(
"invalid mount spec '{spec}': host and virtual paths must not be empty"
));
}
let mode = match mode_str {
"ro" => MountMode::ReadOnly,
"rw" => MountMode::ReadWrite,
"overlay" => MountMode::OverlayMemory(OverlayState::new()),
other => {
return Err(format!(
"invalid mount mode '{other}' in '{spec}': expected 'ro', 'rw', or 'overlay'"
));
}
};
let write_bytes_limit = match limit_str {
Some("") => {
return Err(format!("invalid write limit in '{spec}': value must not be empty"));
}
Some(limit) => Some(
limit
.parse::<u64>()
.map_err(|_| format!("invalid write limit '{limit}' in '{spec}': expected a non-negative integer"))?,
),
None => None,
};
Ok((host_path.to_owned(), virtual_path.to_owned(), mode, write_bytes_limit))
}
fn read_file(file_path: &str) -> Result<String, String> {
match fs::metadata(file_path) {
Ok(metadata) => {
if !metadata.is_file() {
return Err(format!("{file_path} is not a file"));
}
}
Err(err) => {
return Err(format!("reading {file_path}: {err}"));
}
}
match fs::read_to_string(file_path) {
Ok(contents) => Ok(contents),
Err(err) => Err(format!("reading file: {err}")),
}
}
struct FormattedDuration(Duration);
impl fmt::Display for FormattedDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let duration = self.0;
let total_secs = duration.as_secs_f64();
if total_secs < 1e-3 {
let us = total_secs * 1e6;
let decimals = sig_digits_after_decimal(us);
write!(f, "{us:.decimals$}μs")
} else if total_secs < 1.0 {
let ms = total_secs * 1e3;
let decimals = sig_digits_after_decimal(ms);
write!(f, "{ms:.decimals$}ms")
} else {
let decimals = sig_digits_after_decimal(total_secs);
write!(f, "{total_secs:.decimals$}s")
}
}
}
fn sig_digits_after_decimal(value: f64) -> usize {
let before = if value < 1.0 {
1
} else {
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let digits = (value.log10().floor() as u32) + 1;
digits as usize
};
5usize.saturating_sub(before)
}
fn stderr_styled() -> bool {
AutoStream::choice(&io::stderr()) != ColorChoice::Never
}
fn stdout_styled() -> bool {
AutoStream::choice(&io::stdout()) != ColorChoice::Never
}