use anyhow::{Context, Result};
use std::io::Write as _;
use std::path::PathBuf;
use std::process::Command;
use std::sync::{Arc, Mutex, mpsc};
use std::time::Duration;
use crate::codegen::{
self, M_DONE, M_NEW, M_STDIN, M_STDIN_DONE, M_TIME, M_UNICODE, M_UTF8, M_VAL, MAX_ARRAY_DEPTH,
Slot, UNPRINTABLE,
};
use crate::errmap;
use crate::i18n;
use crate::lex;
use crate::proc;
use crate::runtime_checks::{self, Checks, Request};
use crate::session::Session;
pub struct Outcome {
pub slot: Slot,
pub output: String,
pub value: Option<String>,
pub errors: String,
pub runtime_report: Option<String>,
pub streamed_output_needs_newline: bool,
pub stdin_events: Vec<proc::StdinEvent>,
pub warnings: String,
pub abnormal: Option<String>,
pub rewritten: Option<String>,
pub scoped_rebind: bool,
pub file_replacement: Option<usize>,
pub unprintable: bool,
pub duration: Duration,
pub timed_duration: Option<Duration>,
}
pub enum Eval {
Done(Box<Outcome>),
CompileError(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnicodeEncoding {
Utf8,
Utf16,
Utf32,
}
#[derive(Clone, Copy)]
enum ExprPlan {
Normal,
Lvalue,
Scalar,
Array(usize),
Utf8,
Silent,
}
#[derive(Default)]
struct ExprPlans {
revision: u64,
entries: std::collections::HashMap<(String, bool), ExprPlan>,
}
impl UnicodeEncoding {
fn label(self) -> &'static str {
match self {
Self::Utf8 => "UTF-8",
Self::Utf16 => "UTF-16",
Self::Utf32 => "UTF-32",
}
}
fn literal_prefix(self) -> &'static str {
match self {
Self::Utf8 => "u8",
Self::Utf16 => "u",
Self::Utf32 => "U",
}
}
fn unit_width(self) -> usize {
match self {
Self::Utf8 => 1,
Self::Utf16 => 2,
Self::Utf32 => 4,
}
}
fn hex_digits(self) -> usize {
self.unit_width() * 2
}
}
pub struct Evaluator {
#[cfg(test)]
compile_count: std::cell::Cell<usize>,
pub tc: crate::toolchain::Toolchain,
dir: tempfile::TempDir,
pub timeout: Duration,
stream_output: bool,
allow_program_stdin: bool,
runtime_checks: Option<Checks>,
check_cache: std::collections::HashMap<Request, std::result::Result<Checks, String>>,
expr_plans: std::cell::RefCell<ExprPlans>,
}
impl Evaluator {
pub fn new(tc: crate::toolchain::Toolchain, timeout: Duration) -> Result<Self> {
let dir = tempfile::tempdir().context(i18n::text("temp-dir-error"))?;
Ok(Evaluator {
#[cfg(test)]
compile_count: std::cell::Cell::new(0),
tc,
dir,
timeout,
stream_output: false,
allow_program_stdin: false,
runtime_checks: None,
check_cache: std::collections::HashMap::new(),
expr_plans: std::cell::RefCell::new(ExprPlans::default()),
})
}
fn src_path(&self) -> PathBuf {
self.dir.path().join("input.c")
}
fn exe_path(&self) -> PathBuf {
self.dir
.path()
.join(format!("input{}", std::env::consts::EXE_SUFFIX))
}
pub fn set_stream_output(&mut self, enabled: bool) {
self.stream_output = enabled;
}
pub fn set_program_stdin(&mut self, enabled: bool) {
self.allow_program_stdin = enabled;
}
pub fn set_toolchain(&mut self, tc: crate::toolchain::Toolchain) {
self.tc = tc;
self.check_cache.clear();
self.expr_plans.get_mut().entries.clear();
}
pub fn check(&mut self, session: &Session, input: &str, request: Request) -> Result<Eval> {
let checks = self
.check_cache
.entry(request)
.or_insert_with(|| Checks::detect(&self.tc, request, self.timeout))
.clone();
let checks = match checks {
Ok(checks) => checks,
Err(message) => return Ok(Eval::CompileError(message)),
};
let mut checked = Self::new(self.tc.clone(), self.timeout)?;
checked.runtime_checks = Some(checks);
checked.stream_output = self.stream_output;
checked.allow_program_stdin = self.allow_program_stdin;
checked.eval(session, input)
}
fn compile_text(&self, text: &str) -> std::result::Result<(PathBuf, String), String> {
if proc::interrupted() {
return Err(i18n::text("program-interrupted"));
}
#[cfg(test)]
self.compile_count.set(self.compile_count.get() + 1);
let src = self.src_path();
let exe = self.exe_path();
if let Err(e) = std::fs::write(&src, text) {
return Err(i18n::text_with(
"temp-source-write-error",
&[("error", e.to_string())],
));
}
let args = match self.runtime_checks {
Some(checks) => checks.compile_args(&self.tc, &src, &exe),
None => self.tc.compile_args(&src, &exe),
};
let mut cmd = Command::new(&self.tc.path);
cmd.current_dir(self.dir.path());
cmd.args(&args);
let cap = match proc::run_captured(&mut cmd, self.timeout, false) {
Ok(c) => c,
Err(e) => {
return Err(i18n::text_with(
"compiler-run-error",
&[("error", e.to_string())],
));
}
};
let mut d = String::from_utf8_lossy(&cap.stderr).into_owned();
d.push_str(&String::from_utf8_lossy(&cap.stdout));
if cap.stdout_truncated || cap.stderr_truncated {
d.push('\n');
d.push_str(&i18n::text_with(
"compiler-output-truncated",
&[("mib", (proc::MAX_CAPTURE_BYTES / (1024 * 1024)).to_string())],
));
d.push('\n');
}
if cap.interrupted {
return Err(i18n::text("program-interrupted"));
}
match cap.status {
Some(st) if st.success() => Ok((exe, d)),
Some(_) => Err(d),
None => Err(i18n::text_with(
"compiler-timeout",
&[("seconds", self.timeout.as_secs().to_string())],
)),
}
}
pub fn type_of(&self, session: &Session, input: &str) -> Result<Eval> {
let prog = codegen::build_type_probe(session, input);
self.run_probe(session, prog)
}
pub fn bits_of(&self, session: &Session, input: &str, uppercase: bool) -> Result<Eval> {
let prog = codegen::build_bits_probe(session, input, uppercase);
let mut result = self.run_probe(session, prog)?;
if i18n::current().is_chinese()
&& let Eval::Done(outcome) = &mut result
&& let Some(value) = &mut outcome.value
{
*value = localize_bits_output(value);
}
Ok(result)
}
pub fn unicode_of(
&self,
session: &Session,
input: &str,
encoding: UnicodeEncoding,
count: Option<usize>,
) -> Result<Eval> {
const DEFAULT_LIMIT: usize = 100;
let limit = count.unwrap_or(DEFAULT_LIMIT);
let prog = codegen::build_unicode_probe(
session,
input,
encoding.unit_width(),
limit,
count.is_some(),
);
let mut result = self.run_probe(session, prog)?;
if let Eval::Done(outcome) = &mut result
&& outcome.abnormal.is_none()
&& let Some(rendered) = outcome
.value
.as_deref()
.and_then(|value| render_unicode_payload(value, encoding, limit))
{
outcome.value = Some(rendered);
}
Ok(result)
}
fn run_probe(&self, session: &Session, prog: codegen::Program) -> Result<Eval> {
let src = self.src_path().display().to_string();
let (start, count) = (prog.new_start_line, prog.new_line_count);
match self.compile_text(&prog.src) {
Ok((exe, warns)) => {
let warnings = errmap::only_new(&errmap::remap(
&warns,
&src,
&prog.src,
start,
count,
&prog.session_line_ranges,
prog.wrapped,
));
Ok(Eval::Done(Box::new(self.run(
&exe,
Slot::Expr,
warnings,
prog.uses_stdin_tape,
session.stdin_tape(),
)?)))
}
Err(diag) => Ok(Eval::CompileError(errmap::drop_stale_warnings(
&errmap::remap(
&diag,
&src,
&prog.src,
start,
count,
&prog.session_line_ranges,
prog.wrapped,
),
))),
}
}
pub fn eval(&self, session: &Session, input: &str) -> Result<Eval> {
self.eval_inner(session, input, false)
}
pub fn time(&self, session: &Session, input: &str) -> Result<Eval> {
self.eval_inner(session, input, true)
}
fn eval_inner(&self, session: &Session, input: &str, timed: bool) -> Result<Eval> {
if lex::function_definition(input)
.map(|(name, _)| name == "main")
.unwrap_or(false)
{
return Ok(Eval::CompileError(i18n::text("main-already-provided")));
}
let diag = match self.attempt(session, input, timed)? {
Ok(o) => return Ok(Eval::Done(Box::new(o))),
Err(d) => d,
};
let t = input.trim_end();
let repairable = !t.ends_with(';') && !t.trim_start().starts_with('#');
if repairable {
let patched = format!("{t};");
if let Ok(mut o) = self.attempt(session, &patched, timed)? {
o.rewritten = Some(patched);
return Ok(Eval::Done(Box::new(o)));
}
}
Ok(Eval::CompileError(diag))
}
fn attempt(
&self,
session: &Session,
input: &str,
timed: bool,
) -> Result<std::result::Result<Outcome, String>> {
let cacheable = !session.file_items().iter().any(|item| {
lex::contains_code_identifier(item, "include")
|| lex::contains_code_identifier(item, "include_next")
});
let key = (input.to_string(), timed);
let cached = {
let mut plans = self.expr_plans.borrow_mut();
if plans.revision != session.revision() {
plans.entries.clear();
plans.revision = session.revision();
}
cacheable
.then(|| plans.entries.get(&key).copied())
.flatten()
};
if let Some(plan) = cached {
let program = self.expression_program(session, input, timed, plan);
if let Ok(mut outcome) = self.try_program(program, Slot::Expr, session.stdin_tape())? {
if matches!(plan, ExprPlan::Silent) {
outcome.unprintable = true;
outcome.value = None;
}
return Ok(Ok(outcome));
}
self.expr_plans.borrow_mut().entries.remove(&key);
}
if looks_file_scope(input) {
let normal = if timed {
codegen::build_timed(session, input, Slot::FileScope)
} else {
codegen::build(session, input, Slot::FileScope)
};
let reported = match self.try_program(normal, Slot::FileScope, session.stdin_tape())? {
Ok(o) => return Ok(Ok(o)),
Err(diag) => diag,
};
if !is_rebinding_diagnostic(&reported) {
return Ok(Err(reported));
}
for index in (0..session.file_items().len()).rev() {
if session.file_items()[index].trim_start().starts_with('#') {
continue;
}
let prog = if timed {
codegen::build_file_replacement_timed(session, input, index)
} else {
codegen::build_file_replacement(session, input, index)
};
if let Ok(mut o) = self.try_program(prog, Slot::FileScope, session.stdin_tape())? {
o.file_replacement = Some(index);
return Ok(Ok(o));
}
}
return Ok(Err(reported));
}
let mut reported = String::new();
let mut expr_reported = None;
let try_expr = should_try_expr(input);
let slots: &[Slot] = if try_expr {
&[Slot::Expr, Slot::Stmt]
} else {
&[Slot::Stmt]
};
for &slot in slots {
if slot == Slot::Expr && session.must_replay(input) && lex::may_be_array_lvalue(input) {
let bounded = codegen::build_lvalue_expr(session, input, timed);
if let Ok(o) = self.try_program(bounded, slot, session.stdin_tape())? {
return self.finish_expression(
session,
input,
o,
timed,
ExprPlan::Lvalue,
cacheable,
);
}
}
let normal = if timed {
codegen::build_timed(session, input, slot)
} else {
codegen::build(session, input, slot)
};
match self.try_program(normal, slot, session.stdin_tape())? {
Ok(o) if slot == Slot::Expr => {
return self.finish_expression(
session,
input,
o,
timed,
ExprPlan::Normal,
cacheable,
);
}
Ok(o) => return Ok(Ok(o)),
Err(diag) if slot == Slot::Stmt => {
let retry_scoped = is_rebinding_diagnostic(&diag);
reported = diag;
if retry_scoped {
let scoped = if timed {
codegen::build_scoped_stmt_timed(session, input)
} else {
codegen::build_scoped_stmt(session, input)
};
if let Ok(mut o) =
self.try_program(scoped, Slot::Stmt, session.stdin_tape())?
{
o.scoped_rebind = true;
return Ok(Ok(o));
}
}
}
Err(diag) => {
let scalar = codegen::build_scalar_expr(session, input, timed);
if let Ok(o) = self.try_program(scalar, slot, session.stdin_tape())? {
if cacheable {
self.remember_plan(input, timed, ExprPlan::Scalar);
}
return Ok(Ok(o));
}
expr_reported = Some(diag);
}
}
}
if try_expr {
let prog = if timed {
codegen::build_expr_probe_timed(session, input)
} else {
codegen::build_expr_probe(session, input)
};
if let Ok(mut o) = self.try_program(prog, Slot::Expr, session.stdin_tape())? {
o.value = None;
o.unprintable = true;
if cacheable {
self.remember_plan(input, timed, ExprPlan::Silent);
}
return Ok(Ok(o));
}
}
if reported.trim().is_empty()
&& let Some(expr) = expr_reported
{
reported = expr;
}
Ok(Err(reported))
}
fn refine_array(
&self,
session: &Session,
input: &str,
outcome: Outcome,
timed: bool,
initial: ExprPlan,
) -> Result<(Outcome, ExprPlan)> {
if outcome.abnormal.is_some() || session.must_replay(input) {
return Ok((outcome, initial));
}
if session.is_explicit_utf8_array_expr(input) {
let prog = if timed {
codegen::build_utf8_array_expr_timed(session, input)
} else {
codegen::build_utf8_array_expr(session, input)
};
if let Ok(mut candidate) = self.try_program(prog, Slot::Expr, session.stdin_tape())?
&& let Some(rendered) = candidate.value.as_deref().and_then(render_utf8_payload)
{
candidate.value = Some(rendered);
return Ok((candidate, ExprPlan::Utf8));
}
}
let addressed = outcome
.value
.as_deref()
.is_some_and(|v| v.starts_with("0x"));
if !addressed {
return Ok((outcome, initial));
}
let mut best = outcome;
let mut plan = initial;
for depth in 1..=MAX_ARRAY_DEPTH {
let prog = if timed {
codegen::build_array_expr_timed(session, input, depth)
} else {
codegen::build_array_expr(session, input, depth)
};
let Ok(candidate) = self.try_program(prog, Slot::Expr, session.stdin_tape())? else {
break;
};
let Some(value) = candidate.value.as_deref().filter(|v| v.starts_with('{')) else {
break;
};
let elements_unprintable = value.contains(UNPRINTABLE);
best = candidate;
plan = ExprPlan::Array(depth);
if !elements_unprintable {
break;
}
}
Ok((best, plan))
}
fn remember_plan(&self, input: &str, timed: bool, plan: ExprPlan) {
let mut plans = self.expr_plans.borrow_mut();
if plans.entries.len() >= 256 {
plans.entries.clear();
}
plans.entries.insert((input.to_string(), timed), plan);
}
fn finish_expression(
&self,
session: &Session,
input: &str,
outcome: Outcome,
timed: bool,
initial: ExprPlan,
cacheable: bool,
) -> Result<std::result::Result<Outcome, String>> {
let (outcome, plan) = self.refine_array(session, input, outcome, timed, initial)?;
if cacheable && outcome.abnormal.is_none() && !session.is_explicit_utf8_array_expr(input) {
self.remember_plan(input, timed, plan);
}
Ok(Ok(outcome))
}
fn expression_program(
&self,
session: &Session,
input: &str,
timed: bool,
plan: ExprPlan,
) -> codegen::Program {
match plan {
ExprPlan::Normal if timed => codegen::build_timed(session, input, Slot::Expr),
ExprPlan::Normal => codegen::build(session, input, Slot::Expr),
ExprPlan::Lvalue => codegen::build_lvalue_expr(session, input, timed),
ExprPlan::Scalar => codegen::build_scalar_expr(session, input, timed),
ExprPlan::Array(depth) if timed => {
codegen::build_array_expr_timed(session, input, depth)
}
ExprPlan::Array(depth) => codegen::build_array_expr(session, input, depth),
ExprPlan::Utf8 if timed => codegen::build_utf8_array_expr_timed(session, input),
ExprPlan::Utf8 => codegen::build_utf8_array_expr(session, input),
ExprPlan::Silent if timed => codegen::build_expr_probe_timed(session, input),
ExprPlan::Silent => codegen::build_expr_probe(session, input),
}
}
fn try_program(
&self,
prog: codegen::Program,
slot: Slot,
stdin_tape: &[proc::StdinEvent],
) -> Result<std::result::Result<Outcome, String>> {
match self.compile_program(&prog) {
Ok((exe, warnings)) => {
let mut outcome =
self.run(&exe, slot, warnings, prog.uses_stdin_tape, stdin_tape)?;
if let Some(report) = &mut outcome.runtime_report {
*report = errmap::remap_runtime(
report,
&self.src_path().display().to_string(),
&prog,
);
}
Ok(Ok(outcome))
}
Err(diag) => Ok(Err(diag)),
}
}
fn compile_program(
&self,
prog: &codegen::Program,
) -> std::result::Result<(PathBuf, String), String> {
let src = self.src_path().display().to_string();
let (start, count) = (prog.new_start_line, prog.new_line_count);
match self.compile_text(&prog.src) {
Ok((exe, warns)) => {
let warnings = errmap::only_new(&errmap::remap(
&warns,
&src,
&prog.src,
start,
count,
&prog.session_line_ranges,
prog.wrapped,
));
Ok((exe, warnings))
}
Err(diag) => Err(errmap::drop_stale_warnings(&errmap::remap(
&diag,
&src,
&prog.src,
start,
count,
&prog.session_line_ranges,
prog.wrapped,
))),
}
}
fn run(
&self,
exe: &PathBuf,
slot: Slot,
warnings: String,
uses_stdin_tape: bool,
stdin_tape: &[proc::StdinEvent],
) -> Result<Outcome> {
self.run_command(
Command::new(exe),
slot,
warnings,
uses_stdin_tape,
stdin_tape,
)
}
fn run_command(
&self,
mut cmd: Command,
slot: Slot,
warnings: String,
uses_stdin_tape: bool,
stdin_tape: &[proc::StdinEvent],
) -> Result<Outcome> {
let runtime_capture = self
.runtime_checks
.map(|checks| checks.capture(&mut cmd))
.transpose()
.context("Unable to capture runtime check diagnostics")?;
let executable = cmd.get_program().to_os_string();
let taped = uses_stdin_tape || !stdin_tape.is_empty();
let (request_tx, request_rx) = mpsc::channel();
let live = (self.stream_output || taped).then(|| {
LiveStreams::new(
self.stream_output,
request_tx,
self.runtime_checks
.map(|_| self.src_path().display().to_string()),
)
});
let start_time = std::time::Instant::now();
let cap = if taped {
proc::run_observed_with_stdin_tape(
&mut cmd,
self.timeout,
stdin_tape,
self.allow_program_stdin,
request_rx,
live.as_ref().expect("taped observer").observers(),
)
} else if let Some(live) = &live {
proc::run_observed(
&mut cmd,
self.timeout,
self.allow_program_stdin,
live.observers(),
)
} else {
proc::run_captured(&mut cmd, self.timeout, self.allow_program_stdin)
}
.with_context(|| {
i18n::text_with(
"program-start-error",
&[("path", executable.to_string_lossy().into_owned())],
)
})?;
let process_duration = start_time.elapsed();
if let Some(live) = &live {
live.finish();
}
let out = String::from_utf8_lossy(&cap.stdout).into_owned();
let raw_err = String::from_utf8_lossy(&cap.stderr).into_owned();
let (err, stderr_report) = if self.runtime_checks.is_some()
&& cap.status.is_some_and(|status| !status.success())
{
runtime_checks::split_stderr(&raw_err, &self.src_path().display().to_string())
} else {
(raw_err.as_str(), None)
};
let mut runtime_report = runtime_capture
.as_ref()
.map(|capture| capture.read())
.transpose()
.context("Unable to read runtime check diagnostics")?
.filter(|report| !report.is_empty());
if let Some(report) = stderr_report {
runtime_report
.get_or_insert_with(String::new)
.push_str(report);
}
let out_parts = split_new(&out, true);
let err_parts = split_new(err, false);
let timed_duration = out_parts.time_ns.map(Duration::from_nanos);
let duration = timed_duration.unwrap_or(process_duration);
let checked_failure = self.runtime_checks.is_some()
&& (runtime_report.is_some()
|| !cap.status.is_some_and(|status| status.success())
|| !out_parts.done
|| !err_parts.done
|| cap.stdout_truncated
|| cap.stderr_truncated
|| cap.stdin_diverged);
let abnormal = if cap.interrupted {
Some(if self.runtime_checks.is_some() {
"Runtime check interrupted.".to_string()
} else {
i18n::text("program-interrupted")
})
} else if checked_failure {
Some(if cap.status.is_none() {
format!("Runtime check timed out after {}s.", self.timeout.as_secs())
} else if runtime_report.is_some() {
"Runtime check failed.".to_string()
} else if cap.stdout_truncated || cap.stderr_truncated {
"Runtime check output exceeded the capture limit.".to_string()
} else if cap.stdin_diverged {
"Runtime check failed: recorded stdin diverged during replay.".to_string()
} else {
"Runtime check did not complete successfully.".to_string()
})
} else {
match cap.status {
None => Some(i18n::text_with(
"program-killed",
&[("seconds", self.timeout.as_secs().to_string())],
)),
Some(st) => describe_abnormal(&st),
}
.or_else(|| {
(cap.stdout_truncated || cap.stderr_truncated).then(|| {
i18n::text_with(
"program-output-truncated",
&[("mib", (proc::MAX_CAPTURE_BYTES / (1024 * 1024)).to_string())],
)
})
})
.or_else(|| {
cap.stdin_diverged
.then(|| i18n::text("stdin-tape-diverged"))
})
.or_else(|| {
(!out_parts.done || !err_parts.done).then(|| i18n::text("program-exited-early"))
})
};
let errors = if checked_failure {
let diagnostic = if self.stream_output {
match err.find(M_NEW) {
None => err,
Some(_) => err.rsplit_once(M_DONE).map_or("", |(_, tail)| tail),
}
} else {
err
};
diagnostic
.replace(M_NEW, "")
.replace(M_DONE, "")
.replace(M_STDIN, "")
.replace(M_STDIN_DONE, "")
} else if self.stream_output {
String::new()
} else {
err_parts.output
};
if self.stream_output && !checked_failure {
let live = live.as_ref().expect("live output");
live.restore_stderr(&live.take_suppressed_stderr());
}
let streamed_output_needs_newline = live
.as_ref()
.is_some_and(|live| live.terminal_needs_newline());
Ok(Outcome {
slot,
output: if self.stream_output {
String::new()
} else {
out_parts.output
},
value: out_parts.value.filter(|_| {
slot == Slot::Expr
&& !checked_failure
&& out_parts.done
&& err_parts.done
&& !cap.stdout_truncated
&& !cap.stderr_truncated
}),
errors,
runtime_report,
streamed_output_needs_newline,
stdin_events: cap.stdin_recorded,
warnings,
abnormal,
rewritten: None,
scoped_rebind: false,
file_replacement: None,
unprintable: false,
duration,
timed_duration,
})
}
pub fn timeit(&self, session: &Session, input: &str) -> Result<Eval> {
let mut prog = codegen::build_timeit_probe(session, input);
let (exe, warnings) = match self.compile_program(&prog) {
Ok(compiled) => compiled,
Err(_) if !input.trim_end().ends_with(';') => {
let patched = format!("{};", input.trim_end());
prog = codegen::build_timeit_probe(session, &patched);
match self.compile_program(&prog) {
Ok(compiled) => compiled,
Err(diag) => return Ok(Eval::CompileError(diag)),
}
}
Err(diag) => return Ok(Eval::CompileError(diag)),
};
let run = |loops: u64| {
let mut command = Command::new(&exe);
command.arg(loops.to_string());
self.run_command(
command,
Slot::Expr,
warnings.clone(),
prog.uses_stdin_tape,
session.stdin_tape(),
)
};
let target_ns = (self.timeout.as_nanos() / 10).clamp(1, 100_000_000) as u64;
let mut loops = 1;
let mut calibration = run(loops)?;
loop {
if calibration.abnormal.is_some() {
return Ok(Eval::Done(Box::new(calibration)));
}
let elapsed_ns = calibration
.value
.as_deref()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let Some(next) = next_timeit_loops(loops, elapsed_ns, target_ns) else {
break;
};
loops = next;
calibration = run(loops)?;
}
let rounds = 5;
let mut samples_ns = Vec::with_capacity(rounds);
let mut last_outcome = calibration;
for _ in 0..rounds {
let outcome = run(loops)?;
if outcome.abnormal.is_some() {
return Ok(Eval::Done(Box::new(outcome)));
}
if let Some(ns) = outcome
.value
.as_deref()
.and_then(|value| value.parse::<u64>().ok())
{
samples_ns.push(ns as f64 / loops as f64);
last_outcome = outcome;
}
}
if samples_ns.is_empty() {
let elapsed_ns = last_outcome
.value
.as_deref()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(0);
samples_ns.push(elapsed_ns as f64 / loops as f64);
}
let count = samples_ns.len() as f64;
let mean = samples_ns.iter().sum::<f64>() / count;
let variance = if samples_ns.len() > 1 {
samples_ns.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (count - 1.0)
} else {
0.0
};
let std_dev = variance.sqrt();
let (mean_str, dev_str) = format_timeit_stats(mean, std_dev);
let report = i18n::text_with(
"timeit-report",
&[
("mean", mean_str),
("deviation", dev_str),
("runs", samples_ns.len().to_string()),
(
"run-word",
i18n::text(if samples_ns.len() == 1 {
"timeit-run"
} else {
"timeit-runs"
}),
),
("loops", format_integer(loops)),
(
"loop-word",
i18n::text(if loops == 1 {
"timeit-loop"
} else {
"timeit-loops"
}),
),
],
);
Ok(Eval::Done(Box::new(Outcome {
slot: Slot::Expr,
output: last_outcome.output,
value: Some(report),
errors: last_outcome.errors,
runtime_report: last_outcome.runtime_report,
streamed_output_needs_newline: last_outcome.streamed_output_needs_newline,
stdin_events: Vec::new(),
warnings: last_outcome.warnings,
abnormal: None,
rewritten: None,
scoped_rebind: false,
file_replacement: None,
unprintable: false,
duration: last_outcome.duration,
timed_duration: None,
})))
}
}
fn next_timeit_loops(loops: u64, elapsed_ns: u64, target_ns: u64) -> Option<u64> {
const MAX_LOOPS: u64 = 100_000_000;
if elapsed_ns >= target_ns || loops >= MAX_LOOPS {
return None;
}
let estimate = (u128::from(target_ns) * u128::from(loops))
.div_ceil(u128::from(elapsed_ns.max(1)))
.min(u128::from(MAX_LOOPS)) as u64;
Some(
estimate
.max(loops.saturating_mul(2).min(MAX_LOOPS))
.min(loops.saturating_mul(10).min(MAX_LOOPS)),
)
}
fn render_utf8_payload(value: &str) -> Option<String> {
const LIMIT: usize = 100;
let payload = value.strip_prefix(M_UTF8)?;
let (size, hex) = payload.split_once(':')?;
let size = size.parse::<usize>().ok()?;
if hex.len() % 2 != 0 || hex.len() / 2 != size.min(LIMIT) {
return None;
}
let bytes = hex
.as_bytes()
.chunks_exact(2)
.map(|pair| {
let text = std::str::from_utf8(pair).ok()?;
u8::from_str_radix(text, 16).ok()
})
.collect::<Option<Vec<_>>>()?;
if size > LIMIT {
return Some(render_byte_array(&bytes, size));
}
let text_bytes = bytes.strip_suffix(&[0]).unwrap_or(bytes.as_slice());
let Ok(text) = std::str::from_utf8(text_bytes) else {
return Some(render_byte_array(&bytes, size));
};
let escaped = text.chars().map(escape_utf8_char).collect::<String>();
let code_units = bytes
.iter()
.map(|byte| format!("0x{byte:02x}"))
.collect::<Vec<_>>()
.join(", ");
Some(format!(
"u8\"{escaped}\"\n{} {{{code_units}}}",
i18n::text("unicode-code-units")
))
}
fn localize_bits_output(value: &str) -> String {
value
.lines()
.map(|line| {
let translate_prefix = |prefix: &str, key: &str| {
line.strip_prefix(prefix)
.map(|rest| format!("{} {rest}", i18n::text(key)))
};
translate_prefix("type: ", "bits-type")
.or_else(|| {
line.strip_prefix("size: ").map(|rest| {
let rest = rest
.strip_suffix(" bytes")
.map(|number| format!("{number} {}", i18n::text("bits-bytes")))
.or_else(|| {
rest.strip_suffix(" byte")
.map(|number| format!("{number} {}", i18n::text("bits-byte")))
})
.unwrap_or_else(|| rest.to_string());
format!("{} {rest}", i18n::text("bits-size"))
})
})
.or_else(|| translate_prefix("value: ", "bits-value"))
.or_else(|| translate_prefix("hex: ", "bits-hex"))
.or_else(|| translate_prefix("binary: ", "bits-binary"))
.or_else(|| translate_prefix("memory: ", "bits-memory"))
.or_else(|| {
line.strip_prefix("byte order: ").map(|order| {
let order = match order {
"little-endian" => i18n::text("bits-little-endian"),
"big-endian" => i18n::text("bits-big-endian"),
_ => order.to_string(),
};
format!("{} {order}", i18n::text("bits-byte-order"))
})
})
.or_else(|| translate_prefix("sign: ", "bits-sign"))
.or_else(|| {
translate_prefix("exponent: ", "bits-exponent").map(|line| {
line.replace(
"(zero/subnormal)",
&format!("({})", i18n::text("bits-zero-subnormal")),
)
.replace(
"(infinity/NaN)",
&format!("({})", i18n::text("bits-infinity-nan")),
)
})
})
.or_else(|| translate_prefix("fraction: ", "bits-fraction"))
.unwrap_or_else(|| line.to_string())
})
.collect::<Vec<_>>()
.join("\n")
}
fn render_unicode_payload(value: &str, encoding: UnicodeEncoding, limit: usize) -> Option<String> {
let payload = value.strip_prefix(M_UNICODE)?;
let mut fields = payload.split(':');
let actual_width = fields.next()?.parse::<usize>().ok()?;
let address = fields.next()?;
let encoded_units = fields.next()?;
let status = fields.next()?;
let count = fields.next()?.parse::<usize>().ok()?;
if fields.next().is_some()
|| address.is_empty()
|| !address.bytes().all(|byte| byte.is_ascii_hexdigit())
|| !matches!(status, "N" | "M" | "T" | "L" | "E")
{
return None;
}
let units = if encoded_units.is_empty() {
Vec::new()
} else {
encoded_units
.split(',')
.map(|unit| {
(unit.len() == 8)
.then(|| u32::from_str_radix(unit, 16).ok())
.flatten()
})
.collect::<Option<Vec<_>>>()?
};
if count != units.len() || matches!(status, "N" | "M") && count != 0 {
return None;
}
let mut lines = vec![format!(
"{} {}",
i18n::text("unicode-encoding"),
encoding.label()
)];
if status == "N" {
lines.push(format!("{} NULL", i18n::text("unicode-address")));
return Some(lines.join("\n"));
}
lines.push(format!("{} 0x{address}", i18n::text("unicode-address")));
if status == "M" {
lines.push(i18n::text_with(
"unicode-width-error",
&[
("expected", encoding.unit_width().to_string()),
("actual", actual_width.to_string()),
],
));
return Some(lines.join("\n"));
}
if actual_width != encoding.unit_width() {
return None;
}
let text_units = units.strip_suffix(&[0]).unwrap_or(units.as_slice());
let text_label = i18n::text(if status == "L" {
"unicode-text-prefix"
} else {
"unicode-text"
});
match decode_unicode(text_units, encoding) {
Ok(text) => {
let escaped = text.chars().map(escape_utf8_char).collect::<String>();
lines.push(format!(
"{text_label} {}\"{escaped}\"",
encoding.literal_prefix()
));
}
Err(error) => lines.push(format!("{text_label} <{error}>")),
}
let code_units = units
.iter()
.map(|unit| format!("0x{unit:0width$x}", width = encoding.hex_digits()))
.collect::<Vec<_>>()
.join(", ");
lines.push(format!(
"{} {{{code_units}}}",
i18n::text("unicode-code-units")
));
if status == "L" {
lines.push(i18n::text_with(
"unicode-no-nul",
&[("limit", limit.to_string())],
));
}
Some(lines.join("\n"))
}
fn decode_unicode(units: &[u32], encoding: UnicodeEncoding) -> std::result::Result<String, String> {
match encoding {
UnicodeEncoding::Utf8 => {
let bytes = units
.iter()
.enumerate()
.map(|(index, &unit)| {
u8::try_from(unit).map_err(|_| {
i18n::text_with(
"unicode-invalid-utf8-unit",
&[("index", index.to_string())],
)
})
})
.collect::<std::result::Result<Vec<_>, _>>()?;
std::str::from_utf8(&bytes)
.map(str::to_string)
.map_err(|error| {
i18n::text_with(
"unicode-invalid-utf8-sequence",
&[("index", error.valid_up_to().to_string())],
)
})
}
UnicodeEncoding::Utf16 => decode_utf16(units),
UnicodeEncoding::Utf32 => units
.iter()
.enumerate()
.map(|(index, &unit)| {
char::from_u32(unit).ok_or_else(|| {
i18n::text_with("unicode-invalid-utf32", &[("index", index.to_string())])
})
})
.collect(),
}
}
fn decode_utf16(units: &[u32]) -> std::result::Result<String, String> {
let mut text = String::new();
let mut index = 0usize;
while index < units.len() {
let first = units[index];
if first > u16::MAX as u32 {
return Err(i18n::text_with(
"unicode-invalid-utf16-unit",
&[("index", index.to_string())],
));
}
if (0xd800..=0xdbff).contains(&first) {
let Some(&second) = units.get(index + 1) else {
return Err(i18n::text_with(
"unicode-unpaired-high",
&[("index", index.to_string())],
));
};
if !(0xdc00..=0xdfff).contains(&second) {
return Err(i18n::text_with(
"unicode-unpaired-high",
&[("index", index.to_string())],
));
}
let scalar = 0x10000 + ((first - 0xd800) << 10) + (second - 0xdc00);
text.push(char::from_u32(scalar).expect("paired surrogates form a scalar"));
index += 2;
} else if (0xdc00..=0xdfff).contains(&first) {
return Err(i18n::text_with(
"unicode-unpaired-low",
&[("index", index.to_string())],
));
} else {
text.push(char::from_u32(first).expect("non-surrogate u16 is a scalar"));
index += 1;
}
}
Ok(text)
}
fn render_byte_array(bytes: &[u8], size: usize) -> String {
let mut elements = bytes.iter().map(u8::to_string).collect::<Vec<_>>();
if size > bytes.len() {
elements.push(i18n::text_with(
"array-more",
&[("count", (size - bytes.len()).to_string())],
));
}
format!("{{{}}}", elements.join(", "))
}
fn escape_utf8_char(character: char) -> String {
match character {
'"' => "\\\"".to_string(),
'\\' => "\\\\".to_string(),
'\0' => "\\0".to_string(),
'\n' => "\\n".to_string(),
'\r' => "\\r".to_string(),
'\t' => "\\t".to_string(),
character
if character.is_control()
|| matches!(
character,
'\u{00ad}'
| '\u{061c}'
| '\u{200b}'..='\u{200f}'
| '\u{2028}'..='\u{202e}'
| '\u{2060}'..='\u{206f}'
| '\u{feff}'
| '\u{fff9}'..='\u{fffb}'
| '\u{e0000}'..='\u{e007f}'
) =>
{
format!("\\u{{{:x}}}", character as u32)
}
character => character.to_string(),
}
}
fn format_timeit_stats(mean_ns: f64, std_dev_ns: f64) -> (String, String) {
if mean_ns < 1_000.0 {
(
format!("{:.1} ns", mean_ns),
format!("{:.1} ns", std_dev_ns),
)
} else if mean_ns < 1_000_000.0 {
(
format!("{:.2} µs", mean_ns / 1000.0),
format!("{:.2} µs", std_dev_ns / 1000.0),
)
} else if mean_ns < 1_000_000_000.0 {
(
format!("{:.2} ms", mean_ns / 1_000_000.0),
format!("{:.2} ms", std_dev_ns / 1_000_000.0),
)
} else {
(
format!("{:.2} s", mean_ns / 1_000_000_000.0),
format!("{:.2} s", std_dev_ns / 1_000_000_000.0),
)
}
}
fn format_integer(mut n: u64) -> String {
if n == 0 {
return "0".to_string();
}
let mut s = String::new();
let mut count = 0;
while n > 0 {
if count > 0 && count % 3 == 0 {
s.insert(0, ',');
}
s.insert(0, (b'0' + (n % 10) as u8) as char);
n /= 10;
count += 1;
}
s
}
struct SplitOutput {
output: String,
value: Option<String>,
time_ns: Option<u64>,
done: bool,
}
fn split_new(s: &str, has_value: bool) -> SplitOutput {
let Some(start) = s.rfind(M_NEW) else {
return SplitOutput {
output: s.to_string(),
value: None,
time_ns: None,
done: false,
};
};
let tail = &s[start + M_NEW.len()..];
let (body, done) = match tail.rfind(M_DONE) {
Some(i) => (&tail[..i], true),
None => (tail, false),
};
let mut body = body.replace(M_STDIN, "").replace(M_STDIN_DONE, "");
let time_ns = body.rfind(M_TIME).and_then(|i| {
let payload = body[i + M_TIME.len()..]
.trim_end_matches(['\r', '\n'])
.parse()
.ok();
body.truncate(i);
payload
});
if has_value && let Some(i) = body.rfind(M_VAL) {
return SplitOutput {
output: body[..i].to_string(),
value: Some(
body[i + M_VAL.len()..]
.trim_end_matches(['\r', '\n'])
.to_string(),
),
time_ns,
done,
};
}
SplitOutput {
output: body,
value: None,
time_ns,
done,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LiveState {
BeforeInput,
Output,
Value,
Done,
}
struct LiveFilter {
state: LiveState,
stdout: bool,
pending: Vec<u8>,
visible_any: bool,
visible_ends_in_newline: bool,
stdin_requests: Vec<proc::StdinRequest>,
}
impl LiveFilter {
fn new(stdout: bool) -> Self {
Self {
state: LiveState::BeforeInput,
stdout,
pending: Vec::new(),
visible_any: false,
visible_ends_in_newline: false,
stdin_requests: Vec::new(),
}
}
fn feed(&mut self, chunk: &[u8]) -> Vec<u8> {
self.pending.extend_from_slice(chunk);
let mut visible = Vec::new();
loop {
match self.state {
LiveState::BeforeInput => {
let new = find_bytes(&self.pending, M_NEW.as_bytes());
let request = (!self.stdout)
.then(|| find_bytes(&self.pending, M_STDIN.as_bytes()))
.flatten();
let finished = (!self.stdout)
.then(|| find_bytes(&self.pending, M_STDIN_DONE.as_bytes()))
.flatten();
if finished
.is_some_and(|f| new.is_none_or(|n| f < n) && request.is_none_or(|r| f < r))
{
let i = finished.expect("checked completion");
self.pending.drain(..i + M_STDIN_DONE.len());
self.stdin_requests.push(proc::StdinRequest::Finished);
} else if request.is_some_and(|r| new.is_none_or(|n| r < n)) {
let i = request.expect("checked request");
self.pending.drain(..i + M_STDIN.len());
self.stdin_requests.push(proc::StdinRequest::Replay);
} else if let Some(i) = new {
self.pending.drain(..i + M_NEW.len());
self.state = LiveState::Output;
} else {
let markers: &[&[u8]] = if self.stdout {
&[M_NEW.as_bytes()]
} else {
&[
M_NEW.as_bytes(),
M_STDIN.as_bytes(),
M_STDIN_DONE.as_bytes(),
]
};
retain_marker_prefix(&mut self.pending, markers);
break;
}
}
LiveState::Output => {
let value = self
.stdout
.then(|| find_bytes(&self.pending, M_VAL.as_bytes()))
.flatten();
let timing = self
.stdout
.then(|| find_bytes(&self.pending, M_TIME.as_bytes()))
.flatten();
let done = find_bytes(&self.pending, M_DONE.as_bytes());
let request = (!self.stdout)
.then(|| find_bytes(&self.pending, M_STDIN.as_bytes()))
.flatten();
let mut next = Vec::new();
if let Some(i) = value {
next.push((i, M_VAL.len(), Some(LiveState::Value)));
}
if let Some(i) = timing {
next.push((i, M_TIME.len(), Some(LiveState::Value)));
}
if let Some(i) = done {
next.push((i, M_DONE.len(), Some(LiveState::Done)));
}
if let Some(i) = request {
next.push((i, M_STDIN.len(), None));
}
if !self.stdout
&& let Some(i) = find_bytes(&self.pending, M_STDIN_DONE.as_bytes())
{
next.push((i, M_STDIN_DONE.len(), None));
}
if let Some((i, marker_len, state)) =
next.into_iter().min_by_key(|(i, _, _)| *i)
{
visible.extend_from_slice(&self.pending[..i]);
self.pending.drain(..i + marker_len);
if let Some(state) = state {
self.state = state;
} else {
self.stdin_requests
.push(if marker_len == M_STDIN_DONE.len() {
proc::StdinRequest::Finished
} else {
proc::StdinRequest::Current
});
}
} else {
let markers: &[&[u8]] = if self.stdout {
&[M_VAL.as_bytes(), M_TIME.as_bytes(), M_DONE.as_bytes()]
} else {
&[
M_DONE.as_bytes(),
M_STDIN.as_bytes(),
M_STDIN_DONE.as_bytes(),
]
};
let keep = trailing_marker_prefix(&self.pending, markers);
let safe = self.pending.len() - keep;
visible.extend_from_slice(&self.pending[..safe]);
self.pending.drain(..safe);
break;
}
}
LiveState::Value => {
if let Some(i) = find_bytes(&self.pending, M_DONE.as_bytes()) {
self.pending.drain(..i + M_DONE.len());
self.state = LiveState::Done;
} else {
retain_marker_prefix(&mut self.pending, &[M_DONE.as_bytes()]);
break;
}
}
LiveState::Done => {
self.pending.clear();
break;
}
}
}
self.record_visible(&visible);
visible
}
fn take_stdin_requests(&mut self) -> Vec<proc::StdinRequest> {
std::mem::take(&mut self.stdin_requests)
}
fn finish(&mut self) -> Vec<u8> {
let visible = if self.state == LiveState::Output {
std::mem::take(&mut self.pending)
} else {
self.pending.clear();
Vec::new()
};
self.record_visible(&visible);
visible
}
fn record_visible(&mut self, visible: &[u8]) {
if let Some(last) = visible.last() {
self.visible_any = true;
self.visible_ends_in_newline = *last == b'\n';
}
}
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
fn trailing_marker_prefix(bytes: &[u8], markers: &[&[u8]]) -> usize {
markers
.iter()
.map(|marker| {
let max = bytes.len().min(marker.len().saturating_sub(1));
(1..=max)
.rev()
.find(|&n| bytes.ends_with(&marker[..n]))
.unwrap_or(0)
})
.max()
.unwrap_or(0)
}
fn retain_marker_prefix(bytes: &mut Vec<u8>, markers: &[&[u8]]) {
let keep = trailing_marker_prefix(bytes, markers);
bytes.drain(..bytes.len() - keep);
}
#[derive(Default)]
struct TerminalVisibility {
visible_any: bool,
visible_ends_in_newline: bool,
}
impl TerminalVisibility {
fn record(&mut self, visible: &[u8]) {
if let Some(last) = visible.last() {
self.visible_any = true;
self.visible_ends_in_newline = *last == b'\n';
}
}
fn needs_newline(&self) -> bool {
self.visible_any && !self.visible_ends_in_newline
}
}
struct LiveStreams {
stdout: Arc<Mutex<LiveFilter>>,
stderr: Arc<Mutex<LiveFilter>>,
terminal: Arc<Mutex<TerminalVisibility>>,
stream_visible: bool,
stdin_requests: mpsc::Sender<proc::StdinRequest>,
stderr_report_filter: Option<Arc<Mutex<runtime_checks::StderrFilter>>>,
}
impl LiveStreams {
fn restore_stderr(&self, bytes: &[u8]) {
if bytes.is_empty() {
return;
}
let mut terminal = self.terminal.lock().expect("terminal visibility");
let mut stderr = std::io::stderr().lock();
let _ = stderr.write_all(bytes);
let _ = stderr.flush();
terminal.record(bytes);
}
fn take_suppressed_stderr(&self) -> Vec<u8> {
self.stderr_report_filter
.as_ref()
.map(|filter| {
filter
.lock()
.expect("runtime stderr filter")
.take_suppressed()
})
.unwrap_or_default()
}
fn new(
stream_visible: bool,
stdin_requests: mpsc::Sender<proc::StdinRequest>,
runtime_source: Option<String>,
) -> Self {
Self {
stdout: Arc::new(Mutex::new(LiveFilter::new(true))),
stderr: Arc::new(Mutex::new(LiveFilter::new(false))),
terminal: Arc::new(Mutex::new(TerminalVisibility::default())),
stream_visible,
stdin_requests,
stderr_report_filter: runtime_source
.map(|source| Arc::new(Mutex::new(runtime_checks::StderrFilter::new(&source)))),
}
}
fn observers(&self) -> proc::Observers {
let stdout = Arc::clone(&self.stdout);
let stderr = Arc::clone(&self.stderr);
let stdout_terminal = Arc::clone(&self.terminal);
let stderr_terminal = Arc::clone(&self.terminal);
let stdout_requests = self.stdin_requests.clone();
let stderr_requests = self.stdin_requests.clone();
let stream_stdout = self.stream_visible;
let stream_stderr = self.stream_visible;
let stderr_report_filter = self.stderr_report_filter.clone();
proc::Observers {
stdout: Arc::new(move |chunk| {
let (visible, requests) = {
let mut filter = stdout.lock().expect("stdout filter");
let visible = filter.feed(chunk);
(visible, filter.take_stdin_requests())
};
for request in requests {
let _ = stdout_requests.send(request);
}
if stream_stdout && !visible.is_empty() {
let mut terminal = stdout_terminal.lock().expect("terminal visibility");
let mut out = std::io::stdout().lock();
let _ = out.write_all(&visible);
let _ = out.flush();
terminal.record(&visible);
}
}),
stderr: Arc::new(move |chunk| {
let (mut visible, requests) = {
let mut filter = stderr.lock().expect("stderr filter");
let visible = filter.feed(chunk);
(visible, filter.take_stdin_requests())
};
for request in requests {
let _ = stderr_requests.send(request);
}
if let Some(filter) = &stderr_report_filter {
visible = filter.lock().expect("runtime stderr filter").feed(&visible);
}
if stream_stderr && !visible.is_empty() {
let mut terminal = stderr_terminal.lock().expect("terminal visibility");
let mut err = std::io::stderr().lock();
let _ = err.write_all(&visible);
let _ = err.flush();
terminal.record(&visible);
}
}),
}
}
fn finish(&self) {
let mut err_tail = self.stderr.lock().expect("stderr filter").finish();
if let Some(filter) = &self.stderr_report_filter {
let mut filter = filter.lock().expect("runtime stderr filter");
err_tail = filter.feed(&err_tail);
err_tail.extend(filter.finish());
}
let out_tail = self.stdout.lock().expect("stdout filter").finish();
if self.stream_visible && !out_tail.is_empty() {
let mut terminal = self.terminal.lock().expect("terminal visibility");
let mut out = std::io::stdout().lock();
let _ = out.write_all(&out_tail);
let _ = out.flush();
terminal.record(&out_tail);
}
if self.stream_visible && !err_tail.is_empty() {
let mut terminal = self.terminal.lock().expect("terminal visibility");
let mut err = std::io::stderr().lock();
let _ = err.write_all(&err_tail);
let _ = err.flush();
terminal.record(&err_tail);
}
}
fn terminal_needs_newline(&self) -> bool {
self.terminal
.lock()
.expect("terminal visibility")
.needs_newline()
}
}
#[cfg(unix)]
fn describe_abnormal(st: &std::process::ExitStatus) -> Option<String> {
use std::os::unix::process::ExitStatusExt;
let sig = st.signal()?;
Some(match sig {
11 => i18n::text("signal-segv"),
6 => i18n::text("signal-abrt"),
8 => i18n::text("signal-fpe"),
4 => i18n::text("signal-ill"),
7 | 10 => i18n::text("signal-bus"),
_ => i18n::text_with("signal-other", &[("signal", sig.to_string())]),
})
}
#[cfg(windows)]
fn describe_abnormal(st: &std::process::ExitStatus) -> Option<String> {
let code = st.code()? as u32;
Some(match code {
0xC0000005 => i18n::text("windows-access-violation"),
0xC0000094 => i18n::text("windows-division-zero"),
0xC000001D => i18n::text("windows-illegal-instruction"),
0xC00000FD => i18n::text("windows-stack-overflow"),
0xC0000409 => i18n::text("windows-buffer-overrun"),
_ => return None,
})
}
fn is_rebinding_diagnostic(diag: &str) -> bool {
let lower = diag.to_ascii_lowercase();
[
"redefinition",
"redeclaration",
"redeclared",
"conflicting types for",
"already has a body",
]
.iter()
.any(|needle| lower.contains(needle))
}
pub(crate) fn should_try_expr(input: &str) -> bool {
let scan = lex::scan(input);
let Some(last) = input
.as_bytes()
.iter()
.enumerate()
.rev()
.find(|(i, byte)| scan.code[*i] && !byte.is_ascii_whitespace())
.map(|(i, _)| i)
else {
return false;
};
match input.as_bytes()[last] {
b';' => false,
b'}' => ends_in_possible_compound_literal(input, &scan.code, last),
_ => true,
}
}
fn ends_in_possible_compound_literal(input: &str, code: &[bool], close: usize) -> bool {
let bytes = input.as_bytes();
let mut depth = 0usize;
let mut open = None;
for i in (0..=close).rev() {
if !code[i] {
continue;
}
match bytes[i] {
b'}' => depth += 1,
b'{' => {
depth -= 1;
if depth == 0 {
open = Some(i);
break;
}
}
_ => {}
}
}
let Some(open) = open else { return true };
let Some(before) = (0..open)
.rev()
.find(|&i| code[i] && !bytes[i].is_ascii_whitespace())
else {
return false;
};
if bytes[before] != b')' {
return false;
}
!lex::identifiers(input).first().is_some_and(|word| {
matches!(
word.as_str(),
"if" | "for" | "while" | "switch" | "do" | "else" | "case"
)
})
}
fn looks_file_scope(input: &str) -> bool {
let t = lex::trim_leading_trivia(input);
if t.starts_with('#') {
return true;
}
let first: String = t
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if matches!(
first.as_str(),
"if" | "for"
| "while"
| "switch"
| "do"
| "else"
| "return"
| "case"
| "goto"
| "break"
| "continue"
| "sizeof"
) {
return false;
}
if first == "typedef" || lex::function_definition(input).is_some() {
return true;
}
if matches!(first.as_str(), "struct" | "union" | "enum") {
let after_kw = lex::trim_leading_trivia(&t[first.len()..]);
let tag: String = after_kw
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if lex::trim_leading_trivia(&after_kw[tag.len()..]).starts_with('{') {
return true;
}
return false;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "real-compiler performance measurement"]
fn benchmark_evaluation_strategies() {
let tc = crate::toolchain::Toolchain::detect(None, None).unwrap();
println!("compiler: {}", tc.version);
println!("history,target,cold_compiles,warm_compiles,median_ms");
for history in [0, 10, 100, 1000] {
let mut session = Session::default();
session.commit(
"int value = 42; int matrix[2][2] = {{1,2},{3,4}}; int *pointer = &matrix[0][0];",
Slot::Stmt,
);
for index in 0..history {
session.commit(&format!("int item_{index} = {index};"), Slot::Stmt);
}
for target in ["value", "pointer", "matrix"] {
let ev = Evaluator::new(tc.clone(), Duration::from_secs(15)).unwrap();
let mut samples = Vec::new();
let mut counts = Vec::new();
for _ in 0..3 {
ev.compile_count.set(0);
let start = std::time::Instant::now();
let Eval::Done(outcome) = ev.eval(&session, target).unwrap() else {
panic!("benchmark failed");
};
assert!(outcome.abnormal.is_none());
std::hint::black_box(outcome.value);
samples.push(start.elapsed().as_secs_f64() * 1000.0);
counts.push(ev.compile_count.get());
}
samples.sort_by(f64::total_cmp);
println!(
"{history},{target},{},{},{:.2}",
counts[0], counts[2], samples[1]
);
}
}
}
#[test]
fn timeit_calibration_bounds_growth_and_honors_target_and_limit() {
assert_eq!(next_timeit_loops(1, 0, 100_000_000), Some(10));
assert_eq!(next_timeit_loops(100, 1, 100_000_000), Some(1000));
assert_eq!(next_timeit_loops(1000, 50_000_000, 100_000_000), Some(2000));
assert_eq!(next_timeit_loops(100, 99_999_999, 100_000_000), Some(200));
assert_eq!(next_timeit_loops(1, 100_000_000, 100_000_000), None);
assert_eq!(next_timeit_loops(100_000_000, 0, 100_000_000), None);
assert_eq!(
next_timeit_loops(90_000_000, 0, 100_000_000),
Some(100_000_000)
);
}
#[test]
fn skips_impossible_expression_trials_without_losing_compound_literals() {
assert!(!should_try_expr("int value = 1;"));
assert!(!should_try_expr("value++; // retained statement"));
assert!(!should_try_expr("if (value) { value--; }"));
assert!(!should_try_expr("{ value++; }"));
assert!(should_try_expr("value + 1"));
assert!(should_try_expr("(int){ 7 }"));
assert!(should_try_expr("value = (struct Box){ 7 }"));
}
#[test]
fn recognizes_portable_rebinding_diagnostics() {
assert!(is_rebinding_diagnostic("error: redefinition of 'f'"));
assert!(is_rebinding_diagnostic("error: conflicting types for 'x'"));
assert!(is_rebinding_diagnostic(
"error C2084: function 'f' already has a body"
));
assert!(!is_rebinding_diagnostic("error: expected expression"));
}
#[test]
fn recognizes_main_definitions_without_matching_similar_expressions() {
assert_eq!(
lex::function_definition("int main(void) { return 0; }").map(|(name, _)| name),
Some("main")
);
assert_eq!(
lex::function_definition("int (main)(int argc, char **argv) { return argc; }")
.map(|(name, _)| name),
Some("main")
);
assert_eq!(
lex::function_definition("int domain(void) { return 1; }").map(|(name, _)| name),
Some("domain")
);
assert_eq!(lex::function_definition("Point p = (Point){1, 2};"), None);
}
#[test]
fn routes_definitions_to_file_scope() {
assert!(looks_file_scope("int add(int a, int b) { return a + b; }"));
assert!(looks_file_scope(
"int get(struct Local { int x; } v) { return v.x; }"
));
assert!(looks_file_scope("#include <time.h>"));
assert!(looks_file_scope("typedef unsigned long ulong;"));
assert!(looks_file_scope("struct P { int x; int y; };"));
}
#[test]
fn keeps_statements_in_main() {
assert!(!looks_file_scope("if (x > 0) { puts(\"hi\"); }"));
assert!(!looks_file_scope("for (int i = 0; i < 3; i++) { }"));
assert!(!looks_file_scope("(struct P){ 1, 2 }"));
assert!(!looks_file_scope("Point p = (Point){ 1, 2 };"));
assert!(!looks_file_scope("flag ? (Point){ 1, 2 } : other"));
assert!(!looks_file_scope("int x = 41;"));
assert!(!looks_file_scope("struct P p = {1, 2};"));
assert!(!looks_file_scope("x + 1"));
}
#[test]
fn splits_on_markers_and_requires_completion() {
let s = format!("old{M_NEW}printed{M_VAL}42\n{M_DONE}");
let got = split_new(&s, true);
assert_eq!(got.output, "printed");
assert_eq!(got.value.as_deref(), Some("42"));
assert_eq!(got.time_ns, None);
assert!(got.done);
}
#[test]
fn splits_single_input_timing_without_polluting_output_or_value() {
let expression = format!("old{M_NEW}printed{M_VAL}42\n{M_TIME}123\n{M_DONE}");
let got = split_new(&expression, true);
assert_eq!(got.output, "printed");
assert_eq!(got.value.as_deref(), Some("42"));
assert_eq!(got.time_ns, Some(123));
assert!(got.done);
let statement = format!("old{M_NEW}printed{M_TIME}456\n{M_DONE}");
let got = split_new(&statement, true);
assert_eq!(got.output, "printed");
assert_eq!(got.value, None);
assert_eq!(got.time_ns, Some(456));
assert!(got.done);
}
#[test]
fn renders_only_valid_complete_utf8_payloads_as_text() {
let smiley = format!("{M_UTF8}5:f09f998200");
assert_eq!(
render_utf8_payload(&smiley).as_deref(),
Some("u8\"🙂\"\ncode units: {0xf0, 0x9f, 0x99, 0x82, 0x00}")
);
let escaped = format!("{M_UTF8}8:61220a00e280ae00");
assert_eq!(
render_utf8_payload(&escaped).as_deref(),
Some(
"u8\"a\\\"\\n\\0\\u{202e}\"\ncode units: {0x61, 0x22, 0x0a, 0x00, 0xe2, 0x80, 0xae, 0x00}"
)
);
let invalid = format!("{M_UTF8}2:ff00");
assert_eq!(render_utf8_payload(&invalid).as_deref(), Some("{255, 0}"));
let malformed_sequence = format!("{M_UTF8}3:c32800");
assert_eq!(
render_utf8_payload(&malformed_sequence).as_deref(),
Some("{195, 40, 0}")
);
assert!(render_utf8_payload("not an internal payload").is_none());
}
#[test]
fn renders_utf8_utf16_and_utf32_probe_payloads_strictly() {
let utf8 = format!("{M_UNICODE}1:1234:00000041,000000e5,000000a5,000000bd,00000000:T:5");
assert_eq!(
render_unicode_payload(&utf8, UnicodeEncoding::Utf8, 100).as_deref(),
Some(
"encoding: UTF-8\naddress: 0x1234\ntext: u8\"A好\"\ncode units: {0x41, 0xe5, 0xa5, 0xbd, 0x00}"
)
);
let utf16 = format!("{M_UNICODE}2:abcd:00000041,0000597d,0000d83d,0000de00,00000000:T:5");
assert_eq!(
render_unicode_payload(&utf16, UnicodeEncoding::Utf16, 100).as_deref(),
Some(
"encoding: UTF-16\naddress: 0xabcd\ntext: u\"A好😀\"\ncode units: {0x0041, 0x597d, 0xd83d, 0xde00, 0x0000}"
)
);
let utf32 = format!("{M_UNICODE}4:beef:00000041,0001f600,00000000:E:3");
assert_eq!(
render_unicode_payload(&utf32, UnicodeEncoding::Utf32, 3).as_deref(),
Some(
"encoding: UTF-32\naddress: 0xbeef\ntext: U\"A😀\"\ncode units: {0x00000041, 0x0001f600, 0x00000000}"
)
);
}
#[test]
fn unicode_probe_reports_null_mismatch_invalid_and_unterminated_data() {
let null = format!("{M_UNICODE}1:0::N:0");
assert_eq!(
render_unicode_payload(&null, UnicodeEncoding::Utf8, 100).as_deref(),
Some("encoding: UTF-8\naddress: NULL")
);
let mismatch = format!("{M_UNICODE}4:1234::M:0");
assert_eq!(
render_unicode_payload(&mismatch, UnicodeEncoding::Utf16, 100).as_deref(),
Some(
"encoding: UTF-16\naddress: 0x1234\nerror: expected 2-byte code units, but the expression points to 4-byte elements"
)
);
let invalid16 = format!("{M_UNICODE}2:1234:0000d800,00000000:T:2");
assert!(
render_unicode_payload(&invalid16, UnicodeEncoding::Utf16, 100)
.unwrap()
.contains("unpaired UTF-16 high surrogate at index 0")
);
let invalid32 = format!("{M_UNICODE}4:1234:00110000,00000000:T:2");
assert!(
render_unicode_payload(&invalid32, UnicodeEncoding::Utf32, 100)
.unwrap()
.contains("invalid UTF-32 scalar value at index 0")
);
let limited = format!("{M_UNICODE}1:1234:00000041,00000042:L:2");
assert_eq!(
render_unicode_payload(&limited, UnicodeEncoding::Utf8, 2).as_deref(),
Some(
"encoding: UTF-8\naddress: 0x1234\ntext prefix: u8\"AB\"\ncode units: {0x41, 0x42}\nnote: no NUL terminator in the first 2 code units"
)
);
}
#[test]
fn missing_start_or_done_is_incomplete() {
let no_start = split_new("crashed early", true);
assert_eq!(no_start.output, "crashed early");
assert!(!no_start.done);
let no_done = split_new(&format!("{M_NEW}before exit"), true);
assert_eq!(no_done.output, "before exit");
assert!(!no_done.done);
}
#[test]
fn live_filter_handles_markers_split_across_chunks() {
let mut f = LiveFilter::new(true);
let wire = format!("replayed{M_NEW}hello{M_VAL}42\n{M_DONE}");
let mut visible = Vec::new();
for chunk in wire.as_bytes().chunks(3) {
visible.extend(f.feed(chunk));
}
visible.extend(f.finish());
assert_eq!(visible, b"hello");
assert_eq!(f.state, LiveState::Done);
assert!(f.visible_any && !f.visible_ends_in_newline);
}
#[test]
fn live_filter_hides_statement_timing_payloads() {
let mut f = LiveFilter::new(true);
let wire = format!("replayed{M_NEW}hello{M_TIME}123\n{M_DONE}");
let mut visible = Vec::new();
for chunk in wire.as_bytes().chunks(3) {
visible.extend(f.feed(chunk));
}
visible.extend(f.finish());
assert_eq!(visible, b"hello");
assert_eq!(f.state, LiveState::Done);
}
#[test]
fn live_filter_reports_historical_and_current_stdin_requests() {
let mut filter = LiveFilter::new(false);
let wire =
format!("old{M_STDIN}{M_STDIN_DONE}{M_NEW}prompt: {M_STDIN}{M_STDIN_DONE}{M_DONE}");
let mut visible = Vec::new();
let mut requests = Vec::new();
for chunk in wire.as_bytes().chunks(3) {
visible.extend(filter.feed(chunk));
requests.extend(filter.take_stdin_requests());
}
assert_eq!(visible, b"prompt: ");
assert_eq!(
requests,
[
proc::StdinRequest::Replay,
proc::StdinRequest::Finished,
proc::StdinRequest::Current,
proc::StdinRequest::Finished
]
);
}
#[test]
fn terminal_newline_state_follows_the_last_interleaved_stream_write() {
let mut terminal = TerminalVisibility::default();
terminal.record(b"stdout without newline");
assert!(terminal.needs_newline());
terminal.record(b"stderr finishes the terminal line\n");
assert!(!terminal.needs_newline());
terminal.record(b"partial again");
assert!(terminal.needs_newline());
}
#[test]
fn runtime_report_filter_preserves_replay_and_current_stdin_requests() {
let (sender, receiver) = mpsc::channel();
let live = LiveStreams::new(false, sender, Some("/tmp/input.c".to_string()));
let observers = live.observers();
let wire = format!(
"{M_STDIN}{M_STDIN_DONE}{M_NEW}prefix\nexample.c:1:2: runtime error: demo\n{M_STDIN}after\n{M_STDIN_DONE}{M_DONE}"
);
for byte in wire.bytes() {
(observers.stderr)(&[byte]);
}
live.finish();
assert_eq!(
receiver.try_iter().collect::<Vec<_>>(),
[
proc::StdinRequest::Replay,
proc::StdinRequest::Finished,
proc::StdinRequest::Current,
proc::StdinRequest::Finished
]
);
assert_eq!(
live.take_suppressed_stderr(),
b"example.c:1:2: runtime error: demo\nafter\n"
);
}
#[test]
fn stderr_live_filter_streams_until_done() {
let mut f = LiveFilter::new(false);
let wire = format!("old{M_NEW}warning text\n{M_DONE}");
assert_eq!(f.feed(wire.as_bytes()), b"warning text\n");
assert_eq!(f.state, LiveState::Done);
assert!(f.visible_any && f.visible_ends_in_newline);
}
}