use std::collections::BTreeMap;
use crate::ast::{Feature, StructDef, StructField, Value, VarFile, VarType, Variable};
use crate::lexer::{Span, SpannedToken, Token};
#[derive(Debug, Clone, PartialEq)]
pub struct ParseError {
pub message: String,
pub span: Span,
}
struct Parser {
tokens: Vec<SpannedToken>,
pos: usize,
}
impl Parser {
fn new(tokens: Vec<SpannedToken>) -> Self {
Self { tokens, pos: 0 }
}
fn peek(&self) -> Option<&SpannedToken> {
self.tokens.get(self.pos)
}
fn advance(&mut self) -> Option<&SpannedToken> {
let token = self.tokens.get(self.pos);
if token.is_some() {
self.pos += 1;
}
token
}
fn expect(&mut self, expected: &Token) -> Result<&SpannedToken, ParseError> {
match self.peek() {
Some(t) if &t.token == expected => {
self.pos += 1;
Ok(&self.tokens[self.pos - 1])
}
Some(t) => Err(ParseError {
message: format!("expected {:?}, found {:?}", expected, t.token),
span: t.span.clone(),
}),
None => Err(ParseError {
message: format!("expected {:?}, found end of input", expected),
span: self.eof_span(),
}),
}
}
fn eof_span(&self) -> Span {
if let Some(last) = self.tokens.last() {
Span {
offset: last.span.offset + 1,
line: last.span.line,
column: last.span.column + 1,
}
} else {
Span {
offset: 0,
line: 1,
column: 1,
}
}
}
fn current_span(&self) -> Span {
match self.peek() {
Some(t) => t.span.clone(),
None => self.eof_span(),
}
}
fn parse_file(&mut self) -> Result<VarFile, ParseError> {
let mut structs = Vec::new();
let mut features = Vec::new();
while self.peek().is_some() {
let keyword = self.peek_declaration_keyword()?;
match keyword {
Token::Struct => structs.push(self.parse_struct_def()?),
Token::Feature => features.push(self.parse_feature()?),
_ => {
return Err(ParseError {
message: format!("expected Feature or Struct keyword, found {:?}", keyword),
span: self.current_span(),
});
}
}
}
Ok(VarFile { structs, features })
}
fn peek_declaration_keyword(&self) -> Result<Token, ParseError> {
let keyword_pos = self.pos + 2;
match self.tokens.get(keyword_pos) {
Some(t) => Ok(t.token.clone()),
None => Err(ParseError {
message: "expected Feature or Struct declaration, found end of input".to_string(),
span: self.eof_span(),
}),
}
}
fn parse_scoped_id(&mut self, scope: &str) -> Result<u32, ParseError> {
let (raw_id, span) = match self.advance() {
Some(SpannedToken {
token: Token::NumberLit(n),
span,
}) => (*n, span.clone()),
Some(t) => {
return Err(ParseError {
message: format!("expected {} id, found {:?}", scope, t.token),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: format!("expected {} id, found end of input", scope),
span: self.eof_span(),
});
}
};
if raw_id.fract() != 0.0 || raw_id < 0.0 || raw_id > u32::MAX as f64 {
return Err(ParseError {
message: format!("expected {} id to be a u32, found {}", scope, raw_id),
span,
});
}
self.expect(&Token::Colon)?;
Ok(raw_id as u32)
}
fn parse_struct_def(&mut self) -> Result<StructDef, ParseError> {
let span = self.current_span();
let id = self.parse_scoped_id("struct")?;
self.expect(&Token::Struct)?;
let name = match self.advance() {
Some(SpannedToken {
token: Token::Ident(name),
..
}) => name.clone(),
Some(t) => {
return Err(ParseError {
message: format!("expected struct name, found {:?}", t.token),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: "expected struct name, found end of input".to_string(),
span: self.eof_span(),
});
}
};
self.expect(&Token::Equals)?;
self.expect(&Token::LBrace)?;
let mut fields = Vec::new();
while self.peek().is_some_and(|t| t.token != Token::RBrace) {
fields.push(self.parse_struct_field()?);
}
self.expect(&Token::RBrace)?;
Ok(StructDef {
id,
name,
fields,
span,
})
}
fn parse_struct_field(&mut self) -> Result<StructField, ParseError> {
let span = self.current_span();
let id = self.parse_scoped_id("field")?;
let name = match self.advance() {
Some(SpannedToken {
token: Token::Ident(name),
..
}) => name.clone(),
Some(t) => {
return Err(ParseError {
message: format!("expected field name, found {:?}", t.token),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: "expected field name, found end of input".to_string(),
span: self.eof_span(),
});
}
};
let field_type = match self.advance() {
Some(SpannedToken {
token: Token::BooleanType,
..
}) => VarType::Boolean,
Some(SpannedToken {
token: Token::IntegerType,
..
}) => VarType::Integer,
Some(SpannedToken {
token: Token::FloatType,
..
}) => VarType::Float,
Some(SpannedToken {
token: Token::StringType,
..
}) => VarType::String,
Some(t) => {
return Err(ParseError {
message: format!(
"expected field type (Boolean, Integer, Float, or String), found {:?}",
t.token
),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: "expected field type, found end of input".to_string(),
span: self.eof_span(),
});
}
};
self.expect(&Token::Equals)?;
let default = self.parse_primitive_value(&field_type)?;
Ok(StructField {
id,
name,
field_type,
default,
span,
})
}
fn parse_primitive_value(&mut self, var_type: &VarType) -> Result<Value, ParseError> {
match self.advance() {
Some(SpannedToken {
token: Token::BoolLit(b),
..
}) => Ok(Value::Boolean(*b)),
Some(SpannedToken {
token: Token::NumberLit(n),
span,
}) => {
let n = *n;
let span = span.clone();
match var_type {
VarType::Integer => {
if n.fract() != 0.0 {
return Err(ParseError {
message: format!(
"Integer field cannot have fractional default value `{}`",
n
),
span,
});
}
Ok(Value::Integer(n as i64))
}
_ => Ok(Value::Float(n)),
}
}
Some(SpannedToken {
token: Token::StringLit(s),
..
}) => Ok(Value::String(s.clone())),
Some(t) => Err(ParseError {
message: format!("expected default value, found {:?}", t.token),
span: t.span.clone(),
}),
None => Err(ParseError {
message: "expected default value, found end of input".to_string(),
span: self.eof_span(),
}),
}
}
fn parse_struct_literal(&mut self, struct_name: &str) -> Result<Value, ParseError> {
match self.advance() {
Some(SpannedToken {
token: Token::Ident(name),
span,
}) => {
if name != struct_name {
return Err(ParseError {
message: format!(
"expected struct literal `{}`, found `{}`",
struct_name, name
),
span: span.clone(),
});
}
}
Some(t) => {
return Err(ParseError {
message: format!(
"expected struct literal `{}`, found {:?}",
struct_name, t.token
),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: format!(
"expected struct literal `{}`, found end of input",
struct_name
),
span: self.eof_span(),
});
}
}
self.expect(&Token::LBrace)?;
let mut fields = BTreeMap::new();
while self.peek().is_some_and(|t| t.token != Token::RBrace) {
let field_name = match self.advance() {
Some(SpannedToken {
token: Token::Ident(name),
..
}) => name.clone(),
Some(t) => {
return Err(ParseError {
message: format!("expected field name, found {:?}", t.token),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: "expected field name, found end of input".to_string(),
span: self.eof_span(),
});
}
};
self.expect(&Token::Equals)?;
let value = match self.advance() {
Some(SpannedToken {
token: Token::BoolLit(b),
..
}) => Value::Boolean(*b),
Some(SpannedToken {
token: Token::NumberLit(n),
..
}) => {
let n = *n;
if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
Value::Integer(n as i64)
} else {
Value::Float(n)
}
}
Some(SpannedToken {
token: Token::StringLit(s),
..
}) => Value::String(s.clone()),
Some(t) => {
return Err(ParseError {
message: format!(
"expected field value in struct literal, found {:?}",
t.token
),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: "expected field value, found end of input".to_string(),
span: self.eof_span(),
});
}
};
fields.insert(field_name, value);
}
self.expect(&Token::RBrace)?;
Ok(Value::Struct {
struct_name: struct_name.to_string(),
fields,
})
}
fn parse_feature(&mut self) -> Result<Feature, ParseError> {
let span = self.current_span();
let id = self.parse_scoped_id("feature")?;
self.expect(&Token::Feature)?;
let name = match self.advance() {
Some(SpannedToken {
token: Token::Ident(name),
..
}) => name.clone(),
Some(t) => {
return Err(ParseError {
message: format!("expected feature name, found {:?}", t.token),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: "expected feature name, found end of input".to_string(),
span: self.eof_span(),
});
}
};
self.expect(&Token::Equals)?;
self.expect(&Token::LBrace)?;
let mut variables = Vec::new();
while self.peek().is_some_and(|t| t.token != Token::RBrace) {
variables.push(self.parse_variable()?);
}
self.expect(&Token::RBrace)?;
Ok(Feature {
id,
name,
variables,
span,
})
}
fn parse_variable(&mut self) -> Result<Variable, ParseError> {
let span = self.current_span();
let id = self.parse_scoped_id("variable")?;
let name = match self.advance() {
Some(SpannedToken {
token: Token::Ident(name),
..
}) => name.clone(),
Some(t) => {
return Err(ParseError {
message: format!("expected variable name, found {:?}", t.token),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: "expected variable name, found end of input".to_string(),
span: self.eof_span(),
});
}
};
let var_type = match self.advance() {
Some(SpannedToken {
token: Token::BooleanType,
..
}) => VarType::Boolean,
Some(SpannedToken {
token: Token::IntegerType,
..
}) => VarType::Integer,
Some(SpannedToken {
token: Token::FloatType,
..
}) => VarType::Float,
Some(SpannedToken {
token: Token::StringType,
..
}) => VarType::String,
Some(SpannedToken {
token: Token::Ident(name),
..
}) => VarType::Struct(name.clone()),
Some(t) => {
return Err(ParseError {
message: format!(
"expected type (Boolean, Integer, Float, String, or struct name), found {:?}",
t.token
),
span: t.span.clone(),
});
}
None => {
return Err(ParseError {
message: "expected type, found end of input".to_string(),
span: self.eof_span(),
});
}
};
self.expect(&Token::Equals)?;
let default = if let VarType::Struct(ref struct_name) = var_type {
self.parse_struct_literal(struct_name)?
} else {
self.parse_primitive_value(&var_type)?
};
Ok(Variable {
id,
name,
var_type,
default,
span,
})
}
}
pub fn parse(tokens: Vec<SpannedToken>) -> Result<VarFile, ParseError> {
let mut parser = Parser::new(tokens);
parser.parse_file()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::lex;
fn parse_source(input: &str) -> Result<VarFile, ParseError> {
let tokens = lex(input).map_err(|e| ParseError {
message: e.message,
span: e.span,
})?;
parse(tokens)
}
#[test]
fn parse_single_boolean_variable() {
let input = r#"1: Feature Flags = {
1: enabled Boolean = true
}"#;
let file = parse_source(input).unwrap();
assert_eq!(file.features.len(), 1);
assert_eq!(file.features[0].id, 1);
assert_eq!(file.features[0].name, "Flags");
assert_eq!(file.features[0].variables.len(), 1);
assert_eq!(file.features[0].variables[0].id, 1);
assert_eq!(file.features[0].variables[0].name, "enabled");
assert_eq!(file.features[0].variables[0].var_type, VarType::Boolean);
assert_eq!(file.features[0].variables[0].default, Value::Boolean(true));
}
#[test]
fn parse_single_integer_variable() {
let input = r#"1: Feature Config = {
1: max_items Integer = 50
}"#;
let file = parse_source(input).unwrap();
assert_eq!(file.features[0].variables[0].var_type, VarType::Integer);
assert_eq!(file.features[0].variables[0].default, Value::Integer(50));
}
#[test]
fn parse_single_float_variable() {
let input = r#"1: Feature Config = {
1: ratio Float = 3.14
}"#;
let file = parse_source(input).unwrap();
assert_eq!(file.features[0].variables[0].var_type, VarType::Float);
assert_eq!(file.features[0].variables[0].default, Value::Float(3.14));
}
#[test]
fn parse_float_variable_with_integer_literal() {
let input = r#"1: Feature Config = {
1: max_items Float = 50
}"#;
let file = parse_source(input).unwrap();
assert_eq!(file.features[0].variables[0].var_type, VarType::Float);
assert_eq!(file.features[0].variables[0].default, Value::Float(50.0));
}
#[test]
fn parse_single_string_variable() {
let input = r#"1: Feature Config = {
1: title String = "Hello"
}"#;
let file = parse_source(input).unwrap();
assert_eq!(file.features[0].variables[0].var_type, VarType::String);
assert_eq!(
file.features[0].variables[0].default,
Value::String("Hello".to_string())
);
}
#[test]
fn parse_multiple_features() {
let input = r#"1: Feature A = {
1: x Boolean = true
}
2: Feature B = {
1: y Integer = 42
}"#;
let file = parse_source(input).unwrap();
assert_eq!(file.features.len(), 2);
assert_eq!(file.features[0].id, 1);
assert_eq!(file.features[0].name, "A");
assert_eq!(file.features[1].id, 2);
assert_eq!(file.features[1].name, "B");
}
#[test]
fn parse_example_var_file() {
let input = r#"1: Feature Checkout = {
1: enabled Boolean = true
2: max_items Integer = 50
3: header_text String = "Complete your purchase"
}"#;
let file = parse_source(input).unwrap();
assert_eq!(file.features.len(), 1);
let feature = &file.features[0];
assert_eq!(feature.id, 1);
assert_eq!(feature.name, "Checkout");
assert_eq!(feature.variables.len(), 3);
assert_eq!(feature.variables[0].id, 1);
assert_eq!(feature.variables[0].name, "enabled");
assert_eq!(feature.variables[1].id, 2);
assert_eq!(feature.variables[1].name, "max_items");
assert_eq!(feature.variables[2].id, 3);
assert_eq!(feature.variables[2].name, "header_text");
}
#[test]
fn error_missing_lbrace() {
let input = "1: Feature Checkout = 1: x Boolean = true }";
let err = parse_source(input).unwrap_err();
assert!(err.message.contains("expected LBrace"));
}
#[test]
fn error_missing_rbrace() {
let input = r#"1: Feature Checkout = {
1: x Boolean = true"#;
let err = parse_source(input).unwrap_err();
assert!(err.message.contains("expected RBrace"));
}
#[test]
fn error_missing_default_value() {
let input = r#"1: Feature Checkout = {
1: x Boolean =
}"#;
let err = parse_source(input).unwrap_err();
assert!(err.message.contains("expected default value"));
}
#[test]
fn error_missing_type() {
let input = r#"1: Feature Checkout = {
1: x = true
}"#;
let err = parse_source(input).unwrap_err();
assert!(err.message.contains("expected type"));
}
#[test]
fn error_unknown_type_keyword() {
let input = r#"1: Feature Checkout = {
1: x Map = 5
}"#;
let err = parse_source(input).unwrap_err();
assert!(err.message.contains("expected struct literal"));
}
#[test]
fn error_integer_with_fractional_value() {
let input = r#"1: Feature Config = {
1: ratio Integer = 3.14
}"#;
let err = parse_source(input).unwrap_err();
assert!(
err.message
.contains("Integer field cannot have fractional default value")
);
}
#[test]
fn error_spans_point_to_correct_location() {
let input = "1: Feature Checkout = {\n 1: x = true\n}";
let err = parse_source(input).unwrap_err();
assert_eq!(err.span.line, 2);
}
#[test]
fn parse_struct_definition() {
let input = r##"1: Struct Theme = {
1: dark_mode Boolean = false
2: font_size Integer = 14
3: primary_color String = "#000000"
}"##;
let file = parse_source(input).unwrap();
assert_eq!(file.structs.len(), 1);
assert_eq!(file.features.len(), 0);
let s = &file.structs[0];
assert_eq!(s.id, 1);
assert_eq!(s.name, "Theme");
assert_eq!(s.fields.len(), 3);
assert_eq!(s.fields[0].name, "dark_mode");
assert_eq!(s.fields[0].field_type, VarType::Boolean);
assert_eq!(s.fields[0].default, Value::Boolean(false));
assert_eq!(s.fields[1].name, "font_size");
assert_eq!(s.fields[1].field_type, VarType::Integer);
assert_eq!(s.fields[1].default, Value::Integer(14));
assert_eq!(s.fields[2].name, "primary_color");
assert_eq!(s.fields[2].field_type, VarType::String);
assert_eq!(s.fields[2].default, Value::String("#000000".to_string()));
}
#[test]
fn parse_struct_and_feature_together() {
let input = r#"1: Struct Theme = {
1: dark_mode Boolean = false
}
1: Feature Dashboard = {
1: enabled Boolean = true
2: theme Theme = Theme {}
}"#;
let file = parse_source(input).unwrap();
assert_eq!(file.structs.len(), 1);
assert_eq!(file.features.len(), 1);
let var = &file.features[0].variables[1];
assert_eq!(var.name, "theme");
assert_eq!(var.var_type, VarType::Struct("Theme".to_string()));
match &var.default {
Value::Struct {
struct_name,
fields,
} => {
assert_eq!(struct_name, "Theme");
assert!(fields.is_empty());
}
other => panic!("expected Struct value, got {:?}", other),
}
}
#[test]
fn parse_struct_literal_with_field_overrides() {
let input = r#"1: Struct Config = {
1: retries Integer = 3
2: verbose Boolean = false
}
1: Feature App = {
1: config Config = Config { retries = 5 verbose = true }
}"#;
let file = parse_source(input).unwrap();
let var = &file.features[0].variables[0];
match &var.default {
Value::Struct {
struct_name,
fields,
} => {
assert_eq!(struct_name, "Config");
assert_eq!(fields.len(), 2);
assert_eq!(fields["retries"], Value::Integer(5));
assert_eq!(fields["verbose"], Value::Boolean(true));
}
other => panic!("expected Struct value, got {:?}", other),
}
}
#[test]
fn error_missing_feature_id() {
let input = r#"Feature Checkout = {
1: enabled Boolean = true
}"#;
let err = parse_source(input).unwrap_err();
assert!(
err.message.contains("expected feature id")
|| err.message.contains("expected Feature or Struct")
);
}
#[test]
fn error_missing_variable_id() {
let input = r#"1: Feature Checkout = {
enabled Boolean = true
}"#;
let err = parse_source(input).unwrap_err();
assert!(err.message.contains("expected variable id"));
}
#[test]
fn error_non_integer_feature_id() {
let input = r#"1.5: Feature Checkout = {
1: enabled Boolean = true
}"#;
let err = parse_source(input).unwrap_err();
assert!(err.message.contains("expected feature id to be a u32"));
}
}