leo_interpreter/
interpreter.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 super::*;
18use leo_ast::interpreter_value::{GlobalId, Value};
19use leo_errors::{CompilerError, Handler, InterpreterHalt, LeoError, Result};
20use leo_passes::{CompilerState, Pass, SymbolTableCreation, TypeChecking, TypeCheckingInput};
21use snarkvm::prelude::Network;
22
23/// Contains the state of interpretation, in the form of the `Cursor`,
24/// as well as information needed to interact with the user, like
25/// the breakpoints.
26pub struct Interpreter {
27    pub cursor: Cursor,
28    actions: Vec<InterpreterAction>,
29    handler: Handler,
30    pub node_builder: NodeBuilder,
31    breakpoints: Vec<Breakpoint>,
32    pub watchpoints: Vec<Watchpoint>,
33    saved_cursors: Vec<Cursor>,
34    filename_to_program: HashMap<PathBuf, String>,
35    parsed_inputs: u32,
36}
37
38#[derive(Clone, Debug)]
39pub struct Breakpoint {
40    pub program: String,
41    pub line: usize,
42}
43
44#[derive(Clone, Debug)]
45pub struct Watchpoint {
46    pub code: String,
47    pub last_result: Option<String>,
48}
49
50#[derive(Clone, Debug)]
51pub enum InterpreterAction {
52    LeoInterpretInto(String),
53    LeoInterpretOver(String),
54    Watch(String),
55    RunFuture(usize),
56    Breakpoint(Breakpoint),
57    PrintRegister(u64),
58    Into,
59    Over,
60    Step,
61    Run,
62}
63
64impl Interpreter {
65    pub fn new<'a, P: 'a + AsRef<Path>, Q: 'a + AsRef<Path>>(
66        leo_source_files: impl IntoIterator<Item = &'a P>,
67        aleo_source_files: impl IntoIterator<Item = &'a Q>,
68        signer: SvmAddress,
69        block_height: u32,
70        test_flow: bool, // impacts the type checker
71    ) -> Result<Self> {
72        Self::new_impl(
73            &mut leo_source_files.into_iter().map(|p| p.as_ref()),
74            &mut aleo_source_files.into_iter().map(|p| p.as_ref()),
75            signer,
76            block_height,
77            test_flow,
78        )
79    }
80
81    fn get_ast(path: &Path, state: &mut CompilerState) -> Result<Ast> {
82        let text = fs::read_to_string(path).map_err(|e| CompilerError::file_read_error(path, e))?;
83        let filename = FileName::Real(path.to_path_buf());
84        let source_file = with_session_globals(|s| s.source_map.new_source(&text, filename));
85
86        // Parse
87        state.ast = leo_parser::parse_ast::<TestnetV0>(
88            state.handler.clone(),
89            &state.node_builder,
90            &text,
91            source_file.absolute_start,
92        )?;
93
94        // Only run these two passes from the compiler to make sure that type inference runs and the `type_table` is
95        // filled out. Other compiler passes are not necessary at this stage.
96        SymbolTableCreation::do_pass((), state)?;
97        TypeChecking::do_pass(
98            TypeCheckingInput {
99                max_array_elements: TestnetV0::MAX_ARRAY_ELEMENTS,
100                max_mappings: TestnetV0::MAX_MAPPINGS,
101                max_functions: TestnetV0::MAX_FUNCTIONS,
102            },
103            state,
104        )?;
105
106        Ok(state.ast.clone())
107    }
108
109    fn new_impl(
110        leo_source_files: &mut dyn Iterator<Item = &Path>,
111        aleo_source_files: &mut dyn Iterator<Item = &Path>,
112        signer: SvmAddress,
113        block_height: u32,
114        test_flow: bool,
115    ) -> Result<Self> {
116        let mut cursor: Cursor = Cursor::new(
117            true, // really_async
118            signer,
119            block_height,
120        );
121        let mut filename_to_program = HashMap::new();
122
123        let mut state = CompilerState { is_test: test_flow, ..Default::default() };
124
125        for path in leo_source_files {
126            let ast = Self::get_ast(path, &mut state)?;
127            for (&program, scope) in ast.ast.program_scopes.iter() {
128                filename_to_program.insert(path.to_path_buf(), program.to_string());
129                for (name, function) in scope.functions.iter() {
130                    cursor.functions.insert(GlobalId { program, name: *name }, FunctionVariant::Leo(function.clone()));
131                }
132
133                for (name, composite) in scope.structs.iter() {
134                    cursor.structs.insert(
135                        GlobalId { program, name: *name },
136                        composite.members.iter().map(|member| member.identifier.name).collect(),
137                    );
138                }
139
140                for (name, _mapping) in scope.mappings.iter() {
141                    cursor.mappings.insert(GlobalId { program, name: *name }, HashMap::new());
142                }
143
144                for (name, const_declaration) in scope.consts.iter() {
145                    cursor.frames.push(Frame {
146                        step: 0,
147                        element: Element::Expression(const_declaration.value.clone()),
148                        user_initiated: false,
149                    });
150                    cursor.over()?;
151                    let value = cursor.values.pop().unwrap();
152                    cursor.globals.insert(GlobalId { program, name: *name }, value);
153                }
154            }
155        }
156
157        for path in aleo_source_files {
158            let aleo_program = Self::get_aleo_program(path)?;
159            let program = snarkvm_identifier_to_symbol(aleo_program.id().name());
160            filename_to_program.insert(path.to_path_buf(), program.to_string());
161
162            for (name, struct_type) in aleo_program.structs().iter() {
163                cursor.structs.insert(
164                    GlobalId { program, name: snarkvm_identifier_to_symbol(name) },
165                    struct_type.members().keys().map(snarkvm_identifier_to_symbol).collect(),
166                );
167            }
168
169            for (name, record_type) in aleo_program.records().iter() {
170                cursor.structs.insert(
171                    GlobalId { program, name: snarkvm_identifier_to_symbol(name) },
172                    record_type.entries().keys().map(snarkvm_identifier_to_symbol).collect(),
173                );
174            }
175
176            for (name, _mapping) in aleo_program.mappings().iter() {
177                cursor.mappings.insert(GlobalId { program, name: snarkvm_identifier_to_symbol(name) }, HashMap::new());
178            }
179
180            for (name, function) in aleo_program.functions().iter() {
181                cursor.functions.insert(
182                    GlobalId { program, name: snarkvm_identifier_to_symbol(name) },
183                    FunctionVariant::AleoFunction(function.clone()),
184                );
185            }
186
187            for (name, closure) in aleo_program.closures().iter() {
188                cursor.functions.insert(
189                    GlobalId { program, name: snarkvm_identifier_to_symbol(name) },
190                    FunctionVariant::AleoClosure(closure.clone()),
191                );
192            }
193        }
194
195        // Move the type table from `state` to `curser`. It will be used when trying to evaluate expressions that don't
196        // inherently have a type such as unsuffixed literals.
197        cursor.type_table = state.type_table;
198
199        Ok(Interpreter {
200            cursor,
201            handler: state.handler.clone(),
202            node_builder: state.node_builder.clone(),
203            actions: Vec::new(),
204            breakpoints: Vec::new(),
205            watchpoints: Vec::new(),
206            saved_cursors: Vec::new(),
207            filename_to_program,
208            parsed_inputs: 0,
209        })
210    }
211
212    pub fn save_cursor(&mut self) {
213        self.saved_cursors.push(self.cursor.clone());
214    }
215
216    /// Returns false if there was no saved cursor to restore.
217    pub fn restore_cursor(&mut self) -> bool {
218        if let Some(old_cursor) = self.saved_cursors.pop() {
219            self.cursor = old_cursor;
220            true
221        } else {
222            false
223        }
224    }
225
226    fn get_aleo_program(path: &Path) -> Result<Program<TestnetV0>> {
227        let text = fs::read_to_string(path).map_err(|e| CompilerError::file_read_error(path, e))?;
228        let program = text.parse()?;
229        Ok(program)
230    }
231
232    /// Returns true if any watchpoints changed.
233    pub fn update_watchpoints(&mut self) -> Result<bool> {
234        let mut changed = false;
235        let safe_cursor = self.cursor.clone();
236
237        for i in 0..self.watchpoints.len() {
238            let code = self.watchpoints[i].code.clone();
239            let new_value = match self.action(InterpreterAction::LeoInterpretOver(code)) {
240                Ok(None) => None,
241                Ok(Some(ret)) => Some(ret.to_string()),
242                Err(LeoError::InterpreterHalt(halt)) => {
243                    self.cursor = safe_cursor.clone();
244                    Some(halt.to_string())
245                }
246                Err(e) => return Err(e),
247            };
248            if self.watchpoints[i].last_result != new_value {
249                changed = true;
250                self.watchpoints[i].last_result = new_value;
251            }
252        }
253        Ok(changed)
254    }
255
256    pub fn action(&mut self, act: InterpreterAction) -> Result<Option<Value>> {
257        use InterpreterAction::*;
258
259        let ret = match &act {
260            RunFuture(n) => {
261                let future = self.cursor.futures.remove(*n);
262                for async_exec in future.0.into_iter().rev() {
263                    self.cursor.values.extend(async_exec.arguments);
264                    self.cursor.frames.push(Frame {
265                        step: 0,
266                        element: Element::DelayedCall(async_exec.function),
267                        user_initiated: true,
268                    });
269                }
270                self.cursor.step()?
271            }
272            LeoInterpretInto(s) | LeoInterpretOver(s) => {
273                let filename = FileName::Custom(format!("user_input{:04}", self.parsed_inputs));
274                self.parsed_inputs += 1;
275                let source_file = with_session_globals(|globals| globals.source_map.new_source(s, filename));
276                let s = s.trim();
277                if s.ends_with(';') {
278                    let statement = leo_parser::parse_statement::<TestnetV0>(
279                        self.handler.clone(),
280                        &self.node_builder,
281                        s,
282                        source_file.absolute_start,
283                    )
284                    .map_err(|_e| {
285                        LeoError::InterpreterHalt(InterpreterHalt::new("failed to parse statement".into()))
286                    })?;
287                    // The spans of the code the user wrote at the REPL are meaningless, so get rid of them.
288                    self.cursor.frames.push(Frame {
289                        step: 0,
290                        element: Element::Statement(statement),
291                        user_initiated: true,
292                    });
293                } else {
294                    let expression = leo_parser::parse_expression::<TestnetV0>(
295                        self.handler.clone(),
296                        &self.node_builder,
297                        s,
298                        source_file.absolute_start,
299                    )
300                    .map_err(|e| {
301                        LeoError::InterpreterHalt(InterpreterHalt::new(format!("Failed to parse expression: {e}")))
302                    })?;
303                    // The spans of the code the user wrote at the REPL are meaningless, so get rid of them.
304                    self.cursor.frames.push(Frame {
305                        step: 0,
306                        element: Element::Expression(expression),
307                        user_initiated: true,
308                    });
309                };
310
311                if matches!(act, LeoInterpretOver(..)) {
312                    self.cursor.over()?
313                } else {
314                    StepResult { finished: false, value: None }
315                }
316            }
317
318            Step => self.cursor.whole_step()?,
319
320            Into => self.cursor.step()?,
321
322            Over => self.cursor.over()?,
323
324            Breakpoint(breakpoint) => {
325                self.breakpoints.push(breakpoint.clone());
326                StepResult { finished: false, value: None }
327            }
328
329            Watch(code) => {
330                self.watchpoints.push(Watchpoint { code: code.clone(), last_result: None });
331                StepResult { finished: false, value: None }
332            }
333
334            PrintRegister(register_index) => {
335                let Some(Frame { element: Element::AleoExecution { registers, .. }, .. }) = self.cursor.frames.last()
336                else {
337                    halt_no_span!("cannot print register - not currently interpreting Aleo VM code");
338                };
339
340                if let Some(value) = registers.get(register_index) {
341                    StepResult { finished: false, value: Some(value.clone()) }
342                } else {
343                    halt_no_span!("no such register {register_index}");
344                }
345            }
346
347            Run => {
348                while !self.cursor.frames.is_empty() {
349                    if let Some((program, line)) = self.current_program_and_line() {
350                        if self.breakpoints.iter().any(|bp| bp.program == program && bp.line == line) {
351                            return Ok(None);
352                        }
353                    }
354                    self.cursor.step()?;
355                    if self.update_watchpoints()? {
356                        return Ok(None);
357                    }
358                }
359                StepResult { finished: false, value: None }
360            }
361        };
362
363        self.actions.push(act);
364
365        Ok(ret.value)
366    }
367
368    pub fn view_current(&self) -> Option<impl Display> {
369        if let Some(span) = self.current_span() {
370            if span != Default::default() {
371                return with_session_globals(|s| s.source_map.contents_of_span(span));
372            }
373        }
374
375        Some(match &self.cursor.frames.last()?.element {
376            Element::Statement(statement) => format!("{statement}"),
377            Element::Expression(expression) => format!("{expression}"),
378            Element::Block { block, .. } => format!("{block}"),
379            Element::DelayedCall(gid) => format!("Delayed call to {gid}"),
380            Element::AleoExecution { context, instruction_index, .. } => match &**context {
381                AleoContext::Closure(closure) => closure.instructions().get(*instruction_index).map(|i| format!("{i}")),
382                AleoContext::Function(function) => {
383                    function.instructions().get(*instruction_index).map(|i| format!("{i}"))
384                }
385                AleoContext::Finalize(finalize) => finalize.commands().get(*instruction_index).map(|i| format!("{i}")),
386            }
387            .unwrap_or_else(|| "...".to_string()),
388        })
389    }
390
391    pub fn view_current_in_context(&self) -> Option<(impl Display, usize, usize)> {
392        if let Some(Frame { element: Element::AleoExecution { context, instruction_index, .. }, .. }) =
393            self.cursor.frames.last()
394        {
395            // For Aleo VM code, there are no spans; just print out the code without referring to the source code.
396
397            fn write_all<I: Display>(
398                items: impl Iterator<Item = I>,
399                instruction_index: usize,
400                result: &mut String,
401                start: &mut usize,
402                stop: &mut usize,
403            ) {
404                for (i, item) in items.enumerate() {
405                    if i == instruction_index {
406                        *start = result.len();
407                    }
408                    writeln!(result, "    {item}").expect("write shouldn't fail");
409                    if i == instruction_index {
410                        *stop = result.len();
411                    }
412                }
413            }
414
415            let mut result = String::new();
416            let mut start: usize = 0usize;
417            let mut stop: usize = 0usize;
418
419            match &**context {
420                AleoContext::Closure(closure) => {
421                    writeln!(&mut result, "closure {}", closure.name()).expect("write shouldn't fail");
422                    write_all(closure.inputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
423                    write_all(closure.instructions().iter(), *instruction_index, &mut result, &mut start, &mut stop);
424                    write_all(closure.outputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
425                }
426                AleoContext::Function(function) => {
427                    writeln!(&mut result, "function {}", function.name()).expect("write shouldn't fail");
428                    write_all(function.inputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
429                    write_all(function.instructions().iter(), *instruction_index, &mut result, &mut start, &mut stop);
430                    write_all(function.outputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
431                }
432                AleoContext::Finalize(finalize) => {
433                    writeln!(&mut result, "finalize {}", finalize.name()).expect("write shouldn't fail");
434                    write_all(finalize.inputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
435                    write_all(finalize.commands().iter(), *instruction_index, &mut result, &mut start, &mut stop);
436                }
437            }
438
439            Some((result, start, stop))
440        } else {
441            // For Leo code, we use spans to print the original source code.
442            let span = self.current_span()?;
443            if span == Default::default() {
444                return None;
445            }
446            with_session_globals(|s| {
447                let source_file = s.source_map.find_source_file(span.lo)?;
448                let first_span = Span::new(source_file.absolute_start, span.lo);
449                let last_span = Span::new(span.hi, source_file.absolute_end);
450                let mut result = String::new();
451                result.push_str(&s.source_map.contents_of_span(first_span)?);
452                let start = result.len();
453                result.push_str(&s.source_map.contents_of_span(span)?);
454                let stop = result.len();
455                result.push_str(&s.source_map.contents_of_span(last_span)?);
456                Some((result, start, stop))
457            })
458        }
459    }
460
461    fn current_program_and_line(&self) -> Option<(String, usize)> {
462        if let Some(span) = self.current_span() {
463            if let Some(source_file) = with_session_globals(|s| s.source_map.find_source_file(span.lo)) {
464                let (line, _) = source_file.line_col(span.lo);
465                if let FileName::Real(name) = &source_file.name {
466                    if let Some(program) = self.filename_to_program.get(name) {
467                        return Some((program.clone(), line as usize + 1));
468                    }
469                }
470            }
471        }
472        None
473    }
474
475    fn current_span(&self) -> Option<Span> {
476        self.cursor.frames.last().map(|f| f.element.span())
477    }
478}