bloqade_lanes_bytecode_core/isa/
text.rs1use chumsky::prelude::*;
17use vihaco::syntax::{ParsedModule, Resolve};
18use vihaco_parser_core::Parse;
19
20use super::Instruction;
21use super::program::{Program, from_code};
22use crate::version::Version;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum TextError {
27 MissingVersion,
29 InvalidVersion { line: usize, value: String },
33 BadInstruction { line: usize, text: String },
36}
37
38impl std::fmt::Display for TextError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 TextError::MissingVersion => write!(f, "missing version header"),
42 TextError::InvalidVersion { line, value } => {
43 write!(f, "line {line}: invalid version '{value}'")
44 }
45 TextError::BadInstruction { line, text } => {
46 write!(f, "line {line}: cannot parse instruction '{text}'")
47 }
48 }
49 }
50}
51
52impl std::error::Error for TextError {}
53
54#[derive(Debug, Clone, PartialEq)]
58pub enum LanesHeader {
59 Version(Version),
60}
61
62impl<'src> Parse<'src> for LanesHeader {
63 fn parser() -> impl chumsky::Parser<
64 'src,
65 &'src str,
66 Self,
67 chumsky::extra::Err<chumsky::error::Simple<'src, char>>,
68 > {
69 let uint = || {
70 any()
71 .filter(|c: &char| c.is_ascii_digit())
72 .repeated()
73 .at_least(1)
74 .collect::<String>()
75 };
76 just("version")
77 .ignore_then(chumsky::text::whitespace())
78 .ignore_then(uint().then_ignore(just('.')).then(uint()))
79 .try_map(|(maj, min), span| {
80 let major = maj
81 .parse::<u16>()
82 .map_err(|_| chumsky::error::Simple::new(None, span))?;
83 let minor = min
84 .parse::<u16>()
85 .map_err(|_| chumsky::error::Simple::new(None, span))?;
86 Ok(LanesHeader::Version(Version::new(major, minor)))
87 })
88 }
89}
90
91struct LanesResolver;
94
95impl Resolve<Instruction, LanesHeader> for LanesResolver {
96 type Module = Program;
97
98 fn resolve_module(
99 &mut self,
100 parsed: ParsedModule<Instruction, LanesHeader>,
101 ) -> eyre::Result<Program> {
102 let version = match parsed.headers.as_slice() {
106 [] => eyre::bail!("missing version header"),
107 [LanesHeader::Version(v)] => *v,
108 _ => eyre::bail!("multiple version headers"),
109 };
110
111 let func = match parsed.functions.as_slice() {
114 [f] => f,
115 _ => eyre::bail!("expected exactly one function (@main)"),
116 };
117 if func.name != "main" {
118 eyre::bail!("expected function @main, found @{}", func.name);
119 }
120
121 let code = self.resolve_body(func.body.clone())?;
123 Ok(from_code(version, code))
124 }
125}
126
127fn map_resolve_err(err: &eyre::Report) -> TextError {
130 let msg = err.to_string();
131 if msg.contains("missing version header") {
132 TextError::MissingVersion
133 } else {
134 TextError::BadInstruction { line: 0, text: msg }
135 }
136}
137
138pub fn parse_text(src: &str) -> Result<Program, TextError> {
155 let parsed = ParsedModule::<Instruction, LanesHeader>::parser()
156 .parse(src)
157 .into_result()
158 .map_err(|errs| TextError::BadInstruction {
159 line: 0,
160 text: errs
161 .iter()
162 .map(|e| e.to_string())
163 .collect::<Vec<_>>()
164 .join("; "),
165 })?;
166 LanesResolver
167 .resolve_module(parsed)
168 .map_err(|e| map_resolve_err(&e))
169}
170
171pub fn to_text(program: &Program) -> String {
175 let mut out = format!(
176 "version {}.{};\nfn @main() {{\n",
177 program.extra.version.major, program.extra.version.minor
178 );
179 for inst in &program.code {
180 out.push_str(" ");
181 out.push_str(&inst.to_string());
182 out.push('\n');
183 }
184 out.push_str("}\n");
185 out
186}
187
188#[cfg(test)]
191mod tests {
192 use super::*;
193 use vihaco::value::Value;
194 use vihaco_cpu::Instruction as Cpu;
195
196 fn sample() -> Program {
197 from_code(
198 Version::new(1, 2),
199 vec![
200 Instruction::Cpu(Cpu::Const(Value::F64(1.5))),
201 Instruction::Cpu(Cpu::Const(Value::I64(-42))),
202 Instruction::Cpu(Cpu::Dup),
203 Instruction::ConstLoc(0x0000_0000_0100_0000),
204 Instruction::ConstLane(0x0000_0000_0000_0001),
205 Instruction::ConstZone(0x0000_0003),
206 Instruction::InitialFill(2),
207 Instruction::Move(1),
208 Instruction::LocalRz(1),
209 Instruction::LocalR(3),
210 Instruction::GlobalRz,
211 Instruction::Cz,
212 Instruction::Measure(1),
213 Instruction::AwaitMeasure,
214 Instruction::NewArray(2, 10, 20),
215 Instruction::GetItem(2),
216 Instruction::SetDetector,
217 Instruction::Cpu(Cpu::Halt),
218 Instruction::Return,
219 ],
220 )
221 }
222
223 #[test]
224 fn text_round_trips_fn_main() {
225 let src = "version 1.2;\nfn @main() {\n const_loc 0x0000000000000000\n initial_fill 1\n halt\n}\n";
226 let p = parse_text(src).unwrap();
227 assert_eq!(p.extra.version, Version::new(1, 2));
228 assert_eq!(p.code.len(), 3);
229 assert_eq!(parse_text(&to_text(&p)).unwrap(), p);
230 }
231
232 #[test]
233 fn text_round_trips_full_sample() {
234 let program = sample();
235 let text = to_text(&program);
236 assert_eq!(parse_text(&text).unwrap(), program);
237 }
238
239 #[test]
240 fn to_text_contains_fn_main_header() {
241 let text = to_text(&sample());
242 assert!(text.starts_with("version 1.2;\n"));
243 assert!(text.contains("fn @main() {"));
244 assert!(text.ends_with("}\n"));
245 }
246
247 #[test]
248 fn to_text_indents_instructions() {
249 let prog = from_code(Version::new(1, 0), vec![Instruction::Cpu(Cpu::Halt)]);
250 let text = to_text(&prog);
251 assert!(text.contains(" halt\n"));
252 }
253
254 #[test]
255 fn empty_program_round_trips() {
256 let prog = from_code(Version::new(1, 0), vec![]);
257 assert_eq!(parse_text(&to_text(&prog)).unwrap(), prog);
258 }
259
260 #[test]
261 fn missing_version_header_returns_error() {
262 let src = "fn @main() {\n halt\n}\n";
263 assert_eq!(parse_text(src), Err(TextError::MissingVersion));
264 }
265
266 #[test]
267 fn bad_instruction_returns_error() {
268 let src = "version 1.0;\nfn @main() {\n nope_nope\n}\n";
269 assert!(matches!(
270 parse_text(src),
271 Err(TextError::BadInstruction { .. })
272 ));
273 }
274
275 #[test]
276 fn version_preserved() {
277 let prog = from_code(Version::new(3, 7), vec![]);
278 let text = to_text(&prog);
279 assert!(text.starts_with("version 3.7;\n"));
280 let reparsed = parse_text(&text).unwrap();
281 assert_eq!(reparsed.extra.version, Version::new(3, 7));
282 }
283
284 #[test]
285 fn text_error_display_strings() {
286 assert_eq!(
287 TextError::MissingVersion.to_string(),
288 "missing version header"
289 );
290 assert_eq!(
291 TextError::InvalidVersion {
292 line: 2,
293 value: "1.x".into()
294 }
295 .to_string(),
296 "line 2: invalid version '1.x'"
297 );
298 assert_eq!(
299 TextError::BadInstruction {
300 line: 3,
301 text: "nope".into()
302 }
303 .to_string(),
304 "line 3: cannot parse instruction 'nope'"
305 );
306 }
307
308 #[test]
309 fn multiple_functions_rejected() {
310 let src = "version 1.0;\nfn @main() {\n halt\n}\nfn @extra() {\n halt\n}\n";
313 assert!(matches!(
314 parse_text(src),
315 Err(TextError::BadInstruction { .. })
316 ));
317 }
318
319 #[test]
320 fn syntactically_broken_source_is_a_parse_error() {
321 let src = "version 1.0;\nfn @main() {\n halt\n";
324 assert!(matches!(
325 parse_text(src),
326 Err(TextError::BadInstruction { .. })
327 ));
328 }
329
330 #[test]
331 fn parse_error_preserves_diagnostic_text() {
332 let src = "version 1.0;\nfn @main() {\n halt\n";
336 match parse_text(src) {
337 Err(TextError::BadInstruction { text, .. }) => {
338 assert_ne!(text, "parse error");
339 assert!(!text.is_empty(), "diagnostic text should be non-empty");
340 }
341 other => panic!("expected BadInstruction, got {other:?}"),
342 }
343 }
344
345 #[test]
346 fn duplicate_version_headers_rejected() {
347 let src = "version 1.0;\nversion 2.0;\nfn @main() {\n halt\n}\n";
350 assert!(matches!(
351 parse_text(src),
352 Err(TextError::BadInstruction { .. })
353 ));
354 }
355
356 #[test]
357 fn non_main_function_rejected() {
358 let src = "version 1.0;\nfn @extra() {\n halt\n}\n";
360 assert!(matches!(
361 parse_text(src),
362 Err(TextError::BadInstruction { .. })
363 ));
364 }
365}