Skip to main content

solar_codegen/backend/evm/ir/
parse.rs

1//! EVM IR text parser.
2
3use super::*;
4use solar_data_structures::map::{FxHashMap, FxHashSet};
5use std::fmt as std_fmt;
6
7/// Parses an EVM IR module from the text format.
8///
9/// # Errors
10///
11/// Returns an [`EvmIrParseError`] if `input` is malformed.
12pub fn parse_evm_ir_module(input: &str) -> Result<EvmIrModule, EvmIrParseError> {
13    Parser::new(input).parse_module()
14}
15
16/// An error produced while parsing the EVM IR text format.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct EvmIrParseError {
19    /// 1-based line number.
20    pub line: usize,
21    /// 1-based column number.
22    pub col: usize,
23    /// Human-readable message.
24    pub msg: String,
25    /// Source line captured for snippet rendering.
26    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    /// Incoming stack-word names from an `(in %a, %b)` signature, top first.
50    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        // Optional incoming stack signature: `(in %a, %b)`.
390        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}