zshrs 0.11.4

The first compiled Unix shell — bytecode VM, worker pool, AOP intercept, Rkyv caching
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
//! Apply daemon canonical state to a freshly-built ShellExecutor —
//! by reading the daemon's rkyv shard directly from disk. No IPC.
//!
//! **zshrs-original infrastructure — no C source counterpart.** C
//! zsh always runs `Src/init.c::source_startup_files()` to set up
//! a fresh shell from the user's dotfiles. zshrs adds a fast path:
//! if `zshrs-daemon` has a canonical-state shard on disk
//! (`~/.zshrs/images/*-recorder.rkyv`), we mmap and apply it
//! directly into the executor's `pub` HashMaps, skipping the
//! `.zshenv`/`.zprofile`/`.zshrc`/`.zlogin` source pass entirely.
//! The shard format is rkyv (zero-copy archived structs) so the
//! cold-start cost is ~1ms instead of ~150ms.
//!
//! **Why direct shard read, not IPC.** The original spec
//! (`docs/DAEMON.md` "NO WALKING IN CLIENTS" + cache-architecture
//! memory) calls for thin clients that mmap the daemon's pre-built
//! shards as a zero-copy data plane. The earlier IPC version of this
//! file did 1+ `definitions_query` round-trips per cold-start, which
//! at ~600μs per round-trip put us 5-10ms over the spec target. The
//! mmap path is the real architecture: kernel page-cache after first
//! launch + rkyv check_archived + struct copy = sub-millisecond.
//!
//! IPC stays intact for `zd` / editor plugins / dashboards (see
//! `daemon/definitions.rs`). It's the right interface for "give me
//! the current catalog snapshot" from external tools. It's the wrong
//! interface for the shell's own cold-start hot path.
//!
//! The recorder writes one `*-recorder.rkyv` shard per ingest into
//! `~/.zshrs/images/`. We pick the latest by mtime, deserialize,
//! and copy fields straight into the executor's pub HashMaps.
//!
//! Failure mode: any I/O error → return 0; caller falls back to
//! vanilla `source_startup_files()`. Logged so the user can see why.

#![cfg(feature = "daemon")]

use std::path::PathBuf;

use crate::daemon::paths::CachePaths;
use crate::daemon::shard::{list_shards, read_canonical_shard, CanonicalShard};
use crate::exec::{AutoloadFlags, ShellExecutor, zstyle_entry};
// Legacy `zle()` / `KeymapName` removed alongside the
// `extensions::keymaps` dissolution. Recorder-replay paths that
// previously wrote into `ZleManager` (bindkey + user-widget
// registration) now log-and-skip until canonical replay through
// `ported::zle::zle_keymap::keymapnamtab` / `zle_thingy::thingytab`
// is wired.

/// Read the latest recorder shard and apply its canonical state to
/// the executor. Returns total rows applied (`0` if no shard or
/// read failure → caller falls back to vanilla dotfile source).
/// zshrs-original — no C counterpart. C zsh's
/// `source_startup_files()` (Src/init.c) is the only path; this is
/// a faster alternative built on top of the daemon shard.
pub fn apply_all(executor: &mut ShellExecutor) -> usize {
    let t0 = std::time::Instant::now();

    let paths = match CachePaths::resolve() {
        Ok(p) => p,
        Err(e) => {
            tracing::warn!(error = %e, "canonical_apply: cache paths unresolved");
            return 0;
        }
    };

    let shard_path = match latest_recorder_shard(&paths) {
        Some(p) => p,
        None => {
            tracing::info!(
                "canonical_apply: no recorder shard found in {} — vanilla fallback",
                paths.images.display()
            );
            return 0;
        }
    };

    let shard = match read_canonical_shard(&shard_path) {
        Ok(s) => s,
        Err(e) => {
            tracing::warn!(error = %e, path = %shard_path.display(), "canonical_apply: shard read failed");
            return 0;
        }
    };

    let total = apply_shard(executor, shard);
    let elapsed_us = t0.elapsed().as_micros();
    tracing::info!(
        rows = total,
        elapsed_us,
        path = %shard_path.display(),
        "canonical state applied from rkyv shard (no IPC)"
    );
    total
}

