Skip to main content

zsh/extensions/
heredoc_ast.rs

1//! Heredoc AST-glue types — Rust-only, NOT in zsh C.
2//!
3//! zsh tracks pending heredocs via the `struct heredocs` linked-list
4//! node defined at `Src/zsh.h:1152-1157`:
5//!
6//! ```c
7//! struct heredocs {
8//!     struct heredocs *next;
9//!     int type;
10//!     int pc;
11//!     char *str;
12//! };
13//! ```
14//!
15//! The C model defers body collection — the parser records `pc`
16//! (wordcode offset) + `str` (terminator) at the `<<EOF` site, walks
17//! past the redirection emitting normal wordcode for the rest of the
18//! line, then `gethere()` (lex.c:1810) walks the linked list at
19//! newline and reads each body from the input stream into the
20//! wordcode buffer at the saved pc.
21//!
22//! zshrs's pre-wordcode parser collects each heredoc body inline
23//! during lex (no pc, no later resolution), so the live shape of
24//! per-heredoc state is different: `terminator`, `strip_tabs`,
25//! `content`, `quoted`, `processed`. The Vec position carries
26//! ordering (no `next` linked list).
27//!
28//! `HereDoc` is the AST-glue Vec entry the AST consumer
29//! (`fill_heredoc_bodies` in parse.rs) reads. The canonical
30//! `struct heredocs` linked list (parse.c:84) + `gethere()`
31//! (exec.c:4573) are ported as `parse::HDOCS` /
32//! `crate::exec::gethere`; the inline `zshlex()` NEWLIN walk
33//! (lex.c:278-306) writes body content into the next
34//! unprocessed `LEX_HEREDOCS` entry directly (no helper fn).
35//! `HereDocInfo` is the per-redir attachment that flows through
36//! the AST.
37
38use serde::{Deserialize, Serialize};
39
40/// Per-heredoc state collected by the lexer during `<<EOF` parsing.
41/// Held in the lexer-side `LEX_HEREDOCS: Vec<HereDoc>` thread_local
42/// for later attachment to `ZshRedir` entries (via `heredoc_idx`).
43///
44/// Rust-only AST-glue Vec — runs parallel to the canonical
45/// `struct heredocs *hdocs` linked list at `parse::HDOCS` (port of
46/// `Src/parse.c:84`). The inline NEWLIN walk in `zshlex()` drains
47/// both: it pops from HDOCS (the C-faithful list), calls `gethere`,
48/// then walks `LEX_HEREDOCS` to find the next entry with
49/// `processed == false` and writes the body there.
50#[derive(Debug, Clone)]
51pub struct HereDoc {
52    pub terminator: String,
53    pub strip_tabs: bool,
54    pub content: String,
55    /// True if the terminator was originally quoted (`<<'EOF'`,
56    /// `<<"EOF"`, or `<<\EOF`). Disables variable expansion / command
57    /// substitution / arithmetic in the body.
58    pub quoted: bool,
59    /// True once the inline NEWLIN walk in `zshlex()` has read the
60    /// body via `gethere`. Distinct from "content is empty" because
61    /// an empty heredoc legitimately has empty content.
62    pub processed: bool,
63}
64
65/// Heredoc body+metadata attached to a parsed `ZshRedir`. Carried
66/// through the AST and consumed by the compiler when emitting
67/// `Op::HereDoc(idx)` for the fusevm VM.
68///
69/// Rust-only — the wordcode track stores bodies as strs-region
70/// strings indexed by `WCB_REDIR` slot. The AST track keeps this
71/// per-redir attachment so the fusevm compiler can emit a
72/// `Op::HereDoc(idx)` referencing the body verbatim.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct HereDocInfo {
75    pub content: String,
76    pub terminator: String,
77    /// Originally-quoted terminator (`<<'EOF'`, `<<"EOF"`). When true
78    /// the body is passed verbatim — no `$var` / `$(cmd)` / `$((expr))`
79    /// expansion. Plain `<<EOF` runs all expansions.
80    #[serde(default)]
81    pub quoted: bool,
82}