Skip to main content

shuck_semantic/cfg/
recorded.rs

1//! Compact command IR recorded during semantic traversal.
2//!
3//! Semantic traversal walks the parsed shell syntax once and records the pieces
4//! CFG construction needs later. For example:
5//!
6//! ```sh
7//! prepare && deploy | tee log
8//! ```
9//!
10//! is stored as a logical-list command with ordered list items, and the pipeline
11//! is stored with ordered segment records and per-segment scopes. Keeping that
12//! compact representation here lets CFG construction, command topology, and
13//! zsh option analysis share the same command identities and spans instead of
14//! each reinterpreting parser nodes.
15
16use rustc_hash::FxHashMap;
17use shuck_ast::{BuiltinCommand, CaseTerminator, Command, CompoundCommand, Span};
18use shuck_parser::ZshEmulationMode;
19use std::marker::PhantomData;
20
21use crate::cfg::FlowContext;
22use crate::source_closure::SourcePathTemplate;
23use crate::{BindingId, ScopeId, SpanKey};
24
25#[derive(Debug, Clone, Default)]
26pub(crate) struct RecordedProgram {
27    file_commands: RecordedCommandRange,
28    function_bodies: FxHashMap<ScopeId, RecordedCommandRange>,
29    commands: Vec<RecordedCommand>,
30    command_sequence_items: Vec<CommandId>,
31    statement_sequence_commands: Vec<StatementSequenceCommand>,
32    isolated_regions: Vec<IsolatedRegion>,
33    case_arms: Vec<RecordedCaseArm>,
34    pipeline_segments: Vec<RecordedPipelineSegment>,
35    list_items: Vec<RecordedListItem>,
36    elif_branches: Vec<RecordedElifBranch>,
37    command_info_records: Vec<RecordedCommandInfo>,
38    pub(crate) command_infos: FxHashMap<SpanKey, RecordedCommandInfoId>,
39    pub(crate) function_body_scopes: FxHashMap<BindingId, ScopeId>,
40    pub(crate) call_command_spans: FxHashMap<SpanKey, Span>,
41}
42
43/// A statement-sequence item flattened out of structured syntax.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct StatementSequenceCommand {
46    body_span: Span,
47    stmt_span: Span,
48}
49
50impl StatementSequenceCommand {
51    /// Returns the span of the command body without trailing statement punctuation.
52    pub fn body_span(&self) -> Span {
53        self.body_span
54    }
55
56    /// Returns the span of the full statement item.
57    pub fn stmt_span(&self) -> Span {
58        self.stmt_span
59    }
60}
61
62/// Stable identifier for a semantic command in the recorded command stream.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub struct CommandId(pub(crate) u32);
65
66impl CommandId {
67    /// Returns the zero-based index used by internal command-storage vectors.
68    pub fn index(self) -> usize {
69        self.0 as usize
70    }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub(crate) struct RecordedCommandInfoId(u32);
75
76impl RecordedCommandInfoId {
77    fn index(self) -> usize {
78        self.0 as usize
79    }
80}
81
82/// High-level command category recorded by semantic traversal.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub enum CommandKind {
85    /// A simple command.
86    Simple,
87    /// A builtin command with one of the tracked control-flow forms.
88    Builtin(BuiltinCommandKind),
89    /// A declaration builtin command.
90    Decl,
91    /// A binary command such as `[[ ... ]]` or `(( ... ))`.
92    Binary,
93    /// A compound command.
94    Compound(CompoundCommandKind),
95    /// A named function definition.
96    Function,
97    /// An anonymous function-like definition.
98    AnonymousFunction,
99}
100
101impl CommandKind {
102    /// Classifies a parsed AST command into a semantic command kind.
103    pub fn from_command(command: &Command) -> Self {
104        match command {
105            Command::Simple(_) => Self::Simple,
106            Command::Builtin(command) => Self::Builtin(BuiltinCommandKind::from_builtin(command)),
107            Command::Decl(_) => Self::Decl,
108            Command::Binary(_) => Self::Binary,
109            Command::Compound(command) => {
110                Self::Compound(CompoundCommandKind::from_compound(command))
111            }
112            Command::Function(_) => Self::Function,
113            Command::AnonymousFunction(_) => Self::AnonymousFunction,
114        }
115    }
116}
117
118/// Builtin command kinds that affect control flow directly.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
120pub enum BuiltinCommandKind {
121    /// `break`
122    Break,
123    /// `continue`
124    Continue,
125    /// `return`
126    Return,
127    /// `exit`
128    Exit,
129}
130
131impl BuiltinCommandKind {
132    fn from_builtin(command: &BuiltinCommand) -> Self {
133        match command {
134            BuiltinCommand::Break(_) => Self::Break,
135            BuiltinCommand::Continue(_) => Self::Continue,
136            BuiltinCommand::Return(_) => Self::Return,
137            BuiltinCommand::Exit(_) => Self::Exit,
138        }
139    }
140}
141
142/// Compound command kinds tracked by semantic analysis.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub enum CompoundCommandKind {
145    /// `if`
146    If,
147    /// `for`
148    For,
149    /// `repeat`
150    Repeat,
151    /// `foreach`
152    Foreach,
153    /// Arithmetic `for (( ...; ...; ... ))`
154    ArithmeticFor,
155    /// `while`
156    While,
157    /// `until`
158    Until,
159    /// `case`
160    Case,
161    /// `select`
162    Select,
163    /// Subshell `( ... )`
164    Subshell,
165    /// Brace group `{ ...; }`
166    BraceGroup,
167    /// Arithmetic command.
168    Arithmetic,
169    /// `time`
170    Time,
171    /// Conditional command such as `[[ ... ]]`.
172    Conditional,
173    /// `coproc`
174    Coproc,
175    /// `always`
176    Always,
177}
178
179impl CompoundCommandKind {
180    fn from_compound(command: &CompoundCommand) -> Self {
181        match command {
182            CompoundCommand::If(_) => Self::If,
183            CompoundCommand::For(_) => Self::For,
184            CompoundCommand::Repeat(_) => Self::Repeat,
185            CompoundCommand::Foreach(_) => Self::Foreach,
186            CompoundCommand::ArithmeticFor(_) => Self::ArithmeticFor,
187            CompoundCommand::While(_) => Self::While,
188            CompoundCommand::Until(_) => Self::Until,
189            CompoundCommand::Case(_) => Self::Case,
190            CompoundCommand::Select(_) => Self::Select,
191            CompoundCommand::Subshell(_) => Self::Subshell,
192            CompoundCommand::BraceGroup(_) => Self::BraceGroup,
193            CompoundCommand::Arithmetic(_) => Self::Arithmetic,
194            CompoundCommand::Time(_) => Self::Time,
195            CompoundCommand::Conditional(_) => Self::Conditional,
196            CompoundCommand::Coproc(_) => Self::Coproc,
197            CompoundCommand::Always(_) => Self::Always,
198        }
199    }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub(crate) struct RecordedRange<T> {
204    start: u32,
205    len: u32,
206    marker: PhantomData<fn() -> T>,
207}
208
209impl<T> RecordedRange<T> {
210    pub(crate) const fn empty() -> Self {
211        Self {
212            start: 0,
213            len: 0,
214            marker: PhantomData,
215        }
216    }
217
218    pub(crate) fn new(start: usize, len: usize) -> Self {
219        Self {
220            start: match u32::try_from(start) {
221                Ok(start) => start,
222                Err(err) => panic!("recorded IR start fits in u32: {err}"),
223            },
224            len: match u32::try_from(len) {
225                Ok(len) => len,
226                Err(err) => panic!("recorded IR length fits in u32: {err}"),
227            },
228            marker: PhantomData,
229        }
230    }
231
232    pub(crate) fn len(self) -> usize {
233        self.len as usize
234    }
235
236    pub(crate) fn is_empty(&self) -> bool {
237        self.len == 0
238    }
239
240    fn slice(self, store: &[T]) -> &[T] {
241        let start = self.start as usize;
242        &store[start..start + self.len()]
243    }
244}
245
246impl<T> Default for RecordedRange<T> {
247    fn default() -> Self {
248        Self::empty()
249    }
250}
251
252pub(crate) type RecordedCommandRange = RecordedRange<CommandId>;
253pub(crate) type RecordedRegionRange = RecordedRange<IsolatedRegion>;
254pub(crate) type RecordedCaseArmRange = RecordedRange<RecordedCaseArm>;
255pub(crate) type RecordedPipelineSegmentRange = RecordedRange<RecordedPipelineSegment>;
256pub(crate) type RecordedListItemRange = RecordedRange<RecordedListItem>;
257pub(crate) type RecordedElifBranchRange = RecordedRange<RecordedElifBranch>;
258
259#[derive(Debug, Clone, Copy)]
260pub(crate) struct RecordedCommand {
261    pub(crate) span: Span,
262    pub(crate) syntax_span: Span,
263    pub(crate) syntax_kind: Option<CommandKind>,
264    pub(crate) scope: Option<ScopeId>,
265    pub(crate) flow_context: Option<FlowContext>,
266    pub(crate) nested_regions: RecordedRegionRange,
267    pub(crate) command_info: Option<RecordedCommandInfoId>,
268    pub(crate) kind: RecordedCommandKind,
269}
270
271#[derive(Debug, Clone, Copy)]
272pub(crate) struct IsolatedRegion {
273    pub(crate) scope: ScopeId,
274    pub(crate) commands: RecordedCommandRange,
275}
276
277#[derive(Debug, Clone, Copy)]
278pub(crate) enum RecordedCommandKind {
279    Linear,
280    Break {
281        depth: usize,
282    },
283    Continue {
284        depth: usize,
285    },
286    Return,
287    Exit,
288    List {
289        first: CommandId,
290        rest: RecordedListItemRange,
291    },
292    If {
293        condition: RecordedCommandRange,
294        then_branch: RecordedCommandRange,
295        elif_branches: RecordedElifBranchRange,
296        else_branch: RecordedCommandRange,
297    },
298    While {
299        condition: RecordedCommandRange,
300        body: RecordedCommandRange,
301    },
302    Until {
303        condition: RecordedCommandRange,
304        body: RecordedCommandRange,
305    },
306    For {
307        body: RecordedCommandRange,
308    },
309    Select {
310        body: RecordedCommandRange,
311    },
312    ArithmeticFor {
313        body: RecordedCommandRange,
314    },
315    Case {
316        arms: RecordedCaseArmRange,
317    },
318    BraceGroup {
319        body: RecordedCommandRange,
320    },
321    Always {
322        body: RecordedCommandRange,
323        always_body: RecordedCommandRange,
324    },
325    Subshell {
326        body: RecordedCommandRange,
327    },
328    Pipeline {
329        segments: RecordedPipelineSegmentRange,
330    },
331}
332
333#[derive(Debug, Clone, Copy)]
334pub(crate) struct RecordedCaseArm {
335    pub(crate) terminator: CaseTerminator,
336    pub(crate) matches_anything: bool,
337    pub(crate) commands: RecordedCommandRange,
338}
339
340#[derive(Debug, Clone, Copy)]
341pub(crate) struct RecordedPipelineSegment {
342    pub(crate) operator_before: Option<RecordedPipelineOperator>,
343    pub(crate) scope: ScopeId,
344    pub(crate) command: CommandId,
345}
346
347#[derive(Debug, Clone, Copy)]
348pub(crate) struct RecordedListItem {
349    pub(crate) operator: RecordedListOperator,
350    pub(crate) operator_span: Span,
351    pub(crate) command: CommandId,
352}
353
354#[derive(Debug, Clone, Copy)]
355pub(crate) struct RecordedPipelineOperator {
356    pub(crate) operator: RecordedPipelineOperatorKind,
357    pub(crate) span: Span,
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
361pub(crate) enum RecordedPipelineOperatorKind {
362    Pipe,
363    PipeAll,
364}
365
366#[derive(Debug, Clone, Copy)]
367pub(crate) struct RecordedElifBranch {
368    pub(crate) condition: RecordedCommandRange,
369    pub(crate) body: RecordedCommandRange,
370}
371
372#[derive(Debug, Clone, Default)]
373pub(crate) struct RecordedCommandInfo {
374    pub(crate) static_callee: Option<compact_str::CompactString>,
375    pub(crate) dynamic_name_span: Option<Span>,
376    pub(crate) static_args: Box<[Option<String>]>,
377    pub(crate) source_path_template: Option<SourcePathTemplate>,
378    pub(crate) zsh_effects: Vec<RecordedZshCommandEffect>,
379}
380
381impl RecordedCommandInfo {
382    pub(crate) fn is_empty(&self) -> bool {
383        self.static_callee.is_none()
384            && self.dynamic_name_span.is_none()
385            && self.static_args.is_empty()
386            && self.source_path_template.is_none()
387            && self.zsh_effects.is_empty()
388    }
389}
390
391#[derive(Debug, Clone)]
392pub(crate) enum RecordedZshCommandEffect {
393    Emulate {
394        mode: ZshEmulationMode,
395        local: bool,
396    },
397    EmulateUnknown {
398        local: bool,
399    },
400    SetOptions {
401        updates: Vec<RecordedZshOptionUpdate>,
402    },
403}
404
405#[derive(Debug, Clone, PartialEq, Eq)]
406pub(crate) enum RecordedZshOptionUpdate {
407    Named { name: Box<str>, enable: bool },
408    UnknownName,
409    LocalOptions { enable: bool },
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
413pub(crate) enum RecordedListOperator {
414    And,
415    Or,
416}
417
418impl RecordedProgram {
419    pub(crate) fn file_commands(&self) -> RecordedCommandRange {
420        self.file_commands
421    }
422
423    pub(crate) fn set_file_commands(&mut self, commands: RecordedCommandRange) {
424        self.file_commands = commands;
425    }
426
427    pub(crate) fn function_body(&self, scope: ScopeId) -> RecordedCommandRange {
428        self.function_bodies
429            .get(&scope)
430            .copied()
431            .unwrap_or_default()
432    }
433
434    pub(crate) fn set_function_body(&mut self, scope: ScopeId, commands: RecordedCommandRange) {
435        self.function_bodies.insert(scope, commands);
436    }
437
438    pub(crate) fn function_bodies(&self) -> &FxHashMap<ScopeId, RecordedCommandRange> {
439        &self.function_bodies
440    }
441
442    pub(crate) fn command(&self, id: CommandId) -> &RecordedCommand {
443        &self.commands[id.index()]
444    }
445
446    pub(crate) fn command_mut(&mut self, id: CommandId) -> &mut RecordedCommand {
447        &mut self.commands[id.index()]
448    }
449
450    pub(crate) fn command_info(&self, id: RecordedCommandInfoId) -> &RecordedCommandInfo {
451        &self.command_info_records[id.index()]
452    }
453
454    pub(crate) fn command_info_for_span(&self, span: Span) -> Option<&RecordedCommandInfo> {
455        self.command_infos
456            .get(&SpanKey::new(span))
457            .map(|id| self.command_info(*id))
458    }
459
460    pub(crate) fn commands(&self) -> &[RecordedCommand] {
461        &self.commands
462    }
463
464    pub(crate) fn commands_in(&self, range: RecordedCommandRange) -> &[CommandId] {
465        range.slice(&self.command_sequence_items)
466    }
467
468    pub fn statement_sequence_commands(&self) -> &[StatementSequenceCommand] {
469        &self.statement_sequence_commands
470    }
471
472    pub(crate) fn push_statement_sequence_command(&mut self, body_span: Span, stmt_span: Span) {
473        self.statement_sequence_commands
474            .push(StatementSequenceCommand {
475                body_span,
476                stmt_span,
477            });
478    }
479
480    pub(crate) fn nested_regions(&self, range: RecordedRegionRange) -> &[IsolatedRegion] {
481        range.slice(&self.isolated_regions)
482    }
483
484    pub(crate) fn case_arms(&self, range: RecordedCaseArmRange) -> &[RecordedCaseArm] {
485        range.slice(&self.case_arms)
486    }
487
488    pub(crate) fn pipeline_segments(
489        &self,
490        range: RecordedPipelineSegmentRange,
491    ) -> &[RecordedPipelineSegment] {
492        range.slice(&self.pipeline_segments)
493    }
494
495    pub(crate) fn list_items(&self, range: RecordedListItemRange) -> &[RecordedListItem] {
496        range.slice(&self.list_items)
497    }
498
499    pub(crate) fn elif_branches(&self, range: RecordedElifBranchRange) -> &[RecordedElifBranch] {
500        range.slice(&self.elif_branches)
501    }
502
503    pub(crate) fn push_command(&mut self, command: RecordedCommand) -> CommandId {
504        let id = CommandId(match u32::try_from(self.commands.len()) {
505            Ok(id) => id,
506            Err(err) => panic!("recorded command count fits in u32: {err}"),
507        });
508        self.commands.push(command);
509        id
510    }
511
512    pub(crate) fn push_command_info(&mut self, info: RecordedCommandInfo) -> RecordedCommandInfoId {
513        let id = RecordedCommandInfoId(match u32::try_from(self.command_info_records.len()) {
514            Ok(id) => id,
515            Err(err) => panic!("recorded command info count fits in u32: {err}"),
516        });
517        self.command_info_records.push(info);
518        id
519    }
520
521    pub(crate) fn push_command_ids(&mut self, command_ids: Vec<CommandId>) -> RecordedCommandRange {
522        push_range(&mut self.command_sequence_items, command_ids)
523    }
524
525    pub(crate) fn push_regions(&mut self, regions: Vec<IsolatedRegion>) -> RecordedRegionRange {
526        push_range(&mut self.isolated_regions, regions)
527    }
528
529    pub(crate) fn push_case_arms(&mut self, arms: Vec<RecordedCaseArm>) -> RecordedCaseArmRange {
530        push_range(&mut self.case_arms, arms)
531    }
532
533    pub(crate) fn push_pipeline_segments(
534        &mut self,
535        segments: Vec<RecordedPipelineSegment>,
536    ) -> RecordedPipelineSegmentRange {
537        push_range(&mut self.pipeline_segments, segments)
538    }
539
540    pub(crate) fn push_list_items(
541        &mut self,
542        list_items: Vec<RecordedListItem>,
543    ) -> RecordedListItemRange {
544        push_range(&mut self.list_items, list_items)
545    }
546
547    pub(crate) fn push_elif_branches(
548        &mut self,
549        elif_branches: Vec<RecordedElifBranch>,
550    ) -> RecordedElifBranchRange {
551        push_range(&mut self.elif_branches, elif_branches)
552    }
553}
554
555fn push_range<T>(store: &mut Vec<T>, mut items: Vec<T>) -> RecordedRange<T> {
556    if items.is_empty() {
557        return RecordedRange::empty();
558    }
559
560    let start = store.len();
561    store.append(&mut items);
562    RecordedRange::new(start, store.len() - start)
563}