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