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