use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use crate::codegen::{self, M_NEW, M_VAL, Slot};
use crate::errmap;
use crate::lex;
use crate::proc;
use crate::session::Session;
pub struct Outcome {
pub slot: Slot,
pub output: String,
pub value: Option<String>,
pub errors: String,
pub warnings: String,
pub abnormal: Option<String>,
pub rewritten: Option<String>,
pub demoted: bool,
}
pub enum Eval {
Done(Outcome),
CompileError(String),
}
pub struct Evaluator {
pub tc: crate::toolchain::Toolchain,
dir: tempfile::TempDir,
pub timeout: Duration,
}
impl Evaluator {
pub fn new(tc: crate::toolchain::Toolchain, timeout: Duration) -> Result<Self> {
let dir = tempfile::tempdir().context("failed to create temporary directory")?;
Ok(Evaluator { tc, dir, timeout })
}
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_toolchain(&mut self, tc: crate::toolchain::Toolchain) {
self.tc = tc;
}
fn compile_text(&self, text: &str) -> std::result::Result<(PathBuf, String), String> {
let src = self.src_path();
let exe = self.exe_path();
if let Err(e) = std::fs::write(&src, text) {
return Err(format!("cannot write temporary source: {e}"));
}
let args = self.tc.compile_args(&src, &exe, self.dir.path());
let mut cmd = Command::new(&self.tc.path);
cmd.args(&args);
let cap = match proc::run_captured(&mut cmd, self.timeout, false) {
Ok(c) => c,
Err(e) => return Err(format!("cannot run compiler: {e}")),
};
let mut d = String::from_utf8_lossy(&cap.stderr).into_owned();
d.push_str(&String::from_utf8_lossy(&cap.stdout));
match cap.status {
Some(st) if st.success() => Ok((exe, d)),
Some(_) => Err(d),
None => Err(format!(
"compiler timed out after {}s and was killed",
self.timeout.as_secs()
)),
}
}
pub fn eval(&self, session: &Session, input: &str) -> Result<Eval> {
let diag = match self.attempt(session, input)? {
Ok(o) => return Ok(Eval::Done(o)),
Err(d) => d,
};
let t = input.trim_end();
let repairable = !t.ends_with(';') && !t.ends_with('}') && !t.trim_start().starts_with('#');
if repairable {
let patched = format!("{t};");
if let Ok(mut o) = self.attempt(session, &patched)? {
o.rewritten = Some(patched);
return Ok(Eval::Done(o));
}
}
Ok(Eval::CompileError(diag))
}
fn attempt(
&self,
session: &Session,
input: &str,
) -> Result<std::result::Result<Outcome, String>> {
let file_first = looks_file_scope(input);
let order: &[Slot] = if file_first {
&[Slot::FileScope, Slot::Stmt, Slot::Expr]
} else {
&[Slot::Expr, Slot::Stmt]
};
let report_slot = if file_first {
Slot::FileScope
} else {
Slot::Stmt
};
let mut reported = String::new();
for &slot in order {
let prog = codegen::build(session, input, slot);
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, start, count, prog.wrapped));
let mut o = self.run(&exe, slot, warnings)?;
o.demoted = file_first && slot != Slot::FileScope;
return Ok(Ok(o));
}
Err(diag) => {
if slot == report_slot {
reported = errmap::drop_stale_warnings(&errmap::remap(
&diag,
&src,
start,
count,
prog.wrapped,
));
}
}
}
}
Ok(Err(reported))
}
fn run(&self, exe: &PathBuf, slot: Slot, warnings: String) -> Result<Outcome> {
let mut cmd = Command::new(exe);
let cap = proc::run_captured(&mut cmd, self.timeout, true)
.with_context(|| format!("failed to start {}", exe.display()))?;
let abnormal = match cap.status {
None => Some(format!(
"killed after {}s (possible infinite loop)",
self.timeout.as_secs()
)),
Some(st) => describe_abnormal(&st),
};
let out = String::from_utf8_lossy(&cap.stdout).into_owned();
let err = String::from_utf8_lossy(&cap.stderr).into_owned();
let (output, value) = split_new(&out);
let (errors, _) = split_new(&err);
Ok(Outcome {
slot,
output,
value: value.filter(|_| slot == Slot::Expr),
errors,
warnings,
abnormal,
rewritten: None,
demoted: false,
})
}
}
fn split_new(s: &str) -> (String, Option<String>) {
let tail = match s.rfind(M_NEW) {
Some(i) => &s[i + M_NEW.len()..],
None => s,
};
match tail.rfind(M_VAL) {
Some(i) => (
tail[..i].to_string(),
Some(tail[i + M_VAL.len()..].trim_end_matches('\n').to_string()),
),
None => (tail.to_string(), None),
}
}
#[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 => "program crashed: segmentation fault (SIGSEGV) — \
usually a NULL/wild pointer dereference or an out-of-bounds index"
.into(),
6 => "program aborted (SIGABRT) — \
usually a failed assert, or heap corruption caught by the C library"
.into(),
8 => "arithmetic error (SIGFPE) — usually integer division by zero".into(),
4 => "illegal instruction (SIGILL)".into(),
7 | 10 => "bus error (SIGBUS) — usually a misaligned memory access".into(),
_ => format!("program terminated by signal {sig}"),
})
}
#[cfg(windows)]
fn describe_abnormal(st: &std::process::ExitStatus) -> Option<String> {
let code = st.code()? as u32;
Some(match code {
0xC0000005 => "program crashed: access violation — \
usually a NULL/wild pointer dereference or an out-of-bounds index"
.into(),
0xC0000094 => "arithmetic error: integer division by zero".into(),
0xC000001D => "illegal instruction".into(),
0xC00000FD => "stack overflow — usually runaway recursion or a huge stack array".into(),
0xC0000409 => "stack buffer overrun detected".into(),
_ => return None,
})
}
fn looks_file_scope(input: &str) -> bool {
let t = input.trim_start();
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" {
return true;
}
if matches!(first.as_str(), "struct" | "union" | "enum") {
let after_kw = t[first.len()..].trim_start();
let tag: String = after_kw
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if after_kw[tag.len()..].trim_start().starts_with('{') {
return true;
}
return false;
}
has_paren_then_brace(input)
}
fn has_paren_then_brace(input: &str) -> bool {
let sc = lex::scan(input);
let b = input.as_bytes();
let brace = match (0..b.len()).find(|&i| sc.code[i] && b[i] == b'{') {
Some(i) => i,
None => return false,
};
(0..brace)
.rev()
.filter(|&i| sc.code[i])
.find(|&i| !b[i].is_ascii_whitespace())
.map(|i| b[i] == b')')
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn routes_definitions_to_file_scope() {
assert!(looks_file_scope("int add(int a, int b) { return a + b; }"));
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("int x = 41;"));
assert!(!looks_file_scope("struct P p = {1, 2};"));
assert!(!looks_file_scope("x + 1"));
}
#[test]
fn splits_on_markers() {
let s = format!("old{M_NEW}printed{M_VAL}42\n");
let (out, val) = split_new(&s);
assert_eq!(out, "printed");
assert_eq!(val.as_deref(), Some("42"));
}
#[test]
fn shows_everything_when_marker_never_reached() {
let (out, val) = split_new("crashed early");
assert_eq!(out, "crashed early");
assert!(val.is_none());
}
}