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).map(|s: &str| s.parse().unwrap())
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                    .map(|s: &str| s.parse().unwrap())
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                    .map(|s: &str| s.parse().unwrap())
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()
167            .separated_by(just(',').padded())
168            .collect::<Vec<_>>()
169            .delimited_by(just('['), just(']'))
170    }
171}
172
173impl<'src, A, B> Parse<'src> for (A, B)
174where
175    A: Parse<'src> + 'src,
176    B: Parse<'src> + 'src,
177{
178    fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> {
179        A::parser()
180            .padded()
181            .then_ignore(just(',').padded())
182            .then(B::parser().padded())
183            .delimited_by(just('('), just(')'))
184    }
185}
186
187/// Parse the text of an unquoted SST identifier without wrapping it in [`Ident`].
188///
189/// Dots and colons are accepted for hardware and namespaced identifiers. The
190/// `@` sigil, quotes, pattern metacharacters, whitespace, and structural
191/// delimiters are rejected.
192pub fn ident<'src>() -> impl Parser<'src, &'src str, String, E<'src>> + Clone {
193    token_text(|c| c != '@')
194}
195
196/// Parse an unquoted token without wrapping it in [`BareToken`].
197///
198/// Unlike [`ident`], this accepts `@` because a resolver may need to interpret
199/// the token later. Quotes, pattern metacharacters, whitespace, and structural
200/// delimiters are rejected.
201pub fn bare_token<'src>() -> impl Parser<'src, &'src str, String, E<'src>> + Clone {
202    token_text(|_| true)
203}
204
205fn token_text<'src>(
206    additional_filter: impl Fn(char) -> bool + Clone + 'src,
207) -> impl Parser<'src, &'src str, String, E<'src>> + Clone {
208    any()
209        .filter(move |c: &char| {
210            !c.is_whitespace()
211                && !matches!(
212                    *c,
213                    ',' | ';' | '(' | ')' | '{' | '}' | '[' | ']' | '"' | '\'' | '`'
214                )
215                && additional_filter(*c)
216        })
217        .repeated()
218        .at_least(1)
219        .collect::<String>()
220}