Skip to main content

leo_disassembler/
lib.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17#[cfg(target_arch = "wasm32")]
18extern crate self as snarkvm;
19
20// Preserve this crate's existing `snarkvm::...` imports on WASM without pulling
21// in the full native-oriented `snarkvm` dependency graph.
22#[cfg(target_arch = "wasm32")]
23mod snarkvm_wasm;
24#[cfg(target_arch = "wasm32")]
25#[doc(hidden)]
26pub use snarkvm_wasm::{prelude, synthesizer};
27
28mod errors;
29
30use leo_ast::{AleoProgram, Composite, FunctionStub, Identifier, Mapping, NetworkName, ProgramId};
31use leo_errors::LeoError;
32use leo_span::Symbol;
33
34use snarkvm::{
35    prelude::{Itertools, Network},
36    synthesizer::program::{Program, ProgramCore},
37};
38
39use std::{fmt, str::FromStr};
40
41pub fn disassemble<N: Network>(program: ProgramCore<N>) -> AleoProgram {
42    let program_id = ProgramId::from(program.id());
43    AleoProgram {
44        imports: program.imports().into_iter().map(|(id, _)| ProgramId::from(id)).collect(),
45        stub_id: program_id,
46        consts: Vec::new(),
47        composites: [
48            program
49                .structs()
50                .iter()
51                .map(|(id, s)| (Identifier::from(id).name, Composite::from_snarkvm(s, program_id)))
52                .collect_vec(),
53            program
54                .records()
55                .iter()
56                .map(|(id, s)| (Identifier::from(id).name, Composite::from_external_record(s, program_id)))
57                .collect_vec(),
58        ]
59        .concat(),
60        mappings: program
61            .mappings()
62            .into_iter()
63            .map(|(id, m)| (Identifier::from(id).name, Mapping::from_snarkvm(m, program_id)))
64            .collect(),
65        functions: [
66            program
67                .closures()
68                .iter()
69                .map(|(id, closure)| (Identifier::from(id).name, FunctionStub::from_closure(closure, program_id)))
70                .collect_vec(),
71            program
72                .functions()
73                .iter()
74                .map(|(id, function)| {
75                    (Identifier::from(id).name, FunctionStub::from_function_core(function, program_id))
76                })
77                .collect_vec(),
78            program
79                .functions()
80                .iter()
81                .filter_map(|(id, function)| match function.finalize_logic() {
82                    Some(_f) => {
83                        let key_name = Symbol::intern(&format!(
84                            "finalize/{}",
85                            Symbol::intern(&Identifier::from(id).name.to_string())
86                        ));
87                        Some((key_name, FunctionStub::from_finalize(function, key_name, program_id)))
88                    }
89                    None => None,
90                })
91                .collect_vec(),
92            program
93                .views()
94                .iter()
95                .map(|(id, view)| (Identifier::from(id).name, FunctionStub::from_view(view, program_id)))
96                .collect_vec(),
97        ]
98        .concat(),
99        span: Default::default(),
100    }
101}
102
103/// Parse-only disassembly. Performs grammar-level checks via `Program::from_str`
104/// and converts to the leo AST. Prefer `disassemble_from_str` (native) if you
105/// have a `Process` and want the snarkVM semantic checks that reject the class
106/// of malformed-but-parseable bytecode that `disassemble` panics on; this
107/// `_unchecked` variant skips that step.
108pub fn disassemble_from_str_unchecked<N: Network>(
109    name: impl fmt::Display,
110    program: &str,
111) -> Result<AleoProgram, LeoError> {
112    let p = Program::<N>::from_str(program).map_err(|_| crate::errors::snarkvm_parsing_error(name))?;
113    Ok(disassemble(p))
114}
115
116/// Parse, validate via `Process::add_program`, and disassemble. Catches the class
117/// of malformed-but-parseable bytecode that `disassemble` panics on (e.g. issue
118/// #29399, where a non-finalize function declared a future-typed register input).
119/// Pattern matches `crates/compiler/src/test_compiler.rs:62-66`.
120///
121/// `process` must already have all of `program`'s declared imports loaded —
122/// snarkVM's `add_program` is contextual and rejects a program whose imports
123/// aren't yet in the process. Callers that disassemble multiple related
124/// dependencies should reuse the same process across calls in topological
125/// dependency order so each program's imports are present when it's added.
126#[cfg(not(target_arch = "wasm32"))]
127pub fn disassemble_from_str<N: Network>(
128    name: impl fmt::Display,
129    program: &str,
130    process: &mut snarkvm::prelude::Process<N>,
131) -> Result<AleoProgram, LeoError> {
132    let p = Program::<N>::from_str(program).map_err(|_| crate::errors::snarkvm_parsing_error(&name))?;
133    validate_and_disassemble(name, p, process)
134}
135
136/// Validate `program` via `process.add_program` and disassemble. Same `add_program + disassemble` tail as
137/// `disassemble_from_str`, but accepts an already-parsed `Program` so callers that need to peek at imports first
138/// (to build a topological load order, say) don't pay for a re-parse. `process` must already have all of
139/// `program`'s declared imports loaded.
140#[cfg(not(target_arch = "wasm32"))]
141pub fn validate_and_disassemble<N: Network>(
142    name: impl fmt::Display,
143    program: Program<N>,
144    process: &mut snarkvm::prelude::Process<N>,
145) -> Result<AleoProgram, LeoError> {
146    process.lock().add_program(&program).map_err(|e| crate::errors::snarkvm_validation_error(&name, e))?;
147    Ok(disassemble(program))
148}
149
150/// Disassembles Aleo bytecode using the snarkVM network selected by `network`.
151/// Native: validates via a fresh `Process` per call. Note the fresh process has
152/// no other programs loaded, so this rejects programs with imports — callers
153/// that have a typed network at compile time and need import-using programs
154/// should prefer `disassemble_from_str` with a shared process loaded in
155/// topological order. WASM: parse-only via `disassemble_from_str_unchecked`
156/// (`Process` is not in the wasm dep set yet).
157pub fn disassemble_from_str_for_network(
158    name: impl fmt::Display,
159    program: &str,
160    network: NetworkName,
161) -> Result<AleoProgram, LeoError> {
162    #[cfg(not(target_arch = "wasm32"))]
163    {
164        match network {
165            NetworkName::MainnetV0 => {
166                let mut process = snarkvm::prelude::Process::<snarkvm::prelude::MainnetV0>::load()
167                    .map_err(|e| crate::errors::snarkvm_validation_error(&name, e))?;
168                disassemble_from_str(name, program, &mut process)
169            }
170            NetworkName::TestnetV0 => {
171                let mut process = snarkvm::prelude::Process::<snarkvm::prelude::TestnetV0>::load()
172                    .map_err(|e| crate::errors::snarkvm_validation_error(&name, e))?;
173                disassemble_from_str(name, program, &mut process)
174            }
175            NetworkName::CanaryV0 => {
176                let mut process = snarkvm::prelude::Process::<snarkvm::prelude::CanaryV0>::load()
177                    .map_err(|e| crate::errors::snarkvm_validation_error(&name, e))?;
178                disassemble_from_str(name, program, &mut process)
179            }
180        }
181    }
182    #[cfg(target_arch = "wasm32")]
183    {
184        match network {
185            NetworkName::MainnetV0 => disassemble_from_str_unchecked::<snarkvm::prelude::MainnetV0>(name, program),
186            NetworkName::TestnetV0 => disassemble_from_str_unchecked::<snarkvm::prelude::TestnetV0>(name, program),
187            NetworkName::CanaryV0 => disassemble_from_str_unchecked::<snarkvm::prelude::CanaryV0>(name, program),
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use leo_span::create_session_if_not_set_then;
196    use snarkvm::synthesizer::program::Program;
197    use std::fs;
198
199    type CurrentNetwork = snarkvm::prelude::MainnetV0;
200
201    #[test]
202    #[ignore]
203    fn credits_test() {
204        create_session_if_not_set_then(|_| {
205            let program = Program::<CurrentNetwork>::credits();
206            match program {
207                Ok(p) => {
208                    let disassembled = disassemble(p);
209                    println!("{disassembled}");
210                }
211                Err(e) => {
212                    println!("{e}");
213                }
214            }
215        });
216    }
217    #[test]
218    #[ignore]
219    fn array_test() {
220        create_session_if_not_set_then(|_| {
221            let program_from_file =
222                fs::read_to_string("../tmp/.aleo/registry/mainnet/zk_bitwise_stack_v0_0_2.aleo").unwrap();
223            let _program =
224                disassemble_from_str_unchecked::<CurrentNetwork>("zk_bitwise_stack_v0_0_2", &program_from_file)
225                    .unwrap();
226        });
227    }
228
229    /// Regression for #29399: a dependency that declares a non-finalize function
230    /// with a future-typed register input must be rejected with a clean error
231    /// rather than panicking deep in `from_function_core`. Pre-fix this test
232    /// would abort the test process with `Functions do not contain futures as inputs`.
233    #[test]
234    fn rejects_future_typed_register_input_without_panic() {
235        create_session_if_not_set_then(|_| {
236            let src = include_str!("tests/victim_future_input.aleo");
237            let mut process = snarkvm::prelude::Process::<CurrentNetwork>::load().unwrap();
238            let result = disassemble_from_str::<CurrentNetwork>("victim", src, &mut process);
239            assert!(result.is_err(), "expected disassembler to reject malformed bytecode, got Ok");
240        });
241    }
242
243    /// Guards the snarkVM uniqueness check that `disassemble` relies on to flatten closures,
244    /// functions, and views into a single `(Symbol, FunctionStub)` list without collisions.
245    #[test]
246    fn snarkvm_rejects_view_name_colliding_with_function() {
247        create_session_if_not_set_then(|_| {
248            // A program that declares both `function foo:` and `view foo:` — snarkVM must reject.
249            let src = "\
250program collide.aleo;
251function foo:
252    input r0 as u32.public;
253    output r0 as u32.public;
254view foo:
255    input r0 as u32.public;
256    output r0 as u32.public;
257";
258            let result = disassemble_from_str_unchecked::<CurrentNetwork>("collide", src);
259            assert!(
260                result.is_err(),
261                "expected snarkVM to reject a program with `function foo` and `view foo` sharing a name, got Ok"
262            );
263        });
264    }
265}