/// Walk `~/.zshrs/images/` and return the newest
/// `*-recorder.rkyv` shard by mtime.
/// zshrs-original — no C counterpart.
fn latest_recorder_shard(paths: &CachePaths) -> Option<PathBuf> {
    let entries = list_shards(paths).ok()?;
    entries
        .into_iter()
        .filter(|p| {
            p.file_name()
                .and_then(|s| s.to_str())
                .map(|s| s.ends_with("-recorder.rkyv"))
                .unwrap_or(false)
        })
        .max_by_key(|p| {
            std::fs::metadata(p)
                .and_then(|m| m.modified())
                .ok()
        })
}

/// Bulk-copy every subsystem from a deserialized canonical shard
/// into the executor's mutable tables.
/// zshrs-original — no C counterpart. The closest C analog is the
/// per-subsystem builtin dispatch each dotfile triggers
/// (`alias`/`bindkey`/`zstyle`/`compdef`/etc.) but compressed into
/// a single in-memory copy.
fn apply_shard(executor: &mut ShellExecutor, shard: CanonicalShard) -> usize {
    let mut total = 0;

    // Aliases (3 flavors).
    for (n, v) in shard.aliases {
        executor.set_alias(n, v);
        total += 1;
    }
    for (n, v) in shard.global_aliases {
        executor.set_global_alias(n, v);
        total += 1;
    }
    for (n, v) in shard.suffix_aliases {
        executor.set_suffix_alias(n, v);
        total += 1;
    }

    // Exported env: mirror to process env so child commands inherit.
    for (n, v) in shard.env_exports {
        std::env::set_var(&n, &v);
        executor.set_scalar(n, v);
        total += 1;
    }

    // Non-exported shell params.
    for (n, v) in shard.params {
        executor.set_scalar(n, v);
        total += 1;
    }

    // setopt / unsetopt.
    for opt in shard.setopts {
        crate::ported::options::opt_state_set(&opt, true);
        total += 1;
    }
    for opt in shard.unsetopts {
        crate::ported::options::opt_state_set(&opt, false);
        total += 1;
    }

    // path + fpath: ordered Vec<String> in the shard.
    if !shard.path.is_empty() {
        let joined = shard.path.join(":");
        std::env::set_var("PATH", &joined);
        executor.set_scalar("PATH".to_string(), joined);
        total += shard.path.len();
        executor.set_array("path".to_string(), shard.path);
    }
    if !shard.fpath.is_empty() {
        let joined = shard.fpath.join(":");
        std::env::set_var("FPATH", &joined);
        executor.set_scalar("FPATH".to_string(), joined);
        total += shard.fpath.len();
        executor.fpath = shard.fpath.iter().map(PathBuf::from).collect();
        executor.set_array("fpath".to_string(), shard.fpath);
    }

    // named_dir (hash -d): insert into canonical `nameddirtab` (port
    // of C `Src/hashnameddir.c::nameddirtab`).
    for (name, path) in shard.named_dirs {
        if let Ok(mut tab) = crate::ported::hashnameddir::nameddirtab().lock() {
            tab.insert(name.clone(), crate::ported::zsh_h::nameddir {
                node: crate::ported::zsh_h::hashnode {
                    next: None,
                    nam: name,
                    flags: 0,
                },
                dir: path.clone(),
                diff: 0,
            });
            total += 1;
        }
    }

    // autoload_functions: register every name as autoload-pending
    // with the standard `-Uz` flag set (NO_ALIAS + ZSH_STYLE), what
    // every modern compsys / plugin does. The body lookup happens on
    // first call via the autoload resolver; we don't pre-compile.
    // Register each autoload-pending function via the canonical
    // shfunctab stub with `PM_UNDEFINED` set — matches C's
    // `autoload_func` at `Src/exec.c:5215+` flow. `AutoloadFlags`
    // (-U/-z/-k/-t/-d) details were never consumed elsewhere; the
    // canonical bit is just "shfunc exists with PM_UNDEFINED".
    let _ = AutoloadFlags::NO_ALIAS;
    for name in shard.autoload_functions.keys() {
        if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
            tab.add(crate::ported::hashtable::shfunc_autoload(name));
        }
        total += 1;
    }

    // zstyle: shard stores `Vec<(pattern, "style val val ...")>` —
    // split the joined-rest back into (style, values) so the exec
    // side has the same `zstyle_entry { pattern, style, values: Vec<_> }`
    // shape it would build by sourcing `zstyle :ctx style val val …`
    // statements.
    for (pattern, rest) in shard.zstyle {
        let mut parts = rest.split_whitespace();
        let style = match parts.next() {
            Some(s) => s.to_string(),
            None => continue,
        };
        let values: Vec<String> = parts.map(str::to_string).collect();
        executor.zstyles.push(zstyle_entry {
            pattern,
            style,
            values,
        });
        total += 1;
    }

    // bindkey: install each captured (keyseq, widget) into the global
    // KeymapManager. Recorder encodes the keymap-target by prefixing
    // the value with `[KEYMAP] ` (per `bin_bindkey` in
    // src/exec.rs); strip that prefix and dispatch to the right
    // keymap. Default = Main.
    {
        // bindkey replay routes through the canonical `bindkey()`
        // free fn (`ported::zle::zle_bindings.rs:192`) which writes
        // to `keymapnamtab` matching what the C `bindkey` builtin
        // does at runtime.
        for (keyseq, value) in shard.bindkeys {
            let (keymap, widget) = parse_bindkey_value(&value);
            crate::ported::zle::zle_bindings::bindkey(keymap, &keyseq, widget);
            total += 1;
        }
    }

    // compdef: each (function, "cmd1 cmd2 ...") row replays through
    // the same compdef builtin install path
    // (compsys::compdef::compdef_execute) that runtime `compdef _git
    // git` would have used. Recorder captures with format
    // `name=function value="cmd1 cmd2 …"` (per `builtin_compdef` in
    // src/exec.rs).
    if !shard.compdef.is_empty() {
        // Executor's constructor only opens compsys_cache if the
        // .db file already exists (cold start = None). Open it
        // lazily here so the apply path is self-sufficient: first
        // recorder ingest creates the cache; subsequent shells see
        // it and apply normally.
        if executor.compsys_cache.is_none() {
            let cache_path = compsys::cache::default_cache_path();
            if let Some(parent) = cache_path.parent() {
                let _ = std::fs::create_dir_all(parent);
            }
            match compsys::cache::CompsysCache::open(&cache_path) {
                Ok(c) => {
                    tracing::info!(
                        path = %cache_path.display(),
                        "compsys cache lazily created for canonical compdef apply"
                    );
                    executor.compsys_cache = Some(c);
                }
                Err(e) => {
                    tracing::warn!(error = %e, "compsys cache open failed; compdef rows skipped");
                }
            }
        }
        if let Some(cache) = executor.compsys_cache.as_mut() {
            for (function, cmds_joined) in shard.compdef {
                let mut args: Vec<String> = Vec::with_capacity(8);
                args.push(function);
                for cmd in cmds_joined.split_whitespace() {
                    args.push(cmd.to_string());
                }
                if args.len() < 2 {
                    continue; // recorder dropped the cmd list — can't replay
                }
                let _rc = compsys::compdef::compdef_execute(cache, &args);
                total += 1;
            }
        }
    }

    // zle widgets: recorder routes them into shard.extras["zle"]
    // (one per `zle -N name [body]` capture). Reinstall via
    // ZleManager.user_widgets. Body string is whatever the user
    // gave; the widget invocation path looks it up at execution
    // time so re-installing the name+body string is enough.
    if let Some(zle_widgets) = shard.extras.get("zle") {
        // User-widget replay through the canonical `Widget::user_defined`
        // + `bindwidget` machinery in `ported::zle/zle_thingy.rs`,
        // matching the C `bin_zle_new()` registration path at
        // `Src/Zle/zle_thingy.c:584`.
        for (name, body) in zle_widgets {
            let w = std::sync::Arc::new(
                crate::ported::zle::zle_h::widget::user_defined(name, body),
            );
            crate::ported::zle::zle_thingy::rthingy(name);
            crate::ported::zle::zle_thingy::bindwidget(w, name);
            total += 1;
        }
    }

    // inline-defined functions: captured in shard but not yet wired.
    // Two paths to install:
    //   (a) parse body string + fusevm-compile here on cold-start.
    //       ~50ms for 1k functions on M-series — defeats the
    //       zero-cost goal.
    //   (b) recorder ships pre-compiled bytecode in the shard;
    //       shell installs the chunk directly.
    // Path (b) is the right one (needs recorder-side change to
    // capture body bytecode at definition time + a new
    // `functions_compiled: HashMap<String, Vec<u8>>` shard field).
    // Until then, autoload-pending fallback covers the case: when
    // the user calls a function that wasn't pre-installed, the
    // autoload resolver fires + parses the body lazily.
    let _ = shard.functions;

    // zmodload / manpath / plugins / sourced_files / extras: no
    // executor surface today (modules call `zmodload` builtin
    // directly at use time; manpath is read from $MANPATH env;
    // plugins/sourced_files are diagnostic-only; extras is a
    // catch-all).
    let _ = shard.zmodload;
    let _ = shard.manpath;
    let _ = shard.plugins;
    let _ = shard.sourced_files;
    let _ = shard.extras;

    total
}

