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