1use super::*;
4use solar_data_structures::map::{FxHashMap, FxHashSet};
5use std::fmt as std_fmt;
6
7pub fn parse_evm_ir_module(input: &str) -> Result<EvmIrModule, EvmIrParseError> {
13 Parser::new(input).parse_module()
14}
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct EvmIrParseError {
19 pub line: usize,
21 pub col: usize,
23 pub msg: String,
25 pub line_text: String,
27}
28
29impl std_fmt::Display for EvmIrParseError {
30 fn fmt(&self, f: &mut std_fmt::Formatter<'_>) -> std_fmt::Result {
31 writeln!(f, "EVM IR parse error at line {}, col {}: {}", self.line, self.col, self.msg)?;
32 if !self.line_text.is_empty() {
33 writeln!(f, " |")?;
34 writeln!(f, "{:>3} | {}", self.line, self.line_text)?;
35 let caret_pad = " ".repeat(self.col.saturating_sub(1));
36 write!(f, " | {caret_pad}^")?;
37 }
38 Ok(())
39 }
40}
41
42impl std::error::Error for EvmIrParseError {}
43
44#[derive(Clone, Debug)]
45struct ParsedBlockHeader {
46 label: String,
47 entry: bool,
48 hotness: EvmIrBlockHotness,
49 entry_stack: Vec<String>,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54enum BodyEnd {
55 Eof,
56 Brace,
57}
58
59struct Parser<'a> {
60 input: &'a str,
61 pos: usize,
62 line: usize,
63 col: usize,
64}
65
66impl<'a> Parser<'a> {
67 fn new(input: &'a str) -> Self {
68 Self { input, pos: 0, line: 1, col: 1 }
69 }
70
71 fn is_eof(&self) -> bool {
72 self.pos >= self.input.len()
73 }
74
75 fn peek_char(&self) -> Option<char> {
76 self.input[self.pos..].chars().next()
77 }
78
79 fn advance(&mut self) -> Option<char> {
80 let c = self.peek_char()?;
81 self.pos += c.len_utf8();
82 if c == '\n' {
83 self.line += 1;
84 self.col = 1;
85 } else {
86 self.col += 1;
87 }
88 Some(c)
89 }
90
91 fn skip_inline_whitespace(&mut self) {
92 while matches!(self.peek_char(), Some(' ' | '\t')) {
93 self.advance();
94 }
95 }
96
97 fn skip_inline(&mut self) {
98 self.skip_inline_whitespace();
99 }
100
101 fn skip_to_eol(&mut self) {
102 while let Some(c) = self.peek_char() {
103 if c == '\n' {
104 break;
105 }
106 self.advance();
107 }
108 }
109
110 fn skip_blank_and_comments(&mut self) {
111 loop {
112 self.skip_inline_whitespace();
113 match self.peek_char() {
114 Some('\n' | '\r') => {
115 self.advance();
116 }
117 Some('/') if self.input[self.pos..].starts_with("//") => self.skip_to_eol(),
118 Some(';') => {
119 let rest = self.input[self.pos..].trim_start_matches(';').trim_start();
120 if rest.starts_with("evm module") {
121 break;
122 }
123 self.skip_to_eol();
124 }
125 _ => break,
126 }
127 }
128 }
129
130 fn error(&self, msg: impl Into<String>) -> EvmIrParseError {
131 EvmIrParseError {
132 line: self.line,
133 col: self.col,
134 msg: msg.into(),
135 line_text: self.current_line_text(),
136 }
137 }
138
139 fn current_line_text(&self) -> String {
140 let bytes = self.input.as_bytes();
141 let pos = self.pos.min(bytes.len());
142 let mut start = pos;
143 while start > 0 && bytes[start - 1] != b'\n' {
144 start -= 1;
145 }
146 let mut end = start;
147 while end < bytes.len() && bytes[end] != b'\n' {
148 end += 1;
149 }
150 self.input[start..end].trim_end_matches('\r').to_string()
151 }
152
153 fn expect_keyword(&mut self, kw: &str) -> Result<(), EvmIrParseError> {
154 self.skip_inline();
155 if self.input[self.pos..].starts_with(kw) {
156 for _ in 0..kw.chars().count() {
157 self.advance();
158 }
159 Ok(())
160 } else {
161 Err(self.error(format!("expected `{kw}`")))
162 }
163 }
164
165 fn expect_punct(&mut self, expected: char) -> Result<(), EvmIrParseError> {
166 self.skip_inline();
167 match self.peek_char() {
168 Some(c) if c == expected => {
169 self.advance();
170 Ok(())
171 }
172 Some(c) => Err(self.error(format!("expected `{expected}`, found `{c}`"))),
173 None => Err(self.error(format!("expected `{expected}`, found EOF"))),
174 }
175 }
176
177 fn try_punct(&mut self, expected: char) -> bool {
178 self.skip_inline();
179 if self.peek_char() == Some(expected) {
180 self.advance();
181 true
182 } else {
183 false
184 }
185 }
186
187 fn parse_ident(&mut self) -> Result<&'a str, EvmIrParseError> {
188 self.skip_inline();
189 let start = self.pos;
190 match self.peek_char() {
191 Some(c) if is_ident_start(c) => {
192 self.advance();
193 }
194 _ => return Err(self.error("expected identifier")),
195 }
196 while let Some(c) = self.peek_char() {
197 if is_ident_continue(c) {
198 self.advance();
199 } else {
200 break;
201 }
202 }
203 Ok(&self.input[start..self.pos])
204 }
205
206 fn parse_uint_literal(&mut self) -> Result<U256, EvmIrParseError> {
207 self.skip_inline();
208 let start = self.pos;
209 if self.input[self.pos..].starts_with("0x") || self.input[self.pos..].starts_with("0X") {
210 self.advance();
211 self.advance();
212 while let Some(c) = self.peek_char() {
213 if c.is_ascii_hexdigit() {
214 self.advance();
215 } else {
216 break;
217 }
218 }
219 let s = &self.input[start..self.pos];
220 if s.len() == 2 {
221 return Err(self.error("expected hex digits"));
222 }
223 U256::from_str_radix(&s[2..], 16).map_err(|e| self.error(format!("invalid hex: {e}")))
224 } else if matches!(self.peek_char(), Some(c) if c.is_ascii_digit()) {
225 while let Some(c) = self.peek_char() {
226 if c.is_ascii_digit() {
227 self.advance();
228 } else {
229 break;
230 }
231 }
232 let s = &self.input[start..self.pos];
233 s.parse::<U256>().map_err(|e| self.error(format!("invalid integer: {e}")))
234 } else {
235 Err(self.error("expected integer literal"))
236 }
237 }
238
239 fn parse_module(&mut self) -> Result<EvmIrModule, EvmIrParseError> {
240 self.skip_blank_and_comments();
241 let name = if self.try_punct(';') {
242 self.expect_keyword("evm")?;
243 self.expect_keyword("module")?;
244 self.expect_punct('@')?;
245 let name = self.parse_ident()?.to_string();
246 self.skip_to_eol();
247 name
248 } else {
249 "module".to_string()
250 };
251
252 let mut module = EvmIrModule::new(name);
253 self.skip_blank_and_comments();
254 let legacy_function_wrapper = self.input[self.pos..].starts_with("fn");
255 if legacy_function_wrapper {
256 self.expect_keyword("fn")?;
257 self.expect_punct('@')?;
258 let _legacy_function_name = self.parse_ident()?;
259 self.expect_punct('{')?;
260 self.parse_program_body(&mut module, BodyEnd::Brace)?;
261 } else {
262 self.parse_program_body(&mut module, BodyEnd::Eof)?;
263 }
264 Ok(module)
265 }
266
267 fn parse_program_body(
268 &mut self,
269 module: &mut EvmIrModule,
270 body_end: BodyEnd,
271 ) -> Result<(), EvmIrParseError> {
272 self.skip_blank_and_comments();
273 let body_pos = self.pos;
274 let body_line = self.line;
275 let body_col = self.col;
276 let mut block_labels = FxHashMap::default();
277
278 loop {
279 self.skip_blank_and_comments();
280 if self.is_eof() {
281 if body_end == BodyEnd::Brace {
282 return Err(self.error("unterminated EVM IR block body"));
283 }
284 break;
285 }
286 if body_end == BodyEnd::Brace && self.peek_char() == Some('}') {
287 break;
288 }
289 if let Some(header) = self.try_parse_block_header()? {
290 if block_labels.contains_key(&header.label) {
291 return Err(self.error(format!("duplicate block `{}`", header.label)));
292 }
293 let block_id = module.add_block(EvmIrBlock::new(header.label.clone()));
294 block_labels.insert(header.label, block_id);
295 self.skip_to_eol();
296 } else {
297 self.skip_to_eol();
298 }
299 }
300
301 if block_labels.is_empty() {
302 return Err(self.error("program must contain at least one block"));
303 }
304
305 self.pos = body_pos;
306 self.line = body_line;
307 self.col = body_col;
308
309 let mut current_block = None;
310 let mut value_labels = FxHashMap::default();
311 let mut defined_values = FxHashSet::default();
312 loop {
313 self.skip_blank_and_comments();
314 if self.is_eof() {
315 if body_end == BodyEnd::Brace {
316 return Err(self.error("unterminated EVM IR block body"));
317 }
318 break;
319 }
320 if body_end == BodyEnd::Brace && self.try_punct('}') {
321 break;
322 }
323 if let Some(header) = self.try_parse_block_header()? {
324 let block_id = block_labels[&header.label];
325 if header.entry {
326 module.entry_block = Some(block_id);
327 }
328 module.blocks[block_id].metadata.hotness = header.hotness;
329 let mut entry_stack = Vec::with_capacity(header.entry_stack.len());
330 for name in &header.entry_stack {
331 entry_stack.push(value_id(module, &mut value_labels, name));
332 }
333 module.blocks[block_id].entry_stack = entry_stack;
334 current_block = Some(block_id);
335 self.skip_to_eol();
336 continue;
337 }
338
339 let block =
340 current_block.ok_or_else(|| self.error("instruction outside of any block"))?;
341 self.parse_instruction_or_terminator(
342 module,
343 block,
344 &block_labels,
345 &mut value_labels,
346 &mut defined_values,
347 )?;
348 }
349
350 Ok(())
351 }
352
353 fn try_parse_block_header(&mut self) -> Result<Option<ParsedBlockHeader>, EvmIrParseError> {
354 let save = (self.pos, self.line, self.col);
355 self.skip_inline_whitespace();
356 let Some(label) = self.try_parse_block_label_text()? else {
357 self.restore(save);
358 return Ok(None);
359 };
360
361 let mut entry = false;
362 self.skip_inline_whitespace();
363 if self.input[self.pos..].starts_with("(entry)") {
364 for _ in 0.."(entry)".len() {
365 self.advance();
366 }
367 entry = true;
368 }
369
370 let mut hotness = EvmIrBlockHotness::Hot;
371 self.skip_inline_whitespace();
372 if self.try_punct('[') {
373 let key = self.parse_ident()?;
374 if key == "cold" {
375 hotness = EvmIrBlockHotness::Cold;
376 } else if key == "hot" {
377 hotness = EvmIrBlockHotness::Hot;
378 } else if key == "hotness" {
379 self.expect_punct('=')?;
380 let value = self.parse_ident()?;
381 hotness = EvmIrBlockHotness::parse(value)
382 .ok_or_else(|| self.error(format!("unknown block hotness `{value}`")))?;
383 } else {
384 return Err(self.error(format!("unknown block metadata `{key}`")));
385 }
386 self.expect_punct(']')?;
387 }
388
389 let mut entry_stack = Vec::new();
391 self.skip_inline_whitespace();
392 let save_in = (self.pos, self.line, self.col);
393 if self.try_punct('(') {
394 self.skip_inline_whitespace();
395 let keyword = if matches!(self.peek_char(), Some(c) if is_ident_start(c)) {
396 Some(self.parse_ident()?)
397 } else {
398 None
399 };
400 if keyword == Some("in") {
401 loop {
402 self.skip_inline_whitespace();
403 if self.try_punct(')') {
404 break;
405 }
406 entry_stack.push(self.parse_value_name()?);
407 self.skip_inline_whitespace();
408 if self.try_punct(',') {
409 continue;
410 }
411 self.expect_punct(')')?;
412 break;
413 }
414 } else {
415 self.restore(save_in);
416 }
417 }
418
419 self.skip_inline_whitespace();
420 if self.peek_char() != Some(':') {
421 self.restore(save);
422 return Ok(None);
423 }
424 self.advance();
425
426 Ok(Some(ParsedBlockHeader { label, entry, hotness, entry_stack }))
427 }
428
429 fn try_parse_block_label_text(&mut self) -> Result<Option<String>, EvmIrParseError> {
430 self.skip_inline();
431 if !self.input[self.pos..].starts_with("bb") {
432 return Ok(None);
433 }
434 let start = self.pos;
435 self.advance();
436 self.advance();
437 let digits_start = self.pos;
438 while matches!(self.peek_char(), Some(c) if c.is_ascii_digit()) {
439 self.advance();
440 }
441 if self.pos == digits_start {
442 return Err(self.error("expected block number after `bb`"));
443 }
444 Ok(Some(self.input[start..self.pos].to_string()))
445 }
446
447 fn restore(&mut self, saved: (usize, usize, usize)) {
448 (self.pos, self.line, self.col) = saved;
449 }
450
451 fn parse_instruction_or_terminator(
452 &mut self,
453 module: &mut EvmIrModule,
454 block: EvmIrBlockId,
455 block_labels: &FxHashMap<String, EvmIrBlockId>,
456 value_labels: &mut FxHashMap<String, EvmIrValueId>,
457 defined_values: &mut FxHashSet<EvmIrValueId>,
458 ) -> Result<(), EvmIrParseError> {
459 self.skip_inline_whitespace();
460 if module.blocks[block].terminator.is_some() {
461 return Err(self.error(format!(
462 "instruction after terminator in block `{}`",
463 module.blocks[block].label
464 )));
465 }
466
467 let result = self.try_parse_result(module, value_labels, defined_values)?;
468 let mnemonic = self.parse_ident()?.to_string();
469 if let Some(kind) = self.parse_terminator_kind(
470 &mnemonic,
471 module,
472 block_labels,
473 value_labels,
474 defined_values,
475 )? {
476 if result.is_some() {
477 return Err(self.error("terminator cannot produce a result"));
478 }
479 let metadata = self.parse_metadata()?;
480 module.blocks[block].terminator = Some(EvmIrTerminator { kind, metadata });
481 self.skip_to_eol();
482 return Ok(());
483 }
484
485 let operands =
486 self.parse_operand_list(module, block_labels, value_labels, defined_values)?;
487 let metadata = self.parse_metadata()?;
488 let kind = EvmIrStackOp::parse(&mnemonic)
489 .map(EvmIrInstructionKind::Stack)
490 .unwrap_or(EvmIrInstructionKind::Operation(mnemonic));
491 module.blocks[block].instructions.push(EvmIrInstruction {
492 result,
493 kind,
494 operands,
495 metadata,
496 });
497 self.skip_to_eol();
498 Ok(())
499 }
500
501 fn try_parse_result(
502 &mut self,
503 module: &mut EvmIrModule,
504 value_labels: &mut FxHashMap<String, EvmIrValueId>,
505 defined_values: &mut FxHashSet<EvmIrValueId>,
506 ) -> Result<Option<EvmIrValueId>, EvmIrParseError> {
507 let save = (self.pos, self.line, self.col);
508 if self.peek_char() != Some('%') {
509 return Ok(None);
510 }
511 let name = self.parse_value_name()?;
512 self.skip_inline_whitespace();
513 if self.peek_char() != Some('=') {
514 self.restore(save);
515 return Ok(None);
516 }
517 self.advance();
518 let value = value_id(module, value_labels, &name);
519 if !defined_values.insert(value) {
520 return Err(self.error(format!("duplicate value `%{name}`")));
521 }
522 Ok(Some(value))
523 }
524
525 fn parse_value_name(&mut self) -> Result<String, EvmIrParseError> {
526 self.skip_inline();
527 self.expect_punct('%')?;
528 let start = self.pos;
529 match self.peek_char() {
530 Some(c) if is_ident_start(c) || c.is_ascii_digit() => {
531 self.advance();
532 }
533 _ => return Err(self.error("expected value name")),
534 }
535 while let Some(c) = self.peek_char() {
536 if is_ident_continue(c) {
537 self.advance();
538 } else {
539 break;
540 }
541 }
542 Ok(self.input[start..self.pos].to_string())
543 }
544
545 fn parse_terminator_kind(
546 &mut self,
547 mnemonic: &str,
548 module: &mut EvmIrModule,
549 block_labels: &FxHashMap<String, EvmIrBlockId>,
550 value_labels: &mut FxHashMap<String, EvmIrValueId>,
551 defined_values: &mut FxHashSet<EvmIrValueId>,
552 ) -> Result<Option<EvmIrTerminatorKind>, EvmIrParseError> {
553 let kind = match mnemonic {
554 "fallthrough" => EvmIrTerminatorKind::Fallthrough(self.parse_block_ref(block_labels)?),
555 "jump" => EvmIrTerminatorKind::Jump(self.parse_block_ref(block_labels)?),
556 "br" => {
557 let condition =
558 self.parse_operand(module, block_labels, value_labels, defined_values)?;
559 self.expect_punct(',')?;
560 let then_block = self.parse_block_ref(block_labels)?;
561 self.expect_punct(',')?;
562 let else_block = self.parse_block_ref(block_labels)?;
563 EvmIrTerminatorKind::Branch { condition, then_block, else_block }
564 }
565 "switch" => {
566 let value =
567 self.parse_operand(module, block_labels, value_labels, defined_values)?;
568 self.expect_punct(',')?;
569 self.expect_keyword("default")?;
570 let default = self.parse_block_ref(block_labels)?;
571 self.expect_punct(',')?;
572 self.expect_punct('[')?;
573 let mut cases = Vec::new();
574 if !self.try_punct(']') {
575 loop {
576 let case =
577 self.parse_operand(module, block_labels, value_labels, defined_values)?;
578 self.expect_keyword("=>")?;
579 let target = self.parse_block_ref(block_labels)?;
580 cases.push((case, target));
581 if self.try_punct(',') {
582 continue;
583 }
584 self.expect_punct(']')?;
585 break;
586 }
587 }
588 EvmIrTerminatorKind::Switch { value, default, cases }
589 }
590 "return" => {
591 let offset =
592 self.parse_operand(module, block_labels, value_labels, defined_values)?;
593 self.expect_punct(',')?;
594 let size =
595 self.parse_operand(module, block_labels, value_labels, defined_values)?;
596 EvmIrTerminatorKind::Return { offset, size }
597 }
598 "revert" => {
599 let offset =
600 self.parse_operand(module, block_labels, value_labels, defined_values)?;
601 self.expect_punct(',')?;
602 let size =
603 self.parse_operand(module, block_labels, value_labels, defined_values)?;
604 EvmIrTerminatorKind::Revert { offset, size }
605 }
606 "stop" => EvmIrTerminatorKind::Stop,
607 "invalid" => EvmIrTerminatorKind::Invalid,
608 "selfdestruct" => {
609 let recipient =
610 self.parse_operand(module, block_labels, value_labels, defined_values)?;
611 EvmIrTerminatorKind::SelfDestruct { recipient }
612 }
613 "terminal" => {
614 let opcode = self.parse_uint_literal()?;
615 let Ok(opcode) = u8::try_from(opcode) else {
616 return Err(self.error("raw terminal opcode must fit in one byte"));
617 };
618 EvmIrTerminatorKind::RawOpcode(opcode)
619 }
620 _ => return Ok(None),
621 };
622 Ok(Some(kind))
623 }
624
625 fn parse_operand_list(
626 &mut self,
627 module: &mut EvmIrModule,
628 block_labels: &FxHashMap<String, EvmIrBlockId>,
629 value_labels: &mut FxHashMap<String, EvmIrValueId>,
630 defined_values: &mut FxHashSet<EvmIrValueId>,
631 ) -> Result<Vec<EvmIrOperand>, EvmIrParseError> {
632 let mut operands = Vec::new();
633 self.skip_inline();
634 if self.at_end_of_operation() {
635 return Ok(operands);
636 }
637 loop {
638 operands.push(self.parse_operand(
639 module,
640 block_labels,
641 value_labels,
642 defined_values,
643 )?);
644 self.skip_inline();
645 if !self.try_punct(',') {
646 break;
647 }
648 }
649 Ok(operands)
650 }
651
652 fn parse_operand(
653 &mut self,
654 module: &mut EvmIrModule,
655 block_labels: &FxHashMap<String, EvmIrBlockId>,
656 value_labels: &mut FxHashMap<String, EvmIrValueId>,
657 _defined_values: &mut FxHashSet<EvmIrValueId>,
658 ) -> Result<EvmIrOperand, EvmIrParseError> {
659 self.skip_inline();
660 if self.peek_char() == Some('%') {
661 let name = self.parse_value_name()?;
662 return Ok(EvmIrOperand::Value(value_id(module, value_labels, &name)));
663 }
664 if matches!(self.peek_char(), Some(c) if c.is_ascii_digit()) {
665 return Ok(EvmIrOperand::Immediate(self.parse_uint_literal()?));
666 }
667 if self.peek_char() == Some('@') {
668 self.advance();
669 let symbol = self.parse_ident()?;
670 return Ok(EvmIrOperand::Symbol(format!("@{symbol}")));
671 }
672 if self.input[self.pos..].starts_with("bb") {
673 let save = (self.pos, self.line, self.col);
674 if let Some(label) = self.try_parse_block_label_text()? {
675 if let Some(block) = block_labels.get(&label).copied() {
676 return Ok(EvmIrOperand::Block(block));
677 }
678 return Err(self.error(format!("unknown block `{label}`")));
679 }
680 self.restore(save);
681 }
682 Ok(EvmIrOperand::Symbol(self.parse_ident()?.to_string()))
683 }
684
685 fn parse_block_ref(
686 &mut self,
687 block_labels: &FxHashMap<String, EvmIrBlockId>,
688 ) -> Result<EvmIrBlockId, EvmIrParseError> {
689 let label =
690 self.try_parse_block_label_text()?.ok_or_else(|| self.error("expected block label"))?;
691 block_labels
692 .get(&label)
693 .copied()
694 .ok_or_else(|| self.error(format!("unknown block `{label}`")))
695 }
696
697 fn parse_metadata(&mut self) -> Result<EvmIrMetadata, EvmIrParseError> {
698 let mut metadata = EvmIrMetadata::default();
699 self.skip_inline();
700 if !self.try_punct('!') {
701 return Ok(metadata);
702 }
703 self.expect_keyword("meta")?;
704 self.expect_punct('(')?;
705 if self.try_punct(')') {
706 return Ok(metadata);
707 }
708
709 loop {
710 let key = self.parse_ident()?.to_string();
711 if key == "stack" {
712 self.expect_punct('=')?;
713 let inputs = self.parse_u16()?;
714 self.expect_keyword("->")?;
715 let outputs = self.parse_u16()?;
716 metadata.stack = Some(EvmIrStackEffect::new(inputs, outputs));
717 } else if self.try_punct('=') {
718 let value = self.parse_metadata_value()?;
719 metadata.attrs.push(EvmIrMetadataItem { key, value: Some(value) });
720 } else {
721 metadata.attrs.push(EvmIrMetadataItem { key, value: None });
722 }
723
724 if self.try_punct(',') {
725 continue;
726 }
727 self.expect_punct(')')?;
728 break;
729 }
730 Ok(metadata)
731 }
732
733 fn parse_metadata_value(&mut self) -> Result<String, EvmIrParseError> {
734 self.skip_inline();
735 let start = self.pos;
736 while let Some(c) = self.peek_char() {
737 if c == ',' || c == ')' || c == '\n' || c == '\r' {
738 break;
739 }
740 self.advance();
741 }
742 let value = self.input[start..self.pos].trim();
743 if value.is_empty() {
744 return Err(self.error("expected metadata value"));
745 }
746 Ok(value.to_string())
747 }
748
749 fn parse_u16(&mut self) -> Result<u16, EvmIrParseError> {
750 let value = self.parse_uint_literal()?;
751 value.try_into().map_err(|_| self.error(format!("integer `{value}` does not fit in u16")))
752 }
753
754 fn at_end_of_operation(&self) -> bool {
755 matches!(self.peek_char(), None | Some('\n' | '\r' | '!' | '}'))
756 }
757}
758
759fn value_id(
760 module: &mut EvmIrModule,
761 value_labels: &mut FxHashMap<String, EvmIrValueId>,
762 name: &str,
763) -> EvmIrValueId {
764 if let Some(value) = value_labels.get(name).copied() {
765 return value;
766 }
767 let value = module.add_value(name.to_string());
768 value_labels.insert(name.to_string(), value);
769 value
770}
771
772#[cfg(test)]
773mod tests {
774 use super::parse_evm_ir_module;
775 use std::path::{Path, PathBuf};
776
777 fn evm_ir_fixture_dir() -> PathBuf {
778 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
779 .join("..")
780 .join("..")
781 .join("tests")
782 .join("ui")
783 .join("codegen")
784 .join("evm-ir")
785 }
786
787 #[test]
788 fn round_trip_all_evm_ir_fixtures() {
789 let dir = evm_ir_fixture_dir();
790 assert!(dir.exists(), "EVM IR fixture dir not found: {}", dir.display());
791
792 let mut failures = Vec::new();
793 let mut count = 0usize;
794 for entry in std::fs::read_dir(&dir).unwrap() {
795 let path = entry.unwrap().path();
796 if path.extension().and_then(|s| s.to_str()) != Some("evmir") {
797 continue;
798 }
799 count += 1;
800 if let Err(err) = round_trip_fixture(&path) {
801 let name = path.file_name().unwrap().to_string_lossy();
802 failures.push(format!("{name}: {err}"));
803 }
804 }
805
806 assert!(count > 0, "no .evmir fixtures found in {}", dir.display());
807 assert!(
808 failures.is_empty(),
809 "{} EVM IR round-trip failure(s):\n {}",
810 failures.len(),
811 failures.join("\n ")
812 );
813 }
814
815 #[test]
816 fn parser_rejects_instructions_after_terminator() {
817 let input = "\
818; evm module @m
819
820fn @f {
821 bb0 (entry):
822 stop
823 invalid
824}
825";
826 let err = parse_evm_ir_module(input).unwrap_err().to_string();
827 assert!(err.contains("instruction after terminator"), "{err}");
828 }
829
830 fn round_trip_fixture(path: &Path) -> Result<(), String> {
831 #[allow(clippy::disallowed_methods)]
832 let input = std::fs::read_to_string(path).map_err(|err| err.to_string())?;
833 let print1 =
834 parse_evm_ir_module(&input).map_err(|err| err.to_string())?.to_text().to_string();
835 let print2 =
836 parse_evm_ir_module(&print1).map_err(|err| err.to_string())?.to_text().to_string();
837 if print1 != print2 {
838 return Err(first_diff(&print1, &print2)
839 .map(|(line, a, b)| {
840 format!("first diff at line {line}:\n first: {a}\n second: {b}")
841 })
842 .unwrap_or_else(|| "printed text differs".to_string()));
843 }
844 Ok(())
845 }
846
847 fn first_diff<'a>(a: &'a str, b: &'a str) -> Option<(usize, &'a str, &'a str)> {
848 a.lines()
849 .zip(b.lines())
850 .enumerate()
851 .find(|(_, (lhs, rhs))| lhs != rhs)
852 .map(|(index, (lhs, rhs))| (index + 1, lhs, rhs))
853 }
854}