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