Skip to main content

vihaco_parser/
lib.rs

1// SPDX-FileCopyrightText: 2026 The vihaco Authors
2// SPDX-License-Identifier: MIT
3
4pub mod impls;
5
6pub use impls::{bare_token, ident, BareToken, Ident, QuotedString};
7
8use chumsky::error::Simple;
9use chumsky::extra;
10
11/// Marker for enums whose pattern-derived parser represents instruction
12/// syntax.
13///
14/// `#[derive(vihaco_parser_derive::Parse)]` implements this trait for enums annotated
15/// with `#[syntax_class(instruction, ...)]`.
16pub trait SurfaceInstruction {}
17
18/// A parser whose input is `&'src str` (char stream) and whose error type is `Simple<char>`.
19///
20/// The lifetime `'src` is the input lifetime. Output type `Self` is owned and does not borrow
21/// from the input.
22pub trait Parse<'src>: Sized {
23    fn parser() -> impl chumsky::Parser<'src, &'src str, Self, extra::Err<Simple<'src, char>>>;
24}
25
26#[cfg(test)]
27mod tests {
28    use super::*;
29    use chumsky::Parser;
30
31    fn parses<'src, T: Parse<'src>>(input: &'src str) -> T {
32        T::parser().parse(input).into_result().unwrap()
33    }
34
35    #[test]
36    fn i64_basic() {
37        assert_eq!(parses::<i64>("42"), 42);
38    }
39    #[test]
40    fn i32_basic() {
41        assert_eq!(parses::<i32>("7"), 7);
42    }
43    #[test]
44    fn u64_basic() {
45        assert_eq!(parses::<u64>("100"), 100);
46    }
47    #[test]
48    fn u32_basic() {
49        assert_eq!(parses::<u32>("0"), 0);
50    }
51    #[test]
52    fn usize_basic() {
53        assert_eq!(parses::<usize>("9"), 9);
54    }
55    #[test]
56    fn f64_int() {
57        assert_eq!(parses::<f64>("3"), 3.0);
58    }
59    #[test]
60    #[allow(clippy::approx_constant)]
61    fn f64_float() {
62        assert_eq!(parses::<f64>("3.14"), 3.14);
63    }
64    #[test]
65    fn f32_float() {
66        assert!((parses::<f32>("1.5") - 1.5f32).abs() < 1e-6);
67    }
68    #[test]
69    fn i64_negative() {
70        assert_eq!(parses::<i64>("-42"), -42);
71    }
72    #[test]
73    fn i32_negative() {
74        assert_eq!(parses::<i32>("-7"), -7);
75    }
76    #[test]
77    fn f64_negative() {
78        assert_eq!(parses::<f64>("-0.5"), -0.5);
79    }
80    #[test]
81    fn f64_negative_scientific() {
82        assert_eq!(parses::<f64>("-1.0e-3"), -1.0e-3);
83    }
84    #[test]
85    fn u64_rejects_negative() {
86        assert!(u64::parser().parse("-1").into_result().is_err());
87    }
88    #[test]
89    fn bool_true() {
90        assert!(parses::<bool>("true"));
91    }
92    #[test]
93    fn bool_false() {
94        assert!(!parses::<bool>("false"));
95    }
96    #[test]
97    fn ident_word() {
98        assert_eq!(parses::<Ident>("hello"), Ident("hello".to_owned()));
99    }
100
101    #[test]
102    fn ident_stops_at_whitespace() {
103        // Without a trailing end(), Parser::parse() requires consuming all input — so a
104        // token parser given "hello world" fails because " world" is left unconsumed.
105        // Use lazy() / nested combinators for composition; that's not this test's job.
106        let result = Ident::parser().parse("hello world").into_result();
107        assert!(result.is_err());
108    }
109
110    #[test]
111    fn ident_operand_with_colons() {
112        assert_eq!(
113            ident().parse("AOD0:T1:A").into_result().unwrap(),
114            "AOD0:T1:A"
115        );
116    }
117
118    #[test]
119    fn ident_stops_at_comma() {
120        let result = ident()
121            .then_ignore(chumsky::primitive::just(','))
122            .parse("foo,")
123            .into_result();
124        assert_eq!(result.unwrap(), "foo");
125    }
126
127    #[test]
128    fn ident_allows_dots() {
129        assert_eq!(ident().parse("a.b.c").into_result().unwrap(), "a.b.c");
130    }
131
132    #[test]
133    fn ident_digi_target() {
134        assert_eq!(ident().parse("DIGI:0").into_result().unwrap(), "DIGI:0");
135    }
136
137    #[test]
138    fn ident_rejects_symbol_sigil_and_quote_characters() {
139        assert!(Ident::parser().parse("@target").has_errors());
140        assert!(Ident::parser().parse("\"target\"").has_errors());
141        assert!(Ident::parser().parse("'target").has_errors());
142        assert!(Ident::parser().parse("`target`").has_errors());
143    }
144
145    #[test]
146    fn bare_token_accepts_symbol_sigil_but_rejects_quotes() {
147        assert_eq!(
148            parses::<BareToken>("@target"),
149            BareToken("@target".to_owned())
150        );
151        assert!(BareToken::parser().parse("\"target\"").has_errors());
152    }
153
154    #[test]
155    fn ident_rejects_empty() {
156        assert!(ident().parse("").into_result().is_err());
157    }
158
159    #[test]
160    fn ident_rejects_leading_ws() {
161        assert!(ident().parse("  hello").into_result().is_err());
162    }
163
164    #[test]
165    fn ident_stops_at_open_paren() {
166        let result = ident()
167            .then_ignore(chumsky::primitive::just('('))
168            .parse("foo(")
169            .into_result();
170        assert_eq!(result.unwrap(), "foo");
171    }
172
173    #[test]
174    fn ident_stops_at_brace() {
175        let result = ident()
176            .then_ignore(chumsky::primitive::just('{'))
177            .parse("device{")
178            .into_result();
179        assert_eq!(result.unwrap(), "device");
180    }
181
182    #[test]
183    fn quoted_string_supports_spaces_and_escapes() {
184        assert_eq!(
185            parses::<QuotedString>("\"hello\\nworld\""),
186            QuotedString("hello\nworld".to_owned())
187        );
188    }
189
190    #[test]
191    fn lexical_newtypes_expose_owned_and_borrowed_text() {
192        let ident = Ident("target".to_owned());
193        assert_eq!(ident.as_str(), "target");
194        assert_eq!(ident.to_string(), "target");
195        assert_eq!(String::from(ident), "target");
196    }
197
198    #[test]
199    fn vec_uses_square_brackets_and_commas() {
200        assert_eq!(parses::<Vec<f64>>("[1.0, 2.5]"), vec![1.0, 2.5]);
201        assert!(parses::<Vec<f64>>("[]").is_empty());
202    }
203
204    #[test]
205    fn tuple_uses_parentheses_and_a_comma() {
206        assert_eq!(parses::<(i64, f64)>("(1, 2.5)"), (1, 2.5));
207    }
208
209    #[test]
210    fn vec_supports_nested_tuple_items() {
211        assert_eq!(
212            parses::<Vec<(f64, f64)>>("[(1.0, 2.0), (3.0, 4.0)]"),
213            vec![(1.0, 2.0), (3.0, 4.0)]
214        );
215    }
216}