zsh/extensions/funcdef_capture.rs
1//! !!! WARNING: RUST-ONLY MODULE — NOT A PORT OF ANY ZSH C FILE !!!
2//!
3//! Verbatim capture of function-body source text as the lexer consumes it.
4//!
5//! C zsh does not store function source at all. `par_funcdef`
6//! (Src/parse.c:1672) compiles the body into the `ecbuf` wordcode array,
7//! and `functions` / `typeset -f` / `whence -f` RECONSTRUCT the printed
8//! text from that wordcode via `getpermtext` -> `gettext2`
9//! (Src/text.c:189 / :296) — which is why zsh always prints canonical
10//! tab-indented text regardless of how the user spaced the original.
11//!
12//! zshrs runs function bodies through the fusevm compile path, which never
13//! builds a C-shaped `Eprog`; `shfunc.funcdef` (zsh_h.rs:868) is therefore
14//! always `None` and `printshfuncnode` (hashtable.rs:1796) prints the RAW
15//! SOURCE the parser captured instead. Until the fusevm path grows a real
16//! `Eprog`, that raw source has to come from somewhere for EVERY input
17//! source.
18//!
19//! It used to come only from a slice of the Rust-only `LEX_INPUT` window
20//! (`lex::input_slice`), which exists for file / `-c` / `eval` input.
21//! Interactive and `-s` stdin input reaches the lexer through
22//! `hgetc` -> `ingetc` -> the `inbuf` stack and never touches `LEX_INPUT`,
23//! so `LEX_POS` never moved and every function defined at a prompt or piped
24//! in on stdin printed as `name () { }`.
25//!
26//! C's own history-line buffer (`chline`) is NOT a usable substitute: at
27//! c:Src/hist.c:1119 `hbegin` sets
28//! `stophist = (!interact || unset(SHINSTDIN)) ? 2 : 0` and then c:1129
29//! `chline = hptr = NULL`, so for non-interactive stdin there is no history
30//! line at all. The one funnel every input source does pass through is
31//! `hgetc`, so this module hangs an echo buffer off it, active only between
32//! [`body_mark_begin`] and [`body_text`].
33//!
34//! Lives outside `src/ported/` because it has no C counterpart (same
35//! reasoning as `crate::tolerant_sort`).
36
37use crate::ported::lex::{input_slice, pos};
38
39thread_local! {
40/// Verbatim echo of the characters the lexer has consumed while at least
41/// one function-body capture is open. Fed by `hgetc` / `hungetc`
42/// (src/ported/lex.rs); see the module doc above for why it exists and why
43/// C's `chline` cannot stand in for it. Active only between
44/// [`body_mark_begin`] and [`body_text`].
45pub static LEX_SRC_CAPTURE: std::cell::RefCell<String>
46 = const { std::cell::RefCell::new(String::new()) };
47/// !!! WARNING: RUST-ONLY HELPER STATE !!!
48///
49/// Number of function-body captures currently open (nested `f() { g()
50/// { ... } }` opens two). Zero means `hgetc` records nothing.
51pub static LEX_SRC_CAPTURE_DEPTH: std::cell::Cell<usize>
52 = const { std::cell::Cell::new(0) };
53/// !!! WARNING: RUST-ONLY HELPER STATE !!!
54///
55/// One flag byte per character of [`LEX_SRC_CAPTURE`], built from
56/// [`CAP_ALIAS_TEXT`] and [`CAP_ALIAS_NAME`]. zsh compiles a function body
57/// AFTER alias expansion, so [`body_text`] keeps expansion text and drops the
58/// alias names it replaced; [`event_text`] does the reverse, because its
59/// re-parse expands each alias name again.
60pub static LEX_SRC_CAPTURE_ALIAS: std::cell::RefCell<Vec<u8>>
61 = const { std::cell::RefCell::new(Vec::new()) };
62/// !!! WARNING: RUST-ONLY HELPER STATE !!!
63///
64/// Lockstep companion to `lex::LEX_UNGET_BUF`, exactly like
65/// `lex::LEX_UNGET_HPTR`: one entry per queued character, `Some(flags)` when
66/// `hungetc` removed it from [`LEX_SRC_CAPTURE`] (carrying its
67/// [`LEX_SRC_CAPTURE_ALIAS`] flags) and `None` when it did not, so the
68/// matching re-read in `hgetc` puts back exactly what was taken. Without it a
69/// capture that opens or closes between the unget and the re-read
70/// gains or loses one character.
71pub static LEX_UNGET_SRCCAP: std::cell::RefCell<std::collections::VecDeque<Option<u8>>>
72 = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
73}
74
75/// !!! WARNING: RUST-ONLY HELPER !!!
76///
77/// A parser-side bookmark for "where the text of a function body starts".
78/// C has no analogue — `par_funcdef` (Src/parse.c:1672) writes the body
79/// into the `ecbuf` wordcode array and `functions` re-derives the printed
80/// text from it (`getpermtext`, Src/text.c:189). zshrs's fusevm path never
81/// builds that wordcode `Eprog`, so the parser must keep the raw source.
82///
83/// Carries BOTH cursors because zshrs has two input sources: the Rust-only
84/// `LEX_INPUT` window (file / `-c` / `eval` text) and the ported `inbuf`
85/// stack that `hgetc` reads for interactive and `-s` stdin input. See
86/// [`LEX_SRC_CAPTURE`] for why the second one needs an echo buffer.
87#[derive(Debug)]
88pub struct BodyMark {
89 /// `LEX_POS` at the time the mark was taken.
90 lex_pos: usize,
91 /// Offset into [`LEX_SRC_CAPTURE`] at the time the mark was taken.
92 cap_pos: usize,
93 /// Set by [`body_text`]; makes the `Drop` guard a no-op.
94 closed: bool,
95}
96
97impl Drop for BodyMark {
98 /// !!! RUST-ONLY !!! — a `YYERROR` return out of `par_funcdef`
99 /// (c:Src/parse.c:1735 and friends) abandons the mark without asking
100 /// for its text. Close the capture anyway so a syntax error in one
101 /// body cannot leave the echo buffer recording into the next command.
102 fn drop(&mut self) {
103 if !self.closed {
104 close_capture();
105 }
106 }
107}
108
109/// !!! WARNING: RUST-ONLY HELPER !!!
110///
111/// Open a function-body capture and return the mark to close it with.
112/// Every character `hgetc` returns from now until the matching
113/// [`body_text`] is echoed into [`LEX_SRC_CAPTURE`]. See that static for
114/// why this exists.
115pub fn body_mark_begin() -> BodyMark {
116 let cap_pos = LEX_SRC_CAPTURE.with_borrow(|b| b.len());
117 LEX_SRC_CAPTURE_DEPTH.set(LEX_SRC_CAPTURE_DEPTH.get() + 1);
118 BodyMark {
119 lex_pos: pos(),
120 cap_pos,
121 closed: false,
122 }
123}
124
125/// !!! WARNING: RUST-ONLY HELPER !!!
126///
127/// Move an already-open mark forward to the current input position,
128/// discarding what has been captured so far. The funcdef parsers take the
129/// mark before `()` and then re-take it past every separator (`pos()` after
130/// a `zshlex` is already one token ahead, so the mark has to be re-read
131/// BEFORE each advance); this is that re-read for the capture side.
132pub fn body_mark_restart(mark: &mut BodyMark) {
133 mark.lex_pos = pos();
134 mark.cap_pos = LEX_SRC_CAPTURE.with_borrow(|b| b.len());
135}
136
137/// !!! WARNING: RUST-ONLY HELPER !!!
138///
139/// Close the capture opened by [`body_mark_begin`] and return the raw body
140/// text, untrimmed. `LEX_INPUT` wins when it actually advanced (the file /
141/// `-c` / `eval` paths, whose slice behaviour is unchanged by this
142/// helper); the `hgetc` echo buffer is the fallback for the interactive
143/// and `-s` stdin paths, where `LEX_POS` never moves.
144pub fn body_text(mut mark: BodyMark) -> Option<String> {
145 let end = pos();
146 mark.closed = true;
147 let captured = captured_since(mark.cap_pos, CAP_ALIAS_NAME);
148 close_capture();
149 // The capture holds the body as the parse saw it — post alias expansion,
150 // like the wordcode C stores (c:Src/lex.c:1928) — on every input source,
151 // so it wins over the raw `LEX_INPUT` slice. The slice is the fallback for
152 // a body whose characters never passed through `hgetc`.
153 if !captured.is_empty() {
154 return Some(captured);
155 }
156 if end > mark.lex_pos {
157 if let Some(s) = input_slice(mark.lex_pos, end) {
158 return Some(s);
159 }
160 }
161 None
162}
163
164/// !!! WARNING: RUST-ONLY HELPER !!!
165///
166/// [`body_text`] for a top-level EVENT whose text will be parsed again
167/// (loop()'s preexec `$2`/`$3`, src/ported/init.rs). Keeps the alias NAMES
168/// and leaves out what their expansions pushed: the re-parse expands each
169/// name again, so keeping both would render `ll x` (alias ll='print aliased')
170/// as `llprint aliased x`. See [`LEX_SRC_CAPTURE_ALIAS`].
171pub fn event_text(mut mark: BodyMark) -> Option<String> {
172 let end = pos();
173 mark.closed = true;
174 let captured = captured_since(mark.cap_pos, CAP_ALIAS_TEXT);
175 close_capture();
176 if end > mark.lex_pos {
177 if let Some(s) = input_slice(mark.lex_pos, end) {
178 return Some(s);
179 }
180 }
181 Some(captured).filter(|s| !s.is_empty())
182}
183
184/// !!! WARNING: RUST-ONLY HELPER !!!
185///
186/// Drop one level of capture nesting; returns the remaining depth.
187fn close_capture() -> usize {
188 let depth = LEX_SRC_CAPTURE_DEPTH.get().saturating_sub(1);
189 LEX_SRC_CAPTURE_DEPTH.set(depth);
190 if depth == 0 {
191 LEX_SRC_CAPTURE.with_borrow_mut(|b| b.clear());
192 LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|m| m.clear());
193 }
194 depth
195}
196
197/// !!! WARNING: RUST-ONLY HELPER !!!
198///
199/// Drop any capture left open by a parse that bailed out, so a syntax error
200/// in one function body cannot leak text into the next one. Called from
201/// `lex_init` (src/ported/lex.rs) alongside the other lexer-state resets;
202/// the `BodyMark` `Drop` guard covers the ordinary error returns.
203pub fn src_capture_reset() {
204 LEX_SRC_CAPTURE_DEPTH.set(0);
205 LEX_SRC_CAPTURE.with_borrow_mut(|b| b.clear());
206 LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|m| m.clear());
207 LEX_UNGET_SRCCAP.with_borrow_mut(|b| b.clear());
208}
209
210/// !!! WARNING: RUST-ONLY HELPER !!!
211///
212/// [`LEX_SRC_CAPTURE_ALIAS`] flag: an alias expansion pushed this character
213/// (`inpush(an->text, INP_ALIAS, an)`, c:Src/lex.c:1928).
214pub const CAP_ALIAS_TEXT: u8 = 1;
215/// [`LEX_SRC_CAPTURE_ALIAS`] flag: this character belongs to an alias name the
216/// lexer replaced with its expansion.
217pub const CAP_ALIAS_NAME: u8 = 2;
218
219/// !!! WARNING: RUST-ONLY HELPER !!!
220///
221/// The capture from byte offset `cap_pos` on, without the characters whose
222/// flags intersect `drop`.
223fn captured_since(cap_pos: usize, drop: u8) -> String {
224 LEX_SRC_CAPTURE.with_borrow(|b| {
225 // `hungetc` can pop a character that predates the mark, so clamp,
226 // and land on a char boundary (a partially-rewound multibyte char
227 // would otherwise panic the slice).
228 let mut start = cap_pos.min(b.len());
229 while start > 0 && !b.is_char_boundary(start) {
230 start -= 1;
231 }
232 let skip = b[..start].chars().count();
233 LEX_SRC_CAPTURE_ALIAS.with_borrow(|flags| {
234 b[start..]
235 .chars()
236 .enumerate()
237 .filter(|&(i, _)| flags.get(skip + i).copied().unwrap_or(0) & drop == 0)
238 .map(|(_, c)| c)
239 .collect()
240 })
241 })
242}
243
244/// !!! WARNING: RUST-ONLY HELPER !!!
245///
246/// Mark the alias name the lexer is about to replace (`inpush(an->text,
247/// INP_ALIAS, an)`, c:Src/lex.c:1928, and the suffix-alias pushes at
248/// c:1938-1940) as [`CAP_ALIAS_NAME`]. By then `hungetc` has already taken
249/// back the character that ended the word, so the capture ends with `name`;
250/// when it does not (capture closed, name not recorded) nothing is marked.
251///
252/// Inside a command substitution the name stays: skipcomm keeps the raw
253/// text as the word (c:Src/lex.c:2253-2283), from which `inpoptop` has
254/// taken the expansion back out (c:Src/input.c:751-752), so zsh lists
255/// `$(ll)`. `hgetc` flags that expansion text for both renderings to drop.
256pub fn src_capture_mark_alias_name(name: &str) {
257 if LEX_SRC_CAPTURE_DEPTH.get() == 0 || name.is_empty() {
258 return;
259 }
260 if crate::ported::lex::LEX_LEX_ADD_RAW.get() != 0
261 && crate::ported::input::inbufflags.with(|f| f.get()) & crate::ported::zsh_h::INP_RAW_KEEP == 0
262 {
263 return;
264 }
265 LEX_SRC_CAPTURE.with_borrow(|b| {
266 if b.ends_with(name) {
267 let n = name.chars().count();
268 LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|flags| {
269 let len = flags.len();
270 for f in &mut flags[len.saturating_sub(n)..] {
271 *f |= CAP_ALIAS_NAME;
272 }
273 });
274 }
275 });
276}
277
278/// !!! WARNING: RUST-ONLY HELPER !!!
279///
280/// Echo one consumed character into [`LEX_SRC_CAPTURE`] with its
281/// [`LEX_SRC_CAPTURE_ALIAS`] flags. Returns whether it was recorded, so
282/// `hungetc`'s pop and `hgetc`'s re-read stay symmetric. Called from `hgetc`
283/// (src/ported/lex.rs) only.
284pub fn src_capture_add(c: char, flags: u8) -> bool {
285 if LEX_SRC_CAPTURE_DEPTH.get() == 0 {
286 return false;
287 }
288 LEX_SRC_CAPTURE.with_borrow_mut(|b| b.push(c));
289 LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|m| m.push(flags));
290 true
291}
292
293/// !!! WARNING: RUST-ONLY HELPER !!!
294///
295/// Undo the last [`src_capture_add`], but only if the buffer really ends
296/// with `c` — a character the `counts_lineno` gate refused was never
297/// recorded and must not be taken from an earlier one. Returns the removed
298/// character's flags, or `None` when nothing was removed, so the re-read in
299/// `hgetc` restores both. Called from `hungetc` (src/ported/lex.rs) only.
300pub fn src_capture_back(c: char) -> Option<u8> {
301 if LEX_SRC_CAPTURE_DEPTH.get() == 0 {
302 return None;
303 }
304 LEX_SRC_CAPTURE.with_borrow_mut(|b| {
305 if b.ends_with(c) {
306 b.pop();
307 Some(LEX_SRC_CAPTURE_ALIAS.with_borrow_mut(|m| m.pop()).unwrap_or(0))
308 } else {
309 None
310 }
311 })
312}