Skip to main content

mnk_vmf/parser/
mod.rs

1pub(crate) mod error;
2pub(crate) mod lexer;
3pub mod util;
4
5use chumsky::{
6    error::{Rich, RichReason},
7    extra,
8    input::ValueInput,
9    prelude::*,
10    span::SimpleSpan,
11    Parser as ChumskyParser,
12};
13
14/// A shorthand alias for any input source that produces our `lexer::Token` values
15/// along with `SimpleSpan` offsets, and supports value-based parsing (cloning tokens).
16///
17/// This trait is automatically implemented for any `I` that satisfies:
18/// ```ignore
19/// I: ValueInput<'src, Token = lexer::Token<'src>, Span = SimpleSpan>
20/// ```
21///
22/// This is a helper trait for a Chumsky parser over tokens, so we dont have
23/// to spell out the bound everywhere.
24pub(crate) trait TokenSource<'src>:
25    ValueInput<'src, Token = lexer::Token<'src>, Span = SimpleSpan>
26{
27}
28
29/// Seals the `TokenSource` automatically for any `I` that is a ValueInput of the right types
30impl<'src, I> TokenSource<'src> for I where
31    I: ValueInput<'src, Token = lexer::Token<'src>, Span = SimpleSpan>
32{
33}
34
35pub(crate) type TokenError<'src> = extra::Err<Rich<'src, lexer::Token<'src>>>;
36
37/// A private trait that every VMF‐block parser must implement.
38///
39/// Each implementer provides a `parser()` method that builds a Chumsky parser
40/// from any `TokenSource`.  This parser:
41/// - Consumes tokens of type `lexer::Token<'src>` from the input `I`.
42/// - Produces an instance of `Self` on success.
43/// - Yields errors of type `TokenError<'src>` on failure.
44///
45/// By making it generic over `I: TokenSource<'src>`, we can drive the parser
46/// off either a pre-collected slice of tokens (`&[Token<'_, _>]`) or a streaming
47/// iterator wrapped with `Stream::from_iter(...)`.
48pub(crate) trait InternalParser<'src>: Sized {
49    fn parser<I>() -> impl ChumskyParser<'src, I, Self, TokenError<'src>>
50    where
51        I: TokenSource<'src>;
52}
53
54/// A trait that should be implemented on all VMF block types.
55///
56/// example: `let version_info = VersionInfo::parse(input);`
57///
58// We don't expect anyone to implement VMF parsing outside of this crate,
59// so we have the Parser require InternalParser.
60#[allow(private_bounds)]
61pub trait Parser<'src>: InternalParser<'src> {
62    fn parse(
63        src: impl TokenSource<'src>,
64    ) -> Result<Self, Vec<RichReason<'src, lexer::Token<'src>>>> {
65        let result = <Self as InternalParser<'src>>::parser::<_>().parse(src);
66        if result.has_errors() {
67            Err(result.errors().map(|e| e.reason().clone()).collect())
68        } else {
69            Ok(result.unwrap())
70        }
71    }
72}
73
74/// Parse a number from `T`.
75pub(crate) fn number<'a, T, I>() -> impl ChumskyParser<'a, I, T, TokenError<'a>>
76where
77    T: std::str::FromStr,
78    T::Err: std::fmt::Debug,
79    I: TokenSource<'a>,
80{
81    select! { lexer::Token::QuotedText(s) => s }.try_map(|s: &str, span| {
82        s.parse::<T>()
83            .map_err(|_| Rich::custom(span, "integer out of range"))
84    })
85}
86
87/// Parse a boolean literal: `true` or `false`.
88pub(crate) fn boolean<'a, I>() -> impl ChumskyParser<'a, I, bool, TokenError<'a>>
89where
90    I: TokenSource<'a>,
91{
92    quoted_string("1").or(quoted_string("0")).map(|v| match v {
93        "1" => true,
94        "0" => false,
95        _ => unreachable!(),
96    })
97}
98
99/// Parses any string, that is surrounded by quotes.
100pub(crate) fn any_quoted_string<'src, I>(
101) -> impl ChumskyParser<'src, I, &'src str, TokenError<'src>>
102where
103    I: TokenSource<'src>,
104{
105    select! { lexer::Token::QuotedText(s) => s }
106}
107
108/// Parses an exact string `input`, that is surrounded by quotes.
109/// This is usefull when searching for strings, or whne looking up a key-value pair.
110pub(crate) fn quoted_string<'src, I>(
111    input: &'src str,
112) -> impl ChumskyParser<'src, I, &'src str, TokenError<'src>>
113where
114    I: TokenSource<'src>,
115{
116    select! {
117        lexer::Token::QuotedText(s) if s == input => s
118    }
119}
120
121/// Takes a `key` string value, and tries to get a value.
122/// The format of this is: "key" "string".
123pub(crate) fn key_value<'src, I>(
124    key: &'src str,
125) -> impl ChumskyParser<'src, I, &'src str, TokenError<'src>>
126where
127    I: TokenSource<'src>,
128{
129    quoted_string(key).ignore_then(any_quoted_string())
130}
131
132/// Takes a `key` string value, and tries to get a number value.
133/// The format of this is: "key" "10"
134pub(crate) fn key_value_numeric<'src, T, I>(
135    key: &'src str,
136) -> impl ChumskyParser<'src, I, T, TokenError<'src>>
137where
138    T: std::str::FromStr,
139    T::Err: std::fmt::Debug,
140    I: TokenSource<'src>,
141{
142    quoted_string(key).ignore_then(number::<T, I>())
143}
144
145/// Takes a `key` string value, and tries to get a boolean value.
146/// The format of this is: "key" "false"
147pub(crate) fn key_value_boolean<'src, I>(
148    key: &'src str,
149) -> impl ChumskyParser<'src, I, bool, TokenError<'src>>
150where
151    I: TokenSource<'src>,
152{
153    quoted_string(key).ignore_then(boolean())
154}
155
156/// Starts a parser on VMF blocks. VMF block usually starts with a key, then new line and open
157/// bracket.
158///
159/// example:
160/// versioninfo
161/// {
162pub(crate) fn open_block<'src, I>(
163    block: &'src str,
164) -> impl ChumskyParser<'src, I, (), TokenError<'src>>
165where
166    I: TokenSource<'src>,
167{
168    just(lexer::Token::Ident(block))
169        .ignore_then(just(lexer::Token::LBracket))
170        .ignored()
171}
172
173/// Closes a previously [`open_block`]. It just ignores the whitespace and the closing bracket.
174pub(crate) fn close_block<'src, I>() -> impl ChumskyParser<'src, I, (), TokenError<'src>>
175where
176    I: TokenSource<'src>,
177{
178    just(lexer::Token::RBracket).ignored()
179}
180
181/// Parses and skips any unknown/unrecognized block.
182/// It matches any identifier followed by a block, and recursively skips nested blocks.
183pub(crate) fn skip_unknown_block<'src, I>() -> impl ChumskyParser<'src, I, (), TokenError<'src>>
184where
185    I: TokenSource<'src>,
186{
187    recursive(|skip_block| {
188        any()
189            .filter(|tok| matches!(tok, lexer::Token::Ident(_)))
190            .ignore_then(just(lexer::Token::LBracket))
191            .ignore_then(
192                none_of([lexer::Token::LBracket, lexer::Token::RBracket])
193                    .ignored()
194                    .or(skip_block)
195                    .repeated(),
196            )
197            .then_ignore(just(lexer::Token::RBracket))
198            .ignored()
199    })
200}
201
202#[cfg(test)]
203mod tests {
204    use crate::util::lex;
205
206    use super::*;
207    use chumsky::Parser;
208
209    #[test]
210    fn test_number() {
211        let stream = lex("\"12345\"");
212
213        let result = number::<u32, _>().parse(stream);
214        for e in result.errors() {
215            println!("error: {:?}", e.reason());
216        }
217        assert!(!result.has_errors());
218        assert_eq!(result.unwrap(), 12345);
219    }
220
221    #[test]
222    fn test_boolean() {
223        let stream = lex(r#""1""#);
224
225        let result = boolean::<_>().parse(stream);
226        assert!(!result.has_errors());
227        assert!(result.unwrap());
228    }
229
230    #[test]
231    fn test_key_value_numeric() {
232        let stream = lex(r#""num" "42""#);
233        let result = key_value_numeric::<u32, _>("num").parse(stream);
234        assert!(!result.has_errors());
235        assert_eq!(result.unwrap(), 42);
236    }
237
238    #[test]
239    fn test_open_close_block() {
240        let stream = lex("blk {");
241        let r1 = open_block("blk").parse(stream);
242        for e in r1.errors() {
243            println!("error: {:?}", e.reason());
244        }
245        assert!(!r1.has_errors());
246
247        let stream = lex("}");
248        let r2 = close_block().parse(stream);
249        for e in r1.errors() {
250            println!("error: {:?}", e.reason());
251        }
252        assert!(!r2.has_errors());
253    }
254}