use super::parse::ParseStack;
use crate::hist::{hist_context_restore, hist_context_save};
use crate::lex::{lex_context_restore, lex_context_save};
use crate::parse::{parse_context_restore, parse_context_save};
use crate::ported::zsh_h::{hist_stack, lex_stack, ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE};
use crate::signals_h::{queue_signals, unqueue_signals};
use crate::DPUTS;
use std::sync::Mutex;
#[allow(non_camel_case_types)]
pub struct context_stack {
pub next: Option<Box<context_stack>>, pub hist_stack: hist_stack, pub lex_stack: lex_stack, pub parse_stack: ParseStack, }
#[allow(non_snake_case)]
pub fn zcontext_save_partial(parts: i32) {
queue_signals();
let mut cs = Box::new(context_stack {
next: None,
hist_stack: hist_stack {
histactive: 0,
histdone: 0,
stophist: 0,
hlinesz: 0,
defev: 0,
hline: None,
hptr: None,
chwords: Vec::new(),
chwordlen: 0,
chwordpos: 0,
csp: 0,
hist_keep_comment: 0,
},
lex_stack: lex_stack::default(),
parse_stack: ParseStack::default(),
});
let mut head = cstack.lock().unwrap();
let toplevel: i32 = if head.is_none() { 1 } else { 0 }; if (parts & ZCONTEXT_HIST) != 0 {
hist_context_save(&mut cs.hist_stack, toplevel); }
if (parts & ZCONTEXT_LEX) != 0 {
lex_context_save(&mut cs.lex_stack);
}
if (parts & ZCONTEXT_PARSE) != 0 {
parse_context_save(&mut cs.parse_stack);
}
cs.next = head.take(); *head = Some(cs);
unqueue_signals(); }
pub fn zcontext_save() {
zcontext_save_partial(ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE);
}
pub fn zcontext_restore_partial(parts: i32) {
let mut head = cstack.lock().unwrap();
DPUTS!(
head.is_none(),
"BUG: zcontext_restore() without zcontext_save()"
); let mut cs = match head.take() {
Some(cs) => cs,
None => {
return;
}
};
queue_signals(); *head = cs.next.take(); let toplevel: i32 = if head.is_none() { 1 } else { 0 };
if (parts & ZCONTEXT_HIST) != 0 {
hist_context_restore(&cs.hist_stack, toplevel); }
if (parts & ZCONTEXT_LEX) != 0 {
lex_context_restore(&mut cs.lex_stack);
}
if (parts & ZCONTEXT_PARSE) != 0 {
parse_context_restore(&cs.parse_stack);
}
drop(cs);
unqueue_signals(); }
pub fn zcontext_restore() {
zcontext_restore_partial(ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE);
}
static cstack: Mutex<Option<Box<context_stack>>> = Mutex::new(None);
#[cfg(test)]
mod tests {
use super::*;
use crate::lex::{lex_init, set_toklineno, toklineno, LEX_DBPARENS};
fn reset_cstack() {
*cstack.lock().unwrap() = None;
}
#[test]
fn save_restore_balances_stack() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
assert!(cstack.lock().unwrap().is_some());
zcontext_restore();
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn nested_saves_pop_lifo() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
zcontext_save();
zcontext_restore();
assert!(cstack.lock().unwrap().is_some());
zcontext_restore();
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn restore_without_save_is_noop() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_restore();
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn lex_save_restore_roundtrips_state() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("echo hello");
LEX_DBPARENS.set(true);
set_toklineno(42);
zcontext_save();
assert!(LEX_DBPARENS.with(|c| c.get()));
assert_eq!(toklineno(), 42);
zcontext_restore();
assert!(LEX_DBPARENS.with(|c| c.get()));
assert_eq!(toklineno(), 42);
}
#[test]
fn zcontext_flag_bits_are_distinct_and_nonzero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(ZCONTEXT_HIST, 1 << 0);
assert_eq!(ZCONTEXT_LEX, 1 << 1);
assert_eq!(ZCONTEXT_PARSE, 1 << 2);
assert_eq!(ZCONTEXT_HIST & ZCONTEXT_LEX, 0);
assert_eq!(ZCONTEXT_HIST & ZCONTEXT_PARSE, 0);
assert_eq!(ZCONTEXT_LEX & ZCONTEXT_PARSE, 0);
}
#[test]
fn zcontext_save_partial_with_zero_mask_still_pushes_frame() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save_partial(0);
assert!(
cstack.lock().unwrap().is_some(),
"c:70-71 push must fire even when no parts requested"
);
zcontext_restore_partial(0);
assert!(
cstack.lock().unwrap().is_none(),
"c:96 pop must mirror the push regardless of parts mask"
);
}
#[test]
fn zcontext_restore_partial_on_empty_stack_is_noop() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_restore_partial(0);
zcontext_restore_partial(ZCONTEXT_HIST);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn deep_save_restore_lifo_drains_to_empty() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
for _ in 0..5 {
zcontext_save();
}
for i in (0..5).rev() {
assert!(
cstack.lock().unwrap().is_some(),
"stack must still have entries before restore #{}",
5 - i
);
zcontext_restore();
}
assert!(
cstack.lock().unwrap().is_none(),
"5 restores must drain the 5 saves to empty"
);
}
#[test]
fn zcontext_save_equals_save_partial_full_mask() {
let _g = crate::test_util::global_state_lock();
let full = ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE;
reset_cstack();
lex_init("");
zcontext_save();
zcontext_restore();
let after_full = cstack.lock().unwrap().is_none();
reset_cstack();
lex_init("");
zcontext_save_partial(full);
zcontext_restore_partial(full);
let after_partial = cstack.lock().unwrap().is_none();
assert_eq!(
after_full, after_partial,
"save/restore must equal save_partial(ALL)/restore_partial(ALL)"
);
}
#[test]
fn many_saves_without_restore_grow_stack_safely() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
for _ in 0..20 {
zcontext_save();
}
assert!(cstack.lock().unwrap().is_some());
for _ in 0..20 {
zcontext_restore();
}
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn context_corpus_save_then_restore_returns_empty() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
zcontext_restore();
assert!(
cstack.lock().unwrap().is_none(),
"save+restore leaves no stack"
);
}
#[test]
fn context_corpus_nested_save_restore_lifo() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
zcontext_save();
zcontext_save();
zcontext_restore();
zcontext_restore();
zcontext_restore();
assert!(cstack.lock().unwrap().is_none(), "all 3 levels drained");
}
#[test]
fn context_corpus_save_partial_round_trip() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save_partial(1);
zcontext_restore_partial(1);
}
#[test]
fn zcontext_save_pushes_onto_stack() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
assert!(
cstack.lock().unwrap().is_some(),
"save should leave cstack with one frame"
);
zcontext_restore();
}
#[test]
fn zcontext_restore_pops_to_none_when_balanced() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
zcontext_restore();
assert!(
cstack.lock().unwrap().is_none(),
"single save+restore returns cstack to None"
);
}
#[test]
fn zcontext_save_multiple_builds_stack() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
zcontext_save();
zcontext_save();
let head = cstack.lock().unwrap();
let mut count = 0;
let mut cur = head.as_ref();
while let Some(node) = cur {
count += 1;
cur = node.next.as_ref();
}
drop(head);
assert_eq!(count, 3, "3 saves → 3-deep stack");
zcontext_restore();
zcontext_restore();
zcontext_restore();
}
#[test]
fn zcontext_save_partial_zero_still_pushes_frame() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save_partial(0);
assert!(
cstack.lock().unwrap().is_some(),
"even parts=0 pushes a frame for restore symmetry"
);
zcontext_restore_partial(0);
}
#[test]
fn zcontext_restore_on_empty_no_panic() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_restore_partial(0);
}
#[test]
fn zcontext_save_hist_only_round_trip() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_save_lex_only_round_trip() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_LEX);
zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_LEX);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_save_parse_only_round_trip() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_PARSE);
zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_PARSE);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_flag_bits_pin_to_one_two_four() {
assert_eq!(crate::ported::zsh_h::ZCONTEXT_HIST, 1, "c:491 = 1<<0");
assert_eq!(crate::ported::zsh_h::ZCONTEXT_LEX, 2, "c:493 = 1<<1");
assert_eq!(crate::ported::zsh_h::ZCONTEXT_PARSE, 4, "c:495 = 1<<2");
}
#[test]
fn zcontext_flags_are_single_bits() {
for &v in &[
crate::ported::zsh_h::ZCONTEXT_HIST,
crate::ported::zsh_h::ZCONTEXT_LEX,
crate::ported::zsh_h::ZCONTEXT_PARSE,
] {
assert!((v as u32).is_power_of_two(), "{} must be a single bit", v);
}
}
#[test]
fn zcontext_save_partial_zero_round_trip_pops() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save_partial(0);
assert!(cstack.lock().unwrap().is_some(), "zero-mask still pushes");
zcontext_restore_partial(0);
assert!(cstack.lock().unwrap().is_none(), "restore drains to None");
}
#[test]
fn zcontext_save_hist_restore_with_parse_mask_pops_frame() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
assert!(cstack.lock().unwrap().is_some());
zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_PARSE);
assert!(cstack.lock().unwrap().is_none(), "frame popped regardless");
}
#[test]
fn zcontext_save_shorthand_pushes_one_frame() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
{
let head = cstack.lock().unwrap();
assert!(head.is_some(), "save pushed a frame");
assert!(head.as_ref().unwrap().next.is_none(), "stack depth = 1");
}
zcontext_restore();
assert!(cstack.lock().unwrap().is_none());
zcontext_save_partial(
crate::ported::zsh_h::ZCONTEXT_HIST
| crate::ported::zsh_h::ZCONTEXT_LEX
| crate::ported::zsh_h::ZCONTEXT_PARSE,
);
{
let head = cstack.lock().unwrap();
assert!(head.is_some(), "full-mask save_partial pushed a frame");
assert!(head.as_ref().unwrap().next.is_none(), "stack depth = 1");
}
zcontext_restore();
}
#[test]
fn zcontext_restore_drains_exactly_one_frame() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
zcontext_save();
zcontext_save();
zcontext_restore();
assert!(cstack.lock().unwrap().is_some());
zcontext_restore();
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_restore_shorthand_on_empty_no_panic() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_restore();
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_save_hist_lex_combo_round_trip() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
let mask = crate::ported::zsh_h::ZCONTEXT_HIST | crate::ported::zsh_h::ZCONTEXT_LEX;
zcontext_save_partial(mask);
zcontext_restore_partial(mask);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_save_lex_parse_combo_round_trip() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
lex_init("");
let mask = crate::ported::zsh_h::ZCONTEXT_LEX | crate::ported::zsh_h::ZCONTEXT_PARSE;
zcontext_save_partial(mask);
zcontext_restore_partial(mask);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn many_restores_on_empty_no_panic() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
for _ in 0..10 {
zcontext_restore_partial(0);
}
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn restore_on_empty_via_full_shorthand_no_panic() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_restore();
zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
zcontext_restore_partial(
crate::ported::zsh_h::ZCONTEXT_HIST | crate::ported::zsh_h::ZCONTEXT_LEX,
);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_save_partial_hist_pushes_one_frame() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
assert!(
cstack.lock().unwrap().is_some(),
"frame must exist after save"
);
zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
assert!(cstack.lock().unwrap().is_none(), "frame must be drained");
}
#[test]
fn zcontext_save_full_then_restore_full_clears_stack() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_save();
assert!(cstack.lock().unwrap().is_some());
zcontext_restore();
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_save_three_then_restore_three_clears_stack() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_save();
zcontext_save();
zcontext_save();
zcontext_restore();
zcontext_restore();
zcontext_restore();
assert!(
cstack.lock().unwrap().is_none(),
"all 3 frames must drain (LIFO)"
);
}
#[test]
fn zcontext_save_equals_or_of_three_flags_behavior() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_save();
let depth1 = cstack.lock().unwrap().is_some();
reset_cstack();
zcontext_save_partial(
crate::ported::zsh_h::ZCONTEXT_HIST
| crate::ported::zsh_h::ZCONTEXT_LEX
| crate::ported::zsh_h::ZCONTEXT_PARSE,
);
let depth2 = cstack.lock().unwrap().is_some();
assert_eq!(
depth1, depth2,
"shorthand and explicit OR must produce same push behavior"
);
zcontext_restore();
}
#[test]
fn zcontext_all_flags_or_equals_seven() {
let all = crate::ported::zsh_h::ZCONTEXT_HIST
| crate::ported::zsh_h::ZCONTEXT_LEX
| crate::ported::zsh_h::ZCONTEXT_PARSE;
assert_eq!(all, 7, "HIST|LEX|PARSE must equal 0b111 = 7");
}
#[test]
fn zcontext_flags_pairwise_distinct() {
use crate::ported::zsh_h::{ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE};
let codes = [ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE];
let unique: std::collections::HashSet<_> = codes.iter().copied().collect();
assert_eq!(unique.len(), codes.len(), "ZCONTEXT_* must be distinct");
}
#[test]
fn zcontext_save_restore_is_lifo() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_save();
zcontext_save();
assert!(cstack.lock().unwrap().is_some());
zcontext_restore();
assert!(cstack.lock().unwrap().is_some(), "one frame still on stack");
zcontext_restore();
assert!(cstack.lock().unwrap().is_none(), "all frames drained");
}
#[test]
fn zcontext_save_partial_all_bits_no_panic() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_save_partial(-1);
zcontext_restore_partial(-1);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_restore_partial_all_bits_on_empty_no_panic() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_restore_partial(-1);
assert!(cstack.lock().unwrap().is_none());
}
#[test]
fn zcontext_many_saves_then_full_drain_clean() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
for _ in 0..20 {
zcontext_save();
}
for _ in 0..20 {
zcontext_restore();
}
assert!(
cstack.lock().unwrap().is_none(),
"after 20 paired save/restore, stack must be empty"
);
}
#[test]
fn zcontext_restore_with_subset_mask_drains_frame() {
let _g = crate::test_util::global_state_lock();
reset_cstack();
zcontext_save_partial(
crate::ported::zsh_h::ZCONTEXT_HIST | crate::ported::zsh_h::ZCONTEXT_LEX,
);
zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
assert!(
cstack.lock().unwrap().is_none(),
"restore with subset mask still pops the frame"
);
}
}