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    block_timestamp: i64,
152    match_str: &str,
153    network: NetworkName,
154) -> Result<(Vec<TestFunction>, IndexMap<Location, Result<()>>)> {
155    let mut interpreter =
156        Interpreter::new(leo_filenames, aleo_filenames, private_key, block_height, block_timestamp, network)?;
157
158    let mut native_test_functions = Vec::new();
159
160    let private_key_symbol = Symbol::intern("private_key");
161
162    let mut result = IndexMap::new();
163
164    for (id, function) in interpreter.cursor.functions.clone().into_iter() {
165        // Only Leo functions may be tests.
166        let FunctionVariant::Leo(function) = function else {
167            continue;
168        };
169
170        let should_fail = function.annotations.iter().any(|annotation| annotation.identifier.name == sym::should_fail);
171
172        let str_matches = || id.to_string().contains(match_str);
173
174        // If this function is not annotated with @test, skip it.
175        let Some(annotation) = function.annotations.iter().find(|annotation| annotation.identifier.name == sym::test)
176        else {
177            continue;
178        };
179
180        // If the name doesn't match, skip it.
181        if !str_matches() {
182            continue;
183        }
184
185        assert!(function.input.is_empty(), "Type checking should ensure test functions have no inputs.");
186
187        if function.variant.is_transition() {
188            // It's a native test; just store it and move on.
189            let private_key = annotation.map.get(&private_key_symbol).cloned();
190            native_test_functions.push(TestFunction {
191                program: id.program.to_string(),
192                function: id.path.iter().format("::").to_string(),
193                should_fail,
194                private_key,
195            });
196            continue;
197        }
198
199        assert!(function.variant.is_script(), "Type checking should ensure test functions are transitions or scripts.");
200
201        let call = CallExpression {
202            function: function.identifier.into(),
203            const_arguments: vec![], // scripts don't have const parameters for now
204            arguments: Vec::new(),
205            program: Some(id.program),
206            span: Default::default(),
207            id: interpreter.node_builder.next_id(),
208        };
209
210        let statement: Statement = ExpressionStatement {
211            expression: call.into(),
212            span: Default::default(),
213            id: interpreter.node_builder.next_id(),
214        }
215        .into();
216
217        interpreter.cursor.frames.push(Frame {
218            step: 0,
219            element: Element::Statement(statement),
220            user_initiated: false,
221        });
222
223        let run_result = interpreter.cursor.over();
224
225        match (run_result, should_fail) {
226            (Ok(..), true) => {
227                result.insert(
228                    id,
229                    Err(InterpreterHalt::new("Test succeeded when failure was expected.".to_string()).into()),
230                );
231            }
232            (Ok(..), false) => {
233                result.insert(id, Ok(()));
234            }
235            (Err(..), true) => {
236                result.insert(id, Ok(()));
237            }
238            (Err(err), false) => {
239                result.insert(id, Err(err));
240            }
241        }
242
243        // Clear the state for the next test.
244        interpreter.cursor.clear();
245    }
246
247    Ok((native_test_functions, result))
248}
249
250/// Load all the Leo source files indicated and open the interpreter
251/// to commands from the user.
252pub fn interpret(
253    leo_filenames: &[(PathBuf, Vec<PathBuf>)], // Leo source files and their modules
254    aleo_filenames: &[PathBuf],
255    private_key: String,
256    block_height: u32,
257    block_timestamp: i64,
258    tui: bool,
259    network: NetworkName,
260) -> Result<()> {
261    let mut interpreter =
262        Interpreter::new(leo_filenames, aleo_filenames, private_key, block_height, block_timestamp, network)?;
263
264    let mut user_interface: Box<dyn Ui> =
265        if tui { Box::new(ratatui_ui::RatatuiUi::new()) } else { Box::new(dialoguer_input::DialoguerUi::new()) };
266
267    let mut futures = Vec::new();
268    let mut watchpoints = Vec::new();
269    let mut message = INTRO.to_string();
270    let mut result = String::new();
271
272    loop {
273        futures.clear();
274        watchpoints.clear();
275
276        let (code, highlight) = if let Some((code, lo, hi)) = interpreter.view_current_in_context() {
277            (code.to_string(), Some((lo, hi)))
278        } else if let Some(v) = interpreter.view_current() {
279            (v.to_string(), None)
280        } else {
281            ("".to_string(), None)
282        };
283
284        futures.extend(interpreter.cursor.futures.iter().map(|f| f.to_string()));
285
286        interpreter.update_watchpoints()?;
287
288        watchpoints.extend(interpreter.watchpoints.iter().map(|watchpoint| {
289            format!("{:>15} = {}", watchpoint.code, if let Some(s) = &watchpoint.last_result { &**s } else { "?" })
290        }));
291
292        let user_data = ui::UserData {
293            code: &code,
294            highlight,
295            message: &message,
296            futures: &futures,
297            watchpoints: &watchpoints,
298            result: if result.is_empty() { None } else { Some(&result) },
299        };
300
301        user_interface.display_user_data(&user_data);
302
303        message.clear();
304        result.clear();
305
306        let user_input = user_interface.receive_user_input();
307
308        let (command, rest) = tokenize_user_input(&user_input);
309
310        let action = match (command, rest) {
311            ("", "") => continue,
312            ("#h" | "#help", "") => {
313                message = HELP.to_string();
314                continue;
315            }
316            ("#i" | "#into", "") => InterpreterAction::Into,
317            ("#i" | "#into", rest) => InterpreterAction::LeoInterpretInto(rest.into()),
318            ("#s" | "#step", "") => InterpreterAction::Step,
319            ("#o" | "#over", "") => InterpreterAction::Over,
320            ("#r" | "#run", "") => InterpreterAction::Run,
321            ("#q" | "#quit", "") => return Ok(()),
322            ("#f" | "#future", rest) => {
323                if let Ok(num) = rest.trim().parse::<usize>() {
324                    if num >= interpreter.cursor.futures.len() {
325                        message = "No such future.".to_string();
326                        continue;
327                    }
328                    InterpreterAction::RunFuture(num)
329                } else {
330                    message = "Failed to parse future.".to_string();
331                    continue;
332                }
333            }
334            ("#restore", "") => {
335                if !interpreter.restore_cursor() {
336                    message = "No saved state to restore".to_string();
337                }
338                continue;
339            }
340            ("#b" | "#break", rest) => {
341                let Some(breakpoint) = parse_breakpoint(rest) else {
342                    message = "Failed to parse breakpoint".to_string();
343                    continue;
344                };
345                InterpreterAction::Breakpoint(breakpoint)
346            }
347            ("#p" | "#print", rest) => {
348                let without_r = rest.strip_prefix("r").unwrap_or(rest);
349                if let Ok(num) = without_r.parse::<u64>() {
350                    InterpreterAction::PrintRegister(num)
351                } else {
352                    message = "Failed to parse register number".to_string();
353                    continue;
354                }
355            }
356            ("#w" | "#watch", rest) => InterpreterAction::Watch(rest.to_string()),
357            ("#set_program", rest) => {
358                interpreter.cursor.set_program(rest);
359                continue;
360            }
361            ("", rest) => InterpreterAction::LeoInterpretOver(rest.to_string()),
362            _ => {
363                message = "Failed to parse command".to_string();
364                continue;
365            }
366        };
367
368        if matches!(action, InterpreterAction::LeoInterpretInto(..) | InterpreterAction::LeoInterpretOver(..)) {
369            interpreter.save_cursor();
370        }
371
372        match interpreter.action(action) {
373            Ok(Some(value)) => {
374                result = value.to_string();
375            }
376            Ok(None) => {}
377            Err(LeoError::InterpreterHalt(interpreter_halt)) => {
378                message = format!("Halted: {interpreter_halt}");
379            }
380            Err(e) => return Err(e),
381        }
382    }
383}
384
385fn tokenize_user_input(input: &str) -> (&str, &str) {
386    let input = input.trim();
387
388    if !input.starts_with("#") {
389        return ("", input);
390    }
391
392    let Some((first, rest)) = input.split_once(' ') else {
393        return (input, "");
394    };
395
396    (first.trim(), rest.trim())
397}