Skip to main content

zsh/extensions/
plugin_cache.rs

1//! Plugin source cache — stores side effects of `source`/`.` in
2//! SQLite.
3//!
4//! **zshrs-original infrastructure with strong C-zsh ancestry.** C
5//! zsh has `bin_zcompile()` (Src/parse.c) which writes a parsed
6//! AST to a `.zwc` file alongside the source so subsequent reads
7//! skip parsing. zshrs takes the idea further: rather than caching
8//! the AST and still re-running it, we capture the *side effects*
9//! (params/aliases/options/funcs set) and replay those directly —
10//! microseconds instead of milliseconds for plugin startup. The
11//! key/invalidation model (canonical-path + mtime) matches the
12//! `.zwc` invalidation scheme C zsh uses in `try_source_file()`
13//! (Src/init.c:1551).
14//!
15//! First source: execute normally, capture state delta, write
16//! cache on worker thread.
17//! Subsequent sources: check mtime, replay cached side effects in
18//! microseconds.
19//!
20//! Cache key: `(canonical_path, mtime_secs, mtime_nsecs)`.
21//! Cache invalidation: mtime mismatch → re-source, update cache.
22
23use rusqlite::{params, Connection};
24use std::collections::HashMap;
25use std::env;
26use std::path::{Path, PathBuf};
27use std::sync::OnceLock;
28use std::os::unix::fs::MetadataExt;
29use crate::ported::zsh_h::PM_UNDEFINED;
30use crate::ported::utils::{errflag, ERRFLAG_ERROR};
31use std::sync::atomic::Ordering;
32#[allow(unused_imports)]
33use crate::ported::exec::ShellExecutor;
34
35/// State snapshot for plugin delta computation.
36pub(crate) struct PluginSnapshot {
37    pub(crate) functions: std::collections::HashSet<String>,
38    pub(crate) aliases: std::collections::HashSet<String>,
39    pub(crate) global_aliases: std::collections::HashSet<String>,
40    pub(crate) suffix_aliases: std::collections::HashSet<String>,
41    pub(crate) variables: HashMap<String, String>,
42    pub(crate) arrays: std::collections::HashSet<String>,
43    pub(crate) assoc_arrays: std::collections::HashSet<String>,
44    pub(crate) fpath: Vec<PathBuf>,
45    pub(crate) options: HashMap<String, bool>,
46    pub(crate) hooks: HashMap<String, Vec<String>>,
47    pub(crate) autoloads: std::collections::HashSet<String>,
48}
49
50/// Mtime (seconds since epoch) of the running zshrs binary. Same
51/// helper as `script_cache::current_binary_mtime_secs` — we duplicate
52/// it here so plugin_cache doesn't need to take a script_cache dep
53/// and so the OnceLock is per-cache (the value is identical anyway
54/// since it's process-global). Returns None if the executable's
55/// metadata can't be read (extremely rare — usually only if the
56/// binary was deleted out from under us mid-run).
57fn current_binary_mtime() -> Option<i64> {
58    static BIN_MTIME: OnceLock<Option<i64>> = OnceLock::new();
59    *BIN_MTIME.get_or_init(|| {
60        let exe = std::env::current_exe().ok()?;
61        let meta = std::fs::metadata(&exe).ok()?;
62        Some(meta.mtime())
63    })
64}
65
66// Script bytecode caching used to live here behind the BYTECODE_VERSION
67// prefix + script_bytecode SQLite table. It now lives in the rkyv shard at
68// ~/.zshrs/scripts.rkyv (see `crate::script_cache`). The header in
69// that shard carries its own version pin (`zshrs_version`) so this prefix
70// byte is no longer needed — a zshrs rebuild silently invalidates all
71// cached entries via `binary_mtime_at_cache`.
72
73/// Side effects captured from sourcing a plugin file.
74#[derive(Debug, Clone, Default)]
75pub struct PluginDelta {
76    pub functions: Vec<(String, Vec<u8>)>, // name → bincode-serialized bytecode
77    pub aliases: Vec<(String, String, AliasKind)>, // name → value, kind
78    pub global_aliases: Vec<(String, String)>,
79    pub suffix_aliases: Vec<(String, String)>,
80    pub variables: Vec<(String, String)>,
81    pub exports: Vec<(String, String)>, // also set in env
82    pub arrays: Vec<(String, Vec<String>)>,
83    pub assoc_arrays: Vec<(String, HashMap<String, String>)>,
84    pub completions: Vec<(String, String)>, // command → function
85    pub fpath_additions: Vec<String>,
86    pub hooks: Vec<(String, String)>, // hook_name → function
87    pub bindkeys: Vec<(String, String, String)>, // keyseq, widget, keymap
88    pub zstyles: Vec<(String, String, String)>, // pattern, style, value
89    pub options_changed: Vec<(String, bool)>, // option → on/off
90    pub autoloads: Vec<(String, String)>, // function → flags
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum AliasKind {
95    Regular,
96    Global,
97    Suffix,
98}
99
100impl AliasKind {
101    fn as_i32(self) -> i32 {
102        match self {
103            AliasKind::Regular => 0,
104            AliasKind::Global => 1,
105            AliasKind::Suffix => 2,
106        }
107    }
108    fn from_i32(v: i32) -> Self {
109        match v {
110            1 => AliasKind::Global,
111            2 => AliasKind::Suffix,
112            _ => AliasKind::Regular,
113        }
114    }
115}
116
117/// SQLite-backed plugin cache.
118pub struct PluginCache {
119    conn: Connection,
120}
121
122impl PluginCache {
123    pub fn open(path: &Path) -> rusqlite::Result<Self> {
124        let conn = Connection::open(path)?;
125        conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
126        let cache = Self { conn };
127        cache.init_schema()?;
128        Ok(cache)
129    }
130
131    fn init_schema(&self) -> rusqlite::Result<()> {
132        self.conn.execute_batch(
133            r#"
134            CREATE TABLE IF NOT EXISTS plugins (
135                id INTEGER PRIMARY KEY,
136                path TEXT NOT NULL UNIQUE,
137                mtime_secs INTEGER NOT NULL,
138                mtime_nsecs INTEGER NOT NULL,
139                source_time_ms INTEGER NOT NULL,
140                cached_at INTEGER NOT NULL,
141                binary_mtime INTEGER NOT NULL DEFAULT 0
142            );
143
144            CREATE TABLE IF NOT EXISTS plugin_functions (
145                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
146                name TEXT NOT NULL,
147                body BLOB NOT NULL
148            );
149
150            CREATE TABLE IF NOT EXISTS plugin_aliases (
151                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
152                name TEXT NOT NULL,
153                value TEXT NOT NULL,
154                kind INTEGER NOT NULL DEFAULT 0
155            );
156
157            CREATE TABLE IF NOT EXISTS plugin_variables (
158                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
159                name TEXT NOT NULL,
160                value TEXT NOT NULL,
161                is_export INTEGER NOT NULL DEFAULT 0
162            );
163
164            CREATE TABLE IF NOT EXISTS plugin_arrays (
165                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
166                name TEXT NOT NULL,
167                value_json TEXT NOT NULL
168            );
169
170            -- Associative-array deltas (e.g. ZINIT[BIN_DIR]=...). Stored
171            -- as JSON {key: value} so insertion order isn't load-bearing
172            -- (matches HashMap semantics on the Rust side). Direct
173            -- analogue of plugin_arrays for assoc shape.
174            CREATE TABLE IF NOT EXISTS plugin_assoc_arrays (
175                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
176                name TEXT NOT NULL,
177                value_json TEXT NOT NULL
178            );
179
180            CREATE TABLE IF NOT EXISTS plugin_completions (
181                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
182                command TEXT NOT NULL,
183                function TEXT NOT NULL
184            );
185
186            CREATE TABLE IF NOT EXISTS plugin_fpath (
187                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
188                path TEXT NOT NULL
189            );
190
191            CREATE TABLE IF NOT EXISTS plugin_hooks (
192                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
193                hook TEXT NOT NULL,
194                function TEXT NOT NULL
195            );
196
197            CREATE TABLE IF NOT EXISTS plugin_bindkeys (
198                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
199                keyseq TEXT NOT NULL,
200                widget TEXT NOT NULL,
201                keymap TEXT NOT NULL DEFAULT 'main'
202            );
203
204            CREATE TABLE IF NOT EXISTS plugin_zstyles (
205                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
206                pattern TEXT NOT NULL,
207                style TEXT NOT NULL,
208                value TEXT NOT NULL
209            );
210
211            CREATE TABLE IF NOT EXISTS plugin_options (
212                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
213                name TEXT NOT NULL,
214                enabled INTEGER NOT NULL
215            );
216
217            CREATE TABLE IF NOT EXISTS plugin_autoloads (
218                plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
219                function TEXT NOT NULL,
220                flags TEXT NOT NULL DEFAULT ''
221            );
222
223            -- compaudit cache: security audit results per fpath directory
224            CREATE TABLE IF NOT EXISTS compaudit_cache (
225                id INTEGER PRIMARY KEY,
226                path TEXT NOT NULL UNIQUE,
227                mtime_secs INTEGER NOT NULL,
228                mtime_nsecs INTEGER NOT NULL,
229                uid INTEGER NOT NULL,
230                mode INTEGER NOT NULL,
231                is_secure INTEGER NOT NULL,
232                checked_at INTEGER NOT NULL
233            );
234
235            CREATE INDEX IF NOT EXISTS idx_plugins_path ON plugins(path);
236            CREATE INDEX IF NOT EXISTS idx_compaudit_path ON compaudit_cache(path);
237
238            -- Migration: legacy script_bytecode table (bytecode now lives in
239            -- the rkyv shard at ~/.zshrs/scripts.rkyv). Drop on open so
240            -- existing DBs reclaim the space and don't carry stale bytecode.
241            DROP INDEX IF EXISTS idx_script_bytecode_path;
242            DROP TABLE IF EXISTS script_bytecode;
243        "#,
244        )?;
245        // Migrate pre-binary_mtime DBs (column added 2026-05): the
246        // CREATE-IF-NOT-EXISTS above only adds the column for fresh
247        // dbs. ALTER TABLE on an existing db is a one-time no-op
248        // wrapped in an ignored-if-already-applied check. Mirrors the
249        // C analogue of zsh's $ZSH_VERSION-keyed compdump rebuild —
250        // any binary change invalidates the plugin replay shard so
251        // we don't replay deltas captured under the old runtime
252        // semantics. Without this, fixes to paramsubst / option
253        // handling don't take effect until the user manually
254        // `rm ~/.zshrs/plugins.db`.
255        let _ = self
256            .conn
257            .execute("ALTER TABLE plugins ADD COLUMN binary_mtime INTEGER NOT NULL DEFAULT 0", []);
258        Ok(())
259    }
260
261    /// Check if a cached entry exists with matching mtime AND the
262    /// running zshrs binary's mtime is no newer than when the entry
263    /// was cached. Direct port of script_cache.rs's invalidation
264    /// logic (lines 188-194): any zshrs rebuild silently invalidates
265    /// plugin-cached deltas because runtime semantics may have
266    /// shifted (paramsubst flags, option aliases, builtin
267    /// resolution, …). Without this guard, a new build reads stale
268    /// deltas and replays them with the new engine — visible
269    /// regression where `zinit.zsh`'s `${ZINIT[BIN_DIR]}` returned
270    /// empty after re-source until the cache was manually cleared.
271    pub fn check(&self, path: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<i64> {
272        let row: Option<(i64, i64)> = self
273            .conn
274            .query_row(
275                "SELECT id, binary_mtime FROM plugins WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
276                params![path, mtime_secs, mtime_nsecs],
277                |row| Ok((row.get(0)?, row.get(1)?)),
278            )
279            .ok();
280        let (id, cached_bin_mtime) = row?;
281        if let Some(bin_mtime) = current_binary_mtime() {
282            if cached_bin_mtime < bin_mtime {
283                return None;
284            }
285        }
286        Some(id)
287    }
288
289    /// Load cached delta for a plugin by id.
290    pub fn load(&self, plugin_id: i64) -> rusqlite::Result<PluginDelta> {
291        let mut delta = PluginDelta::default();
292
293        // Functions (bincode-serialized AST blobs)
294        let mut stmt = self
295            .conn
296            .prepare("SELECT name, body FROM plugin_functions WHERE plugin_id = ?1")?;
297        let rows = stmt.query_map(params![plugin_id], |row| {
298            Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
299        })?;
300        for r in rows {
301            delta.functions.push(r?);
302        }
303
304        // Aliases
305        let mut stmt = self
306            .conn
307            .prepare("SELECT name, value, kind FROM plugin_aliases WHERE plugin_id = ?1")?;
308        let rows = stmt.query_map(params![plugin_id], |row| {
309            Ok((
310                row.get::<_, String>(0)?,
311                row.get::<_, String>(1)?,
312                AliasKind::from_i32(row.get::<_, i32>(2)?),
313            ))
314        })?;
315        for r in rows {
316            delta.aliases.push(r?);
317        }
318
319        // Variables
320        let mut stmt = self
321            .conn
322            .prepare("SELECT name, value, is_export FROM plugin_variables WHERE plugin_id = ?1")?;
323        let rows = stmt.query_map(params![plugin_id], |row| {
324            Ok((
325                row.get::<_, String>(0)?,
326                row.get::<_, String>(1)?,
327                row.get::<_, bool>(2)?,
328            ))
329        })?;
330        for r in rows {
331            let (name, value, is_export) = r?;
332            if is_export {
333                delta.exports.push((name, value));
334            } else {
335                delta.variables.push((name, value));
336            }
337        }
338
339        // Arrays
340        let mut stmt = self
341            .conn
342            .prepare("SELECT name, value_json FROM plugin_arrays WHERE plugin_id = ?1")?;
343        let rows = stmt.query_map(params![plugin_id], |row| {
344            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
345        })?;
346        for r in rows {
347            let (name, json) = r?;
348            // Simple JSON array: ["a","b","c"]
349            let vals: Vec<String> = json
350                .trim_matches(|c| c == '[' || c == ']')
351                .split(',')
352                .map(|s| s.trim().trim_matches('"').to_string())
353                .filter(|s| !s.is_empty())
354                .collect();
355            delta.arrays.push((name, vals));
356        }
357
358        // Associative arrays (key→value JSON object). Falls back to
359        // an empty map on parse failure rather than a load error so
360        // a malformed row doesn't break the whole replay path.
361        let mut stmt = self
362            .conn
363            .prepare("SELECT name, value_json FROM plugin_assoc_arrays WHERE plugin_id = ?1")?;
364        let rows = stmt.query_map(params![plugin_id], |row| {
365            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
366        })?;
367        for r in rows {
368            let (name, json) = r?;
369            let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or_default();
370            delta.assoc_arrays.push((name, map));
371        }
372
373        // Completions
374        let mut stmt = self
375            .conn
376            .prepare("SELECT command, function FROM plugin_completions WHERE plugin_id = ?1")?;
377        let rows = stmt.query_map(params![plugin_id], |row| {
378            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
379        })?;
380        for r in rows {
381            delta.completions.push(r?);
382        }
383
384        // Fpath
385        let mut stmt = self
386            .conn
387            .prepare("SELECT path FROM plugin_fpath WHERE plugin_id = ?1")?;
388        let rows = stmt.query_map(params![plugin_id], |row| row.get::<_, String>(0))?;
389        for r in rows {
390            delta.fpath_additions.push(r?);
391        }
392
393        // Hooks
394        let mut stmt = self
395            .conn
396            .prepare("SELECT hook, function FROM plugin_hooks WHERE plugin_id = ?1")?;
397        let rows = stmt.query_map(params![plugin_id], |row| {
398            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
399        })?;
400        for r in rows {
401            delta.hooks.push(r?);
402        }
403
404        // Bindkeys
405        let mut stmt = self
406            .conn
407            .prepare("SELECT keyseq, widget, keymap FROM plugin_bindkeys WHERE plugin_id = ?1")?;
408        let rows = stmt.query_map(params![plugin_id], |row| {
409            Ok((
410                row.get::<_, String>(0)?,
411                row.get::<_, String>(1)?,
412                row.get::<_, String>(2)?,
413            ))
414        })?;
415        for r in rows {
416            delta.bindkeys.push(r?);
417        }
418
419        // Zstyles
420        let mut stmt = self
421            .conn
422            .prepare("SELECT pattern, style, value FROM plugin_zstyles WHERE plugin_id = ?1")?;
423        let rows = stmt.query_map(params![plugin_id], |row| {
424            Ok((
425                row.get::<_, String>(0)?,
426                row.get::<_, String>(1)?,
427                row.get::<_, String>(2)?,
428            ))
429        })?;
430        for r in rows {
431            delta.zstyles.push(r?);
432        }
433
434        // Options
435        let mut stmt = self
436            .conn
437            .prepare("SELECT name, enabled FROM plugin_options WHERE plugin_id = ?1")?;
438        let rows = stmt.query_map(params![plugin_id], |row| {
439            Ok((row.get::<_, String>(0)?, row.get::<_, bool>(1)?))
440        })?;
441        for r in rows {
442            delta.options_changed.push(r?);
443        }
444
445        // Autoloads
446        let mut stmt = self
447            .conn
448            .prepare("SELECT function, flags FROM plugin_autoloads WHERE plugin_id = ?1")?;
449        let rows = stmt.query_map(params![plugin_id], |row| {
450            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
451        })?;
452        for r in rows {
453            delta.autoloads.push(r?);
454        }
455
456        Ok(delta)
457    }
458
459    /// Store a plugin delta. Replaces any existing entry for this path.
460    pub fn store(
461        &self,
462        path: &str,
463        mtime_secs: i64,
464        mtime_nsecs: i64,
465        source_time_ms: u64,
466        delta: &PluginDelta,
467    ) -> rusqlite::Result<()> {
468        let now = std::time::SystemTime::now()
469            .duration_since(std::time::UNIX_EPOCH)
470            .map(|d| d.as_secs() as i64)
471            .unwrap_or(0);
472
473        // Delete old entry if exists
474        self.conn
475            .execute("DELETE FROM plugins WHERE path = ?1", params![path])?;
476
477        let bin_mtime = current_binary_mtime().unwrap_or(0);
478        self.conn.execute(
479            "INSERT INTO plugins (path, mtime_secs, mtime_nsecs, source_time_ms, cached_at, binary_mtime) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
480            params![path, mtime_secs, mtime_nsecs, source_time_ms as i64, now, bin_mtime],
481        )?;
482        let plugin_id = self.conn.last_insert_rowid();
483
484        // Functions
485        for (name, body) in &delta.functions {
486            self.conn.execute(
487                "INSERT INTO plugin_functions (plugin_id, name, body) VALUES (?1, ?2, ?3)",
488                params![plugin_id, name, body],
489            )?;
490        }
491
492        // Aliases
493        for (name, value, kind) in &delta.aliases {
494            self.conn.execute(
495                "INSERT INTO plugin_aliases (plugin_id, name, value, kind) VALUES (?1, ?2, ?3, ?4)",
496                params![plugin_id, name, value, kind.as_i32()],
497            )?;
498        }
499
500        // Variables + exports
501        for (name, value) in &delta.variables {
502            self.conn.execute(
503                "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 0)",
504                params![plugin_id, name, value],
505            )?;
506        }
507        for (name, value) in &delta.exports {
508            self.conn.execute(
509                "INSERT INTO plugin_variables (plugin_id, name, value, is_export) VALUES (?1, ?2, ?3, 1)",
510                params![plugin_id, name, value],
511            )?;
512        }
513
514        // Arrays
515        for (name, vals) in &delta.arrays {
516            let json = format!(
517                "[{}]",
518                vals.iter()
519                    .map(|v| format!("\"{}\"", v.replace('"', "\\\"")))
520                    .collect::<Vec<_>>()
521                    .join(",")
522            );
523            self.conn.execute(
524                "INSERT INTO plugin_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
525                params![plugin_id, name, json],
526            )?;
527        }
528
529        // Associative arrays — JSON-encode the key/value map. Use
530        // serde_json so quotes / backslashes / unicode round-trip
531        // correctly through the cache (the simple `["a","b"]`
532        // hand-format used for indexed arrays above doesn't escape
533        // properly for arbitrary-content keys/values).
534        for (name, map) in &delta.assoc_arrays {
535            let json = serde_json::to_string(map).unwrap_or_else(|_| "{}".to_string());
536            self.conn.execute(
537                "INSERT INTO plugin_assoc_arrays (plugin_id, name, value_json) VALUES (?1, ?2, ?3)",
538                params![plugin_id, name, json],
539            )?;
540        }
541
542        // Completions
543        for (cmd, func) in &delta.completions {
544            self.conn.execute(
545                "INSERT INTO plugin_completions (plugin_id, command, function) VALUES (?1, ?2, ?3)",
546                params![plugin_id, cmd, func],
547            )?;
548        }
549
550        // Fpath
551        for p in &delta.fpath_additions {
552            self.conn.execute(
553                "INSERT INTO plugin_fpath (plugin_id, path) VALUES (?1, ?2)",
554                params![plugin_id, p],
555            )?;
556        }
557
558        // Hooks
559        for (hook, func) in &delta.hooks {
560            self.conn.execute(
561                "INSERT INTO plugin_hooks (plugin_id, hook, function) VALUES (?1, ?2, ?3)",
562                params![plugin_id, hook, func],
563            )?;
564        }
565
566        // Bindkeys
567        for (keyseq, widget, keymap) in &delta.bindkeys {
568            self.conn.execute(
569                "INSERT INTO plugin_bindkeys (plugin_id, keyseq, widget, keymap) VALUES (?1, ?2, ?3, ?4)",
570                params![plugin_id, keyseq, widget, keymap],
571            )?;
572        }
573
574        // Zstyles
575        for (pattern, style, value) in &delta.zstyles {
576            self.conn.execute(
577                "INSERT INTO plugin_zstyles (plugin_id, pattern, style, value) VALUES (?1, ?2, ?3, ?4)",
578                params![plugin_id, pattern, style, value],
579            )?;
580        }
581
582        // Options
583        for (name, enabled) in &delta.options_changed {
584            self.conn.execute(
585                "INSERT INTO plugin_options (plugin_id, name, enabled) VALUES (?1, ?2, ?3)",
586                params![plugin_id, name, *enabled],
587            )?;
588        }
589
590        // Autoloads
591        for (func, flags) in &delta.autoloads {
592            self.conn.execute(
593                "INSERT INTO plugin_autoloads (plugin_id, function, flags) VALUES (?1, ?2, ?3)",
594                params![plugin_id, func, flags],
595            )?;
596        }
597
598        Ok(())
599    }
600
601    /// Stats for logging.
602    pub fn stats(&self) -> (i64, i64) {
603        let plugins: i64 = self
604            .conn
605            .query_row("SELECT COUNT(*) FROM plugins", [], |r| r.get(0))
606            .unwrap_or(0);
607        let functions: i64 = self
608            .conn
609            .query_row("SELECT COUNT(*) FROM plugin_functions", [], |r| r.get(0))
610            .unwrap_or(0);
611        (plugins, functions)
612    }
613
614    /// Count plugins whose file mtime no longer matches the cache.
615    pub fn count_stale(&self) -> usize {
616        let mut stmt = match self
617            .conn
618            .prepare("SELECT path, mtime_secs, mtime_nsecs FROM plugins")
619        {
620            Ok(s) => s,
621            Err(_) => return 0,
622        };
623        let rows = match stmt.query_map([], |row| {
624            Ok((
625                row.get::<_, String>(0)?,
626                row.get::<_, i64>(1)?,
627                row.get::<_, i64>(2)?,
628            ))
629        }) {
630            Ok(r) => r,
631            Err(_) => return 0,
632        };
633        let mut count = 0;
634        for (path, cached_s, cached_ns) in rows.flatten() {
635            match file_mtime(std::path::Path::new(&path)) {
636                Some((s, ns)) if s != cached_s || ns != cached_ns => count += 1,
637                None => count += 1, // file deleted
638                _ => {}
639            }
640        }
641        count
642    }
643
644    // -----------------------------------------------------------------
645    // compaudit cache — security audit results per fpath directory
646    // -----------------------------------------------------------------
647
648    /// Check if a directory's security audit result is cached and still valid.
649    /// Returns Some(is_secure) if cache hit, None if miss or stale.
650    pub fn check_compaudit(&self, dir: &str, mtime_secs: i64, mtime_nsecs: i64) -> Option<bool> {
651        self.conn.query_row(
652            "SELECT is_secure FROM compaudit_cache WHERE path = ?1 AND mtime_secs = ?2 AND mtime_nsecs = ?3",
653            params![dir, mtime_secs, mtime_nsecs],
654            |row| row.get::<_, bool>(0),
655        ).ok()
656    }
657
658    /// Store a compaudit result for a directory.
659    pub fn store_compaudit(
660        &self,
661        dir: &str,
662        mtime_secs: i64,
663        mtime_nsecs: i64,
664        uid: u32,
665        mode: u32,
666        is_secure: bool,
667    ) -> rusqlite::Result<()> {
668        let now = std::time::SystemTime::now()
669            .duration_since(std::time::UNIX_EPOCH)
670            .map(|d| d.as_secs() as i64)
671            .unwrap_or(0);
672
673        self.conn.execute(
674            "INSERT OR REPLACE INTO compaudit_cache (path, mtime_secs, mtime_nsecs, uid, mode, is_secure, checked_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
675            params![dir, mtime_secs, mtime_nsecs, uid as i64, mode as i64, is_secure, now],
676        )?;
677        Ok(())
678    }
679
680    /// Run a full compaudit against fpath directories, using cache where valid.
681    /// Returns list of insecure directories (empty = all secure).
682    pub fn compaudit_cached(&self, fpath: &[std::path::PathBuf]) -> Vec<String> {
683
684        let euid = unsafe { libc::geteuid() };
685        let mut insecure = Vec::new();
686
687        for dir in fpath {
688            let dir_str = dir.to_string_lossy().to_string();
689            let meta = match std::fs::metadata(dir) {
690                Ok(m) => m,
691                Err(_) => continue, // dir doesn't exist, skip
692            };
693            let mt_s = meta.mtime();
694            let mt_ns = meta.mtime_nsec();
695
696            // Check cache first
697            if let Some(is_secure) = self.check_compaudit(&dir_str, mt_s, mt_ns) {
698                if !is_secure {
699                    insecure.push(dir_str);
700                }
701                continue;
702            }
703
704            // Cache miss — do the actual security check
705            let mode = meta.mode();
706            let uid = meta.uid();
707            let is_secure = Self::check_dir_security(&meta, euid);
708
709            // Also check parent directory
710            let parent_secure = dir
711                .parent()
712                .and_then(|p| std::fs::metadata(p).ok())
713                .map(|pm| Self::check_dir_security(&pm, euid))
714                .unwrap_or(true);
715
716            let secure = is_secure && parent_secure;
717
718            // Cache the result
719            let _ = self.store_compaudit(&dir_str, mt_s, mt_ns, uid, mode, secure);
720
721            if !secure {
722                insecure.push(dir_str);
723            }
724        }
725
726        if insecure.is_empty() {
727            tracing::debug!(
728                dirs = fpath.len(),
729                "compaudit: all directories secure (cached)"
730            );
731        } else {
732            tracing::warn!(
733                insecure_count = insecure.len(),
734                dirs = fpath.len(),
735                "compaudit: insecure directories found"
736            );
737        }
738
739        insecure
740    }
741
742    /// Check if a directory's permissions are secure.
743    /// Insecure = world-writable or group-writable AND not owned by root or EUID.
744    fn check_dir_security(meta: &std::fs::Metadata, euid: u32) -> bool {
745        let mode = meta.mode();
746        let uid = meta.uid();
747
748        // Owned by root or the current user — always OK
749        if uid == 0 || uid == euid {
750            return true;
751        }
752
753        // Not owned by us — check if world/group writable
754        let group_writable = mode & 0o020 != 0;
755        let world_writable = mode & 0o002 != 0;
756
757        !group_writable && !world_writable
758    }
759}
760
761/// Get mtime from file metadata as (secs, nsecs).
762pub fn file_mtime(path: &Path) -> Option<(i64, i64)> {
763    let meta = std::fs::metadata(path).ok()?;
764    Some((meta.mtime(), meta.mtime_nsec()))
765}
766
767/// Default path for the plugin cache db. Honors $ZSHRS_HOME so the
768/// shell agrees with the daemon on where state lives.
769pub fn default_cache_path() -> PathBuf {
770    if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
771        return PathBuf::from(custom).join("plugins.db");
772    }
773    dirs::home_dir()
774        .unwrap_or_else(|| PathBuf::from("/tmp"))
775        .join(".zshrs/plugins.db")
776}
777
778#[cfg(test)]
779mod migration_tests {
780    use super::*;
781
782    #[test]
783    fn opening_an_existing_db_drops_legacy_script_bytecode_table() {
784        // Simulate a pre-migration DB: open with an old schema that still
785        // had script_bytecode, insert a row, close, then re-open via the
786        // current `PluginCache::open` path. The migration in `init_schema`
787        // must leave the table gone so SQLite holds zero bytecode bytes.
788        let tmp = tempfile::tempdir().unwrap();
789        let db_path = tmp.path().join("legacy.db");
790
791        // Hand-build the legacy table.
792        let pre = Connection::open(&db_path).unwrap();
793        pre.execute_batch(
794            r#"
795            CREATE TABLE script_bytecode (
796                id INTEGER PRIMARY KEY,
797                path TEXT NOT NULL UNIQUE,
798                mtime_secs INTEGER NOT NULL,
799                mtime_nsecs INTEGER NOT NULL,
800                bytecode BLOB NOT NULL,
801                cached_at INTEGER NOT NULL
802            );
803            CREATE INDEX idx_script_bytecode_path ON script_bytecode(path);
804            INSERT INTO script_bytecode (id, path, mtime_secs, mtime_nsecs, bytecode, cached_at)
805                VALUES (1, '/fake/legacy.zsh', 0, 0, x'00deadbeef', 0);
806            "#,
807        )
808        .unwrap();
809        drop(pre);
810
811        // Re-open via the production path — migration runs.
812        let _cache = PluginCache::open(&db_path).expect("open after migration");
813
814        // Confirm script_bytecode is gone.
815        let post = Connection::open(&db_path).unwrap();
816        let exists: i64 = post
817            .query_row(
818                "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='script_bytecode'",
819                [],
820                |row| row.get(0),
821            )
822            .unwrap();
823        assert_eq!(exists, 0, "legacy script_bytecode must be dropped");
824    }
825}
826
827// ===========================================================
828// Methods moved verbatim from src/ported/exec.rs because their
829// C counterpart's source file maps 1:1 to this Rust module.
830// Phase: drift
831// ===========================================================
832
833// BEGIN moved-from-exec-rs
834impl crate::ported::exec::ShellExecutor {
835    /// Snapshot executor state before sourcing a plugin (for delta computation).
836    pub(crate) fn snapshot_state(&self) -> PluginSnapshot {
837        PluginSnapshot {
838            functions: self.function_names().into_iter().collect(),
839            aliases: self.alias_entries().into_iter().map(|(k, _)| k).collect(),
840            global_aliases: self.global_alias_entries().into_iter().map(|(k, _)| k).collect(),
841            suffix_aliases: self.suffix_alias_entries().into_iter().map(|(k, _)| k).collect(),
842            variables: if let Ok(tab) = crate::ported::params::paramtab().read() {
843                tab.iter()
844                    .filter(|(_, pm)| pm.u_arr.is_none())
845                    .map(|(k, pm)| (k.clone(), pm.u_str.clone().unwrap_or_default()))
846                    .collect()
847            } else {
848                std::collections::HashMap::new()
849            },
850            arrays: if let Ok(tab) = crate::ported::params::paramtab().read() {
851                tab.iter()
852                    .filter(|(_, pm)| pm.u_arr.is_some())
853                    .map(|(k, _)| k.clone())
854                    .collect()
855            } else {
856                std::collections::HashSet::new()
857            },
858            assoc_arrays: if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
859                m.keys().cloned().collect()
860            } else {
861                std::collections::HashSet::new()
862            },
863            fpath: self.fpath.clone(),
864            options: crate::ported::options::opt_state_snapshot(),
865            hooks: {
866                // Snapshot `<hook>_functions` arrays from canonical paramtab.
867                let names = ["chpwd", "precmd", "preexec", "periodic", "zshexit", "zshaddhistory"];
868                let mut m = std::collections::HashMap::new();
869                for h in &names {
870                    let arr_name = format!("{}_functions", h);
871                    if let Some(arr) = self.array(&arr_name) {
872                        if !arr.is_empty() {
873                            m.insert(h.to_string(), arr);
874                        }
875                    }
876                }
877                m
878            },
879            autoloads: {
880                // Walk canonical shfunctab for autoload-pending entries
881                // (PM_UNDEFINED set). Snapshot is keyed by function name.
882                crate::ported::hashtable::shfunctab_lock().read().ok()
883                    .map(|t| t.iter()
884                        .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
885                        .map(|(name, _)| name.clone())
886                        .collect())
887                    .unwrap_or_default()
888            },
889        }
890    }
891    /// Compute the delta between current state and a previous snapshot.
892    pub(crate) fn diff_state(&self, snap: &PluginSnapshot) -> crate::plugin_cache::PluginDelta {
893        let mut delta = PluginDelta::default();
894
895        // Walk every HashMap in sorted-key order so the resulting
896        // PluginDelta serializes byte-identically across runs of an
897        // identical state. Without sorting, rkyv-encoded delta blobs
898        // differ run-to-run, defeating cache reuse and tripping
899        // diff-based snapshot tests.
900
901        // New functions — serialize canonical source text (UTF-8 bytes)
902        // for instant replay. Replay parses + compiles via the new pipeline.
903        let mut fn_keys: Vec<&String> = self.function_source.keys().collect();
904        fn_keys.sort();
905        for name in fn_keys {
906            if !snap.functions.contains(name) {
907                let source = self.function_source.get(name).unwrap();
908                delta
909                    .functions
910                    .push((name.clone(), source.as_bytes().to_vec()));
911            }
912        }
913
914        let push_alias = |delta: &mut PluginDelta,
915                          entries: Vec<(String, String)>,
916                          snap_set: &std::collections::HashSet<String>,
917                          kind: AliasKind| {
918            let mut entries = entries;
919            entries.sort_by(|a, b| a.0.cmp(&b.0));
920            for (name, value) in entries {
921                if !snap_set.contains(&name) {
922                    delta.aliases.push((name, value, kind));
923                }
924            }
925        };
926        push_alias(&mut delta, self.alias_entries(), &snap.aliases, AliasKind::Regular);
927        push_alias(&mut delta, self.global_alias_entries(), &snap.global_aliases, AliasKind::Global);
928        push_alias(&mut delta, self.suffix_alias_entries(), &snap.suffix_aliases, AliasKind::Suffix);
929
930        // New/changed variables. Skip shell-special parameters whose
931        // values are runtime-state, not script-state — replaying them
932        // poisons subsequent shells with values frozen from the
933        // capture run. C zsh maintains these per-process and never
934        // serializes them: `_` (last argv of last command, Src/init.c
935        // special_params; gets `/tmp/foo` from a prior bash test then
936        // gets fed into `(( $_ ))` math in a user's .zshrc), `?`
937        // (last exit), `$`/`!`/`PPID` (process IDs), `RANDOM`,
938        // `SECONDS`, `EPOCHSECONDS`, `LINENO`, `OLDPWD`, `PWD`
939        // (volatile; cwd is re-read on replay anyway), `STATUS`,
940        // `OPTIND`, `IFS` (must default to whitespace at shell
941        // startup unless user explicitly sets it). Direct port of
942        // the C analogue's PM_SPECIAL flag — those params don't
943        // round-trip through the parameter-table dump path.
944        const NON_REPLAYABLE_VARS: &[&str] = &[
945            "0", "_", "?", "$", "!", "PPID", "RANDOM", "SECONDS",
946            "EPOCHSECONDS", "EPOCHREALTIME", "LINENO", "OLDPWD", "PWD",
947            "STATUS", "OPTIND", "OPTARG", "IFS", "FUNCNAME",
948            "BASHPID", "BASH_LINENO", "BASH_SOURCE",
949            "ZSH_ARGZERO", "ZSH_EVAL_CONTEXT", "ZSH_SUBSHELL",
950            "HISTCMD", "MATCH", "MBEGIN", "MEND",
951        ];
952        let mut var_keys: Vec<String> =
953            if let Ok(tab) = crate::ported::params::paramtab().read() {
954                tab.iter()
955                    .filter(|(_, pm)| pm.u_arr.is_none())
956                    .map(|(k, _)| k.clone())
957                    .collect()
958            } else {
959                Vec::new()
960            };
961        var_keys.sort();
962        for name in &var_keys {
963            if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
964                continue;
965            }
966            let value = crate::ported::params::getsparam(name).unwrap_or_default();
967            match snap.variables.get(name) {
968                Some(old) if old == &value => {} // unchanged
969                _ => {
970                    // Check if it's also exported
971                    if env::var(name).ok().as_ref() == Some(&value) {
972                        delta.exports.push((name.clone(), value.clone()));
973                    } else {
974                        delta.variables.push((name.clone(), value.clone()));
975                    }
976                }
977            }
978        }
979
980        // New arrays — iterate paramtab for PM_ARRAY entries.
981        let arr_entries: Vec<(String, Vec<String>)> =
982            if let Ok(tab) = crate::ported::params::paramtab().read() {
983                let mut v: Vec<(String, Vec<String>)> = tab.iter()
984                    .filter_map(|(k, pm)| pm.u_arr.clone().map(|a| (k.clone(), a)))
985                    .collect();
986                v.sort_by(|a, b| a.0.cmp(&b.0));
987                v
988            } else {
989                Vec::new()
990            };
991        for (name, values) in arr_entries {
992            if !snap.arrays.contains(&name) {
993                delta.arrays.push((name, values));
994            }
995        }
996
997        // New / changed associative arrays. zinit creates `ZINIT[…]`
998        // entries during sourcing; without this capture, the cache
999        // replay path saw an empty ZINIT and `${ZINIT[BIN_DIR]}`
1000        // returned "" on every subsequent shell start. Direct port of
1001        // zsh's plugin-replay model — assoc deltas are first-class
1002        // captures alongside scalars and arrays.
1003        let assoc_entries: Vec<(String, indexmap::IndexMap<String, String>)> =
1004            if let Ok(m) = crate::ported::params::paramtab_hashed_storage().lock() {
1005                let mut v: Vec<(String, indexmap::IndexMap<String, String>)> = m.iter()
1006                    .map(|(k, v)| (k.clone(), v.clone()))
1007                    .collect();
1008                v.sort_by(|a, b| a.0.cmp(&b.0));
1009                v
1010            } else {
1011                Vec::new()
1012            };
1013        for (name, map) in assoc_entries {
1014            if !snap.assoc_arrays.contains(&name) {
1015                // Executor's assoc storage is IndexMap (insertion-
1016                // ordered, required by `(kv)` etc.). The plugin_cache
1017                // delta uses a plain HashMap since the cache replay
1018                // reseeds the assoc and order is reconstructed by
1019                // the script's own typeset ordering. Convert here.
1020                let plain: HashMap<String, String> =
1021                    map.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
1022                delta.assoc_arrays.push((name, plain));
1023            }
1024        }
1025
1026        // New fpath entries
1027        for p in &self.fpath {
1028            if !snap.fpath.contains(p) {
1029                delta.fpath_additions.push(p.to_string_lossy().to_string());
1030            }
1031        }
1032
1033        // Changed options — diff against canonical OPTS_LIVE snapshot.
1034        let current = crate::ported::options::opt_state_snapshot();
1035        let mut opt_keys: Vec<&String> = current.keys().collect();
1036        opt_keys.sort();
1037        for name in opt_keys {
1038            let value = current.get(name).unwrap();
1039            match snap.options.get(name) {
1040                Some(old) if old == value => {}
1041                _ => delta.options_changed.push((name.clone(), *value)),
1042            }
1043        }
1044
1045        // New hooks — read from canonical `<hook>_functions` arrays.
1046        let names = ["chpwd", "precmd", "preexec", "periodic", "zshexit", "zshaddhistory"];
1047        let mut hook_names: Vec<&&str> = names.iter().collect();
1048        hook_names.sort();
1049        for &h in hook_names {
1050            let arr_name = format!("{}_functions", h);
1051            let funcs = self.array(&arr_name).unwrap_or_default();
1052            let old_funcs = snap.hooks.get(h);
1053            for f in &funcs {
1054                let is_new = old_funcs.is_none_or(|old| !old.contains(f));
1055                if is_new {
1056                    delta.hooks.push((h.to_string(), f.clone()));
1057                }
1058            }
1059        }
1060
1061        // New autoloads — read PM_UNDEFINED entries from canonical shfunctab.
1062        let current_autoloads: Vec<String> = crate::ported::hashtable::shfunctab_lock()
1063            .read().ok()
1064            .map(|t| t.iter()
1065                .filter(|(_, shf)| (shf.node.flags as u32 & PM_UNDEFINED) != 0)
1066                .map(|(name, _)| name.clone())
1067                .collect())
1068            .unwrap_or_default();
1069        let mut autoload_keys: Vec<&String> = current_autoloads.iter().collect();
1070        autoload_keys.sort();
1071        for name in autoload_keys {
1072            if !snap.autoloads.contains(name) {
1073                // Flags stub-string: canonical autoload sets only PM_UNDEFINED
1074                // (the -U/-z/-k/-t/-d details were never consumed by replay).
1075                delta.autoloads.push((name.clone(), String::new()));
1076            }
1077        }
1078
1079        delta
1080    }
1081    /// Replay a cached plugin delta into the executor state.
1082    pub(crate) fn replay_plugin_delta(&mut self, delta: &crate::plugin_cache::PluginDelta) {
1083
1084        // Aliases
1085        for (name, value, kind) in &delta.aliases {
1086            match kind {
1087                AliasKind::Regular => {
1088                    self.set_alias(name.clone(), value.clone());
1089                }
1090                AliasKind::Global => {
1091                    self.set_global_alias(name.clone(), value.clone());
1092                }
1093                AliasKind::Suffix => {
1094                    self.set_suffix_alias(name.clone(), value.clone());
1095                }
1096            }
1097        }
1098
1099        // Variables. Drop shell-special parameters even on the
1100        // replay side — pre-existing caches from before the
1101        // diff_state filter was added still contain entries for
1102        // `_`, `PPID`, etc.; replaying them poisons the new shell.
1103        // Keeping the same exclusion list as `diff_state` so old
1104        // caches self-heal on next read.
1105        const NON_REPLAYABLE_VARS: &[&str] = &[
1106            "0", "_", "?", "$", "!", "PPID", "RANDOM", "SECONDS",
1107            "EPOCHSECONDS", "EPOCHREALTIME", "LINENO", "OLDPWD", "PWD",
1108            "STATUS", "OPTIND", "OPTARG", "IFS", "FUNCNAME",
1109            "BASHPID", "BASH_LINENO", "BASH_SOURCE",
1110            "ZSH_ARGZERO", "ZSH_EVAL_CONTEXT", "ZSH_SUBSHELL",
1111            "HISTCMD", "MATCH", "MBEGIN", "MEND",
1112        ];
1113        for (name, value) in &delta.variables {
1114            if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1115                continue;
1116            }
1117            self.set_scalar(name.clone(), value.clone());
1118        }
1119
1120        // Exports (set in both variables and process env)
1121        for (name, value) in &delta.exports {
1122            if NON_REPLAYABLE_VARS.contains(&name.as_str()) {
1123                continue;
1124            }
1125            self.set_scalar(name.clone(), value.clone());
1126            env::set_var(name, value);
1127        }
1128
1129        // Arrays
1130        for (name, values) in &delta.arrays {
1131            self.set_array(name.clone(), values.clone());
1132        }
1133
1134        // Associative arrays — restore plugin-defined assocs (e.g.
1135        // ZINIT, ZINIT_SNIPPETS, ZINIT_REPORTS) so subsequent shells
1136        // see the same `${ZINIT[BIN_DIR]}` etc. that the original
1137        // sourcing established. Mirrors the diff_state capture above.
1138        for (name, map) in &delta.assoc_arrays {
1139            // Plugin cache uses HashMap; executor uses IndexMap.
1140            // Reseed by inserting key-by-key so the IndexMap variant
1141            // is constructed without needing a HashMap→IndexMap
1142            // From impl that may not be available.
1143            let mut idx_map: indexmap::IndexMap<String, String> =
1144                indexmap::IndexMap::with_capacity(map.len());
1145            // Sort for deterministic order (the diff_state stored
1146            // a HashMap which has no defined order; the original
1147            // insertion order was lost). Sort is the simplest
1148            // reproducible choice — matches `(o)`-flag default.
1149            let mut entries: Vec<(&String, &String)> = map.iter().collect();
1150            entries.sort_by(|a, b| a.0.cmp(b.0));
1151            for (k, v) in entries {
1152                idx_map.insert(k.clone(), v.clone());
1153            }
1154            self.set_assoc(name.clone(), idx_map);
1155        }
1156
1157        // Fpath additions
1158        for p in &delta.fpath_additions {
1159            let pb = PathBuf::from(p);
1160            if !self.fpath.contains(&pb) {
1161                self.fpath.push(pb);
1162            }
1163        }
1164
1165        // Completions
1166        if !delta.completions.is_empty() {
1167            let mut comps = self.assoc("_comps").unwrap_or_default();
1168            for (cmd, func) in &delta.completions {
1169                comps.insert(cmd.clone(), func.clone());
1170            }
1171            self.set_assoc("_comps".to_string(), comps);
1172        }
1173
1174        // Options — write into canonical OPTS_LIVE.
1175        for (name, enabled) in &delta.options_changed {
1176            crate::ported::options::opt_state_set(name, *enabled);
1177        }
1178
1179        // Hooks — append into the canonical `<hook>_functions` paramtab array.
1180        for (hook, func) in &delta.hooks {
1181            self.add_hook(hook, func);
1182        }
1183
1184        // Plugin cache replay: each bincode blob is a ShellCommand AST.
1185        // Replay each function's source text through parse_init + parse + ZshCompiler.
1186        // Delta format: name → UTF-8 source bytes (no AST round-trip needed).
1187        for (name, bytes) in &delta.functions {
1188            let Ok(source) = std::str::from_utf8(bytes) else {
1189                continue;
1190            };
1191            // Mirror Src/init.c errflag save/clear/check around parse.
1192            let saved_errflag = errflag.load(Ordering::Relaxed);
1193            errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
1194            crate::ported::parse::parse_init(source);
1195            let program = crate::ported::parse::parse();
1196            let parse_failed = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
1197            errflag.store(saved_errflag, Ordering::Relaxed);
1198            if parse_failed || program.lists.is_empty() {
1199                continue;
1200            }
1201            let chunk = crate::compile_zsh::ZshCompiler::new().compile(&program);
1202            self.functions_compiled.insert(name.clone(), chunk);
1203            self.function_source
1204                .insert(name.clone(), source.to_string());
1205        }
1206    }
1207}
1208// END moved-from-exec-rs