Skip to main content

zsh/ported/
linklist.rs

1//! Linked list implementation for zshrs
2//!
3//! Direct port from zsh/Src/linklist.c
4//!
5//! Get an empty linked list header                                          // c:99
6//! Insert a node in a linked list after a given node                       // c:129
7//! Remove a node from a linked list                                        // c:247
8//! Free a linked list                                                      // c:283
9//! Count the number of nodes in a linked list                              // c:300
10//!
11//! Provides the canonical `LinkList<T>` used everywhere a C source line
12//! takes a `LinkList`. Backed by `VecDeque<T>` so index-based access used
13//! by `Src/subst.c` walks (`firstnode` / `nextnode` / `incnode` /
14//! `getdata` / `setdata`) is O(1) — same big-O as C's pointer walk over
15//! `linknode->next`.
16//!
17//! Mirrors `struct linklist` from `Src/zsh.h:563` — `first` / `last` /
18//! `flags`. Rust folds `first`/`last` into the `VecDeque`'s head/tail
19//! pointers; the `flags` field is preserved as `u32`. Subst.c sets
20//! `LF_ARRAY` (`Src/subst.c:33`) on the flag word.
21
22use std::collections::VecDeque;
23
24// ===========================================================
25// Free-fn ports of `Src/linklist.c` (functions, not macros).
26// ===========================================================
27
28// Get an empty linked list header                                         // c:116
29/// Port of `newlinklist()` (`Src/linklist.c:103`).
30pub fn newlinklist() -> LinkList<String> {
31    // c:103
32    LinkList::new()
33}
34
35impl<T> Default for LinkList<T> {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl<T> LinkList<T> {
42    // Get an empty linked list header                                        // c:99
43    /// Port of `znewlinklist()` from Src/linklist.c:116 — heap-arena
44    /// fresh empty list. Rust uses `LinkList::new()`.
45    pub fn new() -> Self {
46        // c:116
47        LinkList {
48            nodes: VecDeque::new(),
49            flags: 0,
50        }
51    }
52
53    /// Port of the C macro `empty(list)` (`Src/zsh.h:583`) —
54    /// `firstnode(list) == NULL`.
55    pub fn is_empty(&self) -> bool {
56        // c:zsh.h:583
57        self.nodes.is_empty()
58    }
59
60    // Count the number of nodes in a linked list                             // c:300
61    /// Port of `countlinknodes(LinkList list)` from Src/linklist.c:304.
62    pub fn len(&self) -> usize {
63        // c:304
64        self.nodes.len()
65    }
66
67    /// Push at the head. Port of the C macro `pushnode()` (`Src/zsh.h`).
68    pub fn push_front(&mut self, data: T) {
69        // c:151
70        self.nodes.push_front(data);
71    }
72
73    /// Push at the tail. Port of `addlinknode()` (`Src/zsh.h`) /
74    /// `zaddlinknode()` (`Src/linklist.c:151`).
75    pub fn push_back(&mut self, data: T) {
76        // c:151
77        self.nodes.push_back(data);
78    }
79
80    /// Pop the head. Port of `getlinknode(LinkList list)` (`Src/linklist.c:210`).
81    pub fn pop_front(&mut self) -> Option<T> {
82        // c:210
83        self.nodes.pop_front()
84    }
85
86    /// Pop the tail. Port of `remnode(list, lastnode(list))` idiom.
87    pub fn pop_back(&mut self) -> Option<T> {
88        // c:251
89        self.nodes.pop_back()
90    }
91
92    /// Front-element ref, equivalent to `firstnode(list)->dat`
93    /// (`Src/zsh.h:576,586`).
94    pub fn front(&self) -> Option<&T> {
95        self.nodes.front()
96    }
97    /// `front_mut` — see implementation.
98    pub fn front_mut(&mut self) -> Option<&mut T> {
99        self.nodes.front_mut()
100    }
101
102    /// Back-element ref, equivalent to `lastnode(list)->dat`
103    /// (`Src/zsh.h:577,586`).
104    pub fn back(&self) -> Option<&T> {
105        self.nodes.back()
106    }
107    /// `back_mut` — see implementation.
108    pub fn back_mut(&mut self) -> Option<&mut T> {
109        self.nodes.back_mut()
110    }
111    /// `iter` — see implementation.
112    pub fn iter(&self) -> std::collections::vec_deque::Iter<'_, T> {
113        self.nodes.iter()
114    }
115    /// `iter_mut` — see implementation.
116    pub fn iter_mut(&mut self) -> std::collections::vec_deque::IterMut<'_, T> {
117        self.nodes.iter_mut()
118    }
119
120    /// Append `other` onto the tail; drains `other`. Port of
121    /// `joinlists()` (`Src/linklist.c:360`).
122    pub fn append(&mut self, other: &mut LinkList<T>) {
123        // c:360
124        self.nodes.append(&mut other.nodes);
125    }
126
127    /// Drop every node. Port of `freelinklist(list, NULL)`
128    /// (`Src/linklist.c:287`).
129    pub fn clear(&mut self) {
130        // c:287
131        self.nodes.clear();
132    }
133    /// `to_vec` — see implementation.
134    pub fn to_vec(self) -> Vec<T>
135    where
136        T: Clone,
137    {
138        self.nodes.into_iter().collect()
139    }
140
141    // ===== C-macro accessors (Src/zsh.h:576-590) =====
142
143    /// Port of `firstnode(X)` macro (`Src/zsh.h:576`) — head node
144    /// handle. Rust uses `usize` indices since the `VecDeque` backing
145    /// gives O(1) random access matching C's pointer walk.
146    pub fn firstnode(&self) -> Option<usize> {
147        // c:zsh.h:576
148        if self.nodes.is_empty() {
149            None
150        } else {
151            Some(0)
152        }
153    }
154
155    /// Port of `lastnode(X)` macro (`Src/zsh.h:577`).
156    pub fn lastnode(&self) -> Option<usize> {
157        // c:zsh.h:577
158        if self.nodes.is_empty() {
159            None
160        } else {
161            Some(self.nodes.len() - 1)
162        }
163    }
164
165    /// Port of `nextnode(X)` macro (`Src/zsh.h:588`).
166    pub fn nextnode(&self, idx: usize) -> Option<usize> {
167        // c:zsh.h:588
168        if idx + 1 < self.nodes.len() {
169            Some(idx + 1)
170        } else {
171            None
172        }
173    }
174
175    /// Port of `prevnode(X)` macro (`Src/zsh.h:589`).
176    pub fn prevnode(&self, idx: usize) -> Option<usize> {
177        // c:zsh.h:589
178        if idx > 0 && idx <= self.nodes.len() {
179            Some(idx - 1)
180        } else {
181            None
182        }
183    }
184
185    /// Port of `getdata(X)` macro (`Src/zsh.h:586`).
186    pub fn getdata(&self, idx: usize) -> Option<&T> {
187        // c:zsh.h:586
188        self.nodes.get(idx)
189    }
190
191    /// Port of `setdata(X,Y)` macro (`Src/zsh.h:587`).
192    pub fn setdata(&mut self, idx: usize, data: T) {
193        // c:zsh.h:587
194        if let Some(slot) = self.nodes.get_mut(idx) {
195            *slot = data;
196        }
197    }
198
199    /// Port of `empty(X)` macro (`Src/zsh.h:583`).
200    pub fn empty(&self) -> bool {
201        // c:zsh.h:583
202        self.nodes.is_empty()
203    }
204
205    /// Port of `insertlinknode(list, after, dat)` macro
206    /// (`Src/zsh.h:580`) and the function form (`Src/linklist.c:133`)
207    /// — insert after the supplied node index, return the index of the
208    /// inserted node.
209    /// WARNING: param names don't match C — Rust=(after_idx, data) vs C=(list, node, dat)
210    pub fn insertlinknode(&mut self, after_idx: usize, data: T) -> usize {
211        // c:linklist.c:133
212        let new_idx = after_idx + 1;
213        if new_idx >= self.nodes.len() {
214            self.nodes.push_back(data);
215            self.nodes.len() - 1
216        } else {
217            self.nodes.insert(new_idx, data);
218            new_idx
219        }
220    }
221
222    /// Remove + free a node. Port of `remnode(LinkList list, LinkNode nd)` (`Src/linklist.c:251`).
223    pub fn delete_node(&mut self, idx: usize) -> Option<T> {
224        // c:251
225        self.nodes.remove(idx)
226    }
227
228    /// Port of `pushlinknode(list, val)` head-insert helper.
229    pub fn insert_at(&mut self, idx: usize, data: T) {
230        if idx >= self.nodes.len() {
231            self.nodes.push_back(data);
232        } else {
233            self.nodes.insert(idx, data);
234        }
235    }
236}
237
238impl<T: Clone> Clone for LinkList<T> {
239    fn clone(&self) -> Self {
240        LinkList {
241            nodes: self.nodes.clone(),
242            flags: self.flags,
243        }
244    }
245}
246
247impl<T: std::fmt::Debug> std::fmt::Debug for LinkList<T> {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        f.debug_struct("LinkList")
250            .field("nodes", &self.nodes)
251            .field("flags", &self.flags)
252            .finish()
253    }
254}
255
256impl<T> FromIterator<T> for LinkList<T> {
257    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
258        let mut list = LinkList::new();
259        for item in iter {
260            list.push_back(item);
261        }
262        list
263    }
264}
265
266impl<T> IntoIterator for LinkList<T> {
267    type Item = T;
268    type IntoIter = std::collections::vec_deque::IntoIter<T>;
269
270    fn into_iter(self) -> Self::IntoIter {
271        self.nodes.into_iter()
272    }
273}
274
275impl<'a, T> IntoIterator for &'a LinkList<T> {
276    type Item = &'a T;
277    type IntoIter = std::collections::vec_deque::Iter<'a, T>;
278
279    fn into_iter(self) -> Self::IntoIter {
280        self.nodes.iter()
281    }
282}
283
284/// Port of `znewlinklist()` (`Src/linklist.c:116`).
285pub fn znewlinklist() -> LinkList<String> {
286    // c:116
287    LinkList::new()
288}
289
290// Insert a node in a linked list after a given node                       // c:151
291/// Port of `insertlinknode(LinkList list, LinkNode node, void *dat)` (`Src/linklist.c:133`).
292pub fn insertlinknode<T>(list: &mut LinkList<T>, node: usize, dat: T) -> usize {
293    // c:133
294    list.insertlinknode(node, dat)
295}
296
297/// Port of `zinsertlinknode(LinkList list, LinkNode node, void *dat)` (`Src/linklist.c:151`).
298pub fn zinsertlinknode<T>(list: &mut LinkList<T>, node: usize, dat: T) -> usize {
299    list.insertlinknode(node, dat)
300}
301
302/// Port of `uinsertlinknode(LinkList list, LinkNode node, LinkNode new)` (`Src/linklist.c:173`).
303pub fn uinsertlinknode(list: &mut LinkList<String>, node: usize, new: String) -> Option<usize> {
304    if list.iter().any(|s| s == &new) {
305        None
306    } else {
307        Some(list.insertlinknode(node, new))
308    }
309}
310
311// Insert a list in another list                                           // c:210
312/// Port of `insertlinklist(LinkList l, LinkNode where, LinkList x)` from
313/// `Src/linklist.c:190`. **C semantics: `l` is the SOURCE list, `where`
314/// is the position in DEST list `x`, and `x` is the DESTINATION**. All
315/// nodes of `l` get spliced into `x` right after node `where` —
316/// equivalent to inserting the contents of `l` between `where` and
317/// `where->next` in `x`. Empty `l` is a no-op (c:194 `if (!firstnode(l))
318/// return;`). Param names + positions match C exactly so callers
319/// reading `insertlinklist(sub.in, lastnode(result->in), result->in)`
320/// (the canonical zutil.c:1324 pattern) translate 1:1.
321pub fn insertlinklist<T: Clone>(
322    // c:190
323    l: &LinkList<T>,
324    where_idx: usize,
325    x: &mut LinkList<T>,
326) {
327    if l.is_empty() {
328        // c:194
329        return;
330    }
331    let mut idx = where_idx;
332    for v in l.iter() {
333        // c:196 walk l, splice into x
334        idx = x.insertlinknode(idx, v.clone());
335    }
336}
337
338// Pop the top node off a linked list and free it.                         // c:210
339/// Port of `getlinknode(LinkList list)` (`Src/linklist.c:210`).
340pub fn getlinknode<T>(list: &mut LinkList<T>) -> Option<T> {
341    // c:210
342    list.pop_front()
343}
344
345// Pop the top node off a linked list without freeing it.                  // c:251
346/// Port of `ugetnode(LinkList list)` (`Src/linklist.c:231`).
347pub fn ugetnode<T>(list: &mut LinkList<T>) -> Option<T> {
348    // c:231
349    list.pop_front()
350}
351
352// Remove a node from a linked list                                        // c:270
353/// Port of `remnode(LinkList list, LinkNode nd)` (`Src/linklist.c:251`).
354pub fn remnode<T>(list: &mut LinkList<T>, nd: usize) -> Option<T> {
355    // c:251
356    list.delete_node(nd)
357}
358
359/// Port of `uremnode(LinkList list, LinkNode nd)` (`Src/linklist.c:270`).
360pub fn uremnode<T>(list: &mut LinkList<T>, nd: usize) -> Option<T> {
361    // c:270
362    list.delete_node(nd)
363}
364
365// Free a linked list                                                       // c:304
366/// Port of `freelinklist(LinkList list, FreeFunc freefunc)` (`Src/linklist.c:287`).
367/// WARNING: param names don't match C — Rust=(list) vs C=(list, freefunc)
368pub fn freelinklist<T>(list: &mut LinkList<T>) {
369    // c:287
370    list.clear();
371}
372
373// Count the number of nodes in a linked list                              // c:317
374/// Port of `countlinknodes(LinkList list)` (`Src/linklist.c:304`).
375pub fn countlinknodes<T>(list: &LinkList<T>) -> usize {
376    // c:304
377    list.len()
378}
379
380// Make specified node first, moving preceding nodes to end                // c:317
381/// Port of `rolllist(LinkList l, LinkNode nd)` (`Src/linklist.c:317`).
382pub fn rolllist<T>(l: &mut LinkList<T>, nd: usize) {
383    // c:317
384    let len = l.len();
385    if len > 0 {
386        let nd = nd % len;
387        for _ in 0..nd {
388            if let Some(v) = l.pop_front() {
389                l.push_back(v);
390            }
391        }
392    }
393}
394
395// Create linklist of specified size. node->dats are not initialized.      // c:331
396/// Port of `newsizedlist(int size)` from `Src/linklist.c:331-348`.
397///
398/// C body allocates a header + `size` pre-linked placeholder nodes
399/// with uninitialized data; the C `for` loop wires prev/next
400/// pointers (c:339-341). Callers iterate and fill data into each
401/// slot.
402///
403/// The previous Rust port returned an empty list (ignoring `size`),
404/// so any caller expecting `size` placeholder slots would iterate
405/// over nothing. Fix by pushing `size` default-constructed nodes.
406pub fn newsizedlist<T: Default>(size: usize) -> LinkList<T> {
407    // c:331
408    let mut list = LinkList::new();
409    for _ in 0..size {
410        // c:339-341
411        list.push_back(T::default());
412    }
413    list
414}
415
416/// Port of `joinlists(LinkList first, LinkList second)` (`Src/linklist.c:360`).
417pub fn joinlists<T>(first: &mut LinkList<T>, second: &mut LinkList<T>) {
418    // c:360
419    first.append(second);
420}
421
422/// Port of `linknodebydatum(LinkList list, void *dat)` (`Src/linklist.c:386`).
423pub fn linknodebydatum<T: PartialEq>(list: &LinkList<T>, dat: &T) -> Option<usize> {
424    // c:386
425    list.iter().position(|v| v == dat)
426}
427
428/// Port of `linknodebystring(LinkList list, char *dat)` (`Src/linklist.c:403`).
429pub fn linknodebystring(list: &LinkList<String>, dat: &str) -> Option<usize> {
430    // c:403
431    list.iter().position(|v| v == dat)
432}
433
434/// Convert a linked list of strings to a `Vec`. Port of
435/// `hlinklist2array()` (`Src/linklist.c:423`).
436pub fn hlinklist2array(list: &LinkList<String>) -> Vec<String> {
437    // c:423
438    list.iter().cloned().collect()
439}
440
441/// Port of `zlinklist2array(LinkList list, int copy)` (`Src/linklist.c:449`).
442/// WARNING: param names don't match C — Rust=(list) vs C=(list, copy)
443pub fn zlinklist2array(list: &LinkList<String>) -> Vec<String> {
444    // c:449
445    list.iter().cloned().collect()
446}
447
448/// A doubly-ended list, port of `struct linklist` (`Src/zsh.h:563`).
449/// `flags` carries `LF_ARRAY` and friends from `Src/subst.c:33`.
450pub struct LinkList<T> {
451    pub nodes: VecDeque<T>, // c:zsh.h:565,566
452    pub flags: u32,         // c:zsh.h:567
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    #[test]
460    fn test_new_list() {
461        let _g = crate::test_util::global_state_lock();
462        let list: LinkList<i32> = LinkList::new();
463        assert!(list.is_empty());
464        assert_eq!(list.len(), 0);
465        assert_eq!(list.flags, 0);
466    }
467
468    /// Pin `newsizedlist(N)` to canonical C body at
469    /// `Src/linklist.c:339-341`: pre-allocates N placeholder nodes
470    /// with uninitialized data, ready for callers to fill in.
471    /// The previous Rust port returned an empty list, ignoring `size`.
472    #[test]
473    fn newsizedlist_preallocates_n_slots() {
474        let _g = crate::test_util::global_state_lock();
475        let list: LinkList<i32> = newsizedlist(5);
476        assert_eq!(
477            list.len(),
478            5,
479            "c:339-341 — newsizedlist(5) must pre-allocate 5 nodes"
480        );
481        // Default-constructed i32 is 0; every slot ready for assign.
482        for v in list.iter() {
483            assert_eq!(*v, 0, "pre-allocated slots default to 0");
484        }
485
486        let zero_list: LinkList<String> = newsizedlist(0);
487        assert_eq!(zero_list.len(), 0, "newsizedlist(0) is the same as new()");
488    }
489
490    #[test]
491    fn test_push_front_back() {
492        let _g = crate::test_util::global_state_lock();
493        let mut list = LinkList::new();
494        list.push_back(1);
495        list.push_back(2);
496        list.push_front(0);
497        assert_eq!(list.front(), Some(&0));
498        assert_eq!(list.back(), Some(&2));
499        assert_eq!(list.len(), 3);
500    }
501
502    #[test]
503    fn test_pop_front_back() {
504        let _g = crate::test_util::global_state_lock();
505        let mut list = LinkList::new();
506        list.push_back(1);
507        list.push_back(2);
508        list.push_back(3);
509        assert_eq!(list.pop_front(), Some(1));
510        assert_eq!(list.pop_back(), Some(3));
511        assert_eq!(list.pop_front(), Some(2));
512        assert_eq!(list.pop_front(), None);
513    }
514
515    #[test]
516    fn test_iter() {
517        let _g = crate::test_util::global_state_lock();
518        let mut list = LinkList::new();
519        list.push_back(1);
520        list.push_back(2);
521        list.push_back(3);
522        let v: Vec<_> = list.iter().copied().collect();
523        assert_eq!(v, vec![1, 2, 3]);
524    }
525
526    #[test]
527    fn test_macro_methods() {
528        let _g = crate::test_util::global_state_lock();
529        let mut list: LinkList<String> = LinkList::new();
530        list.push_back("a".to_string());
531        list.push_back("b".to_string());
532        list.push_back("c".to_string());
533
534        assert_eq!(list.firstnode(), Some(0));
535        assert_eq!(list.lastnode(), Some(2));
536        assert_eq!(list.nextnode(0), Some(1));
537        assert_eq!(list.nextnode(2), None);
538        assert_eq!(list.getdata(1).map(String::as_str), Some("b"));
539        list.setdata(1, "B".to_string());
540        assert_eq!(list.getdata(1).map(String::as_str), Some("B"));
541        let new_idx = list.insertlinknode(1, "X".to_string());
542        assert_eq!(new_idx, 2);
543        assert_eq!(list.getdata(2).map(String::as_str), Some("X"));
544        assert_eq!(list.delete_node(2).as_deref(), Some("X"));
545        assert_eq!(list.getdata(2).map(String::as_str), Some("c"));
546    }
547
548    #[test]
549    fn test_append() {
550        let _g = crate::test_util::global_state_lock();
551        let mut a: LinkList<i32> = vec![1, 2].into_iter().collect();
552        let mut b: LinkList<i32> = vec![3, 4].into_iter().collect();
553        a.append(&mut b);
554        assert!(b.is_empty());
555        assert_eq!(a.to_vec(), vec![1, 2, 3, 4]);
556    }
557
558    #[test]
559    fn test_clear() {
560        let _g = crate::test_util::global_state_lock();
561        let mut list: LinkList<i32> = vec![1, 2, 3].into_iter().collect();
562        list.clear();
563        assert!(list.is_empty());
564    }
565
566    #[test]
567    fn test_uinsertlinknode_dedups() {
568        let _g = crate::test_util::global_state_lock();
569        let mut list: LinkList<String> = LinkList::new();
570        list.push_back("a".to_string());
571        assert!(uinsertlinknode(&mut list, 0, "b".to_string()).is_some());
572        assert!(uinsertlinknode(&mut list, 0, "a".to_string()).is_none());
573        assert_eq!(list.len(), 2);
574    }
575
576    /// c:360 — `joinlists(first, second)` moves all of `second` onto
577    /// the end of `first`, draining second. A regression where second
578    /// isn't drained would let the caller iterate doubled entries.
579    #[test]
580    fn joinlists_drains_second_into_first() {
581        let _g = crate::test_util::global_state_lock();
582        let mut a: LinkList<i32> = vec![1, 2].into_iter().collect();
583        let mut b: LinkList<i32> = vec![3, 4, 5].into_iter().collect();
584        joinlists(&mut a, &mut b);
585        assert_eq!(a.to_vec(), vec![1, 2, 3, 4, 5]);
586        assert!(b.is_empty(), "second list must be drained after join");
587    }
588
589    /// c:360 — joining an empty `second` is a no-op. Catches a
590    /// regression that adds phantom empty sentinels.
591    #[test]
592    fn joinlists_empty_second_is_noop() {
593        let _g = crate::test_util::global_state_lock();
594        let mut a: LinkList<i32> = vec![1, 2].into_iter().collect();
595        let mut b: LinkList<i32> = LinkList::new();
596        joinlists(&mut a, &mut b);
597        assert_eq!(a.to_vec(), vec![1, 2]);
598        assert!(b.is_empty());
599    }
600
601    /// c:360 — joining INTO an empty `first` transfers second
602    /// cleanly. The empty-head edge case in the C body has a
603    /// dedicated branch — regression there would lose the data.
604    #[test]
605    fn joinlists_empty_first_receives_all_of_second() {
606        let _g = crate::test_util::global_state_lock();
607        let mut a: LinkList<i32> = LinkList::new();
608        let mut b: LinkList<i32> = vec![1, 2, 3].into_iter().collect();
609        joinlists(&mut a, &mut b);
610        assert_eq!(a.to_vec(), vec![1, 2, 3]);
611        assert!(b.is_empty());
612    }
613
614    /// c:386 — `linknodebydatum` returns Some(idx) for the first
615    /// matching entry, None for miss. Used by `unhash -d` lookups.
616    #[test]
617    fn linknodebydatum_finds_first_match() {
618        let _g = crate::test_util::global_state_lock();
619        let list: LinkList<i32> = vec![10, 20, 30, 20].into_iter().collect();
620        assert_eq!(
621            linknodebydatum(&list, &20),
622            Some(1),
623            "must return FIRST match index"
624        );
625        assert_eq!(linknodebydatum(&list, &99), None);
626    }
627
628    /// c:403 — `linknodebystring` is the string-specialised variant.
629    /// Verifies same FIRST-match contract for the alias-table walks.
630    #[test]
631    fn linknodebystring_finds_first_match() {
632        let _g = crate::test_util::global_state_lock();
633        let list: LinkList<String> = vec!["a".into(), "b".into(), "a".into()]
634            .into_iter()
635            .collect();
636        assert_eq!(linknodebystring(&list, "a"), Some(0));
637        assert_eq!(linknodebystring(&list, "b"), Some(1));
638        assert_eq!(linknodebystring(&list, "x"), None);
639    }
640
641    /// c:423 — `hlinklist2array` flattens to Vec preserving order.
642    /// Used by `${(@k)hash}` array materialisation.
643    #[test]
644    fn hlinklist2array_preserves_order() {
645        let _g = crate::test_util::global_state_lock();
646        let list: LinkList<String> = vec!["a".into(), "b".into(), "c".into()]
647            .into_iter()
648            .collect();
649        let arr = hlinklist2array(&list);
650        assert_eq!(arr, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
651    }
652
653    /// `Src/linklist.c:302-311` — `countlinknodes(list)` walks the
654    /// `next` chain incrementing a counter. Empty list → 0.
655    #[test]
656    fn countlinknodes_returns_len_for_arbitrary_lists() {
657        let _g = crate::test_util::global_state_lock();
658        let empty: LinkList<i32> = LinkList::new();
659        assert_eq!(
660            countlinknodes(&empty),
661            0,
662            "c:309 — empty list traversal yields 0"
663        );
664        let one: LinkList<i32> = vec![42].into_iter().collect();
665        assert_eq!(countlinknodes(&one), 1);
666        let many: LinkList<i32> = (0..100).collect();
667        assert_eq!(countlinknodes(&many), 100);
668    }
669
670    /// `Src/linklist.c:316-325` — `rolllist(l, nd)` makes `nd` first,
671    /// moving preceding nodes to end (circular rotation). The Rust
672    /// port treats `nd` as a 0-indexed position to rotate to the
673    /// front; rotate-by-0 is a no-op, rotate-by-N wraps via modulo.
674    #[test]
675    fn rolllist_rotates_to_index() {
676        let _g = crate::test_util::global_state_lock();
677        // c:319-324 — rotate so nd-th element becomes first.
678        let mut list: LinkList<i32> = vec![10, 20, 30, 40].into_iter().collect();
679        rolllist(&mut list, 2);
680        assert_eq!(
681            list.to_vec(),
682            vec![30, 40, 10, 20],
683            "c:321 — `list.first = nd` then preceding nodes append at end"
684        );
685    }
686
687    /// c:316-325 — rolllist by 0 is the identity. Pin so an off-by-one
688    /// regression doesn't silently rotate every caller by 1.
689    #[test]
690    fn rolllist_zero_index_is_identity() {
691        let _g = crate::test_util::global_state_lock();
692        let mut list: LinkList<i32> = vec![1, 2, 3].into_iter().collect();
693        rolllist(&mut list, 0);
694        assert_eq!(list.to_vec(), vec![1, 2, 3]);
695    }
696
697    /// c:316-325 — rolllist with index >= len wraps via modulo. Pins
698    /// the implementation choice (C version is UB on out-of-range —
699    /// Rust port chose modulo defensively).
700    #[test]
701    fn rolllist_wraps_index_modulo_length() {
702        let _g = crate::test_util::global_state_lock();
703        let mut list: LinkList<i32> = vec![1, 2, 3].into_iter().collect();
704        // index 4 mod 3 == 1 → rotate by 1.
705        rolllist(&mut list, 4);
706        assert_eq!(list.to_vec(), vec![2, 3, 1]);
707    }
708
709    /// `Src/linklist.c:188-206` — `insertlinklist(l, where, x)` splices
710    /// the contents of SOURCE list `l` into DESTINATION list `x` right
711    /// after node `where`. Canonical caller pattern (per
712    /// `Src/Modules/zutil.c:1324`):
713    ///     `insertlinklist(sub.in, lastnode(result->in), result->in);`
714    /// which appends every node from `sub.in` to the end of `result->in`.
715    /// Pin C semantics: source unchanged, dest grows by source's length,
716    /// inserted in the right span and in source order.
717    #[test]
718    fn insertlinklist_splices_source_into_dest_after_position() {
719        let _g = crate::test_util::global_state_lock();
720        // dest: [10, 20, 30], source: [A, B, C], where=0 (after first).
721        // Expected: [10, A, B, C, 20, 30] — source appears AFTER 10.
722        let source: LinkList<i32> = vec![100, 200, 300].into_iter().collect();
723        let mut dest: LinkList<i32> = vec![10, 20, 30].into_iter().collect();
724        insertlinklist(&source, 0, &mut dest);
725        assert_eq!(
726            dest.to_vec(),
727            vec![10, 100, 200, 300, 20, 30],
728            "c:194-202 — source spliced into dest after node 0"
729        );
730        assert_eq!(
731            source.to_vec(),
732            vec![100, 200, 300],
733            "c:188-206 — source list is NOT modified (read-only)"
734        );
735    }
736
737    /// `Src/linklist.c:193-194` — `if (!firstnode(l)) return;` — empty
738    /// source is a no-op. Pins so a regression doesn't accidentally
739    /// insert a phantom sentinel.
740    #[test]
741    fn insertlinklist_empty_source_is_noop() {
742        let _g = crate::test_util::global_state_lock();
743        let source: LinkList<i32> = LinkList::new();
744        let mut dest: LinkList<i32> = vec![1, 2, 3].into_iter().collect();
745        insertlinklist(&source, 1, &mut dest);
746        assert_eq!(
747            dest.to_vec(),
748            vec![1, 2, 3],
749            "c:193-194 — empty l returns early; dest unchanged"
750        );
751    }
752
753    /// `Src/linklist.c:188-206` — canonical zutil.c:1324 pattern:
754    /// `insertlinklist(sub.in, lastnode(result->in), result->in)` —
755    /// append entire source list at end of dest. The Rust port's
756    /// `lastnode_index()` is `len()-1`; passing that as `where_idx`
757    /// inserts after the last node, producing dest++source.
758    #[test]
759    fn insertlinklist_lastnode_append_pattern() {
760        let _g = crate::test_util::global_state_lock();
761        let source: LinkList<&str> = vec!["x", "y"].into_iter().collect();
762        let mut dest: LinkList<&str> = vec!["a", "b", "c"].into_iter().collect();
763        let last = dest.len() - 1;
764        insertlinklist(&source, last, &mut dest);
765        assert_eq!(
766            dest.to_vec(),
767            vec!["a", "b", "c", "x", "y"],
768            "c:188-206 zutil.c:1324 — lastnode anchor → tail-append"
769        );
770    }
771
772    // ═══════════════════════════════════════════════════════════════════
773    // Round-7: LinkList edge cases — pop on empty, countlinknodes,
774    // joinlists, linknodebydatum/string, ugetnode, hlinklist2array.
775    // ═══════════════════════════════════════════════════════════════════
776
777    /// `pop_front` / `pop_back` on empty list → None (no panic).
778    #[test]
779    fn linklist_pop_on_empty_returns_none() {
780        let _g = crate::test_util::global_state_lock();
781        let mut list: LinkList<i32> = LinkList::new();
782        assert_eq!(list.pop_front(), None);
783        assert_eq!(list.pop_back(), None);
784    }
785
786    /// `front` / `back` on empty list → None.
787    #[test]
788    fn linklist_front_back_on_empty_return_none() {
789        let _g = crate::test_util::global_state_lock();
790        let list: LinkList<i32> = LinkList::new();
791        assert!(list.front().is_none());
792        assert!(list.back().is_none());
793    }
794
795    /// `countlinknodes` on empty → 0.
796    #[test]
797    fn countlinknodes_empty_returns_zero() {
798        let _g = crate::test_util::global_state_lock();
799        let list: LinkList<i32> = LinkList::new();
800        assert_eq!(countlinknodes(&list), 0);
801    }
802
803    /// `countlinknodes` matches `len()`.
804    #[test]
805    fn countlinknodes_matches_len_for_populated_list() {
806        let _g = crate::test_util::global_state_lock();
807        let mut list = LinkList::new();
808        for i in 0..7 {
809            list.push_back(i);
810        }
811        assert_eq!(countlinknodes(&list), 7);
812        assert_eq!(countlinknodes(&list), list.len());
813    }
814
815    /// `joinlists`: second appended to first; second empties.
816    #[test]
817    fn joinlists_appends_second_to_first_and_empties_second() {
818        let _g = crate::test_util::global_state_lock();
819        let mut first: LinkList<i32> = LinkList::new();
820        first.push_back(1);
821        first.push_back(2);
822        let mut second: LinkList<i32> = LinkList::new();
823        second.push_back(3);
824        second.push_back(4);
825
826        joinlists(&mut first, &mut second);
827
828        assert_eq!(first.len(), 4, "first must absorb second's elements");
829        assert!(second.is_empty(), "second must be emptied after joinlists");
830    }
831
832    /// `joinlists` with empty second → first unchanged.
833    #[test]
834    fn joinlists_with_empty_second_leaves_first_unchanged() {
835        let _g = crate::test_util::global_state_lock();
836        let mut first: LinkList<i32> = LinkList::new();
837        first.push_back(1);
838        first.push_back(2);
839        let len_before = first.len();
840        let mut second: LinkList<i32> = LinkList::new();
841
842        joinlists(&mut first, &mut second);
843        assert_eq!(first.len(), len_before);
844    }
845
846    /// `linknodebydatum` finds the first matching element (Some(idx))
847    /// or None if absent.
848    #[test]
849    fn linknodebydatum_finds_existing_element() {
850        let _g = crate::test_util::global_state_lock();
851        let mut list = LinkList::new();
852        list.push_back(10);
853        list.push_back(20);
854        list.push_back(30);
855        assert!(linknodebydatum(&list, &20).is_some());
856        assert!(linknodebydatum(&list, &99).is_none());
857    }
858
859    /// `linknodebystring` — same as datum but for &str.
860    #[test]
861    fn linknodebystring_finds_existing_string() {
862        let _g = crate::test_util::global_state_lock();
863        let mut list: LinkList<String> = LinkList::new();
864        list.push_back("alpha".into());
865        list.push_back("beta".into());
866        list.push_back("gamma".into());
867        assert!(linknodebystring(&list, "beta").is_some());
868        assert!(linknodebystring(&list, "delta").is_none());
869    }
870
871    /// `hlinklist2array` converts to Vec<String> preserving order.
872    #[test]
873    fn hlinklist2array_preserves_insertion_order() {
874        let _g = crate::test_util::global_state_lock();
875        let mut list: LinkList<String> = LinkList::new();
876        list.push_back("x".into());
877        list.push_back("y".into());
878        list.push_back("z".into());
879        assert_eq!(
880            hlinklist2array(&list),
881            vec!["x".to_string(), "y".into(), "z".into()]
882        );
883    }
884
885    /// `hlinklist2array` on empty list returns empty Vec.
886    #[test]
887    fn hlinklist2array_on_empty_returns_empty_vec() {
888        let _g = crate::test_util::global_state_lock();
889        let list: LinkList<String> = LinkList::new();
890        let v = hlinklist2array(&list);
891        assert!(v.is_empty());
892    }
893
894    /// `freelinklist` clears the list to length 0.
895    #[test]
896    fn freelinklist_empties_the_list() {
897        let _g = crate::test_util::global_state_lock();
898        let mut list = LinkList::new();
899        for i in 0..5 {
900            list.push_back(i);
901        }
902        assert_eq!(list.len(), 5);
903        freelinklist(&mut list);
904        assert!(list.is_empty());
905    }
906
907    /// `getlinknode` removes and returns the first element (head pop).
908    #[test]
909    fn getlinknode_pops_head_returning_value() {
910        let _g = crate::test_util::global_state_lock();
911        let mut list = LinkList::new();
912        list.push_back(1);
913        list.push_back(2);
914        list.push_back(3);
915        assert_eq!(getlinknode(&mut list), Some(1));
916        assert_eq!(list.len(), 2, "head removed → len down by 1");
917        assert_eq!(getlinknode(&mut list), Some(2));
918        assert_eq!(getlinknode(&mut list), Some(3));
919        assert_eq!(getlinknode(&mut list), None, "empty → None");
920    }
921
922    // ─── zsh-corpus pins ────────────────────────────────────────────
923
924    /// `newlinklist()` returns empty list with countlinknodes = 0.
925    #[test]
926    fn linklist_corpus_new_is_empty() {
927        let l = newlinklist();
928        assert_eq!(countlinknodes(&l), 0);
929        assert!(l.is_empty());
930    }
931
932    /// Push many items, count matches.
933    #[test]
934    fn linklist_corpus_count_after_many_pushes() {
935        let mut l = LinkList::<i32>::new();
936        for i in 0..50 {
937            l.push_back(i);
938        }
939        assert_eq!(countlinknodes(&l), 50);
940        assert_eq!(l.len(), 50);
941    }
942
943    /// `joinlists` concatenates: first grows, second empties.
944    #[test]
945    fn linklist_corpus_joinlists_concatenates() {
946        let mut a = LinkList::<i32>::new();
947        let mut b = LinkList::<i32>::new();
948        a.push_back(1);
949        a.push_back(2);
950        b.push_back(3);
951        b.push_back(4);
952        joinlists(&mut a, &mut b);
953        assert_eq!(a.len(), 4, "first holds union");
954        assert!(b.is_empty(), "second emptied");
955    }
956
957    /// `getlinknode` on empty returns None.
958    #[test]
959    fn linklist_corpus_getlinknode_empty_returns_none() {
960        let mut l = LinkList::<i32>::new();
961        assert_eq!(getlinknode(&mut l), None);
962    }
963
964    /// `getlinknode` empties list element by element until None.
965    #[test]
966    fn linklist_corpus_getlinknode_drains_in_fifo_order() {
967        let mut l = LinkList::<&'static str>::new();
968        l.push_back("a");
969        l.push_back("b");
970        l.push_back("c");
971        assert_eq!(getlinknode(&mut l), Some("a"));
972        assert_eq!(getlinknode(&mut l), Some("b"));
973        assert_eq!(getlinknode(&mut l), Some("c"));
974        assert_eq!(getlinknode(&mut l), None);
975        assert!(l.is_empty());
976    }
977
978    /// `linknodebydatum` finds element by value.
979    #[test]
980    fn linklist_corpus_linknodebydatum_finds_value() {
981        let mut l = LinkList::<i32>::new();
982        l.push_back(10);
983        l.push_back(20);
984        l.push_back(30);
985        assert_eq!(linknodebydatum(&l, &20), Some(1));
986        assert_eq!(linknodebydatum(&l, &99), None);
987    }
988
989    // ═══════════════════════════════════════════════════════════════════
990    // Additional C-parity tests for Src/linklist.c.
991    // ═══════════════════════════════════════════════════════════════════
992
993    /// c:30 — `newlinklist()` returns empty list.
994    #[test]
995    fn newlinklist_returns_empty_pin() {
996        let l = newlinklist();
997        assert!(l.is_empty());
998        assert_eq!(l.len(), 0);
999    }
1000
1001    /// c:285 — `znewlinklist()` returns empty (permanent alloc).
1002    #[test]
1003    fn znewlinklist_returns_empty_pin() {
1004        let l = znewlinklist();
1005        assert!(l.is_empty());
1006    }
1007
1008    /// c:375 — `countlinknodes` returns 0 on empty list.
1009    #[test]
1010    fn countlinknodes_empty_is_zero_pin() {
1011        let l = LinkList::<i32>::new();
1012        assert_eq!(countlinknodes(&l), 0);
1013    }
1014
1015    /// c:375 — `countlinknodes` matches len on populated list.
1016    #[test]
1017    fn countlinknodes_matches_length_pin() {
1018        let mut l = LinkList::<i32>::new();
1019        l.push_back(1);
1020        l.push_back(2);
1021        l.push_back(3);
1022        assert_eq!(countlinknodes(&l), 3);
1023    }
1024
1025    /// c:340 — `getlinknode` empty → None.
1026    #[test]
1027    fn getlinknode_empty_returns_none_pin() {
1028        let mut l = LinkList::<i32>::new();
1029        assert!(getlinknode(&mut l).is_none());
1030    }
1031
1032    /// c:347 — `ugetnode` empty → None.
1033    #[test]
1034    fn ugetnode_empty_returns_none_pin() {
1035        let mut l = LinkList::<i32>::new();
1036        assert!(ugetnode(&mut l).is_none());
1037    }
1038
1039    /// c:368 — `freelinklist` clears list.
1040    #[test]
1041    fn freelinklist_clears_list_pin() {
1042        let mut l = LinkList::<i32>::new();
1043        l.push_back(1);
1044        l.push_back(2);
1045        freelinklist(&mut l);
1046        assert!(l.is_empty());
1047    }
1048
1049    /// c:429 — `linknodebystring` finds string.
1050    #[test]
1051    fn linknodebystring_finds_string_pin() {
1052        let mut l = LinkList::<String>::new();
1053        l.push_back("alpha".to_string());
1054        l.push_back("beta".to_string());
1055        l.push_back("gamma".to_string());
1056        assert_eq!(linknodebystring(&l, "beta"), Some(1));
1057        assert_eq!(linknodebystring(&l, "delta"), None);
1058    }
1059
1060    /// c:429 — finds first match on duplicates.
1061    #[test]
1062    fn linknodebystring_first_match_pin() {
1063        let mut l = LinkList::<String>::new();
1064        l.push_back("x".to_string());
1065        l.push_back("x".to_string());
1066        assert_eq!(linknodebystring(&l, "x"), Some(0));
1067    }
1068
1069    /// c:436 — `hlinklist2array` preserves order.
1070    #[test]
1071    fn hlinklist2array_preserves_order_pin() {
1072        let mut l = LinkList::<String>::new();
1073        l.push_back("a".to_string());
1074        l.push_back("b".to_string());
1075        l.push_back("c".to_string());
1076        let v = hlinklist2array(&l);
1077        assert_eq!(v, vec!["a", "b", "c"]);
1078    }
1079
1080    /// c:436 — empty list → empty Vec.
1081    #[test]
1082    fn hlinklist2array_empty_returns_empty_vec_pin() {
1083        let l = LinkList::<String>::new();
1084        let v = hlinklist2array(&l);
1085        assert!(v.is_empty());
1086    }
1087
1088    /// c:406 — `newsizedlist(0)` returns empty list.
1089    #[test]
1090    fn newsizedlist_zero_returns_empty_pin() {
1091        let l: LinkList<i32> = newsizedlist(0);
1092        assert!(l.is_empty());
1093    }
1094
1095    // ═══════════════════════════════════════════════════════════════════
1096    // Additional C-parity tests for Src/linklist.c
1097    // c:173 uinsertlinknode / c:317 rolllist / c:331 newsizedlist /
1098    // c:403 linknodebystring / c:423 hlinklist2array / c:449 zlinklist2array
1099    // ═══════════════════════════════════════════════════════════════════
1100
1101    /// c:173 — `uinsertlinknode` returns None when dup already in list.
1102    #[test]
1103    fn uinsertlinknode_dup_returns_none() {
1104        let mut l: LinkList<String> = LinkList::new();
1105        l.push_back("a".to_string());
1106        l.push_back("b".to_string());
1107        let r = uinsertlinknode(&mut l, 0, "a".to_string());
1108        assert_eq!(r, None, "dup must return None");
1109        assert_eq!(l.len(), 2, "list size unchanged");
1110    }
1111
1112    /// c:173 — `uinsertlinknode` returns Some(idx) when value is new.
1113    #[test]
1114    fn uinsertlinknode_new_returns_some() {
1115        let mut l: LinkList<String> = LinkList::new();
1116        l.push_back("a".to_string());
1117        let r = uinsertlinknode(&mut l, 0, "b".to_string());
1118        assert!(r.is_some(), "new value must return Some");
1119        assert_eq!(l.len(), 2, "list size incremented");
1120    }
1121
1122    /// c:317 — `rolllist` on empty list is safe (no panic, no-op).
1123    #[test]
1124    fn rolllist_empty_no_panic() {
1125        let mut l: LinkList<i32> = LinkList::new();
1126        rolllist(&mut l, 5);
1127        assert!(l.is_empty());
1128    }
1129
1130    /// c:317 — `rolllist` by length is identity (full cycle).
1131    #[test]
1132    fn rolllist_full_length_is_identity() {
1133        let mut l: LinkList<i32> = LinkList::new();
1134        for i in 1..=5 {
1135            l.push_back(i);
1136        }
1137        let before: Vec<i32> = l.iter().copied().collect();
1138        rolllist(&mut l, 5); // full rotation
1139        let after: Vec<i32> = l.iter().copied().collect();
1140        assert_eq!(before, after, "full rotation is identity");
1141    }
1142
1143    /// c:331 — `newsizedlist(N)` returns exactly N default-constructed nodes.
1144    #[test]
1145    fn newsizedlist_n_returns_n_default_nodes() {
1146        for n in [1usize, 3, 10, 100] {
1147            let l: LinkList<i32> = newsizedlist(n);
1148            assert_eq!(l.len(), n, "newsizedlist({}) must return {} nodes", n, n);
1149            for v in l.iter() {
1150                assert_eq!(*v, 0, "default i32 = 0");
1151            }
1152        }
1153    }
1154
1155    /// c:403 — `linknodebystring` returns None for non-existent string.
1156    #[test]
1157    fn linknodebystring_missing_returns_none_pin() {
1158        let mut l: LinkList<String> = LinkList::new();
1159        l.push_back("apple".to_string());
1160        l.push_back("banana".to_string());
1161        assert_eq!(linknodebystring(&l, "cherry"), None);
1162    }
1163
1164    /// c:403 — `linknodebystring` is case-sensitive.
1165    #[test]
1166    fn linknodebystring_case_sensitive() {
1167        let mut l: LinkList<String> = LinkList::new();
1168        l.push_back("APPLE".to_string());
1169        assert_eq!(
1170            linknodebystring(&l, "apple"),
1171            None,
1172            "case mismatch must miss"
1173        );
1174        assert!(linknodebystring(&l, "APPLE").is_some(), "exact case match");
1175    }
1176
1177    /// c:386 — `linknodebydatum` for absent value returns None.
1178    #[test]
1179    fn linknodebydatum_missing_returns_none_pin() {
1180        let mut l: LinkList<i32> = LinkList::new();
1181        for i in [1, 2, 3] {
1182            l.push_back(i);
1183        }
1184        assert_eq!(linknodebydatum(&l, &99), None);
1185    }
1186
1187    /// c:449 — `zlinklist2array` agrees with `hlinklist2array` on
1188    /// the same input (both produce identical Vec).
1189    #[test]
1190    fn zlinklist2array_matches_hlinklist2array() {
1191        let mut l: LinkList<String> = LinkList::new();
1192        for s in ["a", "b", "c", "d"] {
1193            l.push_back(s.to_string());
1194        }
1195        let h = hlinklist2array(&l);
1196        let z = zlinklist2array(&l);
1197        assert_eq!(h, z, "h and z variants must agree");
1198    }
1199
1200    /// c:386 + c:403 — both lookup fns are deterministic.
1201    #[test]
1202    fn lookup_fns_are_deterministic() {
1203        let mut l: LinkList<String> = LinkList::new();
1204        for s in ["a", "b", "c"] {
1205            l.push_back(s.to_string());
1206        }
1207        let a = linknodebystring(&l, "b");
1208        for _ in 0..5 {
1209            assert_eq!(linknodebystring(&l, "b"), a);
1210        }
1211        let mut ll: LinkList<i32> = LinkList::new();
1212        for i in [1, 2, 3] {
1213            ll.push_back(i);
1214        }
1215        let b = linknodebydatum(&ll, &2);
1216        for _ in 0..5 {
1217            assert_eq!(linknodebydatum(&ll, &2), b);
1218        }
1219    }
1220
1221    // ═══════════════════════════════════════════════════════════════════
1222    // Additional C-parity tests for Src/linklist.c
1223    // c:292 insertlinknode / c:340 getlinknode / c:354 remnode /
1224    // c:368 freelinklist / c:375 countlinknodes / c:382 rolllist /
1225    // c:406 newsizedlist / c:417 joinlists
1226    // ═══════════════════════════════════════════════════════════════════
1227
1228    /// c:340 — `getlinknode` returns Option<T> (compile-time type pin).
1229    #[test]
1230    fn getlinknode_returns_option_t_type() {
1231        let mut l: LinkList<i32> = LinkList::new();
1232        let _: Option<i32> = getlinknode(&mut l);
1233    }
1234
1235    /// c:340 — `getlinknode` on empty list returns None.
1236    #[test]
1237    fn getlinknode_empty_returns_none() {
1238        let mut l: LinkList<i32> = LinkList::new();
1239        assert!(getlinknode(&mut l).is_none());
1240    }
1241
1242    /// c:340 — `getlinknode` drains list to empty.
1243    #[test]
1244    fn getlinknode_drains_list_to_empty() {
1245        let mut l: LinkList<i32> = LinkList::new();
1246        for i in 1..=5 {
1247            l.push_back(i);
1248        }
1249        for _ in 0..5 {
1250            assert!(getlinknode(&mut l).is_some());
1251        }
1252        assert!(getlinknode(&mut l).is_none(), "after 5 pops → None");
1253        assert!(l.is_empty());
1254    }
1255
1256    /// c:368 — `freelinklist` empties the list.
1257    #[test]
1258    fn freelinklist_empties_list() {
1259        let mut l: LinkList<i32> = LinkList::new();
1260        for i in 1..=10 {
1261            l.push_back(i);
1262        }
1263        freelinklist(&mut l);
1264        assert!(l.is_empty(), "freelinklist empties");
1265    }
1266
1267    /// c:375 — `countlinknodes` returns usize.
1268    #[test]
1269    fn countlinknodes_returns_usize_type() {
1270        let l: LinkList<i32> = LinkList::new();
1271        let _: usize = countlinknodes(&l);
1272    }
1273
1274    /// c:375 — `countlinknodes(empty)` returns 0.
1275    #[test]
1276    fn countlinknodes_empty_returns_zero_pin() {
1277        let l: LinkList<i32> = LinkList::new();
1278        assert_eq!(countlinknodes(&l), 0);
1279    }
1280
1281    /// c:417 — `joinlists` is associative: (a+b)+c == a+(b+c) on len.
1282    #[test]
1283    fn joinlists_associative_on_length() {
1284        let make = |xs: &[i32]| -> LinkList<i32> {
1285            let mut l = LinkList::new();
1286            for &x in xs {
1287                l.push_back(x);
1288            }
1289            l
1290        };
1291        // (a + b) + c
1292        let mut a1 = make(&[1, 2]);
1293        let mut b1 = make(&[3, 4]);
1294        let mut c1 = make(&[5, 6]);
1295        joinlists(&mut a1, &mut b1);
1296        joinlists(&mut a1, &mut c1);
1297
1298        // a + (b + c)
1299        let mut a2 = make(&[1, 2]);
1300        let mut b2 = make(&[3, 4]);
1301        let mut c2 = make(&[5, 6]);
1302        joinlists(&mut b2, &mut c2);
1303        joinlists(&mut a2, &mut b2);
1304
1305        assert_eq!(
1306            a1.len(),
1307            a2.len(),
1308            "joinlists associative: (a+b)+c.len = a+(b+c).len"
1309        );
1310    }
1311
1312    /// c:382 — `rolllist` on 1-element list is no-op for any index.
1313    #[test]
1314    fn rolllist_single_element_is_noop() {
1315        let mut l: LinkList<i32> = LinkList::new();
1316        l.push_back(42);
1317        for n in [0usize, 1, 5, 100] {
1318            rolllist(&mut l, n);
1319            assert_eq!(l.len(), 1);
1320        }
1321    }
1322
1323    /// c:406 — `newsizedlist::<i32>(N)` returns Vec of N zeros.
1324    #[test]
1325    fn newsizedlist_i32_fills_with_default_zero() {
1326        for n in [1usize, 3, 10] {
1327            let l: LinkList<i32> = newsizedlist(n);
1328            assert_eq!(l.len(), n);
1329            for v in l.iter() {
1330                assert_eq!(*v, 0, "i32 default = 0");
1331            }
1332        }
1333    }
1334
1335    /// c:417 — `joinlists` with second containing one element appends.
1336    #[test]
1337    fn joinlists_single_elem_second_appends() {
1338        let mut a: LinkList<i32> = LinkList::new();
1339        a.push_back(1);
1340        a.push_back(2);
1341        let mut b: LinkList<i32> = LinkList::new();
1342        b.push_back(3);
1343        joinlists(&mut a, &mut b);
1344        assert_eq!(a.len(), 3);
1345        assert!(b.is_empty(), "second emptied after join");
1346    }
1347
1348    // ═══════════════════════════════════════════════════════════════════
1349    // Additional C-parity tests for Src/linklist.c
1350    // c:30 newlinklist / c:285 znewlinklist / c:375 countlinknodes /
1351    // c:417 joinlists / c:423 linknodebydatum / c:429 linknodebystring /
1352    // c:436 hlinklist2array / c:443 zlinklist2array
1353    // ═══════════════════════════════════════════════════════════════════
1354
1355    /// c:30 — `newlinklist` returns LinkList<String> (compile-time pin).
1356    #[test]
1357    fn newlinklist_returns_linklist_string_type() {
1358        let _: LinkList<String> = newlinklist();
1359    }
1360
1361    /// c:30 — `newlinklist` starts empty.
1362    #[test]
1363    fn newlinklist_starts_empty() {
1364        let l = newlinklist();
1365        assert!(l.is_empty(), "new list must be empty");
1366        assert_eq!(l.len(), 0, "new list len = 0");
1367    }
1368
1369    /// c:285 — `znewlinklist` starts empty (same contract as newlinklist).
1370    #[test]
1371    fn znewlinklist_starts_empty() {
1372        let l = znewlinklist();
1373        assert!(l.is_empty(), "znewlinklist must start empty");
1374    }
1375
1376    /// c:375 — `countlinknodes` returns usize (compile-time pin, alt).
1377    #[test]
1378    fn countlinknodes_returns_usize_pin_alt() {
1379        let l: LinkList<i32> = LinkList::new();
1380        let _: usize = countlinknodes(&l);
1381    }
1382
1383    /// c:375 — `countlinknodes` on empty list returns 0 (alt name).
1384    #[test]
1385    fn countlinknodes_empty_returns_zero_alt() {
1386        let l: LinkList<i32> = LinkList::new();
1387        assert_eq!(countlinknodes(&l), 0);
1388    }
1389
1390    /// c:375 — `countlinknodes` matches `LinkList::len`.
1391    #[test]
1392    fn countlinknodes_matches_list_len() {
1393        for n in [0usize, 1, 3, 10, 100] {
1394            let mut l: LinkList<i32> = LinkList::new();
1395            for i in 0..n {
1396                l.push_back(i as i32);
1397            }
1398            assert_eq!(
1399                countlinknodes(&l),
1400                l.len(),
1401                "countlinknodes must equal list.len() for n={}",
1402                n
1403            );
1404        }
1405    }
1406
1407    /// c:417 — `joinlists` empty into non-empty leaves first unchanged.
1408    #[test]
1409    fn joinlists_empty_into_nonempty_unchanged() {
1410        let mut a: LinkList<i32> = LinkList::new();
1411        a.push_back(1);
1412        a.push_back(2);
1413        let mut b: LinkList<i32> = LinkList::new();
1414        let before_len = a.len();
1415        joinlists(&mut a, &mut b);
1416        assert_eq!(a.len(), before_len, "empty append leaves a unchanged");
1417    }
1418
1419    /// c:417 — `joinlists` both-empty is a clean no-op.
1420    #[test]
1421    fn joinlists_both_empty_no_op() {
1422        let mut a: LinkList<i32> = LinkList::new();
1423        let mut b: LinkList<i32> = LinkList::new();
1424        joinlists(&mut a, &mut b);
1425        assert!(a.is_empty() && b.is_empty(), "both still empty");
1426    }
1427
1428    /// c:423 — `linknodebydatum` for missing datum returns None.
1429    #[test]
1430    fn linknodebydatum_missing_returns_none() {
1431        let mut l: LinkList<i32> = LinkList::new();
1432        l.push_back(1);
1433        l.push_back(2);
1434        assert!(linknodebydatum(&l, &999).is_none(), "missing datum → None");
1435    }
1436
1437    /// c:429 — `linknodebystring("")` empty needle is deterministic.
1438    #[test]
1439    fn linknodebystring_empty_needle_is_deterministic() {
1440        let mut l: LinkList<String> = LinkList::new();
1441        l.push_back("foo".to_string());
1442        let a = linknodebystring(&l, "");
1443        let b = linknodebystring(&l, "");
1444        assert_eq!(
1445            a.is_some(),
1446            b.is_some(),
1447            "linknodebystring('') must be deterministic"
1448        );
1449    }
1450
1451    /// c:436 — `hlinklist2array` returns Vec<String> (compile-time pin).
1452    #[test]
1453    fn hlinklist2array_returns_vec_string_type() {
1454        let l: LinkList<String> = LinkList::new();
1455        let _: Vec<String> = hlinklist2array(&l);
1456    }
1457
1458    /// c:436 — `hlinklist2array` empty list returns empty Vec.
1459    #[test]
1460    fn hlinklist2array_empty_returns_empty_vec() {
1461        let l: LinkList<String> = LinkList::new();
1462        assert!(hlinklist2array(&l).is_empty());
1463    }
1464
1465    /// c:443 — `zlinklist2array` length preserved across the conversion.
1466    #[test]
1467    fn zlinklist2array_length_preserved() {
1468        let mut l: LinkList<String> = LinkList::new();
1469        for i in 0..7 {
1470            l.push_back(format!("item_{}", i));
1471        }
1472        let arr = zlinklist2array(&l);
1473        assert_eq!(arr.len(), 7, "array length = list length");
1474    }
1475}