zshrs 0.13.41

The first JIT-compiled Unix shell — bytecode VM, Cranelift JIT, worker pool, AOP intercept, Rkyv caching
Documentation
//! !!! WARNING: RUST-ONLY MODULE — NOT A PORT OF ANY ZSH C FILE !!!
//!
//! Verbatim capture of function-body source text as the lexer consumes it.
//!
//! C zsh does not store function source at all. `par_funcdef`
//! (Src/parse.c:1672) compiles the body into the `ecbuf` wordcode array,
//! and `functions` / `typeset -f` / `whence -f` RECONSTRUCT the printed
//! text from that wordcode via `getpermtext` -> `gettext2`
//! (Src/text.c:189 / :296) — which is why zsh always prints canonical
//! tab-indented text regardless of how the user spaced the original.
//!
//! zshrs runs function bodies through the fusevm compile path, which never
//! builds a C-shaped `Eprog`; `shfunc.funcdef` (zsh_h.rs:868) is therefore
//! always `None` and `printshfuncnode` (hashtable.rs:1796) prints the RAW
//! SOURCE the parser captured instead. Until the fusevm path grows a real
//! `Eprog`, that raw source has to come from somewhere for EVERY input
//! source.
//!
//! It used to come only from a slice of the Rust-only `LEX_INPUT` window
//! (`lex::input_slice`), which exists for file / `-c` / `eval` input.
//! Interactive and `-s` stdin input reaches the lexer through
//! `hgetc` -> `ingetc` -> the `inbuf` stack and never touches `LEX_INPUT`,
//! so `LEX_POS` never moved and every function defined at a prompt or piped
//! in on stdin printed as `name () { }`.
//!
//! C's own history-line buffer (`chline`) is NOT a usable substitute: at
//! c:Src/hist.c:1119 `hbegin` sets
//! `stophist = (!interact || unset(SHINSTDIN)) ? 2 : 0` and then c:1129
//! `chline = hptr = NULL`, so for non-interactive stdin there is no history
//! line at all. The one funnel every input source does pass through is
//! `hgetc`, so this module hangs an echo buffer off it, active only between
//! [`body_mark_begin`] and [`body_text`].
//!
//! Lives outside `src/ported/` because it has no C counterpart (same
//! reasoning as `crate::tolerant_sort`).

use crate::ported::lex::{input_slice, pos};

