Skip to main content

aam_core/pipeline/
parser.rs

1//! Parser stage: builds an Abstract Syntax Tree from tokens.
2//!
3//! The Parser consumes tokens from the Lexer and produces an AST,
4//! preserving line number information for error diagnostics.
5
6use crate::error::{AamlError, ErrorDiagnostics};
7use crate::pipeline::lexer::{Token, TokenKind};
8use crate::pipeline::tasks::{ExecutionTask, ParseTask};
9
10/// Represents a value in the AST, supporting nested structures.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ValueNode<'a> {
13    Literal(std::borrow::Cow<'a, str>),
14    Object(std::sync::Arc<[(std::borrow::Cow<'a, str>, Self)]>),
15    List(std::sync::Arc<[Self]>),
16}
17
18impl std::fmt::Display for ValueNode<'_> {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            ValueNode::Literal(s) => write!(f, "{s}"),
22            ValueNode::Object(pairs) => {
23                let formatted_pairs: Vec<String> =
24                    pairs.iter().map(|(k, v)| format!("{k} = {v}")).collect();
25                write!(f, "{{ {} }}", formatted_pairs.join(", "))
26            }
27            ValueNode::List(items) => {
28                let formatted_items: Vec<String> = items.iter().map(ToString::to_string).collect();
29                write!(f, "[{}]", formatted_items.join(", "))
30            }
31        }
32    }
33}
34
35/// A node in the Abstract Syntax Tree.
36#[derive(Debug, Clone)]
37pub enum AstNode<'a> {
38    /// Key-value assignment: `key = value`
39    Assignment {
40        key: std::borrow::Cow<'a, str>,
41        value: ValueNode<'a>,
42        line: usize,
43    },
44    /// Directive: `@directive_name arguments`
45    Directive {
46        name: std::borrow::Cow<'a, str>,
47        args: std::borrow::Cow<'a, str>,
48        body: Option<ValueNode<'a>>,
49        line: usize,
50    },
51}
52
53impl AstNode<'_> {
54    /// Returns the line number where this node appears
55    #[must_use]
56    pub const fn line(&self) -> usize {
57        match self {
58            AstNode::Assignment { line, .. } | AstNode::Directive { line, .. } => *line,
59        }
60    }
61}
62
63/// Trait for parsing tokens into an AST.
64pub trait Parser: Send + Sync {
65    /// Parses a token stream into an AST.
66    ///
67    /// # Errors
68    /// Returns `AamlError::ParseError` if the token stream is malformed.
69    fn parse<'a>(&self, tokens: &[Token<'a>]) -> Result<Vec<AstNode<'a>>, AamlError>;
70
71    /// Parses with recovery and returns both partial AST and all parse errors.
72    fn parse_with_recovery<'a>(&self, tokens: &[Token<'a>]) -> ParseOutput<'a>;
73
74    /// Generates parse tasks from an AST
75    fn generate_parse_tasks<'a>(&self, ast: &[AstNode<'a>]) -> Vec<ParseTask<'a>>;
76
77    /// Generates execution tasks from an AST
78    fn generate_execution_tasks<'a>(&self, ast: &[AstNode<'a>]) -> Vec<ExecutionTask<'a>>;
79}
80
81/// Parser output for LSP use-cases where partial AST plus diagnostics are required.
82pub struct ParseOutput<'a> {
83    pub ast: Vec<AstNode<'a>>,
84    pub errors: Vec<AamlError>,
85}
86
87/// Default implementation of the Parser stage.
88pub struct DefaultParser;
89
90impl DefaultParser {
91    #[must_use]
92    pub const fn new() -> Self {
93        Self
94    }
95
96    /// Filters out comment and newline tokens
97    fn filter_tokens<'a, 'b>(tokens: &'b [Token<'a>]) -> Vec<&'b Token<'a>> {
98        use crate::pipeline::lexer::TokenKind;
99        tokens
100            .iter()
101            .filter(|t| t.kind != TokenKind::Comment)
102            .collect()
103    }
104
105    /// Parses assignment tokens: `identifier = value`
106    fn parse_assignment<'a>(
107        tokens: &[&Token<'a>],
108        start: usize,
109    ) -> Result<(std::borrow::Cow<'a, str>, ValueNode<'a>, usize), AamlError> {
110        use crate::pipeline::lexer::TokenKind;
111
112        if start >= tokens.len() {
113            return Err(AamlError::ParseError {
114                line: tokens.get(start).map_or(1, |t| t.line),
115                content: "incomplete assignment".to_string(),
116                details: "Expected: key = value".to_string(),
117                diagnostics: Some(Box::new(ErrorDiagnostics::new(
118                    "Incomplete assignment",
119                    "Assignment must have at least 3 tokens: key, =, value".to_string(),
120                    "Check format: key = value".to_string(),
121                ))),
122            });
123        }
124
125        if tokens[start].kind != TokenKind::Identifier {
126            return Err(AamlError::ParseError {
127                line: tokens[start].line,
128                content: format!("Expected identifier, got {:?}", tokens[start].kind),
129                details: "First token of assignment must be an identifier".to_string(),
130                diagnostics: None,
131            });
132        }
133
134        let mut assign_pos = start + 1;
135        while assign_pos < tokens.len()
136            && tokens[assign_pos].kind != TokenKind::Assign
137            && tokens[assign_pos].kind != TokenKind::Newline
138        {
139            assign_pos += 1;
140        }
141
142        if assign_pos >= tokens.len() || tokens[assign_pos].kind != TokenKind::Assign {
143            let got = tokens.get(start + 1).map_or("<eof>", |t| t.text.as_ref());
144            return Err(AamlError::ParseError {
145                line: tokens.get(start + 1).map_or(tokens[start].line, |t| t.line),
146                content: format!("Expected '=', got '{got}'"),
147                details: "Assignment operator '=' expected after key".to_string(),
148                diagnostics: None,
149            });
150        }
151
152        let key = if assign_pos == start + 1 {
153            tokens[start].text.clone()
154        } else {
155            let key_text = tokens[start..assign_pos]
156                .iter()
157                .map(|t| t.text.as_ref())
158                .collect::<Vec<_>>()
159                .join(" ");
160            key_text.into()
161        };
162
163        let (value, consumed) = Self::parse_value(tokens, assign_pos + 1)?;
164        Ok((key, value, consumed))
165    }
166
167    fn parse_braced_literal<'a, 'b>(
168        tokens: &'b [&'b Token<'a>],
169        start: usize,
170    ) -> Result<(ValueNode<'a>, usize), AamlError> {
171        use crate::pipeline::lexer::TokenKind;
172
173        let mut depth = 0_i32;
174        let mut pos = start;
175
176        while pos < tokens.len() {
177            match tokens[pos].kind {
178                TokenKind::LeftBrace => depth += 1,
179                TokenKind::RightBrace => {
180                    depth -= 1;
181                    if depth == 0 {
182                        let first = &tokens[start];
183                        let last = &tokens[pos];
184                        let text = &first.source[first.start..last.end];
185                        return Ok((ValueNode::Literal(text.into()), pos + 1));
186                    }
187                }
188                TokenKind::Newline if depth == 0 => break,
189                _ => {}
190            }
191            pos += 1;
192        }
193
194        Err(AamlError::ParseError {
195            line: tokens[start].line,
196            content: "unclosed brace".to_string(),
197            details: "Expected '}' to close inline object".to_string(),
198            diagnostics: None,
199        })
200    }
201
202    /// Parses a value (which may be literal, inline object, or inline list)
203    fn parse_value<'a, 'b>(
204        tokens: &'b [&'b Token<'a>],
205        start: usize,
206    ) -> Result<(ValueNode<'a>, usize), AamlError> {
207        use crate::pipeline::lexer::TokenKind;
208
209        if start >= tokens.len() {
210            return Err(AamlError::ParseError {
211                line: tokens.len(),
212                content: "unexpected end of input".to_string(),
213                details: "Expected a value after '='".to_string(),
214                diagnostics: None,
215            });
216        }
217
218        match &tokens[start].kind {
219            TokenKind::LeftBrace => {
220                // Prefer structured inline object; fallback to raw braced literal for nested schema-like values.
221                match Self::parse_inline_object(tokens, start) {
222                    Ok((obj, consumed)) => Ok((obj, consumed)),
223                    Err(_) => Self::parse_braced_literal(tokens, start),
224                }
225            }
226            TokenKind::LeftBracket => {
227                // Inline list
228                let (list, consumed) = Self::parse_inline_list(tokens, start)?;
229                Ok((list, consumed))
230            }
231            _ => {
232                let end_pos = tokens[start..]
233                    .iter()
234                    .position(|t| {
235                        matches!(
236                            t.kind,
237                            TokenKind::Newline
238                                | TokenKind::RightBrace
239                                | TokenKind::RightBracket
240                                | TokenKind::Comma
241                        )
242                    })
243                    .map_or(tokens.len(), |p| start + p);
244
245                if end_pos == start {
246                    return Err(AamlError::ParseError {
247                        line: tokens[start].line,
248                        content: "invalid value".to_string(),
249                        details: "Could not parse literal value".to_string(),
250                        diagnostics: None,
251                    });
252                }
253
254                // Preserve the exact source text between the first and last token
255                // so whitespace and `=` characters inside values are not lost.
256                let first = &tokens[start];
257                let last = &tokens[end_pos - 1];
258                let exact = &first.source[first.start..last.end];
259
260                Ok((ValueNode::Literal(exact.into()), end_pos))
261            }
262        }
263    }
264
265    /// Parses an inline object: `{ key = val, key = val, ... }`
266    fn parse_inline_object<'a, 'b>(
267        tokens: &'b [&'b Token<'a>],
268        start: usize,
269    ) -> Result<(ValueNode<'a>, usize), AamlError> {
270        use crate::pipeline::lexer::TokenKind;
271
272        let mut pairs = Vec::new();
273        let mut pos = start + 1;
274        let mut expect_field = true;
275        let mut has_any_field = false;
276
277        while pos < tokens.len() {
278            match tokens[pos].kind {
279                TokenKind::RightBrace if expect_field && has_any_field => {
280                    return Err(AamlError::ParseError {
281                        line: tokens[pos].line,
282                        content: "trailing comma in inline object".to_string(),
283                        details: "Expected another field after ',' or remove trailing comma"
284                            .to_string(),
285                        diagnostics: None,
286                    });
287                }
288                TokenKind::RightBrace => return Ok((ValueNode::Object(pairs.into()), pos + 1)),
289                TokenKind::Identifier if expect_field => {
290                    pos = Self::parse_inline_object_pair(tokens, pos, &mut pairs)?;
291                    expect_field = false;
292                    has_any_field = true;
293                }
294                TokenKind::Identifier => {
295                    return Err(AamlError::ParseError {
296                        line: tokens[pos].line,
297                        content: "missing comma in inline object".to_string(),
298                        details: "Expected ',' between object fields".to_string(),
299                        diagnostics: None,
300                    });
301                }
302                TokenKind::Comma if expect_field => {
303                    return Err(AamlError::ParseError {
304                        line: tokens[pos].line,
305                        content: "unexpected comma in inline object".to_string(),
306                        details: "Expected an object field before ','".to_string(),
307                        diagnostics: None,
308                    });
309                }
310                TokenKind::Comma => {
311                    expect_field = true;
312                    pos += 1;
313                }
314                _ => {
315                    return Err(AamlError::ParseError {
316                        line: tokens[pos].line,
317                        content: "invalid inline object format".to_string(),
318                        details: "Expected identifier or closing brace".to_string(),
319                        diagnostics: None,
320                    });
321                }
322            }
323        }
324
325        Err(AamlError::ParseError {
326            line: tokens[start].line,
327            content: "unclosed brace".to_string(),
328            details: "Expected '}' to close inline object".to_string(),
329            diagnostics: None,
330        })
331    }
332
333    fn parse_inline_object_pair<'a, 'b>(
334        tokens: &'b [&'b Token<'a>],
335        pos: usize,
336        pairs: &mut Vec<(std::borrow::Cow<'a, str>, ValueNode<'a>)>,
337    ) -> Result<usize, AamlError> {
338        use crate::pipeline::lexer::TokenKind;
339        let key: std::borrow::Cow<'a, str> = tokens[pos].text.clone();
340        if pos + 1 >= tokens.len() || tokens[pos + 1].kind != TokenKind::Assign {
341            return Err(AamlError::ParseError {
342                line: tokens[pos].line,
343                content: "invalid inline object format".to_string(),
344                details: "Expected '=' after key".to_string(),
345                diagnostics: None,
346            });
347        }
348
349        let value_pos = pos + 2;
350        let (value, next_pos) = if value_pos < tokens.len()
351            && matches!(
352                tokens[value_pos].kind,
353                TokenKind::RightBrace | TokenKind::Comma
354            ) {
355            (ValueNode::Literal("".into()), value_pos)
356        } else {
357            Self::parse_value(tokens, value_pos)?
358        };
359        pairs.push((key, value));
360        Ok(next_pos)
361    }
362
363    /// Parses an inline list: `[item, item, ...]`
364    fn parse_inline_list<'a, 'b>(
365        tokens: &'b [&'b Token<'a>],
366        start: usize,
367    ) -> Result<(ValueNode<'a>, usize), AamlError> {
368        use crate::pipeline::lexer::TokenKind;
369
370        let mut items = Vec::new();
371        let mut pos = start + 1;
372
373        while pos < tokens.len() {
374            if tokens[pos].kind == TokenKind::RightBracket {
375                return Ok((ValueNode::List(items.into()), pos + 1));
376            }
377
378            if tokens[pos].kind == TokenKind::Comma {
379                pos += 1;
380                continue;
381            }
382
383            let (value, next_pos) = Self::parse_value(tokens, pos)?;
384            items.push(value);
385            pos = next_pos;
386        }
387
388        Err(AamlError::ParseError {
389            line: tokens[start].line,
390            content: "unclosed bracket".to_string(),
391            details: "Expected ']' to close inline list".to_string(),
392            diagnostics: None,
393        })
394    }
395
396    fn missing_directive_name_error(line: usize) -> AamlError {
397        AamlError::ParseError {
398            line,
399            content: "@".to_string(),
400            details: "Directive name expected after '@'".to_string(),
401            diagnostics: Some(Box::new(ErrorDiagnostics::new(
402                "Missing directive name",
403                "Directive requires a name after '@'".to_string(),
404                "Use format: @directive_name arguments".to_string(),
405            ))),
406        }
407    }
408
409    fn should_stop_directive_args(kind: &TokenKind, brace_count: i32, bracket_count: i32) -> bool {
410        brace_count == 0
411            && bracket_count == 0
412            && (*kind == TokenKind::At || *kind == TokenKind::Newline)
413    }
414
415    fn collect_directive_args(tokens_filtered: &[&Token<'_>], start_pos: usize) -> (String, usize) {
416        let mut args = String::new();
417        let mut arg_pos = start_pos;
418        let mut brace_count = 0;
419        let mut bracket_count = 0;
420
421        while arg_pos < tokens_filtered.len() {
422            let tk = tokens_filtered[arg_pos];
423            match tk.kind {
424                TokenKind::LeftBrace => brace_count += 1,
425                TokenKind::RightBrace => brace_count -= 1,
426                TokenKind::LeftBracket => bracket_count += 1,
427                TokenKind::RightBracket => bracket_count -= 1,
428                _ => {}
429            }
430
431            if Self::should_stop_directive_args(&tk.kind, brace_count, bracket_count) {
432                break;
433            }
434
435            if !args.is_empty() {
436                args.push(' ');
437            }
438            args.push_str(&tk.text);
439            arg_pos += 1;
440        }
441
442        (args, arg_pos)
443    }
444
445    fn validate_schema_directive_args(line: usize, trimmed_args: &str) -> Result<(), AamlError> {
446        let (name_part, body_opt) = trimmed_args.split_once('{').unwrap_or((trimmed_args, ""));
447        if name_part.trim().is_empty() {
448            return Err(AamlError::ParseError {
449                line,
450                content: "@schema".to_string(),
451                details: "Schema name is missing".to_string(),
452                diagnostics: None,
453            });
454        }
455
456        if !trimmed_args.contains('{') || !trimmed_args.ends_with('}') {
457            return Err(AamlError::ParseError {
458                line,
459                content: "@schema".to_string(),
460                details: "Schema block must end with '}'".to_string(),
461                diagnostics: None,
462            });
463        }
464
465        let body = body_opt.trim_end_matches('}').trim();
466        for field in body.split([',', '\n']) {
467            let field = field.trim();
468            if field.is_empty() {
469                continue;
470            }
471
472            if !field.contains(':') {
473                return Err(AamlError::ParseError {
474                    line,
475                    content: "invalid schema field".to_string(),
476                    details: format!("Field '{field}' must be of the form 'name: type'"),
477                    diagnostics: None,
478                });
479            }
480        }
481
482        Ok(())
483    }
484
485    fn parse_directive<'a>(
486        tokens_filtered: &[&Token<'a>],
487        pos: &mut usize,
488        line: usize,
489    ) -> Result<AstNode<'a>, AamlError> {
490        if *pos + 1 >= tokens_filtered.len() {
491            return Err(Self::missing_directive_name_error(line));
492        }
493
494        let dir_name: std::borrow::Cow<'a, str> = tokens_filtered[*pos + 1].text.clone();
495        let (args, arg_pos) = Self::collect_directive_args(tokens_filtered, *pos + 2);
496
497        *pos = arg_pos;
498
499        let trimmed_args = args.trim();
500        if &*dir_name == "schema" {
501            Self::validate_schema_directive_args(line, trimmed_args)?;
502        }
503
504        Ok(AstNode::Directive {
505            name: dir_name,
506            args: trimmed_args.to_string().into(),
507            body: None,
508            line,
509        })
510    }
511
512    fn synchronize(tokens_filtered: &[&Token<'_>], mut pos: usize) -> usize {
513        while pos < tokens_filtered.len() {
514            let kind = &tokens_filtered[pos].kind;
515            if *kind == TokenKind::Newline || *kind == TokenKind::RightBrace {
516                return pos + 1;
517            }
518            pos += 1;
519        }
520        pos
521    }
522
523    fn scope_from_key(key: &str) -> std::borrow::Cow<'static, str> {
524        if let Some((prefix, _)) = key.rsplit_once('.') {
525            return format!("root::{}", prefix.replace('.', "::")).into();
526        }
527        std::borrow::Cow::Borrowed("root")
528    }
529
530    fn emit_assignment_parse_tasks<'a>(
531        tasks: &mut Vec<ParseTask<'a>>,
532        key: &str,
533        value: &ValueNode<'a>,
534        line: usize,
535    ) {
536        tasks.push(ParseTask::ProcessVariable {
537            variable_name: key.to_string().into(),
538            value: value.to_string().into(),
539            scope: Self::scope_from_key(key).into_owned().into(),
540            line,
541        });
542
543        if let ValueNode::Object(pairs) = value {
544            for (field, child_value) in pairs.iter() {
545                let child_key = format!("{key}.{field}");
546                Self::emit_assignment_parse_tasks(tasks, &child_key, child_value, line);
547            }
548        }
549    }
550
551    fn build_type_task<'a>(args: &str, line: usize) -> ParseTask<'a> {
552        let (type_name, type_spec) = args
553            .split_once('=')
554            .map_or(("", ""), |(lhs, rhs)| (lhs.trim(), rhs.trim()));
555
556        ParseTask::RegisterType {
557            type_name: type_name.to_string().into(),
558            type_spec: type_spec.to_string().into(),
559            line,
560        }
561    }
562
563    fn build_schema_task<'a>(args: &str, line: usize) -> ParseTask<'a> {
564        let schema_name = args.split_whitespace().next().unwrap_or("").to_string();
565        let body = args
566            .split_once('{')
567            .and_then(|(_, b)| b.rsplit_once('}'))
568            .map_or("", |(b, _)| b);
569        let fields = body
570            .split([',', '\n'])
571            .filter_map(|field_def| {
572                let (field_name, type_name) = field_def.split_once(':')?;
573                let field_name = field_name.trim();
574                let type_name = type_name.trim();
575
576                if field_name.is_empty() {
577                    return None;
578                }
579
580                Some(format!("{field_name}:{type_name}"))
581            })
582            .collect::<Vec<_>>()
583            .join(",");
584
585        ParseTask::RegisterSchema {
586            schema_name: schema_name.into(),
587            fields: fields.into(),
588            line,
589        }
590    }
591
592    fn build_directive_task<'a>(
593        name: &std::borrow::Cow<'a, str>,
594        args: &std::borrow::Cow<'a, str>,
595        line: usize,
596    ) -> ParseTask<'a> {
597        match &**name {
598            "type" => Self::build_type_task(args, line),
599            "schema" => Self::build_schema_task(args, line),
600            "derive" => ParseTask::ResolveDeriveImport {
601                derive_path: args.clone(),
602                line,
603            },
604            _ => ParseTask::ExecuteDirective {
605                directive_name: name.clone(),
606                arguments: args.clone(),
607                line,
608            },
609        }
610    }
611}
612
613impl Default for DefaultParser {
614    fn default() -> Self {
615        Self::new()
616    }
617}
618
619impl Parser for DefaultParser {
620    fn parse<'a>(&self, tokens: &[Token<'a>]) -> Result<Vec<AstNode<'a>>, AamlError> {
621        let result = self.parse_with_recovery(tokens);
622        if result.errors.is_empty() {
623            Ok(result.ast)
624        } else {
625            Err(result
626                .errors
627                .into_iter()
628                .next()
629                .unwrap_or_else(|| AamlError::ParseError {
630                    line: 1,
631                    content: "parse failed".to_string(),
632                    details: "Unknown parser failure".to_string(),
633                    diagnostics: None,
634                }))
635        }
636    }
637
638    fn parse_with_recovery<'a>(&self, tokens: &[Token<'a>]) -> ParseOutput<'a> {
639        use crate::pipeline::lexer::TokenKind;
640
641        let mut ast: Vec<AstNode<'a>> = Vec::new();
642        let mut errors = Vec::new();
643        let tokens_filtered = Self::filter_tokens(tokens);
644        let mut pos = 0;
645
646        while pos < tokens_filtered.len() {
647            let token = tokens_filtered[pos];
648
649            match &token.kind {
650                TokenKind::At => {
651                    match Self::parse_directive(&tokens_filtered, &mut pos, token.line) {
652                        Ok(dir_node) => ast.push(dir_node),
653                        Err(err) => {
654                            errors.push(err);
655                            pos = Self::synchronize(&tokens_filtered, pos + 1);
656                        }
657                    }
658                }
659                TokenKind::Identifier => match Self::parse_assignment(&tokens_filtered, pos) {
660                    Ok((key, value, new_pos)) => {
661                        ast.push(AstNode::Assignment {
662                            key,
663                            value,
664                            line: token.line,
665                        });
666                        pos = new_pos;
667                    }
668                    Err(err) => {
669                        errors.push(err);
670                        pos = Self::synchronize(&tokens_filtered, pos + 1);
671                    }
672                },
673                _ => pos += 1,
674            }
675        }
676
677        ParseOutput { ast, errors }
678    }
679    fn generate_parse_tasks<'a>(&self, ast: &[AstNode<'a>]) -> Vec<ParseTask<'a>> {
680        let mut tasks = Vec::new();
681
682        for node in ast {
683            match node {
684                AstNode::Assignment { key, value, line } => {
685                    Self::emit_assignment_parse_tasks(&mut tasks, key, value, *line);
686                }
687                AstNode::Directive {
688                    name, args, line, ..
689                } => tasks.push(Self::build_directive_task(name, args, *line)),
690            }
691        }
692
693        tasks
694    }
695
696    fn generate_execution_tasks<'a>(&self, ast: &[AstNode<'a>]) -> Vec<ExecutionTask<'a>> {
697        let mut tasks = Vec::new();
698
699        for node in ast {
700            match node {
701                AstNode::Assignment { key, value, line } => {
702                    tasks.push(ExecutionTask::SetValue {
703                        key: key.clone(),
704                        value: value.to_string().into(),
705                        line: *line,
706                    });
707                }
708                // Directives translated to execution tasks...
709                AstNode::Directive {
710                    name, args, line, ..
711                } => {
712                    if &**name == "import" {
713                        tasks.push(ExecutionTask::ImportFile {
714                            file_path: args.clone(),
715                            merge_strategy: std::borrow::Cow::Borrowed("merge"),
716                            line: *line,
717                        });
718                    } else if &**name == "derive" {
719                        tasks.push(ExecutionTask::ExecuteInheritance {
720                            derive_path: args.clone(),
721                            child_key: std::borrow::Cow::Borrowed(""), // For AAM v2
722                            line: *line,
723                        });
724                    }
725                }
726            }
727        }
728        tasks
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    use super::*;
735    use crate::pipeline::lexer::{DefaultLexer, Lexer};
736
737    #[test]
738    fn test_parse_simple_assignment() {
739        let lexer = DefaultLexer::new();
740        let tokens = lexer.tokenize("key = value").unwrap();
741        let parser = DefaultParser::new();
742        let ast = parser.parse(&tokens).unwrap();
743
744        assert_eq!(ast.len(), 1);
745        match &ast[0] {
746            AstNode::Assignment { key, value, .. } => {
747                assert_eq!(&**key, "key");
748                if let ValueNode::Literal(s) = value {
749                    assert_eq!(&**s, "value");
750                } else {
751                    panic!("Expected ValueNode::Literal");
752                }
753            }
754            AstNode::Directive { .. } => panic!("Expected assignment"),
755        }
756    }
757
758    #[test]
759    fn test_parse_directive() {
760        let lexer = DefaultLexer::new();
761        let tokens = lexer.tokenize("@import base.aam").unwrap();
762        let parser = DefaultParser::new();
763        let ast = parser.parse(&tokens).unwrap();
764
765        assert_eq!(ast.len(), 1);
766        match &ast[0] {
767            AstNode::Directive { name, args: _, .. } => {
768                assert_eq!(&**name, "import");
769            }
770            AstNode::Assignment { .. } => panic!("Expected directive"),
771        }
772    }
773
774    #[test]
775    fn test_parse_multiple_assignments() {
776        let lexer = DefaultLexer::new();
777        let tokens = lexer.tokenize("a = b\nc = d").unwrap();
778        let parser = DefaultParser::new();
779        let ast = parser.parse(&tokens).unwrap();
780
781        assert_eq!(ast.len(), 2);
782    }
783}