1pub mod impls;
5
6pub use impls::{bare_token, ident, metadata_key, trivia, BareToken, Ident, QuotedString};
7
8use chumsky::error::Simple;
9use chumsky::extra;
10
11pub trait SurfaceInstruction {}
17
18pub 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 integer_overflow_is_a_parse_error() {
90 assert!(i32::parser().parse("2147483648").has_errors());
91 assert!(i32::parser().parse("-2147483649").has_errors());
92 assert!(i64::parser().parse("9223372036854775808").has_errors());
93 assert!(u32::parser().parse("4294967296").has_errors());
94 assert!(u64::parser().parse("18446744073709551616").has_errors());
95 assert_eq!(parses::<i32>("-2147483648"), i32::MIN);
96 assert_eq!(parses::<u64>("18446744073709551615"), u64::MAX);
97 }
98
99 #[test]
100 fn bool_true() {
101 assert!(parses::<bool>("true"));
102 }
103 #[test]
104 fn bool_false() {
105 assert!(!parses::<bool>("false"));
106 }
107 #[test]
108 fn ident_word() {
109 assert_eq!(parses::<Ident>("hello"), Ident("hello".to_owned()));
110 }
111
112 #[test]
113 fn ident_stops_at_whitespace() {
114 let result = Ident::parser().parse("hello world").into_result();
118 assert!(result.is_err());
119 }
120
121 #[test]
122 fn ident_operand_with_colons() {
123 assert_eq!(
124 ident().parse("AOD0:T1:A").into_result().unwrap(),
125 "AOD0:T1:A"
126 );
127 }
128
129 #[test]
130 fn ident_stops_at_comma() {
131 let result = ident()
132 .then_ignore(chumsky::primitive::just(','))
133 .parse("foo,")
134 .into_result();
135 assert_eq!(result.unwrap(), "foo");
136 }
137
138 #[test]
139 fn ident_allows_dots() {
140 assert_eq!(ident().parse("a.b.c").into_result().unwrap(), "a.b.c");
141 }
142
143 #[test]
144 fn ident_digi_target() {
145 assert_eq!(ident().parse("DIGI:0").into_result().unwrap(), "DIGI:0");
146 }
147
148 #[test]
149 fn ident_rejects_symbol_sigil_and_quote_characters() {
150 assert!(Ident::parser().parse("@target").has_errors());
151 assert!(Ident::parser().parse("\"target\"").has_errors());
152 assert!(Ident::parser().parse("'target").has_errors());
153 assert!(Ident::parser().parse("`target`").has_errors());
154 }
155
156 #[test]
157 fn bare_token_accepts_symbol_sigil_but_rejects_quotes() {
158 assert_eq!(
159 parses::<BareToken>("@target"),
160 BareToken("@target".to_owned())
161 );
162 assert!(BareToken::parser().parse("\"target\"").has_errors());
163 }
164
165 #[test]
166 fn ident_rejects_empty() {
167 assert!(ident().parse("").into_result().is_err());
168 }
169
170 #[test]
171 fn ident_rejects_leading_ws() {
172 assert!(ident().parse(" hello").into_result().is_err());
173 }
174
175 #[test]
176 fn ident_stops_at_open_paren() {
177 let result = ident()
178 .then_ignore(chumsky::primitive::just('('))
179 .parse("foo(")
180 .into_result();
181 assert_eq!(result.unwrap(), "foo");
182 }
183
184 #[test]
185 fn ident_stops_at_brace() {
186 let result = ident()
187 .then_ignore(chumsky::primitive::just('{'))
188 .parse("device{")
189 .into_result();
190 assert_eq!(result.unwrap(), "device");
191 }
192
193 #[test]
194 fn quoted_string_supports_spaces_and_escapes() {
195 assert_eq!(
196 parses::<QuotedString>("\"hello\\nworld\""),
197 QuotedString("hello\nworld".to_owned())
198 );
199 }
200
201 #[test]
202 fn lexical_newtypes_expose_owned_and_borrowed_text() {
203 let ident = Ident("target".to_owned());
204 assert_eq!(ident.as_str(), "target");
205 assert_eq!(ident.to_string(), "target");
206 assert_eq!(String::from(ident), "target");
207 }
208
209 #[test]
210 fn vec_uses_square_brackets_and_commas() {
211 assert_eq!(parses::<Vec<f64>>("[1.0, 2.5]"), vec![1.0, 2.5]);
212 assert!(parses::<Vec<f64>>("[]").is_empty());
213 }
214
215 #[test]
216 fn tuple_uses_parentheses_and_a_comma() {
217 assert_eq!(parses::<(i64, f64)>("(1, 2.5)"), (1, 2.5));
218 }
219
220 #[test]
221 fn vec_supports_nested_tuple_items() {
222 assert_eq!(
223 parses::<Vec<(f64, f64)>>("[(1.0, 2.0), (3.0, 4.0)]"),
224 vec![(1.0, 2.0), (3.0, 4.0)]
225 );
226 }
227}