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: {err}. Starting with an empty history."
89                )));
90            }
91        }
92
93        let component_dependencies = match config.component_source {
94            Some(ref details) => {
95                let component_dependency = config
96                    .dependency_manager
97                    .add_component(&details.source_path, details.component_name.clone())
98                    .await
99                    .map_err(|err| ReplBootstrapError::ComponentLoadError(err.to_string()))?;
100
101                Ok(vec![component_dependency])
102            }
103            None => {
104                let dependencies = config.dependency_manager.get_dependencies().await;
105
106                match dependencies {
107                    Ok(dependencies) => {
108                        let component_dependencies = dependencies.component_dependencies;
109
110                        if component_dependencies.is_empty() {
111                            return Err(ReplBootstrapError::NoComponentsFound);
112                        }
113
114                        Ok(component_dependencies)
115                    }
116                    Err(err) => Err(ReplBootstrapError::ComponentLoadError(format!(
117                        "failed to register components: {err}"
118                    ))),
119                }
120            }
121        };
122
123        // Once https://github.com/golemcloud/golem/issues/1608 is resolved,
124        // component dependency will not be required in the REPL state
125        let repl_state = ReplState::new(
126            config.worker_function_invoke,
127            RibCompiler::new(RibCompilerConfig::new(component_dependencies?, vec![])),
128            history_file_path.clone(),
129        );
130
131        Ok(RibRepl {
132            printer: config
133                .printer
134                .unwrap_or_else(|| Box::new(DefaultReplResultPrinter)),
135            editor: rl,
136            repl_state: Arc::new(repl_state),
137            prompt: config
138                .prompt
139                .unwrap_or_else(|| ">>> ".truecolor(192, 192, 192).to_string()),
140            command_registry,
141        })
142    }
143
144    /// Reads a single line of input from the REPL prompt.
145    ///
146    /// This method is exposed for users who want to manage their own REPL loop
147    /// instead of using the built-in [`Self::run`] method.
148    pub fn read_line(&mut self) -> rustyline::Result<String> {
149        self.editor.readline(&self.prompt)
150    }
151
152    /// Executes a single line of Rib code and returns the result.
153    ///
154    /// This function is exposed for users who want to implement custom REPL loops
155    /// or integrate Rib execution into other workflows.
156    /// For a built-in REPL loop, see [`Self::run`].
157    pub async fn execute(
158        &mut self,
159        script_or_command: &str,
160    ) -> Result<Option<RibResult>, RibExecutionError> {
161        let script_or_command = CommandOrExpr::from_str(script_or_command, &self.command_registry)
162            .map_err(RibExecutionError::Custom)?;
163
164        match script_or_command {
165            CommandOrExpr::Command { args, executor } => {
166                let mut repl_context =
167                    ReplContext::new(self.printer.as_ref(), &self.repl_state, &mut self.editor);
168
169                executor.run(args.as_str(), &mut repl_context);
170
171                Ok(None)
172            }
173            CommandOrExpr::RawExpr(script) => {
174                // If the script is empty, we do not execute it
175                if !script.is_empty() {
176                    let rib = script.strip_suffix(";").unwrap_or(script.as_str()).trim();
177
178                    self.repl_state.update_rib(rib);
179
180                    // Add every rib script into the history (in memory) and save it
181                    // regardless of whether it compiles or not
182                    // History is never used for any progressive compilation or interpretation
183                    let _ = self.editor.add_history_entry(rib);
184                    let _ = self
185                        .editor
186                        .save_history(self.repl_state.history_file_path());
187
188                    match compile_rib_script(&self.current_rib_program(), self.repl_state.clone()) {
189                        Ok(compiler_output) => {
190                            let rib_edit = self.editor.helper_mut().unwrap();
191
192                            rib_edit.update_progression(&compiler_output);
193
194                            let result =
195                                eval(compiler_output.rib_byte_code, &self.repl_state).await;
196
197                            match result {
198                                Ok(result) => Ok(Some(result)),
199                                Err(err) => {
200                                    self.repl_state.remove_last_rib_expression();
201
202                                    Err(RibExecutionError::RibRuntimeError(err))
203                                }
204                            }
205                        }
206                        Err(err) => {
207                            self.repl_state.remove_last_rib_expression();
208
209                            Err(RibExecutionError::RibCompilationError(err))
210                        }
211                    }
212                } else {
213                    Ok(None)
214                }
215            }
216        }
217    }
218
219    /// Starts the default REPL loop for executing Rib code interactively.
220    ///
221    /// This is a convenience method that repeatedly reads user input and executes
222    /// it using [`Self::execute`]. If you need more control over the REPL behavior,
223    /// use [`Self::read_line`] and [`Self::execute`] directly.
224    pub async fn run(&mut self) {
225        loop {
226            let readline = self.read_line();
227            match readline {
228                Ok(rib) => {
229                    let result = self.execute(rib.as_str()).await;
230
231                    match result {
232                        Ok(Some(result)) => {
233                            self.printer.print_rib_result(&result);
234                        }
235
236                        Ok(None) => {}
237
238                        Err(err) => match err {
239                            RibExecutionError::RibRuntimeError(runtime_error) => {
240                                self.printer.print_rib_runtime_error(&runtime_error);
241                            }
242                            RibExecutionError::RibCompilationError(runtime_error) => {
243                                self.printer.print_rib_compilation_error(&runtime_error);
244                            }
245                            RibExecutionError::Custom(custom_error) => {
246                                self.printer.print_custom_error(&custom_error);
247                            }
248                        },
249                    }
250                }
251                Err(ReadlineError::Eof) | Err(ReadlineError::Interrupted) => break,
252                Err(_) => continue,
253            }
254        }
255    }
256
257    fn current_rib_program(&self) -> String {
258        self.repl_state.current_rib_program()
259    }
260}
261
262enum CommandOrExpr {
263    Command {
264        args: String,
265        executor: Arc<dyn UntypedCommand>,
266    },
267    RawExpr(String),
268}
269
270impl CommandOrExpr {
271    pub fn from_str(input: &str, command_registry: &CommandRegistry) -> Result<Self, String> {
272        if input.starts_with(":") {
273            let repl_input = input.split_whitespace().collect::<Vec<&str>>();
274
275            let command_name = repl_input
276                .first()
277                .map(|x| x.strip_prefix(":").unwrap_or(x).trim())
278                .ok_or("Expecting a command name after `:`".to_string())?;
279
280            let input_args = repl_input[1..].join(" ");
281
282            let command = command_registry
283                .get_command(command_name)
284                .map(|command| CommandOrExpr::Command {
285                    args: input_args,
286                    executor: command,
287                })
288                .ok_or_else(|| format!("Command '{command_name}' not found"))?;
289
290            Ok(command)
291        } else {
292            Ok(CommandOrExpr::RawExpr(input.trim().to_string()))
293        }
294    }
295}
296
297/// Represents the source of a component in the REPL session.
298///
299/// The `source_path` must include the full file path,
300/// including the `.wasm` file (e.g., `"/path/to/shopping-cart.wasm"`).
301pub struct ComponentSource {
302    /// The name of the component
303    pub component_name: String,
304
305    /// The full file path to the WebAssembly source file, including the `.wasm` extension.
306    pub source_path: PathBuf,
307}
308
309fn get_default_history_file() -> PathBuf {
310    let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
311    path.push(".rib_history");
312    path
313}