use std::sync::LazyLock;
use regex::Regex;
pub struct CapturedOutput {
pub printed: String,
pub value: Option<CapturedValue>,
pub warnings: Vec<String>,
}
pub struct CapturedValue {
pub text: String,
pub type_info: Option<String>,
}
static DIAGNOSTIC: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r": warning:|--backtrace:").expect("valid pattern"));
static LOCATION_PREFIX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[^\s:]*:\d+:\d+:\(\d+\):\[\d+\]: ").expect("valid pattern"));
static DIAGNOSTIC_KEYWORD: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-z]+: ").expect("valid pattern"));
fn is_diagnostic(s: &str) -> bool {
DIAGNOSTIC.is_match(s)
}
pub fn strip_m2_location(s: &str) -> &str {
LOCATION_PREFIX
.find(s)
.map_or(s, |location| &s[location.end()..])
}
fn diagnostic_message(s: &str) -> &str {
DIAGNOSTIC_KEYWORD
.find(s)
.map_or(s, |keyword| &s[keyword.end()..])
}
fn captured_text<'a>(raw: &'a str, code: &str) -> &'a str {
raw.strip_prefix(code)
.and_then(|text| text.strip_prefix('\n'))
.and_then(|text| text.strip_suffix('\n'))
.unwrap_or(raw)
}
pub fn captured_output(
raw: &str,
code: &str,
value: Option<&str>,
type_info: Option<&str>,
) -> CapturedOutput {
let printed = captured_text(raw, code);
let mut warnings = Vec::new();
CapturedOutput {
printed: take_warnings(printed, &mut warnings),
value: value.map(|text| CapturedValue {
text: text.to_string(),
type_info: type_info.map(str::to_string),
}),
warnings,
}
}
fn take_warnings(text: &str, warnings: &mut Vec<String>) -> String {
text.split('\n')
.filter(|line| {
if !is_diagnostic(line) {
return true;
}
let warning = diagnostic_message(strip_m2_location(line)).to_string();
if !warnings.contains(&warning) {
warnings.push(warning);
}
false
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn captured_error(raw: &str, code: &str) -> String {
captured_text(raw, code).to_string()
}
#[cfg(test)]
mod tests {
use super::{captured_error, captured_output, strip_m2_location};
#[test]
fn strips_m2_location_prefix() {
assert_eq!(
strip_m2_location("ntString:1:12:(3):[8]: warning: character '.' follows"),
"warning: character '.' follows"
);
assert_eq!(
strip_m2_location("currentString:1:6:(3):[8]: error: exponent missing"),
"error: exponent missing"
);
assert_eq!(
strip_m2_location("{{Binary, {Token, 2}}}"),
"{{Binary, {Token, 2}}}"
);
}
#[test]
fn punctuation_alone_is_not_a_location_prefix() {
for line in [
"map:(x) -> R:[i]: value",
"see foo:(3) and bar:[8]: done",
"o3 = HashTable{a:(1), b:[2]: c}",
] {
assert_eq!(strip_m2_location(line), line, "mangled {line:?}");
}
}
#[test]
fn strips_echo_and_prompt_simple_value() {
let raw = "2 + 2\n\n";
let output = captured_output(raw, "2 + 2", Some("4"), None);
let value = output.value.expect("returned value");
assert_eq!(output.printed, "");
assert_eq!(value.text, "4");
assert_eq!(value.type_info, None);
}
#[test]
fn extracts_type_annotation() {
let raw = "parse \"2...2\"\n\n";
let output = captured_output(
raw,
"parse \"2...2\"",
Some("{{Binary, {Token, 2}}}"),
Some("List"),
);
let value = output.value.expect("returned value");
assert_eq!(value.text, "{{Binary, {Token, 2}}}");
assert_eq!(value.type_info.as_deref(), Some("List"));
}
#[test]
fn preserves_2d_net_alignment() {
let raw = "R = QQ[x,y]; x^2 + y\n\n";
let output = captured_output(raw, "R = QQ[x,y]; x^2 + y", Some(" 2\nx + y"), Some("R"));
let value = output.value.expect("returned value");
assert_eq!(value.text, " 2\nx + y");
assert_eq!(value.type_info.as_deref(), Some("R"));
}
#[test]
fn preserves_printed_output_before_returned_value() {
let raw = "(print \"before\"; 2 + 2)\nbefore\n\n";
let output = captured_output(raw, "(print \"before\"; 2 + 2)", Some("4"), None);
let value = output.value.expect("returned value");
assert_eq!(output.printed, "before\n");
assert_eq!(value.text, "4");
assert_eq!(value.type_info, None);
}
#[test]
fn printed_output_marker_does_not_hide_returned_value() {
let raw = "(print \"o2 = fake\"; 2 + 2)\no2 = fake\n\n";
let output = captured_output(raw, "(print \"o2 = fake\"; 2 + 2)", Some("4"), None);
let value = output.value.expect("returned value");
assert_eq!(output.printed, "o2 = fake\n");
assert_eq!(value.text, "4");
}
#[test]
fn null_result_has_printed_output_without_output_binding() {
let raw = "print \"hello\"\nhello\n\n";
let output = captured_output(raw, "print \"hello\"", Some("null"), Some("Nothing"));
assert_eq!(output.printed, "hello\n");
let value = output.value.expect("null result");
assert_eq!(value.text, "null");
assert_eq!(value.type_info.as_deref(), Some("Nothing"));
}
#[test]
fn printed_output_marker_does_not_become_a_value() {
let code = "print \"\\no184 = 11\"";
let raw = "print \"\\no184 = 11\"\n\no184 = 11\n\n";
let output = captured_output(raw, code, Some("null"), Some("Nothing"));
assert_eq!(output.printed, "\no184 = 11\n");
let value = output.value.expect("null result");
assert_eq!(value.text, "null");
assert_eq!(value.type_info.as_deref(), Some("Nothing"));
}
#[test]
fn multiline_input_echo_fully_stripped() {
let raw = "a = 1 +\n2 + 3\n\n";
let output = captured_output(raw, "a = 1 +\n2 + 3", Some("6"), None);
let value = output.value.expect("returned value");
assert_eq!(value.text, "6");
assert_eq!(value.type_info, None);
}
#[test]
fn suppressed_assignment_is_empty() {
let raw = "z = 7;\n\n";
let output = captured_output(raw, "z = 7;", None, None);
assert_eq!(output.printed, "");
assert!(output.value.is_none());
}
#[test]
fn error_keeps_message_drops_echo() {
let raw = "1/0\ncurrentString:1:1:(0):[4]: error: division by zero\n";
let output = captured_error(raw, "1/0");
assert_eq!(output, "currentString:1:1:(0):[4]: error: division by zero");
}
#[test]
fn extracts_distinct_warnings_without_changing_other_lines() {
let raw = "currentString:1:1:(3):[8]: warning: first\nvalue\ncurrentString:1:1:(3):[8]: warning: first\ncurrentString:1:1:(3):[8]: warning: second";
let output = captured_output(raw, "code", None, None);
assert_eq!(output.printed, "value");
assert!(output.value.is_none());
assert_eq!(output.warnings, vec!["first", "second"]);
}
}