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::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
33pub struct RibRepl {
34    history_file_path: PathBuf,
35    printer: Box<dyn ReplPrinter>,
36    editor: Editor<RibEdit, DefaultHistory>,
37    repl_state: ReplState,
38}
39
40impl RibRepl {
41    pub fn read_line(&mut self) -> rustyline::Result<String> {
42        self.editor.readline(">>> ".magenta().to_string().as_str())
43    }
44
45    pub async fn bootstrap(
46        history_file: Option<PathBuf>,
47        dependency_manager: Arc<dyn RibDependencyManager + Sync + Send>,
48        worker_function_invoke: Arc<dyn WorkerFunctionInvoke + Sync + Send>,
49        printer: Box<dyn ReplPrinter>,
50        component_details: Option<ComponentDetails>,
51    ) -> Result<RibRepl, ReplBootstrapError> {
52        let history_file_path = history_file.unwrap_or_else(get_default_history_file);
53
54        let rib_editor = RibEdit::init();
55
56        let mut rl = Editor::<RibEdit, DefaultHistory>::with_history(
57            Config::default(),
58            DefaultHistory::new(),
59        )
60        .unwrap();
61
62        rl.set_helper(Some(rib_editor));
63
64        if history_file_path.exists() {
65            if let Err(err) = rl.load_history(&history_file_path) {
66                return Err(ReplBootstrapError::ReplHistoryFileError(format!(
67                    "Failed to load history: {}. Starting with an empty history.",
68                    err
69                )));
70            }
71        }
72
73        let component_dependency = match component_details {
74            Some(ref details) => dependency_manager
75                .add_component(&details.source_path, details.component_name.clone())
76                .await
77                .map_err(ReplBootstrapError::ComponentLoadError),
78            None => {
79                let dependencies = dependency_manager.get_dependencies().await;
80
81                match dependencies {
82                    Ok(dependencies) => {
83                        let component_dependencies = dependencies.component_dependencies;
84
85                        match &component_dependencies.len() {
86                            0 => Err(ReplBootstrapError::NoComponentsFound),
87                            1 => Ok(component_dependencies[0].clone()),
88                            _ => Err(ReplBootstrapError::MultipleComponentsFound(
89                                "multiple components detected. rib repl currently support only a single component".to_string(),
90                            )),
91                        }
92                    }
93                    Err(err) => Err(ReplBootstrapError::ComponentLoadError(format!(
94                        "failed to register components: {}",
95                        err
96                    ))),
97                }
98            }
99        }?;
100
101        let rib_function_invoke = Arc::new(ReplRibFunctionInvoke::new(
102            component_dependency.clone(),
103            worker_function_invoke,
104        ));
105
106        let repl_state = ReplState::new(&component_dependency, rib_function_invoke);
107
108        Ok(RibRepl {
109            history_file_path,
110            printer,
111            editor: rl,
112            repl_state,
113        })
114    }
115
116    pub async fn process_command(&mut self, prompt: &str) -> Result<Option<RibResult>, RibError> {
117        if !prompt.is_empty() {
118            self.update_rib_text_in_session(prompt);
119
120            // Add every rib script into the history (in memory) and save it
121            // regardless of whether it compiles or not
122            // History is never used for any progressive compilation or interpretation
123            let _ = self.editor.add_history_entry(prompt);
124            let _ = self.editor.save_history(&self.history_file_path);
125
126            match compile_rib_script(&self.current_rib_program(), &mut self.repl_state) {
127                Ok(compilation) => {
128                    let helper = self.editor.helper_mut().unwrap();
129
130                    helper.update_progression(&compilation);
131
132                    // Before evaluation
133                    let result = eval(compilation.rib_byte_code, &mut self.repl_state).await;
134                    match result {
135                        Ok(result) => {
136                            self.printer.print_rib_result(&result);
137                            Ok(Some(result))
138                        }
139                        Err(err) => {
140                            self.remove_rib_text_in_session();
141                            self.printer.print_runtime_error(&err);
142                            Err(RibError::InternalError(err))
143                        }
144                    }
145                }
146                Err(err) => {
147                    self.remove_rib_text_in_session();
148                    self.printer.print_rib_error(&err);
149                    Err(RibError::InternalError(err.to_string()))
150                }
151            }
152        } else {
153            Ok(None)
154        }
155    }
156
157    pub async fn run(&mut self) {
158        loop {
159            let readline = self.editor.readline(">>> ".magenta().to_string().as_str());
160            match readline {
161                Ok(line) => {
162                    let _ = self.process_command(&line).await;
163                }
164                Err(ReadlineError::Eof) | Err(ReadlineError::Interrupted) => break,
165                Err(_) => continue,
166            }
167        }
168    }
169
170    fn update_rib_text_in_session(&mut self, rib_text: &str) {
171        self.repl_state.update_rib_text(rib_text);
172    }
173
174    fn remove_rib_text_in_session(&mut self) {
175        self.repl_state.pop_rib_text()
176    }
177
178    fn current_rib_program(&self) -> String {
179        self.repl_state.current_rib_program()
180    }
181}
182
183pub struct ComponentDetails {
184    // Name of the component
185    pub component_name: String,
186    // The complete path of the wasm source file
187    pub source_path: PathBuf,
188}
189
190async fn eval(rib_byte_code: RibByteCode, repl_state: &mut ReplState) -> Result<RibResult, String> {
191    repl_state
192        .interpreter()
193        .run(rib_byte_code)
194        .await
195        .map_err(|e| format!("Runtime error: {}", e))
196}
197
198fn get_default_history_file() -> PathBuf {
199    let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
200    path.push(".rib_history");
201    path
202}
203
204#[derive(Debug, Clone)]
205pub enum ReplBootstrapError {
206    // Currently not supported
207    // So either the context should have only 1 component
208    // or specifically specify the component when starting the REPL
209    // In future, when Rib supports multiple components (which may require the need
210    // of root package names being the component name)
211    MultipleComponentsFound(String),
212    NoComponentsFound,
213    ComponentLoadError(String),
214    ReplHistoryFileError(String),
215}
216
217// As of now Rib interpreter can work with only one component, and the details
218// and hence it needs to only know about worker-name, which is optional, and function arguments
219// When Rib supports multiple component, the RibFunctionInvoke will come to know
220// about the component_id and the worker_name and this indirection can be avoided
221pub struct ReplRibFunctionInvoke {
222    component_dependency: RibComponentMetadata,
223    worker_function_invoke: Arc<dyn WorkerFunctionInvoke + Sync + Send>,
224}
225
226impl ReplRibFunctionInvoke {
227    pub fn new(
228        component_dependency: RibComponentMetadata,
229        worker_function_invoke: Arc<dyn WorkerFunctionInvoke + Sync + Send>,
230    ) -> Self {
231        Self {
232            component_dependency,
233            worker_function_invoke,
234        }
235    }
236}
237
238#[async_trait]
239impl RibFunctionInvoke for ReplRibFunctionInvoke {
240    async fn invoke(
241        &self,
242        worker_name: Option<EvaluatedWorkerName>,
243        function_name: EvaluatedFqFn,
244        args: EvaluatedFnArgs,
245    ) -> Result<ValueAndType, String> {
246        let component_id = self.component_dependency.component_id;
247
248        self.worker_function_invoke
249            .invoke(component_id, worker_name, function_name, args)
250            .await
251    }
252}