leo_lang/cli/commands/common/
util.rs

1// Copyright (C) 2019-2025 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
17use leo_errors::CliError;
18use leo_package::{Package, ProgramData};
19use leo_span::Symbol;
20
21use indexmap::IndexSet;
22use snarkvm::prelude::{ConsensusVersion, Network, Program};
23use std::path::PathBuf;
24use walkdir::WalkDir;
25
26/// Threshold percentage for program size warnings.
27const PROGRAM_SIZE_WARNING_THRESHOLD: usize = 90;
28
29/// Formats program size as KB and returns a warning message if approaching the limit.
30///
31/// Both `size` and `max_size` are expected in bytes.
32/// Returns `(size_kb, max_kb, warning)` where `warning` is `Some` if size exceeds 90% of max.
33pub fn format_program_size(size: usize, max_size: usize) -> (f64, f64, Option<String>) {
34    let size_kb = size as f64 / 1024.0;
35    let max_kb = max_size as f64 / 1024.0;
36    let percentage = (size as f64 / max_size as f64) * 100.0;
37
38    let warning = if size > max_size * PROGRAM_SIZE_WARNING_THRESHOLD / 100 {
39        Some(format!("approaching the size limit ({percentage:.1}% of {max_kb:.2} KB)"))
40    } else {
41        None
42    };
43
44    (size_kb, max_kb, warning)
45}
46
47/// Collects paths to Leo source files for each program in the package.
48///
49/// For each non-test program, it searches the `src` directory for `.leo` files.
50/// It separates the `main.leo` file from the rest and returns a tuple:
51/// (`main.leo` path, list of other `.leo` file paths).
52/// Test programs are included with an empty list of additional files.
53/// Programs with bytecode data are ignored.
54///
55/// # Arguments
56/// * `package` - Reference to the package containing programs.
57///
58/// # Returns
59/// A vector of tuples with the main file and other source files.
60pub fn collect_leo_paths(package: &Package) -> Vec<(PathBuf, Vec<PathBuf>)> {
61    let mut partitioned_leo_paths = Vec::new();
62    for program in &package.programs {
63        match &program.data {
64            ProgramData::SourcePath { directory, source } => {
65                if program.is_test {
66                    partitioned_leo_paths.push((source.clone(), vec![]));
67                } else {
68                    let src_dir = directory.join("src");
69                    if !src_dir.exists() {
70                        continue;
71                    }
72
73                    let mut all_files: Vec<PathBuf> = WalkDir::new(&src_dir)
74                        .into_iter()
75                        .filter_map(Result::ok)
76                        .filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("leo"))
77                        .map(|entry| entry.into_path())
78                        .collect();
79
80                    if let Some(index) =
81                        all_files.iter().position(|p| p.file_name().and_then(|s| s.to_str()) == Some("main.leo"))
82                    {
83                        let main = all_files.remove(index);
84                        partitioned_leo_paths.push((main, all_files));
85                    }
86                }
87            }
88            ProgramData::Bytecode(..) => {}
89        }
90    }
91    partitioned_leo_paths
92}
93
94/// Collects paths to `.aleo` files that are external (non-local) dependencies.
95///
96/// Scans the package's `imports` directory and filters out files that match
97/// the names of local source-based dependencies.
98/// Only retains `.aleo` files corresponding to true external dependencies.
99///
100/// # Arguments
101/// * `package` - Reference to the package whose imports are being examined.
102///
103/// # Returns
104/// A vector of paths to `.aleo` files not associated with local source dependencies.
105pub fn collect_aleo_paths(package: &Package) -> Vec<PathBuf> {
106    let local_dependency_symbols: IndexSet<Symbol> = package
107        .programs
108        .iter()
109        .flat_map(|program| match &program.data {
110            ProgramData::SourcePath { .. } => {
111                // It's a local Leo dependency.
112                Some(program.name)
113            }
114            ProgramData::Bytecode(..) => {
115                // It's a network dependency or local .aleo dependency.
116                None
117            }
118        })
119        .collect();
120
121    package
122        .imports_directory()
123        .read_dir()
124        .ok()
125        .into_iter()
126        .flatten()
127        .flat_map(|maybe_filename| maybe_filename.ok())
128        .filter(|entry| entry.file_type().ok().map(|filetype| filetype.is_file()).unwrap_or(false))
129        .flat_map(|entry| {
130            let path = entry.path();
131            if let Some(filename) = leo_package::filename_no_aleo_extension(&path) {
132                let symbol = Symbol::intern(filename);
133                if local_dependency_symbols.contains(&symbol) { None } else { Some(path) }
134            } else {
135                None
136            }
137        })
138        .collect()
139}
140
141/// Default edition for local programs during local operations (run, execute, synthesize).
142///
143/// Local programs don't have an on-chain edition yet. We default to edition 1 to avoid
144/// snarkVM's V8+ check that rejects edition 0 programs without constructors. That check
145/// is only relevant for deployed programs, not local development.
146pub const LOCAL_PROGRAM_DEFAULT_EDITION: leo_package::Edition = 1;
147
148/// Prints a program's ID and source (local or network edition).
149pub fn print_program_source(id: &str, edition: Option<leo_package::Edition>) {
150    match (id, edition) {
151        ("credits.aleo", _) => println!("  - {id} (already included)"),
152        (_, Some(e)) => println!("  - {id} (edition: {e})"),
153        (_, None) => println!("  - {id} (local)"),
154    }
155}
156
157/// Checks if any programs violate edition/constructor requirements.
158///
159/// Programs at edition 0 without a constructor cannot be executed after ConsensusVersion::V8.
160/// This check should be performed before attempting execution to provide a clear error message.
161///
162/// # Arguments
163/// * `programs` - Slice of (program, edition) tuples to check
164/// * `consensus_version` - The current consensus version
165/// * `action` - Description of the action being attempted (e.g., "deploy", "execute", "upgrade")
166///
167/// # Returns
168/// `Ok(())` if all programs pass the check, or an error with a descriptive message if not.
169pub fn check_edition_constructor_requirements<N: Network>(
170    programs: &[(Program<N>, leo_package::Edition)],
171    consensus_version: ConsensusVersion,
172    action: &str,
173) -> Result<(), CliError> {
174    // Only check for V8+ consensus versions.
175    if consensus_version < ConsensusVersion::V8 {
176        return Ok(());
177    }
178
179    for (program, edition) in programs {
180        // Programs at edition 0 without a constructor cannot be executed after V8.
181        if *edition == 0 && !program.contains_constructor() {
182            let id = program.id();
183            // Skip credits.aleo as it's a special case.
184            if id.to_string() != "credits.aleo" {
185                return Err(CliError::custom(format!(
186                    "Cannot {action} with dependency '{id}' (edition 0)\n\n\
187                    Programs at edition 0 without a constructor cannot be executed under \
188                    consensus version V8 or later (current: V{}).\n\n\
189                    The program '{id}' must be upgraded on-chain before it can be used.",
190                    consensus_version as u8
191                )));
192            }
193        }
194    }
195
196    Ok(())
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use snarkvm::prelude::TestnetV0;
203    use std::str::FromStr;
204
205    #[test]
206    fn test_edition_constructor_error_message() {
207        // A program without a constructor at edition 0 should fail under V8+
208        let program = Program::<TestnetV0>::from_str(
209            "program old_program.aleo;\nfunction main:\n    input r0 as u32.public;\n    output r0 as u32.public;\n",
210        )
211        .unwrap();
212
213        let result = check_edition_constructor_requirements(&[(program, 0)], ConsensusVersion::V9, "deploy");
214        assert!(result.is_err());
215    }
216}