Skip to main content

variable_core/
parser.rs

1use std::collections::BTreeMap;
2
3use crate::ast::{Feature, StructDef, StructField, Value, VarFile, VarType, Variable};
4use crate::lexer::{Span, SpannedToken, Token};
5
6#[derive(Debug, Clone, PartialEq)]
7pub struct ParseError {
8    pub message: String,
9    pub span: Span,
10}
11
12struct Parser {
13    tokens: Vec<SpannedToken>,
14    pos: usize,
15}
16
17impl Parser {
18    fn new(tokens: Vec<SpannedToken>) -> Self {
19        Self { tokens, pos: 0 }
20    }
21
22    fn peek(&self) -> Option<&SpannedToken> {
23        self.tokens.get(self.pos)
24    }
25
26    fn advance(&mut self) -> Option<&SpannedToken> {
27        let token = self.tokens.get(self.pos);
28        if token.is_some() {
29            self.pos += 1;
30        }
31        token
32    }
33
34    fn expect(&mut self, expected: &Token) -> Result<&SpannedToken, ParseError> {
35        match self.peek() {
36            Some(t) if &t.token == expected => {
37                self.pos += 1;
38                Ok(&self.tokens[self.pos - 1])
39            }
40            Some(t) => Err(ParseError {
41                message: format!("expected {:?}, found {:?}", expected, t.token),
42                span: t.span.clone(),
43            }),
44            None => Err(ParseError {
45                message: format!("expected {:?}, found end of input", expected),
46                span: self.eof_span(),
47            }),
48        }
49    }
50
51    fn eof_span(&self) -> Span {
52        if let Some(last) = self.tokens.last() {
53            Span {
54                offset: last.span.offset + 1,
55                line: last.span.line,
56                column: last.span.column + 1,
57            }
58        } else {
59            Span {
60                offset: 0,
61                line: 1,
62                column: 1,
63            }
64        }
65    }
66
67    fn current_span(&self) -> Span {
68        match self.peek() {
69            Some(t) => t.span.clone(),
70            None => self.eof_span(),
71        }
72    }
73
74    fn parse_file(&mut self) -> Result<VarFile, ParseError> {
75        let mut structs = Vec::new();
76        let mut features = Vec::new();
77        while self.peek().is_some() {
78            // Peek past the ID and colon to determine if this is a Struct or Feature
79            let keyword = self.peek_declaration_keyword()?;
80            match keyword {
81                Token::Struct => structs.push(self.parse_struct_def()?),
82                Token::Feature => features.push(self.parse_feature()?),
83                _ => {
84                    return Err(ParseError {
85                        message: format!("expected Feature or Struct keyword, found {:?}", keyword),
86                        span: self.current_span(),
87                    });
88                }
89            }
90        }
91        Ok(VarFile { structs, features })
92    }
93
94    /// Peek ahead past the `ID :` prefix to see whether the next declaration
95    /// is a Feature or Struct, without consuming any tokens.
96    fn peek_declaration_keyword(&self) -> Result<Token, ParseError> {
97        // tokens[pos] should be the numeric ID
98        // tokens[pos+1] should be ':'
99        // tokens[pos+2] should be Feature or Struct
100        let keyword_pos = self.pos + 2;
101        match self.tokens.get(keyword_pos) {
102            Some(t) => Ok(t.token.clone()),
103            None => Err(ParseError {
104                message: "expected Feature or Struct declaration, found end of input".to_string(),
105                span: self.eof_span(),
106            }),
107        }
108    }
109
110    fn parse_scoped_id(&mut self, scope: &str) -> Result<u32, ParseError> {
111        let (raw_id, span) = match self.advance() {
112            Some(SpannedToken {
113                token: Token::NumberLit(n),
114                span,
115            }) => (*n, span.clone()),
116            Some(t) => {
117                return Err(ParseError {
118                    message: format!("expected {} id, found {:?}", scope, t.token),
119                    span: t.span.clone(),
120                });
121            }
122            None => {
123                return Err(ParseError {
124                    message: format!("expected {} id, found end of input", scope),
125                    span: self.eof_span(),
126                });
127            }
128        };
129
130        if raw_id.fract() != 0.0 || raw_id < 0.0 || raw_id > u32::MAX as f64 {
131            return Err(ParseError {
132                message: format!("expected {} id to be a u32, found {}", scope, raw_id),
133                span,
134            });
135        }
136
137        self.expect(&Token::Colon)?;
138
139        Ok(raw_id as u32)
140    }
141
142    fn parse_struct_def(&mut self) -> Result<StructDef, ParseError> {
143        let span = self.current_span();
144        let id = self.parse_scoped_id("struct")?;
145        self.expect(&Token::Struct)?;
146
147        let name = match self.advance() {
148            Some(SpannedToken {
149                token: Token::Ident(name),
150                ..
151            }) => name.clone(),
152            Some(t) => {
153                return Err(ParseError {
154                    message: format!("expected struct name, found {:?}", t.token),
155                    span: t.span.clone(),
156                });
157            }
158            None => {
159                return Err(ParseError {
160                    message: "expected struct name, found end of input".to_string(),
161                    span: self.eof_span(),
162                });
163            }
164        };
165
166        self.expect(&Token::Equals)?;
167        self.expect(&Token::LBrace)?;
168
169        let mut fields = Vec::new();
170        while self.peek().is_some_and(|t| t.token != Token::RBrace) {
171            fields.push(self.parse_struct_field()?);
172        }
173
174        self.expect(&Token::RBrace)?;
175
176        Ok(StructDef {
177            id,
178            name,
179            fields,
180            span,
181        })
182    }
183
184    fn parse_struct_field(&mut self) -> Result<StructField, ParseError> {
185        let span = self.current_span();
186        let id = self.parse_scoped_id("field")?;
187        let name = match self.advance() {
188            Some(SpannedToken {
189                token: Token::Ident(name),
190                ..
191            }) => name.clone(),
192            Some(t) => {
193                return Err(ParseError {
194                    message: format!("expected field name, found {:?}", t.token),
195                    span: t.span.clone(),
196                });
197            }
198            None => {
199                return Err(ParseError {
200                    message: "expected field name, found end of input".to_string(),
201                    span: self.eof_span(),
202                });
203            }
204        };
205
206        // Struct fields can only be primitive types
207        let field_type = match self.advance() {
208            Some(SpannedToken {
209                token: Token::BooleanType,
210                ..
211            }) => VarType::Boolean,
212            Some(SpannedToken {
213                token: Token::IntegerType,
214                ..
215            }) => VarType::Integer,
216            Some(SpannedToken {
217                token: Token::FloatType,
218                ..
219            }) => VarType::Float,
220            Some(SpannedToken {
221                token: Token::StringType,
222                ..
223            }) => VarType::String,
224            Some(t) => {
225                return Err(ParseError {
226                    message: format!(
227                        "expected field type (Boolean, Integer, Float, or String), found {:?}",
228                        t.token
229                    ),
230                    span: t.span.clone(),
231                });
232            }
233            None => {
234                return Err(ParseError {
235                    message: "expected field type, found end of input".to_string(),
236                    span: self.eof_span(),
237                });
238            }
239        };
240
241        self.expect(&Token::Equals)?;
242
243        let default = self.parse_primitive_value(&field_type)?;
244
245        Ok(StructField {
246            id,
247            name,
248            field_type,
249            default,
250            span,
251        })
252    }
253
254    /// Parse a primitive value (boolean, number, or string literal).
255    fn parse_primitive_value(&mut self, var_type: &VarType) -> Result<Value, ParseError> {
256        match self.advance() {
257            Some(SpannedToken {
258                token: Token::BoolLit(b),
259                ..
260            }) => Ok(Value::Boolean(*b)),
261            Some(SpannedToken {
262                token: Token::NumberLit(n),
263                span,
264            }) => {
265                let n = *n;
266                let span = span.clone();
267                match var_type {
268                    VarType::Integer => {
269                        if n.fract() != 0.0 {
270                            return Err(ParseError {
271                                message: format!(
272                                    "Integer field cannot have fractional default value `{}`",
273                                    n
274                                ),
275                                span,
276                            });
277                        }
278                        Ok(Value::Integer(n as i64))
279                    }
280                    _ => Ok(Value::Float(n)),
281                }
282            }
283            Some(SpannedToken {
284                token: Token::StringLit(s),
285                ..
286            }) => Ok(Value::String(s.clone())),
287            Some(t) => Err(ParseError {
288                message: format!("expected default value, found {:?}", t.token),
289                span: t.span.clone(),
290            }),
291            None => Err(ParseError {
292                message: "expected default value, found end of input".to_string(),
293                span: self.eof_span(),
294            }),
295        }
296    }
297
298    /// Parse a struct literal: `StructName { field = value, ... }` or `StructName {}`
299    fn parse_struct_literal(&mut self, struct_name: &str) -> Result<Value, ParseError> {
300        // Expect the struct name as an identifier token
301        match self.advance() {
302            Some(SpannedToken {
303                token: Token::Ident(name),
304                span,
305            }) => {
306                if name != struct_name {
307                    return Err(ParseError {
308                        message: format!(
309                            "expected struct literal `{}`, found `{}`",
310                            struct_name, name
311                        ),
312                        span: span.clone(),
313                    });
314                }
315            }
316            Some(t) => {
317                return Err(ParseError {
318                    message: format!(
319                        "expected struct literal `{}`, found {:?}",
320                        struct_name, t.token
321                    ),
322                    span: t.span.clone(),
323                });
324            }
325            None => {
326                return Err(ParseError {
327                    message: format!(
328                        "expected struct literal `{}`, found end of input",
329                        struct_name
330                    ),
331                    span: self.eof_span(),
332                });
333            }
334        }
335
336        self.expect(&Token::LBrace)?;
337
338        let mut fields = BTreeMap::new();
339        while self.peek().is_some_and(|t| t.token != Token::RBrace) {
340            // Parse: field_name = value
341            let field_name = match self.advance() {
342                Some(SpannedToken {
343                    token: Token::Ident(name),
344                    ..
345                }) => name.clone(),
346                Some(t) => {
347                    return Err(ParseError {
348                        message: format!("expected field name, found {:?}", t.token),
349                        span: t.span.clone(),
350                    });
351                }
352                None => {
353                    return Err(ParseError {
354                        message: "expected field name, found end of input".to_string(),
355                        span: self.eof_span(),
356                    });
357                }
358            };
359
360            self.expect(&Token::Equals)?;
361
362            // Parse the value — we don't know the field type here at parse time,
363            // so accept any primitive literal. Validation resolves types later.
364            let value = match self.advance() {
365                Some(SpannedToken {
366                    token: Token::BoolLit(b),
367                    ..
368                }) => Value::Boolean(*b),
369                Some(SpannedToken {
370                    token: Token::NumberLit(n),
371                    ..
372                }) => {
373                    let n = *n;
374                    // Store as integer if it's a whole number, float otherwise
375                    if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
376                        Value::Integer(n as i64)
377                    } else {
378                        Value::Float(n)
379                    }
380                }
381                Some(SpannedToken {
382                    token: Token::StringLit(s),
383                    ..
384                }) => Value::String(s.clone()),
385                Some(t) => {
386                    return Err(ParseError {
387                        message: format!(
388                            "expected field value in struct literal, found {:?}",
389                            t.token
390                        ),
391                        span: t.span.clone(),
392                    });
393                }
394                None => {
395                    return Err(ParseError {
396                        message: "expected field value, found end of input".to_string(),
397                        span: self.eof_span(),
398                    });
399                }
400            };
401
402            fields.insert(field_name, value);
403        }
404
405        self.expect(&Token::RBrace)?;
406
407        Ok(Value::Struct {
408            struct_name: struct_name.to_string(),
409            fields,
410        })
411    }
412
413    fn parse_feature(&mut self) -> Result<Feature, ParseError> {
414        let span = self.current_span();
415        let id = self.parse_scoped_id("feature")?;
416        self.expect(&Token::Feature)?;
417
418        let name = match self.advance() {
419            Some(SpannedToken {
420                token: Token::Ident(name),
421                ..
422            }) => name.clone(),
423            Some(t) => {
424                return Err(ParseError {
425                    message: format!("expected feature name, found {:?}", t.token),
426                    span: t.span.clone(),
427                });
428            }
429            None => {
430                return Err(ParseError {
431                    message: "expected feature name, found end of input".to_string(),
432                    span: self.eof_span(),
433                });
434            }
435        };
436
437        self.expect(&Token::Equals)?;
438        self.expect(&Token::LBrace)?;
439
440        let mut variables = Vec::new();
441        while self.peek().is_some_and(|t| t.token != Token::RBrace) {
442            variables.push(self.parse_variable()?);
443        }
444
445        self.expect(&Token::RBrace)?;
446
447        Ok(Feature {
448            id,
449            name,
450            variables,
451            span,
452        })
453    }
454
455    fn parse_variable(&mut self) -> Result<Variable, ParseError> {
456        let span = self.current_span();
457        let id = self.parse_scoped_id("variable")?;
458        let name = match self.advance() {
459            Some(SpannedToken {
460                token: Token::Ident(name),
461                ..
462            }) => name.clone(),
463            Some(t) => {
464                return Err(ParseError {
465                    message: format!("expected variable name, found {:?}", t.token),
466                    span: t.span.clone(),
467                });
468            }
469            None => {
470                return Err(ParseError {
471                    message: "expected variable name, found end of input".to_string(),
472                    span: self.eof_span(),
473                });
474            }
475        };
476
477        let var_type = match self.advance() {
478            Some(SpannedToken {
479                token: Token::BooleanType,
480                ..
481            }) => VarType::Boolean,
482            Some(SpannedToken {
483                token: Token::IntegerType,
484                ..
485            }) => VarType::Integer,
486            Some(SpannedToken {
487                token: Token::FloatType,
488                ..
489            }) => VarType::Float,
490            Some(SpannedToken {
491                token: Token::StringType,
492                ..
493            }) => VarType::String,
494            Some(SpannedToken {
495                token: Token::Ident(name),
496                ..
497            }) => VarType::Struct(name.clone()),
498            Some(t) => {
499                return Err(ParseError {
500                    message: format!(
501                        "expected type (Boolean, Integer, Float, String, or struct name), found {:?}",
502                        t.token
503                    ),
504                    span: t.span.clone(),
505                });
506            }
507            None => {
508                return Err(ParseError {
509                    message: "expected type, found end of input".to_string(),
510                    span: self.eof_span(),
511                });
512            }
513        };
514
515        self.expect(&Token::Equals)?;
516
517        let default = if let VarType::Struct(ref struct_name) = var_type {
518            self.parse_struct_literal(struct_name)?
519        } else {
520            self.parse_primitive_value(&var_type)?
521        };
522
523        Ok(Variable {
524            id,
525            name,
526            var_type,
527            default,
528            span,
529        })
530    }
531}
532
533pub fn parse(tokens: Vec<SpannedToken>) -> Result<VarFile, ParseError> {
534    let mut parser = Parser::new(tokens);
535    parser.parse_file()
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::lexer::lex;
542
543    fn parse_source(input: &str) -> Result<VarFile, ParseError> {
544        let tokens = lex(input).map_err(|e| ParseError {
545            message: e.message,
546            span: e.span,
547        })?;
548        parse(tokens)
549    }
550
551    #[test]
552    fn parse_single_boolean_variable() {
553        let input = r#"1: Feature Flags = {
554    1: enabled Boolean = true
555}"#;
556        let file = parse_source(input).unwrap();
557        assert_eq!(file.features.len(), 1);
558        assert_eq!(file.features[0].id, 1);
559        assert_eq!(file.features[0].name, "Flags");
560        assert_eq!(file.features[0].variables.len(), 1);
561        assert_eq!(file.features[0].variables[0].id, 1);
562        assert_eq!(file.features[0].variables[0].name, "enabled");
563        assert_eq!(file.features[0].variables[0].var_type, VarType::Boolean);
564        assert_eq!(file.features[0].variables[0].default, Value::Boolean(true));
565    }
566
567    #[test]
568    fn parse_single_integer_variable() {
569        let input = r#"1: Feature Config = {
570    1: max_items Integer = 50
571}"#;
572        let file = parse_source(input).unwrap();
573        assert_eq!(file.features[0].variables[0].var_type, VarType::Integer);
574        assert_eq!(file.features[0].variables[0].default, Value::Integer(50));
575    }
576
577    #[test]
578    fn parse_single_float_variable() {
579        let input = r#"1: Feature Config = {
580    1: ratio Float = 3.14
581}"#;
582        let file = parse_source(input).unwrap();
583        assert_eq!(file.features[0].variables[0].var_type, VarType::Float);
584        assert_eq!(file.features[0].variables[0].default, Value::Float(3.14));
585    }
586
587    #[test]
588    fn parse_float_variable_with_integer_literal() {
589        let input = r#"1: Feature Config = {
590    1: max_items Float = 50
591}"#;
592        let file = parse_source(input).unwrap();
593        assert_eq!(file.features[0].variables[0].var_type, VarType::Float);
594        assert_eq!(file.features[0].variables[0].default, Value::Float(50.0));
595    }
596
597    #[test]
598    fn parse_single_string_variable() {
599        let input = r#"1: Feature Config = {
600    1: title String = "Hello"
601}"#;
602        let file = parse_source(input).unwrap();
603        assert_eq!(file.features[0].variables[0].var_type, VarType::String);
604        assert_eq!(
605            file.features[0].variables[0].default,
606            Value::String("Hello".to_string())
607        );
608    }
609
610    #[test]
611    fn parse_multiple_features() {
612        let input = r#"1: Feature A = {
613    1: x Boolean = true
614}
615
6162: Feature B = {
617    1: y Integer = 42
618}"#;
619        let file = parse_source(input).unwrap();
620        assert_eq!(file.features.len(), 2);
621        assert_eq!(file.features[0].id, 1);
622        assert_eq!(file.features[0].name, "A");
623        assert_eq!(file.features[1].id, 2);
624        assert_eq!(file.features[1].name, "B");
625    }
626
627    #[test]
628    fn parse_example_var_file() {
629        let input = r#"1: Feature Checkout = {
630    1: enabled Boolean = true
631    2: max_items Integer = 50
632    3: header_text String = "Complete your purchase"
633}"#;
634        let file = parse_source(input).unwrap();
635        assert_eq!(file.features.len(), 1);
636        let feature = &file.features[0];
637        assert_eq!(feature.id, 1);
638        assert_eq!(feature.name, "Checkout");
639        assert_eq!(feature.variables.len(), 3);
640        assert_eq!(feature.variables[0].id, 1);
641        assert_eq!(feature.variables[0].name, "enabled");
642        assert_eq!(feature.variables[1].id, 2);
643        assert_eq!(feature.variables[1].name, "max_items");
644        assert_eq!(feature.variables[2].id, 3);
645        assert_eq!(feature.variables[2].name, "header_text");
646    }
647
648    #[test]
649    fn error_missing_lbrace() {
650        let input = "1: Feature Checkout = 1: x Boolean = true }";
651        let err = parse_source(input).unwrap_err();
652        assert!(err.message.contains("expected LBrace"));
653    }
654
655    #[test]
656    fn error_missing_rbrace() {
657        let input = r#"1: Feature Checkout = {
658    1: x Boolean = true"#;
659        let err = parse_source(input).unwrap_err();
660        assert!(err.message.contains("expected RBrace"));
661    }
662
663    #[test]
664    fn error_missing_default_value() {
665        let input = r#"1: Feature Checkout = {
666    1: x Boolean =
667}"#;
668        let err = parse_source(input).unwrap_err();
669        assert!(err.message.contains("expected default value"));
670    }
671
672    #[test]
673    fn error_missing_type() {
674        let input = r#"1: Feature Checkout = {
675    1: x = true
676}"#;
677        let err = parse_source(input).unwrap_err();
678        assert!(err.message.contains("expected type"));
679    }
680
681    #[test]
682    fn error_unknown_type_keyword() {
683        // "Map" is now parsed as a struct type reference (Ident), so it parses
684        // successfully but would fail in validation as an unknown struct type.
685        // Here it fails at the default because "5" is not a struct literal.
686        let input = r#"1: Feature Checkout = {
687    1: x Map = 5
688}"#;
689        let err = parse_source(input).unwrap_err();
690        assert!(err.message.contains("expected struct literal"));
691    }
692
693    #[test]
694    fn error_integer_with_fractional_value() {
695        let input = r#"1: Feature Config = {
696    1: ratio Integer = 3.14
697}"#;
698        let err = parse_source(input).unwrap_err();
699        assert!(
700            err.message
701                .contains("Integer field cannot have fractional default value")
702        );
703    }
704
705    #[test]
706    fn error_spans_point_to_correct_location() {
707        let input = "1: Feature Checkout = {\n    1: x = true\n}";
708        let err = parse_source(input).unwrap_err();
709        // The "=" is the token where we expected a type
710        assert_eq!(err.span.line, 2);
711    }
712
713    #[test]
714    fn parse_struct_definition() {
715        let input = r##"1: Struct Theme = {
716    1: dark_mode Boolean = false
717    2: font_size Integer = 14
718    3: primary_color String = "#000000"
719}"##;
720        let file = parse_source(input).unwrap();
721        assert_eq!(file.structs.len(), 1);
722        assert_eq!(file.features.len(), 0);
723        let s = &file.structs[0];
724        assert_eq!(s.id, 1);
725        assert_eq!(s.name, "Theme");
726        assert_eq!(s.fields.len(), 3);
727        assert_eq!(s.fields[0].name, "dark_mode");
728        assert_eq!(s.fields[0].field_type, VarType::Boolean);
729        assert_eq!(s.fields[0].default, Value::Boolean(false));
730        assert_eq!(s.fields[1].name, "font_size");
731        assert_eq!(s.fields[1].field_type, VarType::Integer);
732        assert_eq!(s.fields[1].default, Value::Integer(14));
733        assert_eq!(s.fields[2].name, "primary_color");
734        assert_eq!(s.fields[2].field_type, VarType::String);
735        assert_eq!(s.fields[2].default, Value::String("#000000".to_string()));
736    }
737
738    #[test]
739    fn parse_struct_and_feature_together() {
740        let input = r#"1: Struct Theme = {
741    1: dark_mode Boolean = false
742}
743
7441: Feature Dashboard = {
745    1: enabled Boolean = true
746    2: theme Theme = Theme {}
747}"#;
748        let file = parse_source(input).unwrap();
749        assert_eq!(file.structs.len(), 1);
750        assert_eq!(file.features.len(), 1);
751        let var = &file.features[0].variables[1];
752        assert_eq!(var.name, "theme");
753        assert_eq!(var.var_type, VarType::Struct("Theme".to_string()));
754        match &var.default {
755            Value::Struct {
756                struct_name,
757                fields,
758            } => {
759                assert_eq!(struct_name, "Theme");
760                assert!(fields.is_empty());
761            }
762            other => panic!("expected Struct value, got {:?}", other),
763        }
764    }
765
766    #[test]
767    fn parse_struct_literal_with_field_overrides() {
768        let input = r#"1: Struct Config = {
769    1: retries Integer = 3
770    2: verbose Boolean = false
771}
772
7731: Feature App = {
774    1: config Config = Config { retries = 5 verbose = true }
775}"#;
776        let file = parse_source(input).unwrap();
777        let var = &file.features[0].variables[0];
778        match &var.default {
779            Value::Struct {
780                struct_name,
781                fields,
782            } => {
783                assert_eq!(struct_name, "Config");
784                assert_eq!(fields.len(), 2);
785                assert_eq!(fields["retries"], Value::Integer(5));
786                assert_eq!(fields["verbose"], Value::Boolean(true));
787            }
788            other => panic!("expected Struct value, got {:?}", other),
789        }
790    }
791
792    #[test]
793    fn error_missing_feature_id() {
794        let input = r#"Feature Checkout = {
795    1: enabled Boolean = true
796}"#;
797        let err = parse_source(input).unwrap_err();
798        // Without a numeric ID prefix, the parser hits a different error path
799        assert!(
800            err.message.contains("expected feature id")
801                || err.message.contains("expected Feature or Struct")
802        );
803    }
804
805    #[test]
806    fn error_missing_variable_id() {
807        let input = r#"1: Feature Checkout = {
808    enabled Boolean = true
809}"#;
810        let err = parse_source(input).unwrap_err();
811        assert!(err.message.contains("expected variable id"));
812    }
813
814    #[test]
815    fn error_non_integer_feature_id() {
816        let input = r#"1.5: Feature Checkout = {
817    1: enabled Boolean = true
818}"#;
819        let err = parse_source(input).unwrap_err();
820        assert!(err.message.contains("expected feature id to be a u32"));
821    }
822}