Skip to main content

forge_kit/
parser.rs

1//! High-performance AST parser for ForgeScript with optional validation
2//!
3//! This module provides a fast, single-pass parser that builds a proper Abstract Syntax Tree
4//! with extensive optimizations for speed and memory efficiency, plus optional validation.
5
6use smallvec::SmallVec;
7
8// Optional validation support
9#[cfg(feature = "validation")]
10use crate::metadata::MetadataManager;
11#[cfg(feature = "validation")]
12use crate::types::{Arg, Function};
13#[cfg(feature = "validation")]
14use std::sync::Arc;
15
16// ============================================================================
17// Utility: Escape Detection
18// ============================================================================
19
20/// Determines if a character at a given byte index is escaped by backslashes.
21/// This checks for an odd number of preceding backslashes.
22#[inline]
23pub fn is_escaped(code: &str, byte_idx: usize) -> bool {
24    if byte_idx == 0 || !code.is_char_boundary(byte_idx) {
25        return false;
26    }
27    let bytes = code.as_bytes();
28    let mut count = 0;
29    let mut i = byte_idx;
30    while i > 0 && bytes[i - 1] == b'\\' {
31        count += 1;
32        i -= 1;
33    }
34    count % 2 != 0
35}
36
37// ============================================================================
38// Validation Configuration
39// ============================================================================
40
41/// Configuration for parser validation
42#[derive(Debug, Clone, Default)]
43pub struct ValidationConfig {
44    /// Validate argument counts against function metadata
45    pub validate_arguments: bool,
46    /// Validate enum values against defined enums
47    pub validate_enums: bool,
48    /// Validate that all functions exist in metadata
49    pub validate_functions: bool,
50    /// Validate bracket usage (required/optional/forbidden)
51    pub validate_brackets: bool,
52}
53
54impl ValidationConfig {
55    /// Enable all validations
56    pub fn strict() -> Self {
57        Self {
58            validate_arguments: true,
59            validate_enums: true,
60            validate_functions: true,
61            validate_brackets: true,
62        }
63    }
64
65    /// Enable only syntax validations (no metadata required)
66    pub fn syntax_only() -> Self {
67        Self {
68            validate_arguments: false,
69            validate_enums: false,
70            validate_functions: false,
71            validate_brackets: true,
72        }
73    }
74
75    /// Check if any validation is enabled
76    #[inline]
77    pub fn is_enabled(&self) -> bool {
78        self.validate_arguments
79            || self.validate_enums
80            || self.validate_functions
81            || self.validate_brackets
82    }
83}
84
85// ============================================================================
86// AST Node Definitions
87// ============================================================================
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct Span {
91    pub start: usize,
92    pub end: usize,
93}
94
95impl Span {
96    #[inline(always)]
97    pub const fn new(start: usize, end: usize) -> Self {
98        Self { start, end }
99    }
100
101    #[inline(always)]
102    pub fn offset(&mut self, offset: usize) {
103        self.start += offset;
104        self.end += offset;
105    }
106
107    #[inline(always)]
108    pub fn len(&self) -> usize {
109        self.end - self.start
110    }
111
112    #[inline(always)]
113    pub fn is_empty(&self) -> bool {
114        self.start >= self.end
115    }
116}
117
118#[derive(Debug, Clone, Default)]
119pub struct Modifiers {
120    pub silent: bool,
121    pub negated: bool,
122    pub count: Option<String>,
123    /// Span covering all modifier characters (e.g. `!#@[n]` before the name).
124    /// `None` if no modifiers were present.
125    pub span: Option<Span>,
126}
127
128#[derive(Debug, Clone)]
129pub struct Argument {
130    pub parts: SmallVec<[AstNode; 4]>,
131    pub span: Span,
132}
133
134impl Argument {
135    /// Check if argument is effectively empty (only whitespace/empty text nodes)
136    pub fn is_empty(&self) -> bool {
137        self.parts.iter().all(|part| match part {
138            AstNode::Text { content, .. } => content.trim().is_empty(),
139            _ => false,
140        })
141    }
142
143    /// Get literal text value if argument is purely text
144    pub fn as_text(&self) -> Option<String> {
145        if self.parts.len() == 1 {
146            if let AstNode::Text { content, .. } = &self.parts[0] {
147                return Some(content.clone());
148            }
149        }
150
151        // Try to concatenate if all parts are text
152        if self.parts.iter().all(|p| matches!(p, AstNode::Text { .. })) {
153            let mut result = String::new();
154            for part in &self.parts {
155                if let AstNode::Text { content, .. } = part {
156                    result.push_str(&content);
157                }
158            }
159            return Some(result);
160        }
161
162        None
163    }
164}
165
166#[derive(Debug, Clone)]
167pub enum AstNode {
168    Program {
169        body: Vec<AstNode>,
170        span: Span,
171    },
172    Text {
173        content: String,
174        span: Span,
175    },
176    FunctionCall {
177        name: String,
178        /// Span of the function name identifier including any modifier characters (excludes `$`).
179        name_span: Span,
180        /// Span of the modifier characters between `$` and the name (e.g. `!#@[2]`).
181        /// `None` when no modifiers are present.
182        modifier_span: Option<Span>,
183        /// Span of the argument list including the surrounding `[` and `]`.
184        /// `None` when the function was called without brackets.
185        args_span: Option<Span>,
186        args: Option<Vec<Argument>>,
187        modifiers: Modifiers,
188        /// Full span from the start of modifiers to the closing `]` (or end of name when no args).
189        /// This is the function call without the leading `$`.
190        full_span: Span,
191        /// Full span from `$` to the closing `]` (or end of name when no args).
192        span: Span,
193    },
194    JavaScript {
195        code: String,
196        span: Span,
197    },
198    Escaped {
199        content: String,
200        span: Span,
201    },
202}
203
204impl AstNode {
205    pub fn span(&self) -> Span {
206        match self {
207            AstNode::Program { span, .. }
208            | AstNode::Text { span, .. }
209            | AstNode::FunctionCall { span, .. }
210            | AstNode::JavaScript { span, .. }
211            | AstNode::Escaped { span, .. } => *span,
212        }
213    }
214
215    pub fn offset_spans(&mut self, offset: usize) {
216        match self {
217            AstNode::Program { body, span } => {
218                span.offset(offset);
219                for node in body {
220                    node.offset_spans(offset);
221                }
222            }
223            AstNode::Text { span, .. }
224            | AstNode::JavaScript { span, .. }
225            | AstNode::Escaped { span, .. } => {
226                span.offset(offset);
227            }
228            AstNode::FunctionCall {
229                args,
230                span,
231                name_span,
232                modifier_span,
233                args_span,
234                full_span,
235                ..
236            } => {
237                span.offset(offset);
238                name_span.offset(offset);
239                full_span.offset(offset);
240                if let Some(ms) = modifier_span {
241                    ms.offset(offset);
242                }
243                if let Some(as_) = args_span {
244                    as_.offset(offset);
245                }
246                if let Some(args) = args {
247                    for arg in args {
248                        arg.span.offset(offset);
249                        for part in &mut arg.parts {
250                            part.offset_spans(offset);
251                        }
252                    }
253                }
254            }
255        }
256    }
257}
258
259// ============================================================================
260// Parse Errors
261// ============================================================================
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum ErrorKind {
265    Syntax,
266    ArgumentCount,
267    EnumValue,
268    UnknownFunction,
269    BracketUsage,
270}
271
272#[derive(Debug, Clone)]
273pub struct ParseError {
274    pub message: String,
275    pub span: Span,
276    pub kind: ErrorKind,
277}
278
279impl ParseError {
280    #[inline]
281    pub fn new(message: impl Into<String>, span: Span, kind: ErrorKind) -> Self {
282        Self {
283            message: message.into(),
284            span,
285            kind,
286        }
287    }
288
289    #[inline]
290    pub fn syntax(message: impl Into<String>, span: Span) -> Self {
291        Self::new(message, span, ErrorKind::Syntax)
292    }
293}
294
295// ============================================================================
296// Parser
297// ============================================================================
298
299pub struct Parser<'src> {
300    source: &'src str,
301    bytes: &'src [u8],
302    pos: usize,
303    errors: Vec<ParseError>,
304    config: ValidationConfig,
305    #[cfg(feature = "validation")]
306    metadata: Option<Arc<MetadataManager>>,
307}
308
309impl<'src> Parser<'src> {
310    #[inline]
311    pub fn new(source: &'src str) -> Self {
312        Self {
313            source,
314            bytes: source.as_bytes(),
315            pos: 0,
316            errors: Vec::new(),
317            config: ValidationConfig::default(),
318            #[cfg(feature = "validation")]
319            metadata: None,
320        }
321    }
322
323    /// Create parser with validation configuration (requires "validation" feature)
324    #[cfg(feature = "validation")]
325    #[inline]
326    pub fn with_config(source: &'src str, config: ValidationConfig) -> Self {
327        Self {
328            source,
329            bytes: source.as_bytes(),
330            pos: 0,
331            errors: Vec::new(),
332            config,
333            metadata: None,
334        }
335    }
336
337    /// Create parser with validation and metadata (requires "validation" feature)
338    #[cfg(feature = "validation")]
339    #[inline]
340    pub fn with_validation(
341        source: &'src str,
342        config: ValidationConfig,
343        metadata: Arc<MetadataManager>,
344    ) -> Self {
345        Self {
346            source,
347            bytes: source.as_bytes(),
348            pos: 0,
349            errors: Vec::new(),
350            config,
351            metadata: Some(metadata),
352        }
353    }
354
355    pub fn parse(mut self) -> (AstNode, Vec<ParseError>) {
356        let start = self.pos;
357        let mut body = Vec::new();
358
359        while !self.is_eof() {
360            // Find start of "code: `" block
361            if let Some(block_start) = self.find_code_block_start() {
362                // Add text before block
363                if block_start > self.pos {
364                    body.push(AstNode::Text {
365                        content: self.slice(self.pos, block_start).to_string(),
366                        span: Span::new(self.pos, block_start),
367                    });
368                }
369
370                // Move pos to start of content (after "code: `")
371                let content_start = block_start + 7; // len("code: `")
372                self.pos = content_start;
373
374                // Find end of block (unescaped `)
375                if let Some(block_end) = self.find_code_block_end() {
376                    let content_len = block_end - content_start;
377
378                    if content_len > 0 {
379                        // Parse content inside block
380                        let inner_source = self.slice(content_start, block_end);
381
382                        #[cfg(feature = "validation")]
383                        let inner_parser = if self.config.is_enabled() {
384                            if let Some(ref metadata) = self.metadata {
385                                Parser::with_validation(
386                                    inner_source,
387                                    self.config.clone(),
388                                    metadata.clone(),
389                                )
390                            } else {
391                                Parser::with_config(inner_source, self.config.clone())
392                            }
393                        } else {
394                            Parser::new(inner_source)
395                        };
396
397                        #[cfg(not(feature = "validation"))]
398                        let inner_parser = Parser::new(inner_source);
399
400                        let (mut inner_ast, inner_errors) = inner_parser.parse_forge_script();
401
402                        inner_ast.offset_spans(content_start);
403
404                        match inner_ast {
405                            AstNode::Program {
406                                body: inner_body, ..
407                            } => {
408                                body.extend(inner_body);
409                            }
410                            _ => body.push(inner_ast),
411                        }
412
413                        for mut error in inner_errors {
414                            error.span.offset(content_start);
415                            self.errors.push(error);
416                        }
417                    }
418
419                    // Move past closing backtick
420                    self.pos = block_end + 1;
421                } else {
422                    // Unclosed block
423                    if self.config.validate_brackets {
424                        self.errors.push(ParseError::syntax(
425                            "Unclosed code block",
426                            Span::new(block_start, self.source.len()),
427                        ));
428                    }
429                    body.push(AstNode::Text {
430                        content: self.slice(block_start, self.source.len()).to_string(),
431                        span: Span::new(block_start, self.source.len()),
432                    });
433                    self.pos = self.source.len();
434                }
435            } else {
436                // No more blocks, rest is text
437                if self.pos < self.source.len() {
438                    body.push(AstNode::Text {
439                        content: self.slice(self.pos, self.source.len()).to_string(),
440                        span: Span::new(self.pos, self.source.len()),
441                    });
442                }
443                self.pos = self.source.len();
444            }
445        }
446
447        let span = Span::new(start, self.source.len());
448        (AstNode::Program { body, span }, self.errors)
449    }
450
451    fn parse_forge_script(mut self) -> (AstNode, Vec<ParseError>) {
452        let start = self.pos;
453        let mut body = Vec::new();
454
455        while !self.is_eof() {
456            if let Some(node) = self.parse_forge_node() {
457                body.push(node);
458            }
459        }
460
461        let span = Span::new(start, self.source.len());
462        (AstNode::Program { body, span }, self.errors)
463    }
464
465    // ========================================================================
466    // Character/Position Utilities
467    // ========================================================================
468
469    #[inline(always)]
470    fn is_eof(&self) -> bool {
471        self.pos >= self.bytes.len()
472    }
473
474    #[inline(always)]
475    fn current_byte(&self) -> Option<u8> {
476        self.bytes.get(self.pos).copied()
477    }
478
479    #[inline(always)]
480    fn peek_byte(&self, offset: usize) -> Option<u8> {
481        self.bytes.get(self.pos + offset).copied()
482    }
483
484    #[inline(always)]
485    fn advance(&mut self) -> Option<u8> {
486        let byte = self.current_byte()?;
487        self.pos += 1;
488        Some(byte)
489    }
490
491    #[inline]
492    fn slice(&self, start: usize, end: usize) -> &'src str {
493        &self.source[start..end.min(self.source.len())]
494    }
495
496    #[inline]
497    fn is_escaped_at(&self, pos: usize) -> bool {
498        is_escaped(self.source, pos)
499    }
500
501    fn find_code_block_start(&self) -> Option<usize> {
502        let mut p = self.pos;
503        while p + 7 <= self.bytes.len() {
504            // Check for "code: `"
505            if &self.bytes[p..p + 7] == b"code: `" {
506                let preceded_by_valid = p == 0
507                    || self.bytes[p - 1].is_ascii_whitespace()
508                    || self.bytes[p - 1] == b'{'
509                    || self.bytes[p - 1] == b',';
510                if preceded_by_valid && !is_escaped(self.source, p + 6) {
511                    return Some(p);
512                }
513            }
514            p += 1;
515        }
516        None
517    }
518
519    fn find_code_block_end(&self) -> Option<usize> {
520        let mut p = self.pos;
521        while p < self.bytes.len() {
522            if self.bytes[p] == b'`' && !is_escaped(self.source, p) {
523                return Some(p);
524            }
525            p += 1;
526        }
527        None
528    }
529
530    // ========================================================================
531    // High-Level Parsing
532    // ========================================================================
533
534    fn parse_forge_node(&mut self) -> Option<AstNode> {
535        // Handle backslash escapes explicitly
536        if self.current_byte() == Some(b'\\') {
537            return self.parse_escape_sequence();
538        }
539
540        // Handle $ sequences
541        if self.current_byte() == Some(b'$') && !self.is_escaped_at(self.pos) {
542            if self.peek_byte(1) == Some(b'{') {
543                return Some(self.parse_javascript());
544            }
545            return Some(self.parse_function_call());
546        }
547
548        self.parse_text()
549    }
550
551    fn parse_text(&mut self) -> Option<AstNode> {
552        let start = self.pos;
553        while !self.is_eof() {
554            if self.current_byte() == Some(b'\\') {
555                break;
556            }
557            if self.current_byte() == Some(b'$') && !self.is_escaped_at(self.pos) {
558                break;
559            }
560            self.advance();
561        }
562
563        if self.pos > start {
564            Some(AstNode::Text {
565                content: self.slice(start, self.pos).to_string(),
566                span: Span::new(start, self.pos),
567            })
568        } else {
569            None
570        }
571    }
572
573    fn parse_escape_sequence(&mut self) -> Option<AstNode> {
574        let start = self.pos;
575        self.advance(); // consume '\'
576
577        if let Some(next) = self.current_byte() {
578            // Escaped backslash: \\ -> single \
579            if next == b'\\' {
580                self.advance();
581                return Some(AstNode::Text {
582                    content: "\\".to_string(),
583                    span: Span::new(start, self.pos),
584                });
585            }
586            // Escaped special character
587            if matches!(next, b'$' | b'[' | b']' | b';' | b'`') {
588                self.advance();
589                return Some(AstNode::Text {
590                    content: self.slice(start + 1, self.pos).to_string(),
591                    span: Span::new(start, self.pos),
592                });
593            }
594        }
595
596        // Lone backslash or unrecognised escape — emit as-is with no error
597        Some(AstNode::Text {
598            content: "\\".to_string(),
599            span: Span::new(start, start + 1),
600        })
601    }
602
603    fn parse_javascript(&mut self) -> AstNode {
604        let start = self.pos;
605        self.advance(); // '$'
606        self.advance(); // '{'
607        let brace_start = self.pos - 1;
608
609        if let Some(end) = self.find_matching_brace(brace_start) {
610            let code = self.slice(brace_start + 1, end).to_string();
611            self.pos = end + 1;
612            AstNode::JavaScript {
613                code,
614                span: Span::new(start, self.pos),
615            }
616        } else {
617            if self.config.validate_brackets {
618                self.errors.push(ParseError::syntax(
619                    "Unclosed JavaScript expression",
620                    Span::new(start, self.source.len()),
621                ));
622            }
623            self.pos = self.source.len();
624            AstNode::JavaScript {
625                code: String::new(),
626                span: Span::new(start, self.pos),
627            }
628        }
629    }
630
631    fn parse_function_call(&mut self) -> AstNode {
632        let start = self.pos;
633        self.advance(); // '$'
634
635        // Record where modifiers start (right after '$')
636        let modifier_start = self.pos;
637        let modifiers = self.parse_modifiers();
638        let modifier_end = self.pos;
639
640        // modifier_span is Some only when modifier characters were actually consumed
641        let modifier_span = if modifier_end > modifier_start {
642            Some(Span::new(modifier_start, modifier_end))
643        } else {
644            None
645        };
646
647        // Record where the name begins and ends
648        let name = self.parse_identifier();
649        let name_end = self.pos;
650
651        if name.is_empty() {
652            return AstNode::Text {
653                content: "$".to_string(),
654                span: Span::new(start, start + 1),
655            };
656        }
657
658        // name_span includes '$' and modifiers up to end of name
659        let name_span = Span::new(start, name_end);
660
661        if self.is_escape_function(&name) {
662            return self.parse_escape_function(start, name, name_span);
663        }
664
665        // Record bracket/args span
666        let has_brackets = self.current_byte() == Some(b'[');
667        let bracket_open = self.pos;
668
669        let args = if has_brackets {
670            self.parse_function_arguments()
671        } else {
672            None
673        };
674
675        let args_span = if has_brackets {
676            // self.pos now points just past the closing ']'
677            Some(Span::new(bracket_open, self.pos))
678        } else {
679            None
680        };
681
682        let full_span = Span::new(modifier_start, self.pos);
683        let span = Span::new(start, self.pos);
684
685        // Validate with metadata if available
686        #[cfg(feature = "validation")]
687        if self.config.is_enabled() {
688            let full_name = if name.starts_with('$') {
689                name.clone()
690            } else {
691                format!("${}", name)
692            };
693
694            if let Some(ref metadata) = self.metadata {
695                if let Some(func) = metadata.get(&full_name) {
696                    self.validate_function_call(
697                        &full_name,
698                        &func,
699                        args.as_ref(),
700                        has_brackets,
701                        name_span,
702                    );
703                } else if self.config.validate_functions {
704                    // Try to find the longest known-function prefix of the written name.
705                    // e.g. $getSHiodhwbubw -> matches $get when $get is registered.
706                    // get_prefix() uses the trie for efficient longest-prefix lookup.
707                    // Only try when the call has brackets so bare unknown names stay simple.
708                    let prefix_match: Option<String> = if has_brackets {
709                        metadata.get_prefix(&full_name).map(|(name, _)| name)
710                    } else {
711                        None
712                    };
713
714                    if let Some(matched) = prefix_match {
715                        self.errors.push(ParseError::new(
716                            format!(
717                                "Unknown function: {} (did you mean {}?)",
718                                full_name, matched
719                            ),
720                            name_span,
721                            ErrorKind::UnknownFunction,
722                        ));
723                    } else {
724                        self.errors.push(ParseError::new(
725                            format!("Unknown function: {}", full_name),
726                            name_span,
727                            ErrorKind::UnknownFunction,
728                        ));
729                    }
730                }
731            } else if self.config.validate_functions {
732                self.errors.push(ParseError::new(
733                    format!(
734                        "Cannot validate function {}: no metadata available",
735                        full_name
736                    ),
737                    name_span,
738                    ErrorKind::UnknownFunction,
739                ));
740            }
741        }
742
743        AstNode::FunctionCall {
744            name,
745            name_span,
746            modifier_span,
747            args_span,
748            args,
749            modifiers,
750            full_span,
751            span,
752        }
753    }
754
755    // ========================================================================
756    // Validation
757    // ========================================================================
758
759    #[cfg(feature = "validation")]
760    fn validate_function_call(
761        &mut self,
762        name: &str,
763        func: &Function,
764        args: Option<&Vec<Argument>>,
765        has_brackets: bool,
766        name_span: Span,
767    ) {
768        // Validate brackets usage
769        if self.config.validate_brackets {
770            match func.brackets {
771                Some(true) => {
772                    if !has_brackets {
773                        self.errors.push(ParseError::new(
774                            format!("{} requires brackets", name),
775                            name_span,
776                            ErrorKind::BracketUsage,
777                        ));
778                    }
779                }
780                Some(false) => {
781                    // Brackets optional — no error either way
782                }
783                None => {
784                    if has_brackets {
785                        self.errors.push(ParseError::new(
786                            format!("{} does not accept brackets", name),
787                            name_span,
788                            ErrorKind::BracketUsage,
789                        ));
790                    }
791                }
792            }
793        }
794
795        // Validate argument count and enums
796        if (self.config.validate_arguments || self.config.validate_enums) && has_brackets {
797            if let (Some(args), Some(func_args)) = (args, &func.args) {
798                self.validate_arguments(name, args, func_args, name_span);
799            }
800        }
801    }
802
803    #[cfg(feature = "validation")]
804    fn validate_arguments(
805        &mut self,
806        func_name: &str,
807        provided_args: &[Argument],
808        func_args: &[Arg],
809        name_span: Span,
810    ) {
811        let provided_count = provided_args.len();
812
813        let has_rest = func_args.iter().any(|a| a.rest);
814        let required_count = func_args
815            .iter()
816            .filter(|a| a.required.unwrap_or(false) && !a.rest)
817            .count();
818        let max_count = if has_rest {
819            usize::MAX
820        } else {
821            func_args.len()
822        };
823
824        if self.config.validate_arguments {
825            if provided_count < required_count {
826                self.errors.push(ParseError::new(
827                    format!(
828                        "{} requires at least {} argument(s), got {}",
829                        func_name, required_count, provided_count
830                    ),
831                    name_span,
832                    ErrorKind::ArgumentCount,
833                ));
834            } else if !has_rest && provided_count > max_count {
835                self.errors.push(ParseError::new(
836                    format!(
837                        "{} accepts at most {} argument(s), got {}",
838                        func_name, max_count, provided_count
839                    ),
840                    name_span,
841                    ErrorKind::ArgumentCount,
842                ));
843            }
844        }
845
846        if self.config.validate_enums {
847            for (i, provided_arg) in provided_args.iter().enumerate() {
848                let func_arg = if i < func_args.len() {
849                    &func_args[i]
850                } else if has_rest {
851                    func_args.last().unwrap()
852                } else {
853                    continue;
854                };
855
856                // Use the argument's own span so the error underlines the bad value.
857                self.validate_enum_value(func_name, provided_arg, func_arg, provided_arg.span);
858            }
859        }
860    }
861
862    #[cfg(feature = "validation")]
863    fn validate_enum_value(
864        &mut self,
865        func_name: &str,
866        arg: &Argument,
867        func_arg: &Arg,
868        name_span: Span,
869    ) {
870        if !func_arg.required.unwrap_or(false) && arg.is_empty() {
871            return;
872        }
873
874        let enum_values = if let Some(enum_name) = &func_arg.enum_name {
875            if let Some(ref metadata) = self.metadata {
876                metadata.get_enum(enum_name)
877            } else {
878                None
879            }
880        } else {
881            func_arg.arg_enum.clone()
882        };
883
884        if let Some(valid_values) = enum_values {
885            if let Some(text_value) = arg.as_text() {
886                let trimmed = text_value.trim();
887                if !trimmed.is_empty() && !valid_values.contains(&trimmed.to_string()) {
888                    self.errors.push(ParseError::new(
889                        format!(
890                            "Invalid value for {} argument {}: expected one of {:?}",
891                            func_name, func_arg.name, valid_values
892                        ),
893                        name_span,
894                        ErrorKind::EnumValue,
895                    ));
896                }
897            }
898        }
899    }
900
901    // ========================================================================
902    // Parsing Helpers
903    // ========================================================================
904
905    fn parse_modifiers(&mut self) -> Modifiers {
906        let mut modifiers = Modifiers::default();
907        let start = self.pos;
908
909        loop {
910            match self.current_byte() {
911                Some(b'!') => {
912                    modifiers.silent = true;
913                    self.advance();
914                }
915                Some(b'#') => {
916                    modifiers.negated = true;
917                    self.advance();
918                }
919                Some(b'@') if self.peek_byte(1) == Some(b'[') => {
920                    self.advance(); // '@'
921                    let bracket_start = self.pos;
922                    self.advance(); // '['
923                    if let Some(end) = self.find_matching_bracket(bracket_start) {
924                        modifiers.count = Some(self.slice(bracket_start + 1, end).to_string());
925                        self.pos = end + 1;
926                    } else if self.config.validate_brackets {
927                        self.errors.push(ParseError::syntax(
928                            "Unclosed modifier bracket",
929                            Span::new(bracket_start, bracket_start + 1),
930                        ));
931                        break;
932                    } else {
933                        break;
934                    }
935                }
936                _ => break,
937            }
938        }
939
940        let end = self.pos;
941        if end > start {
942            modifiers.span = Some(Span::new(start, end));
943        }
944
945        modifiers
946    }
947
948    #[inline]
949    fn parse_identifier(&mut self) -> String {
950        let start = self.pos;
951        while let Some(b) = self.current_byte() {
952            if b.is_ascii_alphanumeric() || b == b'_' {
953                self.advance();
954            } else {
955                break;
956            }
957        }
958        self.slice(start, self.pos).to_string()
959    }
960
961    fn is_escape_function(&self, name: &str) -> bool {
962        matches!(name, "c" | "C" | "escape")
963    }
964
965    fn parse_escape_function(&mut self, start: usize, name: String, name_span: Span) -> AstNode {
966        if self.current_byte() != Some(b'[') {
967            if self.config.validate_brackets {
968                self.errors.push(ParseError::new(
969                    format!("${} requires brackets", name),
970                    name_span,
971                    ErrorKind::BracketUsage,
972                ));
973            }
974            return AstNode::Text {
975                content: self.slice(start, self.pos).to_string(),
976                span: Span::new(start, self.pos),
977            };
978        }
979
980        let bracket_start = self.pos;
981        self.advance();
982        if let Some(end) = self.find_matching_bracket(bracket_start) {
983            let content = self.slice(bracket_start + 1, end).to_string();
984            self.pos = end + 1;
985            AstNode::Escaped {
986                content,
987                span: Span::new(start, self.pos),
988            }
989        } else {
990            if self.config.validate_brackets {
991                self.errors.push(ParseError::syntax(
992                    format!("Unclosed '[' for ${}", name),
993                    name_span,
994                ));
995            }
996            self.pos = self.source.len();
997            AstNode::Escaped {
998                content: String::new(),
999                span: Span::new(start, self.pos),
1000            }
1001        }
1002    }
1003
1004    fn parse_function_arguments(&mut self) -> Option<Vec<Argument>> {
1005        let bracket_start = self.pos;
1006        self.advance();
1007        if let Some(end) = self.find_matching_bracket(bracket_start) {
1008            let args_content = self.slice(bracket_start + 1, end);
1009            let parsed_args = self.parse_arguments(args_content, bracket_start + 1);
1010            self.pos = end + 1;
1011            Some(parsed_args)
1012        } else {
1013            if self.config.validate_brackets {
1014                self.errors.push(ParseError::syntax(
1015                    "Unclosed function arguments",
1016                    Span::new(bracket_start, bracket_start + 1),
1017                ));
1018            }
1019            None
1020        }
1021    }
1022
1023    fn parse_arguments(&mut self, content: &str, base_offset: usize) -> Vec<Argument> {
1024        let mut args = Vec::new();
1025        let mut current = String::new();
1026        let mut depth = 0;
1027        let bytes = content.as_bytes();
1028        let mut i = 0;
1029        // `arg_start` and `arg_end` are byte indices into `content`.
1030        // We track `arg_end` explicitly so spans are correct even when
1031        // `current` has been built by pushing individual chars (which may
1032        // not equal i - arg_start for multi-byte sequences or newlines).
1033        let mut arg_start = 0usize;
1034        let mut arg_end = 0usize; // inclusive end index (past the last byte of this arg)
1035
1036        while i < bytes.len() {
1037            if bytes[i] == b'$' && depth == 0 {
1038                if let Some(esc_end) = self.find_escape_function_end(content, i) {
1039                    current.push_str(&content[i..=esc_end]);
1040                    i = esc_end + 1;
1041                    arg_end = i;
1042                    continue;
1043                }
1044            }
1045
1046            if bytes[i] == b'\\' {
1047                if let Some(next) = bytes.get(i + 1) {
1048                    if matches!(*next, b'`' | b'$' | b'[' | b']' | b';' | b'\\') {
1049                        current.push_str(&content[i..i + 2]);
1050                        i += 2;
1051                        arg_end = i;
1052                        continue;
1053                    }
1054                }
1055                current.push('\\');
1056                i += 1;
1057                arg_end = i;
1058                continue;
1059            }
1060
1061            match bytes[i] {
1062                b'[' if !is_escaped(content, i) && self.is_function_bracket(content, i) => {
1063                    depth += 1;
1064                    current.push('[');
1065                    arg_end = i + 1;
1066                }
1067                b']' if depth > 0 && !is_escaped(content, i) => {
1068                    depth -= 1;
1069                    current.push(']');
1070                    arg_end = i + 1;
1071                }
1072                b';' if depth == 0 => {
1073                    let arg_offset = base_offset + arg_start;
1074                    let arg_len = arg_end - arg_start;
1075                    let parts = self.parse_argument_parts(&current, arg_offset);
1076                    args.push(Argument {
1077                        parts,
1078                        span: Span::new(arg_offset, arg_offset + arg_len),
1079                    });
1080                    current.clear();
1081                    arg_start = i + 1;
1082                    arg_end = i + 1;
1083                }
1084                _ => {
1085                    // For multi-byte UTF-8 we must push the full char worth of bytes.
1086                    // Since we are working with raw bytes we need to find the char boundary.
1087                    let ch_len = content[i..]
1088                        .chars()
1089                        .next()
1090                        .map(|c| c.len_utf8())
1091                        .unwrap_or(1);
1092                    current.push_str(&content[i..i + ch_len]);
1093                    arg_end = i + ch_len;
1094                    // advance by ch_len - 1 extra; the loop does +1 below
1095                    i += ch_len - 1;
1096                }
1097            }
1098            i += 1;
1099        }
1100
1101        if !current.is_empty() || !args.is_empty() {
1102            let arg_offset = base_offset + arg_start;
1103            let arg_len = arg_end - arg_start;
1104            let parts = self.parse_argument_parts(&current, arg_offset);
1105            args.push(Argument {
1106                parts,
1107                span: Span::new(arg_offset, arg_offset + arg_len),
1108            });
1109        }
1110        args
1111    }
1112
1113    fn parse_argument_parts(&mut self, content: &str, offset: usize) -> SmallVec<[AstNode; 4]> {
1114        if content.is_empty() {
1115            let mut parts = SmallVec::new();
1116            parts.push(AstNode::Text {
1117                content: String::new(),
1118                span: Span::new(offset, offset),
1119            });
1120            return parts;
1121        }
1122
1123        #[cfg(feature = "validation")]
1124        let inner_parser = if self.config.is_enabled() {
1125            if let Some(ref metadata) = self.metadata {
1126                Parser::with_validation(content, self.config.clone(), metadata.clone())
1127            } else {
1128                Parser::with_config(content, self.config.clone())
1129            }
1130        } else {
1131            Parser::new(content)
1132        };
1133
1134        #[cfg(not(feature = "validation"))]
1135        let inner_parser = Parser::new(content);
1136
1137        let (ast, errors) = inner_parser.parse_forge_script();
1138
1139        let nodes = if let AstNode::Program { mut body, .. } = ast {
1140            for node in &mut body {
1141                node.offset_spans(offset);
1142            }
1143            body
1144        } else {
1145            vec![ast]
1146        };
1147
1148        for mut error in errors {
1149            error.span.offset(offset);
1150            self.errors.push(error);
1151        }
1152
1153        let mut parts = SmallVec::new();
1154        for node in nodes {
1155            parts.push(node);
1156        }
1157        parts
1158    }
1159
1160    // ========================================================================
1161    // Matching Utilities
1162    // ========================================================================
1163
1164    fn find_matching_bracket(&self, open_pos: usize) -> Option<usize> {
1165        let mut depth = 1;
1166        let mut p = open_pos + 1;
1167        while p < self.bytes.len() {
1168            if self.bytes[p] == b'\\' {
1169                p += 2;
1170                continue;
1171            }
1172            if self.bytes[p] == b'[' && !is_escaped(self.source, p) {
1173                depth += 1;
1174            } else if self.bytes[p] == b']' && !is_escaped(self.source, p) {
1175                depth -= 1;
1176                if depth == 0 {
1177                    return Some(p);
1178                }
1179            }
1180            p += 1;
1181        }
1182        None
1183    }
1184
1185    fn find_matching_brace(&self, open_pos: usize) -> Option<usize> {
1186        let mut depth = 1;
1187        let mut p = open_pos + 1;
1188        while p < self.bytes.len() {
1189            match self.bytes[p] {
1190                b'{' => depth += 1,
1191                b'}' => {
1192                    depth -= 1;
1193                    if depth == 0 {
1194                        return Some(p);
1195                    }
1196                }
1197                _ => {}
1198            }
1199            p += 1;
1200        }
1201        None
1202    }
1203
1204    fn is_function_bracket(&self, content: &str, idx: usize) -> bool {
1205        if idx == 0 || content.as_bytes().get(idx) != Some(&b'[') {
1206            return false;
1207        }
1208        let bytes = content.as_bytes();
1209        let mut i = idx;
1210        while i > 0 && (bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_') {
1211            i -= 1;
1212        }
1213        while i > 0 && matches!(bytes[i - 1], b'!' | b'#' | b']') {
1214            if bytes[i - 1] == b']' {
1215                let mut d = 1;
1216                while i > 1 && d > 0 {
1217                    i -= 1;
1218                    if bytes[i - 1] == b']' {
1219                        d += 1;
1220                    } else if bytes[i - 1] == b'[' {
1221                        d -= 1;
1222                    }
1223                }
1224                if i < 2 || bytes[i - 2] != b'@' {
1225                    return false;
1226                }
1227                i -= 2;
1228            } else {
1229                i -= 1;
1230            }
1231        }
1232        i > 0 && bytes[i - 1] == b'$' && (i == 1 || bytes[i - 2] != b'\\')
1233    }
1234
1235    fn find_escape_function_end(&self, content: &str, start: usize) -> Option<usize> {
1236        let bytes = content.as_bytes();
1237        let mut p = start + 1;
1238        while p < bytes.len() && matches!(bytes[p], b'!' | b'#') {
1239            p += 1;
1240        }
1241        let name_start = p;
1242        while p < bytes.len() && (bytes[p].is_ascii_alphanumeric() || bytes[p] == b'_') {
1243            p += 1;
1244        }
1245        if !self.is_escape_function(&content[name_start..p]) || bytes.get(p) != Some(&b'[') {
1246            return None;
1247        }
1248        let mut depth = 1;
1249        p += 1;
1250        while p < bytes.len() {
1251            if bytes[p] == b'\\' {
1252                p += 2;
1253                continue;
1254            }
1255            if bytes[p] == b'[' && !is_escaped(content, p) {
1256                depth += 1;
1257            } else if bytes[p] == b']' && !is_escaped(content, p) {
1258                depth -= 1;
1259                if depth == 0 {
1260                    return Some(p);
1261                }
1262            }
1263            p += 1;
1264        }
1265        None
1266    }
1267}
1268
1269// ============================================================================
1270// Public API
1271// ============================================================================
1272
1273/// Parse ForgeScript source code into an AST (no validation)
1274pub fn parse(source: &str) -> (AstNode, Vec<ParseError>) {
1275    Parser::new(source).parse()
1276}
1277
1278/// Parse with error handling
1279pub fn parse_with_errors(source: &str) -> Result<AstNode, Vec<ParseError>> {
1280    let (ast, errors) = parse(source);
1281    if errors.is_empty() {
1282        Ok(ast)
1283    } else {
1284        Err(errors)
1285    }
1286}
1287
1288/// Parse with validation configuration (requires "validation" feature)
1289#[cfg(feature = "validation")]
1290pub fn parse_with_config(source: &str, config: ValidationConfig) -> (AstNode, Vec<ParseError>) {
1291    Parser::with_config(source, config).parse()
1292}
1293
1294/// Parse with validation and metadata (requires "validation" feature)
1295#[cfg(feature = "validation")]
1296pub fn parse_with_validation(
1297    source: &str,
1298    config: ValidationConfig,
1299    metadata: Arc<MetadataManager>,
1300) -> (AstNode, Vec<ParseError>) {
1301    Parser::with_validation(source, config, metadata).parse()
1302}
1303
1304/// Parse ForgeScript directly (no wrapper) with validation
1305#[cfg(feature = "validation")]
1306pub fn parse_forge_script_with_validation(
1307    source: &str,
1308    config: ValidationConfig,
1309    metadata: Arc<MetadataManager>,
1310) -> (AstNode, Vec<ParseError>) {
1311    Parser::with_validation(source, config, metadata).parse_forge_script()
1312}
1313
1314/// Parse with strict validation (requires "validation" feature)
1315#[cfg(feature = "validation")]
1316pub fn parse_strict(source: &str, metadata: Arc<MetadataManager>) -> (AstNode, Vec<ParseError>) {
1317    Parser::with_validation(source, ValidationConfig::strict(), metadata).parse()
1318}