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        if let Ok(line) = strings[1].parse::<usize>() {
127            let program = strings[0].strip_suffix(".aleo").unwrap_or(strings[0]).to_string();
128            return Some(Breakpoint { program, line });
129        }
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    signer: Value,
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, signer, 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
242    Ok((native_test_functions, result))
243}
244
245/// Load all the Leo source files indicated and open the interpreter
246/// to commands from the user.
247pub fn interpret(
248    leo_filenames: &[(PathBuf, Vec<PathBuf>)], // Leo source files and their modules
249    aleo_filenames: &[PathBuf],
250    signer: Value,
251    block_height: u32,
252    tui: bool,
253    network: NetworkName,
254) -> Result<()> {
255    let mut interpreter = Interpreter::new(leo_filenames, aleo_filenames, signer, block_height, network)?;
256
257    let mut user_interface: Box<dyn Ui> =
258        if tui { Box::new(ratatui_ui::RatatuiUi::new()) } else { Box::new(dialoguer_input::DialoguerUi::new()) };
259
260    let mut code = String::new();
261    let mut futures = Vec::new();
262    let mut watchpoints = Vec::new();
263    let mut message = INTRO.to_string();
264    let mut result = String::new();
265
266    loop {
267        code.clear();
268        futures.clear();
269        watchpoints.clear();
270
271        let (code, highlight) = if let Some((code, lo, hi)) = interpreter.view_current_in_context() {
272            (code.to_string(), Some((lo, hi)))
273        } else if let Some(v) = interpreter.view_current() {
274            (v.to_string(), None)
275        } else {
276            ("".to_string(), None)
277        };
278
279        futures.extend(interpreter.cursor.futures.iter().map(|f| f.to_string()));
280
281        interpreter.update_watchpoints()?;
282
283        watchpoints.extend(interpreter.watchpoints.iter().map(|watchpoint| {
284            format!("{:>15} = {}", watchpoint.code, if let Some(s) = &watchpoint.last_result { &**s } else { "?" })
285        }));
286
287        let user_data = ui::UserData {
288            code: &code,
289            highlight,
290            message: &message,
291            futures: &futures,
292            watchpoints: &watchpoints,
293            result: if result.is_empty() { None } else { Some(&result) },
294        };
295
296        user_interface.display_user_data(&user_data);
297
298        message.clear();
299        result.clear();
300
301        let user_input = user_interface.receive_user_input();
302
303        let (command, rest) = tokenize_user_input(&user_input);
304
305        let action = match (command, rest) {
306            ("", "") => continue,
307            ("#h" | "#help", "") => {
308                message = HELP.to_string();
309                continue;
310            }
311            ("#i" | "#into", "") => InterpreterAction::Into,
312            ("#i" | "#into", rest) => InterpreterAction::LeoInterpretInto(rest.into()),
313            ("#s" | "#step", "") => InterpreterAction::Step,
314            ("#o" | "#over", "") => InterpreterAction::Over,
315            ("#r" | "#run", "") => InterpreterAction::Run,
316            ("#q" | "#quit", "") => return Ok(()),
317            ("#f" | "#future", rest) => {
318                if let Ok(num) = rest.trim().parse::<usize>() {
319                    if num >= interpreter.cursor.futures.len() {
320                        message = "No such future.".to_string();
321                        continue;
322                    }
323                    InterpreterAction::RunFuture(num)
324                } else {
325                    message = "Failed to parse future.".to_string();
326                    continue;
327                }
328            }
329            ("#restore", "") => {
330                if !interpreter.restore_cursor() {
331                    message = "No saved state to restore".to_string();
332                }
333                continue;
334            }
335            ("#b" | "#break", rest) => {
336                let Some(breakpoint) = parse_breakpoint(rest) else {
337                    message = "Failed to parse breakpoint".to_string();
338                    continue;
339                };
340                InterpreterAction::Breakpoint(breakpoint)
341            }
342            ("#p" | "#print", rest) => {
343                let without_r = rest.strip_prefix("r").unwrap_or(rest);
344                if let Ok(num) = without_r.parse::<u64>() {
345                    InterpreterAction::PrintRegister(num)
346                } else {
347                    message = "Failed to parse register number".to_string();
348                    continue;
349                }
350            }
351            ("#w" | "#watch", rest) => InterpreterAction::Watch(rest.to_string()),
352            ("#set_program", rest) => {
353                interpreter.cursor.set_program(rest);
354                continue;
355            }
356            ("", rest) => InterpreterAction::LeoInterpretOver(rest.to_string()),
357            _ => {
358                message = "Failed to parse command".to_string();
359                continue;
360            }
361        };
362
363        if matches!(action, InterpreterAction::LeoInterpretInto(..) | InterpreterAction::LeoInterpretOver(..)) {
364            interpreter.save_cursor();
365        }
366
367        match interpreter.action(action) {
368            Ok(Some(value)) => {
369                result = value.to_string();
370            }
371            Ok(None) => {}
372            Err(LeoError::InterpreterHalt(interpreter_halt)) => {
373                message = format!("Halted: {interpreter_halt}");
374            }
375            Err(e) => return Err(e),
376        }
377    }
378}
379
380fn tokenize_user_input(input: &str) -> (&str, &str) {
381    let input = input.trim();
382
383    if !input.starts_with("#") {
384        return ("", input);
385    }
386
387    let Some((first, rest)) = input.split_once(' ') else {
388        return (input, "");
389    };
390
391    (first.trim(), rest.trim())
392}