leo_interpreter/
lib.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_ast::{
18    Ast,
19    CallExpression,
20    ExpressionStatement,
21    NetworkName,
22    Node as _,
23    NodeBuilder,
24    Path,
25    Statement,
26    interpreter_value::{GlobalId, Value},
27};
28use leo_errors::{InterpreterHalt, LeoError, Result};
29use leo_span::{Span, Symbol, source_map::FileName, sym, with_session_globals};
30
31use snarkvm::prelude::{Program, TestnetV0};
32
33use indexmap::IndexMap;
34use itertools::Itertools;
35use std::{
36    collections::HashMap,
37    fmt::{Display, Write as _},
38    fs,
39    path::PathBuf,
40};
41
42#[cfg(test)]
43mod test;
44
45#[cfg(test)]
46mod test_interpreter;
47
48mod util;
49use util::*;
50
51mod cursor;
52use cursor::*;
53
54mod interpreter;
55use interpreter::*;
56
57mod cursor_aleo;
58
59mod ui;
60use ui::Ui;
61
62mod dialoguer_input;
63
64mod ratatui_ui;
65
66const INTRO: &str = "This is the Leo Interpreter. Try the command `#help`.";
67
68const HELP: &str = "
69You probably want to start by running a function or transition.
70For instance
71#into program.aleo/main()
72Once a function is running, commands include
73#into    to evaluate into the next expression or statement;
74#step    to take one step towards evaluating the current expression or statement;
75#over    to complete evaluating the current expression or statement;
76#run     to finish evaluating
77#quit    to quit the interpreter.
78
79You can set a breakpoint with
80#break program_name line_number
81
82When executing Aleo VM code, you can print the value of a register like this:
83#print 2
84
85Some of the commands may be run with one letter abbreviations, such as #i.
86
87Note that this interpreter is not line oriented as in many common debuggers;
88rather it is oriented around expressions and statements.
89As you step into code, individual expressions or statements will
90be evaluated one by one, including arguments of function calls.
91
92You may simply enter Leo expressions or statements on the command line
93to evaluate. For instance, if you want to see the value of a variable w:
94w
95If you want to set w to a new value:
96w = z + 2u8;
97
98Note that statements (like the assignment above) must end with a semicolon.
99
100If there are futures available to be executed, they will be listed by
101numerical index, and you may run them using `#future` (or `#f`); for instance
102#future 0
103
104The interpreter begins in a global context, not in any Leo program. You can set
105the current program with
106
107#set_program program_name
108
109This allows you to refer to structs and other items in the indicated program.
110
111The interpreter may enter an invalid state, often due to Leo code entered at the
112REPL. In this case, you may use the command
113
114#restore
115
116Which will restore to the last saved state of the interpreter. Any time you
117enter Leo code at the prompt, interpreter state is saved.
118
119Input history is available - use the up and down arrow keys.
120";
121
122fn parse_breakpoint(s: &str) -> Option<Breakpoint> {
123    let strings: Vec<&str> = s.split_whitespace().collect();
124    if strings.len() == 2 {
125        if let Ok(line) = strings[1].parse::<usize>() {
126            let program = strings[0].strip_suffix(".aleo").unwrap_or(strings[0]).to_string();
127            return Some(Breakpoint { program, line });
128        }
129    }
130    None
131}
132
133pub struct TestFunction {
134    pub program: String,
135    pub function: String,
136    pub should_fail: bool,
137    pub private_key: Option<String>,
138}
139
140/// Run interpreter tests and return data about native tests.
141// It's slightly goofy to have this function responsible for both of these tasks, but
142// it's expedient as the `Interpreter` will already parse all the files and collect
143// all the functions with annotations.
144#[allow(clippy::type_complexity)]
145pub fn find_and_run_tests(
146    leo_filenames: &[(PathBuf, Vec<PathBuf>)], // Leo source files and their modules
147    aleo_filenames: &[PathBuf],
148    signer: Value,
149    block_height: u32,
150    match_str: &str,
151    network: NetworkName,
152) -> Result<(Vec<TestFunction>, IndexMap<GlobalId, Result<()>>)> {
153    let mut interpreter = Interpreter::new(leo_filenames, aleo_filenames, signer, block_height, network)?;
154
155    let mut native_test_functions = Vec::new();
156
157    let private_key_symbol = Symbol::intern("private_key");
158
159    let mut result = IndexMap::new();
160
161    for (id, function) in interpreter.cursor.functions.clone().into_iter() {
162        // Only Leo functions may be tests.
163        let FunctionVariant::Leo(function) = function else {
164            continue;
165        };
166
167        let should_fail = function.annotations.iter().any(|annotation| annotation.identifier.name == sym::should_fail);
168
169        let str_matches = || id.to_string().contains(match_str);
170
171        // If this function is not annotated with @test, skip it.
172        let Some(annotation) = function.annotations.iter().find(|annotation| annotation.identifier.name == sym::test)
173        else {
174            continue;
175        };
176
177        // If the name doesn't match, skip it.
178        if !str_matches() {
179            continue;
180        }
181
182        assert!(function.input.is_empty(), "Type checking should ensure test functions have no inputs.");
183
184        if function.variant.is_transition() {
185            // It's a native test; just store it and move on.
186            let private_key = annotation.map.get(&private_key_symbol).cloned();
187            native_test_functions.push(TestFunction {
188                program: id.program.to_string(),
189                function: id.path.iter().format("::").to_string(),
190                should_fail,
191                private_key,
192            });
193            continue;
194        }
195
196        assert!(function.variant.is_script(), "Type checking should ensure test functions are transitions or scripts.");
197
198        let call = CallExpression {
199            function: function.identifier.into(),
200            const_arguments: vec![], // scripts don't have const parameters for now
201            arguments: Vec::new(),
202            program: Some(id.program),
203            span: Default::default(),
204            id: interpreter.node_builder.next_id(),
205        };
206
207        let statement: Statement = ExpressionStatement {
208            expression: call.into(),
209            span: Default::default(),
210            id: interpreter.node_builder.next_id(),
211        }
212        .into();
213
214        interpreter.cursor.frames.push(Frame {
215            step: 0,
216            element: Element::Statement(statement),
217            user_initiated: false,
218        });
219
220        let run_result = interpreter.cursor.over();
221
222        match (run_result, should_fail) {
223            (Ok(..), true) => {
224                result.insert(
225                    id,
226                    Err(InterpreterHalt::new("Test succeeded when failure was expected.".to_string()).into()),
227                );
228            }
229            (Ok(..), false) => {
230                result.insert(id, Ok(()));
231            }
232            (Err(..), true) => {
233                result.insert(id, Ok(()));
234            }
235            (Err(err), false) => {
236                result.insert(id, Err(err));
237            }
238        }
239    }
240
241    Ok((native_test_functions, result))
242}
243
244/// Load all the Leo source files indicated and open the interpreter
245/// to commands from the user.
246pub fn interpret(
247    leo_filenames: &[(PathBuf, Vec<PathBuf>)], // Leo source files and their modules
248    aleo_filenames: &[PathBuf],
249    signer: Value,
250    block_height: u32,
251    tui: bool,
252    network: NetworkName,
253) -> Result<()> {
254    let mut interpreter = Interpreter::new(leo_filenames, aleo_filenames, signer, block_height, network)?;
255
256    let mut user_interface: Box<dyn Ui> =
257        if tui { Box::new(ratatui_ui::RatatuiUi::new()) } else { Box::new(dialoguer_input::DialoguerUi::new()) };
258
259    let mut code = String::new();
260    let mut futures = Vec::new();
261    let mut watchpoints = Vec::new();
262    let mut message = INTRO.to_string();
263    let mut result = String::new();
264
265    loop {
266        code.clear();
267        futures.clear();
268        watchpoints.clear();
269
270        let (code, highlight) = if let Some((code, lo, hi)) = interpreter.view_current_in_context() {
271            (code.to_string(), Some((lo, hi)))
272        } else if let Some(v) = interpreter.view_current() {
273            (v.to_string(), None)
274        } else {
275            ("".to_string(), None)
276        };
277
278        futures.extend(interpreter.cursor.futures.iter().map(|f| f.to_string()));
279
280        interpreter.update_watchpoints()?;
281
282        watchpoints.extend(interpreter.watchpoints.iter().map(|watchpoint| {
283            format!("{:>15} = {}", watchpoint.code, if let Some(s) = &watchpoint.last_result { &**s } else { "?" })
284        }));
285
286        let user_data = ui::UserData {
287            code: &code,
288            highlight,
289            message: &message,
290            futures: &futures,
291            watchpoints: &watchpoints,
292            result: if result.is_empty() { None } else { Some(&result) },
293        };
294
295        user_interface.display_user_data(&user_data);
296
297        message.clear();
298        result.clear();
299
300        let user_input = user_interface.receive_user_input();
301
302        let (command, rest) = tokenize_user_input(&user_input);
303
304        let action = match (command, rest) {
305            ("", "") => continue,
306            ("#h" | "#help", "") => {
307                message = HELP.to_string();
308                continue;
309            }
310            ("#i" | "#into", "") => InterpreterAction::Into,
311            ("#i" | "#into", rest) => InterpreterAction::LeoInterpretInto(rest.into()),
312            ("#s" | "#step", "") => InterpreterAction::Step,
313            ("#o" | "#over", "") => InterpreterAction::Over,
314            ("#r" | "#run", "") => InterpreterAction::Run,
315            ("#q" | "#quit", "") => return Ok(()),
316            ("#f" | "#future", rest) => {
317                if let Ok(num) = rest.trim().parse::<usize>() {
318                    if num >= interpreter.cursor.futures.len() {
319                        message = "No such future.".to_string();
320                        continue;
321                    }
322                    InterpreterAction::RunFuture(num)
323                } else {
324                    message = "Failed to parse future.".to_string();
325                    continue;
326                }
327            }
328            ("#restore", "") => {
329                if !interpreter.restore_cursor() {
330                    message = "No saved state to restore".to_string();
331                }
332                continue;
333            }
334            ("#b" | "#break", rest) => {
335                let Some(breakpoint) = parse_breakpoint(rest) else {
336                    message = "Failed to parse breakpoint".to_string();
337                    continue;
338                };
339                InterpreterAction::Breakpoint(breakpoint)
340            }
341            ("#p" | "#print", rest) => {
342                let without_r = rest.strip_prefix("r").unwrap_or(rest);
343                if let Ok(num) = without_r.parse::<u64>() {
344                    InterpreterAction::PrintRegister(num)
345                } else {
346                    message = "Failed to parse register number".to_string();
347                    continue;
348                }
349            }
350            ("#w" | "#watch", rest) => InterpreterAction::Watch(rest.to_string()),
351            ("#set_program", rest) => {
352                interpreter.cursor.set_program(rest);
353                continue;
354            }
355            ("", rest) => InterpreterAction::LeoInterpretOver(rest.to_string()),
356            _ => {
357                message = "Failed to parse command".to_string();
358                continue;
359            }
360        };
361
362        if matches!(action, InterpreterAction::LeoInterpretInto(..) | InterpreterAction::LeoInterpretOver(..)) {
363            interpreter.save_cursor();
364        }
365
366        match interpreter.action(action) {
367            Ok(Some(value)) => {
368                result = value.to_string();
369            }
370            Ok(None) => {}
371            Err(LeoError::InterpreterHalt(interpreter_halt)) => {
372                message = format!("Halted: {interpreter_halt}");
373            }
374            Err(e) => return Err(e),
375        }
376    }
377}
378
379fn tokenize_user_input(input: &str) -> (&str, &str) {
380    let input = input.trim();
381
382    if !input.starts_with("#") {
383        return ("", input);
384    }
385
386    let Some((first, rest)) = input.split_once(' ') else {
387        return (input, "");
388    };
389
390    (first.trim(), rest.trim())
391}