Skip to main content

zsh/ported/
context.rs

1//! context.c - context save and restore
2//!
3//! Port of Src/context.c
4//!
5//! This short file provides a home for the stack of saved contexts.
6//! The actions for saving and restoring are encapsulated within
7//! individual modules. After P7-P8 dissolved the ZshLexer and ZshParser structs into
8//! free ported + thread_locals, the save/restore signatures simplified —
9//! the lexer/parser parameters went away. Callers just call zcontext_*
10//! and the underlying thread_local state is what's saved/restored.
11
12use super::parse::ParseStack;
13use crate::hist::{hist_context_restore, hist_context_save};
14use crate::lex::{lex_context_restore, lex_context_save};
15use crate::parse::{parse_context_restore, parse_context_save};
16use crate::ported::zsh_h::{hist_stack, lex_stack, ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE};
17use crate::signals_h::{queue_signals, unqueue_signals};
18use crate::DPUTS;
19use std::sync::Mutex;
20
21/// Port of `struct context_stack` from Src/context.c:38-44.
22#[allow(non_camel_case_types)]
23pub struct context_stack {
24    // c:52
25    pub next: Option<Box<context_stack>>, // c:52
26    pub hist_stack: hist_stack,           // c:52
27    pub lex_stack: lex_stack,             // c:52
28    pub parse_stack: ParseStack,          // c:52
29}
30
31/// Port of `void zcontext_save_partial(int parts)` from Src/context.c:52.
32#[allow(non_snake_case)]
33pub fn zcontext_save_partial(parts: i32) {
34    // c:52
35    queue_signals(); // c:52
36
37    let mut cs = Box::new(context_stack {
38        // c:58
39        next: None,
40        hist_stack: hist_stack {
41            histactive: 0,
42            histdone: 0,
43            stophist: 0,
44            hlinesz: 0,
45            defev: 0,
46            hline: None,
47            hptr: None,
48            chwords: Vec::new(),
49            chwordlen: 0,
50            chwordpos: 0,
51            csp: 0,
52            hist_keep_comment: 0,
53        },
54        lex_stack: lex_stack::default(),
55        parse_stack: ParseStack::default(),
56    });
57
58    let mut head = cstack.lock().unwrap();
59
60    let toplevel: i32 = if head.is_none() { 1 } else { 0 }; // !cstack
61    if (parts & ZCONTEXT_HIST) != 0 {
62        // c:60
63        hist_context_save(&mut cs.hist_stack, toplevel); // c:61
64    }
65    if (parts & ZCONTEXT_LEX) != 0 {
66        // c:63
67        lex_context_save(&mut cs.lex_stack);
68    }
69    if (parts & ZCONTEXT_PARSE) != 0 {
70        // c:80
71        parse_context_save(&mut cs.parse_stack);
72    }
73
74    cs.next = head.take(); // c:89
75    *head = Some(cs); // c:89
76
77    unqueue_signals(); // c:89
78}
79
80/// Port of `void zcontext_save(void)` from Src/context.c:80.
81pub fn zcontext_save() {
82    // c:80
83    zcontext_save_partial(ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE);
84}
85
86/// Port of `void zcontext_restore_partial(int parts)` from Src/context.c:89.
87pub fn zcontext_restore_partial(parts: i32) {
88    // c:89
89    let mut head = cstack.lock().unwrap();
90    // c:93 — DPUTS(!cstack, "BUG: zcontext_restore() without zcontext_save()")
91    DPUTS!(
92        head.is_none(),
93        "BUG: zcontext_restore() without zcontext_save()"
94    ); // c:93
95    let mut cs = match head.take() {
96        // c:91
97        Some(cs) => cs,
98        None => {
99            return;
100        }
101    };
102
103    queue_signals(); // c:95
104    *head = cs.next.take(); // c:96
105    let toplevel: i32 = if head.is_none() { 1 } else { 0 };
106
107    if (parts & ZCONTEXT_HIST) != 0 {
108        // c:98
109        hist_context_restore(&cs.hist_stack, toplevel); // c:99
110    }
111    if (parts & ZCONTEXT_LEX) != 0 {
112        // c:101
113        lex_context_restore(&mut cs.lex_stack);
114    }
115    if (parts & ZCONTEXT_PARSE) != 0 {
116        // c:117
117        parse_context_restore(&cs.parse_stack);
118    }
119
120    drop(cs); // c:117
121
122    unqueue_signals(); // c:117
123}
124
125/// Port of `void zcontext_restore(void)` from Src/context.c:117.
126pub fn zcontext_restore() {
127    // c:117
128    zcontext_restore_partial(ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE);
129}
130
131/// Port of `static struct context_stack *cstack` from Src/context.c:52.
132static cstack: Mutex<Option<Box<context_stack>>> = Mutex::new(None); // c:52
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::lex::{lex_init, set_toklineno, toklineno, LEX_DBPARENS};
138
139    fn reset_cstack() {
140        *cstack.lock().unwrap() = None;
141    }
142
143    #[test]
144    fn save_restore_balances_stack() {
145        let _g = crate::test_util::global_state_lock();
146        reset_cstack();
147        lex_init("");
148        zcontext_save();
149        assert!(cstack.lock().unwrap().is_some());
150        zcontext_restore();
151        assert!(cstack.lock().unwrap().is_none());
152    }
153
154    #[test]
155    fn nested_saves_pop_lifo() {
156        let _g = crate::test_util::global_state_lock();
157        reset_cstack();
158        lex_init("");
159        zcontext_save();
160        zcontext_save();
161        zcontext_restore();
162        assert!(cstack.lock().unwrap().is_some());
163        zcontext_restore();
164        assert!(cstack.lock().unwrap().is_none());
165    }
166
167    #[test]
168    fn restore_without_save_is_noop() {
169        let _g = crate::test_util::global_state_lock();
170        reset_cstack();
171        lex_init("");
172        zcontext_restore();
173        assert!(cstack.lock().unwrap().is_none());
174    }
175
176    #[test]
177    fn lex_save_restore_roundtrips_state() {
178        let _g = crate::test_util::global_state_lock();
179        reset_cstack();
180        lex_init("echo hello");
181        LEX_DBPARENS.set(true);
182        set_toklineno(42);
183        zcontext_save();
184        assert!(LEX_DBPARENS.with(|c| c.get()));
185        assert_eq!(toklineno(), 42);
186        zcontext_restore();
187        assert!(LEX_DBPARENS.with(|c| c.get()));
188        assert_eq!(toklineno(), 42);
189    }
190
191    /// `Src/zsh.h:491-495` — `ZCONTEXT_HIST = (1<<0)`,
192    /// `ZCONTEXT_LEX = (1<<1)`, `ZCONTEXT_PARSE = (1<<2)`. The three
193    /// bits must be distinct and non-overlapping; `zcontext_save_partial`
194    /// at c:60/63/66 AND-tests each independently and a flag collision
195    /// would silently double-save one substrate and skip another on
196    /// every nested context (heredoc-inside-eval, `read -E`, …).
197    #[test]
198    fn zcontext_flag_bits_are_distinct_and_nonzero() {
199        let _g = crate::test_util::global_state_lock();
200        // Pin the exact C constants from Src/zsh.h:491-495.
201        assert_eq!(ZCONTEXT_HIST, 1 << 0);
202        assert_eq!(ZCONTEXT_LEX, 1 << 1);
203        assert_eq!(ZCONTEXT_PARSE, 1 << 2);
204        // Bitfield discipline: no overlapping bits.
205        assert_eq!(ZCONTEXT_HIST & ZCONTEXT_LEX, 0);
206        assert_eq!(ZCONTEXT_HIST & ZCONTEXT_PARSE, 0);
207        assert_eq!(ZCONTEXT_LEX & ZCONTEXT_PARSE, 0);
208    }
209
210    /// `Src/context.c:52-72` — `zcontext_save_partial(parts)` allocates
211    /// the frame BEFORE the `parts &` gates at c:60/63/66, then pushes
212    /// at c:70-71 unconditionally (`cs->next = cstack; cstack = cs`).
213    /// `parts == 0` is a legitimate caller pattern (probe-save); the
214    /// push must still fire. Regression that early-exits on a zero
215    /// mask would mis-balance the stack for any such caller.
216    #[test]
217    fn zcontext_save_partial_with_zero_mask_still_pushes_frame() {
218        let _g = crate::test_util::global_state_lock();
219        reset_cstack();
220        lex_init("");
221        zcontext_save_partial(0);
222        assert!(
223            cstack.lock().unwrap().is_some(),
224            "c:70-71 push must fire even when no parts requested"
225        );
226        zcontext_restore_partial(0);
227        assert!(
228            cstack.lock().unwrap().is_none(),
229            "c:96 pop must mirror the push regardless of parts mask"
230        );
231    }
232
233    /// `Src/context.c:89-96` — `zcontext_restore_partial` reads
234    /// `cs = cstack` at c:91 and a `DPUTS(!cstack, ...)` at c:93 fires
235    /// in C debug builds. Release C dereferences a NULL `cs` — UB —
236    /// but the Rust port chose to silently no-op on empty stack
237    /// (`head.take()` returns None → early return). Pin the no-op so
238    /// a refactor doesn't suddenly start panicking on unbalanced
239    /// restore calls.
240    #[test]
241    fn zcontext_restore_partial_on_empty_stack_is_noop() {
242        let _g = crate::test_util::global_state_lock();
243        reset_cstack();
244        zcontext_restore_partial(0);
245        zcontext_restore_partial(ZCONTEXT_HIST);
246        assert!(cstack.lock().unwrap().is_none());
247    }
248
249    /// `Src/context.c:52` — Deep LIFO check. 5 saves pushed; 5
250    /// restores must drain to empty. Pin the depth-handling so a
251    /// regression that uses a fixed-size buffer instead of the
252    /// linked stack would surface on the third+ push.
253    #[test]
254    fn deep_save_restore_lifo_drains_to_empty() {
255        let _g = crate::test_util::global_state_lock();
256        reset_cstack();
257        lex_init("");
258        for _ in 0..5 {
259            zcontext_save();
260        }
261        // Stack now has 5 frames
262        for i in (0..5).rev() {
263            assert!(
264                cstack.lock().unwrap().is_some(),
265                "stack must still have entries before restore #{}",
266                5 - i
267            );
268            zcontext_restore();
269        }
270        assert!(
271            cstack.lock().unwrap().is_none(),
272            "5 restores must drain the 5 saves to empty"
273        );
274    }
275
276    /// `Src/context.c:80` — `zcontext_save()` is a convenience
277    /// wrapper for `zcontext_save_partial(ALL)`. Pin the alias
278    /// contract: a full-mask partial-save followed by any restore
279    /// yields the same final state as save→restore.
280    #[test]
281    fn zcontext_save_equals_save_partial_full_mask() {
282        let _g = crate::test_util::global_state_lock();
283        let full = ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE;
284
285        reset_cstack();
286        lex_init("");
287        zcontext_save();
288        zcontext_restore();
289        let after_full = cstack.lock().unwrap().is_none();
290
291        reset_cstack();
292        lex_init("");
293        zcontext_save_partial(full);
294        zcontext_restore_partial(full);
295        let after_partial = cstack.lock().unwrap().is_none();
296
297        assert_eq!(
298            after_full, after_partial,
299            "save/restore must equal save_partial(ALL)/restore_partial(ALL)"
300        );
301    }
302
303    /// `Src/context.c:52` — Many save calls with NO matching restores
304    /// must not corrupt or panic, just grow the stack. Pin defensive
305    /// behavior for shell-script abort paths where partial state
306    /// rewinds may skip restores.
307    #[test]
308    fn many_saves_without_restore_grow_stack_safely() {
309        let _g = crate::test_util::global_state_lock();
310        reset_cstack();
311        lex_init("");
312        for _ in 0..20 {
313            zcontext_save();
314        }
315        // Stack should still be intact, top frame accessible
316        assert!(cstack.lock().unwrap().is_some());
317        // Now drain — must not panic
318        for _ in 0..20 {
319            zcontext_restore();
320        }
321        assert!(cstack.lock().unwrap().is_none());
322    }
323
324    // ─── zsh-corpus pins for context save/restore ───────────────────
325
326    /// `zcontext_save` then `zcontext_restore` leaves the stack empty.
327    #[test]
328    fn context_corpus_save_then_restore_returns_empty() {
329        let _g = crate::test_util::global_state_lock();
330        reset_cstack();
331        lex_init("");
332        zcontext_save();
333        zcontext_restore();
334        assert!(
335            cstack.lock().unwrap().is_none(),
336            "save+restore leaves no stack"
337        );
338    }
339
340    /// Nested save/restore is correctly LIFO.
341    #[test]
342    fn context_corpus_nested_save_restore_lifo() {
343        let _g = crate::test_util::global_state_lock();
344        reset_cstack();
345        lex_init("");
346        zcontext_save();
347        zcontext_save();
348        zcontext_save();
349        zcontext_restore();
350        zcontext_restore();
351        zcontext_restore();
352        assert!(cstack.lock().unwrap().is_none(), "all 3 levels drained");
353    }
354
355    /// `zcontext_save_partial(1)` + `zcontext_restore_partial(1)`
356    /// round-trips without panic.
357    #[test]
358    fn context_corpus_save_partial_round_trip() {
359        let _g = crate::test_util::global_state_lock();
360        reset_cstack();
361        lex_init("");
362        zcontext_save_partial(1);
363        zcontext_restore_partial(1);
364    }
365
366    // ═══════════════════════════════════════════════════════════════════
367    // Additional C-parity tests for Src/context.c.
368    // ═══════════════════════════════════════════════════════════════════
369
370    /// c:80 — `zcontext_save` pushes onto stack (cstack becomes Some).
371    #[test]
372    fn zcontext_save_pushes_onto_stack() {
373        let _g = crate::test_util::global_state_lock();
374        reset_cstack();
375        lex_init("");
376        zcontext_save();
377        assert!(
378            cstack.lock().unwrap().is_some(),
379            "save should leave cstack with one frame"
380        );
381        zcontext_restore();
382    }
383
384    /// c:117 — `zcontext_restore` pops stack (cstack returns to None).
385    #[test]
386    fn zcontext_restore_pops_to_none_when_balanced() {
387        let _g = crate::test_util::global_state_lock();
388        reset_cstack();
389        lex_init("");
390        zcontext_save();
391        zcontext_restore();
392        assert!(
393            cstack.lock().unwrap().is_none(),
394            "single save+restore returns cstack to None"
395        );
396    }
397
398    /// c:80 — multiple saves build a stack of N frames.
399    #[test]
400    fn zcontext_save_multiple_builds_stack() {
401        let _g = crate::test_util::global_state_lock();
402        reset_cstack();
403        lex_init("");
404        zcontext_save();
405        zcontext_save();
406        zcontext_save();
407        // Walk the linked-list and count.
408        let head = cstack.lock().unwrap();
409        let mut count = 0;
410        let mut cur = head.as_ref();
411        while let Some(node) = cur {
412            count += 1;
413            cur = node.next.as_ref();
414        }
415        drop(head);
416        assert_eq!(count, 3, "3 saves → 3-deep stack");
417        zcontext_restore();
418        zcontext_restore();
419        zcontext_restore();
420    }
421
422    /// c:52 — `zcontext_save_partial(0)` saves NOTHING from the
423    /// constituent sub-stacks but still pushes a frame.
424    #[test]
425    fn zcontext_save_partial_zero_still_pushes_frame() {
426        let _g = crate::test_util::global_state_lock();
427        reset_cstack();
428        lex_init("");
429        zcontext_save_partial(0);
430        assert!(
431            cstack.lock().unwrap().is_some(),
432            "even parts=0 pushes a frame for restore symmetry"
433        );
434        zcontext_restore_partial(0);
435    }
436
437    /// c:89 — `zcontext_restore_partial(0)` on empty stack DPUTS warns
438    /// but doesn't panic.
439    #[test]
440    fn zcontext_restore_on_empty_no_panic() {
441        let _g = crate::test_util::global_state_lock();
442        reset_cstack();
443        // No prior save — restore should not panic.
444        zcontext_restore_partial(0);
445    }
446
447    /// c:52 — `zcontext_save_partial(ZCONTEXT_HIST)` + restore round-trip.
448    #[test]
449    fn zcontext_save_hist_only_round_trip() {
450        let _g = crate::test_util::global_state_lock();
451        reset_cstack();
452        lex_init("");
453        zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
454        zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
455        assert!(cstack.lock().unwrap().is_none());
456    }
457
458    /// c:52 — `zcontext_save_partial(ZCONTEXT_LEX)` + restore round-trip.
459    #[test]
460    fn zcontext_save_lex_only_round_trip() {
461        let _g = crate::test_util::global_state_lock();
462        reset_cstack();
463        lex_init("");
464        zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_LEX);
465        zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_LEX);
466        assert!(cstack.lock().unwrap().is_none());
467    }
468
469    /// c:52 — `zcontext_save_partial(ZCONTEXT_PARSE)` + restore round-trip.
470    #[test]
471    fn zcontext_save_parse_only_round_trip() {
472        let _g = crate::test_util::global_state_lock();
473        reset_cstack();
474        lex_init("");
475        zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_PARSE);
476        zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_PARSE);
477        assert!(cstack.lock().unwrap().is_none());
478    }
479
480    // ═══════════════════════════════════════════════════════════════════
481    // Additional C-parity tests for Src/context.c partial save/restore
482    // semantics + ZCONTEXT_* flag invariants.
483    // ═══════════════════════════════════════════════════════════════════
484
485    /// c:491-495 — ZCONTEXT_HIST/LEX/PARSE are distinct single bits.
486    #[test]
487    fn zcontext_flag_bits_pin_to_one_two_four() {
488        assert_eq!(crate::ported::zsh_h::ZCONTEXT_HIST, 1, "c:491 = 1<<0");
489        assert_eq!(crate::ported::zsh_h::ZCONTEXT_LEX, 2, "c:493 = 1<<1");
490        assert_eq!(crate::ported::zsh_h::ZCONTEXT_PARSE, 4, "c:495 = 1<<2");
491    }
492
493    /// c:491-495 — every ZCONTEXT_* is a single bit (power of two).
494    #[test]
495    fn zcontext_flags_are_single_bits() {
496        for &v in &[
497            crate::ported::zsh_h::ZCONTEXT_HIST,
498            crate::ported::zsh_h::ZCONTEXT_LEX,
499            crate::ported::zsh_h::ZCONTEXT_PARSE,
500        ] {
501            assert!((v as u32).is_power_of_two(), "{} must be a single bit", v);
502        }
503    }
504
505    /// c:52 — `zcontext_save_partial(0)` followed by restore should
506    /// drain the stack to empty (the c:60-78 branches all skip when
507    /// no flags match, but the frame still pushes per c:89).
508    #[test]
509    fn zcontext_save_partial_zero_round_trip_pops() {
510        let _g = crate::test_util::global_state_lock();
511        reset_cstack();
512        lex_init("");
513        zcontext_save_partial(0);
514        assert!(cstack.lock().unwrap().is_some(), "zero-mask still pushes");
515        zcontext_restore_partial(0);
516        assert!(cstack.lock().unwrap().is_none(), "restore drains to None");
517    }
518
519    /// c:52 — save HIST then restore with PARSE alone should still
520    /// pop the frame (c:91 unconditional take), but NOT restore HIST.
521    /// Pins the asymmetric-mask edge case from c:96.
522    #[test]
523    fn zcontext_save_hist_restore_with_parse_mask_pops_frame() {
524        let _g = crate::test_util::global_state_lock();
525        reset_cstack();
526        lex_init("");
527        zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
528        assert!(cstack.lock().unwrap().is_some());
529        zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_PARSE);
530        assert!(cstack.lock().unwrap().is_none(), "frame popped regardless");
531    }
532
533    /// c:80 — `zcontext_save` and `zcontext_save_partial(HIST|LEX|PARSE)`
534    /// must each push exactly one frame.
535    #[test]
536    fn zcontext_save_shorthand_pushes_one_frame() {
537        let _g = crate::test_util::global_state_lock();
538        reset_cstack();
539        lex_init("");
540        zcontext_save();
541        // Stack must have one frame (top != None, top.next == None).
542        {
543            let head = cstack.lock().unwrap();
544            assert!(head.is_some(), "save pushed a frame");
545            assert!(head.as_ref().unwrap().next.is_none(), "stack depth = 1");
546        }
547        zcontext_restore();
548        assert!(cstack.lock().unwrap().is_none());
549
550        zcontext_save_partial(
551            crate::ported::zsh_h::ZCONTEXT_HIST
552                | crate::ported::zsh_h::ZCONTEXT_LEX
553                | crate::ported::zsh_h::ZCONTEXT_PARSE,
554        );
555        {
556            let head = cstack.lock().unwrap();
557            assert!(head.is_some(), "full-mask save_partial pushed a frame");
558            assert!(head.as_ref().unwrap().next.is_none(), "stack depth = 1");
559        }
560        zcontext_restore();
561    }
562
563    /// c:117 — `zcontext_restore` shorthand drains exactly one frame.
564    #[test]
565    fn zcontext_restore_drains_exactly_one_frame() {
566        let _g = crate::test_util::global_state_lock();
567        reset_cstack();
568        lex_init("");
569        zcontext_save();
570        zcontext_save();
571        // Two pushes.
572        zcontext_restore();
573        // One frame remains.
574        assert!(cstack.lock().unwrap().is_some());
575        zcontext_restore();
576        assert!(cstack.lock().unwrap().is_none());
577    }
578
579    /// c:91 — `zcontext_restore` shorthand on empty stack is a no-op
580    /// (matches DPUTS-trip path: warn + return without panic).
581    #[test]
582    fn zcontext_restore_shorthand_on_empty_no_panic() {
583        let _g = crate::test_util::global_state_lock();
584        reset_cstack();
585        zcontext_restore();
586        assert!(cstack.lock().unwrap().is_none());
587    }
588
589    /// c:52 — `ZCONTEXT_HIST | ZCONTEXT_LEX` combo (no PARSE) round-trips.
590    #[test]
591    fn zcontext_save_hist_lex_combo_round_trip() {
592        let _g = crate::test_util::global_state_lock();
593        reset_cstack();
594        lex_init("");
595        let mask = crate::ported::zsh_h::ZCONTEXT_HIST | crate::ported::zsh_h::ZCONTEXT_LEX;
596        zcontext_save_partial(mask);
597        zcontext_restore_partial(mask);
598        assert!(cstack.lock().unwrap().is_none());
599    }
600
601    /// c:52 — `ZCONTEXT_LEX | ZCONTEXT_PARSE` combo (no HIST) round-trips.
602    #[test]
603    fn zcontext_save_lex_parse_combo_round_trip() {
604        let _g = crate::test_util::global_state_lock();
605        reset_cstack();
606        lex_init("");
607        let mask = crate::ported::zsh_h::ZCONTEXT_LEX | crate::ported::zsh_h::ZCONTEXT_PARSE;
608        zcontext_save_partial(mask);
609        zcontext_restore_partial(mask);
610        assert!(cstack.lock().unwrap().is_none());
611    }
612
613    /// c:91 — multiple restores on empty stack stay no-op (no DPUTS panic).
614    #[test]
615    fn many_restores_on_empty_no_panic() {
616        let _g = crate::test_util::global_state_lock();
617        reset_cstack();
618        for _ in 0..10 {
619            zcontext_restore_partial(0);
620        }
621        assert!(cstack.lock().unwrap().is_none());
622    }
623
624    /// c:91 — DPUTS WARN-only: restore on empty stack must not crash.
625    #[test]
626    fn restore_on_empty_via_full_shorthand_no_panic() {
627        let _g = crate::test_util::global_state_lock();
628        reset_cstack();
629        zcontext_restore();
630        zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
631        zcontext_restore_partial(
632            crate::ported::zsh_h::ZCONTEXT_HIST | crate::ported::zsh_h::ZCONTEXT_LEX,
633        );
634        assert!(cstack.lock().unwrap().is_none());
635    }
636
637    // ═══════════════════════════════════════════════════════════════════
638    // Additional C-parity pins for Src/context.c
639    // c:52 zcontext_save_partial / c:80 zcontext_save /
640    // c:89 zcontext_restore_partial / c:126 zcontext_restore /
641    // zsh.h:491-495 ZCONTEXT_* flags
642    // ═══════════════════════════════════════════════════════════════════
643
644    /// c:52 — `zcontext_save_partial(ZCONTEXT_HIST)` pushes exactly one frame.
645    #[test]
646    fn zcontext_save_partial_hist_pushes_one_frame() {
647        let _g = crate::test_util::global_state_lock();
648        reset_cstack();
649        zcontext_save_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
650        assert!(
651            cstack.lock().unwrap().is_some(),
652            "frame must exist after save"
653        );
654        zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
655        assert!(cstack.lock().unwrap().is_none(), "frame must be drained");
656    }
657
658    /// c:80-83 — `zcontext_save()` saves all three (HIST|LEX|PARSE).
659    #[test]
660    fn zcontext_save_full_then_restore_full_clears_stack() {
661        let _g = crate::test_util::global_state_lock();
662        reset_cstack();
663        zcontext_save();
664        assert!(cstack.lock().unwrap().is_some());
665        zcontext_restore();
666        assert!(cstack.lock().unwrap().is_none());
667    }
668
669    /// c:52 — three saves push three frames; three restores drain all.
670    #[test]
671    fn zcontext_save_three_then_restore_three_clears_stack() {
672        let _g = crate::test_util::global_state_lock();
673        reset_cstack();
674        zcontext_save();
675        zcontext_save();
676        zcontext_save();
677        zcontext_restore();
678        zcontext_restore();
679        zcontext_restore();
680        assert!(
681            cstack.lock().unwrap().is_none(),
682            "all 3 frames must drain (LIFO)"
683        );
684    }
685
686    /// c:80 — `zcontext_save` is the shorthand for the OR of all three.
687    #[test]
688    fn zcontext_save_equals_or_of_three_flags_behavior() {
689        let _g = crate::test_util::global_state_lock();
690        reset_cstack();
691        zcontext_save();
692        let depth1 = cstack.lock().unwrap().is_some();
693        reset_cstack();
694        zcontext_save_partial(
695            crate::ported::zsh_h::ZCONTEXT_HIST
696                | crate::ported::zsh_h::ZCONTEXT_LEX
697                | crate::ported::zsh_h::ZCONTEXT_PARSE,
698        );
699        let depth2 = cstack.lock().unwrap().is_some();
700        assert_eq!(
701            depth1, depth2,
702            "shorthand and explicit OR must produce same push behavior"
703        );
704        zcontext_restore();
705    }
706
707    /// zsh.h:491-495 — ZCONTEXT_HIST|LEX|PARSE = 0b111 = 7.
708    #[test]
709    fn zcontext_all_flags_or_equals_seven() {
710        let all = crate::ported::zsh_h::ZCONTEXT_HIST
711            | crate::ported::zsh_h::ZCONTEXT_LEX
712            | crate::ported::zsh_h::ZCONTEXT_PARSE;
713        assert_eq!(all, 7, "HIST|LEX|PARSE must equal 0b111 = 7");
714    }
715
716    /// zsh.h:491-495 — flags are pairwise distinct.
717    #[test]
718    fn zcontext_flags_pairwise_distinct() {
719        use crate::ported::zsh_h::{ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE};
720        let codes = [ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE];
721        let unique: std::collections::HashSet<_> = codes.iter().copied().collect();
722        assert_eq!(unique.len(), codes.len(), "ZCONTEXT_* must be distinct");
723    }
724
725    /// c:52 / c:89 — save/restore is LIFO (nested frames).
726    #[test]
727    fn zcontext_save_restore_is_lifo() {
728        let _g = crate::test_util::global_state_lock();
729        reset_cstack();
730        zcontext_save();
731        zcontext_save();
732        assert!(cstack.lock().unwrap().is_some());
733        zcontext_restore();
734        assert!(cstack.lock().unwrap().is_some(), "one frame still on stack");
735        zcontext_restore();
736        assert!(cstack.lock().unwrap().is_none(), "all frames drained");
737    }
738
739    /// c:52 — `zcontext_save_partial(-1)` (all-bits) doesn't panic.
740    #[test]
741    fn zcontext_save_partial_all_bits_no_panic() {
742        let _g = crate::test_util::global_state_lock();
743        reset_cstack();
744        zcontext_save_partial(-1);
745        zcontext_restore_partial(-1);
746        assert!(cstack.lock().unwrap().is_none());
747    }
748
749    /// c:91 — `zcontext_restore_partial(-1)` on empty doesn't panic.
750    #[test]
751    fn zcontext_restore_partial_all_bits_on_empty_no_panic() {
752        let _g = crate::test_util::global_state_lock();
753        reset_cstack();
754        zcontext_restore_partial(-1);
755        assert!(cstack.lock().unwrap().is_none());
756    }
757
758    /// c:52 — many saves then full drain leaves stack empty.
759    #[test]
760    fn zcontext_many_saves_then_full_drain_clean() {
761        let _g = crate::test_util::global_state_lock();
762        reset_cstack();
763        for _ in 0..20 {
764            zcontext_save();
765        }
766        for _ in 0..20 {
767            zcontext_restore();
768        }
769        assert!(
770            cstack.lock().unwrap().is_none(),
771            "after 20 paired save/restore, stack must be empty"
772        );
773    }
774
775    /// c:91 — restore with subset mask of saved flags still drains frame.
776    #[test]
777    fn zcontext_restore_with_subset_mask_drains_frame() {
778        let _g = crate::test_util::global_state_lock();
779        reset_cstack();
780        zcontext_save_partial(
781            crate::ported::zsh_h::ZCONTEXT_HIST | crate::ported::zsh_h::ZCONTEXT_LEX,
782        );
783        zcontext_restore_partial(crate::ported::zsh_h::ZCONTEXT_HIST);
784        assert!(
785            cstack.lock().unwrap().is_none(),
786            "restore with subset mask still pops the frame"
787        );
788    }
789}