golem_rib_repl/
rib_repl.rs

1// Copyright 2024-2025 Golem Cloud
2//
3// Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
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::{RibComponentMetadata, RibDependencyManager};
17use crate::invoke::WorkerFunctionInvoke;
18use crate::repl_printer::{DefaultReplResultPrinter, ReplPrinter};
19use crate::repl_state::ReplState;
20use crate::rib_edit::RibEdit;
21use async_trait::async_trait;
22use colored::Colorize;
23use golem_wasm_rpc::ValueAndType;
24use rib::RibResult;
25use rib::{EvaluatedFnArgs, EvaluatedFqFn, EvaluatedWorkerName, RibByteCode};
26use rib::{RibError, RibFunctionInvoke};
27use rustyline::error::ReadlineError;
28use rustyline::history::DefaultHistory;
29use rustyline::{Config, Editor};
30use std::path::PathBuf;
31use std::sync::Arc;
32
33/// The REPL environment for Rib, providing an interactive shell for executing Rib code.
34pub struct RibRepl {
35    history_file_path: PathBuf,
36    printer: Box<dyn ReplPrinter>,
37    editor: Editor<RibEdit, DefaultHistory>,
38    repl_state: ReplState,
39}
40
41impl RibRepl {
42    /// Bootstraps and initializes the Rib REPL environment and returns a `RibRepl` instance,
43    /// which can be used to `run` the REPL.
44    ///
45    /// # Arguments
46    ///
47    /// - `history_file`: Optional path to a file where the REPL history will be stored and loaded from.
48    ///   If `None`, it will be loaded from `~/.rib_history`.
49    /// - `dependency_manager`: This is responsible for how to load all the components or a specific
50    ///   custom component.
51    /// - `worker_function_invoke`: An implementation of the `WorkerFunctionInvoke` trait,
52    /// - `printer`: Optional custom printer for displaying results and errors in the REPL. If `None`
53    ///   a default printer will be used.
54    /// - `component_source`: Optional details about the component to be loaded, including its name
55    ///   and source file path. If `None`, the REPL will try to load all the components using the
56    ///   `dependency_manager`, otherwise, `dependency_manager` will load only the specified component.
57    ///
58    pub async fn bootstrap(
59        history_file: Option<PathBuf>,
60        dependency_manager: Arc<dyn RibDependencyManager + Sync + Send>,
61        worker_function_invoke: Arc<dyn WorkerFunctionInvoke + Sync + Send>,
62        printer: Option<Box<dyn ReplPrinter>>,
63        component_source: Option<ComponentSource>,
64    ) -> Result<RibRepl, ReplBootstrapError> {
65        let history_file_path = history_file.unwrap_or_else(get_default_history_file);
66
67        let rib_editor = RibEdit::init();
68
69        let mut rl = Editor::<RibEdit, DefaultHistory>::with_history(
70            Config::default(),
71            DefaultHistory::new(),
72        )
73        .unwrap();
74
75        rl.set_helper(Some(rib_editor));
76
77        if history_file_path.exists() {
78            if let Err(err) = rl.load_history(&history_file_path) {
79                return Err(ReplBootstrapError::ReplHistoryFileError(format!(
80                    "Failed to load history: {}. Starting with an empty history.",
81                    err
82                )));
83            }
84        }
85
86        let component_dependency = match component_source {
87            Some(ref details) => dependency_manager
88                .add_component(&details.source_path, details.component_name.clone())
89                .await
90                .map_err(ReplBootstrapError::ComponentLoadError),
91            None => {
92                let dependencies = dependency_manager.get_dependencies().await;
93
94                match dependencies {
95                    Ok(dependencies) => {
96                        let component_dependencies = dependencies.component_dependencies;
97
98                        match &component_dependencies.len() {
99                            0 => Err(ReplBootstrapError::NoComponentsFound),
100                            1 => Ok(component_dependencies[0].clone()),
101                            _ => Err(ReplBootstrapError::MultipleComponentsFound(
102                                "multiple components detected. rib repl currently support only a single component".to_string(),
103                            )),
104                        }
105                    }
106                    Err(err) => Err(ReplBootstrapError::ComponentLoadError(format!(
107                        "failed to register components: {}",
108                        err
109                    ))),
110                }
111            }
112        }?;
113
114        let rib_function_invoke = Arc::new(ReplRibFunctionInvoke::new(
115            component_dependency.clone(),
116            worker_function_invoke,
117        ));
118
119        let repl_state = ReplState::new(&component_dependency, rib_function_invoke);
120
121        Ok(RibRepl {
122            history_file_path,
123            printer: printer.unwrap_or_else(|| Box::new(DefaultReplResultPrinter)),
124            editor: rl,
125            repl_state,
126        })
127    }
128
129    /// Reads a single line of input from the REPL prompt.
130    ///
131    /// This method is exposed for users who want to manage their own REPL loop
132    /// instead of using the built-in [`Self::run`] method.
133    pub fn read_line(&mut self) -> rustyline::Result<String> {
134        self.editor.readline(">>> ".magenta().to_string().as_str())
135    }
136
137    /// Executes a single line of Rib code and returns the result.
138    ///
139    /// This function is exposed for users who want to implement custom REPL loops
140    /// or integrate Rib execution into other workflows.
141    /// For a built-in REPL loop, see [`Self::run`].
142    pub async fn execute_rib(&mut self, rib: &str) -> Result<Option<RibResult>, RibError> {
143        if !rib.is_empty() {
144            self.update_rib(rib);
145
146            // Add every rib script into the history (in memory) and save it
147            // regardless of whether it compiles or not
148            // History is never used for any progressive compilation or interpretation
149            let _ = self.editor.add_history_entry(rib);
150            let _ = self.editor.save_history(&self.history_file_path);
151
152            match compile_rib_script(&self.current_rib_program(), &mut self.repl_state) {
153                Ok(compilation) => {
154                    let helper = self.editor.helper_mut().unwrap();
155
156                    helper.update_progression(&compilation);
157
158                    // Before evaluation
159                    let result = eval(compilation.rib_byte_code, &mut self.repl_state).await;
160                    match result {
161                        Ok(result) => {
162                            self.printer.print_rib_result(&result);
163                            Ok(Some(result))
164                        }
165                        Err(err) => {
166                            self.remove_rib_text_in_session();
167                            self.printer.print_runtime_error(&err);
168                            Err(RibError::InternalError(err))
169                        }
170                    }
171                }
172                Err(err) => {
173                    self.remove_rib_text_in_session();
174                    self.printer.print_rib_error(&err);
175                    Err(RibError::InternalError(err.to_string()))
176                }
177            }
178        } else {
179            Ok(None)
180        }
181    }
182
183    /// Dynamically updates the REPL session with a new component dependency.
184    ///
185    /// This method is intended for use in interactive or user-controlled REPL loops,
186    /// allowing runtime replacement of active component metadata.
187    ///
188    /// Note: Currently, only a single component is supported per session. Multi-component
189    /// support is planned but not yet implemented.
190    pub fn update_component_dependency(&mut self, dependency: RibComponentMetadata) {
191        self.repl_state.update_dependency(dependency);
192    }
193
194    /// Starts the default REPL loop for executing Rib code interactively.
195    ///
196    /// This is a convenience method that repeatedly reads user input and executes
197    /// it using [`Self::execute_rib`]. If you need more control over the REPL behavior,
198    /// use [`Self::read_line`] and [`Self::execute_rib`] directly.
199    pub async fn run(&mut self) {
200        loop {
201            let readline = self.editor.readline(">>> ".magenta().to_string().as_str());
202            match readline {
203                Ok(rib) => {
204                    let _ = self.execute_rib(&rib).await;
205                }
206                Err(ReadlineError::Eof) | Err(ReadlineError::Interrupted) => break,
207                Err(_) => continue,
208            }
209        }
210    }
211
212    fn update_rib(&mut self, rib_text: &str) {
213        self.repl_state.update_rib(rib_text);
214    }
215
216    fn remove_rib_text_in_session(&mut self) {
217        self.repl_state.pop_rib_text()
218    }
219
220    fn current_rib_program(&self) -> String {
221        self.repl_state.current_rib_program()
222    }
223}
224
225/// Represents the source of a component in the REPL session.
226///
227/// The `source_path` must include the full file path,
228/// including the `.wasm` file (e.g., `"/path/to/shopping-cart.wasm"`).
229pub struct ComponentSource {
230    /// The name of the component
231    pub component_name: String,
232
233    /// The full file path to the WebAssembly source file, including the `.wasm` extension.
234    pub source_path: PathBuf,
235}
236
237async fn eval(rib_byte_code: RibByteCode, repl_state: &mut ReplState) -> Result<RibResult, String> {
238    repl_state
239        .interpreter()
240        .run(rib_byte_code)
241        .await
242        .map_err(|e| format!("Runtime error: {}", e))
243}
244
245fn get_default_history_file() -> PathBuf {
246    let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
247    path.push(".rib_history");
248    path
249}
250
251/// Represents errors that can occur during the bootstrap phase of the Rib REPL environment.
252#[derive(Debug, Clone)]
253pub enum ReplBootstrapError {
254    /// Multiple components were found, but the REPL requires a single component context.
255    ///
256    /// To resolve this, either:
257    /// - Ensure the context includes only one component, or
258    /// - Explicitly specify the component to load when starting the REPL.
259    ///
260    /// In the future, Rib will support multiple components
261    MultipleComponentsFound(String),
262
263    /// No components were found in the given context.
264    NoComponentsFound,
265
266    /// Failed to load a specified component.
267    ComponentLoadError(String),
268
269    /// Failed to read from or write to the REPL history file.
270    ReplHistoryFileError(String),
271}
272
273// Note: Currently, the Rib interpreter supports only one component, so the
274// `RibFunctionInvoke` trait in the `golem-rib` module does not include `component_id` in
275// the `invoke` arguments. It only requires the optional worker name, function name, and arguments.
276// Once multi-component support is added, the trait will be updated to include `component_id`,
277// and we can use it directly instead of `WorkerFunctionInvoke` in the `golem-rib-repl` module.
278struct ReplRibFunctionInvoke {
279    component_dependency: RibComponentMetadata,
280    worker_function_invoke: Arc<dyn WorkerFunctionInvoke + Sync + Send>,
281}
282
283impl ReplRibFunctionInvoke {
284    fn new(
285        component_dependency: RibComponentMetadata,
286        worker_function_invoke: Arc<dyn WorkerFunctionInvoke + Sync + Send>,
287    ) -> Self {
288        Self {
289            component_dependency,
290            worker_function_invoke,
291        }
292    }
293}
294
295#[async_trait]
296impl RibFunctionInvoke for ReplRibFunctionInvoke {
297    async fn invoke(
298        &self,
299        worker_name: Option<EvaluatedWorkerName>,
300        function_name: EvaluatedFqFn,
301        args: EvaluatedFnArgs,
302    ) -> Result<ValueAndType, String> {
303        let component_id = self.component_dependency.component_id;
304
305        self.worker_function_invoke
306            .invoke(component_id, worker_name, function_name, args)
307            .await
308    }
309}