Skip to main content

awk_rs/interpreter/
mod.rs

1mod builtins;
2mod expr;
3pub mod stmt;
4
5use std::cell::Cell;
6use std::collections::HashMap;
7use std::fs::File;
8use std::io::{BufRead, BufReader, Write};
9use std::process::{Child, ChildStdin, ChildStdout};
10
11use crate::ast::*;
12use crate::error::{Error, Result};
13use crate::value::Value;
14
15use regex::Regex;
16
17/// Input source for getline from pipe
18pub struct PipeInput {
19    #[allow(dead_code)]
20    child: Child,
21    reader: BufReader<ChildStdout>,
22}
23
24/// Output destination for print/printf redirection
25pub enum OutputFile {
26    File(File),
27    Pipe(ChildStdin),
28}
29
30impl Write for OutputFile {
31    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
32        match self {
33            OutputFile::File(f) => f.write(buf),
34            OutputFile::Pipe(p) => p.write(buf),
35        }
36    }
37
38    fn flush(&mut self) -> std::io::Result<()> {
39        match self {
40            OutputFile::File(f) => f.flush(),
41            OutputFile::Pipe(p) => p.flush(),
42        }
43    }
44}
45
46thread_local! {
47    /// Type-erased pointer to the main input reader that is currently being
48    /// processed by [`Interpreter::process_input`].
49    ///
50    /// This exists so that a bare `getline` / `getline var` (which is evaluated
51    /// deep inside expression evaluation) can pull the next record from the very
52    /// same reader the main record loop is draining.
53    ///
54    /// Invariants that make the `unsafe` use of this pointer sound:
55    /// * It is only ever set by [`MainInputGuard::new`], which is created inside
56    ///   `process_input` from a `&mut` borrow of a reader that lives on that
57    ///   stack frame.
58    /// * The guard restores the previous value on drop (including while
59    ///   unwinding), so the pointer is never observable after the reader dies,
60    ///   and nested interpreters/inputs stack correctly.
61    /// * The reader is never touched directly while the guard is alive; every
62    ///   access goes through this pointer, so no two `&mut` are live at once.
63    static MAIN_INPUT: Cell<Option<*mut (dyn BufRead + 'static)>> = const { Cell::new(None) };
64}
65
66/// RAII guard that publishes the active main input reader for bare `getline`.
67struct MainInputGuard {
68    previous: Option<*mut (dyn BufRead + 'static)>,
69}
70
71impl MainInputGuard {
72    /// # Safety
73    /// `reader` must remain valid, and must not be accessed through any other
74    /// reference, for the entire lifetime of the returned guard.
75    unsafe fn new(reader: &mut dyn BufRead) -> Self {
76        let ptr: *mut (dyn BufRead + '_) = reader;
77        // Erase the lifetime; see the invariants documented on MAIN_INPUT.
78        let ptr: *mut (dyn BufRead + 'static) = unsafe { std::mem::transmute(ptr) };
79        let previous = MAIN_INPUT.with(|slot| slot.replace(Some(ptr)));
80        MainInputGuard { previous }
81    }
82}
83
84impl Drop for MainInputGuard {
85    fn drop(&mut self) {
86        MAIN_INPUT.with(|slot| slot.set(self.previous));
87    }
88}
89
90/// Read a single record from `reader`.
91///
92/// Returns `Ok(None)` at end of input. The record separator is stripped.
93/// When `paragraph` is true (RS == ""), a record is a run of non-blank lines
94/// delimited by one or more blank lines.
95fn read_record_from(reader: &mut dyn BufRead, paragraph: bool) -> Result<Option<String>> {
96    let mut line = String::new();
97
98    if !paragraph {
99        let bytes_read = reader.read_line(&mut line).map_err(Error::Io)?;
100        if bytes_read == 0 {
101            return Ok(None);
102        }
103        if line.ends_with('\n') {
104            line.pop();
105            if line.ends_with('\r') {
106                line.pop();
107            }
108        }
109        return Ok(Some(line));
110    }
111
112    // Paragraph mode: skip leading blank lines, then accumulate until blank/EOF.
113    let mut record = String::new();
114    let mut in_record = false;
115
116    loop {
117        line.clear();
118        let bytes_read = reader.read_line(&mut line).map_err(Error::Io)?;
119
120        if bytes_read == 0 {
121            // EOF - emit whatever we have accumulated
122            if in_record && !record.is_empty() {
123                return Ok(Some(record));
124            }
125            return Ok(None);
126        }
127
128        if line.trim().is_empty() {
129            if in_record && !record.is_empty() {
130                return Ok(Some(record));
131            }
132            // Leading blank line - skip it
133            continue;
134        }
135
136        if line.ends_with('\n') {
137            line.pop();
138            if line.ends_with('\r') {
139                line.pop();
140            }
141        }
142        if in_record {
143            record.push('\n');
144        }
145        record.push_str(&line);
146        in_record = true;
147    }
148}
149
150/// The AWK interpreter runtime
151pub struct Interpreter<'a> {
152    /// The parsed program
153    program: &'a Program,
154
155    /// Global variables
156    pub(crate) variables: HashMap<String, Value>,
157
158    /// Associative arrays
159    pub(crate) arrays: HashMap<String, HashMap<String, Value>>,
160
161    /// User-defined functions
162    pub(crate) functions: HashMap<String, &'a FunctionDef>,
163
164    /// Built-in variables
165    /// Field separator (FS)
166    pub(crate) fs: String,
167    /// Output field separator (OFS)
168    pub(crate) ofs: String,
169    /// Record separator (RS)
170    pub(crate) rs: String,
171    /// Output record separator (ORS)
172    pub(crate) ors: String,
173    /// Number format for output (OFMT)
174    pub(crate) ofmt: String,
175    /// Conversion format (CONVFMT)
176    pub(crate) convfmt: String,
177    /// Subscript separator (SUBSEP)
178    pub(crate) subsep: String,
179    /// Field pattern (FPAT) - gawk extension
180    pub(crate) fpat: String,
181    /// Fixed field widths (FIELDWIDTHS) - gawk extension
182    pub(crate) fieldwidths: String,
183
184    /// Mode flags
185    pub(crate) posix_mode: bool,
186    pub(crate) traditional_mode: bool,
187
188    /// Current record ($0)
189    pub(crate) record: String,
190    /// Current fields ($1, $2, ...)
191    pub(crate) fields: Vec<String>,
192    /// Number of fields (NF)
193    pub(crate) nf: usize,
194    /// Record number (NR)
195    pub(crate) nr: usize,
196    /// File record number (FNR)
197    pub(crate) fnr: usize,
198    /// Current filename (FILENAME)
199    pub(crate) filename: String,
200
201    /// Names of the input sources passed to `run()`, in order.
202    /// Used to keep FILENAME correct across a multi-file run.
203    pub(crate) filenames: Vec<String>,
204
205    /// RSTART and RLENGTH from match()
206    pub(crate) rstart: usize,
207    pub(crate) rlength: i32,
208
209    /// Control flow flags
210    should_exit: bool,
211    exit_code: i32,
212    should_next: bool,
213    should_nextfile: bool,
214
215    /// Open files for output redirection
216    pub(crate) output_files: HashMap<String, OutputFile>,
217
218    /// Open files for input (getline)
219    pub(crate) input_files: HashMap<String, BufReader<File>>,
220
221    /// Open pipes for input (getline from command)
222    pub(crate) pipes: HashMap<String, PipeInput>,
223
224    /// Compiled regex cache
225    pub(crate) regex_cache: HashMap<String, Regex>,
226
227    /// Range pattern state (for /start/,/end/ patterns)
228    range_states: HashMap<usize, bool>,
229
230    /// Random number generator state
231    pub(crate) rand_seed: u64,
232    pub(crate) rand_state: u64,
233
234    /// Command line arguments (ARGC, ARGV)
235    pub(crate) argc: usize,
236    pub(crate) argv: Vec<String>,
237
238    /// Environment variables (ENVIRON)
239    pub(crate) environ: HashMap<String, String>,
240
241    /// Array aliases for pass-by-reference in functions
242    /// Maps parameter name -> actual array name
243    pub(crate) array_aliases: HashMap<String, String>,
244}
245
246impl<'a> Interpreter<'a> {
247    pub fn new(program: &'a Program) -> Self {
248        let mut functions = HashMap::new();
249        for func in &program.functions {
250            functions.insert(func.name.clone(), func);
251        }
252
253        // Initialize environment variables
254        let environ: HashMap<String, String> = std::env::vars().collect();
255
256        // Initialize random seed from current time
257        use std::time::{SystemTime, UNIX_EPOCH};
258        let rand_seed = SystemTime::now()
259            .duration_since(UNIX_EPOCH)
260            .map(|d| d.as_nanos() as u64)
261            .unwrap_or(12345);
262
263        Self {
264            program,
265            variables: HashMap::new(),
266            arrays: HashMap::new(),
267            functions,
268            fs: " ".to_string(),
269            ofs: " ".to_string(),
270            rs: "\n".to_string(),
271            ors: "\n".to_string(),
272            ofmt: "%.6g".to_string(),
273            convfmt: "%.6g".to_string(),
274            subsep: "\x1c".to_string(),
275            fpat: String::new(),
276            fieldwidths: String::new(),
277            posix_mode: false,
278            traditional_mode: false,
279            record: String::new(),
280            fields: Vec::new(),
281            nf: 0,
282            nr: 0,
283            fnr: 0,
284            filename: String::new(),
285            filenames: Vec::new(),
286            rstart: 0,
287            rlength: -1,
288            should_exit: false,
289            exit_code: 0,
290            should_next: false,
291            should_nextfile: false,
292            output_files: HashMap::new(),
293            input_files: HashMap::new(),
294            pipes: HashMap::new(),
295            regex_cache: HashMap::new(),
296            range_states: HashMap::new(),
297            rand_seed,
298            rand_state: rand_seed,
299            argc: 0,
300            argv: Vec::new(),
301            environ,
302            array_aliases: HashMap::new(),
303        }
304    }
305
306    /// Set command line arguments (ARGC and ARGV)
307    pub fn set_args(&mut self, args: Vec<String>) {
308        self.argc = args.len();
309        self.argv = args;
310    }
311
312    /// Set the field separator
313    pub fn set_fs(&mut self, fs: &str) {
314        self.fs = fs.to_string();
315        // Clear FPAT and FIELDWIDTHS when FS is set
316        self.fpat.clear();
317        self.fieldwidths.clear();
318    }
319
320    /// Set POSIX strict mode
321    pub fn set_posix_mode(&mut self, enabled: bool) {
322        self.posix_mode = enabled;
323        if enabled {
324            self.traditional_mode = false;
325        }
326    }
327
328    /// Set traditional AWK mode (no gawk extensions)
329    pub fn set_traditional_mode(&mut self, enabled: bool) {
330        self.traditional_mode = enabled;
331        if enabled {
332            self.posix_mode = false;
333        }
334    }
335
336    /// Set a variable before execution
337    pub fn set_variable(&mut self, name: &str, value: &str) {
338        self.variables
339            .insert(name.to_string(), Value::from_string(value.to_string()));
340    }
341
342    /// Set the current filename (FILENAME)
343    pub fn set_filename(&mut self, filename: &str) {
344        self.filename = filename.to_string();
345    }
346
347    /// Set the names of the input sources that will be passed to [`Self::run`].
348    ///
349    /// The i-th name is installed into FILENAME before the i-th reader is
350    /// processed, so a single `run()` call over several files still reports the
351    /// correct FILENAME while keeping BEGIN/END to one execution each.
352    pub fn set_filenames(&mut self, filenames: Vec<String>) {
353        if let Some(first) = filenames.first() {
354            self.filename = first.clone();
355        }
356        self.filenames = filenames;
357    }
358
359    /// Run the AWK program with given input
360    pub fn run<R: BufRead, W: Write>(&mut self, inputs: Vec<R>, output: &mut W) -> Result<i32> {
361        // Execute BEGIN rules
362        for rule in &self.program.rules {
363            if matches!(&rule.pattern, Some(Pattern::Begin)) {
364                if let Some(action) = &rule.action {
365                    self.execute_block(action, output)?;
366                }
367                if self.should_exit {
368                    return Ok(self.exit_code);
369                }
370            }
371        }
372
373        // Process input files
374        for (idx, input) in inputs.into_iter().enumerate() {
375            self.fnr = 0;
376
377            // Keep FILENAME correct per file within this single run.
378            if let Some(name) = self.filenames.get(idx) {
379                self.filename = name.clone();
380            }
381
382            // Execute BEGINFILE rules (gawk extension)
383            for rule in &self.program.rules {
384                if matches!(&rule.pattern, Some(Pattern::BeginFile)) {
385                    if let Some(action) = &rule.action {
386                        self.execute_block(action, output)?;
387                    }
388                    if self.should_exit {
389                        return Ok(self.exit_code);
390                    }
391                }
392            }
393
394            self.process_input(input, output)?;
395
396            // Execute ENDFILE rules (gawk extension)
397            for rule in &self.program.rules {
398                if matches!(&rule.pattern, Some(Pattern::EndFile)) {
399                    if let Some(action) = &rule.action {
400                        self.execute_block(action, output)?;
401                    }
402                    if self.should_exit {
403                        return Ok(self.exit_code);
404                    }
405                }
406            }
407
408            if self.should_exit {
409                return Ok(self.exit_code);
410            }
411        }
412
413        // Execute END rules
414        for rule in &self.program.rules {
415            if let (Some(Pattern::End), Some(action)) = (&rule.pattern, &rule.action) {
416                self.execute_block(action, output)?;
417            }
418        }
419
420        Ok(self.exit_code)
421    }
422
423    fn process_input<R: BufRead, W: Write>(&mut self, mut input: R, output: &mut W) -> Result<()> {
424        // Publish the reader so that a bare `getline` can pull from it too.
425        // The guard clears it again when this frame ends (see MAIN_INPUT).
426        let _guard = unsafe { MainInputGuard::new(&mut input) };
427        self.process_input_loop(output)
428    }
429
430    /// The main record loop. Records are pulled through `next_main_record` so
431    /// that bare `getline` shares the exact same reader position.
432    fn process_input_loop<W: Write>(&mut self, output: &mut W) -> Result<()> {
433        while let Some(record) = self.next_main_record()? {
434            self.nr += 1;
435            self.fnr += 1;
436            self.set_record(&record);
437
438            self.process_current_record(output)?;
439
440            if self.should_nextfile {
441                self.should_nextfile = false;
442                break;
443            }
444
445            if self.should_exit {
446                break;
447            }
448        }
449
450        Ok(())
451    }
452
453    /// Read the next record from the main input, honouring paragraph mode
454    /// (RS == ""). Returns `Ok(None)` at EOF or when there is no main input
455    /// (e.g. inside BEGIN before any file has been opened).
456    ///
457    /// Does *not* touch NR/FNR/$0 - callers decide what to update.
458    pub(crate) fn next_main_record(&mut self) -> Result<Option<String>> {
459        let paragraph = self.rs.is_empty();
460        let ptr = MAIN_INPUT.with(|slot| slot.get());
461        match ptr {
462            // SAFETY: the pointer is only ever non-null while the guard created
463            // in `process_input` is alive, which keeps the reader borrowed and
464            // untouched by anything else. See MAIN_INPUT for the full argument.
465            Some(ptr) => read_record_from(unsafe { &mut *ptr }, paragraph),
466            None => Ok(None),
467        }
468    }
469
470    /// Process the current record through all matching rules
471    fn process_current_record<W: Write>(&mut self, output: &mut W) -> Result<()> {
472        for (idx, rule) in self.program.rules.iter().enumerate() {
473            // Skip special patterns that are handled separately
474            if matches!(
475                &rule.pattern,
476                Some(Pattern::Begin)
477                    | Some(Pattern::End)
478                    | Some(Pattern::BeginFile)
479                    | Some(Pattern::EndFile)
480            ) {
481                continue;
482            }
483
484            let matches = self.pattern_matches(&rule.pattern, idx)?;
485            if matches {
486                if let Some(action) = &rule.action {
487                    self.execute_block(action, output)?;
488                } else {
489                    // Default action is to print $0
490                    writeln!(output, "{}", self.record).map_err(Error::Io)?;
491                }
492            }
493
494            if self.should_next {
495                self.should_next = false;
496                break;
497            }
498
499            if self.should_nextfile || self.should_exit {
500                break;
501            }
502        }
503        Ok(())
504    }
505
506    pub(crate) fn set_record(&mut self, record: &str) {
507        self.record = record.to_string();
508        self.split_fields();
509    }
510
511    fn split_fields(&mut self) {
512        self.fields.clear();
513
514        if self.record.is_empty() {
515            self.nf = 0;
516            return;
517        }
518
519        // Pre-estimate capacity to reduce reallocations
520        let estimated_fields = self.record.len() / 8 + 1;
521        self.fields.reserve(estimated_fields.min(64));
522
523        // Check for FPAT (field pattern) - gawk extension
524        if !self.fpat.is_empty() && !self.posix_mode && !self.traditional_mode {
525            self.split_fields_fpat();
526            return;
527        }
528
529        // Check for FIELDWIDTHS - gawk extension
530        if !self.fieldwidths.is_empty() && !self.posix_mode && !self.traditional_mode {
531            self.split_fields_widths();
532            return;
533        }
534
535        // Standard FS-based splitting
536        if self.fs == " " {
537            // Special case: split on runs of whitespace, trimming leading/trailing
538            // Use byte-based iteration for ASCII optimization
539            self.fields
540                .extend(self.record.split_whitespace().map(String::from));
541        } else if self.fs.len() == 1 {
542            // Single character separator - most common case, optimize for it
543            let sep = self.fs.as_bytes()[0];
544            let bytes = self.record.as_bytes();
545            let mut start = 0;
546
547            for (i, &b) in bytes.iter().enumerate() {
548                if b == sep {
549                    self.fields.push(self.record[start..i].to_string());
550                    start = i + 1;
551                }
552            }
553            // Don't forget the last field
554            self.fields.push(self.record[start..].to_string());
555        } else {
556            // Regex separator - cache the compiled regex
557            let fs = self.fs.clone();
558            let record = self.record.clone();
559            if let Some(regex) = self.regex_cache.get(&fs) {
560                self.fields.extend(regex.split(&record).map(String::from));
561            } else if let Ok(regex) = Regex::new(&fs) {
562                self.fields.extend(regex.split(&record).map(String::from));
563                self.regex_cache.insert(fs, regex);
564            } else {
565                // If regex fails, treat as literal string
566                self.fields.extend(record.split(&fs).map(String::from));
567            }
568        }
569
570        self.nf = self.fields.len();
571    }
572
573    /// Split fields using FPAT (field pattern matching)
574    fn split_fields_fpat(&mut self) {
575        let fpat = self.fpat.clone();
576        let record = self.record.clone();
577
578        if let Some(regex) = self.regex_cache.get(&fpat) {
579            for mat in regex.find_iter(&record) {
580                self.fields.push(mat.as_str().to_string());
581            }
582        } else if let Ok(regex) = Regex::new(&fpat) {
583            for mat in regex.find_iter(&record) {
584                self.fields.push(mat.as_str().to_string());
585            }
586            self.regex_cache.insert(fpat, regex);
587        }
588
589        self.nf = self.fields.len();
590    }
591
592    /// Split fields using FIELDWIDTHS (fixed-width fields)
593    fn split_fields_widths(&mut self) {
594        let widths: Vec<usize> = self
595            .fieldwidths
596            .split_whitespace()
597            .filter_map(|s| s.parse().ok())
598            .collect();
599
600        let mut pos = 0;
601        let chars: Vec<char> = self.record.chars().collect();
602
603        for width in widths {
604            if pos >= chars.len() {
605                break;
606            }
607            let end = (pos + width).min(chars.len());
608            let field: String = chars[pos..end].iter().collect();
609            self.fields.push(field);
610            pos = end;
611        }
612
613        self.nf = self.fields.len();
614    }
615
616    #[inline]
617    pub(crate) fn get_field(&self, index: usize) -> String {
618        if index == 0 {
619            self.record.clone()
620        } else if index <= self.fields.len() {
621            self.fields[index - 1].clone()
622        } else {
623            String::new()
624        }
625    }
626
627    /// Get field reference without cloning (for read-only access)
628    #[inline]
629    #[allow(dead_code)]
630    pub(crate) fn get_field_ref(&self, index: usize) -> &str {
631        if index == 0 {
632            &self.record
633        } else if index <= self.fields.len() {
634            &self.fields[index - 1]
635        } else {
636            ""
637        }
638    }
639
640    pub(crate) fn set_field(&mut self, index: usize, value: String) {
641        if index == 0 {
642            self.record = value;
643            self.split_fields();
644        } else {
645            // Extend fields if necessary
646            while self.fields.len() < index {
647                self.fields.push(String::new());
648            }
649            self.fields[index - 1] = value;
650            self.nf = self.fields.len();
651            // Rebuild $0
652            self.record = self.fields.join(&self.ofs);
653        }
654    }
655
656    fn pattern_matches(&mut self, pattern: &Option<Pattern>, rule_idx: usize) -> Result<bool> {
657        match pattern {
658            None => Ok(true), // No pattern means always match
659            Some(Pattern::Begin)
660            | Some(Pattern::End)
661            | Some(Pattern::BeginFile)
662            | Some(Pattern::EndFile) => Ok(false),
663            Some(Pattern::Expr(expr)) => {
664                let val = self.eval_expr(expr)?;
665                Ok(val.is_truthy())
666            }
667            Some(Pattern::Regex(regex)) => {
668                let record = self.record.clone();
669                let re = self.get_regex(regex)?;
670                Ok(re.is_match(&record))
671            }
672            Some(Pattern::Range { start, end }) => {
673                let active = self.range_states.get(&rule_idx).copied().unwrap_or(false);
674                if !active {
675                    // Check if start pattern matches
676                    if self.pattern_matches(&Some(start.as_ref().clone()), rule_idx)? {
677                        self.range_states.insert(rule_idx, true);
678                        return Ok(true);
679                    }
680                    Ok(false)
681                } else {
682                    // Range is active, check if end pattern matches
683                    if self.pattern_matches(&Some(end.as_ref().clone()), rule_idx)? {
684                        self.range_states.insert(rule_idx, false);
685                    }
686                    Ok(true)
687                }
688            }
689            Some(Pattern::And(left, right)) => Ok(self
690                .pattern_matches(&Some(left.as_ref().clone()), rule_idx)?
691                && self.pattern_matches(&Some(right.as_ref().clone()), rule_idx)?),
692            Some(Pattern::Or(left, right)) => Ok(self
693                .pattern_matches(&Some(left.as_ref().clone()), rule_idx)?
694                || self.pattern_matches(&Some(right.as_ref().clone()), rule_idx)?),
695            Some(Pattern::Not(inner)) => {
696                Ok(!self.pattern_matches(&Some(inner.as_ref().clone()), rule_idx)?)
697            }
698        }
699    }
700
701    pub(crate) fn get_regex(&mut self, pattern: &str) -> Result<&Regex> {
702        if !self.regex_cache.contains_key(pattern) {
703            let regex = Regex::new(pattern).map_err(Error::Regex)?;
704            self.regex_cache.insert(pattern.to_string(), regex);
705        }
706        Ok(self.regex_cache.get(pattern).unwrap())
707    }
708
709    pub(crate) fn get_variable(&self, name: &str) -> Value {
710        // Check special variables first
711        match name {
712            "NF" => Value::Number(self.nf as f64),
713            "NR" => Value::Number(self.nr as f64),
714            "FNR" => Value::Number(self.fnr as f64),
715            "FS" => Value::from_string(self.fs.clone()),
716            "OFS" => Value::from_string(self.ofs.clone()),
717            "RS" => Value::from_string(self.rs.clone()),
718            "ORS" => Value::from_string(self.ors.clone()),
719            "OFMT" => Value::from_string(self.ofmt.clone()),
720            "CONVFMT" => Value::from_string(self.convfmt.clone()),
721            "SUBSEP" => Value::from_string(self.subsep.clone()),
722            "FILENAME" => Value::from_string(self.filename.clone()),
723            "RSTART" => Value::Number(self.rstart as f64),
724            "RLENGTH" => Value::Number(self.rlength as f64),
725            "ARGC" => Value::Number(self.argc as f64),
726            // gawk extensions
727            "FPAT" => Value::from_string(self.fpat.clone()),
728            "FIELDWIDTHS" => Value::from_string(self.fieldwidths.clone()),
729            _ => self
730                .variables
731                .get(name)
732                .cloned()
733                .unwrap_or(Value::Uninitialized),
734        }
735    }
736
737    /// Get an element from ARGV, ENVIRON, or PROCINFO arrays
738    pub(crate) fn get_special_array(&self, array: &str, key: &str) -> Option<Value> {
739        match array {
740            "ARGV" => key
741                .parse::<usize>()
742                .ok()
743                .and_then(|i| self.argv.get(i))
744                .map(|s| Value::from_string(s.clone())),
745            "ENVIRON" => self.environ.get(key).map(|s| Value::from_string(s.clone())),
746            "PROCINFO" => {
747                // gawk PROCINFO array - system information
748                match key {
749                    "version" => Some(Value::from_string(env!("CARGO_PKG_VERSION").to_string())),
750                    "strftime" => Some(Value::from_string("%a %b %e %H:%M:%S %Z %Y".to_string())),
751                    "FS" => {
752                        if !self.fpat.is_empty() {
753                            Some(Value::from_string("FPAT".to_string()))
754                        } else if !self.fieldwidths.is_empty() {
755                            Some(Value::from_string("FIELDWIDTHS".to_string()))
756                        } else {
757                            Some(Value::from_string("FS".to_string()))
758                        }
759                    }
760                    "identifiers" => Some(Value::Number(0.0)), // Not implemented
761                    "pid" => Some(Value::Number(std::process::id() as f64)),
762                    "ppid" => Some(Value::Number(0.0)), // Not easily available in Rust
763                    "uid" => Some(Value::Number(0.0)),  // Platform specific
764                    "gid" => Some(Value::Number(0.0)),  // Platform specific
765                    "euid" => Some(Value::Number(0.0)), // Platform specific
766                    "egid" => Some(Value::Number(0.0)), // Platform specific
767                    "pgrpid" => Some(Value::Number(0.0)), // Platform specific
768                    _ => Some(Value::Uninitialized),
769                }
770            }
771            _ => None,
772        }
773    }
774
775    pub(crate) fn set_variable_value(&mut self, name: &str, value: Value) {
776        // Handle special variables
777        match name {
778            "NF" => {
779                let new_nf = value.to_number() as usize;
780                if new_nf < self.nf {
781                    self.fields.truncate(new_nf);
782                } else {
783                    while self.fields.len() < new_nf {
784                        self.fields.push(String::new());
785                    }
786                }
787                self.nf = new_nf;
788                self.record = self.fields.join(&self.ofs);
789            }
790            "FS" => {
791                self.fs = value.to_string_val();
792                // Clear FPAT and FIELDWIDTHS when FS is set
793                self.fpat.clear();
794                self.fieldwidths.clear();
795            }
796            "OFS" => self.ofs = value.to_string_val(),
797            "RS" => self.rs = value.to_string_val(),
798            "ORS" => self.ors = value.to_string_val(),
799            "OFMT" => self.ofmt = value.to_string_val(),
800            "CONVFMT" => self.convfmt = value.to_string_val(),
801            "SUBSEP" => self.subsep = value.to_string_val(),
802            // gawk extensions
803            "FPAT" => {
804                self.fpat = value.to_string_val();
805                // FPAT takes precedence over FS and FIELDWIDTHS
806                self.fieldwidths.clear();
807            }
808            "FIELDWIDTHS" => {
809                self.fieldwidths = value.to_string_val();
810                // FIELDWIDTHS takes precedence over FS
811                self.fpat.clear();
812            }
813            _ => {
814                self.variables.insert(name.to_string(), value);
815            }
816        }
817    }
818
819    /// Resolve array name through aliases (for pass-by-reference in functions)
820    fn resolve_array_name<'b>(&'b self, array: &'b str) -> &'b str {
821        self.array_aliases
822            .get(array)
823            .map(|s| s.as_str())
824            .unwrap_or(array)
825    }
826
827    pub(crate) fn get_array_element(&self, array: &str, key: &str) -> Value {
828        let array = self.resolve_array_name(array);
829
830        // Check for special arrays first
831        if let Some(val) = self.get_special_array(array, key) {
832            return val;
833        }
834
835        self.arrays
836            .get(array)
837            .and_then(|arr| arr.get(key))
838            .cloned()
839            .unwrap_or(Value::Uninitialized)
840    }
841
842    pub(crate) fn set_array_element(&mut self, array: &str, key: &str, value: Value) {
843        let array = self.resolve_array_name(array).to_string();
844        self.arrays
845            .entry(array)
846            .or_default()
847            .insert(key.to_string(), value);
848    }
849
850    pub(crate) fn array_key_exists(&self, array: &str, key: &str) -> bool {
851        let array = self.resolve_array_name(array);
852
853        // Check special arrays
854        match array {
855            "ARGV" => key
856                .parse::<usize>()
857                .ok()
858                .map(|i| i < self.argv.len())
859                .unwrap_or(false),
860            "ENVIRON" => self.environ.contains_key(key),
861            _ => self
862                .arrays
863                .get(array)
864                .map(|arr| arr.contains_key(key))
865                .unwrap_or(false),
866        }
867    }
868
869    pub(crate) fn delete_array_element(&mut self, array: &str, key: &str) {
870        let array = self.resolve_array_name(array).to_string();
871        if let Some(arr) = self.arrays.get_mut(&array) {
872            arr.remove(key);
873        }
874    }
875
876    pub(crate) fn make_array_key(&self, indices: &[Value]) -> String {
877        indices
878            .iter()
879            .map(|v| self.to_conv_str(v))
880            .collect::<Vec<_>>()
881            .join(&self.subsep)
882    }
883
884    /// Convert a value to a string for use inside an expression (concatenation,
885    /// string comparison, array subscripts, ...), honoring CONVFMT.
886    #[inline]
887    pub(crate) fn to_conv_str(&self, value: &Value) -> String {
888        value.to_string_with_format(&self.convfmt)
889    }
890
891    /// Convert a value to a string for `print`, honoring OFMT.
892    #[inline]
893    pub(crate) fn to_output_str(&self, value: &Value) -> String {
894        value.to_string_with_format(&self.ofmt)
895    }
896}
897
898#[cfg(test)]
899mod tests {
900    use super::*;
901    use crate::lexer::Lexer;
902    use crate::parser::Parser;
903    use std::io::Cursor;
904
905    fn run_awk(program: &str, input: &str) -> String {
906        let mut lexer = Lexer::new(program);
907        let tokens = lexer.tokenize().unwrap();
908        let mut parser = Parser::new(tokens);
909        let ast = parser.parse().unwrap();
910
911        let mut interpreter = Interpreter::new(&ast);
912        let mut output = Vec::new();
913        let inputs: Vec<std::io::BufReader<Cursor<&str>>> = if input.is_empty() {
914            vec![]
915        } else {
916            vec![std::io::BufReader::new(Cursor::new(input))]
917        };
918
919        interpreter.run(inputs, &mut output).unwrap();
920        String::from_utf8(output).unwrap()
921    }
922
923    #[test]
924    fn test_begin_print() {
925        let output = run_awk(r#"BEGIN { print "hello" }"#, "");
926        assert_eq!(output, "hello\n");
927    }
928
929    #[test]
930    fn test_print_field() {
931        let output = run_awk("{ print $1 }", "one two three");
932        assert_eq!(output, "one\n");
933    }
934
935    #[test]
936    fn test_print_multiple_fields() {
937        let output = run_awk("{ print $1, $3 }", "one two three");
938        assert_eq!(output, "one three\n");
939    }
940
941    #[test]
942    fn test_arithmetic() {
943        let output = run_awk("BEGIN { print 2 + 3 * 4 }", "");
944        assert_eq!(output, "14\n");
945    }
946
947    #[test]
948    fn test_variable() {
949        let output = run_awk("BEGIN { x = 5; print x }", "");
950        assert_eq!(output, "5\n");
951    }
952
953    #[test]
954    fn test_if_statement() {
955        let output = run_awk("BEGIN { x = 10; if (x > 5) print \"big\" }", "");
956        assert_eq!(output, "big\n");
957    }
958
959    #[test]
960    fn test_while_loop() {
961        let output = run_awk("BEGIN { i = 1; while (i <= 3) { print i; i++ } }", "");
962        assert_eq!(output, "1\n2\n3\n");
963    }
964
965    #[test]
966    fn test_pattern_match() {
967        let output = run_awk("/two/ { print $0 }", "one\ntwo\nthree");
968        assert_eq!(output, "two\n");
969    }
970
971    #[test]
972    fn test_for_loop() {
973        let output = run_awk("BEGIN { for (i = 1; i <= 3; i++) print i }", "");
974        assert_eq!(output, "1\n2\n3\n");
975    }
976
977    #[test]
978    fn test_for_in_loop() {
979        let output = run_awk(
980            "BEGIN { a[1]=1; a[2]=2; for (k in a) count++; print count }",
981            "",
982        );
983        assert_eq!(output, "2\n");
984    }
985
986    #[test]
987    fn test_do_while() {
988        let output = run_awk("BEGIN { i = 0; do { i++ } while (i < 3); print i }", "");
989        assert_eq!(output, "3\n");
990    }
991
992    #[test]
993    fn test_break() {
994        let output = run_awk(
995            "BEGIN { for (i=1; i<=10; i++) { if (i==3) break; print i } }",
996            "",
997        );
998        assert_eq!(output, "1\n2\n");
999    }
1000
1001    #[test]
1002    fn test_continue() {
1003        let output = run_awk(
1004            "BEGIN { for (i=1; i<=3; i++) { if (i==2) continue; print i } }",
1005            "",
1006        );
1007        assert_eq!(output, "1\n3\n");
1008    }
1009
1010    #[test]
1011    fn test_next() {
1012        let output = run_awk("/skip/ { next } { print }", "one\nskip\ntwo");
1013        assert_eq!(output, "one\ntwo\n");
1014    }
1015
1016    #[test]
1017    fn test_exit() {
1018        // exit is called before print on NR == 2
1019        let output = run_awk("NR == 2 { exit } { print }", "one\ntwo\nthree");
1020        assert_eq!(output, "one\n");
1021    }
1022
1023    #[test]
1024    fn test_exit_in_end() {
1025        let output = run_awk("{ print } END { print \"done\" }", "one\ntwo");
1026        assert!(output.contains("done"));
1027    }
1028
1029    #[test]
1030    fn test_array_access() {
1031        let output = run_awk("BEGIN { a[\"x\"] = 1; print a[\"x\"] }", "");
1032        assert_eq!(output, "1\n");
1033    }
1034
1035    #[test]
1036    fn test_array_in() {
1037        let output = run_awk("BEGIN { a[1]=1; print (1 in a), (2 in a) }", "");
1038        assert_eq!(output, "1 0\n");
1039    }
1040
1041    #[test]
1042    fn test_delete() {
1043        let output = run_awk(
1044            "BEGIN { a[1]=1; a[2]=2; delete a[1]; for(k in a) print k }",
1045            "",
1046        );
1047        assert_eq!(output, "2\n");
1048    }
1049
1050    #[test]
1051    fn test_special_variables() {
1052        let output = run_awk("{ print NR, NF, $0 }", "a b c");
1053        assert_eq!(output, "1 3 a b c\n");
1054    }
1055
1056    #[test]
1057    fn test_fs_change() {
1058        let output = run_awk("BEGIN { FS = \":\" } { print $1 }", "a:b:c");
1059        assert_eq!(output, "a\n");
1060    }
1061
1062    #[test]
1063    fn test_ofs() {
1064        let output = run_awk("BEGIN { OFS = \"-\" } { print $1, $2 }", "a b c");
1065        assert_eq!(output, "a-b\n");
1066    }
1067
1068    #[test]
1069    fn test_nf_access() {
1070        let output = run_awk("{ print $NF }", "a b c");
1071        assert_eq!(output, "c\n");
1072    }
1073
1074    #[test]
1075    fn test_field_modify() {
1076        let output = run_awk("{ $2 = \"X\"; print $0 }", "a b c");
1077        assert_eq!(output, "a X c\n");
1078    }
1079
1080    #[test]
1081    fn test_user_function() {
1082        let output = run_awk(
1083            "function double(x) { return x*2 } BEGIN { print double(5) }",
1084            "",
1085        );
1086        assert_eq!(output, "10\n");
1087    }
1088
1089    #[test]
1090    fn test_recursion() {
1091        let output = run_awk(
1092            "function fact(n) { return n<=1 ? 1 : n*fact(n-1) } BEGIN { print fact(5) }",
1093            "",
1094        );
1095        assert_eq!(output, "120\n");
1096    }
1097
1098    #[test]
1099    fn test_printf() {
1100        let output = run_awk("BEGIN { printf \"%d %s\\n\", 42, \"hello\" }", "");
1101        assert_eq!(output, "42 hello\n");
1102    }
1103
1104    #[test]
1105    fn test_range_pattern() {
1106        let output = run_awk(
1107            "/start/,/end/ { print }",
1108            "before\nstart\nmiddle\nend\nafter",
1109        );
1110        assert_eq!(output, "start\nmiddle\nend\n");
1111    }
1112
1113    #[test]
1114    fn test_compound_pattern_and() {
1115        let output = run_awk("NR > 1 && NR < 4 { print }", "one\ntwo\nthree\nfour");
1116        assert_eq!(output, "two\nthree\n");
1117    }
1118
1119    #[test]
1120    fn test_logical_or_in_expr() {
1121        let output = run_awk("{ if (/a/ || /c/) print }", "a\nb\nc");
1122        assert_eq!(output, "a\nc\n");
1123    }
1124
1125    #[test]
1126    fn test_negated_pattern() {
1127        let output = run_awk("!/skip/ { print }", "keep\nskip\nkeep");
1128        assert_eq!(output, "keep\nkeep\n");
1129    }
1130
1131    #[test]
1132    fn test_builtin_length() {
1133        let output = run_awk("BEGIN { print length(\"hello\") }", "");
1134        assert_eq!(output, "5\n");
1135    }
1136
1137    #[test]
1138    fn test_builtin_substr() {
1139        let output = run_awk("BEGIN { print substr(\"hello\", 2, 3) }", "");
1140        assert_eq!(output, "ell\n");
1141    }
1142
1143    #[test]
1144    fn test_builtin_index() {
1145        let output = run_awk("BEGIN { print index(\"hello\", \"ll\") }", "");
1146        assert_eq!(output, "3\n");
1147    }
1148
1149    #[test]
1150    fn test_builtin_split() {
1151        let output = run_awk(
1152            "BEGIN { n = split(\"a:b:c\", arr, \":\"); print n, arr[1], arr[2] }",
1153            "",
1154        );
1155        assert_eq!(output, "3 a b\n");
1156    }
1157
1158    #[test]
1159    fn test_builtin_sub() {
1160        let output = run_awk("BEGIN { x = \"hello\"; sub(\"l\", \"L\", x); print x }", "");
1161        assert_eq!(output, "heLlo\n");
1162    }
1163
1164    #[test]
1165    fn test_builtin_gsub() {
1166        let output = run_awk(
1167            "BEGIN { x = \"hello\"; gsub(\"l\", \"L\", x); print x }",
1168            "",
1169        );
1170        assert_eq!(output, "heLLo\n");
1171    }
1172
1173    #[test]
1174    fn test_builtin_match() {
1175        let output = run_awk("BEGIN { print match(\"hello\", \"ll\") }", "");
1176        assert_eq!(output, "3\n");
1177    }
1178
1179    #[test]
1180    fn test_builtin_sprintf() {
1181        let output = run_awk("BEGIN { print sprintf(\"%05d\", 42) }", "");
1182        assert_eq!(output, "00042\n");
1183    }
1184
1185    #[test]
1186    fn test_builtin_tolower() {
1187        let output = run_awk("BEGIN { print tolower(\"HELLO\") }", "");
1188        assert_eq!(output, "hello\n");
1189    }
1190
1191    #[test]
1192    fn test_builtin_toupper() {
1193        let output = run_awk("BEGIN { print toupper(\"hello\") }", "");
1194        assert_eq!(output, "HELLO\n");
1195    }
1196
1197    #[test]
1198    fn test_builtin_math() {
1199        let output = run_awk("BEGIN { print int(3.7), sqrt(4), sin(0) }", "");
1200        assert_eq!(output, "3 2 0\n");
1201    }
1202
1203    #[test]
1204    fn test_ternary() {
1205        let output = run_awk("BEGIN { print 1 ? \"yes\" : \"no\" }", "");
1206        assert_eq!(output, "yes\n");
1207    }
1208
1209    #[test]
1210    fn test_concatenation() {
1211        let output = run_awk("BEGIN { print \"a\" \"b\" \"c\" }", "");
1212        assert_eq!(output, "abc\n");
1213    }
1214
1215    #[test]
1216    fn test_unary_ops() {
1217        let output = run_awk("BEGIN { x = 5; print -x, +x, !0 }", "");
1218        assert_eq!(output, "-5 5 1\n");
1219    }
1220
1221    #[test]
1222    fn test_post_increment() {
1223        let output = run_awk("BEGIN { x = 5; print x++ \" \" x }", "");
1224        assert_eq!(output, "5 6\n");
1225    }
1226
1227    #[test]
1228    fn test_pre_increment() {
1229        let output = run_awk("BEGIN { x = 5; print ++x }", "");
1230        assert_eq!(output, "6\n");
1231    }
1232
1233    #[test]
1234    fn test_compound_assign() {
1235        let output = run_awk("BEGIN { x = 10; x += 5; x -= 3; x *= 2; print x }", "");
1236        assert_eq!(output, "24\n");
1237    }
1238
1239    #[test]
1240    fn test_getline_var() {
1241        // Reading "a", getline pulls "b" into next_line without touching $0.
1242        let output = run_awk("{ getline next_line; print $0, next_line }", "a\nb");
1243        assert_eq!(output, "a b\n");
1244    }
1245
1246    #[test]
1247    fn test_getline_var_updates_nr_but_not_record() {
1248        // `getline var` advances NR/FNR but leaves $0 and NF alone.
1249        let output = run_awk(
1250            "{ getline v; print $0 \"|\" v \"|\" NR \"|\" NF }",
1251            "a b\nc d",
1252        );
1253        assert_eq!(output, "a b|c d|2|2\n");
1254    }
1255
1256    #[test]
1257    fn test_bare_getline_sets_record() {
1258        // Bare getline replaces $0 (and NF) with the next record.
1259        let output = run_awk("{ getline; print $0, NF, NR }", "a\nx y z");
1260        assert_eq!(output, "x y z 3 2\n");
1261    }
1262
1263    #[test]
1264    fn test_bare_getline_returns_zero_at_eof() {
1265        let output = run_awk("{ r = getline; print r, $0 }", "a\nb\nc");
1266        assert_eq!(output, "1 b\n0 c\n");
1267    }
1268
1269    #[test]
1270    fn test_getline_in_begin_without_input_is_eof() {
1271        // No main input is open during BEGIN, so getline reports EOF.
1272        let output = run_awk("BEGIN { print getline }", "");
1273        assert_eq!(output, "0\n");
1274    }
1275
1276    #[test]
1277    fn test_begin_end_run_once_over_multiple_inputs() {
1278        let mut lexer = Lexer::new(r#"BEGIN { print "hi" } END { print NR }"#);
1279        let tokens = lexer.tokenize().unwrap();
1280        let mut parser = Parser::new(tokens);
1281        let ast = parser.parse().unwrap();
1282
1283        let mut interpreter = Interpreter::new(&ast);
1284        interpreter.set_filenames(vec!["one".to_string(), "two".to_string()]);
1285        let mut output = Vec::new();
1286        let inputs = vec![
1287            std::io::BufReader::new(Cursor::new("a\nb\n")),
1288            std::io::BufReader::new(Cursor::new("c\n")),
1289        ];
1290        interpreter.run(inputs, &mut output).unwrap();
1291
1292        assert_eq!(String::from_utf8(output).unwrap(), "hi\n3\n");
1293    }
1294
1295    #[test]
1296    fn test_filename_and_fnr_track_each_input() {
1297        let mut lexer = Lexer::new("{ print FILENAME, NR, FNR }");
1298        let tokens = lexer.tokenize().unwrap();
1299        let mut parser = Parser::new(tokens);
1300        let ast = parser.parse().unwrap();
1301
1302        let mut interpreter = Interpreter::new(&ast);
1303        interpreter.set_filenames(vec!["one".to_string(), "two".to_string()]);
1304        let mut output = Vec::new();
1305        let inputs = vec![
1306            std::io::BufReader::new(Cursor::new("a\nb\n")),
1307            std::io::BufReader::new(Cursor::new("c\n")),
1308        ];
1309        interpreter.run(inputs, &mut output).unwrap();
1310
1311        assert_eq!(
1312            String::from_utf8(output).unwrap(),
1313            "one 1 1\none 2 2\ntwo 3 1\n"
1314        );
1315    }
1316
1317    #[test]
1318    fn test_fpat() {
1319        let output = run_awk("BEGIN { FPAT = \"[^,]+\" } { print $1, $2 }", "a,b,c");
1320        assert_eq!(output, "a b\n");
1321    }
1322
1323    #[test]
1324    fn test_fieldwidths() {
1325        let output = run_awk(
1326            "BEGIN { FIELDWIDTHS = \"2 3 2\" } { print $1, $2 }",
1327            "abcdefg",
1328        );
1329        assert_eq!(output, "ab cde\n");
1330    }
1331
1332    #[test]
1333    fn test_paragraph_mode() {
1334        let output = run_awk("BEGIN { RS = \"\" } { print NR, NF }", "a b\nc d\n\ne f");
1335        // First paragraph has 4 words across 2 lines, second has 2 words
1336        assert!(output.contains("1"));
1337    }
1338}