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