Skip to main content

vihaco_parser/
impls.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4use crate::Parse;
5use chumsky::error::Simple;
6use chumsky::extra;
7use chumsky::prelude::*;
8use std::{borrow::Borrow, fmt};
9
10type E<'src> = extra::Err<Simple<'src, char>>;
11
12/// An unquoted SST identifier, without a leading `@` sigil.
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct Ident(pub String);
15
16/// An unquoted SST token whose interpretation is deferred to a resolver.
17#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct BareToken(pub String);
19
20/// The decoded contents of a quoted SST string literal.
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct QuotedString(pub String);
23
24macro_rules! impl_lexical_string {
25    ($ty:ty) => {
26        impl $ty {
27            /// Borrow the lexical text.
28            pub fn as_str(&self) -> &str {
29                &self.0
30            }
31
32            /// Consume this value and return its owned text.
33            pub fn into_inner(self) -> String {
34                self.0
35            }
36        }
37
38        impl AsRef<str> for $ty {
39            fn as_ref(&self) -> &str {
40                self.as_str()
41            }
42        }
43
44        impl Borrow<str> for $ty {
45            fn borrow(&self) -> &str {
46                self.as_str()
47            }
48        }
49
50        impl fmt::Display for $ty {
51            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52                f.write_str(self.as_str())
53            }
54        }
55
56        impl From<$ty> for String {
57            fn from(value: $ty) -> Self {
58                value.into_inner()
59            }
60        }
61    };
62}
63
64impl_lexical_string!(Ident);
65impl_lexical_string!(BareToken);
66impl_lexical_string!(QuotedString);
67
68macro_rules! impl_uint {
69    ($($t:ty),+) => {
70        $(impl<'src> Parse<'src> for $t {
71            fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
72                text::int(10).try_map(|s: &str, span| s.parse().map_err(|_| Simple::new(None, span)))
73            }
74        })+
75    };
76}
77impl_uint!(u64, u32, usize);
78
79macro_rules! impl_sint {
80    ($($t:ty),+) => {
81        $(impl<'src> Parse<'src> for $t {
82            fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
83                just('-')
84                    .or_not()
85                    .then(text::int(10))
86                    .to_slice()
87                    .try_map(|s: &str, span| s.parse().map_err(|_| Simple::new(None, span)))
88            }
89        })+
90    };
91}
92impl_sint!(i64, i32);
93
94macro_rules! impl_float {
95    ($($t:ty),+) => {
96        $(impl<'src> Parse<'src> for $t {
97            fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
98                // Accepts: optional unary `-`, integer, optional `.fraction`,
99                // optional `e[+-]?digits`. Real .sst sources include both
100                // scientific-notation literals like `1.9999999999998004e-6`
101                // and negative basis components like `-0.5`; rejecting either
102                // would force every consumer to roll its own float parser.
103                let exp = one_of("eE")
104                    .then(one_of("+-").or_not())
105                    .then(text::digits(10));
106                just('-')
107                    .or_not()
108                    .then(text::int(10))
109                    .then(just('.').then(text::digits(10)).or_not())
110                    .then(exp.or_not())
111                    .to_slice()
112                    .try_map(|s: &str, span| s.parse().map_err(|_| Simple::new(None, span)))
113            }
114        })+
115    };
116}
117impl_float!(f64, f32);
118
119impl<'src> Parse<'src> for bool {
120    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
121        just("true").to(true).or(just("false").to(false))
122    }
123}
124
125impl<'src> Parse<'src> for Ident {
126    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
127        ident().map(Self)
128    }
129}
130
131impl<'src> Parse<'src> for BareToken {
132    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
133        bare_token().map(Self)
134    }
135}
136
137impl<'src> Parse<'src> for QuotedString {
138    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
139        let escape = just('\\').ignore_then(choice((
140            just('"').to('"'),
141            just('\\').to('\\'),
142            just('n').to('\n'),
143            just('t').to('\t'),
144            just('r').to('\r'),
145            just('0').to('\0'),
146        )));
147        let character = choice((
148            escape,
149            any().and_is(just('"').not()).and_is(just('\\').not()),
150        ));
151
152        character
153            .repeated()
154            .collect::<String>()
155            .delimited_by(just('"'), just('"'))
156            .map(Self)
157    }
158}
159
160impl<'src, T> Parse<'src> for Vec<T>
161where
162    T: Parse<'src> + 'src,
163{
164    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
165        T::parser()
166            .padded_by(trivia())
167            .separated_by(just(',').padded_by(trivia()))
168            .allow_trailing()
169            .collect::<Vec<_>>()
170            .delimited_by(just('[').padded_by(trivia()), just(']').padded_by(trivia()))
171    }
172}
173
174impl<'src, A, B> Parse<'src> for (A, B)
175where
176    A: Parse<'src> + 'src,
177    B: Parse<'src> + 'src,
178{
179    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
180        A::parser()
181            .padded()
182            .then_ignore(just(',').padded())
183            .then(B::parser().padded())
184            .delimited_by(just('('), just(')'))
185    }
186}
187
188/// Parse the text of an unquoted SST identifier without wrapping it in [`Ident`].
189///
190/// Dots and colons are accepted for hardware and namespaced identifiers. The
191/// `@` sigil, quotes, pattern metacharacters, whitespace, and structural
192/// delimiters are rejected.
193pub fn ident<'src>() -> impl Parser<'src, &'src str, String, E<'src>> + Clone {
194    token_text(|c| c != '@')
195}
196
197/// Parse an unquoted token without wrapping it in [`BareToken`].
198///
199/// Unlike [`ident`], this accepts `@` because a resolver may need to interpret
200/// the token later. Quotes, pattern metacharacters, whitespace, and structural
201/// delimiters are rejected.
202pub fn bare_token<'src>() -> impl Parser<'src, &'src str, String, E<'src>> + Clone {
203    token_text(|_| true)
204}
205
206fn token_text<'src>(
207    additional_filter: impl Fn(char) -> bool + Clone + 'src,
208) -> impl Parser<'src, &'src str, String, E<'src>> + Clone {
209    any()
210        .filter(move |c: &char| {
211            !c.is_whitespace()
212                && !matches!(
213                    *c,
214                    ',' | ';' | '(' | ')' | '{' | '}' | '[' | ']' | '"' | '\'' | '`'
215                )
216                && additional_filter(*c)
217        })
218        .repeated()
219        .at_least(1)
220        .collect::<String>()
221}
222
223/// Whitespace and line comments shared by metadata records and collections.
224pub fn trivia<'src>() -> impl Parser<'src, &'src str, (), E<'src>> + Clone {
225    let whitespace = any().filter(|c: &char| c.is_whitespace()).ignored();
226    let comment = just("//")
227        .then(any().and_is(just('\n').not()).repeated())
228        .ignored();
229    whitespace.or(comment).repeated().collect::<()>()
230}
231
232/// Match a bare or quoted record key, including an identifier boundary.
233pub fn metadata_key<'src>(key: &'static str) -> impl Parser<'src, &'src str, (), E<'src>> {
234    text::ascii::ident()
235        .map(str::to_owned)
236        .or(QuotedString::parser().map(|s| s.into_inner()))
237        .try_map(move |value, span| {
238            if value == key {
239                Ok(())
240            } else {
241                Err(Simple::new(None, span))
242            }
243        })
244        .padded_by(trivia())
245}
246
247impl<'src> Parse<'src> for String {
248    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
249        QuotedString::parser().map(|s| s.into_inner())
250    }
251}
252
253impl<'src, T: Parse<'src> + 'src> Parse<'src> for Option<T> {
254    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
255        text::keyword("null")
256            .map(|_| None)
257            .or(T::parser().map(Some))
258    }
259}