Skip to main content

verificar/generator/
bash_enum.rs

1//! Bash exhaustive enumeration
2//!
3//! Generates all valid Bash programs up to a specified AST depth.
4//! Uses a simplified Bash grammar for combinatorial generation.
5
6#![allow(clippy::redundant_closure_for_method_calls)]
7#![allow(clippy::uninlined_format_args)]
8#![allow(clippy::match_same_arms)]
9#![allow(clippy::format_collect)]
10#![allow(clippy::inefficient_to_string)]
11
12use super::GeneratedCode;
13use crate::Language;
14
15/// Bash AST node types for generation
16#[derive(Debug, Clone, PartialEq)]
17#[allow(missing_docs)]
18pub enum BashNode {
19    /// Script (root node with optional shebang)
20    Script {
21        /// Optional shebang line
22        shebang: Option<String>,
23        /// Script body statements
24        body: Vec<BashNode>,
25    },
26    /// Variable assignment: `name=value`
27    Assignment {
28        /// Variable name
29        name: String,
30        /// Value expression
31        value: Box<BashNode>,
32    },
33    /// Command execution
34    Command {
35        /// Command name
36        name: String,
37        /// Command arguments
38        args: Vec<BashNode>,
39    },
40    /// If statement
41    If {
42        /// Condition (test command)
43        condition: Box<BashNode>,
44        /// Then body
45        then_body: Vec<BashNode>,
46        /// Optional else body
47        else_body: Vec<BashNode>,
48    },
49    /// For loop
50    For {
51        /// Loop variable name
52        var: String,
53        /// Iterable (words)
54        items: Vec<BashNode>,
55        /// Loop body
56        body: Vec<BashNode>,
57    },
58    /// While loop
59    While {
60        /// Condition
61        condition: Box<BashNode>,
62        /// Loop body
63        body: Vec<BashNode>,
64    },
65    /// Function definition
66    Function {
67        /// Function name
68        name: String,
69        /// Function body
70        body: Vec<BashNode>,
71    },
72    /// Case statement
73    Case {
74        /// Value to match
75        value: Box<BashNode>,
76        /// Pattern-body pairs
77        patterns: Vec<(String, Vec<BashNode>)>,
78    },
79    /// Test expression: `[ expr ]` or `[[ expr ]]`
80    Test {
81        /// Use double brackets `[[`
82        double: bool,
83        /// Test expression
84        expr: Box<BashNode>,
85    },
86    /// Binary comparison: `left op right`
87    Compare {
88        /// Left operand
89        left: Box<BashNode>,
90        /// Comparison operator
91        op: BashCompareOp,
92        /// Right operand
93        right: Box<BashNode>,
94    },
95    /// Arithmetic expression: `$((expr))`
96    Arithmetic(Box<BashNode>),
97    /// Binary arithmetic operation
98    ArithOp {
99        /// Left operand
100        left: Box<BashNode>,
101        /// Operator
102        op: BashArithOp,
103        /// Right operand
104        right: Box<BashNode>,
105    },
106    /// Variable reference: `$name` or `${name}`
107    Variable(String),
108    /// String literal
109    StringLit(String),
110    /// Integer literal
111    IntLit(i64),
112    /// Array literal
113    Array(Vec<BashNode>),
114    /// Pipe: `cmd1 | cmd2`
115    Pipe {
116        /// Left command
117        left: Box<BashNode>,
118        /// Right command
119        right: Box<BashNode>,
120    },
121    /// Command substitution: `$(cmd)`
122    CommandSubst(Box<BashNode>),
123    /// Redirection
124    Redirect {
125        /// Command
126        command: Box<BashNode>,
127        /// Redirect type
128        redirect_type: RedirectType,
129        /// Target file/fd
130        target: String,
131    },
132}
133
134/// Bash comparison operators
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum BashCompareOp {
137    /// Numeric equal `-eq`
138    NumEq,
139    /// Numeric not equal `-ne`
140    NumNe,
141    /// Numeric less than `-lt`
142    NumLt,
143    /// Numeric greater than `-gt`
144    NumGt,
145    /// Numeric less than or equal `-le`
146    NumLe,
147    /// Numeric greater than or equal `-ge`
148    NumGe,
149    /// String equal `=` or `==`
150    StrEq,
151    /// String not equal `!=`
152    StrNe,
153    /// String less than `<`
154    StrLt,
155    /// String greater than `>`
156    StrGt,
157}
158
159impl BashCompareOp {
160    /// Get all comparison operators
161    #[must_use]
162    pub fn all() -> &'static [Self] {
163        &[
164            Self::NumEq,
165            Self::NumNe,
166            Self::NumLt,
167            Self::NumGt,
168            Self::NumLe,
169            Self::NumGe,
170            Self::StrEq,
171            Self::StrNe,
172        ]
173    }
174
175    /// Convert to Bash operator string
176    #[must_use]
177    pub fn to_str(self) -> &'static str {
178        match self {
179            Self::NumEq => "-eq",
180            Self::NumNe => "-ne",
181            Self::NumLt => "-lt",
182            Self::NumGt => "-gt",
183            Self::NumLe => "-le",
184            Self::NumGe => "-ge",
185            Self::StrEq => "==",
186            Self::StrNe => "!=",
187            Self::StrLt => "<",
188            Self::StrGt => ">",
189        }
190    }
191}
192
193/// Bash arithmetic operators
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum BashArithOp {
196    /// Addition `+`
197    Add,
198    /// Subtraction `-`
199    Sub,
200    /// Multiplication `*`
201    Mult,
202    /// Division `/`
203    Div,
204    /// Modulo `%`
205    Mod,
206}
207
208impl BashArithOp {
209    /// Get all arithmetic operators
210    #[must_use]
211    pub fn all() -> &'static [Self] {
212        &[Self::Add, Self::Sub, Self::Mult, Self::Div, Self::Mod]
213    }
214
215    /// Convert to Bash operator string
216    #[must_use]
217    pub fn to_str(self) -> &'static str {
218        match self {
219            Self::Add => "+",
220            Self::Sub => "-",
221            Self::Mult => "*",
222            Self::Div => "/",
223            Self::Mod => "%",
224        }
225    }
226}
227
228/// Redirect types
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum RedirectType {
231    /// Output `>`
232    Output,
233    /// Append `>>`
234    Append,
235    /// Input `<`
236    Input,
237    /// Stderr to stdout `2>&1`
238    StderrToStdout,
239}
240
241impl RedirectType {
242    /// Convert to Bash redirect string
243    #[must_use]
244    pub fn to_str(self) -> &'static str {
245        match self {
246            Self::Output => ">",
247            Self::Append => ">>",
248            Self::Input => "<",
249            Self::StderrToStdout => "2>&1",
250        }
251    }
252}
253
254impl BashNode {
255    /// Convert AST node to Bash code string
256    #[must_use]
257    #[allow(clippy::too_many_lines)]
258    pub fn to_code(&self) -> String {
259        match self {
260            Self::Script { shebang, body } => {
261                let mut code = String::new();
262                if let Some(s) = shebang {
263                    code.push_str(s);
264                    code.push('\n');
265                }
266                for stmt in body {
267                    code.push_str(&stmt.to_code());
268                    code.push('\n');
269                }
270                code.trim_end().to_string()
271            }
272            Self::Assignment { name, value } => {
273                format!("{}={}", name, value.to_code())
274            }
275            Self::Command { name, args } => {
276                if args.is_empty() {
277                    name.clone()
278                } else {
279                    let args_str: Vec<String> = args.iter().map(|a| a.to_code()).collect();
280                    format!("{} {}", name, args_str.join(" "))
281                }
282            }
283            Self::If {
284                condition,
285                then_body,
286                else_body,
287            } => {
288                let cond = condition.to_code();
289                let then_str: String = then_body
290                    .iter()
291                    .map(|s| format!("    {}", s.to_code()))
292                    .collect::<Vec<_>>()
293                    .join("\n");
294
295                if else_body.is_empty() {
296                    format!("if {}; then\n{}\nfi", cond, then_str)
297                } else {
298                    let else_str: String = else_body
299                        .iter()
300                        .map(|s| format!("    {}", s.to_code()))
301                        .collect::<Vec<_>>()
302                        .join("\n");
303                    format!("if {}; then\n{}\nelse\n{}\nfi", cond, then_str, else_str)
304                }
305            }
306            Self::For { var, items, body } => {
307                let items_str: Vec<String> = items.iter().map(|i| i.to_code()).collect();
308                let body_str: String = body
309                    .iter()
310                    .map(|s| format!("    {}", s.to_code()))
311                    .collect::<Vec<_>>()
312                    .join("\n");
313                format!(
314                    "for {} in {}; do\n{}\ndone",
315                    var,
316                    items_str.join(" "),
317                    body_str
318                )
319            }
320            Self::While { condition, body } => {
321                let body_str: String = body
322                    .iter()
323                    .map(|s| format!("    {}", s.to_code()))
324                    .collect::<Vec<_>>()
325                    .join("\n");
326                format!("while {}; do\n{}\ndone", condition.to_code(), body_str)
327            }
328            Self::Function { name, body } => {
329                let body_str: String = body
330                    .iter()
331                    .map(|s| format!("    {}", s.to_code()))
332                    .collect::<Vec<_>>()
333                    .join("\n");
334                format!("{}() {{\n{}\n}}", name, body_str)
335            }
336            Self::Case { value, patterns } => {
337                let patterns_str: String = patterns
338                    .iter()
339                    .map(|(pat, body)| {
340                        let body_str: String = body
341                            .iter()
342                            .map(|s| s.to_code())
343                            .collect::<Vec<_>>()
344                            .join("; ");
345                        format!("    {}) {};;\n", pat, body_str)
346                    })
347                    .collect();
348                format!("case {} in\n{}esac", value.to_code(), patterns_str)
349            }
350            Self::Test { double, expr } => {
351                if *double {
352                    format!("[[ {} ]]", expr.to_code())
353                } else {
354                    format!("[ {} ]", expr.to_code())
355                }
356            }
357            Self::Compare { left, op, right } => {
358                format!("{} {} {}", left.to_code(), op.to_str(), right.to_code())
359            }
360            Self::Arithmetic(expr) => {
361                format!("$(({}))", expr.to_code())
362            }
363            Self::ArithOp { left, op, right } => {
364                format!("{} {} {}", left.to_code(), op.to_str(), right.to_code())
365            }
366            Self::Variable(name) => format!("${}", name),
367            Self::StringLit(s) => {
368                if s.contains(' ') || s.contains('$') {
369                    format!("\"{}\"", s)
370                } else {
371                    s.clone()
372                }
373            }
374            Self::IntLit(n) => n.to_string(),
375            Self::Array(items) => {
376                let items_str: Vec<String> = items.iter().map(|i| i.to_code()).collect();
377                format!("({})", items_str.join(" "))
378            }
379            Self::Pipe { left, right } => {
380                format!("{} | {}", left.to_code(), right.to_code())
381            }
382            Self::CommandSubst(cmd) => {
383                format!("$({})", cmd.to_code())
384            }
385            Self::Redirect {
386                command,
387                redirect_type,
388                target,
389            } => {
390                format!(
391                    "{} {} {}",
392                    command.to_code(),
393                    redirect_type.to_str(),
394                    target
395                )
396            }
397        }
398    }
399
400    /// Calculate AST depth
401    #[must_use]
402    pub fn depth(&self) -> usize {
403        match self {
404            Self::Script { body, .. } => 1 + body.iter().map(Self::depth).max().unwrap_or(0),
405            Self::Assignment { value, .. } => 1 + value.depth(),
406            Self::Command { args, .. } => 1 + args.iter().map(Self::depth).max().unwrap_or(0),
407            Self::If {
408                condition,
409                then_body,
410                else_body,
411            } => {
412                let cond_depth = condition.depth();
413                let then_depth = then_body.iter().map(Self::depth).max().unwrap_or(0);
414                let else_depth = else_body.iter().map(Self::depth).max().unwrap_or(0);
415                1 + cond_depth.max(then_depth).max(else_depth)
416            }
417            Self::For { items, body, .. } => {
418                let items_depth = items.iter().map(Self::depth).max().unwrap_or(0);
419                let body_depth = body.iter().map(Self::depth).max().unwrap_or(0);
420                1 + items_depth.max(body_depth)
421            }
422            Self::While { condition, body } => {
423                let cond_depth = condition.depth();
424                let body_depth = body.iter().map(Self::depth).max().unwrap_or(0);
425                1 + cond_depth.max(body_depth)
426            }
427            Self::Function { body, .. } => 1 + body.iter().map(Self::depth).max().unwrap_or(0),
428            Self::Case { value, patterns } => {
429                let val_depth = value.depth();
430                let pat_depth = patterns
431                    .iter()
432                    .flat_map(|(_, body)| body.iter().map(Self::depth))
433                    .max()
434                    .unwrap_or(0);
435                1 + val_depth.max(pat_depth)
436            }
437            Self::Test { expr, .. } => 1 + expr.depth(),
438            Self::Compare { left, right, .. } => 1 + left.depth().max(right.depth()),
439            Self::Arithmetic(expr) => 1 + expr.depth(),
440            Self::ArithOp { left, right, .. } => 1 + left.depth().max(right.depth()),
441            Self::Pipe { left, right } => 1 + left.depth().max(right.depth()),
442            Self::CommandSubst(cmd) => 1 + cmd.depth(),
443            Self::Redirect { command, .. } => 1 + command.depth(),
444            Self::Variable(_) | Self::StringLit(_) | Self::IntLit(_) => 1,
445            Self::Array(items) => 1 + items.iter().map(Self::depth).max().unwrap_or(0),
446        }
447    }
448
449    /// Extract features used in this node
450    #[must_use]
451    pub fn features(&self) -> Vec<String> {
452        let mut features = Vec::new();
453
454        match self {
455            Self::Script { body, .. } => {
456                features.push("script".to_string());
457                for stmt in body {
458                    features.extend(stmt.features());
459                }
460            }
461            Self::Assignment { .. } => features.push("assignment".to_string()),
462            Self::Command { name, .. } => {
463                features.push("command".to_string());
464                features.push(format!("cmd_{}", name));
465            }
466            Self::If { .. } => features.push("if".to_string()),
467            Self::For { .. } => features.push("for".to_string()),
468            Self::While { .. } => features.push("while".to_string()),
469            Self::Function { .. } => features.push("function".to_string()),
470            Self::Case { .. } => features.push("case".to_string()),
471            Self::Test { double, .. } => {
472                features.push(
473                    if *double {
474                        "test_double"
475                    } else {
476                        "test_single"
477                    }
478                    .to_string(),
479                );
480            }
481            Self::Compare { op, .. } => features.push(format!("compare_{}", op.to_str())),
482            Self::Arithmetic(_) => features.push("arithmetic".to_string()),
483            Self::ArithOp { op, .. } => features.push(format!("arith_{}", op.to_str())),
484            Self::Variable(_) => features.push("variable".to_string()),
485            Self::StringLit(_) => features.push("string".to_string()),
486            Self::IntLit(_) => features.push("integer".to_string()),
487            Self::Array(_) => features.push("array".to_string()),
488            Self::Pipe { .. } => features.push("pipe".to_string()),
489            Self::CommandSubst(_) => features.push("command_subst".to_string()),
490            Self::Redirect { redirect_type, .. } => {
491                features.push(format!("redirect_{}", redirect_type.to_str()));
492            }
493        }
494
495        features
496    }
497}
498
499/// Bash program enumerator
500#[derive(Debug)]
501pub struct BashEnumerator {
502    /// Maximum AST depth
503    max_depth: usize,
504}
505
506impl BashEnumerator {
507    /// Create a new Bash enumerator
508    #[must_use]
509    pub fn new(max_depth: usize) -> Self {
510        Self { max_depth }
511    }
512
513    /// Enumerate all Bash programs up to the configured depth
514    #[must_use]
515    #[allow(clippy::too_many_lines)]
516    pub fn enumerate_programs(&self) -> Vec<GeneratedCode> {
517        let mut programs = Vec::new();
518
519        // Generate simple assignments
520        for var in &["x", "y", "result"] {
521            for val in [1, 0, 42] {
522                let node = BashNode::Assignment {
523                    name: var.to_string(),
524                    value: Box::new(BashNode::IntLit(val)),
525                };
526                if node.depth() <= self.max_depth {
527                    programs.push(self.node_to_generated(&node));
528                }
529            }
530            // String assignments
531            for s in &["hello", "world"] {
532                let node = BashNode::Assignment {
533                    name: var.to_string(),
534                    value: Box::new(BashNode::StringLit(s.to_string())),
535                };
536                if node.depth() <= self.max_depth {
537                    programs.push(self.node_to_generated(&node));
538                }
539            }
540        }
541
542        // Generate echo commands
543        for arg in &["$x", "hello", "$HOME"] {
544            let node = BashNode::Command {
545                name: "echo".to_string(),
546                args: vec![BashNode::StringLit(arg.to_string())],
547            };
548            if node.depth() <= self.max_depth {
549                programs.push(self.node_to_generated(&node));
550            }
551        }
552
553        // Generate if statements (depth 2+)
554        if self.max_depth >= 2 {
555            for var in &["x", "y"] {
556                for op in &[
557                    BashCompareOp::NumEq,
558                    BashCompareOp::NumGt,
559                    BashCompareOp::NumLt,
560                ] {
561                    let node = BashNode::If {
562                        condition: Box::new(BashNode::Test {
563                            double: false,
564                            expr: Box::new(BashNode::Compare {
565                                left: Box::new(BashNode::Variable(var.to_string())),
566                                op: *op,
567                                right: Box::new(BashNode::IntLit(0)),
568                            }),
569                        }),
570                        then_body: vec![BashNode::Command {
571                            name: "echo".to_string(),
572                            args: vec![BashNode::StringLit("yes".to_string())],
573                        }],
574                        else_body: vec![],
575                    };
576                    if node.depth() <= self.max_depth {
577                        programs.push(self.node_to_generated(&node));
578                    }
579                }
580            }
581        }
582
583        // Generate for loops (depth 2+)
584        if self.max_depth >= 2 {
585            let node = BashNode::For {
586                var: "i".to_string(),
587                items: vec![
588                    BashNode::IntLit(1),
589                    BashNode::IntLit(2),
590                    BashNode::IntLit(3),
591                ],
592                body: vec![BashNode::Command {
593                    name: "echo".to_string(),
594                    args: vec![BashNode::Variable("i".to_string())],
595                }],
596            };
597            if node.depth() <= self.max_depth {
598                programs.push(self.node_to_generated(&node));
599            }
600        }
601
602        // Generate while loops (depth 2+)
603        if self.max_depth >= 2 {
604            let node = BashNode::While {
605                condition: Box::new(BashNode::Test {
606                    double: false,
607                    expr: Box::new(BashNode::Compare {
608                        left: Box::new(BashNode::Variable("x".to_string())),
609                        op: BashCompareOp::NumGt,
610                        right: Box::new(BashNode::IntLit(0)),
611                    }),
612                }),
613                body: vec![BashNode::Assignment {
614                    name: "x".to_string(),
615                    value: Box::new(BashNode::Arithmetic(Box::new(BashNode::ArithOp {
616                        left: Box::new(BashNode::Variable("x".to_string())),
617                        op: BashArithOp::Sub,
618                        right: Box::new(BashNode::IntLit(1)),
619                    }))),
620                }],
621            };
622            if node.depth() <= self.max_depth {
623                programs.push(self.node_to_generated(&node));
624            }
625        }
626
627        // Generate functions (depth 2+)
628        if self.max_depth >= 2 {
629            for name in &["greet", "main"] {
630                let node = BashNode::Function {
631                    name: name.to_string(),
632                    body: vec![BashNode::Command {
633                        name: "echo".to_string(),
634                        args: vec![BashNode::StringLit("hello".to_string())],
635                    }],
636                };
637                if node.depth() <= self.max_depth {
638                    programs.push(self.node_to_generated(&node));
639                }
640            }
641        }
642
643        // Generate arithmetic expressions
644        for op in BashArithOp::all() {
645            let node = BashNode::Assignment {
646                name: "result".to_string(),
647                value: Box::new(BashNode::Arithmetic(Box::new(BashNode::ArithOp {
648                    left: Box::new(BashNode::IntLit(1)),
649                    op: *op,
650                    right: Box::new(BashNode::IntLit(2)),
651                }))),
652            };
653            if node.depth() <= self.max_depth {
654                programs.push(self.node_to_generated(&node));
655            }
656        }
657
658        // Generate pipes (depth 2+)
659        if self.max_depth >= 2 {
660            let node = BashNode::Pipe {
661                left: Box::new(BashNode::Command {
662                    name: "echo".to_string(),
663                    args: vec![BashNode::StringLit("hello".to_string())],
664                }),
665                right: Box::new(BashNode::Command {
666                    name: "wc".to_string(),
667                    args: vec![BashNode::StringLit("-c".to_string())],
668                }),
669            };
670            if node.depth() <= self.max_depth {
671                programs.push(self.node_to_generated(&node));
672            }
673        }
674
675        // Generate array assignments
676        let node = BashNode::Assignment {
677            name: "arr".to_string(),
678            value: Box::new(BashNode::Array(vec![
679                BashNode::IntLit(1),
680                BashNode::IntLit(2),
681                BashNode::IntLit(3),
682            ])),
683        };
684        if node.depth() <= self.max_depth {
685            programs.push(self.node_to_generated(&node));
686        }
687
688        // Generate command substitution
689        let node = BashNode::Assignment {
690            name: "output".to_string(),
691            value: Box::new(BashNode::CommandSubst(Box::new(BashNode::Command {
692                name: "echo".to_string(),
693                args: vec![BashNode::StringLit("hello".to_string())],
694            }))),
695        };
696        if node.depth() <= self.max_depth {
697            programs.push(self.node_to_generated(&node));
698        }
699
700        // Generate redirections
701        for redirect_type in &[
702            RedirectType::Output,
703            RedirectType::Append,
704            RedirectType::Input,
705        ] {
706            let target = match redirect_type {
707                RedirectType::Input => "/dev/null",
708                _ => "/tmp/output.txt",
709            };
710            let node = BashNode::Redirect {
711                command: Box::new(BashNode::Command {
712                    name: "echo".to_string(),
713                    args: vec![BashNode::StringLit("test".to_string())],
714                }),
715                redirect_type: *redirect_type,
716                target: target.to_string(),
717            };
718            if node.depth() <= self.max_depth {
719                programs.push(self.node_to_generated(&node));
720            }
721        }
722
723        // Generate more commands (cat, test, true, false)
724        for cmd in &["cat", "test", "true", "false", "pwd", "ls"] {
725            let node = BashNode::Command {
726                name: cmd.to_string(),
727                args: vec![],
728            };
729            if node.depth() <= self.max_depth {
730                programs.push(self.node_to_generated(&node));
731            }
732        }
733
734        // Generate double bracket tests (depth 2+)
735        if self.max_depth >= 2 {
736            for var in &["x", "str"] {
737                let node = BashNode::If {
738                    condition: Box::new(BashNode::Test {
739                        double: true,
740                        expr: Box::new(BashNode::Compare {
741                            left: Box::new(BashNode::Variable(var.to_string())),
742                            op: BashCompareOp::StrEq,
743                            right: Box::new(BashNode::StringLit("hello".to_string())),
744                        }),
745                    }),
746                    then_body: vec![BashNode::Command {
747                        name: "echo".to_string(),
748                        args: vec![BashNode::StringLit("match".to_string())],
749                    }],
750                    else_body: vec![],
751                };
752                if node.depth() <= self.max_depth {
753                    programs.push(self.node_to_generated(&node));
754                }
755            }
756        }
757
758        // Generate if-else statements (depth 2+)
759        if self.max_depth >= 2 {
760            let node = BashNode::If {
761                condition: Box::new(BashNode::Test {
762                    double: false,
763                    expr: Box::new(BashNode::Compare {
764                        left: Box::new(BashNode::Variable("x".to_string())),
765                        op: BashCompareOp::NumEq,
766                        right: Box::new(BashNode::IntLit(1)),
767                    }),
768                }),
769                then_body: vec![BashNode::Command {
770                    name: "echo".to_string(),
771                    args: vec![BashNode::StringLit("one".to_string())],
772                }],
773                else_body: vec![BashNode::Command {
774                    name: "echo".to_string(),
775                    args: vec![BashNode::StringLit("not one".to_string())],
776                }],
777            };
778            if node.depth() <= self.max_depth {
779                programs.push(self.node_to_generated(&node));
780            }
781        }
782
783        // Generate case statements (depth 2+)
784        if self.max_depth >= 2 {
785            let node = BashNode::Case {
786                value: Box::new(BashNode::Variable("x".to_string())),
787                patterns: vec![
788                    (
789                        "1".to_string(),
790                        vec![BashNode::Command {
791                            name: "echo".to_string(),
792                            args: vec![BashNode::StringLit("one".to_string())],
793                        }],
794                    ),
795                    (
796                        "2".to_string(),
797                        vec![BashNode::Command {
798                            name: "echo".to_string(),
799                            args: vec![BashNode::StringLit("two".to_string())],
800                        }],
801                    ),
802                    (
803                        "*".to_string(),
804                        vec![BashNode::Command {
805                            name: "echo".to_string(),
806                            args: vec![BashNode::StringLit("other".to_string())],
807                        }],
808                    ),
809                ],
810            };
811            if node.depth() <= self.max_depth {
812                programs.push(self.node_to_generated(&node));
813            }
814        }
815
816        // Generate string comparison operators
817        if self.max_depth >= 2 {
818            for op in &[BashCompareOp::StrEq, BashCompareOp::StrNe] {
819                let node = BashNode::If {
820                    condition: Box::new(BashNode::Test {
821                        double: true,
822                        expr: Box::new(BashNode::Compare {
823                            left: Box::new(BashNode::Variable("str".to_string())),
824                            op: *op,
825                            right: Box::new(BashNode::StringLit("test".to_string())),
826                        }),
827                    }),
828                    then_body: vec![BashNode::Command {
829                        name: "echo".to_string(),
830                        args: vec![BashNode::StringLit("matched".to_string())],
831                    }],
832                    else_body: vec![],
833                };
834                if node.depth() <= self.max_depth {
835                    programs.push(self.node_to_generated(&node));
836                }
837            }
838        }
839
840        // Generate for loops with file patterns
841        if self.max_depth >= 2 {
842            let node = BashNode::For {
843                var: "f".to_string(),
844                items: vec![BashNode::StringLit("*.txt".to_string())],
845                body: vec![BashNode::Command {
846                    name: "echo".to_string(),
847                    args: vec![BashNode::Variable("f".to_string())],
848                }],
849            };
850            if node.depth() <= self.max_depth {
851                programs.push(self.node_to_generated(&node));
852            }
853        }
854
855        // Generate nested pipes (depth 3+)
856        if self.max_depth >= 3 {
857            let node = BashNode::Pipe {
858                left: Box::new(BashNode::Command {
859                    name: "cat".to_string(),
860                    args: vec![BashNode::StringLit("/etc/passwd".to_string())],
861                }),
862                right: Box::new(BashNode::Pipe {
863                    left: Box::new(BashNode::Command {
864                        name: "grep".to_string(),
865                        args: vec![BashNode::StringLit("root".to_string())],
866                    }),
867                    right: Box::new(BashNode::Command {
868                        name: "wc".to_string(),
869                        args: vec![BashNode::StringLit("-l".to_string())],
870                    }),
871                }),
872            };
873            if node.depth() <= self.max_depth {
874                programs.push(self.node_to_generated(&node));
875            }
876        }
877
878        // ==== MASSIVE EXPANSION FOR 1000+ PROGRAMS ====
879
880        // Extended variable and value sets for combinatorial explosion
881        let vars = [
882            "x", "y", "z", "a", "b", "n", "i", "j", "k", "count", "sum", "result", "tmp", "val",
883            "num",
884        ];
885        let ints = [0, 1, 2, 3, 5, 10, 42, 100, 255, -1];
886        let strings = [
887            "hello", "world", "test", "foo", "bar", "baz", "value", "data", "file", "",
888        ];
889
890        // Generate more variable assignments with all combinations
891        for var in &vars {
892            for val in &ints {
893                let node = BashNode::Assignment {
894                    name: var.to_string(),
895                    value: Box::new(BashNode::IntLit(*val)),
896                };
897                if node.depth() <= self.max_depth {
898                    programs.push(self.node_to_generated(&node));
899                }
900            }
901        }
902
903        // Generate string assignments with all combinations
904        for var in &vars {
905            for s in &strings {
906                let node = BashNode::Assignment {
907                    name: var.to_string(),
908                    value: Box::new(BashNode::StringLit(s.to_string())),
909                };
910                if node.depth() <= self.max_depth {
911                    programs.push(self.node_to_generated(&node));
912                }
913            }
914        }
915
916        // Extensive command generation
917        let cmds = [
918            "echo", "printf", "cat", "ls", "pwd", "cd", "mkdir", "rm", "cp", "mv", "grep", "sed",
919            "awk", "cut", "sort", "uniq", "wc", "head", "tail", "tee", "true", "false", "test",
920            "exit", "return", "break", "continue", "read", "export", "unset", "local", "declare",
921            "typeset", "readonly",
922        ];
923
924        for cmd in &cmds {
925            // Command with no args
926            let node = BashNode::Command {
927                name: cmd.to_string(),
928                args: vec![],
929            };
930            if node.depth() <= self.max_depth {
931                programs.push(self.node_to_generated(&node));
932            }
933
934            // Command with various args
935            for arg in &[
936                "$x",
937                "$1",
938                "$@",
939                "-n",
940                "-e",
941                "-r",
942                "-f",
943                "file.txt",
944                "/dev/null",
945            ] {
946                let node = BashNode::Command {
947                    name: cmd.to_string(),
948                    args: vec![BashNode::StringLit(arg.to_string())],
949                };
950                if node.depth() <= self.max_depth {
951                    programs.push(self.node_to_generated(&node));
952                }
953            }
954        }
955
956        // All comparison operators with multiple variables
957        if self.max_depth >= 2 {
958            for var in &["x", "y", "z", "n", "count"] {
959                for op in BashCompareOp::all() {
960                    for right_val in &[0, 1, 10] {
961                        // Single bracket test
962                        let node = BashNode::If {
963                            condition: Box::new(BashNode::Test {
964                                double: false,
965                                expr: Box::new(BashNode::Compare {
966                                    left: Box::new(BashNode::Variable(var.to_string())),
967                                    op: *op,
968                                    right: Box::new(BashNode::IntLit(*right_val)),
969                                }),
970                            }),
971                            then_body: vec![BashNode::Command {
972                                name: "echo".to_string(),
973                                args: vec![BashNode::StringLit("true".to_string())],
974                            }],
975                            else_body: vec![],
976                        };
977                        if node.depth() <= self.max_depth {
978                            programs.push(self.node_to_generated(&node));
979                        }
980
981                        // Double bracket test
982                        let node = BashNode::If {
983                            condition: Box::new(BashNode::Test {
984                                double: true,
985                                expr: Box::new(BashNode::Compare {
986                                    left: Box::new(BashNode::Variable(var.to_string())),
987                                    op: *op,
988                                    right: Box::new(BashNode::IntLit(*right_val)),
989                                }),
990                            }),
991                            then_body: vec![BashNode::Command {
992                                name: "echo".to_string(),
993                                args: vec![BashNode::StringLit("true".to_string())],
994                            }],
995                            else_body: vec![],
996                        };
997                        if node.depth() <= self.max_depth {
998                            programs.push(self.node_to_generated(&node));
999                        }
1000                    }
1001                }
1002            }
1003        }
1004
1005        // All arithmetic operators with multiple operand combinations
1006        for left_val in &[0, 1, 2, 5, 10] {
1007            for right_val in &[1, 2, 3, 5] {
1008                for op in BashArithOp::all() {
1009                    // Skip division by zero
1010                    if matches!(op, BashArithOp::Div | BashArithOp::Mod) && *right_val == 0 {
1011                        continue;
1012                    }
1013
1014                    let node = BashNode::Assignment {
1015                        name: "result".to_string(),
1016                        value: Box::new(BashNode::Arithmetic(Box::new(BashNode::ArithOp {
1017                            left: Box::new(BashNode::IntLit(*left_val)),
1018                            op: *op,
1019                            right: Box::new(BashNode::IntLit(*right_val)),
1020                        }))),
1021                    };
1022                    if node.depth() <= self.max_depth {
1023                        programs.push(self.node_to_generated(&node));
1024                    }
1025                }
1026            }
1027        }
1028
1029        // For loops with different iterators
1030        if self.max_depth >= 2 {
1031            for var in &["i", "j", "x", "item", "file"] {
1032                // Numeric sequences
1033                for items in &[
1034                    vec![BashNode::IntLit(1)],
1035                    vec![BashNode::IntLit(1), BashNode::IntLit(2)],
1036                    vec![
1037                        BashNode::IntLit(1),
1038                        BashNode::IntLit(2),
1039                        BashNode::IntLit(3),
1040                    ],
1041                    vec![
1042                        BashNode::IntLit(0),
1043                        BashNode::IntLit(1),
1044                        BashNode::IntLit(2),
1045                        BashNode::IntLit(3),
1046                        BashNode::IntLit(4),
1047                    ],
1048                ] {
1049                    let node = BashNode::For {
1050                        var: var.to_string(),
1051                        items: items.clone(),
1052                        body: vec![BashNode::Command {
1053                            name: "echo".to_string(),
1054                            args: vec![BashNode::Variable(var.to_string())],
1055                        }],
1056                    };
1057                    if node.depth() <= self.max_depth {
1058                        programs.push(self.node_to_generated(&node));
1059                    }
1060                }
1061
1062                // Glob patterns
1063                for pattern in &["*.txt", "*.sh", "*.log", "*", "file*", "*.{txt,md}"] {
1064                    let node = BashNode::For {
1065                        var: var.to_string(),
1066                        items: vec![BashNode::StringLit(pattern.to_string())],
1067                        body: vec![BashNode::Command {
1068                            name: "echo".to_string(),
1069                            args: vec![BashNode::Variable(var.to_string())],
1070                        }],
1071                    };
1072                    if node.depth() <= self.max_depth {
1073                        programs.push(self.node_to_generated(&node));
1074                    }
1075                }
1076            }
1077        }
1078
1079        // While loops with different conditions
1080        if self.max_depth >= 2 {
1081            for var in &["x", "n", "count", "i"] {
1082                for limit in &[0, 1, 5, 10] {
1083                    for op in &[
1084                        BashCompareOp::NumGt,
1085                        BashCompareOp::NumLt,
1086                        BashCompareOp::NumGe,
1087                        BashCompareOp::NumLe,
1088                    ] {
1089                        let node = BashNode::While {
1090                            condition: Box::new(BashNode::Test {
1091                                double: false,
1092                                expr: Box::new(BashNode::Compare {
1093                                    left: Box::new(BashNode::Variable(var.to_string())),
1094                                    op: *op,
1095                                    right: Box::new(BashNode::IntLit(*limit)),
1096                                }),
1097                            }),
1098                            body: vec![BashNode::Assignment {
1099                                name: var.to_string(),
1100                                value: Box::new(BashNode::Arithmetic(Box::new(
1101                                    BashNode::ArithOp {
1102                                        left: Box::new(BashNode::Variable(var.to_string())),
1103                                        op: BashArithOp::Sub,
1104                                        right: Box::new(BashNode::IntLit(1)),
1105                                    },
1106                                ))),
1107                            }],
1108                        };
1109                        if node.depth() <= self.max_depth {
1110                            programs.push(self.node_to_generated(&node));
1111                        }
1112                    }
1113                }
1114            }
1115        }
1116
1117        // Functions with different names and bodies
1118        if self.max_depth >= 2 {
1119            let func_names = [
1120                "main", "init", "setup", "cleanup", "run", "process", "validate", "check", "build",
1121                "deploy",
1122            ];
1123            let bodies = ["echo done", "return 0", "exit 0", "true", "pwd"];
1124
1125            for name in &func_names {
1126                for body_cmd in &bodies {
1127                    let parts: Vec<&str> = body_cmd.split_whitespace().collect();
1128                    let cmd_name = parts[0];
1129                    let args: Vec<BashNode> = parts[1..]
1130                        .iter()
1131                        .map(|a| BashNode::StringLit(a.to_string()))
1132                        .collect();
1133
1134                    let node = BashNode::Function {
1135                        name: name.to_string(),
1136                        body: vec![BashNode::Command {
1137                            name: cmd_name.to_string(),
1138                            args,
1139                        }],
1140                    };
1141                    if node.depth() <= self.max_depth {
1142                        programs.push(self.node_to_generated(&node));
1143                    }
1144                }
1145            }
1146        }
1147
1148        // Case statements with different patterns
1149        if self.max_depth >= 2 {
1150            for var in &["x", "opt", "arg", "cmd"] {
1151                for patterns in &[
1152                    vec![("1", "one"), ("2", "two"), ("*", "other")],
1153                    vec![("a", "alpha"), ("b", "beta"), ("*", "default")],
1154                    vec![
1155                        ("start", "starting"),
1156                        ("stop", "stopping"),
1157                        ("*", "unknown"),
1158                    ],
1159                    vec![("yes", "y"), ("no", "n"), ("*", "invalid")],
1160                ] {
1161                    let pattern_nodes: Vec<(String, Vec<BashNode>)> = patterns
1162                        .iter()
1163                        .map(|(pat, resp)| {
1164                            (
1165                                pat.to_string(),
1166                                vec![BashNode::Command {
1167                                    name: "echo".to_string(),
1168                                    args: vec![BashNode::StringLit(resp.to_string())],
1169                                }],
1170                            )
1171                        })
1172                        .collect();
1173
1174                    let node = BashNode::Case {
1175                        value: Box::new(BashNode::Variable(var.to_string())),
1176                        patterns: pattern_nodes,
1177                    };
1178                    if node.depth() <= self.max_depth {
1179                        programs.push(self.node_to_generated(&node));
1180                    }
1181                }
1182            }
1183        }
1184
1185        // Pipe combinations
1186        if self.max_depth >= 2 {
1187            let pipe_lefts = [
1188                ("echo", "hello"),
1189                ("cat", "file.txt"),
1190                ("ls", "-la"),
1191                ("ps", "aux"),
1192            ];
1193            let pipe_rights = [
1194                ("grep", "pattern"),
1195                ("wc", "-l"),
1196                ("head", "-n10"),
1197                ("sort", "-n"),
1198                ("cut", "-d:"),
1199            ];
1200
1201            for (left_cmd, left_arg) in &pipe_lefts {
1202                for (right_cmd, right_arg) in &pipe_rights {
1203                    let node = BashNode::Pipe {
1204                        left: Box::new(BashNode::Command {
1205                            name: left_cmd.to_string(),
1206                            args: vec![BashNode::StringLit(left_arg.to_string())],
1207                        }),
1208                        right: Box::new(BashNode::Command {
1209                            name: right_cmd.to_string(),
1210                            args: vec![BashNode::StringLit(right_arg.to_string())],
1211                        }),
1212                    };
1213                    if node.depth() <= self.max_depth {
1214                        programs.push(self.node_to_generated(&node));
1215                    }
1216                }
1217            }
1218        }
1219
1220        // Redirections with all types
1221        for redirect_type in &[
1222            RedirectType::Output,
1223            RedirectType::Append,
1224            RedirectType::Input,
1225            RedirectType::StderrToStdout,
1226        ] {
1227            for target in &[
1228                "/dev/null",
1229                "/tmp/out.txt",
1230                "output.log",
1231                "result.txt",
1232                "&1",
1233                "&2",
1234            ] {
1235                for cmd in &["echo", "cat", "ls"] {
1236                    let node = BashNode::Redirect {
1237                        command: Box::new(BashNode::Command {
1238                            name: cmd.to_string(),
1239                            args: vec![BashNode::StringLit("test".to_string())],
1240                        }),
1241                        redirect_type: *redirect_type,
1242                        target: target.to_string(),
1243                    };
1244                    if node.depth() <= self.max_depth {
1245                        programs.push(self.node_to_generated(&node));
1246                    }
1247                }
1248            }
1249        }
1250
1251        // Command substitution variations
1252        for var in &["output", "result", "data", "lines", "count"] {
1253            for cmd in &["pwd", "date", "whoami", "hostname", "uname -a"] {
1254                let parts: Vec<&str> = cmd.split_whitespace().collect();
1255                let cmd_name = parts[0];
1256                let args: Vec<BashNode> = parts[1..]
1257                    .iter()
1258                    .map(|a| BashNode::StringLit(a.to_string()))
1259                    .collect();
1260
1261                let node = BashNode::Assignment {
1262                    name: var.to_string(),
1263                    value: Box::new(BashNode::CommandSubst(Box::new(BashNode::Command {
1264                        name: cmd_name.to_string(),
1265                        args,
1266                    }))),
1267                };
1268                if node.depth() <= self.max_depth {
1269                    programs.push(self.node_to_generated(&node));
1270                }
1271            }
1272        }
1273
1274        // Array assignments with different sizes
1275        for var in &["arr", "list", "items", "values", "data"] {
1276            for items in &[
1277                vec![BashNode::IntLit(1)],
1278                vec![BashNode::IntLit(1), BashNode::IntLit(2)],
1279                vec![
1280                    BashNode::IntLit(1),
1281                    BashNode::IntLit(2),
1282                    BashNode::IntLit(3),
1283                ],
1284                vec![BashNode::StringLit("a".to_string())],
1285                vec![
1286                    BashNode::StringLit("a".to_string()),
1287                    BashNode::StringLit("b".to_string()),
1288                ],
1289                vec![
1290                    BashNode::StringLit("foo".to_string()),
1291                    BashNode::StringLit("bar".to_string()),
1292                    BashNode::StringLit("baz".to_string()),
1293                ],
1294            ] {
1295                let node = BashNode::Assignment {
1296                    name: var.to_string(),
1297                    value: Box::new(BashNode::Array(items.clone())),
1298                };
1299                if node.depth() <= self.max_depth {
1300                    programs.push(self.node_to_generated(&node));
1301                }
1302            }
1303        }
1304
1305        // If-else with different conditions and bodies
1306        if self.max_depth >= 2 {
1307            for var in &["x", "n", "flag", "status"] {
1308                for op in &[
1309                    BashCompareOp::NumEq,
1310                    BashCompareOp::NumNe,
1311                    BashCompareOp::NumGt,
1312                ] {
1313                    for val in &[0, 1] {
1314                        let node = BashNode::If {
1315                            condition: Box::new(BashNode::Test {
1316                                double: false,
1317                                expr: Box::new(BashNode::Compare {
1318                                    left: Box::new(BashNode::Variable(var.to_string())),
1319                                    op: *op,
1320                                    right: Box::new(BashNode::IntLit(*val)),
1321                                }),
1322                            }),
1323                            then_body: vec![BashNode::Command {
1324                                name: "echo".to_string(),
1325                                args: vec![BashNode::StringLit("then".to_string())],
1326                            }],
1327                            else_body: vec![BashNode::Command {
1328                                name: "echo".to_string(),
1329                                args: vec![BashNode::StringLit("else".to_string())],
1330                            }],
1331                        };
1332                        if node.depth() <= self.max_depth {
1333                            programs.push(self.node_to_generated(&node));
1334                        }
1335                    }
1336                }
1337            }
1338        }
1339
1340        // Additional combinations to reach 1000+
1341
1342        // More variable reference patterns
1343        for var in &["HOME", "USER", "PWD", "PATH", "SHELL", "TERM", "HOSTNAME"] {
1344            let node = BashNode::Command {
1345                name: "echo".to_string(),
1346                args: vec![BashNode::Variable(var.to_string())],
1347            };
1348            if node.depth() <= self.max_depth {
1349                programs.push(self.node_to_generated(&node));
1350            }
1351        }
1352
1353        // Assignment from variable
1354        for src in &["x", "y", "HOME", "USER"] {
1355            for dst in &["a", "b", "tmp"] {
1356                let node = BashNode::Assignment {
1357                    name: dst.to_string(),
1358                    value: Box::new(BashNode::Variable(src.to_string())),
1359                };
1360                if node.depth() <= self.max_depth {
1361                    programs.push(self.node_to_generated(&node));
1362                }
1363            }
1364        }
1365
1366        // Nested command substitution in echo
1367        for cmd in &["pwd", "date", "whoami", "hostname"] {
1368            let node = BashNode::Command {
1369                name: "echo".to_string(),
1370                args: vec![BashNode::CommandSubst(Box::new(BashNode::Command {
1371                    name: cmd.to_string(),
1372                    args: vec![],
1373                }))],
1374            };
1375            if node.depth() <= self.max_depth {
1376                programs.push(self.node_to_generated(&node));
1377            }
1378        }
1379
1380        // String comparisons
1381        if self.max_depth >= 2 {
1382            for var in &["str", "name", "arg"] {
1383                for op in &[BashCompareOp::StrEq, BashCompareOp::StrNe] {
1384                    for value in &["", "test", "value", "hello"] {
1385                        let node = BashNode::If {
1386                            condition: Box::new(BashNode::Test {
1387                                double: true,
1388                                expr: Box::new(BashNode::Compare {
1389                                    left: Box::new(BashNode::Variable(var.to_string())),
1390                                    op: *op,
1391                                    right: Box::new(BashNode::StringLit(value.to_string())),
1392                                }),
1393                            }),
1394                            then_body: vec![BashNode::Command {
1395                                name: "echo".to_string(),
1396                                args: vec![BashNode::StringLit("match".to_string())],
1397                            }],
1398                            else_body: vec![],
1399                        };
1400                        if node.depth() <= self.max_depth {
1401                            programs.push(self.node_to_generated(&node));
1402                        }
1403                    }
1404                }
1405            }
1406        }
1407
1408        // More arithmetic with variables
1409        for var in &["x", "y", "n"] {
1410            for op in BashArithOp::all() {
1411                for val in &[1, 2, 5] {
1412                    let node = BashNode::Assignment {
1413                        name: "result".to_string(),
1414                        value: Box::new(BashNode::Arithmetic(Box::new(BashNode::ArithOp {
1415                            left: Box::new(BashNode::Variable(var.to_string())),
1416                            op: *op,
1417                            right: Box::new(BashNode::IntLit(*val)),
1418                        }))),
1419                    };
1420                    if node.depth() <= self.max_depth {
1421                        programs.push(self.node_to_generated(&node));
1422                    }
1423                }
1424            }
1425        }
1426
1427        // Command with multiple arguments
1428        for cmd in &["echo", "printf", "ls", "cat"] {
1429            for arg1 in &["hello", "world", "-l", "-a"] {
1430                for arg2 in &["test", "file", "-n", "-r"] {
1431                    let node = BashNode::Command {
1432                        name: cmd.to_string(),
1433                        args: vec![
1434                            BashNode::StringLit(arg1.to_string()),
1435                            BashNode::StringLit(arg2.to_string()),
1436                        ],
1437                    };
1438                    if node.depth() <= self.max_depth {
1439                        programs.push(self.node_to_generated(&node));
1440                    }
1441                }
1442            }
1443        }
1444
1445        programs
1446    }
1447
1448    fn node_to_generated(&self, node: &BashNode) -> GeneratedCode {
1449        GeneratedCode {
1450            code: node.to_code(),
1451            language: Language::Bash,
1452            ast_depth: node.depth(),
1453            features: node.features(),
1454        }
1455    }
1456}
1457
1458#[cfg(test)]
1459mod tests {
1460    use super::*;
1461
1462    #[test]
1463    fn test_bash_node_assignment() {
1464        let node = BashNode::Assignment {
1465            name: "x".to_string(),
1466            value: Box::new(BashNode::IntLit(42)),
1467        };
1468        assert_eq!(node.to_code(), "x=42");
1469    }
1470
1471    #[test]
1472    fn test_bash_node_string_assignment() {
1473        let node = BashNode::Assignment {
1474            name: "msg".to_string(),
1475            value: Box::new(BashNode::StringLit("hello world".to_string())),
1476        };
1477        assert_eq!(node.to_code(), "msg=\"hello world\"");
1478    }
1479
1480    #[test]
1481    fn test_bash_node_echo() {
1482        let node = BashNode::Command {
1483            name: "echo".to_string(),
1484            args: vec![BashNode::StringLit("hello".to_string())],
1485        };
1486        assert_eq!(node.to_code(), "echo hello");
1487    }
1488
1489    #[test]
1490    fn test_bash_node_if() {
1491        let node = BashNode::If {
1492            condition: Box::new(BashNode::Test {
1493                double: false,
1494                expr: Box::new(BashNode::Compare {
1495                    left: Box::new(BashNode::Variable("x".to_string())),
1496                    op: BashCompareOp::NumEq,
1497                    right: Box::new(BashNode::IntLit(1)),
1498                }),
1499            }),
1500            then_body: vec![BashNode::Command {
1501                name: "echo".to_string(),
1502                args: vec![BashNode::StringLit("yes".to_string())],
1503            }],
1504            else_body: vec![],
1505        };
1506        let code = node.to_code();
1507        assert!(code.contains("if [ $x -eq 1 ]"));
1508        assert!(code.contains("then"));
1509        assert!(code.contains("fi"));
1510    }
1511
1512    #[test]
1513    fn test_bash_node_for() {
1514        let node = BashNode::For {
1515            var: "i".to_string(),
1516            items: vec![BashNode::IntLit(1), BashNode::IntLit(2)],
1517            body: vec![BashNode::Command {
1518                name: "echo".to_string(),
1519                args: vec![BashNode::Variable("i".to_string())],
1520            }],
1521        };
1522        let code = node.to_code();
1523        assert!(code.contains("for i in 1 2"));
1524        assert!(code.contains("do"));
1525        assert!(code.contains("done"));
1526    }
1527
1528    #[test]
1529    fn test_bash_node_while() {
1530        let node = BashNode::While {
1531            condition: Box::new(BashNode::Test {
1532                double: true,
1533                expr: Box::new(BashNode::Compare {
1534                    left: Box::new(BashNode::Variable("x".to_string())),
1535                    op: BashCompareOp::NumGt,
1536                    right: Box::new(BashNode::IntLit(0)),
1537                }),
1538            }),
1539            body: vec![BashNode::Command {
1540                name: "echo".to_string(),
1541                args: vec![BashNode::Variable("x".to_string())],
1542            }],
1543        };
1544        let code = node.to_code();
1545        assert!(code.contains("while [[ $x -gt 0 ]]"));
1546        assert!(code.contains("done"));
1547    }
1548
1549    #[test]
1550    fn test_bash_node_function() {
1551        let node = BashNode::Function {
1552            name: "greet".to_string(),
1553            body: vec![BashNode::Command {
1554                name: "echo".to_string(),
1555                args: vec![BashNode::StringLit("Hello".to_string())],
1556            }],
1557        };
1558        let code = node.to_code();
1559        assert!(code.contains("greet()"));
1560        assert!(code.contains("{"));
1561        assert!(code.contains("}"));
1562    }
1563
1564    #[test]
1565    fn test_bash_node_arithmetic() {
1566        let node = BashNode::Arithmetic(Box::new(BashNode::ArithOp {
1567            left: Box::new(BashNode::IntLit(1)),
1568            op: BashArithOp::Add,
1569            right: Box::new(BashNode::IntLit(2)),
1570        }));
1571        assert_eq!(node.to_code(), "$((1 + 2))");
1572    }
1573
1574    #[test]
1575    fn test_bash_node_pipe() {
1576        let node = BashNode::Pipe {
1577            left: Box::new(BashNode::Command {
1578                name: "ls".to_string(),
1579                args: vec![],
1580            }),
1581            right: Box::new(BashNode::Command {
1582                name: "wc".to_string(),
1583                args: vec![BashNode::StringLit("-l".to_string())],
1584            }),
1585        };
1586        assert_eq!(node.to_code(), "ls | wc -l");
1587    }
1588
1589    #[test]
1590    fn test_bash_node_depth() {
1591        let simple = BashNode::IntLit(1);
1592        assert_eq!(simple.depth(), 1);
1593
1594        let assign = BashNode::Assignment {
1595            name: "x".to_string(),
1596            value: Box::new(BashNode::IntLit(1)),
1597        };
1598        assert_eq!(assign.depth(), 2);
1599    }
1600
1601    #[test]
1602    fn test_bash_compare_op_all() {
1603        let ops = BashCompareOp::all();
1604        assert!(ops.len() >= 6);
1605    }
1606
1607    #[test]
1608    fn test_bash_arith_op_all() {
1609        let ops = BashArithOp::all();
1610        assert_eq!(ops.len(), 5);
1611    }
1612
1613    #[test]
1614    fn test_bash_enumerator_generates_programs() {
1615        let enumerator = BashEnumerator::new(3);
1616        let programs = enumerator.enumerate_programs();
1617        assert!(!programs.is_empty());
1618
1619        for prog in &programs {
1620            assert_eq!(prog.language, Language::Bash);
1621            assert!(prog.ast_depth <= 3);
1622        }
1623    }
1624
1625    #[test]
1626    fn test_bash_enumerator_depth_1() {
1627        let enumerator = BashEnumerator::new(1);
1628        let programs = enumerator.enumerate_programs();
1629
1630        for prog in &programs {
1631            assert!(prog.ast_depth <= 1);
1632        }
1633    }
1634
1635    #[test]
1636    fn test_bash_enumerator_depth_2() {
1637        let enumerator = BashEnumerator::new(2);
1638        let programs = enumerator.enumerate_programs();
1639
1640        // Should have more programs than depth 1
1641        let depth1_count = BashEnumerator::new(1).enumerate_programs().len();
1642        assert!(programs.len() > depth1_count);
1643    }
1644
1645    #[test]
1646    fn test_bash_node_features() {
1647        let node = BashNode::If {
1648            condition: Box::new(BashNode::Test {
1649                double: false,
1650                expr: Box::new(BashNode::Variable("x".to_string())),
1651            }),
1652            then_body: vec![],
1653            else_body: vec![],
1654        };
1655        let features = node.features();
1656        assert!(features.contains(&"if".to_string()));
1657    }
1658}
1659
1660#[test]
1661fn test_bash_program_count() {
1662    let enumerator = BashEnumerator::new(3);
1663    let programs = enumerator.enumerate_programs();
1664    println!("Generated {} bash programs at depth 3", programs.len());
1665    assert!(
1666        programs.len() >= 1000,
1667        "Expected at least 1000 programs, got {}",
1668        programs.len()
1669    );
1670}
1671
1672#[test]
1673fn test_bash_program_uniqueness() {
1674    use std::collections::HashSet;
1675
1676    let enumerator = BashEnumerator::new(3);
1677    let programs = enumerator.enumerate_programs();
1678
1679    let unique: HashSet<_> = programs.iter().map(|p| &p.code).collect();
1680    let unique_count = unique.len();
1681    let total_count = programs.len();
1682
1683    println!(
1684        "Unique: {}/{} ({:.1}%)",
1685        unique_count,
1686        total_count,
1687        100.0 * unique_count as f64 / total_count as f64
1688    );
1689
1690    // At least 80% should be unique
1691    assert!(
1692        unique_count as f64 / total_count as f64 >= 0.80,
1693        "Expected at least 80% unique programs, got {}%",
1694        100 * unique_count / total_count
1695    );
1696}
1697
1698#[test]
1699fn test_bash_feature_coverage() {
1700    use std::collections::HashSet;
1701
1702    // Use depth 5 to include while loops (they have depth 5: While > body > Assignment > Arithmetic > ArithOp)
1703    let enumerator = BashEnumerator::new(5);
1704    let programs = enumerator.enumerate_programs();
1705
1706    let mut all_features = HashSet::new();
1707    let mut all_code = String::new();
1708
1709    for prog in &programs {
1710        for feature in &prog.features {
1711            all_features.insert(feature.clone());
1712        }
1713        all_code.push_str(&prog.code);
1714        all_code.push('\n');
1715    }
1716
1717    println!("Covered features: {:?}", all_features);
1718
1719    // Should cover core bash features (check code content for control flow)
1720    let required_features = ["assignment", "command", "for", "function", "pipe", "case"];
1721    for feature in required_features {
1722        assert!(
1723            all_features.iter().any(|f| f.contains(feature)),
1724            "Missing feature: {feature}"
1725        );
1726    }
1727
1728    // Check code content for if/while (may not be in features list due to AST structure)
1729    // Note: If statements generate "if [ ... ]" or "if [[ ... ]]"
1730    let has_if = all_code.contains("if [") || all_code.contains("if;");
1731    let has_while = all_code.contains("while [") || all_code.contains("while;");
1732
1733    // Verify control flow features
1734    let has_if_feature = all_features.contains("if");
1735    let has_while_feature = all_features.contains("while");
1736
1737    assert!(has_if || has_if_feature, "Missing if statements");
1738    assert!(has_while || has_while_feature, "Missing while loops");
1739}
1740
1741#[test]
1742fn test_bash_depth_distribution() {
1743    let enumerator = BashEnumerator::new(3);
1744    let programs = enumerator.enumerate_programs();
1745
1746    let mut depth_counts = [0usize; 4];
1747    for prog in &programs {
1748        if prog.ast_depth <= 3 {
1749            depth_counts[prog.ast_depth] += 1;
1750        }
1751    }
1752
1753    println!("Depth distribution: {:?}", depth_counts);
1754
1755    // Should have programs at multiple depths
1756    assert!(depth_counts[1] > 0, "No depth-1 programs");
1757    assert!(depth_counts[2] > 0, "No depth-2 programs");
1758}