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::{NetworkName, interpreter_value::AsyncExecution};
19use leo_errors::{CompilerError, Handler, InterpreterHalt, LeoError, Result};
20
21/// Contains the state of interpretation, in the form of the `Cursor`,
22/// as well as information needed to interact with the user, like
23/// the breakpoints.
24pub struct Interpreter {
25    pub cursor: Cursor,
26    actions: Vec<InterpreterAction>,
27    handler: Handler,
28    pub node_builder: NodeBuilder,
29    breakpoints: Vec<Breakpoint>,
30    pub watchpoints: Vec<Watchpoint>,
31    saved_cursors: Vec<Cursor>,
32    filename_to_program: HashMap<PathBuf, String>,
33    parsed_inputs: u32,
34    /// The network.
35    network: NetworkName,
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, Q: 'a + AsRef<std::path::Path> + ?Sized>(
66        leo_source_files: &[(PathBuf, Vec<PathBuf>)], // Leo source files and their modules
67        aleo_source_files: impl IntoIterator<Item = &'a Q>,
68        private_key: String,
69        block_height: u32,
70        block_timestamp: i64,
71        network: NetworkName,
72    ) -> Result<Self> {
73        Self::new_impl(
74            leo_source_files,
75            &mut aleo_source_files.into_iter().map(|p| p.as_ref()),
76            private_key,
77            block_height,
78            block_timestamp,
79            network,
80        )
81    }
82
83    /// Parses a Leo source file and its modules into an `Ast`.
84    ///
85    /// # Arguments
86    /// - `path`: The path to the main `.leo` source file (e.g. `main.leo`).
87    /// - `modules`: A list of paths to module `.leo` files associated with the main file.
88    /// - `handler`: The compiler's diagnostic handler for reporting errors.
89    /// - `node_builder`: Utility for constructing unique node IDs in the AST.
90    /// - `network`: The target network.
91    ///
92    /// # Returns
93    /// - `Ok(Ast)`: If parsing succeeds.
94    /// - `Err(CompilerError)`: If file I/O or parsing fails.
95    ///
96    /// # Behavior
97    /// - Reads the contents of the main file and all modules.
98    /// - Registers each source file with the compiler's source map (via `with_session_globals`).
99    /// - Invokes the parser to produce the full AST including modules.
100    fn get_ast(
101        path: &std::path::PathBuf,
102        modules: &[std::path::PathBuf],
103        handler: &Handler,
104        node_builder: &NodeBuilder,
105        network: NetworkName,
106    ) -> Result<Ast> {
107        let text = fs::read_to_string(path).map_err(|e| CompilerError::file_read_error(path, e))?;
108        let source_file = with_session_globals(|s| s.source_map.new_source(&text, FileName::Real(path.to_path_buf())));
109
110        let modules = modules
111            .iter()
112            .map(|filename| {
113                let source = fs::read_to_string(filename).unwrap();
114                with_session_globals(|s| s.source_map.new_source(&source, FileName::Real(filename.to_path_buf())))
115            })
116            .collect::<Vec<_>>();
117
118        leo_parser::parse_ast(handler.clone(), node_builder, &source_file, &modules, network)
119    }
120
121    fn new_impl(
122        leo_source_files: &[(PathBuf, Vec<PathBuf>)],
123        aleo_source_files: &mut dyn Iterator<Item = &std::path::Path>,
124        private_key: String,
125        block_height: u32,
126        block_timestamp: i64,
127        network: NetworkName,
128    ) -> Result<Self> {
129        let handler = Handler::default();
130        let node_builder = Default::default();
131        let mut cursor: Cursor = Cursor::new(
132            true, // really_async
133            private_key,
134            block_height,
135            block_timestamp,
136            network,
137        );
138        let mut filename_to_program = HashMap::new();
139
140        for (path, modules) in leo_source_files {
141            let ast = Self::get_ast(path, modules, &handler, &node_builder, network)?;
142            for (&program, scope) in ast.ast.program_scopes.iter() {
143                filename_to_program.insert(path.to_path_buf(), program.to_string());
144                for (name, function) in scope.functions.iter() {
145                    cursor
146                        .functions
147                        .insert(Location::new(program, vec![*name]), FunctionVariant::Leo(function.clone()));
148                }
149
150                for (name, composite) in scope.structs.iter() {
151                    cursor.structs.insert(
152                        vec![*name],
153                        composite
154                            .members
155                            .iter()
156                            .map(|leo_ast::Member { identifier, type_, .. }| (identifier.name, type_.clone()))
157                            .collect::<IndexMap<_, _>>(),
158                    );
159                }
160
161                for (name, _mapping) in scope.mappings.iter() {
162                    cursor.mappings.insert(Location::new(program, vec![*name]), HashMap::new());
163                }
164
165                for (name, const_declaration) in scope.consts.iter() {
166                    cursor.frames.push(Frame {
167                        step: 0,
168                        element: Element::Expression(
169                            const_declaration.value.clone(),
170                            Some(const_declaration.type_.clone()),
171                        ),
172                        user_initiated: false,
173                    });
174                    cursor.over()?;
175                    let value = cursor.values.pop().unwrap();
176                    cursor.globals.insert(Location::new(program, vec![*name]), value);
177                }
178            }
179
180            for (mod_path, module) in ast.ast.modules.iter() {
181                let program = module.program_name;
182                let to_absolute_path = |name: Symbol| {
183                    let mut full_name = mod_path.clone();
184                    full_name.push(name);
185                    full_name
186                };
187                for (name, function) in module.functions.iter() {
188                    cursor.functions.insert(
189                        Location::new(program, to_absolute_path(*name)),
190                        FunctionVariant::Leo(function.clone()),
191                    );
192                }
193
194                for (name, composite) in module.structs.iter() {
195                    cursor.structs.insert(
196                        to_absolute_path(*name),
197                        composite
198                            .members
199                            .iter()
200                            .map(|leo_ast::Member { identifier, type_, .. }| (identifier.name, type_.clone()))
201                            .collect::<IndexMap<_, _>>(),
202                    );
203                }
204
205                for (name, const_declaration) in module.consts.iter() {
206                    cursor.frames.push(Frame {
207                        step: 0,
208                        element: Element::Expression(
209                            const_declaration.value.clone(),
210                            Some(const_declaration.type_.clone()),
211                        ),
212                        user_initiated: false,
213                    });
214                    cursor.over()?;
215                    let value = cursor.values.pop().unwrap();
216                    cursor.globals.insert(Location::new(program, to_absolute_path(*name)), value);
217                }
218            }
219        }
220
221        for path in aleo_source_files {
222            let aleo_program = Self::get_aleo_program(path)?;
223            let program = snarkvm_identifier_to_symbol(aleo_program.id().name());
224            filename_to_program.insert(path.to_path_buf(), program.to_string());
225
226            for (name, struct_type) in aleo_program.structs().iter() {
227                cursor.structs.insert(
228                    vec![snarkvm_identifier_to_symbol(name)],
229                    struct_type
230                        .members()
231                        .iter()
232                        .map(|(id, type_)| {
233                            (leo_ast::Identifier::from(id).name, leo_ast::Type::from_snarkvm(type_, None))
234                        })
235                        .collect::<IndexMap<_, _>>(),
236                );
237            }
238
239            for (name, record_type) in aleo_program.records().iter() {
240                use snarkvm::prelude::EntryType;
241                cursor.structs.insert(
242                    vec![snarkvm_identifier_to_symbol(name)],
243                    record_type
244                        .entries()
245                        .iter()
246                        .map(|(id, entry)| {
247                            // Destructure to get the inner type `t` directly
248                            let t = match entry {
249                                EntryType::Public(t) | EntryType::Private(t) | EntryType::Constant(t) => t,
250                            };
251
252                            (leo_ast::Identifier::from(id).name, leo_ast::Type::from_snarkvm(t, None))
253                        })
254                        .collect::<IndexMap<_, _>>(),
255                );
256            }
257
258            for (name, _mapping) in aleo_program.mappings().iter() {
259                cursor
260                    .mappings
261                    .insert(Location::new(program, vec![snarkvm_identifier_to_symbol(name)]), HashMap::new());
262            }
263
264            for (name, function) in aleo_program.functions().iter() {
265                cursor.functions.insert(
266                    Location::new(program, vec![snarkvm_identifier_to_symbol(name)]),
267                    FunctionVariant::AleoFunction(function.clone()),
268                );
269            }
270
271            for (name, closure) in aleo_program.closures().iter() {
272                cursor.functions.insert(
273                    Location::new(program, vec![snarkvm_identifier_to_symbol(name)]),
274                    FunctionVariant::AleoClosure(closure.clone()),
275                );
276            }
277        }
278
279        Ok(Interpreter {
280            cursor,
281            handler,
282            node_builder,
283            actions: Vec::new(),
284            breakpoints: Vec::new(),
285            watchpoints: Vec::new(),
286            saved_cursors: Vec::new(),
287            filename_to_program,
288            parsed_inputs: 0,
289            network,
290        })
291    }
292
293    pub fn save_cursor(&mut self) {
294        self.saved_cursors.push(self.cursor.clone());
295    }
296
297    /// Returns false if there was no saved cursor to restore.
298    pub fn restore_cursor(&mut self) -> bool {
299        if let Some(old_cursor) = self.saved_cursors.pop() {
300            self.cursor = old_cursor;
301            true
302        } else {
303            false
304        }
305    }
306
307    fn get_aleo_program(path: &std::path::Path) -> Result<Program<TestnetV0>> {
308        let text = fs::read_to_string(path).map_err(|e| CompilerError::file_read_error(path, e))?;
309        let program = text.parse()?;
310        Ok(program)
311    }
312
313    /// Returns true if any watchpoints changed.
314    pub fn update_watchpoints(&mut self) -> Result<bool> {
315        let mut changed = false;
316        let safe_cursor = self.cursor.clone();
317
318        for i in 0..self.watchpoints.len() {
319            let code = self.watchpoints[i].code.clone();
320            let new_value = match self.action(InterpreterAction::LeoInterpretOver(code)) {
321                Ok(None) => None,
322                Ok(Some(ret)) => Some(ret.to_string()),
323                Err(LeoError::InterpreterHalt(halt)) => {
324                    self.cursor = safe_cursor.clone();
325                    Some(halt.to_string())
326                }
327                Err(e) => return Err(e),
328            };
329            if self.watchpoints[i].last_result != new_value {
330                changed = true;
331                self.watchpoints[i].last_result = new_value;
332            }
333        }
334        Ok(changed)
335    }
336
337    pub fn action(&mut self, act: InterpreterAction) -> Result<Option<Value>> {
338        use InterpreterAction::*;
339
340        let ret = match &act {
341            RunFuture(n) => {
342                let future = self.cursor.futures.remove(*n);
343                match future {
344                    AsyncExecution::AsyncFunctionCall { function, arguments } => {
345                        self.cursor.values.extend(arguments);
346                        self.cursor.frames.push(Frame {
347                            step: 0,
348                            element: Element::DelayedCall(function),
349                            user_initiated: true,
350                        });
351                    }
352                    AsyncExecution::AsyncBlock { containing_function, block, names, .. } => {
353                        self.cursor.frames.push(Frame {
354                            step: 0,
355                            element: Element::DelayedAsyncBlock {
356                                program: containing_function.program,
357                                block,
358                                // Keep track of all the known variables up to this point.
359                                // These are available to use inside the block when we actually execute it.
360                                names: names.clone().into_iter().collect(),
361                            },
362                            user_initiated: false,
363                        });
364                    }
365                }
366                self.cursor.step()?
367            }
368            LeoInterpretInto(s) | LeoInterpretOver(s) => {
369                let filename = FileName::Custom(format!("user_input{:04}", self.parsed_inputs));
370                self.parsed_inputs += 1;
371                let source_file = with_session_globals(|globals| globals.source_map.new_source(s, filename));
372                let s = s.trim();
373                if s.ends_with(';') {
374                    let statement = leo_parser::parse_statement(
375                        self.handler.clone(),
376                        &self.node_builder,
377                        s,
378                        source_file.absolute_start,
379                        self.network,
380                    )
381                    .map_err(|_e| {
382                        LeoError::InterpreterHalt(InterpreterHalt::new("failed to parse statement".into()))
383                    })?;
384                    // The spans of the code the user wrote at the REPL are meaningless, so get rid of them.
385                    self.cursor.frames.push(Frame {
386                        step: 0,
387                        element: Element::Statement(statement),
388                        user_initiated: true,
389                    });
390                } else {
391                    let expression = leo_parser::parse_expression(
392                        self.handler.clone(),
393                        &self.node_builder,
394                        s,
395                        source_file.absolute_start,
396                        self.network,
397                    )
398                    .map_err(|e| {
399                        LeoError::InterpreterHalt(InterpreterHalt::new(format!("Failed to parse expression: {e}")))
400                    })?;
401                    // The spans of the code the user wrote at the REPL are meaningless, so get rid of them.
402                    self.cursor.frames.push(Frame {
403                        step: 0,
404                        element: Element::Expression(expression, None),
405                        user_initiated: true,
406                    });
407                };
408
409                if matches!(act, LeoInterpretOver(..)) {
410                    self.cursor.over()?
411                } else {
412                    StepResult { finished: false, value: None }
413                }
414            }
415
416            Step => self.cursor.whole_step()?,
417
418            Into => self.cursor.step()?,
419
420            Over => self.cursor.over()?,
421
422            Breakpoint(breakpoint) => {
423                self.breakpoints.push(breakpoint.clone());
424                StepResult { finished: false, value: None }
425            }
426
427            Watch(code) => {
428                self.watchpoints.push(Watchpoint { code: code.clone(), last_result: None });
429                StepResult { finished: false, value: None }
430            }
431
432            PrintRegister(register_index) => {
433                let Some(Frame { element: Element::AleoExecution { registers, .. }, .. }) = self.cursor.frames.last()
434                else {
435                    halt_no_span!("cannot print register - not currently interpreting Aleo VM code");
436                };
437
438                if let Some(value) = registers.get(register_index) {
439                    StepResult { finished: false, value: Some(value.clone()) }
440                } else {
441                    halt_no_span!("no such register {register_index}");
442                }
443            }
444
445            Run => {
446                while !self.cursor.frames.is_empty() {
447                    if let Some((program, line)) = self.current_program_and_line()
448                        && self.breakpoints.iter().any(|bp| bp.program == program && bp.line == line)
449                    {
450                        return Ok(None);
451                    }
452                    self.cursor.step()?;
453                    if self.update_watchpoints()? {
454                        return Ok(None);
455                    }
456                }
457                StepResult { finished: false, value: None }
458            }
459        };
460
461        self.actions.push(act);
462
463        Ok(ret.value)
464    }
465
466    pub fn view_current(&self) -> Option<impl Display> {
467        if let Some(span) = self.current_span()
468            && span != Default::default()
469        {
470            return with_session_globals(|s| s.source_map.contents_of_span(span));
471        }
472
473        Some(match &self.cursor.frames.last()?.element {
474            Element::Statement(statement) => format!("{statement}"),
475            Element::Expression(expression, _) => format!("{expression}"),
476            Element::Block { block, .. } => format!("{block}"),
477            Element::DelayedCall(gid) => format!("Delayed call to {gid}"),
478            Element::DelayedAsyncBlock { .. } => "Delayed async block".to_string(),
479            Element::AleoExecution { context, instruction_index, .. } => match &**context {
480                AleoContext::Closure(closure) => closure.instructions().get(*instruction_index).map(|i| format!("{i}")),
481                AleoContext::Function(function) => {
482                    function.instructions().get(*instruction_index).map(|i| format!("{i}"))
483                }
484                AleoContext::Finalize(finalize) => finalize.commands().get(*instruction_index).map(|i| format!("{i}")),
485            }
486            .unwrap_or_else(|| "...".to_string()),
487        })
488    }
489
490    pub fn view_current_in_context(&self) -> Option<(impl Display, usize, usize)> {
491        if let Some(Frame { element: Element::AleoExecution { context, instruction_index, .. }, .. }) =
492            self.cursor.frames.last()
493        {
494            // For Aleo VM code, there are no spans; just print out the code without referring to the source code.
495
496            fn write_all<I: Display>(
497                items: impl Iterator<Item = I>,
498                instruction_index: usize,
499                result: &mut String,
500                start: &mut usize,
501                stop: &mut usize,
502            ) {
503                for (i, item) in items.enumerate() {
504                    if i == instruction_index {
505                        *start = result.len();
506                    }
507                    writeln!(result, "    {item}").expect("write shouldn't fail");
508                    if i == instruction_index {
509                        *stop = result.len();
510                    }
511                }
512            }
513
514            let mut result = String::new();
515            let mut start: usize = 0usize;
516            let mut stop: usize = 0usize;
517
518            match &**context {
519                AleoContext::Closure(closure) => {
520                    writeln!(&mut result, "closure {}", closure.name()).expect("write shouldn't fail");
521                    write_all(closure.inputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
522                    write_all(closure.instructions().iter(), *instruction_index, &mut result, &mut start, &mut stop);
523                    write_all(closure.outputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
524                }
525                AleoContext::Function(function) => {
526                    writeln!(&mut result, "function {}", function.name()).expect("write shouldn't fail");
527                    write_all(function.inputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
528                    write_all(function.instructions().iter(), *instruction_index, &mut result, &mut start, &mut stop);
529                    write_all(function.outputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
530                }
531                AleoContext::Finalize(finalize) => {
532                    writeln!(&mut result, "finalize {}", finalize.name()).expect("write shouldn't fail");
533                    write_all(finalize.inputs().iter(), usize::MAX, &mut result, &mut 0usize, &mut 0usize);
534                    write_all(finalize.commands().iter(), *instruction_index, &mut result, &mut start, &mut stop);
535                }
536            }
537
538            Some((result, start, stop))
539        } else {
540            // For Leo code, we use spans to print the original source code.
541            let span = self.current_span()?;
542            if span == Default::default() {
543                return None;
544            }
545            with_session_globals(|s| {
546                let source_file = s.source_map.find_source_file(span.lo)?;
547                let first_span = Span::new(source_file.absolute_start, span.lo);
548                let last_span = Span::new(span.hi, source_file.absolute_end);
549                let mut result = String::new();
550                result.push_str(&s.source_map.contents_of_span(first_span)?);
551                let start = result.len();
552                result.push_str(&s.source_map.contents_of_span(span)?);
553                let stop = result.len();
554                result.push_str(&s.source_map.contents_of_span(last_span)?);
555                Some((result, start, stop))
556            })
557        }
558    }
559
560    fn current_program_and_line(&self) -> Option<(String, usize)> {
561        if let Some(span) = self.current_span()
562            && let Some(source_file) = with_session_globals(|s| s.source_map.find_source_file(span.lo))
563        {
564            let (line, _) = source_file.line_col(span.lo);
565            if let FileName::Real(name) = &source_file.name
566                && let Some(program) = self.filename_to_program.get(name)
567            {
568                return Some((program.clone(), line as usize + 1));
569            }
570        }
571        None
572    }
573
574    fn current_span(&self) -> Option<Span> {
575        self.cursor.frames.last().map(|f| f.element.span())
576    }
577}