thread_local! {
/// Verbatim echo of the characters the lexer has consumed while at least
/// one function-body capture is open. Fed by `hgetc` / `hungetc`
/// (src/ported/lex.rs); see the module doc above for why it exists and why
/// C's `chline` cannot stand in for it. Active only between
/// [`body_mark_begin`] and [`body_text`].
pub static LEX_SRC_CAPTURE: std::cell::RefCell<String>
    = const { std::cell::RefCell::new(String::new()) };
/// !!! WARNING: RUST-ONLY HELPER STATE !!!
///
/// Number of function-body captures currently open (nested `f() { g()
/// { ... } }` opens two). Zero means `hgetc` records nothing.
pub static LEX_SRC_CAPTURE_DEPTH: std::cell::Cell<usize>
    = const { std::cell::Cell::new(0) };
/// !!! WARNING: RUST-ONLY HELPER STATE !!!
///
/// One flag byte per character of [`LEX_SRC_CAPTURE`], built from
/// [`CAP_ALIAS_TEXT`] and [`CAP_ALIAS_NAME`]. zsh compiles a function body
/// AFTER alias expansion, so [`body_text`] keeps expansion text and drops the
/// alias names it replaced; [`event_text`] does the reverse, because its
/// re-parse expands each alias name again.
pub static LEX_SRC_CAPTURE_ALIAS: std::cell::RefCell<Vec<u8>>
    = const { std::cell::RefCell::new(Vec::new()) };
/// !!! WARNING: RUST-ONLY HELPER STATE !!!
///
/// Lockstep companion to `lex::LEX_UNGET_BUF`, exactly like
/// `lex::LEX_UNGET_HPTR`: one entry per queued character, `Some(flags)` when
/// `hungetc` removed it from [`LEX_SRC_CAPTURE`] (carrying its
/// [`LEX_SRC_CAPTURE_ALIAS`] flags) and `None` when it did not, so the
/// matching re-read in `hgetc` puts back exactly what was taken. Without it a
/// capture that opens or closes between the unget and the re-read
/// gains or loses one character.
pub static LEX_UNGET_SRCCAP: std::cell::RefCell<std::collections::VecDeque<Option<u8>>>
    = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// A parser-side bookmark for "where the text of a function body starts".
/// C has no analogue — `par_funcdef` (Src/parse.c:1672) writes the body
/// into the `ecbuf` wordcode array and `functions` re-derives the printed
/// text from it (`getpermtext`, Src/text.c:189). zshrs's fusevm path never
/// builds that wordcode `Eprog`, so the parser must keep the raw source.
///
/// Carries BOTH cursors because zshrs has two input sources: the Rust-only
/// `LEX_INPUT` window (file / `-c` / `eval` text) and the ported `inbuf`
/// stack that `hgetc` reads for interactive and `-s` stdin input. See
/// [`LEX_SRC_CAPTURE`] for why the second one needs an echo buffer.
#[derive(Debug)]
pub struct BodyMark {
    /// `LEX_POS` at the time the mark was taken.
    lex_pos: usize,
    /// Offset into [`LEX_SRC_CAPTURE`] at the time the mark was taken.
    cap_pos: usize,
    /// Set by [`body_text`]; makes the `Drop` guard a no-op.
    closed: bool,
}

impl Drop for BodyMark {
    /// !!! RUST-ONLY !!! — a `YYERROR` return out of `par_funcdef`
    /// (c:Src/parse.c:1735 and friends) abandons the mark without asking
    /// for its text. Close the capture anyway so a syntax error in one
    /// body cannot leave the echo buffer recording into the next command.
    fn drop(&mut self) {
        if !self.closed {
            close_capture();
        }
    }
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// Open a function-body capture and return the mark to close it with.
/// Every character `hgetc` returns from now until the matching
/// [`body_text`] is echoed into [`LEX_SRC_CAPTURE`]. See that static for
/// why this exists.
pub fn body_mark_begin() -> BodyMark {
    let cap_pos = LEX_SRC_CAPTURE.with_borrow(|b| b.len());
    LEX_SRC_CAPTURE_DEPTH.set(LEX_SRC_CAPTURE_DEPTH.get() + 1);
    BodyMark {
        lex_pos: pos(),
        cap_pos,
        closed: false,
    }
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// Move an already-open mark forward to the current input position,
/// discarding what has been captured so far. The funcdef parsers take the
/// mark before `()` and then re-take it past every separator (`pos()` after
/// a `zshlex` is already one token ahead, so the mark has to be re-read
/// BEFORE each advance); this is that re-read for the capture side.
pub fn body_mark_restart(mark: &mut BodyMark) {
    mark.lex_pos = pos();
    mark.cap_pos = LEX_SRC_CAPTURE.with_borrow(|b| b.len());
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// Close the capture opened by [`body_mark_begin`] and return the raw body
/// text, untrimmed. `LEX_INPUT` wins when it actually advanced (the file /
/// `-c` / `eval` paths, whose slice behaviour is unchanged by this
/// helper); the `hgetc` echo buffer is the fallback for the interactive
/// and `-s` stdin paths, where `LEX_POS` never moves.
pub fn body_text(mut mark: BodyMark) -> Option<String> {
    let end = pos();
    mark.closed = true;
    let captured = captured_since(mark.cap_pos, CAP_ALIAS_NAME);
    close_capture();
    // The capture holds the body as the parse saw it — post alias expansion,
    // like the wordcode C stores (c:Src/lex.c:1928) — on every input source,
    // so it wins over the raw `LEX_INPUT` slice. The slice is the fallback for
    // a body whose characters never passed through `hgetc`.
    if !captured.is_empty() {
        return Some(captured);
    }
    if end > mark.lex_pos {
        if let Some(s) = input_slice(mark.lex_pos, end) {
            return Some(s);
        }
    }
    None
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// [`body_text`] for a top-level EVENT whose text will be parsed again
/// (loop()'s preexec `$2`/`$3`, src/ported/init.rs). Keeps the alias NAMES
/// and leaves out what their expansions pushed: the re-parse expands each
/// name again, so keeping both would render `ll x` (alias ll='print aliased')
/// as `llprint aliased x`. See [`LEX_SRC_CAPTURE_ALIAS`].
pub fn event_text(mut mark: BodyMark) -> Option<String> {
    let end = pos();
    mark.closed = true;
    let captured = captured_since(mark.cap_pos, CAP_ALIAS_TEXT);
    close_capture();
    if end > mark.lex_pos {
        if let Some(s) = input_slice(mark.lex_pos, end) {
            return Some(s);
        }
    }
    Some(captured).filter(|s| !s.is_empty())
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// Drop one level of capture nesting; returns the remaining depth.
fn close_capture() -> usize {
    let depth = LEX_SRC_CAPTURE_DEPTH.get().saturating_sub(1);
    LEX_SRC_CAPTURE_DEPTH.set(depth);
    if depth == 0 {
        LEX_SRC_CAPTURE.with_borrow_mut(|b| b.clear());
        LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|m| m.clear());
    }
    depth
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// Drop any capture left open by a parse that bailed out, so a syntax error
/// in one function body cannot leak text into the next one. Called from
/// `lex_init` (src/ported/lex.rs) alongside the other lexer-state resets;
/// the `BodyMark` `Drop` guard covers the ordinary error returns.
pub fn src_capture_reset() {
    LEX_SRC_CAPTURE_DEPTH.set(0);
    LEX_SRC_CAPTURE.with_borrow_mut(|b| b.clear());
    LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|m| m.clear());
    LEX_UNGET_SRCCAP.with_borrow_mut(|b| b.clear());
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// [`LEX_SRC_CAPTURE_ALIAS`] flag: an alias expansion pushed this character
/// (`inpush(an->text, INP_ALIAS, an)`, c:Src/lex.c:1928).
pub const CAP_ALIAS_TEXT: u8 = 1;
/// [`LEX_SRC_CAPTURE_ALIAS`] flag: this character belongs to an alias name the
/// lexer replaced with its expansion.
pub const CAP_ALIAS_NAME: u8 = 2;

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// The capture from byte offset `cap_pos` on, without the characters whose
/// flags intersect `drop`.
fn captured_since(cap_pos: usize, drop: u8) -> String {
    LEX_SRC_CAPTURE.with_borrow(|b| {
        // `hungetc` can pop a character that predates the mark, so clamp,
        // and land on a char boundary (a partially-rewound multibyte char
        // would otherwise panic the slice).
        let mut start = cap_pos.min(b.len());
        while start > 0 && !b.is_char_boundary(start) {
            start -= 1;
        }
        let skip = b[..start].chars().count();
        LEX_SRC_CAPTURE_ALIAS.with_borrow(|flags| {
            b[start..]
                .chars()
                .enumerate()
                .filter(|&(i, _)| flags.get(skip + i).copied().unwrap_or(0) & drop == 0)
                .map(|(_, c)| c)
                .collect()
        })
    })
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// Mark the alias name the lexer is about to replace (`inpush(an->text,
/// INP_ALIAS, an)`, c:Src/lex.c:1928, and the suffix-alias pushes at
/// c:1938-1940) as [`CAP_ALIAS_NAME`]. By then `hungetc` has already taken
/// back the character that ended the word, so the capture ends with `name`;
/// when it does not (capture closed, name not recorded) nothing is marked.
///
/// Inside a command substitution the name stays: skipcomm keeps the raw
/// text as the word (c:Src/lex.c:2253-2283), from which `inpoptop` has
/// taken the expansion back out (c:Src/input.c:751-752), so zsh lists
/// `$(ll)`. `hgetc` flags that expansion text for both renderings to drop.
pub fn src_capture_mark_alias_name(name: &str) {
    if LEX_SRC_CAPTURE_DEPTH.get() == 0 || name.is_empty() {
        return;
    }
    if crate::ported::lex::LEX_LEX_ADD_RAW.get() != 0
        && crate::ported::input::inbufflags.with(|f| f.get()) & crate::ported::zsh_h::INP_RAW_KEEP == 0
    {
        return;
    }
    LEX_SRC_CAPTURE.with_borrow(|b| {
        if b.ends_with(name) {
            let n = name.chars().count();
            LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|flags| {
                let len = flags.len();
                for f in &mut flags[len.saturating_sub(n)..] {
                    *f |= CAP_ALIAS_NAME;
                }
            });
        }
    });
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// Echo one consumed character into [`LEX_SRC_CAPTURE`] with its
/// [`LEX_SRC_CAPTURE_ALIAS`] flags. Returns whether it was recorded, so
/// `hungetc`'s pop and `hgetc`'s re-read stay symmetric. Called from `hgetc`
/// (src/ported/lex.rs) only.
pub fn src_capture_add(c: char, flags: u8) -> bool {
    if LEX_SRC_CAPTURE_DEPTH.get() == 0 {
        return false;
    }
    LEX_SRC_CAPTURE.with_borrow_mut(|b| b.push(c));
    LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|m| m.push(flags));
    true
}

/// !!! WARNING: RUST-ONLY HELPER !!!
///
/// Undo the last [`src_capture_add`], but only if the buffer really ends
/// with `c` — a character the `counts_lineno` gate refused was never
/// recorded and must not be taken from an earlier one. Returns the removed
/// character's flags, or `None` when nothing was removed, so the re-read in
/// `hgetc` restores both. Called from `hungetc` (src/ported/lex.rs) only.
pub fn src_capture_back(c: char) -> Option<u8> {
    if LEX_SRC_CAPTURE_DEPTH.get() == 0 {
        return None;
    }
    LEX_SRC_CAPTURE.with_borrow_mut(|b| {
        if b.ends_with(c) {
            b.pop();
            Some(LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|m| m.pop()).unwrap_or(0))
        } else {
            None
        }
    })
}