Skip to main content

zsh/extensions/
pat_cache.rs

1//! Global compiled-pattern cache — a zshrs-only optimization with NO C
2//! counterpart, so it lives here in `src/extensions` rather than in
3//! `src/ported`.
4//!
5//! Why it exists: C zsh recompiles a pattern on every match too, but its C
6//! `patcompile` is ~40-75x faster than this Rust port. Shell code re-matches
7//! identical patterns constantly — `[[ x == pat ]]`, `${(M)a:#pat}`, `case`,
8//! `${x//pat/…}` — and p10k's `_p9k_param` re-matches the SAME
9//! `(#b)prompt_([a-z0-9_]#)(*)` thousands of times per precmd. At the port's
10//! compile cost that dominated interactive startup (the "hang before prompt").
11//! Caching the compiled program turns those repeats into a hash lookup + a
12//! cheap clone of the bytecode, and lets zshrs actually beat zsh here.
13//!
14//! Safety: the compiled `Patprog` is self-contained — matching (`pattry*`)
15//! reads `prog.0.flags` / `prog.0.globflags` (never the global compile
16//! statics) and borrows the prog by `&` (read-only). So a cached clone is
17//! interchangeable with a freshly-compiled one.
18//!
19//! Correctness of the key: the compiled output depends on the pattern text,
20//! `inflags`, and exactly six options — EXTENDEDGLOB/KSHGLOB/SHGLOB (which
21//! shape `zpc_special` in `patcompcharsset`) and CASEGLOB/CASEPATHS/MULTIBYTE
22//! (which shape `patglobflags` in `patcompstart`). All are folded into the
23//! key, so any option change yields a distinct key rather than a stale hit.
24//!
25//! GLOBAL (not thread-local): the VM dispatches `[[ … ]]`/pattern work across
26//! zshrs's worker threads, so a per-thread cache would miss on every worker.
27//! A single `RwLock<HashMap>` is shared: reads (the common case, a hit) take
28//! the read lock and run concurrently; only a miss's store takes the write
29//! lock. The check runs BEFORE `patcompile`'s own compile mutex, so hits never
30//! serialise on the compiler.
31
32use crate::ported::pattern::Patprog;
33use crate::ported::zsh_h::{
34    isset, CASEGLOB, CASEPATHS, EXTENDEDGLOB, KSHGLOB, MULTIBYTE, SHGLOB,
35};
36use std::cell::RefCell;
37use std::collections::HashMap;
38use std::sync::{LazyLock, RwLock};
39
40/// (pattern text, inflags, option fingerprint).
41type Key = (String, i32, u8);
42
43// Two-level cache. L1 is thread-local and LOCK-FREE — the common hit takes
44// no lock, so a hot loop (`repeat 20000 { [[ x == pat ]] }`) doesn't pay
45// RwLock read-contention across zshrs's worker threads (which otherwise
46// dominated the profile). L2 is the shared global that lets a pattern
47// compiled on one worker be reused by another; an L2 hit is promoted into
48// the reader's L1 so subsequent hits stay lock-free.
49thread_local! {
50    static L1: RefCell<HashMap<Key, Patprog>> = RefCell::new(HashMap::new());
51}
52
53static L2: LazyLock<RwLock<HashMap<Key, Patprog>>> =
54    LazyLock::new(|| RwLock::new(HashMap::new()));
55
56/// Bound the cache so a pathological workload (unique patterns forever)
57/// can't grow it without limit. On overflow we clear wholesale rather than
58/// track LRU — pattern working sets are small and stable (a shell reuses a
59/// handful of patterns), so a rare full flush costs one recompile burst,
60/// not a steady eviction tax.
61const MAX_ENTRIES: usize = 8192;
62
63/// Fold the six compilation-affecting options into a fingerprint byte.
64#[inline]
65fn opt_fingerprint() -> u8 {
66    (isset(EXTENDEDGLOB) as u8)
67        | ((isset(KSHGLOB) as u8) << 1)
68        | ((isset(SHGLOB) as u8) << 2)
69        | ((isset(CASEGLOB) as u8) << 3)
70        | ((isset(CASEPATHS) as u8) << 4)
71        | ((isset(MULTIBYTE) as u8) << 5)
72}
73
74/// Look up a compiled pattern. Returns a clone of the cached `Patprog` on a
75/// hit (interchangeable with a fresh compile), `None` on a miss. Checks the
76/// lock-free thread-local L1 first, then the shared L2 (promoting an L2 hit
77/// into L1).
78pub fn get(exp: &str, inflags: i32) -> Option<Patprog> {
79    let key = (exp.to_string(), inflags, opt_fingerprint());
80    if let Some(p) = L1.with(|c| c.borrow().get(&key).cloned()) {
81        return Some(p);
82    }
83    let hit = L2.read().ok()?.get(&key).cloned();
84    if let Some(ref p) = hit {
85        promote_l1(key, p);
86    }
87    hit
88}
89
90/// Store a freshly-compiled pattern in both cache levels.
91pub fn put(exp: &str, inflags: i32, prog: &Patprog) {
92    let key = (exp.to_string(), inflags, opt_fingerprint());
93    if let Ok(mut m) = L2.write() {
94        if m.len() >= MAX_ENTRIES {
95            m.clear();
96        }
97        m.insert(key.clone(), prog.clone());
98    }
99    promote_l1(key, prog);
100}
101
102/// Insert into the thread-local L1 (bounded, wholesale-flush on overflow).
103#[inline]
104fn promote_l1(key: Key, prog: &Patprog) {
105    L1.with(|c| {
106        let mut m = c.borrow_mut();
107        if m.len() >= MAX_ENTRIES {
108            m.clear();
109        }
110        m.insert(key, prog.clone());
111    });
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    /// A stored pattern is retrievable, and a distinct option state (encoded
119    /// in the fingerprint) does not return the wrong cached program.
120    #[test]
121    fn get_after_put_roundtrips_and_key_is_option_sensitive() {
122        // Compile a trivial pattern and cache it.
123        let prog = crate::ported::pattern::patcompile("abc", 0, None)
124            .expect("compile abc");
125        put("zzz_test_pat_abc", 0, &prog);
126        assert!(
127            get("zzz_test_pat_abc", 0).is_some(),
128            "just-put pattern must be retrievable"
129        );
130        // A never-stored pattern misses.
131        assert!(
132            get("zzz_test_pat_never_stored_xyz", 0).is_none(),
133            "unseen pattern must miss"
134        );
135    }
136}