golem_rib_repl/
rib_repl.rs

1// Copyright 2024-2025 Golem Cloud
2//
3// Licensed under the Golem Source License v1.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://license.golem.cloud/LICENSE
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::compiler::compile_rib_script;
16use crate::dependency_manager::RibDependencyManager;
17use crate::eval::eval;
18use crate::invoke::WorkerFunctionInvoke;
19use crate::repl_printer::{DefaultReplResultPrinter, ReplPrinter};
20use crate::repl_state::ReplState;
21use crate::rib_context::ReplContext;
22use crate::rib_edit::RibEdit;
23use crate::{CommandRegistry, ReplBootstrapError, RibExecutionError, UntypedCommand};
24use colored::Colorize;
25use rib::{RibCompiler, RibCompilerConfig, RibResult};
26use rustyline::error::ReadlineError;
27use rustyline::history::DefaultHistory;
28use rustyline::{Config, Editor};
29use std::path::PathBuf;
30use std::sync::Arc;
31
32/// Config options:
33///
34/// - `history_file`: Optional path to a file where the REPL history will be stored and loaded from.
35///   If `None`, it will be loaded from `~/.rib_history`.
36/// - `dependency_manager`: This is responsible for how to load all the components or a specific
37///   custom component.
38/// - `worker_function_invoke`: An implementation of the `WorkerFunctionInvoke` trait,
39/// - `printer`: Optional custom printer for displaying results and errors in the REPL. If `None`
40///   a default printer will be used.
41/// - `component_source`: Optional details about the component to be loaded, including its name
42///   and source file path. If `None`, the REPL will try to load all the components using the
43///   `dependency_manager`, otherwise, `dependency_manager` will load only the specified component.
44/// - `prompt`: optional custom prompt, defaults to `>>>` in cyan
45pub struct RibReplConfig {
46    pub history_file: Option<PathBuf>,
47    pub dependency_manager: Arc<dyn RibDependencyManager + Sync + Send>,
48    pub worker_function_invoke: Arc<dyn WorkerFunctionInvoke + Sync + Send>,
49    pub printer: Option<Box<dyn ReplPrinter>>,
50    pub component_source: Option<ComponentSource>,
51    pub prompt: Option<String>,
52    pub command_registry: Option<CommandRegistry>,
53}
54
55/// The REPL environment for Rib, providing an interactive shell for executing Rib code.
56pub struct RibRepl {
57    printer: Box<dyn ReplPrinter>,
58    editor: Editor<RibEdit, DefaultHistory>,
59    repl_state: Arc<ReplState>,
60    prompt: String,
61    command_registry: CommandRegistry,
62}
63
64impl RibRepl {
65    /// Bootstraps and initializes the Rib REPL environment and returns a `RibRepl` instance,
66    /// which can be used to `run` the REPL.
67    ///
68    pub async fn bootstrap(config: RibReplConfig) -> Result<RibRepl, ReplBootstrapError> {
69        let history_file_path = config.history_file.unwrap_or_else(get_default_history_file);
70
71        let mut command_registry = CommandRegistry::built_in();
72        let external_commands = config.command_registry.unwrap_or_default();
73        command_registry.merge(external_commands);
74
75        let helper = RibEdit::init(&command_registry);
76
77        let mut rl = Editor::<RibEdit, DefaultHistory>::with_history(
78            Config::default(),
79            DefaultHistory::new(),
80        )
81        .unwrap();
82
83        rl.set_helper(Some(helper));
84
85        if history_file_path.exists() {
86            if let Err(err) = rl.load_history(&history_file_path) {
87                return Err(ReplBootstrapError::ReplHistoryFileError(format!(
88                    "Failed to load history: {}. Starting with an empty history.",
89                    err
90                )));
91            }
92        }
93
94        let component_dependencies = match config.component_source {
95            Some(ref details) => {
96                let component_dependency = config
97                    .dependency_manager
98                    .add_component(&details.source_path, details.component_name.clone())
99                    .await
100                    .map_err(|err| ReplBootstrapError::ComponentLoadError(err.to_string()))?;
101
102                Ok(vec![component_dependency])
103            }
104            None => {
105                let dependencies = config.dependency_manager.get_dependencies().await;
106
107                match dependencies {
108                    Ok(dependencies) => {
109                        let component_dependencies = dependencies.component_dependencies;
110
111                        if component_dependencies.is_empty() {
112                            return Err(ReplBootstrapError::NoComponentsFound);
113                        }
114
115                        Ok(component_dependencies)
116                    }
117                    Err(err) => Err(ReplBootstrapError::ComponentLoadError(format!(
118                        "failed to register components: {}",
119                        err
120                    ))),
121                }
122            }
123        };
124
125        // Once https://github.com/golemcloud/golem/issues/1608 is resolved,
126        // component dependency will not be required in the REPL state
127        let repl_state = ReplState::new(
128            config.worker_function_invoke,
129            RibCompiler::new(RibCompilerConfig::new(component_dependencies?, vec![])),
130            history_file_path.clone(),
131        );
132
133        Ok(RibRepl {
134            printer: config
135                .printer
136                .unwrap_or_else(|| Box::new(DefaultReplResultPrinter)),
137            editor: rl,
138            repl_state: Arc::new(repl_state),
139            prompt: config
140                .prompt
141                .unwrap_or_else(|| ">>> ".truecolor(192, 192, 192).to_string()),
142            command_registry,
143        })
144    }
145
146    /// Reads a single line of input from the REPL prompt.
147    ///
148    /// This method is exposed for users who want to manage their own REPL loop
149    /// instead of using the built-in [`Self::run`] method.
150    pub fn read_line(&mut self) -> rustyline::Result<String> {
151        self.editor.readline(&self.prompt)
152    }
153
154    /// Executes a single line of Rib code and returns the result.
155    ///
156    /// This function is exposed for users who want to implement custom REPL loops
157    /// or integrate Rib execution into other workflows.
158    /// For a built-in REPL loop, see [`Self::run`].
159    pub async fn execute(
160        &mut self,
161        script_or_command: &str,
162    ) -> Result<Option<RibResult>, RibExecutionError> {
163        let script_or_command = CommandOrExpr::from_str(script_or_command, &self.command_registry)
164            .map_err(RibExecutionError::Custom)?;
165
166        match script_or_command {
167            CommandOrExpr::Command { args, executor } => {
168                let mut repl_context =
169                    ReplContext::new(self.printer.as_ref(), &self.repl_state, &mut self.editor);
170
171                executor.run(args.as_str(), &mut repl_context);
172
173                Ok(None)
174            }
175            CommandOrExpr::RawExpr(script) => {
176                // If the script is empty, we do not execute it
177                if !script.is_empty() {
178                    let rib = script.strip_suffix(";").unwrap_or(script.as_str()).trim();
179
180                    self.repl_state.update_rib(rib);
181
182                    // Add every rib script into the history (in memory) and save it
183                    // regardless of whether it compiles or not
184                    // History is never used for any progressive compilation or interpretation
185                    let _ = self.editor.add_history_entry(rib);
186                    let _ = self
187                        .editor
188                        .save_history(self.repl_state.history_file_path());
189
190                    match compile_rib_script(&self.current_rib_program(), self.repl_state.clone()) {
191                        Ok(compiler_output) => {
192                            let rib_edit = self.editor.helper_mut().unwrap();
193
194                            rib_edit.update_progression(&compiler_output);
195
196                            let result =
197                                eval(compiler_output.rib_byte_code, &self.repl_state).await;
198
199                            match result {
200                                Ok(result) => Ok(Some(result)),
201                                Err(err) => {
202                                    self.repl_state.remove_last_rib_expression();
203
204                                    Err(RibExecutionError::RibRuntimeError(err))
205                                }
206                            }
207                        }
208                        Err(err) => {
209                            self.repl_state.remove_last_rib_expression();
210
211                            Err(RibExecutionError::RibCompilationError(err))
212                        }
213                    }
214                } else {
215                    Ok(None)
216                }
217            }
218        }
219    }
220
221    /// Starts the default REPL loop for executing Rib code interactively.
222    ///
223    /// This is a convenience method that repeatedly reads user input and executes
224    /// it using [`Self::execute`]. If you need more control over the REPL behavior,
225    /// use [`Self::read_line`] and [`Self::execute`] directly.
226    pub async fn run(&mut self) {
227        loop {
228            let readline = self.read_line();
229            match readline {
230                Ok(rib) => {
231                    let result = self.execute(rib.as_str()).await;
232
233                    match result {
234                        Ok(Some(result)) => {
235                            self.printer.print_rib_result(&result);
236                        }
237
238                        Ok(None) => {}
239
240                        Err(err) => match err {
241                            RibExecutionError::RibRuntimeError(runtime_error) => {
242                                self.printer.print_rib_runtime_error(&runtime_error);
243                            }
244                            RibExecutionError::RibCompilationError(runtime_error) => {
245                                self.printer.print_rib_compilation_error(&runtime_error);
246                            }
247                            RibExecutionError::Custom(custom_error) => {
248                                self.printer.print_custom_error(&custom_error);
249                            }
250                        },
251                    }
252                }
253                Err(ReadlineError::Eof) | Err(ReadlineError::Interrupted) => break,
254                Err(_) => continue,
255            }
256        }
257    }
258
259    fn current_rib_program(&self) -> String {
260        self.repl_state.current_rib_program()
261    }
262}
263
264enum CommandOrExpr {
265    Command {
266        args: String,
267        executor: Arc<dyn UntypedCommand>,
268    },
269    RawExpr(String),
270}
271
272impl CommandOrExpr {
273    pub fn from_str(input: &str, command_registry: &CommandRegistry) -> Result<Self, String> {
274        if input.starts_with(":") {
275            let repl_input = input.split_whitespace().collect::<Vec<&str>>();
276
277            let command_name = repl_input
278                .first()
279                .map(|x| x.strip_prefix(":").unwrap_or(x).trim())
280                .ok_or("Expecting a command name after `:`".to_string())?;
281
282            let input_args = repl_input[1..].join(" ");
283
284            let command = command_registry
285                .get_command(command_name)
286                .map(|command| CommandOrExpr::Command {
287                    args: input_args,
288                    executor: command,
289                })
290                .ok_or_else(|| format!("Command '{}' not found", command_name))?;
291
292            Ok(command)
293        } else {
294            Ok(CommandOrExpr::RawExpr(input.trim().to_string()))
295        }
296    }
297}
298
299/// Represents the source of a component in the REPL session.
300///
301/// The `source_path` must include the full file path,
302/// including the `.wasm` file (e.g., `"/path/to/shopping-cart.wasm"`).
303pub struct ComponentSource {
304    /// The name of the component
305    pub component_name: String,
306
307    /// The full file path to the WebAssembly source file, including the `.wasm` extension.
308    pub source_path: PathBuf,
309}
310
311fn get_default_history_file() -> PathBuf {
312    let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
313    path.push(".rib_history");
314    path
315}