diff --git a/Cargo.toml b/Cargo.toml index 9c8f38fab..a4bc86e6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["triton-vm", "constraint-evaluation-generator"] +members = ["triton-vm", "triton-tui", "constraint-evaluation-generator"] resolver = "2" [profile.test] diff --git a/triton-tui/.config/default_config.json b/triton-tui/.config/default_config.json new file mode 100644 index 000000000..846f79f4d --- /dev/null +++ b/triton-tui/.config/default_config.json @@ -0,0 +1,43 @@ +{ + "keybindings": { + "Home": { + "": "Quit", + "": "Quit", + "": "Suspend", + + "": "Mode::Help", + "": "Mode::Memory", + + "": "Continue", + "": "Step", + "": "Next", + "": "Finish", + "": "Undo", + "": "Reset", + + "": "ToggleAll", + "": "ToggleTypeHintDisplay", + "": "ToggleCallStackDisplay", + "": "ToggleSpongeStateDisplay", + "": "ToggleInputDisplay" + }, + "Help": { + "": "Quit", + "": "Quit", + "": "Suspend", + + "": "HideHelpScreen", + "": "Mode::Memory", + "": "Mode::Home" + }, + "Memory": { + "": "Quit", + "": "Quit", + "": "Suspend", + + "": "Mode::Help", + "": "Mode::Home", + "": "Mode::Home" + } + } +} diff --git a/triton-tui/.envrc b/triton-tui/.envrc new file mode 100644 index 000000000..2f9597031 --- /dev/null +++ b/triton-tui/.envrc @@ -0,0 +1,3 @@ +export TRITON_TUI_CONFIG=$(pwd)/.config +export TRITON_TUI_DATA=$(pwd)/.data +export TRITON_TUI_LOG_LEVEL=debug diff --git a/triton-tui/Cargo.toml b/triton-tui/Cargo.toml new file mode 100644 index 000000000..5c34309bb --- /dev/null +++ b/triton-tui/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "triton-tui" +description = "A TUI that helps debugging programs written for Triton VM." + +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true + +[dependencies] +arbitrary.workspace = true +better-panic = "0.3" +clap = { version = "4", features = ["derive", "cargo", "wrap_help", "unicode", "string", "unstable-styles"] } +color-eyre = "0.6" +config = "0.13" +crossterm = { version = "0.27", features = ["serde", "event-stream"] } +derive_deref = "1" +directories = "5" +fs-err = "2.0" +futures = "0.3" +human-panic = "1" +itertools.workspace = true +lazy_static.workspace = true +libc = "0.2" +num-traits.workspace = true +ratatui = { version = "0.25", features = ["serde", "macros"] } +serde.workspace = true +serde_json = "1.0" +signal-hook = "0.3" +strip-ansi-escapes = "0.2" +strum.workspace = true +tokio = { version = "1", features = ["full"] } +tokio-util = "0.7" +tracing = "0.1" +tracing-error = "0.2" +tracing-subscriber = { version = "0.3", features = ["env-filter", "serde"] } +triton-vm = { path = "../triton-vm" } +tui-textarea = "0.4" + +[dev-dependencies] +assert2.workspace = true +pretty_assertions.workspace = true +proptest.workspace = true +proptest-arbitrary-interop.workspace = true +rexpect = "0.5" +test-strategy.workspace = true + +[[bin]] +name = "triton-tui" +path = "src/main.rs" + +[[test]] +name = "integration" +path = "tests/tests.rs" diff --git a/triton-tui/README.md b/triton-tui/README.md new file mode 100644 index 000000000..637399431 --- /dev/null +++ b/triton-tui/README.md @@ -0,0 +1,29 @@ +# Triton TUI + +A TUI to run and debug programs written for Triton VM. + +Example run of Triton TUI + +## Getting Started + +Triton TUI tries to be helpful πŸ™‚. List possible (and required) arguments with `triton-tui --help`. In the TUI, press `h` to access the help screen. + +The [example program](./examples/program.tasm), serving as the tutorial, can be run with + +```sh +triton-tui --program examples/program.tasm +``` + +## Installation + +Through [crates.io](https://crates.io/crates/triton-tui): + +```sh +cargo install triton-tui +``` + +Locally: + +```sh +cargo install --path . +``` diff --git a/triton-tui/build.rs b/triton-tui/build.rs new file mode 100644 index 000000000..c4a03c30c --- /dev/null +++ b/triton-tui/build.rs @@ -0,0 +1,62 @@ +fn main() { + let git_dir = maybe_get_git_dir(); + trigger_rebuild_if_head_or_some_relevant_ref_changes(git_dir); + + let git_output = std::process::Command::new("git") + .args(["describe", "--always", "--tags", "--long", "--dirty"]) + .output() + .ok(); + let git_info = git_output + .as_ref() + .and_then(|output| std::str::from_utf8(&output.stdout).ok().map(str::trim)); + let cargo_pkg_version = env!("CARGO_PKG_VERSION"); + + // Default git_describe to cargo_pkg_version + let mut git_describe = String::from(cargo_pkg_version); + + if let Some(git_info) = git_info { + // If the `git_info` contains `CARGO_PKG_VERSION`, we simply use `git_info` as it is. + // Otherwise, prepend `CARGO_PKG_VERSION` to `git_info`. + if git_info.contains(cargo_pkg_version) { + // Remove the 'g' before the commit sha + let git_info = &git_info.replace('g', ""); + git_describe = git_info.to_string(); + } else { + git_describe = format!("v{cargo_pkg_version}-{git_info}"); + } + } + + println!("cargo:rustc-env=TRITON_TUI_GIT_INFO={git_describe}"); +} + +fn maybe_get_git_dir() -> Option { + let git_output = std::process::Command::new("git") + .args(["rev-parse", "--git-dir"]) + .output() + .ok(); + git_output.as_ref().and_then(|output| { + std::str::from_utf8(&output.stdout) + .ok() + .and_then(|s| s.strip_suffix('\n').or_else(|| s.strip_suffix("\r\n"))) + .map(str::to_string) + }) +} + +fn trigger_rebuild_if_head_or_some_relevant_ref_changes(git_dir: Option) { + if let Some(git_dir) = git_dir { + let git_path = std::path::Path::new(&git_dir); + let refs_path = git_path.join("refs"); + if git_path.join("HEAD").exists() { + println!("cargo:rerun-if-changed={git_dir}/HEAD"); + } + if git_path.join("packed-refs").exists() { + println!("cargo:rerun-if-changed={git_dir}/packed-refs"); + } + if refs_path.join("heads").exists() { + println!("cargo:rerun-if-changed={git_dir}/refs/heads"); + } + if refs_path.join("tags").exists() { + println!("cargo:rerun-if-changed={git_dir}/refs/tags"); + } + } +} diff --git a/triton-tui/examples/non_determinism.json b/triton-tui/examples/non_determinism.json new file mode 100644 index 000000000..4e3a1ab7a --- /dev/null +++ b/triton-tui/examples/non_determinism.json @@ -0,0 +1,5 @@ +{ + "individual_tokens" : [497, 598, 699], + "digests" : [[3, 4, 5, 6, 7], [4, 5, 6, 7, 8]], + "ram" : {"5": 50, "6": 60, "7": 70} +} diff --git a/triton-tui/examples/program.tasm b/triton-tui/examples/program.tasm new file mode 100644 index 000000000..c2ec348d2 --- /dev/null +++ b/triton-tui/examples/program.tasm @@ -0,0 +1,52 @@ +//! Demonstrates use and functionality of the Triton VM TUI. +//! +//! Serves as the tutorial: execution intentionally fails at first, hopefully +//! encouraging you to play around. :) The inline comments should make matters trivial. +//! +//! Press `h` in Triton VM TUI to show the help screen. + +// Let's start! +push 3 +hint loop_counter = stack[0] // Current stack can be annotated to help debugging. +break + +call my_loop +hint numbers: array = stack[1..4] // Annotations also work with ranges. + +call check_result +call write_to_memory +call read_from_secret_input +call sponge_instructions +halt + +my_loop: + dup 0 push 0 eq skiz return + read_io 3 // call with `--input ./public_input.txt` + mul mul + break + dup 0 write_io 1 + swap 1 push -1 add recurse + +check_result: // Try enabling + // pop 1 // <- this line for + assert // <- this assertion to work. + return // Hot reload & reset of the VM is `r`. + +write_to_memory: + push 42 + write_mem 2 // You can inspect memory with `m`. + pop 1 + return + +read_from_secret_input: + divine 3 // flag `--non-determinism ./non_determinism.json` + mul mul + swap 5 + divine_sibling pop 5 + divine_sibling hash + return + +sponge_instructions: + sponge_init // Show the Sponge state with `t,s`. + sponge_squeeze + return diff --git a/triton-tui/examples/public_input.txt b/triton-tui/examples/public_input.txt new file mode 100644 index 000000000..5049e9bdc --- /dev/null +++ b/triton-tui/examples/public_input.txt @@ -0,0 +1,3 @@ + 2 3 5 +17 19 42 + 1 1 1 diff --git a/triton-tui/examples/triton-tui.gif b/triton-tui/examples/triton-tui.gif new file mode 100644 index 000000000..835ea897a Binary files /dev/null and b/triton-tui/examples/triton-tui.gif differ diff --git a/triton-tui/examples/triton-tui.tape b/triton-tui/examples/triton-tui.tape new file mode 100644 index 000000000..44bd90e77 --- /dev/null +++ b/triton-tui/examples/triton-tui.tape @@ -0,0 +1,36 @@ +Output triton-tui.gif + +Set Shell zsh +Set Width 2000 +Set Height 1000 + +Sleep 1s +Hide +Type "triton-tui -p ./program.tasm -i public_input.txt -n non_determinism.json" +Show +Sleep 1s +Enter +Sleep 3s + +Type "c" +Sleep 2s +Type "c" +Sleep 2s +Type "c" +Sleep 2s +Type "ti" +Type "tc" +Sleep 2s +Type "ff" +Sleep 3s + +Type "m" +Sleep 3s +Enter +Type "30" +Enter +Sleep 3s + +Hide +Type "q" +Ctrl+D diff --git a/triton-tui/src/action.rs b/triton-tui/src/action.rs new file mode 100644 index 000000000..2be83cf68 --- /dev/null +++ b/triton-tui/src/action.rs @@ -0,0 +1,182 @@ +use std::fmt; + +use arbitrary::Arbitrary; +use itertools::Itertools; +use serde::de::*; +use serde::*; + +use triton_vm::instruction::Instruction; +use triton_vm::op_stack::NUM_OP_STACK_REGISTERS; +use triton_vm::BFieldElement; + +use crate::mode::Mode; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub(crate) enum Action { + Tick, + Render, + Resize(u16, u16), + Suspend, + Resume, + Quit, + Refresh, + Error(String), + + Execute(Execute), + + /// Undo the last [`Execute`] action. + Undo, + + RecordUndoInfo, + + /// Reset the program state. + Reset, + + Toggle(ToggleWidget), + + HideHelpScreen, + + Mode(Mode), + + ExecutedInstruction(Box), +} + +/// Various ways to advance the program state. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub(crate) enum Execute { + /// Continue program execution until next breakpoint. + Continue, + + /// Execute a single instruction. + Step, + + /// Execute a single instruction, stepping over `call`s. + Next, + + /// Execute instructions until the current `call` returns. + Finish, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Arbitrary)] +pub(crate) enum ToggleWidget { + All, + TypeHint, + CallStack, + SpongeState, + Input, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Arbitrary)] +pub(crate) struct ExecutedInstruction { + pub instruction: Instruction, + pub old_top_of_stack: [BFieldElement; NUM_OP_STACK_REGISTERS], + pub new_top_of_stack: [BFieldElement; NUM_OP_STACK_REGISTERS], +} + +impl<'de> Deserialize<'de> for Action { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ActionVisitor; + + impl<'de> Visitor<'de> for ActionVisitor { + type Value = Action; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a valid string representation of Action") + } + + fn visit_str(self, value: &str) -> Result + where + E: Error, + { + match value { + "Tick" => Ok(Action::Tick), + "Render" => Ok(Action::Render), + "Suspend" => Ok(Action::Suspend), + "Resume" => Ok(Action::Resume), + "Quit" => Ok(Action::Quit), + "Refresh" => Ok(Action::Refresh), + + "Continue" => Ok(Action::Execute(Execute::Continue)), + "Step" => Ok(Action::Execute(Execute::Step)), + "Next" => Ok(Action::Execute(Execute::Next)), + "Finish" => Ok(Action::Execute(Execute::Finish)), + + "Undo" => Ok(Action::Undo), + "Reset" => Ok(Action::Reset), + + "ToggleAll" => Ok(Action::Toggle(ToggleWidget::All)), + "ToggleTypeHintDisplay" => Ok(Action::Toggle(ToggleWidget::TypeHint)), + "ToggleCallStackDisplay" => Ok(Action::Toggle(ToggleWidget::CallStack)), + "ToggleSpongeStateDisplay" => Ok(Action::Toggle(ToggleWidget::SpongeState)), + "ToggleInputDisplay" => Ok(Action::Toggle(ToggleWidget::Input)), + + "HideHelpScreen" => Ok(Action::HideHelpScreen), + + mode if mode.starts_with("Mode::") => Self::parse_mode(mode), + data if data.starts_with("Error(") => Self::parse_error(data), + data if data.starts_with("Resize(") => Self::parse_resize(data), + _ => Err(E::custom(format!("Unknown Action variant: {value}"))), + } + } + } + + impl ActionVisitor { + fn parse_mode(mode: &str) -> Result + where + E: Error, + { + let maybe_mode_and_variant = mode.split("::").collect_vec(); + let maybe_variant = maybe_mode_and_variant.get(1).copied(); + let mode_variant = + maybe_variant.ok_or(E::custom(format!("Missing Mode variant: {mode}")))?; + let mode = Mode::deserialize(mode_variant.into_deserializer())?; + Ok(Action::Mode(mode)) + } + + fn parse_error(data: &str) -> Result + where + E: Error, + { + let error_msg = data.trim_start_matches("Error(").trim_end_matches(')'); + Ok(Action::Error(error_msg.to_string())) + } + + fn parse_resize(data: &str) -> Result + where + E: Error, + { + let parts: Vec<&str> = data + .trim_start_matches("Resize(") + .trim_end_matches(')') + .split(',') + .collect(); + if parts.len() == 2 { + let width: u16 = parts[0].trim().parse().map_err(E::custom)?; + let height: u16 = parts[1].trim().parse().map_err(E::custom)?; + Ok(Action::Resize(width, height)) + } else { + Err(E::custom(format!("Invalid Resize format: {data}"))) + } + } + } + + deserializer.deserialize_str(ActionVisitor) + } +} + +impl ExecutedInstruction { + pub fn new( + instruction: Instruction, + old_top_of_stack: [BFieldElement; NUM_OP_STACK_REGISTERS], + new_top_of_stack: [BFieldElement; NUM_OP_STACK_REGISTERS], + ) -> Self { + Self { + instruction, + old_top_of_stack, + new_top_of_stack, + } + } +} diff --git a/triton-tui/src/args.rs b/triton-tui/src/args.rs new file mode 100644 index 000000000..8a4dad629 --- /dev/null +++ b/triton-tui/src/args.rs @@ -0,0 +1,46 @@ +use clap::Parser; + +use crate::utils::version; + +const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); +const DEFAULT_PROGRAM_PATH: &str = "examples/program.tasm"; + +#[derive(Debug, Clone, PartialEq, Parser)] +#[command(author, version = version(), about)] +pub(crate) struct Args { + #[arg(short, long, value_name = "PATH")] + /// Path to program to run + pub program: String, + + #[arg(short, long, value_name = "PATH")] + /// Path to file containing public input + pub input: Option, + + #[arg(short, long, value_name = "PATH")] + /// Path to JSON file containing all non-determinism + pub non_determinism: Option, +} + +impl Default for Args { + fn default() -> Self { + let program = format!("{MANIFEST_DIR}/{DEFAULT_PROGRAM_PATH}"); + Self { + program, + input: None, + non_determinism: None, + } + } +} + +#[cfg(test)] +mod tests { + use assert2::let_assert; + + use super::*; + + #[test] + fn tui_requires_some_arguments() { + let cli_args: Vec = vec![]; + let_assert!(Err(_) = Args::try_parse_from(cli_args)); + } +} diff --git a/triton-tui/src/components.rs b/triton-tui/src/components.rs new file mode 100644 index 000000000..391ea7070 --- /dev/null +++ b/triton-tui/src/components.rs @@ -0,0 +1,68 @@ +use std::fmt::Debug; + +use color_eyre::eyre::Result; +use crossterm::event::KeyEvent; +use crossterm::event::MouseEvent; +use ratatui::prelude::*; +use tokio::sync::mpsc::UnboundedSender; + +use crate::action::Action; +use crate::triton_vm_state::TritonVMState; +use crate::tui::Event; + +pub(crate) mod help; +pub(crate) mod home; +pub(crate) mod memory; + +/// `Component` is a trait that represents a visual and interactive element of the user interface. +/// Implementors of this trait can be registered with the main application loop and will be able to +/// receive events, update state, and be rendered on the screen. +pub(crate) trait Component: Debug { + fn register_action_handler(&mut self, _tx: UnboundedSender) -> Result<()> { + Ok(()) + } + + fn request_exclusive_key_event_handling(&self) -> bool { + false + } + + fn handle_event(&mut self, event: Option) -> Result> { + let r = match event { + Some(Event::Key(key_event)) => self.handle_key_event(key_event)?, + Some(Event::Mouse(mouse_event)) => self.handle_mouse_event(mouse_event)?, + _ => None, + }; + Ok(r) + } + + fn handle_key_event(&mut self, _: KeyEvent) -> Result> { + Ok(None) + } + + fn handle_mouse_event(&mut self, _: MouseEvent) -> Result> { + Ok(None) + } + + /// Update the state of the component based on a received action. + fn update(&mut self, _: Action) -> Result> { + Ok(None) + } + + /// Render the component on the screen. + fn draw(&mut self, _frame: &mut Frame<'_>, _state: &TritonVMState) -> Result<()> { + Ok(()) + } +} + +/// helper function to create a centered rect using up certain percentage of the available rect `r` +fn centered_rect(area: Rect, percent_x: u16, percent_y: u16) -> Rect { + let area = centered_rect_in_direction(area, percent_y, Direction::Vertical); + centered_rect_in_direction(area, percent_x, Direction::Horizontal) +} + +fn centered_rect_in_direction(area: Rect, percentage: u16, direction: Direction) -> Rect { + let requested = Constraint::Percentage(percentage); + let half_of_remainder = Constraint::Percentage((100 - percentage) / 2); + let constraints = [half_of_remainder, requested, half_of_remainder]; + Layout::new(direction, constraints).split(area)[1] +} diff --git a/triton-tui/src/components/help.rs b/triton-tui/src/components/help.rs new file mode 100644 index 000000000..b7a07b967 --- /dev/null +++ b/triton-tui/src/components/help.rs @@ -0,0 +1,93 @@ +use std::fmt::Display; + +use arbitrary::Arbitrary; +use color_eyre::eyre::Result; +use ratatui::prelude::*; +use ratatui::widgets::block::*; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +use crate::action::Action; +use crate::components::centered_rect; +use crate::components::Component; +use crate::mode::Mode; +use crate::triton_vm_state::TritonVMState; + +#[derive(Default, Debug, Clone, Copy, Arbitrary)] +pub(crate) struct Help { + pub previous_mode: Mode, +} + +impl Component for Help { + fn update(&mut self, action: Action) -> Result> { + match action { + Action::HideHelpScreen => Ok(Some(Action::Mode(self.previous_mode))), + Action::Mode(mode) if mode != Mode::Help => { + self.previous_mode = mode; + Ok(None) + } + _ => Ok(None), + } + } + + fn draw(&mut self, frame: &mut Frame<'_>, _: &TritonVMState) -> Result<()> { + let title = Title::from(" Triton TUI β€” Help").alignment(Alignment::Left); + let text = [ + Help::mode_line("Home"), + Help::help_line("c", "continue – execute to next breakpoint"), + Help::help_line("s", "step – execute one instruction"), + Help::help_line("n", "next – like β€œstep” but steps over β€œcall”"), + Help::help_line("f", "finish – step out of current β€œcall”"), + Help::help_line("u", "undo last command that advanced execution"), + Help::help_line("r", "reload files and restart Triton VM"), + String::new(), + Help::help_line("t,a", "toggle all widgets"), + Help::help_line("t,t", "toggle type annotations"), + Help::help_line("t,c", "toggle call stack"), + Help::help_line("t,i", "toggle displaying input (if any)"), + String::new(), + "General:".to_string(), + Help::help_line("esc", "show Home screen"), + Help::help_line("m", "toggle Memory screen"), + Help::help_line("h", "toggle Help"), + String::new(), + Help::help_line("q", "quit"), + ] + .map(Line::from) + .to_vec(); + + let block = Block::default().title(title).padding(Padding::uniform(1)); + let paragraph = Paragraph::new(text).block(block); + + let area = centered_rect(frame.size(), 50, 80); + frame.render_widget(paragraph, area); + Ok(()) + } +} + +impl Help { + fn mode_line(mode: impl Display) -> String { + format!("{mode}:") + } + + fn help_line(keys: impl Display, help: impl Display) -> String { + format!(" {keys: <4} {help}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest_arbitrary_interop::arb; + use ratatui::backend::TestBackend; + use test_strategy::proptest; + + #[proptest] + fn render(#[strategy(arb())] mut help: Help) { + let state = TritonVMState::new(&Default::default()).unwrap(); + + let backend = TestBackend::new(150, 50); + let mut terminal = Terminal::new(backend)?; + terminal.draw(|f| help.draw(f, &state).unwrap()).unwrap(); + } +} diff --git a/triton-tui/src/components/home.rs b/triton-tui/src/components/home.rs new file mode 100644 index 000000000..a7ed3db4f --- /dev/null +++ b/triton-tui/src/components/home.rs @@ -0,0 +1,551 @@ +use arbitrary::Arbitrary; +use color_eyre::eyre::Result; +use itertools::Itertools; +use ratatui::prelude::*; +use ratatui::widgets::block::*; +use ratatui::widgets::*; +use strum::EnumCount; + +use triton_vm::instruction::*; +use triton_vm::op_stack::OpStackElement; + +use crate::action::*; +use crate::element_type_hint::ElementTypeHint; +use crate::triton_vm_state::TritonVMState; + +use super::Component; +use super::Frame; + +#[derive(Debug, Clone, Copy, Arbitrary)] +pub(crate) struct Home { + show_type_hints: bool, + show_call_stack: bool, + show_sponge_state: bool, + show_inputs: bool, +} + +impl Default for Home { + fn default() -> Self { + Self { + show_type_hints: true, + show_call_stack: true, + show_sponge_state: false, + show_inputs: true, + } + } +} + +impl Home { + fn address_render_width(&self, state: &TritonVMState) -> usize { + let max_address = state.program.len_bwords(); + max_address.to_string().len() + } + + fn toggle_widget(&mut self, toggle: ToggleWidget) { + match toggle { + ToggleWidget::All => self.toggle_all_widgets(), + ToggleWidget::TypeHint => self.show_type_hints = !self.show_type_hints, + ToggleWidget::CallStack => self.show_call_stack = !self.show_call_stack, + ToggleWidget::SpongeState => self.show_sponge_state = !self.show_sponge_state, + ToggleWidget::Input => self.show_inputs = !self.show_inputs, + }; + } + + fn toggle_all_widgets(&mut self) { + let any_widget_is_shown = self.show_type_hints + || self.show_call_stack + || self.show_sponge_state + || self.show_inputs; + if any_widget_is_shown { + self.show_type_hints = false; + self.show_call_stack = false; + self.show_sponge_state = false; + self.show_inputs = false; + } else { + self.show_type_hints = true; + self.show_call_stack = true; + self.show_sponge_state = true; + self.show_inputs = true; + } + } + + fn distribute_area_for_widgets(&self, state: &TritonVMState, area: Rect) -> WidgetAreas { + let public_input_height = match self.maybe_render_public_input(state).is_some() { + true => Constraint::Min(2), + false => Constraint::Max(0), + }; + let secret_input_height = match self.maybe_render_secret_input(state).is_some() { + true => Constraint::Min(2), + false => Constraint::Max(0), + }; + let message_box_height = Constraint::Min(2); + let constraints = [ + Constraint::Percentage(100), + public_input_height, + secret_input_height, + message_box_height, + ]; + let layout = Layout::new(Direction::Vertical, constraints).split(area); + let state_area = layout[0]; + let public_input_area = layout[1]; + let secret_input_area = layout[2]; + let message_box_area = layout[3]; + + let op_stack_widget_width = Constraint::Min(30); + let remaining_width = Constraint::Percentage(100); + let sponge_state_width = match self.show_sponge_state { + true => Constraint::Min(32), + false => Constraint::Min(1), + }; + let state_layout_constraints = [op_stack_widget_width, remaining_width, sponge_state_width]; + let state_layout = + Layout::new(Direction::Horizontal, state_layout_constraints).split(state_area); + let op_stack_area = state_layout[0]; + let remaining_area = state_layout[1]; + let sponge_state_area = state_layout[2]; + + let nothing = Constraint::Max(0); + let third = Constraint::Ratio(1, 3); + let half = Constraint::Ratio(1, 2); + let everything = Constraint::Ratio(1, 1); + let hints_program_calls_constraints = match (self.show_type_hints, self.show_call_stack) { + (true, true) => [third, third, third], + (true, false) => [half, half, nothing], + (false, true) => [nothing, half, half], + (false, false) => [nothing, everything, nothing], + }; + let type_hint_program_and_call_stack_layout = + Layout::new(Direction::Horizontal, hints_program_calls_constraints) + .split(remaining_area); + + WidgetAreas { + op_stack: op_stack_area, + type_hint: type_hint_program_and_call_stack_layout[0], + program: type_hint_program_and_call_stack_layout[1], + call_stack: type_hint_program_and_call_stack_layout[2], + sponge: sponge_state_area, + public_input: public_input_area, + secret_input: secret_input_area, + message_box: message_box_area, + } + } + + fn render_op_stack_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + let op_stack = &render_info.state.vm_state.op_stack.stack; + let render_area = render_info.areas.op_stack; + + let stack_size = op_stack.len(); + let title = format!(" Stack (size: {stack_size:>4}) "); + let title = Title::from(title).alignment(Alignment::Left); + let num_padding_lines = (render_area.height as usize).saturating_sub(stack_size + 3); + let mut text = vec![Line::from(""); num_padding_lines]; + for (i, st) in op_stack.iter().rev().enumerate() { + let stack_index_style = match i { + i if i < OpStackElement::COUNT => Style::new().bold(), + _ => Style::new().dim(), + }; + let stack_index = Span::from(format!("{i:>3}")).set_style(stack_index_style); + let separator = Span::from(" "); + let stack_element = Span::from(format!("{st}")); + let line = Line::from(vec![stack_index, separator, stack_element]); + text.push(line); + } + + let border_set = symbols::border::Set { + bottom_left: symbols::line::ROUNDED.vertical_right, + ..symbols::border::ROUNDED + }; + let block = Block::default() + .padding(Padding::new(1, 1, 1, 0)) + .borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM) + .border_set(border_set) + .title(title); + let paragraph = Paragraph::new(text).block(block).alignment(Alignment::Left); + frame.render_widget(paragraph, render_area); + } + + fn render_type_hint_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + if !self.show_type_hints { + return; + } + let render_area = render_info.areas.type_hint; + let type_hints = &render_info.state.type_hints.stack; + + let highest_hint = type_hints.last().cloned().flatten(); + let lowest_hint = type_hints.first().cloned().flatten(); + + let num_padding_lines = (render_area.height as usize).saturating_sub(type_hints.len() + 3); + let mut text = vec![Line::from(""); num_padding_lines]; + + text.push(ElementTypeHint::render(&highest_hint).into()); + for (hint_0, hint_1, hint_2) in type_hints.iter().rev().tuple_windows() { + if ElementTypeHint::is_continuous_sequence(&[hint_0, hint_1, hint_2]) { + text.push("β‹…".dim().into()); + } else { + text.push(ElementTypeHint::render(hint_1).into()); + } + } + text.push(ElementTypeHint::render(&lowest_hint).into()); + + let block = Block::default() + .padding(Padding::new(0, 1, 1, 0)) + .borders(Borders::TOP | Borders::BOTTOM); + let paragraph = Paragraph::new(text).block(block).alignment(Alignment::Left); + frame.render_widget(paragraph, render_area); + } + + fn render_program_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + let state = &render_info.state; + let render_area = render_info.areas.program; + + let cycle_count = state.vm_state.cycle_count; + let title = format!(" Program (cycle: {cycle_count:>5}) "); + let title = Title::from(title).alignment(Alignment::Left); + + let address_width = self.address_render_width(state).max(2); + let mut address = 0; + let mut text = vec![]; + let instruction_pointer = state.vm_state.instruction_pointer; + let mut line_number_of_ip = 0; + let mut is_breakpoint = false; + for labelled_instruction in state.program.labelled_instructions() { + if labelled_instruction == LabelledInstruction::Breakpoint { + is_breakpoint = true; + continue; + } + if let LabelledInstruction::TypeHint(_) = labelled_instruction { + continue; + } + let ip_points_here = instruction_pointer == address + && matches!(labelled_instruction, LabelledInstruction::Instruction(_)); + if ip_points_here { + line_number_of_ip = text.len(); + } + let ip = match ip_points_here { + true => Span::from("β†’").bold(), + false => Span::from(" "), + }; + let mut gutter_item = match is_breakpoint { + true => format!("{:>address_width$} ", "πŸ”΄").into(), + false => format!(" {address:>address_width$} ").dim(), + }; + if let LabelledInstruction::Label(_) = labelled_instruction { + gutter_item = " ".into(); + } + let instruction = Span::from(format!("{labelled_instruction}")); + let line = Line::from(vec![ip, gutter_item, instruction]); + text.push(line); + if let LabelledInstruction::Instruction(instruction) = labelled_instruction { + address += instruction.size(); + } + is_breakpoint = false; + } + + let border_set = symbols::border::Set { + top_left: symbols::line::ROUNDED.horizontal_down, + bottom_left: symbols::line::ROUNDED.horizontal_up, + ..symbols::border::ROUNDED + }; + + let block = Block::default() + .padding(Padding::new(1, 1, 1, 0)) + .title(title) + .borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM) + .border_set(border_set); + let render_area_for_lines = block.inner(render_area).height; + let num_total_lines = text.len() as u16; + let num_lines_to_show_at_top = render_area_for_lines / 2; + let maximum_scroll_amount = num_total_lines.saturating_sub(render_area_for_lines); + let num_lines_to_scroll = (line_number_of_ip as u16) + .saturating_sub(num_lines_to_show_at_top) + .min(maximum_scroll_amount); + + let paragraph = Paragraph::new(text) + .block(block) + .scroll((num_lines_to_scroll, 0)); + frame.render_widget(paragraph, render_area); + } + + fn render_call_stack_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + if !self.show_call_stack { + return; + } + + let state = &render_info.state; + let jump_stack = &state.vm_state.jump_stack; + let render_area = render_info.areas.call_stack; + + let jump_stack_depth = jump_stack.len(); + let title = format!(" Calls (depth: {jump_stack_depth:>3}) "); + let title = Title::from(title).alignment(Alignment::Left); + + let num_padding_lines = (render_area.height as usize).saturating_sub(jump_stack_depth + 3); + let mut text = vec![Line::from(""); num_padding_lines]; + let address_width = self.address_render_width(state); + for (return_address, call_address) in jump_stack.iter().rev() { + let return_address = return_address.value(); + let call_address = call_address.value(); + let addresses = Span::from(format!( + "({return_address:>address_width$}, {call_address:>address_width$})" + )); + let separator = Span::from(" "); + let label = Span::from(state.program.label_for_address(call_address)); + let line = Line::from(vec![addresses, separator, label]); + text.push(line); + } + + let border_set = symbols::border::Set { + top_left: symbols::line::ROUNDED.horizontal_down, + bottom_left: symbols::line::ROUNDED.horizontal_up, + ..symbols::border::ROUNDED + }; + let block = Block::default() + .padding(Padding::new(1, 1, 1, 0)) + .title(title) + .borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM) + .border_set(border_set); + let paragraph = Paragraph::new(text).block(block).alignment(Alignment::Left); + frame.render_widget(paragraph, render_area); + } + + fn render_sponge_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + let title = Title::from(" Sponge "); + let border_set = symbols::border::Set { + top_left: symbols::line::ROUNDED.horizontal_down, + bottom_left: symbols::line::ROUNDED.horizontal_up, + bottom_right: symbols::line::ROUNDED.vertical_left, + ..symbols::border::ROUNDED + }; + let borders = match self.show_sponge_state { + true => Borders::ALL, + false => Borders::TOP | Borders::RIGHT | Borders::BOTTOM, + }; + let block = Block::default() + .borders(borders) + .border_set(border_set) + .title(title) + .padding(Padding::new(1, 1, 1, 0)); + + let render_area = render_info.areas.sponge; + let sponge_state = &render_info.state.vm_state.sponge_state; + let Some(state) = sponge_state else { + let paragraph = Paragraph::new("").block(block); + frame.render_widget(paragraph, render_area); + return; + }; + + let num_available_lines = block.inner(render_area).height as usize; + let num_padding_lines = num_available_lines.saturating_sub(state.len()); + let mut text = vec![Line::from(""); num_padding_lines]; + for (i, sp) in state.iter().enumerate() { + let sponge_index = Span::from(format!("{i:>3}")).dim(); + let separator = Span::from(" "); + let sponge_element = Span::from(format!("{sp}")); + let line = Line::from(vec![sponge_index, separator, sponge_element]); + text.push(line); + } + let paragraph = Paragraph::new(text).block(block).alignment(Alignment::Left); + frame.render_widget(paragraph, render_area); + } + + fn render_public_input_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + let public_input = self + .maybe_render_public_input(render_info.state) + .unwrap_or_default(); + + let border_set = symbols::border::Set { + bottom_left: symbols::line::ROUNDED.vertical_right, + bottom_right: symbols::line::ROUNDED.vertical_left, + ..symbols::border::ROUNDED + }; + let block = Block::default() + .padding(Padding::horizontal(1)) + .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) + .border_set(border_set); + let paragraph = Paragraph::new(public_input).block(block); + frame.render_widget(paragraph, render_info.areas.public_input); + } + + fn maybe_render_public_input(&self, state: &TritonVMState) -> Option { + if state.vm_state.public_input.is_empty() || !self.show_inputs { + return None; + } + let header = Span::from("Public input").bold(); + let colon = Span::from(": ["); + let input = state.vm_state.public_input.iter().join(", "); + let input = Span::from(input); + let footer = Span::from("]"); + Some(Line::from(vec![header, colon, input, footer])) + } + + fn render_secret_input_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + let secret_input = self + .maybe_render_secret_input(render_info.state) + .unwrap_or_default(); + + let border_set = symbols::border::Set { + bottom_left: symbols::line::ROUNDED.vertical_right, + bottom_right: symbols::line::ROUNDED.vertical_left, + ..symbols::border::ROUNDED + }; + let block = Block::default() + .padding(Padding::horizontal(1)) + .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) + .border_set(border_set); + let paragraph = Paragraph::new(secret_input).block(block); + frame.render_widget(paragraph, render_info.areas.secret_input); + } + + fn maybe_render_secret_input(&self, state: &TritonVMState) -> Option { + if state.vm_state.secret_individual_tokens.is_empty() || !self.show_inputs { + return None; + } + let header = Span::from("Secret input").bold(); + let colon = Span::from(": ["); + let input = state.vm_state.secret_individual_tokens.iter().join(", "); + let input = Span::from(input); + let footer = Span::from("]"); + Some(Line::from(vec![header, colon, input, footer])) + } + + fn render_message_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + let message = self.message(render_info.state); + let status = match render_info.state.vm_state.halting { + true => Title::from(" HALT ".bold().green()), + false => Title::default(), + }; + + let block = Block::default() + .padding(Padding::horizontal(1)) + .title(status) + .title_position(Position::Bottom) + .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) + .border_type(BorderType::Rounded); + let paragraph = Paragraph::new(message).block(block); + frame.render_widget(paragraph, render_info.areas.message_box); + } + + fn message(&self, state: &TritonVMState) -> Line { + if let Some(error_message) = self.maybe_render_error_message(state) { + return error_message; + } + if let Some(warning_message) = self.maybe_render_warning_message(state) { + return warning_message; + } + if let Some(public_output) = self.maybe_render_public_output(state) { + return public_output; + } + self.render_welcome_message() + } + + fn maybe_render_error_message(&self, state: &TritonVMState) -> Option { + let error = "ERROR".bold().red(); + let colon = ": ".into(); + let message = state.error?.to_string().into(); + Some(Line::from(vec![error, colon, message])) + } + + fn maybe_render_warning_message(&self, state: &TritonVMState) -> Option { + let Some(ref message) = state.warning else { + return None; + }; + let warning = "WARNING".bold().yellow(); + let colon = ": ".into(); + let message = message.to_string().into(); + Some(Line::from(vec![warning, colon, message])) + } + + fn maybe_render_public_output(&self, state: &TritonVMState) -> Option { + if state.vm_state.public_output.is_empty() { + return None; + } + let header = Span::from("Public output").bold(); + let colon = Span::from(": ["); + let output = state.vm_state.public_output.iter().join(", "); + let output = Span::from(output); + let footer = Span::from("]"); + Some(Line::from(vec![header, colon, output, footer])) + } + + fn render_welcome_message(&self) -> Line { + let welcome = "Welcome to the Triton VM TUI! ".into(); + let help_hint = "Press `h` for help.".dim(); + Line::from(vec![welcome, help_hint]) + } +} + +impl Component for Home { + fn update(&mut self, action: Action) -> Result> { + if let Action::Toggle(toggle) = action { + self.toggle_widget(toggle); + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame<'_>, state: &TritonVMState) -> Result<()> { + let widget_areas = self.distribute_area_for_widgets(state, frame.size()); + let render_info = RenderInfo { + state, + areas: widget_areas, + }; + + self.render_op_stack_widget(frame, render_info); + self.render_type_hint_widget(frame, render_info); + self.render_program_widget(frame, render_info); + self.render_call_stack_widget(frame, render_info); + self.render_sponge_widget(frame, render_info); + self.render_public_input_widget(frame, render_info); + self.render_secret_input_widget(frame, render_info); + self.render_message_widget(frame, render_info); + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +struct RenderInfo<'s> { + state: &'s TritonVMState, + areas: WidgetAreas, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct WidgetAreas { + op_stack: Rect, + type_hint: Rect, + program: Rect, + call_stack: Rect, + sponge: Rect, + public_input: Rect, + secret_input: Rect, + message_box: Rect, +} + +#[cfg(test)] +mod tests { + use proptest_arbitrary_interop::arb; + use ratatui::backend::TestBackend; + use test_strategy::proptest; + + use triton_vm::vm::VMState; + use triton_vm::BFieldElement; + use triton_vm::NonDeterminism; + use triton_vm::Program; + use triton_vm::PublicInput; + + use super::*; + + #[proptest] + fn render_arbitrary_vm_state( + #[strategy(arb())] mut home: Home, + #[strategy(arb())] program: Program, + #[strategy(arb())] public_input: PublicInput, + #[strategy(arb())] non_determinism: NonDeterminism, + ) { + let mut state = TritonVMState::new(&Default::default()).unwrap(); + state.vm_state = VMState::new(&program, public_input, non_determinism); + state.program = program; + + let backend = TestBackend::new(150, 50); + let mut terminal = Terminal::new(backend)?; + terminal.draw(|f| home.draw(f, &state).unwrap()).unwrap(); + } +} diff --git a/triton-tui/src/components/memory.rs b/triton-tui/src/components/memory.rs new file mode 100644 index 000000000..6eea4f04f --- /dev/null +++ b/triton-tui/src/components/memory.rs @@ -0,0 +1,343 @@ +use color_eyre::eyre::Result; +use crossterm::event::KeyEventKind::Release; +use crossterm::event::*; +use num_traits::One; +use ratatui::prelude::*; +use ratatui::widgets::*; +use ratatui::Frame; +use tui_textarea::TextArea; + +use triton_vm::instruction::Instruction; +use triton_vm::BFieldElement; + +use crate::action::Action; +use crate::action::ExecutedInstruction; +use crate::components::Component; +use crate::element_type_hint::ElementTypeHint; +use crate::triton_vm_state::TritonVMState; +use crate::tui::Event; + +#[derive(Debug, Clone)] +pub(crate) struct Memory<'a> { + /// The address touched last by any `read_mem` or `write_mem` instruction. + pub most_recent_address: BFieldElement, + + /// The address to show. Can be manually set (and unset) by the user. + pub user_address: Option, + + pub text_area: TextArea<'a>, + pub text_area_in_focus: bool, + + pub undo_stack: Vec, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct UndoInformation { + pub most_recent_address: BFieldElement, +} + +#[derive(Debug, Clone, Copy)] +struct RenderInfo<'s> { + state: &'s TritonVMState, + areas: WidgetAreas, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct WidgetAreas { + memory: Rect, + text_input: Rect, +} + +impl<'a> Default for Memory<'a> { + fn default() -> Self { + Self { + most_recent_address: 0_u64.into(), + user_address: None, + text_area: Self::initial_text_area(), + undo_stack: vec![], + text_area_in_focus: false, + } + } +} + +impl<'a> Memory<'a> { + fn initial_text_area() -> TextArea<'a> { + let mut text_area = TextArea::default(); + text_area.set_cursor_line_style(Style::default()); + text_area + } + + pub fn undo(&mut self) { + let Some(undo_information) = self.undo_stack.pop() else { + return; + }; + + self.most_recent_address = undo_information.most_recent_address; + } + + pub fn record_undo_information(&mut self) { + let undo_information = UndoInformation { + most_recent_address: self.most_recent_address, + }; + self.undo_stack.push(undo_information); + } + + pub fn reset(&mut self) { + self.most_recent_address = 0_u64.into(); + self.user_address = None; + self.undo_stack.clear(); + } + + pub fn handle_instruction(&mut self, executed_instruction: ExecutedInstruction) { + let presumed_ram_pointer = executed_instruction.new_top_of_stack[0]; + let overshoot_adjustment = match executed_instruction.instruction { + Instruction::ReadMem(_) => BFieldElement::one(), + Instruction::WriteMem(_) => -BFieldElement::one(), + _ => return, + }; + let last_ram_pointer = presumed_ram_pointer + overshoot_adjustment; + self.most_recent_address = last_ram_pointer; + } + + fn submit_address(&mut self) { + let user_input = self.text_area.lines()[0].trim(); + let Ok(address) = user_input.parse::() else { + self.user_address = None; + return; + }; + + let modulus = BFieldElement::P as i128; + if address < -modulus || modulus <= address { + self.user_address = None; + return; + } + let address = (address + modulus) % modulus; + let address = BFieldElement::from(address as u64); + self.user_address = Some(address); + } + + fn requested_address(&self) -> BFieldElement { + self.user_address.unwrap_or(self.most_recent_address) + } + + fn paste(&mut self, s: &str) { + self.text_area_in_focus = true; + let s = s.replace(['\r', '\n'], ""); + self.text_area.insert_str(s); + } + + fn distribute_area_for_widgets(&self, area: Rect) -> WidgetAreas { + let text_area_height = Constraint::Min(2); + let constraints = [Constraint::Percentage(100), text_area_height]; + let layout = Layout::new(Direction::Vertical, constraints).split(area); + + WidgetAreas { + memory: layout[0], + text_input: layout[1], + } + } + + fn render_memory_widget(&self, frame: &mut Frame<'_>, render_info: RenderInfo) { + let block = Self::memory_widget_block(); + let draw_area = render_info.areas.memory; + + let num_lines = block.inner(draw_area).height as u64; + let address_range_start = self.requested_address() - BFieldElement::from(num_lines / 2); + let address_range_end = address_range_start + BFieldElement::from(num_lines); + + let mut text = vec![]; + let mut address = address_range_start; + while address != address_range_end { + let memory_cell = self.render_memory_cell_at_address(render_info, address); + let separator = vec![Span::from(" ")]; + let type_hint = Self::render_type_hint_at_address(render_info, address); + + text.push(Line::from([memory_cell, separator, type_hint].concat())); + address.increment(); + } + + let paragraph = Paragraph::new(text).block(block); + frame.render_widget(paragraph, draw_area); + } + + fn render_memory_cell_at_address( + &self, + render_info: RenderInfo, + address: BFieldElement, + ) -> Vec { + let address_style = match address == self.requested_address() { + true => Style::new().bold(), + false => Style::new().dim(), + }; + + let maybe_value = render_info.state.vm_state.ram.get(&address); + let value = maybe_value.copied().unwrap_or(0_u64.into()); + + // additional `.to_string()` to circumvent padding bug (?) in `format` + let address = Span::from(format!("{address: >20}", address = address.to_string())); + let address = address.set_style(address_style); + let separator = Span::from(" "); + let value = Span::from(format!("{value: <20}", value = value.to_string())); + + vec![address, separator, value] + } + + fn render_type_hint_at_address(render_info: RenderInfo, address: BFieldElement) -> Vec { + let prev_address = address - BFieldElement::one(); + let next_address = address + BFieldElement::one(); + + let shadow_ram = &render_info.state.type_hints.ram; + let prev_hint = shadow_ram.get(&prev_address).unwrap_or(&None); + let curr_hint = shadow_ram.get(&address).unwrap_or(&None); + let next_hint = shadow_ram.get(&next_address).unwrap_or(&None); + + if ElementTypeHint::is_continuous_sequence(&[prev_hint, curr_hint, next_hint]) { + vec!["β‹…".dim()] + } else { + ElementTypeHint::render(curr_hint) + } + } + + fn render_text_input_widget(&mut self, frame: &mut Frame<'_>, render_info: RenderInfo) { + let placeholder_text = match self.text_area_in_focus { + true => "", + false => "Go to address. Empty for most recent read / write.", + }; + self.text_area.set_placeholder_text(placeholder_text); + + let cursor_style = match self.text_area_in_focus { + true => Style::default().add_modifier(Modifier::REVERSED), + false => Style::default(), + }; + self.text_area.set_cursor_style(cursor_style); + + let text_style = match self.text_area_in_focus { + true => Style::default(), + false => Style::default().dim(), + }; + self.text_area.set_style(text_style); + + let block = Self::text_input_block(); + self.text_area.set_block(block); + frame.render_widget(self.text_area.widget(), render_info.areas.text_input); + } + + fn memory_widget_block() -> Block<'a> { + let border_set = symbols::border::Set { + bottom_left: symbols::line::ROUNDED.vertical_right, + bottom_right: symbols::line::ROUNDED.vertical_left, + ..symbols::border::ROUNDED + }; + Block::default() + .padding(Padding::new(1, 1, 1, 0)) + .borders(Borders::ALL) + .border_set(border_set) + .title(" Random Access Memory ") + } + + fn text_input_block() -> Block<'a> { + Block::default() + .padding(Padding::horizontal(1)) + .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) + .border_type(BorderType::Rounded) + } +} + +impl<'a> Component for Memory<'a> { + fn request_exclusive_key_event_handling(&self) -> bool { + self.text_area_in_focus + } + + fn handle_event(&mut self, event: Option) -> Result> { + let Some(event) = event else { + return Ok(None); + }; + + if let Event::Paste(ref s) = event { + self.paste(s); + } + + let response = match event { + Event::Key(key_event) => self.handle_key_event(key_event)?, + Event::Mouse(mouse_event) => self.handle_mouse_event(mouse_event)?, + _ => None, + }; + Ok(response) + } + + fn handle_key_event(&mut self, key_event: KeyEvent) -> Result> { + if key_event.kind == Release { + return Ok(None); + } + if key_event.code == KeyCode::Esc { + self.text_area_in_focus = false; + return Ok(None); + } + if key_event.code == KeyCode::Enter { + if self.text_area_in_focus { + self.submit_address(); + } + self.text_area_in_focus = !self.text_area_in_focus; + return Ok(None); + } + if self.text_area_in_focus { + self.text_area.input(key_event); + } + Ok(None) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Mode(_) => self.text_area_in_focus = false, + Action::Undo => self.undo(), + Action::RecordUndoInfo => self.record_undo_information(), + Action::Reset => self.reset(), + Action::ExecutedInstruction(instruction) => self.handle_instruction(*instruction), + _ => (), + } + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame<'_>, state: &TritonVMState) -> Result<()> { + let widget_areas = self.distribute_area_for_widgets(frame.size()); + let render_info = RenderInfo { + state, + areas: widget_areas, + }; + + self.render_memory_widget(frame, render_info); + self.render_text_input_widget(frame, render_info); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use proptest_arbitrary_interop::arb; + use ratatui::backend::TestBackend; + use test_strategy::proptest; + + use triton_vm::vm::VMState; + use triton_vm::BFieldElement; + use triton_vm::NonDeterminism; + use triton_vm::Program; + use triton_vm::PublicInput; + + use super::*; + + #[proptest] + fn render_arbitrary_vm_state( + #[strategy(arb())] program: Program, + #[strategy(arb())] public_input: PublicInput, + #[strategy(arb())] non_determinism: NonDeterminism, + ) { + let mut memory = Memory::default(); + let mut state = TritonVMState::new(&Default::default()).unwrap(); + state.vm_state = VMState::new(&program, public_input, non_determinism); + state.program = program; + + let backend = TestBackend::new(150, 50); + let mut terminal = Terminal::new(backend)?; + terminal.draw(|f| memory.draw(f, &state).unwrap()).unwrap(); + } +} diff --git a/triton-tui/src/config.rs b/triton-tui/src/config.rs new file mode 100644 index 000000000..ff06bea66 --- /dev/null +++ b/triton-tui/src/config.rs @@ -0,0 +1,546 @@ +use std::collections::HashMap; + +use color_eyre::eyre::Result; +use config::ConfigError; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyModifiers; +use derive_deref::Deref; +use derive_deref::DerefMut; +use ratatui::style::Color; +use ratatui::style::Modifier; +use ratatui::style::Style; +use serde::de::Deserializer; +use serde::de::Error; +use serde::Deserialize; +use tracing::error; +use tracing::info; + +use crate::action::Action; +use crate::mode::Mode; +use crate::utils::*; + +const DEFAULT_CONFIG: &str = include_str!("../.config/default_config.json"); + +#[derive(Clone, Debug, Default, Deserialize)] +pub(crate) struct Config { + #[serde(default)] + pub keybindings: KeyBindings, + + #[serde(default)] + pub styles: Styles, +} + +impl Config { + pub fn new() -> Result { + let default_config = serde_json::from_str(DEFAULT_CONFIG).map_err(|e| { + let error = format!("Unable to parse default config: {e}"); + error!(error); + ConfigError::custom(error) + })?; + + let mut cfg = Self::aggregate_config_from_various_locations()?; + cfg.add_missing_keybindings_using_defaults(&default_config); + cfg.add_missing_styles_using_defaults(&default_config); + + Ok(cfg) + } + + fn aggregate_config_from_various_locations() -> Result { + let data_dir = get_data_dir(); + let config_dir = get_config_dir(); + let mut config_builder = config::Config::builder() + .set_default("_data_dir", data_dir.to_str().unwrap())? + .set_default("_config_dir", config_dir.to_str().unwrap())?; + + let config_files = [ + ("config.json", config::FileFormat::Json), + ("config.yaml", config::FileFormat::Yaml), + ("config.toml", config::FileFormat::Toml), + ("config.ini", config::FileFormat::Ini), + ]; + for (file, format) in config_files { + let config_path = config_dir.join(file); + if config_path.exists() { + info!("Adding configuration file: {}", config_path.display()); + let config_file = config::File::from(config_path) + .format(format) + .required(false); + config_builder = config_builder.add_source(config_file); + } else { + info!("Configuration file not found: {}", config_path.display()); + } + } + + config_builder.build()?.try_deserialize() + } + + fn add_missing_keybindings_using_defaults(&mut self, default_config: &Self) { + for (&mode, default_bindings) in default_config.keybindings.iter() { + let user_bindings = self.keybindings.entry(mode).or_default(); + for (key, cmd) in default_bindings.iter() { + user_bindings + .entry(key.clone()) + .or_insert_with(|| cmd.clone()); + } + } + } + + fn add_missing_styles_using_defaults(&mut self, default_config: &Self) { + for (&mode, default_styles) in default_config.styles.iter() { + let user_styles = self.styles.entry(mode).or_default(); + for (style_key, &style) in default_styles.iter() { + user_styles + .entry(style_key.clone()) + .or_insert_with(|| style); + } + } + } +} + +pub(crate) type KeyEvents = Vec; + +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub(crate) struct KeyBindings(pub HashMap>); + +impl<'de> Deserialize<'de> for KeyBindings { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let parsed_map = HashMap::>::deserialize(deserializer)?; + + let mut keybindings = HashMap::new(); + for (mode, key_str_to_action_map) in parsed_map { + let mut key_events_to_action_map = HashMap::new(); + for (key_str, action) in key_str_to_action_map { + let key_events = parse_key_sequence(&key_str) + .map_err(|e| Error::custom(format!("Unable to parse `{key_str}`: {e}")))?; + key_events_to_action_map.insert(key_events, action); + } + keybindings.insert(mode, key_events_to_action_map); + } + Ok(KeyBindings(keybindings)) + } +} + +fn parse_key_event(raw: &str) -> Result { + let raw_lower = raw.to_ascii_lowercase(); + let (remaining, modifiers) = extract_modifiers(&raw_lower); + parse_key_code_with_modifiers(remaining, modifiers) +} + +fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { + let mut modifiers = KeyModifiers::empty(); + let mut current = raw; + + loop { + match current { + rest if rest.starts_with("ctrl-") => { + modifiers.insert(KeyModifiers::CONTROL); + current = &rest[5..]; + } + rest if rest.starts_with("alt-") => { + modifiers.insert(KeyModifiers::ALT); + current = &rest[4..]; + } + rest if rest.starts_with("shift-") => { + modifiers.insert(KeyModifiers::SHIFT); + current = &rest[6..]; + } + _ => break, + }; + } + + (current, modifiers) +} + +fn parse_key_code_with_modifiers( + raw: &str, + mut modifiers: KeyModifiers, +) -> Result { + let c = match raw { + "esc" => KeyCode::Esc, + "enter" => KeyCode::Enter, + "left" => KeyCode::Left, + "right" => KeyCode::Right, + "up" => KeyCode::Up, + "down" => KeyCode::Down, + "home" => KeyCode::Home, + "end" => KeyCode::End, + "pageup" => KeyCode::PageUp, + "pagedown" => KeyCode::PageDown, + "backtab" => { + modifiers.insert(KeyModifiers::SHIFT); + KeyCode::BackTab + } + "backspace" => KeyCode::Backspace, + "delete" => KeyCode::Delete, + "insert" => KeyCode::Insert, + "f1" => KeyCode::F(1), + "f2" => KeyCode::F(2), + "f3" => KeyCode::F(3), + "f4" => KeyCode::F(4), + "f5" => KeyCode::F(5), + "f6" => KeyCode::F(6), + "f7" => KeyCode::F(7), + "f8" => KeyCode::F(8), + "f9" => KeyCode::F(9), + "f10" => KeyCode::F(10), + "f11" => KeyCode::F(11), + "f12" => KeyCode::F(12), + "space" => KeyCode::Char(' '), + "hyphen" => KeyCode::Char('-'), + "minus" => KeyCode::Char('-'), + "tab" => KeyCode::Tab, + c if c.len() == 1 => { + let mut c = c.chars().next().unwrap(); + if modifiers.contains(KeyModifiers::SHIFT) { + c = c.to_ascii_uppercase(); + } + KeyCode::Char(c) + } + _ => return Err(format!("Unable to parse {raw}")), + }; + Ok(KeyEvent::new(c, modifiers)) +} + +pub fn _key_event_to_string(key_event: &KeyEvent) -> String { + let char; + let key_code = match key_event.code { + KeyCode::Backspace => "backspace", + KeyCode::Enter => "enter", + KeyCode::Left => "left", + KeyCode::Right => "right", + KeyCode::Up => "up", + KeyCode::Down => "down", + KeyCode::Home => "home", + KeyCode::End => "end", + KeyCode::PageUp => "pageup", + KeyCode::PageDown => "pagedown", + KeyCode::Tab => "tab", + KeyCode::BackTab => "backtab", + KeyCode::Delete => "delete", + KeyCode::Insert => "insert", + KeyCode::F(c) => { + char = format!("f({c})"); + &char + } + KeyCode::Char(' ') => "space", + KeyCode::Char(c) => { + char = c.to_string(); + &char + } + KeyCode::Esc => "esc", + KeyCode::Null => "", + KeyCode::CapsLock => "", + KeyCode::Menu => "", + KeyCode::ScrollLock => "", + KeyCode::Media(_) => "", + KeyCode::NumLock => "", + KeyCode::PrintScreen => "", + KeyCode::Pause => "", + KeyCode::KeypadBegin => "", + KeyCode::Modifier(_) => "", + }; + + let mut modifiers = Vec::with_capacity(3); + + if key_event.modifiers.intersects(KeyModifiers::CONTROL) { + modifiers.push("ctrl"); + } + + if key_event.modifiers.intersects(KeyModifiers::SHIFT) { + modifiers.push("shift"); + } + + if key_event.modifiers.intersects(KeyModifiers::ALT) { + modifiers.push("alt"); + } + + let mut key = modifiers.join("-"); + + if !key.is_empty() { + key.push('-'); + } + key.push_str(key_code); + + key +} + +fn parse_key_sequence(raw: &str) -> Result { + let raw = raw + .strip_prefix('<') + .ok_or_else(|| format!("Missing `<` in `{raw}`"))?; + let raw = raw + .strip_suffix('>') + .ok_or_else(|| format!("Missing `>` in `{raw}`"))?; + let sequences = raw.split("><"); + + sequences.map(parse_key_event).collect() +} + +#[derive(Clone, Debug, Default, Deref, DerefMut)] +pub(crate) struct Styles(pub HashMap>); + +impl<'de> Deserialize<'de> for Styles { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let parsed_map = HashMap::>::deserialize(deserializer)?; + + let styles = parsed_map + .into_iter() + .map(|(mode, inner_map)| { + let converted_inner_map = inner_map + .into_iter() + .map(|(str, style)| (str, parse_style(&style))) + .collect(); + (mode, converted_inner_map) + }) + .collect(); + + Ok(Styles(styles)) + } +} + +pub fn parse_style(line: &str) -> Style { + let fg_bg_splitpoint = line.to_lowercase().find("on ").unwrap_or(line.len()); + let (fg, bg) = line.split_at(fg_bg_splitpoint); + let (fg_color, fg_modifiers) = process_color_string(fg); + let (bg_color, bg_modifiers) = process_color_string(&bg.replace("on ", "")); + + let mut style = Style::default(); + if let Some(fg) = parse_color(&fg_color) { + style = style.fg(fg); + } + if let Some(bg) = parse_color(&bg_color) { + style = style.bg(bg); + } + style = style.add_modifier(fg_modifiers | bg_modifiers); + style +} + +fn process_color_string(color_str: &str) -> (String, Modifier) { + let color = color_str + .replace("grey", "gray") + .replace("bright ", "") + .replace("bold ", "") + .replace("underline ", "") + .replace("inverse ", ""); + + let mut modifiers = Modifier::empty(); + if color_str.contains("underline") { + modifiers |= Modifier::UNDERLINED; + } + if color_str.contains("bold") { + modifiers |= Modifier::BOLD; + } + if color_str.contains("inverse") { + modifiers |= Modifier::REVERSED; + } + + (color, modifiers) +} + +fn parse_color(s: &str) -> Option { + let s = s.trim_start(); + let s = s.trim_end(); + match s { + s if s.contains("bright color") => { + let s = s.trim_start_matches("bright "); + let c = parse_color_as_color_index(s); + Some(Color::Indexed(c.wrapping_shl(8))) + } + s if s.contains("color") => Some(Color::Indexed(parse_color_as_color_index(s))), + s if s.contains("gray") => { + let s = s.trim_start_matches("gray"); + let c = s.parse::().unwrap_or_default(); + Some(Color::Indexed(232 + c)) + } + s if s.contains("rgb") => { + let s = s.trim_start_matches("rgb"); + let red = (s.as_bytes()[0] as char).to_digit(10).unwrap_or_default() as u8; + let green = (s.as_bytes()[1] as char).to_digit(10).unwrap_or_default() as u8; + let blue = (s.as_bytes()[2] as char).to_digit(10).unwrap_or_default() as u8; + let c = 16 + red * 36 + green * 6 + blue; + Some(Color::Indexed(c)) + } + "black" => Some(Color::Indexed(0)), + "red" => Some(Color::Indexed(1)), + "green" => Some(Color::Indexed(2)), + "yellow" => Some(Color::Indexed(3)), + "blue" => Some(Color::Indexed(4)), + "magenta" => Some(Color::Indexed(5)), + "cyan" => Some(Color::Indexed(6)), + "white" => Some(Color::Indexed(7)), + "bold black" => Some(Color::Indexed(8)), + "bold red" => Some(Color::Indexed(9)), + "bold green" => Some(Color::Indexed(10)), + "bold yellow" => Some(Color::Indexed(11)), + "bold blue" => Some(Color::Indexed(12)), + "bold magenta" => Some(Color::Indexed(13)), + "bold cyan" => Some(Color::Indexed(14)), + "bold white" => Some(Color::Indexed(15)), + _ => None, + } +} + +fn parse_color_as_color_index(s: &str) -> u8 { + let maybe_color_index = s.trim_start_matches("color").parse(); + maybe_color_index.unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use assert2::assert; + + use super::*; + + #[test] + fn parse_default_style() { + let style = parse_style(""); + assert!(Style::default() == style); + } + + #[test] + fn parse_foreground_style() { + let style = parse_style("red"); + let red = Some(Color::Indexed(1)); + assert!(red == style.fg); + } + + #[test] + fn parse_background_style() { + let style = parse_style("on blue"); + let blue = Some(Color::Indexed(4)); + assert!(blue == style.bg); + } + + #[test] + fn parse_style_modifiers() { + let style = parse_style("underline red on blue"); + let red = Some(Color::Indexed(1)); + let blue = Some(Color::Indexed(4)); + assert!(red == style.fg); + assert!(blue == style.bg); + } + + #[test] + fn parse_style_modifiers_with_multiple_backgrounds() { + let style = parse_style("bold green on blue on underline red"); + let green = Some(Color::Indexed(2)); + assert!(green == style.fg); + assert!(None == style.bg); + } + + #[test] + fn parse_color_string() { + let (color, modifiers) = process_color_string("underline bold inverse gray"); + assert!("gray" == color); + assert!(modifiers.contains(Modifier::UNDERLINED)); + assert!(modifiers.contains(Modifier::BOLD)); + assert!(modifiers.contains(Modifier::REVERSED)); + } + + #[test] + fn parse_rgb_color() { + let color = parse_color("rgb123"); + let digits = [1, 2, 3]; + + let num_well_known_ansi_colors = 16; + let ansi_color_resolution = 6_u8; + let expected = digits + .into_iter() + .rev() + .enumerate() + .map(|(index, digit)| digit * ansi_color_resolution.pow(index as u32)) + .sum::() + + num_well_known_ansi_colors; + assert!(color == Some(Color::Indexed(expected))); + } + + #[test] + fn parse_unknown_color() { + let no_color = parse_color("unknown"); + assert!(None == no_color); + } + + #[test] + fn quit_from_default_config() -> Result<()> { + let c = Config::new()?; + let quitting_key_sequence = parse_key_sequence("").unwrap_or_default(); + let mode_home_key_to_event_map = c.keybindings.get(&Mode::Home).unwrap(); + let quitting_action = mode_home_key_to_event_map + .get(&quitting_key_sequence) + .unwrap(); + assert!(&Action::Quit == quitting_action); + Ok(()) + } + + #[test] + fn parse_keys_without_modifiers() { + let empty_modifiers = KeyModifiers::empty(); + let key_code_a = KeyCode::Char('a'); + let key_event_a = KeyEvent::new(key_code_a, empty_modifiers); + assert!(key_event_a == parse_key_event("a").unwrap()); + + let key_event_enter = KeyEvent::new(KeyCode::Enter, empty_modifiers); + assert!(key_event_enter == parse_key_event("enter").unwrap()); + + let key_event_esc = KeyEvent::new(KeyCode::Esc, empty_modifiers); + assert!(key_event_esc == parse_key_event("esc").unwrap()); + } + + #[test] + fn parse_keys_with_modifiers() { + let ctrl_a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL); + assert!(ctrl_a == parse_key_event("ctrl-a").unwrap()); + + let alt_enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT); + assert!(alt_enter == parse_key_event("alt-enter").unwrap()); + + let shift_esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::SHIFT); + assert!(shift_esc == parse_key_event("shift-esc").unwrap()); + } + + #[test] + fn parse_keys_with_multiple_modifiers() { + let ctrl_alt = KeyModifiers::CONTROL | KeyModifiers::ALT; + let ctr_alt_a = KeyEvent::new(KeyCode::Char('a'), ctrl_alt); + assert!(ctr_alt_a == parse_key_event("ctrl-alt-a").unwrap()); + + let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT; + let ctr_shift_enter = KeyEvent::new(KeyCode::Enter, ctrl_shift); + assert_eq!( + ctr_shift_enter, + parse_key_event("ctrl-shift-enter").unwrap() + ); + } + + #[test] + fn stringify_key_event() { + let ctrl_alt = KeyModifiers::CONTROL | KeyModifiers::ALT; + let ctrl_alt_a = KeyEvent::new(KeyCode::Char('a'), ctrl_alt); + let generated_string = _key_event_to_string(&ctrl_alt_a); + + let expected = "ctrl-alt-a".to_string(); + assert!(expected == generated_string); + } + + #[test] + fn parsing_invalid_keys_gives_error() { + assert!(let Err(_) = parse_key_event("invalid-key")); + assert!(let Err(_) = parse_key_event("ctrl-invalid-key")); + } + + #[test] + fn key_parsing_is_case_insensitive() { + let ctrl_a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL); + assert!(ctrl_a == parse_key_event("CTRL-a").unwrap()); + + let alt_enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT); + assert!(alt_enter == parse_key_event("AlT-eNtEr").unwrap()); + } +} diff --git a/triton-tui/src/element_type_hint.rs b/triton-tui/src/element_type_hint.rs new file mode 100644 index 000000000..3b697ce15 --- /dev/null +++ b/triton-tui/src/element_type_hint.rs @@ -0,0 +1,185 @@ +use std::cmp::Ordering; + +use arbitrary::Arbitrary; +use itertools::Itertools; +use ratatui::prelude::*; + +/// A hint about the type of a single stack element. Helps debugging programs written for Triton VM. +/// **Does not enforce types.** +#[derive(Debug, Clone, PartialEq, Eq, Hash, Arbitrary)] +pub(crate) struct ElementTypeHint { + /// The name of the type. See [`TypeHint`][type_hint] for details. + /// + /// [type_hint]: TypeHint + pub type_name: Option, + + /// The name of the variable. See [`TypeHint`][type_hint] for details. + /// + /// [type_hint]: TypeHint + pub variable_name: String, + + /// The index of the element within the type. For example, if the type is `Digest`, then this + /// could be `0` for the first element, `1` for the second element, and so on. + /// + /// Does not apply to types that are not composed of multiple [`BFieldElement`][bfe]s, like `u32` or + /// [`BFieldElement`][bfe] itself. + /// + /// [bfe]: triton_vm::BFieldElement + pub index: Option, +} + +impl ElementTypeHint { + pub fn is_continuous_sequence(sequence: &[&Option]) -> bool { + Self::is_continuous_sequence_for_ordering(sequence, Ordering::Greater) + || Self::is_continuous_sequence_for_ordering(sequence, Ordering::Less) + } + + fn is_continuous_sequence_for_ordering(sequence: &[&Option], ordering: Ordering) -> bool { + for (left, right) in sequence.iter().tuple_windows() { + let (Some(left), Some(right)) = (left, right) else { + return false; + }; + if left.partial_cmp(right) != Some(ordering) { + return false; + } + } + true + } + + pub fn render(maybe_self: &Option) -> Vec { + let Some(element_type_hint) = maybe_self else { + return vec![]; + }; + + let mut line = vec![]; + line.push(element_type_hint.variable_name.clone().into()); + if let Some(ref type_name) = element_type_hint.type_name { + line.push(": ".dim()); + line.push(type_name.into()); + } + if let Some(index) = element_type_hint.index { + line.push(format!(" ({index})").dim()); + } + line + } +} + +impl PartialOrd for ElementTypeHint { + fn partial_cmp(&self, other: &Self) -> Option { + if self.variable_name != other.variable_name { + return None; + } + if self.type_name != other.type_name { + return None; + } + match (self.index, other.index) { + (Some(self_index), Some(other_index)) => self_index.partial_cmp(&other_index), + (None, None) => Some(Ordering::Equal), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + use proptest_arbitrary_interop::arb; + use test_strategy::proptest; + + use super::*; + + #[proptest] + fn comparison_with_different_variable_names_is_impossible( + #[strategy(arb())] type_hint_0: ElementTypeHint, + #[strategy(arb())] + #[filter(#type_hint_0.variable_name != #type_hint_1.variable_name)] + type_hint_1: ElementTypeHint, + ) { + prop_assert_eq!(type_hint_0.partial_cmp(&type_hint_1), None); + } + + #[proptest] + fn comparison_with_different_type_names_is_impossible( + #[strategy(arb())] type_hint_0: ElementTypeHint, + #[strategy(arb())] + #[filter(#type_hint_0.type_name != #type_hint_1.type_name)] + type_hint_1: ElementTypeHint, + ) { + prop_assert_eq!(type_hint_0.partial_cmp(&type_hint_1), None); + } + + #[test] + fn continuous_increasing_sequence() { + let template = ElementTypeHint { + type_name: None, + variable_name: "x".to_string(), + index: None, + }; + let mut hint_0 = template.clone(); + let mut hint_1 = template.clone(); + let mut hint_2 = template.clone(); + + hint_0.index = Some(0); + hint_1.index = Some(1); + hint_2.index = Some(2); + + let sequence = [&Some(hint_0), &Some(hint_1), &Some(hint_2)]; + assert!(ElementTypeHint::is_continuous_sequence(&sequence)); + } + + #[test] + fn continuous_decreasing_sequence() { + let template = ElementTypeHint { + type_name: None, + variable_name: "x".to_string(), + index: None, + }; + let mut hint_0 = template.clone(); + let mut hint_1 = template.clone(); + let mut hint_2 = template.clone(); + + hint_0.index = Some(2); + hint_1.index = Some(1); + hint_2.index = Some(0); + + let sequence = [&Some(hint_0), &Some(hint_1), &Some(hint_2)]; + assert!(ElementTypeHint::is_continuous_sequence(&sequence)); + } + + #[test] + fn non_continuous_sequence() { + let template = ElementTypeHint { + type_name: None, + variable_name: "x".to_string(), + index: None, + }; + let mut hint_0 = template.clone(); + let mut hint_1 = template.clone(); + let mut hint_2 = template.clone(); + + hint_0.index = Some(0); + hint_1.index = Some(2); + hint_2.index = Some(1); + + let sequence = [&Some(hint_0), &Some(hint_1), &Some(hint_2)]; + assert!(!ElementTypeHint::is_continuous_sequence(&sequence)); + } + + #[test] + fn interrupted_sequence() { + let template = ElementTypeHint { + type_name: None, + variable_name: "x".to_string(), + index: None, + }; + + let mut hint_0 = template.clone(); + let mut hint_2 = template.clone(); + + hint_0.index = Some(0); + hint_2.index = Some(2); + + let sequence = [&Some(hint_0), &None, &Some(hint_2)]; + assert!(!ElementTypeHint::is_continuous_sequence(&sequence)); + } +} diff --git a/triton-tui/src/main.rs b/triton-tui/src/main.rs new file mode 100644 index 000000000..13a9999aa --- /dev/null +++ b/triton-tui/src/main.rs @@ -0,0 +1,36 @@ +use clap::Parser; +use color_eyre::eyre::Result; +use tracing::error; + +use args::Args; + +use crate::triton_tui::TritonTUI; +use crate::utils::initialize_logging; +use crate::utils::initialize_panic_handler; + +pub(crate) mod action; +pub(crate) mod args; +pub(crate) mod components; +pub(crate) mod config; +pub(crate) mod element_type_hint; +pub(crate) mod mode; +pub(crate) mod shadow_memory; +pub(crate) mod triton_tui; +pub(crate) mod triton_vm_state; +pub(crate) mod tui; +pub(crate) mod utils; + +#[tokio::main] +async fn main() -> Result<()> { + initialize_logging()?; + initialize_panic_handler()?; + + let args = Args::parse(); + let mut triton_tui = TritonTUI::new(args)?; + let execution_result = triton_tui.run().await; + if let Err(ref err) = execution_result { + error!("{err}"); + triton_tui.terminate()?; + }; + execution_result +} diff --git a/triton-tui/src/mode.rs b/triton-tui/src/mode.rs new file mode 100644 index 000000000..0c670e686 --- /dev/null +++ b/triton-tui/src/mode.rs @@ -0,0 +1,35 @@ +use arbitrary::Arbitrary; +use serde::*; +use strum::EnumCount; + +#[derive( + Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, EnumCount, Arbitrary, +)] +#[repr(usize)] +pub(crate) enum Mode { + #[default] + Home, + Memory, + Help, +} + +impl Mode { + pub const fn id(self) -> usize { + self as usize + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_mode_is_home() { + assert_eq!(Mode::Home, Mode::default()); + } + + #[test] + fn default_mode_id_is_zero() { + assert_eq!(0, Mode::default().id()); + } +} diff --git a/triton-tui/src/shadow_memory.rs b/triton-tui/src/shadow_memory.rs new file mode 100644 index 000000000..8f7238355 --- /dev/null +++ b/triton-tui/src/shadow_memory.rs @@ -0,0 +1,448 @@ +use std::collections::HashMap; +use std::iter::once; + +use color_eyre::eyre::bail; +use color_eyre::eyre::Result; +use itertools::Itertools; + +use triton_vm::instruction::*; +use triton_vm::op_stack::NumberOfWords::*; +use triton_vm::op_stack::*; +use triton_vm::BFieldElement; + +use crate::action::ExecutedInstruction; +use crate::element_type_hint::ElementTypeHint; + +/// Mimics the behavior of the actual memory. Helps debugging programs written for Triton VM by +/// tracking (manually set) type hints next to stack or RAM elements. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ShadowMemory { + /// Shadow stack mimicking the actual stack. + pub stack: Vec>, + + /// Shadow RAM mimicking the actual RAM. + pub ram: HashMap>, +} + +impl ShadowMemory { + pub fn new() -> Self { + let stack = vec![None; NUM_OP_STACK_REGISTERS]; + let ram = HashMap::new(); + let initial_hint = Self::initial_program_digest_type_hint(); + + let mut hints = Self { stack, ram }; + hints.apply_type_hint(initial_hint).unwrap(); + hints + } + + fn initial_program_digest_type_hint() -> TypeHint { + let digest_length = triton_vm::Digest::default().0.len(); + TypeHint { + type_name: Some("Digest".to_string()), + variable_name: "program_digest".to_string(), + starting_index: NUM_OP_STACK_REGISTERS - digest_length, + length: digest_length, + } + } + + pub fn apply_type_hint(&mut self, type_hint: TypeHint) -> Result<()> { + let type_hint_range_end = type_hint.starting_index + type_hint.length; + if type_hint_range_end > self.stack.len() { + bail!("stack is not large enough to apply type hint \"{type_hint}\""); + } + + let element_type_hint_template = ElementTypeHint { + type_name: type_hint.type_name, + variable_name: type_hint.variable_name, + index: None, + }; + + if type_hint.length <= 1 { + let insertion_index = self.stack.len() - type_hint.starting_index - 1; + self.stack[insertion_index] = Some(element_type_hint_template); + return Ok(()); + } + + let stack_indices = type_hint.starting_index..type_hint_range_end; + for (index_in_variable, stack_index) in stack_indices.enumerate() { + let mut element_type_hint = element_type_hint_template.clone(); + element_type_hint.index = Some(index_in_variable); + let insertion_index = self.stack.len() - stack_index - 1; + self.stack[insertion_index] = Some(element_type_hint); + } + Ok(()) + } + + pub fn mimic_instruction(&mut self, executed_instruction: ExecutedInstruction) { + let old_top_of_stack = executed_instruction.old_top_of_stack; + match executed_instruction.instruction { + Instruction::Pop(n) => _ = self.pop_n(n), + Instruction::Push(_) => self.push(None), + Instruction::Divine(n) => self.extend_by(n), + Instruction::Dup(st) => self.dup(st), + Instruction::Swap(st) => self.swap_top_with(st), + Instruction::Halt => (), + Instruction::Nop => (), + Instruction::Skiz => _ = self.pop(), + Instruction::Call(_) => (), + Instruction::Return => (), + Instruction::Recurse => (), + Instruction::Assert => _ = self.pop(), + Instruction::ReadMem(n) => self.read_mem(n, old_top_of_stack), + Instruction::WriteMem(n) => self.write_mem(n, old_top_of_stack), + Instruction::Hash => self.hash(), + Instruction::DivineSibling => self.divine_sibling(), + Instruction::AssertVector => _ = self.pop_n(N5), + Instruction::SpongeInit => (), + Instruction::SpongeAbsorb => self.sponge_absorb(), + Instruction::SpongeSqueeze => self.sponge_squeeze(), + Instruction::Add => self.binop_maybe_keep_hint(), + Instruction::Mul => self.binop_maybe_keep_hint(), + Instruction::Invert => self.unop(), + Instruction::Eq => self.eq(), + Instruction::Split => self.split(), + Instruction::Lt => self.lt(), + Instruction::And => self.binop(), + Instruction::Xor => self.binop(), + Instruction::Log2Floor => self.unop(), + Instruction::Pow => self.binop(), + Instruction::DivMod => self.div_mod(), + Instruction::PopCount => self.unop(), + Instruction::XxAdd => _ = self.pop_n(N3), + Instruction::XxMul => _ = self.pop_n(N3), + Instruction::XInvert => self.x_invert(), + Instruction::XbMul => self.xb_mul(), + Instruction::ReadIo(n) => self.extend_by(n), + Instruction::WriteIo(n) => _ = self.pop_n(n), + } + } + + fn push(&mut self, element_type_hint: Option) { + self.stack.push(element_type_hint); + } + + fn extend_by(&mut self, n: NumberOfWords) { + self.stack.extend(vec![None; n.into()]); + } + + fn swap_top_with(&mut self, index: OpStackElement) { + let top_index = self.stack.len() - 1; + let other_index = self.stack.len() - usize::from(index) - 1; + self.stack.swap(top_index, other_index); + } + + fn pop(&mut self) -> Option { + self.stack.pop().flatten() + } + + fn pop_n(&mut self, n: NumberOfWords) -> Vec> { + let start_index = self.stack.len() - usize::from(n); + self.stack.drain(start_index..).rev().collect() + } + + fn dup(&mut self, st: OpStackElement) { + let dup_index = self.stack.len() - usize::from(st) - 1; + self.push(self.stack[dup_index].clone()); + } + + fn read_mem( + &mut self, + n: NumberOfWords, + old_top_of_stack: [BFieldElement; NUM_OP_STACK_REGISTERS], + ) { + let ram_pointer_hint = self.pop(); + let mut ram_pointer = old_top_of_stack[0]; + for _ in 0..n.num_words() { + let hint = self.ram.get(&ram_pointer).cloned().flatten(); + self.push(hint); + ram_pointer.decrement(); + } + self.push(ram_pointer_hint); + } + + fn write_mem( + &mut self, + n: NumberOfWords, + old_top_of_stack: [BFieldElement; NUM_OP_STACK_REGISTERS], + ) { + let ram_pointer_hint = self.pop(); + let mut ram_pointer = old_top_of_stack[0]; + for _ in 0..n.num_words() { + let hint = self.pop(); + self.ram.insert(ram_pointer, hint); + ram_pointer.increment(); + } + self.push(ram_pointer_hint); + } + + fn hash(&mut self) { + let mut popped = self.pop_n(N5); + popped.extend(self.pop_n(N5)); + self.extend_by(N5); + + let all_hashed_elements = popped.iter().collect_vec(); + + let index_of_first_non_hashed_element = self.stack.len() - N5.num_words() - 1; + let first_non_hashed_element = &self.stack[index_of_first_non_hashed_element]; + let all_hashed_and_first_non_hashed_elements = popped + .iter() + .chain(once(first_non_hashed_element)) + .collect_vec(); + + let hashed_a_sequence = ElementTypeHint::is_continuous_sequence(&all_hashed_elements); + let did_not_interrupt_sequence = + !ElementTypeHint::is_continuous_sequence(&all_hashed_and_first_non_hashed_elements); + let hashed_exactly_one_object = hashed_a_sequence && did_not_interrupt_sequence; + + if hashed_exactly_one_object { + let Some(ref hash_type_hint) = popped[0] else { + return; + }; + let type_hint = TypeHint { + type_name: Some("Digest".to_string()), + variable_name: format!("{}_hash", hash_type_hint.variable_name), + starting_index: 0, + length: triton_vm::Digest::default().0.len(), + }; + self.apply_type_hint(type_hint).unwrap(); + } + } + + fn divine_sibling(&mut self) { + self.pop_n(N5); + self.extend_by(N5); + self.extend_by(N5); + } + + fn sponge_absorb(&mut self) { + self.pop_n(N5); + self.pop_n(N5); + } + + fn sponge_squeeze(&mut self) { + self.extend_by(N5); + self.extend_by(N5); + } + + fn binop_maybe_keep_hint(&mut self) { + let lhs = self.pop(); + let rhs = self.pop(); + self.push(lhs.xor(rhs)); + } + + fn unop(&mut self) { + self.pop(); + self.push(None); + } + + fn binop(&mut self) { + self.pop_n(N2); + self.push(None); + } + + fn eq(&mut self) { + let lhs = self.pop(); + let rhs = self.pop(); + let (Some(lhs), Some(rhs)) = (lhs, rhs) else { + self.push(None); + return; + }; + + let type_hint = ElementTypeHint { + type_name: Some("bool".to_string()), + variable_name: format!("{} == {}", lhs.variable_name, rhs.variable_name), + index: None, + }; + self.push(Some(type_hint)); + } + + fn split(&mut self) { + let maybe_type_hint = self.pop(); + self.extend_by(N2); + + let Some(type_hint) = maybe_type_hint else { + return; + }; + let lo_index = self.stack.len() - 1; + let hi_index = self.stack.len() - 2; + + let mut lo = type_hint.clone(); + lo.variable_name.push_str("_lo"); + self.stack[lo_index] = Some(lo); + + let mut hi = type_hint; + hi.variable_name.push_str("_hi"); + self.stack[hi_index] = Some(hi); + } + + fn lt(&mut self) { + let smaller = self.pop(); + let bigger = self.pop(); + let (Some(smaller), Some(bigger)) = (smaller, bigger) else { + self.push(None); + return; + }; + + let type_hint = ElementTypeHint { + type_name: Some("bool".to_string()), + variable_name: format!("{} < {}", smaller.variable_name, bigger.variable_name), + index: None, + }; + self.push(Some(type_hint)); + } + + fn div_mod(&mut self) { + self.pop_n(N2); + self.extend_by(N2); + } + + fn x_invert(&mut self) { + self.pop_n(N3); + self.extend_by(N3); + } + + fn xb_mul(&mut self) { + self.pop_n(N4); + self.extend_by(N3); + } +} + +impl Default for ShadowMemory { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use assert2::assert; + use assert2::let_assert; + use num_traits::One; + use num_traits::Zero; + use proptest::collection::vec; + use proptest::prelude::*; + use proptest_arbitrary_interop::arb; + use test_strategy::proptest; + + use super::*; + + impl Arbitrary for ShadowMemory { + type Parameters = (); + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + let stack_strategy = vec(arb(), NUM_OP_STACK_REGISTERS..=100); + let ram_strategy = arb(); + (stack_strategy, ram_strategy) + .prop_map(|(stack, ram)| Self { stack, ram }) + .boxed() + } + + type Strategy = BoxedStrategy; + } + + #[test] + fn default_type_hint_stack_is_as_long_as_default_actual_stack() { + let actual_stack_length = ShadowMemory::default().stack.len(); + let expected_stack_length = OpStack::new(Default::default()).stack.len(); + assert!(expected_stack_length == actual_stack_length); + } + + #[proptest] + fn type_hint_stack_grows_and_shrinks_like_actual_stack( + mut type_hints: ShadowMemory, + #[strategy(arb())] executed_instruction: ExecutedInstruction, + ) { + let initial_length = type_hints.stack.len(); + type_hints.mimic_instruction(executed_instruction); + let actual_stack_delta = type_hints.stack.len() as i32 - initial_length as i32; + let expected_stack_delta = executed_instruction.instruction.op_stack_size_influence(); + assert!(expected_stack_delta == actual_stack_delta); + } + + #[proptest] + fn write_mem_then_read_mem_preserves_type_hints_on_stack( + mut type_hints: ShadowMemory, + #[strategy(arb())] num_words: NumberOfWords, + #[strategy(arb())] ram_pointer: BFieldElement, + ) { + let mut top_of_stack_before_write = [BFieldElement::zero(); NUM_OP_STACK_REGISTERS]; + top_of_stack_before_write[0] = ram_pointer; + + let offset_of_last_written_element = BFieldElement::from(num_words) - BFieldElement::one(); + let mut top_of_stack_before_read = [BFieldElement::zero(); NUM_OP_STACK_REGISTERS]; + top_of_stack_before_read[0] = ram_pointer + offset_of_last_written_element; + + let initial_type_hints = type_hints.clone(); + type_hints.mimic_instruction(ExecutedInstruction::new( + Instruction::WriteMem(num_words), + top_of_stack_before_write, + Default::default(), + )); + prop_assert_ne!(&initial_type_hints.stack, &type_hints.stack); + type_hints.mimic_instruction(ExecutedInstruction::new( + Instruction::ReadMem(num_words), + top_of_stack_before_read, + Default::default(), + )); + prop_assert_eq!(initial_type_hints.stack, type_hints.stack); + } + + #[test] + fn apply_type_hint_of_length_one() { + let type_name = Some("u32".to_string()); + let variable_name = "foo".to_string(); + let type_hint_to_apply = TypeHint { + type_name: type_name.clone(), + variable_name: variable_name.clone(), + starting_index: 0, + length: 1, + }; + let expected_hint = ElementTypeHint { + type_name, + variable_name, + index: None, + }; + + let mut type_hints = ShadowMemory::new(); + let_assert!(Ok(()) = type_hints.apply_type_hint(type_hint_to_apply)); + let_assert!(Some(maybe_hint) = type_hints.stack.last()); + let_assert!(Some(hint) = maybe_hint.clone()); + assert!(expected_hint == hint); + } + + #[test] + fn applying_type_hint_at_illegal_index_gives_error() { + let type_hint_to_apply = TypeHint { + type_name: Some("u32".to_string()), + variable_name: "foo".to_string(), + starting_index: 100, + length: 1, + }; + + let mut type_hints = ShadowMemory::new(); + let_assert!(Err(_) = type_hints.apply_type_hint(type_hint_to_apply)); + } + + #[test] + fn hashing_one_complete_object_gives_type_hint_for_digest() { + let type_hint_to_apply = TypeHint { + type_name: Some("array".to_string()), + variable_name: "foo".to_string(), + starting_index: 0, + length: 10, + }; + let executed_instruction = ExecutedInstruction::new( + Instruction::Hash, + [BFieldElement::zero(); NUM_OP_STACK_REGISTERS], + Default::default(), + ); + + let mut type_hints = ShadowMemory::new(); + let_assert!(Ok(()) = type_hints.apply_type_hint(type_hint_to_apply)); + type_hints.mimic_instruction(executed_instruction); + + let_assert!(Some(maybe_hint) = type_hints.stack.last()); + let_assert!(Some(hint) = maybe_hint.clone()); + assert!(hint.type_name == Some("Digest".to_string())); + assert!(hint.variable_name == "foo_hash".to_string()); + } +} diff --git a/triton-tui/src/triton_tui.rs b/triton-tui/src/triton_tui.rs new file mode 100644 index 000000000..48d5204c6 --- /dev/null +++ b/triton-tui/src/triton_tui.rs @@ -0,0 +1,206 @@ +use color_eyre::eyre::Result; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; +use ratatui::prelude::Rect; +use strum::EnumCount; +use tokio::sync::mpsc; +use tokio::sync::mpsc::UnboundedSender; + +use crate::action::*; +use crate::args::Args; +use crate::components::help::Help; +use crate::components::home::Home; +use crate::components::memory::Memory; +use crate::components::Component; +use crate::config::Config; +use crate::config::KeyEvents; +use crate::mode::Mode; +use crate::triton_vm_state::TritonVMState; +use crate::tui::*; + +const RECENT_KEY_EVENTS_RESET_DELAY: u32 = 1; + +pub(crate) struct TritonTUI { + pub args: Args, + pub config: Config, + + pub tui: Tui, + pub mode: Mode, + pub components: [Box; Mode::COUNT], + + pub should_quit: bool, + pub should_suspend: bool, + + pub recent_key_events_reset_delay: u32, + pub recent_key_events: KeyEvents, + + pub vm_state: TritonVMState, +} + +impl TritonTUI { + pub fn new(args: Args) -> Result { + let tui = Self::tui(&args)?; + let config = Config::new()?; + + let mode = Mode::default(); + let components: [Box; Mode::COUNT] = [ + Box::::default(), + Box::::default(), + Box::::default(), + ]; + + let vm_state = TritonVMState::new(&args)?; + + Ok(Self { + args, + config, + tui, + mode, + components, + should_quit: false, + should_suspend: false, + recent_key_events_reset_delay: 0, + recent_key_events: vec![], + vm_state, + }) + } + + pub async fn run(&mut self) -> Result<()> { + let (action_tx, mut action_rx) = mpsc::unbounded_channel(); + self.tui.enter()?; + + self.vm_state.register_action_handler(action_tx.clone())?; + for component in self.components.iter_mut() { + component.register_action_handler(action_tx.clone())?; + } + + // in case of infinite loop (or similar) before first breakpoint, provide visual feedback + self.render()?; + action_tx.send(Action::Execute(Execute::Continue))?; + + while !self.should_quit { + if let Some(e) = self.tui.next().await { + match e { + Event::Quit => action_tx.send(Action::Quit)?, + Event::Tick => action_tx.send(Action::Tick)?, + Event::Render => action_tx.send(Action::Render)?, + Event::Key(key) => self.handle_key_sequence(&action_tx, key)?, + Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, + _ => {} + } + for component in self.components.iter_mut() { + if let Some(action) = component.handle_event(Some(e.clone()))? { + action_tx.send(action)?; + } + } + } + + while let Ok(action) = action_rx.try_recv() { + match action { + Action::Tick => self.maybe_clear_recent_key_events(), + Action::Render => self.render()?, + Action::Resize(w, h) => { + self.tui.resize(Rect::new(0, 0, w, h))?; + self.render()?; + } + Action::Mode(mode) => { + self.mode = mode; + self.render()?; + } + Action::Reset => self.reset_state(action_tx.clone())?, + Action::Suspend => self.should_suspend = true, + Action::Resume => self.should_suspend = false, + Action::Quit => self.should_quit = true, + _ => {} + } + self.vm_state.update(action.clone())?; + for component in self.components.iter_mut() { + if let Some(action) = component.update(action.clone())? { + action_tx.send(action)? + }; + } + } + if self.should_suspend { + self.tui.suspend()?; + action_tx.send(Action::Resume)?; + self.tui = Self::tui(&self.args)?; + self.tui.resume()?; + } + } + + self.tui.exit() + } + + fn maybe_clear_recent_key_events(&mut self) { + if self.recent_key_events_reset_delay > 0 { + self.recent_key_events_reset_delay -= 1; + } else { + self.recent_key_events.clear(); + } + } + + fn reset_state(&mut self, action_tx: UnboundedSender) -> Result<()> { + let vm_state = match TritonVMState::new(&self.args) { + Ok(vm_state) => vm_state, + Err(report) => { + self.vm_state.warning = Some(report); + return Ok(()); + } + }; + self.vm_state = vm_state; + self.vm_state.register_action_handler(action_tx.clone())?; + self.render()?; + action_tx.send(Action::Execute(Execute::Continue))?; + Ok(()) + } + + fn tui(args: &Args) -> Result { + let mut tui = Tui::new()?; + tui.apply_args(args); + Ok(tui) + } + + fn render(&mut self) -> Result<()> { + let mode_id = self.mode.id(); + let vm_state = &self.vm_state; + let mut draw_result = Ok(()); + self.tui.draw(|frame| { + let maybe_err = self.components[mode_id].draw(frame, vm_state); + if maybe_err.is_err() { + draw_result = maybe_err; + } + })?; + draw_result + } + + fn handle_key_sequence( + &mut self, + action_tx: &UnboundedSender, + key: KeyEvent, + ) -> Result<()> { + if self.components[self.mode.id()].request_exclusive_key_event_handling() { + return Ok(()); + } + let Some(keymap) = self.config.keybindings.get(&self.mode) else { + return Ok(()); + }; + self.recent_key_events.push(key); + self.recent_key_events_reset_delay = RECENT_KEY_EVENTS_RESET_DELAY; + if let Some(action) = keymap.get(&self.recent_key_events) { + action_tx.send(action.clone())?; + self.recent_key_events.clear(); + } + if key.code == KeyCode::Esc && key.kind != KeyEventKind::Release { + self.recent_key_events_reset_delay = 0; + self.recent_key_events.clear(); + } + Ok(()) + } + + pub fn terminate(&mut self) -> Result<()> { + self.tui.exit()?; + self.should_quit = true; + Ok(()) + } +} diff --git a/triton-tui/src/triton_vm_state.rs b/triton-tui/src/triton_vm_state.rs new file mode 100644 index 000000000..a40e53b5a --- /dev/null +++ b/triton-tui/src/triton_vm_state.rs @@ -0,0 +1,268 @@ +use color_eyre::eyre::anyhow; +use color_eyre::eyre::Result; +use color_eyre::Report; +use fs_err as fs; +use itertools::Itertools; +use tokio::sync::mpsc::UnboundedSender; +use tracing::error; +use tracing::info; +use tracing::warn; + +use triton_vm::error::InstructionError; +use triton_vm::instruction::*; +use triton_vm::op_stack::NUM_OP_STACK_REGISTERS; +use triton_vm::vm::VMState; +use triton_vm::*; + +use crate::action::*; +use crate::args::Args; +use crate::components::Component; +use crate::shadow_memory::ShadowMemory; + +#[derive(Debug)] +pub(crate) struct TritonVMState { + pub action_tx: Option>, + + pub program: Program, + pub vm_state: VMState, + + pub type_hints: ShadowMemory, + pub undo_stack: Vec, + + pub warning: Option, + pub error: Option, +} + +#[derive(Debug, Clone)] +pub(crate) struct UndoInformation { + vm_state: VMState, + type_hints: ShadowMemory, +} + +impl TritonVMState { + pub fn new(args: &Args) -> Result { + let program = Self::program_from_args(args)?; + let public_input = Self::public_input_from_args(args)?; + let non_determinism = Self::non_determinism_from_args(args)?; + + let vm_state = VMState::new(&program, public_input.clone(), non_determinism); + + let mut state = Self { + action_tx: None, + program, + vm_state, + type_hints: ShadowMemory::default(), + undo_stack: vec![], + warning: None, + error: None, + }; + state.apply_type_hints(); + Ok(state) + } + + fn program_from_args(args: &Args) -> Result { + let source_code = fs::read_to_string(&args.program)?; + let program = Program::from_code(&source_code) + .map_err(|err| anyhow!("program parsing error: {err}"))?; + Ok(program) + } + + fn public_input_from_args(args: &Args) -> Result { + let Some(ref input_path) = args.input else { + return Ok(PublicInput::default()); + }; + let file_content = fs::read_to_string(input_path)?; + let string_tokens = file_content.split_whitespace(); + let mut elements = vec![]; + for string_token in string_tokens { + let element = string_token.parse::()?; + elements.push(element.into()); + } + Ok(PublicInput::new(elements)) + } + + fn non_determinism_from_args(args: &Args) -> Result> { + let Some(ref non_determinism_path) = args.non_determinism else { + return Ok(NonDeterminism::default()); + }; + let file_content = fs::read_to_string(non_determinism_path)?; + let non_determinism: NonDeterminism = serde_json::from_str(&file_content)?; + Ok(NonDeterminism::from(&non_determinism)) + } + + fn top_of_stack(&self) -> [BFieldElement; NUM_OP_STACK_REGISTERS] { + let stack_len = self.vm_state.op_stack.stack.len(); + let index_of_lowest_accessible_element = stack_len - NUM_OP_STACK_REGISTERS; + let top_of_stack = &self.vm_state.op_stack.stack[index_of_lowest_accessible_element..]; + let top_of_stack = top_of_stack.iter().copied(); + top_of_stack.rev().collect_vec().try_into().unwrap() + } + + fn vm_has_stopped(&self) -> bool { + self.vm_state.halting || self.error.is_some() + } + + fn vm_is_running(&self) -> bool { + !self.vm_has_stopped() + } + + fn at_breakpoint(&self) -> bool { + let ip = self.vm_state.instruction_pointer as u64; + self.program.is_breakpoint(ip) + } + + fn apply_type_hints(&mut self) { + let ip = self.vm_state.instruction_pointer as u64; + for type_hint in self.program.type_hints_at(ip) { + let maybe_error = self.type_hints.apply_type_hint(type_hint); + if let Err(report) = maybe_error { + info!("Error applying type hint: {report}"); + self.warning = Some(report); + }; + } + } + + fn execute(&mut self, execute: Execute) { + self.record_undo_information(); + match execute { + Execute::Continue => self.continue_execution(), + Execute::Step => self.step(), + Execute::Next => self.next(), + Execute::Finish => self.finish(), + } + } + + /// Handle [`Execute::Continue`]. + fn continue_execution(&mut self) { + self.step(); + while self.vm_is_running() && !self.at_breakpoint() { + self.step(); + } + } + + /// Handle [`Execute::Step`]. + fn step(&mut self) { + if self.vm_has_stopped() { + return; + } + + let instruction = self.vm_state.current_instruction().ok(); + let old_top_of_stack = self.top_of_stack(); + if let Err(err) = self.vm_state.step() { + warn!("Error stepping: {err}"); + self.error = Some(err); + return; + } + self.warning = None; + + let instruction = instruction.expect("instruction should exist after successful `step`"); + let new_top_of_stack = self.top_of_stack(); + let executed_instruction = + ExecutedInstruction::new(instruction, old_top_of_stack, new_top_of_stack); + + self.send_executed_transaction(executed_instruction); + self.type_hints.mimic_instruction(executed_instruction); + self.apply_type_hints(); + } + + fn send_executed_transaction(&mut self, executed_instruction: ExecutedInstruction) { + let Some(ref action_tx) = self.action_tx else { + error!("action_tx should exist"); + return; + }; + let _ = action_tx.send(Action::ExecutedInstruction(Box::new(executed_instruction))); + } + + /// Handle [`Execute::Next`]. + fn next(&mut self) { + let instruction = self.vm_state.current_instruction(); + let instruction_is_call = matches!(instruction, Ok(Instruction::Call(_))); + self.step(); + if instruction_is_call { + self.finish(); + } + } + + /// Handle [`Execute::Finish`]. + fn finish(&mut self) { + let current_jump_stack_depth = self.vm_state.jump_stack.len(); + while self.vm_is_running() && self.vm_state.jump_stack.len() >= current_jump_stack_depth { + self.step(); + } + } + + fn record_undo_information(&mut self) { + if self.vm_has_stopped() { + return; + } + let undo_information = UndoInformation { + vm_state: self.vm_state.clone(), + type_hints: self.type_hints.clone(), + }; + self.undo_stack.push(undo_information); + + let Some(ref action_tx) = self.action_tx else { + error!("action_tx must exist"); + return; + }; + let _ = action_tx.send(Action::RecordUndoInfo); + } + + fn program_undo(&mut self) { + let Some(undo_information) = self.undo_stack.pop() else { + self.warning = Some(anyhow!("no more undo information available")); + return; + }; + self.warning = None; + self.error = None; + self.vm_state = undo_information.vm_state; + self.type_hints = undo_information.type_hints; + } +} + +impl Component for TritonVMState { + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.action_tx = Some(tx); + Ok(()) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::Execute(execute) => self.execute(execute), + Action::Undo => self.program_undo(), + _ => (), + } + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use proptest::collection::vec; + use proptest::prelude::*; + use proptest_arbitrary_interop::arb; + use test_strategy::proptest; + + use super::*; + + #[proptest] + fn presumed_top_of_stack_is_actually_top_of_stack( + #[strategy(vec(arb(), NUM_OP_STACK_REGISTERS..100))] stack: Vec, + ) { + let mut triton_vm_state = TritonVMState::new(&Default::default()).unwrap(); + triton_vm_state.vm_state.op_stack.stack = stack.clone(); + let top_of_stack = triton_vm_state.top_of_stack(); + prop_assert_eq!(top_of_stack[0], stack[stack.len() - 1]); + prop_assert_eq!(top_of_stack[1], stack[stack.len() - 2]); + prop_assert_eq!(top_of_stack[2], stack[stack.len() - 3]); + } + + #[proptest] + fn serialize_and_deserialize_non_determinism_to_and_from_json( + #[strategy(arb())] non_determinism: NonDeterminism, + ) { + let serialized = serde_json::to_string(&non_determinism).unwrap(); + let deserialized: NonDeterminism = serde_json::from_str(&serialized).unwrap(); + prop_assert_eq!(non_determinism, deserialized); + } +} diff --git a/triton-tui/src/tui.rs b/triton-tui/src/tui.rs new file mode 100644 index 000000000..128892e69 --- /dev/null +++ b/triton-tui/src/tui.rs @@ -0,0 +1,266 @@ +use std::ops::Deref; +use std::ops::DerefMut; +use std::time::Duration; + +use color_eyre::eyre::bail; +use color_eyre::eyre::Result; +use crossterm::event::Event as CrosstermEvent; +use crossterm::event::*; +use crossterm::terminal::*; +use crossterm::tty::IsTty; +use crossterm::*; +use futures::*; +use ratatui::backend::CrosstermBackend as Backend; +use ratatui::Terminal; +use serde::Deserialize; +use serde::Serialize; +use tokio::sync::mpsc::*; +use tokio::task::JoinHandle; +use tokio::time::interval; +use tokio_util::sync::CancellationToken; +use tracing::error; + +use crate::args::Args; + +pub(crate) type IO = std::io::Stdout; + +pub(crate) fn io() -> IO { + std::io::stdout() +} + +const DEFAULT_TICK_RATE: f64 = 1.0; +const DEFAULT_FRAME_RATE: f64 = 32.0; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) enum Event { + Init, + Quit, + Error, + Closed, + Tick, + Render, + FocusGained, + FocusLost, + Paste(String), + Key(KeyEvent), + Mouse(MouseEvent), + Resize(u16, u16), +} + +#[derive(Debug)] +pub(crate) struct Tui { + pub terminal: Terminal>, + pub task: JoinHandle<()>, + pub cancellation_token: CancellationToken, + pub event_rx: UnboundedReceiver, + pub event_tx: UnboundedSender, + pub frame_rate: f64, + pub tick_rate: f64, + pub mouse: bool, + pub paste: bool, +} + +impl Tui { + pub fn new() -> Result { + if !io().is_tty() { + error!("not a TTY"); + bail!("not a TTY"); + } + + let tick_rate = DEFAULT_TICK_RATE; + let frame_rate = DEFAULT_FRAME_RATE; + let terminal = Terminal::new(Backend::new(io()))?; + let (event_tx, event_rx) = unbounded_channel(); + let cancellation_token = CancellationToken::new(); + let task = tokio::spawn(async {}); + Ok(Self { + terminal, + task, + cancellation_token, + event_rx, + event_tx, + frame_rate, + tick_rate, + mouse: true, + paste: true, + }) + } + + pub fn apply_args(&mut self, _: &Args) -> &mut Self { + self.frame_rate(DEFAULT_FRAME_RATE); + self.mouse(true); + self.paste(true); + self + } + + pub fn frame_rate(&mut self, frame_rate: f64) -> &mut Self { + self.frame_rate = frame_rate; + self + } + + pub fn mouse(&mut self, mouse: bool) -> &mut Self { + self.mouse = mouse; + self + } + + pub fn paste(&mut self, paste: bool) -> &mut Self { + self.paste = paste; + self + } + + pub fn start(&mut self) { + let tick_delay = Duration::from_secs_f64(1.0 / self.tick_rate); + let render_delay = Duration::from_secs_f64(1.0 / self.frame_rate); + + self.cancel(); + self.cancellation_token = CancellationToken::new(); + let cancellation_token = self.cancellation_token.clone(); + + let event_tx = self.event_tx.clone(); + self.task = tokio::spawn(async move { + let mut reader = EventStream::new(); + let mut tick_interval = interval(tick_delay); + let mut render_interval = interval(render_delay); + event_tx.send(Event::Init).unwrap(); + loop { + let tick_delay = tick_interval.tick(); + let render_delay = render_interval.tick(); + let crossterm_event = reader.next().fuse(); + tokio::select! { + maybe_event = crossterm_event => { + match maybe_event { + Some(Ok(evt)) => { + match evt { + CrosstermEvent::Key(key) => { + if key.kind == KeyEventKind::Press { + event_tx.send(Event::Key(key)).unwrap() + } + }, + CrosstermEvent::Mouse(mouse) => { + event_tx.send(Event::Mouse(mouse)).unwrap(); + }, + CrosstermEvent::Resize(x, y) => { + event_tx.send(Event::Resize(x, y)).unwrap(); + }, + CrosstermEvent::FocusLost => { + event_tx.send(Event::FocusLost).unwrap(); + }, + CrosstermEvent::FocusGained => { + event_tx.send(Event::FocusGained).unwrap(); + }, + CrosstermEvent::Paste(s) => { + event_tx.send(Event::Paste(s)).unwrap(); + }, + } + } + Some(Err(_)) => { + event_tx.send(Event::Error).unwrap(); + } + None => {}, + } + }, + _ = cancellation_token.cancelled() => return, + _ = tick_delay => event_tx.send(Event::Tick).unwrap(), + _ = render_delay => event_tx.send(Event::Render).unwrap(), + } + } + }); + } + + pub fn enter(&mut self) -> Result<()> { + enable_raw_mode()?; + execute!(io(), EnterAlternateScreen, cursor::Hide)?; + if self.mouse { + execute!(io(), EnableMouseCapture)?; + } + if self.paste { + execute!(io(), EnableBracketedPaste)?; + } + self.start(); + Ok(()) + } + + pub fn exit(&mut self) -> Result<()> { + self.stop()?; + if is_raw_mode_enabled()? { + self.flush()?; + if self.paste { + execute!(io(), DisableBracketedPaste)?; + } + if self.mouse { + execute!(io(), DisableMouseCapture)?; + } + execute!(io(), LeaveAlternateScreen, cursor::Show)?; + disable_raw_mode()?; + } + Ok(()) + } + + pub fn stop(&self) -> Result<()> { + self.cancel(); + let mut counter = 0; + while !self.task.is_finished() { + std::thread::sleep(Duration::from_millis(1)); + counter += 1; + if counter > 50 { + self.task.abort(); + } + if counter > 100 { + error!("Failed to abort task in 100 milliseconds for unknown reason"); + break; + } + } + Ok(()) + } + + pub fn cancel(&self) { + self.cancellation_token.cancel(); + } + + pub fn suspend(&mut self) -> Result<()> { + self.exit()?; + #[cfg(not(windows))] + signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; + Ok(()) + } + + pub fn resume(&mut self) -> Result<()> { + self.enter() + } + + pub async fn next(&mut self) -> Option { + self.event_rx.recv().await + } +} + +impl Deref for Tui { + type Target = Terminal>; + + fn deref(&self) -> &Self::Target { + &self.terminal + } +} + +impl DerefMut for Tui { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.terminal + } +} + +impl Drop for Tui { + fn drop(&mut self) { + self.exit().unwrap(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert2::let_assert; + + #[test] + fn creating_tui_outside_of_tty_gives_error() { + let_assert!(Err(err) = Tui::new()); + assert!(err.to_string().contains("TTY")); + } +} diff --git a/triton-tui/src/utils.rs b/triton-tui/src/utils.rs new file mode 100644 index 000000000..ee337456c --- /dev/null +++ b/triton-tui/src/utils.rs @@ -0,0 +1,153 @@ +use std::path::PathBuf; + +use color_eyre::eyre::Result; +use directories::ProjectDirs; +use lazy_static::lazy_static; +use tracing::error; +use tracing_error::ErrorLayer; +use tracing_subscriber::prelude::*; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::Layer; + +lazy_static! { + pub(crate) static ref PROJECT_NAME: String = + env!("CARGO_CRATE_NAME").to_uppercase().to_string(); + pub(crate) static ref DATA_FOLDER: Option = + std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) + .ok() + .map(PathBuf::from); + pub(crate) static ref CONFIG_FOLDER: Option = + std::env::var(format!("{}_CONFIG", PROJECT_NAME.clone())) + .ok() + .map(PathBuf::from); + pub(crate) static ref GIT_COMMIT_HASH: String = + std::env::var(format!("{}_GIT_INFO", PROJECT_NAME.clone())) + .unwrap_or_else(|_| String::from("UNKNOWN")); + pub(crate) static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); + pub(crate) static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); +} + +fn project_directory() -> Option { + ProjectDirs::from( + "org.triton-vm.triton-tui", + "Triton VM", + env!("CARGO_PKG_NAME"), + ) +} + +pub(crate) fn initialize_panic_handler() -> Result<()> { + let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default() + .panic_section(format!( + "This is a bug. Consider reporting it at {}", + env!("CARGO_PKG_REPOSITORY") + )) + .capture_span_trace_by_default(false) + .display_location_section(false) + .display_env_section(false) + .into_hooks(); + eyre_hook.install()?; + std::panic::set_hook(Box::new(move |panic_info| { + if let Ok(mut t) = crate::tui::Tui::new() { + if let Err(r) = t.exit() { + error!("Unable to exit Terminal: {:?}", r); + } + } + + #[cfg(not(debug_assertions))] + { + use human_panic::handle_dump; + use human_panic::print_msg; + use human_panic::Metadata; + let meta = Metadata { + version: env!("CARGO_PKG_VERSION").into(), + name: env!("CARGO_PKG_NAME").into(), + authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(), + homepage: env!("CARGO_PKG_HOMEPAGE").into(), + }; + + let file_path = handle_dump(&meta, panic_info); + // prints human-panic message + print_msg(file_path, &meta) + .expect("human-panic: printing error message to console failed"); + // prints color-eyre stack trace to stderr + eprintln!("{}", panic_hook.panic_report(panic_info)); + } + let msg = format!("{}", panic_hook.panic_report(panic_info)); + error!("Error: {}", strip_ansi_escapes::strip_str(msg)); + + #[cfg(debug_assertions)] + { + // Better Panic stacktrace that is only enabled when debugging. + better_panic::Settings::auto() + .most_recent_first(false) + .lineno_suffix(true) + .verbosity(better_panic::Verbosity::Full) + .create_panic_handler()(panic_info); + } + + std::process::exit(libc::EXIT_FAILURE); + })); + Ok(()) +} + +pub(crate) fn get_data_dir() -> PathBuf { + if let Some(s) = DATA_FOLDER.clone() { + s + } else if let Some(proj_dirs) = project_directory() { + proj_dirs.data_local_dir().to_path_buf() + } else { + PathBuf::from(".").join(".data") + } +} + +pub(crate) fn get_config_dir() -> PathBuf { + if let Some(s) = CONFIG_FOLDER.clone() { + s + } else if let Some(proj_dirs) = project_directory() { + proj_dirs.config_local_dir().to_path_buf() + } else { + PathBuf::from(".").join(".config") + } +} + +pub(crate) fn initialize_logging() -> Result<()> { + let directory = get_data_dir(); + std::fs::create_dir_all(directory.clone())?; + let log_path = directory.join(LOG_FILE.clone()); + let log_file = std::fs::File::create(log_path)?; + std::env::set_var( + "RUST_LOG", + std::env::var("RUST_LOG") + .or_else(|_| std::env::var(LOG_ENV.clone())) + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))), + ); + let file_subscriber = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true) + .with_writer(log_file) + .with_target(false) + .with_ansi(false) + .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); + tracing_subscriber::registry() + .with(file_subscriber) + .with(ErrorLayer::default()) + .init(); + Ok(()) +} + +pub(crate) fn version() -> String { + let author = clap::crate_authors!(); + + let commit_hash = GIT_COMMIT_HASH.clone(); + + let config_dir_path = get_config_dir().display().to_string(); + let data_dir_path = get_data_dir().display().to_string(); + + format!( + "{commit_hash}\n\n\ + Authors: {author}\n\n\ + Config directory: {config_dir_path}\n\ + Data directory: {data_dir_path}\ + ", + ) +} diff --git a/triton-tui/tests/tests.rs b/triton-tui/tests/tests.rs new file mode 100644 index 000000000..74c596c79 --- /dev/null +++ b/triton-tui/tests/tests.rs @@ -0,0 +1,38 @@ +use std::path::PathBuf; + +use assert2::let_assert; +use rexpect::error::Error; +use rexpect::session::PtySession; +use rexpect::spawn; + +#[test] +#[ignore = "breaks code-coverage tool `cargo-tarpaulin`"] +fn setup_and_shutdown_triton_tui_with_trivial_program() { + let timeout = Some(180_000); + let mut child = setup_and_start_triton_tui_with_trivial_program(timeout); + let_assert!(Ok(_) = child.send_line("q")); + let_assert!(Ok(_) = child.exp_eof()); +} + +#[test] +#[ignore = "breaks code-coverage tool `cargo-tarpaulin`"] +fn setup_without_shutdown_of_triton_tui_with_trivial_program_leaves_tui_open() { + let timeout = Some(10_000); + let mut child = setup_and_start_triton_tui_with_trivial_program(timeout); + let_assert!(Err(Error::Timeout { .. }) = child.exp_eof()); +} + +fn setup_and_start_triton_tui_with_trivial_program(timeout: Option) -> PtySession { + let path_to_trivial_program = manifest_dir().join("tests/trivial_program.tasm"); + assert!(path_to_trivial_program.exists()); + let_assert!(Some(path_to_trivial_program) = path_to_trivial_program.to_str()); + + let command = format!("cargo run --offline --bin triton-tui -- -p {path_to_trivial_program}"); + let_assert!(Ok(child) = spawn(&command, timeout)); + child +} + +/// The directory containing the Cargo.toml file. +fn manifest_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} diff --git a/triton-tui/tests/trivial_program.tasm b/triton-tui/tests/trivial_program.tasm new file mode 100644 index 000000000..3736b63e0 --- /dev/null +++ b/triton-tui/tests/trivial_program.tasm @@ -0,0 +1 @@ +halt diff --git a/triton-vm/src/instruction.rs b/triton-vm/src/instruction.rs index e95bbb3e6..0200f3e50 100644 --- a/triton-vm/src/instruction.rs +++ b/triton-vm/src/instruction.rs @@ -21,10 +21,9 @@ use AnInstruction::*; use crate::error::InstructionError; use crate::instruction::InstructionBit::*; -use crate::op_stack::NumberOfWords; use crate::op_stack::NumberOfWords::*; -use crate::op_stack::OpStackElement; use crate::op_stack::OpStackElement::*; +use crate::op_stack::*; type Result = result::Result; @@ -56,6 +55,66 @@ pub enum LabelledInstruction { Label(String), Breakpoint, + + TypeHint(TypeHint), +} + +/// A hint about a range of stack elements. Helps debugging programs written for Triton VM. +/// **Does not enforce types.** +/// +/// Usually constructed by parsing special annotations in the assembly code, for example: +/// ```tasm +/// hint variable_name: the_type = stack[0] +/// hint my_list = stack[1..4] +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash, GetSize, Serialize, Deserialize)] +pub struct TypeHint { + pub starting_index: usize, + pub length: usize, + + /// The name of the type, _e.g._, `u32`, `list`, `Digest`, et cetera. + pub type_name: Option, + + /// The name of the variable. + pub variable_name: String, +} + +impl Display for TypeHint { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let variable = &self.variable_name; + let type_name = match self.type_name { + Some(ref type_name) => format!(": {type_name}"), + None => "".to_string(), + }; + + let start = self.starting_index; + let range = match self.length { + 1 => format!("{start}"), + _ => format!("{start}..{end}", end = start + self.length), + }; + + write!(f, "hint {variable}{type_name} = stack[{range}]") + } +} + +impl<'a> Arbitrary<'a> for TypeHint { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let starting_index = u.arbitrary()?; + let length = u.int_in_range(1..=500)?; + let type_name = match u.arbitrary()? { + true => Some(u.arbitrary::()?.into()), + false => None, + }; + let variable_name = u.arbitrary::()?.into(); + + let type_hint = Self { + starting_index, + length, + type_name, + variable_name, + }; + Ok(type_hint) + } } impl LabelledInstruction { @@ -82,9 +141,10 @@ impl LabelledInstruction { impl Display for LabelledInstruction { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { - LabelledInstruction::Instruction(instr) => write!(f, "{instr}"), - LabelledInstruction::Label(label_name) => write!(f, "{label_name}:"), + LabelledInstruction::Instruction(instruction) => write!(f, "{instruction}"), + LabelledInstruction::Label(label) => write!(f, "{label}:"), LabelledInstruction::Breakpoint => write!(f, "break"), + LabelledInstruction::TypeHint(type_hint) => write!(f, "{type_hint}"), } } } @@ -607,6 +667,7 @@ impl<'a> Arbitrary<'a> for LabelledInstruction { 0 => u.arbitrary::>()?, 1 => return Ok(Self::Label(u.arbitrary::()?.into())), 2 => return Ok(Self::Breakpoint), + 3 => return Ok(Self::TypeHint(u.arbitrary()?)), _ => unreachable!(), }; let legal_label = String::from(u.arbitrary::()?); @@ -649,6 +710,57 @@ impl<'a> Arbitrary<'a> for InstructionLabel { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct TypeHintVariableName(String); + +impl From for String { + fn from(label: TypeHintVariableName) -> Self { + label.0 + } +} + +impl<'a> Arbitrary<'a> for TypeHintVariableName { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let legal_start_characters = 'a'..='z'; + let legal_characters = legal_start_characters + .clone() + .chain('0'..='9') + .chain('_'..='_') + .collect_vec(); + + let mut variable_name = u.choose(&legal_start_characters.collect_vec())?.to_string(); + for _ in 0..u.arbitrary_len::()? { + variable_name.push(*u.choose(&legal_characters)?); + } + Ok(Self(variable_name)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct TypeHintTypeName(String); + +impl From for String { + fn from(label: TypeHintTypeName) -> Self { + label.0 + } +} + +impl<'a> Arbitrary<'a> for TypeHintTypeName { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let legal_start_characters = ('a'..='z').chain('A'..='Z'); + let legal_characters = legal_start_characters + .clone() + .chain('0'..='9') + .collect_vec(); + + let mut type_name = u.choose(&legal_start_characters.collect_vec())?.to_string(); + for _ in 0..u.arbitrary_len::()? { + type_name.push(*u.choose(&legal_characters)?); + } + Ok(Self(type_name)) + } +} + #[cfg(test)] mod tests { use assert2::assert; diff --git a/triton-vm/src/lib.rs b/triton-vm/src/lib.rs index a94ca7cc9..076cb1c2d 100644 --- a/triton-vm/src/lib.rs +++ b/triton-vm/src/lib.rs @@ -329,6 +329,38 @@ macro_rules! triton_asm { (@fmt $fmt:expr, $($args:expr,)*; ) => { format_args!($fmt $(,$args)*).to_string() }; + (@fmt $fmt:expr, $($args:expr,)*; + hint $var:ident: $ty:ident = stack[$start:literal..$end:literal] $($tail:tt)*) => { + $crate::triton_asm!(@fmt + concat!($fmt, " hint {}: {} = stack[{}..{}] "), + $($args,)* stringify!($var), stringify!($ty), $start, $end,; + $($tail)* + ) + }; + (@fmt $fmt:expr, $($args:expr,)*; + hint $var:ident = stack[$start:literal..$end:literal] $($tail:tt)*) => { + $crate::triton_asm!(@fmt + concat!($fmt, " hint {} = stack[{}..{}] "), + $($args,)* stringify!($var), $start, $end,; + $($tail)* + ) + }; + (@fmt $fmt:expr, $($args:expr,)*; + hint $var:ident: $ty:ident = stack[$index:literal] $($tail:tt)*) => { + $crate::triton_asm!(@fmt + concat!($fmt, " hint {}: {} = stack[{}] "), + $($args,)* stringify!($var), stringify!($ty), $index,; + $($tail)* + ) + }; + (@fmt $fmt:expr, $($args:expr,)*; + hint $var:ident = stack[$index:literal] $($tail:tt)*) => { + $crate::triton_asm!(@fmt + concat!($fmt, " hint {} = stack[{}] "), + $($args,)* stringify!($var), $index,; + $($tail)* + ) + }; (@fmt $fmt:expr, $($args:expr,)*; $label_declaration:ident: $($tail:tt)*) => { $crate::triton_asm!(@fmt concat!($fmt, " ", stringify!($label_declaration), ": "), $($args,)*; $($tail)* @@ -565,6 +597,8 @@ mod tests { use proptest_arbitrary_interop::arb; use test_strategy::proptest; + use crate::instruction::LabelledInstruction; + use crate::instruction::TypeHint; use crate::shared_tests::*; use crate::stark::StarkHasher; @@ -810,4 +844,105 @@ mod tests { fn parsing_pop_with_illegal_argument_fails() { let _ = triton_instr!(pop 0); } + + #[test] + fn triton_asm_macro_can_parse_type_hints() { + let instructions = triton_asm!( + hint name_0: Type0 = stack[0..8] + hint name_1 = stack[1..9] + hint name_2: Type2 = stack[2] + hint name_3 = stack[3] + ); + + assert!(4 == instructions.len()); + let_assert!(LabelledInstruction::TypeHint(type_hint_0) = instructions[0].clone()); + let_assert!(LabelledInstruction::TypeHint(type_hint_1) = instructions[1].clone()); + let_assert!(LabelledInstruction::TypeHint(type_hint_2) = instructions[2].clone()); + let_assert!(LabelledInstruction::TypeHint(type_hint_3) = instructions[3].clone()); + + let expected_type_hint_0 = TypeHint { + starting_index: 0, + length: 8, + type_name: Some("Type0".to_string()), + variable_name: "name_0".to_string(), + }; + let expected_type_hint_1 = TypeHint { + starting_index: 1, + length: 8, + type_name: None, + variable_name: "name_1".to_string(), + }; + let expected_type_hint_2 = TypeHint { + starting_index: 2, + length: 1, + type_name: Some("Type2".to_string()), + variable_name: "name_2".to_string(), + }; + let expected_type_hint_3 = TypeHint { + starting_index: 3, + length: 1, + type_name: None, + variable_name: "name_3".to_string(), + }; + + assert!(expected_type_hint_0 == type_hint_0); + assert!(expected_type_hint_1 == type_hint_1); + assert!(expected_type_hint_2 == type_hint_2); + assert!(expected_type_hint_3 == type_hint_3); + } + + #[test] + fn triton_program_macro_can_parse_type_hints() { + let program = triton_program! { + push 3 hint loop_counter = stack[0] + call my_loop + pop 1 + halt + + my_loop: + dup 0 push 0 eq + hint return_condition: bool = stack[0] + skiz return + divine 3 + swap 3 + hint magic_number: XFE = stack[1..4] + hint fizzled_magic = stack[5..8] + recurse + }; + + let expected_type_hint_address_02 = TypeHint { + starting_index: 0, + length: 1, + type_name: None, + variable_name: "loop_counter".to_string(), + }; + let expected_type_hint_address_12 = TypeHint { + starting_index: 0, + length: 1, + type_name: Some("bool".to_string()), + variable_name: "return_condition".to_string(), + }; + let expected_type_hint_address_18_0 = TypeHint { + starting_index: 1, + length: 3, + type_name: Some("XFE".to_string()), + variable_name: "magic_number".to_string(), + }; + let expected_type_hint_address_18_1 = TypeHint { + starting_index: 5, + length: 3, + type_name: None, + variable_name: "fizzled_magic".to_string(), + }; + + assert!(vec![expected_type_hint_address_02] == program.type_hints_at(2)); + + assert!(vec![expected_type_hint_address_12] == program.type_hints_at(12)); + + let expected_type_hints_address_18 = vec![ + expected_type_hint_address_18_0, + expected_type_hint_address_18_1, + ]; + assert!(expected_type_hints_address_18 == program.type_hints_at(18)); + } } diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index ad595a34f..731477729 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -17,10 +17,8 @@ use twenty_first::shared_math::digest::Digest; use twenty_first::shared_math::tip5::DIGEST_LENGTH; use twenty_first::shared_math::x_field_element::XFieldElement; -use crate::error::InstructionError; use crate::error::InstructionError::*; -use crate::error::NumberOfWordsError; -use crate::error::OpStackElementError; +use crate::error::*; use crate::op_stack::OpStackElement::*; type Result = std::result::Result; diff --git a/triton-vm/src/parser.rs b/triton-vm/src/parser.rs index 832dffa53..f6f91f4e0 100644 --- a/triton-vm/src/parser.rs +++ b/triton-vm/src/parser.rs @@ -14,10 +14,10 @@ use nom::multi::*; use nom::Finish; use nom::IResult; -use crate::instruction::AnInstruction; use crate::instruction::AnInstruction::*; use crate::instruction::LabelledInstruction; use crate::instruction::ALL_INSTRUCTION_NAMES; +use crate::instruction::*; use crate::op_stack::NumberOfWords; use crate::op_stack::NumberOfWords::*; use crate::op_stack::OpStackElement; @@ -37,6 +37,7 @@ pub enum InstructionToken<'a> { Instruction(AnInstruction, &'a str), Label(String, &'a str), Breakpoint(&'a str), + TypeHint(TypeHint, &'a str), } impl<'a> Display for ParseError<'a> { @@ -53,6 +54,7 @@ impl<'a> InstructionToken<'a> { InstructionToken::Instruction(_, token_str) => token_str, InstructionToken::Label(_, token_str) => token_str, InstructionToken::Breakpoint(token_str) => token_str, + InstructionToken::TypeHint(_, token_str) => token_str, } } @@ -62,6 +64,7 @@ impl<'a> InstructionToken<'a> { Instruction(instr, _) => LabelledInstruction::Instruction(instr.to_owned()), Label(label, _) => LabelledInstruction::Label(label.to_owned()), Breakpoint(_) => LabelledInstruction::Breakpoint, + TypeHint(type_hint, _) => LabelledInstruction::TypeHint(type_hint.to_owned()), } } } @@ -180,7 +183,7 @@ type ParseResult<'input, Out> = IResult<&'input str, Out, VerboseError<&'input s pub fn tokenize(s: &str) -> ParseResult> { let (s, _) = comment_or_whitespace0(s)?; - let (s, instructions) = many0(alt((label, labelled_instruction, breakpoint)))(s)?; + let (s, instructions) = many0(alt((label, labelled_instruction, breakpoint, type_hint)))(s)?; let (s, _) = context("expecting label, instruction or eof", eof)(s)?; Ok((s, instructions)) @@ -193,7 +196,7 @@ fn labelled_instruction(s_instr: &str) -> ParseResult { fn label(label_s: &str) -> ParseResult { let (s, addr) = label_addr(label_s)?; - let (s, _) = token0("")(s)?; // whitespace between label and ':' is allowed + let (s, _) = whitespace0(s)?; // whitespace between label and ':' is allowed let (s, _) = token0(":")(s)?; // don't require space after ':' // Checking if `