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