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