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_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 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 enum Eval {
Done(Outcome),
CompileError(String),
}
pub struct Evaluator {
pub tc: crate::toolchain::Toolchain,
dir: tempfile::TempDir,
pub timeout: Duration,
stream_output: bool,
allow_program_stdin: bool,
}
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,
stream_output: false,
allow_program_stdin: false,
})
}
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;
}
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));
if cap.stdout_truncated || cap.stderr_truncated {
d.push_str(&format!(
"\ncompiler output exceeded {} MiB per stream and was truncated\n",
proc::MAX_CAPTURE_BYTES / (1024 * 1024)
));
}
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 type_of(&self, session: &Session, input: &str) -> Result<Eval> {
let prog = codegen::build_type_probe(session, input);
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(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> {
if function_definition_name(input).as_deref() == Some("main") {
return Ok(Eval::CompileError(
"c-shell already provides main(); enter the statements from its body directly and omit the final return"
.to_string(),
));
}
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>> {
if looks_file_scope(input) {
let normal = 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 = 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 {
let normal = codegen::build(session, input, slot);
match self.try_program(normal, slot, session.stdin_tape())? {
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 = 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) => expr_reported = Some(diag),
}
}
if try_expr {
let prog = 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;
return Ok(Ok(o));
}
}
if reported.trim().is_empty()
&& let Some(expr) = expr_reported
{
reported = expr;
}
Ok(Err(reported))
}
fn try_program(
&self,
prog: codegen::Program,
slot: Slot,
stdin_tape: &[proc::StdinEvent],
) -> Result<std::result::Result<Outcome, 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(Ok(self.run(
&exe,
slot,
warnings,
prog.uses_stdin_tape,
stdin_tape,
)?))
}
Err(diag) => Ok(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> {
let mut cmd = Command::new(exe);
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));
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(|| format!("failed to start {}", exe.display()))?;
if let Some(live) = &live {
live.finish();
}
let out = String::from_utf8_lossy(&cap.stdout).into_owned();
let err = String::from_utf8_lossy(&cap.stderr).into_owned();
let out_parts = split_new(&out, true);
let err_parts = split_new(&err, false);
let abnormal = match cap.status {
None => Some(format!(
"killed after {}s (possible infinite loop)",
self.timeout.as_secs()
)),
Some(st) => describe_abnormal(&st),
}
.or_else(|| {
(cap.stdout_truncated || cap.stderr_truncated).then(|| {
format!(
"program output exceeded {} MiB per stream and was truncated",
proc::MAX_CAPTURE_BYTES / (1024 * 1024)
)
})
})
.or_else(|| {
cap.stdin_diverged.then(|| {
"stdin tape diverged while replaying retained input; use %undo or %reset"
.to_string()
})
})
.or_else(|| {
(!out_parts.done || !err_parts.done)
.then(|| "program exited before the input completed".to_string())
});
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
&& out_parts.done
&& err_parts.done
&& !cap.stdout_truncated
&& !cap.stderr_truncated
}),
errors: if self.stream_output {
String::new()
} else {
err_parts.output
},
streamed_output_needs_newline,
stdin_events: cap.stdin_recorded,
warnings,
abnormal,
rewritten: None,
scoped_rebind: false,
file_replacement: None,
unprintable: false,
})
}
}
struct SplitOutput {
output: String,
value: Option<String>,
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,
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 body = body.replace(M_STDIN, "");
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(),
),
done,
};
}
SplitOutput {
output: body,
value: None,
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();
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()]
};
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 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) = done {
next.push((i, M_DONE.len(), Some(LiveState::Done)));
}
if let Some(i) = request {
next.push((i, M_STDIN.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(proc::StdinRequest::Current);
}
} else {
let markers: &[&[u8]] = if self.stdout {
&[M_VAL.as_bytes(), M_DONE.as_bytes()]
} else {
&[M_DONE.as_bytes(), M_STDIN.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>,
}
impl LiveStreams {
fn new(stream_visible: bool, stdin_requests: mpsc::Sender<proc::StdinRequest>) -> 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,
}
}
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;
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 (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 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 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);
}
let err_tail = self.stderr.lock().expect("stderr filter").finish();
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 => "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 is_rebinding_diagnostic(diag: &str) -> bool {
let lower = diag.to_ascii_lowercase();
[
"redefinition",
"redeclaration",
"redeclared",
"conflicting types for",
"already has a body",
"c2011",
"c2084",
"c2086",
"c2365",
"c2371",
"c2373",
"c2374",
"c2375",
"c2556",
]
.iter()
.any(|needle| lower.contains(needle))
}
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 = 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;
}
!first.is_empty() && function_definition_name(input).is_some()
}
fn function_definition_name(input: &str) -> Option<String> {
let sc = lex::scan(input);
let b = input.as_bytes();
let mut parens = 0i32;
let mut brackets = 0i32;
let mut brace = None;
for (i, &byte) in b.iter().enumerate() {
if !sc.code[i] {
continue;
}
match byte {
b'(' => parens += 1,
b')' => parens -= 1,
b'[' => brackets += 1,
b']' => brackets -= 1,
b'=' if parens == 0 && brackets == 0 => return None,
b'{' if parens == 0 && brackets == 0 => {
brace = Some(i);
break;
}
b'{' => {}
_ => {}
}
}
let brace = brace?;
let close = (0..brace)
.rev()
.find(|&i| sc.code[i] && !b[i].is_ascii_whitespace())?;
if b[close] != b')' {
return None;
}
let mut depth = 0i32;
let mut open = None;
for i in (0..=close).rev() {
if !sc.code[i] {
continue;
}
match b[i] {
b')' => depth += 1,
b'(' => {
depth -= 1;
if depth == 0 {
open = Some(i);
break;
}
}
_ => {}
}
}
let open = open?;
let before = (0..open)
.rev()
.find(|&i| sc.code[i] && !b[i].is_ascii_whitespace())?;
if b[before] == b')' {
let mut depth = 0i32;
let mut inner_open = None;
for i in (0..=before).rev() {
if !sc.code[i] {
continue;
}
match b[i] {
b')' => depth += 1,
b'(' => {
depth -= 1;
if depth == 0 {
inner_open = Some(i);
break;
}
}
_ => {}
}
}
let inner_open = inner_open?;
return identifier_between(input, &sc.code, inner_open + 1, before);
}
identifier_ending_at(input, &sc.code, before)
}
fn identifier_ending_at(input: &str, code: &[bool], end: usize) -> Option<String> {
let b = input.as_bytes();
if !(b[end] == b'_' || b[end].is_ascii_alphanumeric()) {
return None;
}
let mut start = end;
while start > 0
&& code[start - 1]
&& (b[start - 1] == b'_' || b[start - 1].is_ascii_alphanumeric())
{
start -= 1;
}
(!b[start].is_ascii_digit()).then(|| input[start..=end].to_string())
}
fn identifier_between(input: &str, code: &[bool], start: usize, end: usize) -> Option<String> {
let b = input.as_bytes();
let first = (start..end).find(|&i| code[i] && !b[i].is_ascii_whitespace())?;
let last = (first..end)
.rev()
.find(|&i| code[i] && !b[i].is_ascii_whitespace())?;
let ident = identifier_ending_at(input, code, last)?;
let ident_start = last + 1 - ident.len();
(ident_start == first).then_some(ident)
}
#[cfg(test)]
mod tests {
use super::*;
#[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!(
function_definition_name("int main(void) { return 0; }").as_deref(),
Some("main")
);
assert_eq!(
function_definition_name("int (main)(int argc, char **argv) { return argc; }")
.as_deref(),
Some("main")
);
assert_eq!(
function_definition_name("int domain(void) { return 1; }").as_deref(),
Some("domain")
);
assert_eq!(function_definition_name("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!(got.done);
}
#[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_reports_historical_and_current_stdin_requests() {
let mut filter = LiveFilter::new(false);
let wire = format!("old{M_STDIN}{M_NEW}prompt: {M_STDIN}{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::Current]
);
}
#[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 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);
}
}