/// Decode a recorder-emitted bindkey value into (keymap, widget).
///
/// Format from `src/exec.rs:bin_bindkey`:
///   `widget_name`               → KeymapName::Main
///   `[keymap_name] widget_name` → KeymapName::from_str("keymap_name")
///
/// Unknown keymap names fall back to `Main` (matches what zsh's
/// `bindkey` does for unrecognized -M targets — a safer default than
/// silently dropping the binding).
/// Parse a `bindkey` shard value into `(keymap, sequence)`.
/// zshrs-original — splits the canonical form `"keymap:sequence"`
/// the recorder writes back into the two arguments
/// `bindkey` (Src/Zle/zle_keymap.c) takes at the C builtin layer.
fn parse_bindkey_value(value: &str) -> (&str, &str) {
    if let Some(rest) = value.strip_prefix('[') {
        if let Some(close_idx) = rest.find(']') {
            let keymap_str = &rest[..close_idx];
            let widget = rest[close_idx + 1..].trim_start();
            return (keymap_str, widget);
        }
    }
    ("main", value)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bindkey_value_parses_main_keymap_default() {
        let (km, w) = parse_bindkey_value("history-search-backward");
        assert_eq!(km, "main");
        assert_eq!(w, "history-search-backward");
    }

    #[test]
    fn bindkey_value_parses_explicit_keymap() {
        let (km, w) = parse_bindkey_value("[viins] backward-delete-char");
        assert_eq!(km, "viins");
        assert_eq!(w, "backward-delete-char");
    }

    #[test]
    fn bindkey_value_unknown_keymap_falls_back_to_main() {
        let (km, w) = parse_bindkey_value("[totally-not-real] do-thing");
        // No longer falls back to "main" — the C `bindkey` builtin
        // forwards the literal name to keymapnamtab lookup, which
        // surfaces the error there. parse_bindkey_value just returns
        // the bracketed text verbatim.
        assert_eq!(km, "totally-not-real");
        assert_eq!(w, "do-thing");
    }

    #[test]
    fn bindkey_value_handles_extra_whitespace_after_close_bracket() {
        let (km, w) = parse_bindkey_value("[vicmd]   forward-word");
        assert_eq!(km, "vicmd");
        assert_eq!(w, "forward-word");
    }
}