Skip to main content

solar_codegen/mir/
mod.rs

1//! Mid-level Intermediate Representation (MIR).
2//!
3//! MIR is an SSA-form IR that sits between HIR and EVM bytecode.
4
5use solar_data_structures::newtype_index;
6
7mod types;
8pub use types::MirType;
9
10mod value;
11pub use value::{Immediate, Value};
12
13mod inst;
14pub use inst::{
15    EffectKind, InstKind, Instruction, InstructionMetadata, MemoryRegion, StorageAlias,
16};
17
18mod block;
19pub use block::{BasicBlock, Terminator};
20
21mod function;
22pub use function::{Function, FunctionAttributes};
23
24mod module;
25pub use module::{DataSegment, IMMUTABLE_WORD_SIZE, ImmutableSlot, Module, StorageSlot};
26
27mod builder;
28pub use builder::FunctionBuilder;
29
30mod display;
31
32mod parser;
33pub use parser::{ParseError, parse_function, parse_module};
34
35pub(crate) mod utils;
36
37newtype_index! {
38    /// A unique identifier for a value in the MIR.
39    pub struct ValueId;
40}
41
42newtype_index! {
43    /// A unique identifier for an instruction in the MIR.
44    pub struct InstId;
45}
46
47newtype_index! {
48    /// A unique identifier for a basic block in the MIR.
49    pub struct BlockId;
50}
51
52newtype_index! {
53    /// A unique identifier for a function in the MIR.
54    pub struct FunctionId;
55}
56
57/// Property tests verifying that the MIR printer/parser pair is self-consistent.
58///
59/// For each fixture under `tests/ui/codegen/`:
60/// 1. Obtain a `Module` (either by lowering Solidity or by parsing `.mir` text).
61/// 2. Print it (`print1`).
62/// 3. Parse `print1` (`parsed1`).
63/// 4. Print `parsed1` (`print2`).
64/// 5. Parse `print2` (`parsed2`).
65/// 6. Print `parsed2` (`print3`).
66/// 7. Assert `print2 == print3` — i.e., the parser+printer pair is **idempotent**.
67///
68/// Why not assert `print1 == print2`? Raw `.mir` fixtures may use arbitrary
69/// `vN` labels, and the first print canonicalizes them to result-instruction
70/// indices. A *second* round-trip must be stable.
71#[cfg(test)]
72mod round_trip {
73    use super::{Module, parse_module};
74    use crate::{analysis::validate_module, lower};
75    use solar_interface::{ColorChoice, Session};
76    use solar_sema::Compiler;
77    use std::{
78        ops::ControlFlow,
79        path::{Path, PathBuf},
80    };
81
82    /// Path to `tests/ui/codegen/` (the workspace's UI test directory).
83    fn ui_codegen_dir() -> PathBuf {
84        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
85            .join("..")
86            .join("..")
87            .join("tests")
88            .join("ui")
89            .join("codegen")
90    }
91
92    /// Returns the (line index, line A, line B) of the first divergence between
93    /// two strings, or `None` if they're equal. Used to keep failure messages
94    /// readable when the printed MIR is large.
95    fn first_diff<'a>(a: &'a str, b: &'a str) -> Option<(usize, &'a str, &'a str)> {
96        a.lines()
97            .zip(b.lines())
98            .enumerate()
99            .find(|(_, (la, lb))| la != lb)
100            .map(|(i, (la, lb))| (i + 1, la, lb))
101    }
102
103    #[test]
104    fn round_trip_all_sol_files() {
105        let dir = ui_codegen_dir();
106        assert!(dir.exists(), "ui codegen dir not found: {}", dir.display());
107
108        let mut failures: Vec<String> = Vec::new();
109        let mut count = 0usize;
110        for entry in std::fs::read_dir(&dir).unwrap() {
111            let path = entry.unwrap().path();
112            if path.extension().and_then(|s| s.to_str()) != Some("sol") {
113                continue;
114            }
115            count += 1;
116            if let Err(e) = round_trip_sol(&path) {
117                let name = path.file_name().unwrap().to_string_lossy().into_owned();
118                failures.push(format!("{name}: {e}"));
119            }
120        }
121        assert!(count > 0, "no .sol fixtures found in {}", dir.display());
122        assert!(
123            failures.is_empty(),
124            "{} round-trip failure(s):\n  {}",
125            failures.len(),
126            failures.join("\n  ")
127        );
128    }
129
130    #[test]
131    fn validate_all_lowered_sol_modules() {
132        // Sanity check: every .sol fixture under tests/ui/codegen/ should
133        // lower to well-formed MIR (the validator finds zero errors).
134        let dir = ui_codegen_dir();
135        let mut failures: Vec<String> = Vec::new();
136        let mut count = 0usize;
137        for entry in std::fs::read_dir(&dir).unwrap() {
138            let path = entry.unwrap().path();
139            if path.extension().and_then(|s| s.to_str()) != Some("sol") {
140                continue;
141            }
142            count += 1;
143            if let Err(e) = validate_sol(&path) {
144                let name = path.file_name().unwrap().to_string_lossy().into_owned();
145                failures.push(format!("{name}: {e}"));
146            }
147        }
148        assert!(count > 0, "no .sol fixtures found");
149        assert!(
150            failures.is_empty(),
151            "{} validation failure(s):\n  {}",
152            failures.len(),
153            failures.join("\n  ")
154        );
155    }
156
157    fn validate_sol(path: &Path) -> Result<(), String> {
158        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
159        let mut compiler = Compiler::new(sess);
160
161        let parse_result = compiler.enter_mut(|c| -> solar_interface::Result<()> {
162            let mut pcx = c.parse();
163            pcx.load_files([path])?;
164            pcx.parse();
165            Ok(())
166        });
167        if parse_result.is_err() {
168            return Err("parse failed".into());
169        }
170
171        let mut result: Result<(), String> = Ok(());
172        let _ = compiler.enter_mut(|c| -> solar_interface::Result<()> {
173            let ControlFlow::Continue(()) = c.lower_asts()? else { return Ok(()) };
174            let ControlFlow::Continue(()) = c.analysis()? else { return Ok(()) };
175            let gcx = c.gcx();
176            for id in gcx.hir.contract_ids() {
177                let contract = gcx.hir.contract(id);
178                if contract.kind.is_interface() || contract.kind.is_abstract_contract() {
179                    continue;
180                }
181                let module = lower::lower_contract(gcx, id);
182                let errors = validate_module(&module);
183                if !errors.is_empty() {
184                    result = Err(format!(
185                        "contract `{}` has {} validation error(s):\n    {}",
186                        contract.name,
187                        errors.len(),
188                        errors.iter().map(|e| e.to_string()).collect::<Vec<_>>().join("\n    ")
189                    ));
190                    return Ok(());
191                }
192            }
193            Ok(())
194        });
195        result
196    }
197
198    #[test]
199    fn round_trip_all_mir_files() {
200        let dir = ui_codegen_dir().join("mir");
201        assert!(dir.exists(), "mir test dir not found: {}", dir.display());
202
203        let mut failures: Vec<String> = Vec::new();
204        let mut count = 0usize;
205        for entry in std::fs::read_dir(&dir).unwrap() {
206            let path = entry.unwrap().path();
207            if path.extension().and_then(|s| s.to_str()) != Some("mir") {
208                continue;
209            }
210            count += 1;
211            if let Err(e) = round_trip_mir(&path) {
212                let name = path.file_name().unwrap().to_string_lossy().into_owned();
213                failures.push(format!("{name}: {e}"));
214            }
215        }
216        assert!(count > 0, "no .mir fixtures found in {}", dir.display());
217        assert!(
218            failures.is_empty(),
219            "{} round-trip failure(s):\n  {}",
220            failures.len(),
221            failures.join("\n  ")
222        );
223    }
224
225    /// Round-trips one Solidity file: lower → print → parse → print → parse →
226    /// print and asserts the last two prints match.
227    fn round_trip_sol(path: &Path) -> Result<(), String> {
228        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
229        let mut compiler = Compiler::new(sess);
230
231        let parse_result = compiler.enter_mut(|c| -> solar_interface::Result<()> {
232            let mut pcx = c.parse();
233            pcx.load_files([path])?;
234            pcx.parse();
235            Ok(())
236        });
237        if parse_result.is_err() {
238            return Err("parse failed".into());
239        }
240
241        let mut result: Result<(), String> = Ok(());
242        let _ = compiler.enter_mut(|c| -> solar_interface::Result<()> {
243            let ControlFlow::Continue(()) = c.lower_asts()? else {
244                return Ok(());
245            };
246            let ControlFlow::Continue(()) = c.analysis()? else {
247                return Ok(());
248            };
249
250            let gcx = c.gcx();
251            for id in gcx.hir.contract_ids() {
252                let contract = gcx.hir.contract(id);
253                if contract.kind.is_interface() || contract.kind.is_abstract_contract() {
254                    continue;
255                }
256                let module = lower::lower_contract(gcx, id);
257                if let Err(e) = check_round_trip_module(&module) {
258                    result = Err(format!("contract `{}`: {e}", contract.name));
259                    return Ok(());
260                }
261            }
262            Ok(())
263        });
264        result
265    }
266
267    /// Round-trips one `.mir` file. Skips the lowering step.
268    fn round_trip_mir(path: &Path) -> Result<(), String> {
269        // Tests don't need a SourceMap; reading a fixture as plain text is fine.
270        #[allow(clippy::disallowed_methods)]
271        let raw = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
272        // Strip `//@compile-flags:` annotations the test harness reads — they're
273        // not valid MIR and the parser would treat them as comments anyway, but
274        // be explicit so we don't accidentally rely on parser behavior.
275        let text: String =
276            raw.lines().filter(|l| !l.starts_with("//@")).collect::<Vec<_>>().join("\n");
277
278        let sess = Session::builder().with_buffer_emitter(ColorChoice::Never).build();
279        let mut result: Result<(), String> = Ok(());
280        sess.enter(|| {
281            let parsed1 = match parse_module(&text) {
282                Ok(m) => m,
283                Err(e) => {
284                    result = Err(format!("first parse failed: {e}"));
285                    return;
286                }
287            };
288            let print1 = parsed1.to_text().to_string();
289            let parsed2 = match parse_module(&print1) {
290                Ok(m) => m,
291                Err(e) => {
292                    result = Err(format!("second parse failed: {e}"));
293                    return;
294                }
295            };
296            let print2 = parsed2.to_text().to_string();
297            if print1 != print2 {
298                let diff = first_diff(&print1, &print2)
299                    .map(|(i, a, b)| format!("line {i}: `{a}` vs `{b}`"))
300                    .unwrap_or_else(|| "(length mismatch)".to_string());
301                result = Err(format!("not idempotent: {diff}"));
302            }
303        });
304        result
305    }
306
307    /// Common idempotency check: print → parse → print → parse → print, last two
308    /// must match. Caller must already be inside an active `Session::enter`.
309    fn check_round_trip_module(module: &Module) -> Result<(), String> {
310        let print1 = module.to_text().to_string();
311        let parsed1 = parse_module(&print1)
312            .map_err(|e| format!("first parse: {e}\n--- print1 ---\n{print1}"))?;
313        let print2 = parsed1.to_text().to_string();
314        let parsed2 = parse_module(&print2).map_err(|e| {
315            format!("second parse: {e}\n--- print1 ---\n{print1}\n--- print2 ---\n{print2}")
316        })?;
317        let print3 = parsed2.to_text().to_string();
318
319        if print2 != print3 {
320            let diff = first_diff(&print2, &print3)
321                .map(|(i, a, b)| format!("line {i}: `{a}` vs `{b}`"))
322                .unwrap_or_else(|| "(length mismatch)".to_string());
323            return Err(format!(
324                "not idempotent: {diff}\n--- print2 ---\n{print2}\n--- print3 ---\n{print3}"
325            ));
326        }
327        Ok(())
328    }
329}