use std::process::Command;
use ratex_layout::{layout, to_display_list, LayoutOptions};
use ratex_parser::parser::parse;
const CHILD_ENV: &str = "RATEX_STACK_SAFETY_CHILD";
fn nested(prefix: &str, suffix: &str, depth: usize) -> String {
format!("{}x{}", prefix.repeat(depth), suffix.repeat(depth))
}
fn nested_fraction(depth: usize) -> String {
format!(r"{}x{}", r"\frac{1}{".repeat(depth), "}".repeat(depth))
}
fn nested_superscript(depth: usize) -> String {
format!(r"{}x{}", r"x^{".repeat(depth), "}".repeat(depth))
}
fn unbraced_command_chain(command: &str, depth: usize) -> String {
format!("{}x", command.repeat(depth))
}
fn nested_tag(depth: usize) -> String {
format!("{}{{x}}", r"\tag".repeat(depth))
}
fn nested_braket(depth: usize) -> String {
let mut input = "x".to_string();
for _ in 0..depth {
input = format!(r"\Braket{{{input}}}");
}
input
}
fn nested_raisebox(depth: usize) -> String {
let mut body = "x".to_owned();
for _ in 0..depth {
body = format!(r"\raisebox{{0pt}}{{{body}}}");
}
body
}
fn environment_with_explicit_tag(environment: &str, tag: &str) -> String {
let body = if environment == "align" {
r"x &= y"
} else {
"x"
};
format!(r"\begin{{{environment}}}{body}\tag{{{tag}}}\end{{{environment}}}")
}
fn unary_prooftree(inferences: usize) -> String {
format!(
r"\begin{{prooftree}}\AxiomC{{P}}{}\end{{prooftree}}",
r"\UnaryInfC{P}".repeat(inferences)
)
}
fn braced_body(depth: usize) -> String {
format!("{}x{}", "{".repeat(depth), "}".repeat(depth))
}
fn prooftree_axiom_payload(payload_depth: usize) -> String {
format!(
r"\begin{{prooftree}}\AxiomC{{{}}}\end{{prooftree}}",
braced_body(payload_depth)
)
}
fn prooftree_left_label_payload(payload_depth: usize) -> String {
format!(
r"\begin{{prooftree}}\LeftLabel{{{}}}\AxiomC{{P}}\UnaryInfC{{Q}}\end{{prooftree}}",
braced_body(payload_depth)
)
}
fn assert_pipeline_ok(input: &str) {
let ast = parse(input).unwrap_or_else(|error| panic!("failed to parse bounded input: {error}"));
let layout = layout(&ast, &LayoutOptions::default());
let _ = to_display_list(&layout);
}
fn assert_recursion_limit(input: &str) {
let error = parse(input).expect_err("over-limit input unexpectedly parsed");
assert!(
error.to_string().contains("Recursion limit exceeded"),
"unexpected error: {error}"
);
}
fn assert_parse_error(input: &str) {
parse(input).expect_err("adversarial input unexpectedly parsed");
}
fn run_small_stack_cases() {
#[cfg(not(debug_assertions))]
run_boundary_cases();
let over_limit_cases = [
("group-33", nested("{", "}", 33)),
("sqrt-33", nested(r"\sqrt{", "}", 33)),
("frac-33", nested_fraction(33)),
("left-right-33", nested(r"\left(", r"\right)", 33)),
("superscript-33", nested_superscript(33)),
("dots-33", r"\dots".repeat(33)),
];
for (name, input) in &over_limit_cases {
eprintln!("stack-safety over-limit case: {name}");
assert_recursion_limit(input);
}
let adversarial_cases = [
("group-300", nested("{", "}", 300)),
("sqrt-300", nested(r"\sqrt{", "}", 300)),
("frac-300", nested_fraction(300)),
("left-right-300", nested(r"\left(", r"\right)", 300)),
("superscript-300", nested_superscript(300)),
("dots-300", r"\dots".repeat(300)),
("tag-4200", nested_tag(4_200)),
("braket-300", nested_braket(300)),
("prooftree-axiom-payload-32", prooftree_axiom_payload(32)),
(
"prooftree-left-label-payload-32",
prooftree_left_label_payload(32),
),
(
"equation-explicit-tag-raisebox-12",
environment_with_explicit_tag("equation", &nested_raisebox(12)),
),
(
"align-explicit-tag-raisebox-12",
environment_with_explicit_tag("align", &nested_raisebox(12)),
),
];
for (name, input) in &adversarial_cases {
eprintln!("stack-safety adversarial case: {name}");
assert_recursion_limit(input);
}
let accents_4200 = "\u{301}".repeat(4_200);
assert_recursion_limit(&format!("x{accents_4200}"));
assert_recursion_limit(&format!(r"\text{{x{accents_4200}}}"));
assert_recursion_limit(&format!(r"\text{{x{}}}", "\u{301}".repeat(32)));
assert_recursion_limit(&unary_prooftree(32));
eprintln!("stack-safety parse-error case: unbraced-sqrt-4200");
assert_parse_error(&unbraced_command_chain(r"\sqrt", 4_200));
eprintln!("stack-safety parse-error case: unbraced-bigl-4200");
assert_parse_error(&format!("{}(", r"\bigl".repeat(4_200)));
eprintln!("stack-safety parse-error case: unbraced-left-4200");
assert_parse_error(&format!("{}(", r"\left".repeat(4_200)));
eprintln!("stack-safety flat lexer case: comments-4200");
assert_pipeline_ok(&("%\n".repeat(4_200) + "x"));
eprintln!("stack-safety flat prefix case: global-4200");
assert_pipeline_ok(&format!(r"{}\def\foo{{x}}\foo", r"\global".repeat(4_200)));
}
fn run_boundary_cases() {
let boundary_cases = [
("group", nested("{", "}", 32)),
("sqrt", nested(r"\sqrt{", "}", 32)),
("frac", nested_fraction(32)),
("left-right", nested(r"\left(", r"\right)", 32)),
("superscript", nested_superscript(32)),
("dots", r"\dots".repeat(32)),
];
for (_name, input) in &boundary_cases {
assert_pipeline_ok(input);
}
let accents_32 = "\u{301}".repeat(32);
let accents_31 = "\u{301}".repeat(31);
assert_pipeline_ok(&format!("x{accents_32}"));
assert_pipeline_ok(&format!(r"\text{{x{accents_31}}}"));
assert_pipeline_ok(&unary_prooftree(31));
assert_pipeline_ok(&nested_tag(32));
assert_pipeline_ok(&nested_braket(16));
assert_pipeline_ok(&prooftree_axiom_payload(31));
assert_pipeline_ok(&prooftree_left_label_payload(31));
}
#[test]
fn parser_driven_pipeline_is_safe_on_a_small_stack() {
if cfg!(debug_assertions) {
run_boundary_cases();
run_small_stack_cases();
return;
}
if std::env::var_os(CHILD_ENV).is_some() {
std::thread::Builder::new()
.name("ratex-512k-stack".into())
.stack_size(512 * 1024)
.spawn(run_small_stack_cases)
.expect("failed to spawn small-stack thread")
.join()
.expect("small-stack test thread panicked");
return;
}
run_boundary_cases();
let status = Command::new(std::env::current_exe().expect("test executable path"))
.arg("--exact")
.arg("parser_driven_pipeline_is_safe_on_a_small_stack")
.arg("--nocapture")
.env(CHILD_ENV, "1")
.status()
.expect("failed to start isolated stack-safety test process");
assert!(status.success(), "isolated stack-safety process failed");
}