Skip to main content

orust_runtime/
lib.rs

1pub use async_trait;
2pub use futures;
3pub use futures::StreamExt;
4use std::pin::Pin;
5use std::sync::Arc;
6pub use tokio;
7
8pub type Stream<T> = Pin<Box<dyn futures::Stream<Item = T> + Send>>;
9
10pub fn runtime_error_code(message: &str) -> &'static str {
11    if message.contains("out of range for a List") {
12        "OR0010"
13    } else if message.contains("already borrowed") {
14        "OR0011"
15    } else if message.contains("not a char boundary") {
16        "OR0012"
17    } else if message.contains("attempt to divide by zero") {
18        "OR0013"
19    } else if message.contains("called `Option::unwrap() on a `None` value")
20        || message.contains("called `Option::unwrap()` on a `None` value")
21    {
22        "OR0014"
23    } else if message.contains("overflow") {
24        "OR0008"
25    } else {
26        "OR0099"
27    }
28}
29
30pub fn friendly_panic_message(message: &str) -> String {
31    if let Some(rest) = message.strip_prefix("byte index ") {
32        if let Some(index) = rest.split_whitespace().next() {
33            return format!("byte {index} is in the middle of a character");
34        }
35    }
36    if message.contains("attempt to divide by zero") {
37        return "integer division by zero".into();
38    }
39    if message.contains("called `Option::unwrap()` on a `None` value") {
40        return "tried to unwrap null; check the value before using it".into();
41    }
42    message.into()
43}
44
45pub fn install_panic_hook() {
46    std::panic::set_hook(Box::new(|panic| {
47        let message = panic
48            .payload()
49            .downcast_ref::<&str>()
50            .copied()
51            .or_else(|| panic.payload().downcast_ref::<String>().map(String::as_str))
52            .unwrap_or("runtime panic");
53        let code = runtime_error_code(message);
54        let message = friendly_panic_message(message);
55        if std::env::var_os("ORUST_BACKTRACE").is_some() {
56            eprintln!("ORust runtime error [{code}]: {message}");
57            if let Some(location) = panic.location() {
58                eprintln!(
59                    "at {}:{}:{}",
60                    location.file(),
61                    location.line(),
62                    location.column()
63                );
64            }
65        } else {
66            eprintln!("ORust runtime error [{code}]: {message}");
67        }
68    }));
69}
70
71pub fn display_option<T: std::fmt::Display>(value: &Option<T>) -> String {
72    match value {
73        Some(value) => value.to_string(),
74        None => "null".into(),
75    }
76}
77
78pub struct Channel<T> {
79    sender: tokio::sync::mpsc::Sender<T>,
80    receiver: Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<T>>>,
81}
82
83pub fn channel<T: Send + 'static>(capacity: usize) -> Channel<T> {
84    let (sender, receiver) = tokio::sync::mpsc::channel(capacity);
85    Channel {
86        sender,
87        receiver: Arc::new(tokio::sync::Mutex::new(receiver)),
88    }
89}
90
91impl<T: Send + 'static> Clone for Channel<T> {
92    fn clone(&self) -> Self {
93        Self {
94            sender: self.sender.clone(),
95            receiver: Arc::clone(&self.receiver),
96        }
97    }
98}
99
100impl<T: Send + 'static> Channel<T> {
101    pub async fn send(&self, value: T) -> Result<(), Error> {
102        self.sender
103            .send(value)
104            .await
105            .map_err(|_| Error::new("channel receiver was dropped"))
106    }
107
108    pub async fn recv(&self) -> Option<T> {
109        self.receiver.lock().await.recv().await
110    }
111}
112pub use tokio::time::{sleep, Duration};
113
114#[derive(Debug)]
115pub struct Error {
116    pub message: String,
117}
118impl Error {
119    pub fn new(message: impl Into<String>) -> Self {
120        Self {
121            message: message.into(),
122        }
123    }
124}
125
126impl<E> From<E> for Error
127where
128    E: std::error::Error + Send + Sync + 'static,
129{
130    fn from(error: E) -> Self {
131        Self::new(error.to_string())
132    }
133}
134
135pub struct Task<T>(tokio::task::JoinHandle<T>);
136impl<T> Task<T> {
137    pub async fn await_task(self) -> Result<T, Error> {
138        self.0.await.map_err(|error| Error::new(error.to_string()))
139    }
140}
141pub fn spawn<T: Send + 'static>(
142    future: impl std::future::Future<Output = T> + Send + 'static,
143) -> Task<T> {
144    Task(tokio::spawn(future))
145}
146
147pub fn millis(value: u64) -> Duration {
148    Duration::from_millis(value)
149}
150pub fn seconds(value: u64) -> Duration {
151    Duration::from_secs(value)
152}
153
154#[allow(non_upper_case_globals)]
155pub mod std_math {
156    pub const pi: f64 = std::f64::consts::PI;
157    pub const e: f64 = std::f64::consts::E;
158
159    pub fn sqrt(value: f64) -> f64 {
160        value.sqrt()
161    }
162    pub fn pow(value: f64, exponent: f64) -> f64 {
163        value.powf(exponent)
164    }
165    pub fn sin(value: f64) -> f64 {
166        value.sin()
167    }
168    pub fn cos(value: f64) -> f64 {
169        value.cos()
170    }
171    pub fn tan(value: f64) -> f64 {
172        value.tan()
173    }
174    pub fn log(value: f64) -> f64 {
175        value.ln()
176    }
177    pub fn exp(value: f64) -> f64 {
178        value.exp()
179    }
180    pub fn min(left: f64, right: f64) -> f64 {
181        left.min(right)
182    }
183    pub fn max(left: f64, right: f64) -> f64 {
184        left.max(right)
185    }
186    pub fn abs(value: f64) -> f64 {
187        value.abs()
188    }
189    pub fn floor(value: f64) -> f64 {
190        value.floor()
191    }
192    pub fn ceil(value: f64) -> f64 {
193        value.ceil()
194    }
195}
196
197pub mod std_env {
198    pub fn args() -> Vec<String> {
199        std::env::args().collect()
200    }
201
202    pub fn get(name: String) -> Option<String> {
203        std::env::var(name).ok()
204    }
205
206    pub fn set(name: String, value: String) {
207        std::env::set_var(name, value);
208    }
209
210    pub fn cwd() -> String {
211        std::env::current_dir()
212            .ok()
213            .and_then(|path| path.into_os_string().into_string().ok())
214            .unwrap_or_default()
215    }
216}
217
218pub mod std_io {
219    use super::Error;
220
221    pub struct File;
222
223    impl File {
224        pub fn exists(path: String) -> bool {
225            std::path::Path::new(&path).exists()
226        }
227
228        pub fn read_string(path: String) -> Result<String, Error> {
229            std::fs::read_to_string(path).map_err(|error| Error::new(error.to_string()))
230        }
231
232        pub fn write_string(path: String, text: String) -> Result<(), Error> {
233            std::fs::write(path, text).map_err(|error| Error::new(error.to_string()))
234        }
235
236        pub fn append_string(path: String, text: String) -> Result<(), Error> {
237            use std::io::Write;
238            let mut file = std::fs::OpenOptions::new()
239                .create(true)
240                .append(true)
241                .open(path)
242                .map_err(|error| Error::new(error.to_string()))?;
243            file.write_all(text.as_bytes())
244                .map_err(|error| Error::new(error.to_string()))
245        }
246    }
247
248    pub struct Path;
249
250    impl Path {
251        pub fn join(left: String, right: String) -> String {
252            std::path::Path::new(&left)
253                .join(right)
254                .to_string_lossy()
255                .into_owned()
256        }
257
258        pub fn parent(path: String) -> Option<String> {
259            std::path::Path::new(&path)
260                .parent()
261                .map(|value| value.to_string_lossy().into_owned())
262        }
263
264        pub fn file_name(path: String) -> Option<String> {
265            std::path::Path::new(&path)
266                .file_name()
267                .map(|value| value.to_string_lossy().into_owned())
268        }
269    }
270}
271
272pub fn checked_index(index: i64, length: usize) -> usize {
273    if index < 0 || index as usize >= length {
274        panic!(
275            "index {} is out of range for a List of length {}",
276            index, length
277        );
278    }
279    index as usize
280}
281
282#[cfg(test)]
283mod tests {
284    use super::{checked_index, friendly_panic_message, runtime_error_code};
285
286    #[test]
287    fn accepts_in_range_indices() {
288        assert_eq!(checked_index(1, 3), 1);
289    }
290
291    #[test]
292    #[should_panic(expected = "index -1 is out of range for a List of length 3")]
293    fn rejects_negative_indices() {
294        checked_index(-1, 3);
295    }
296
297    #[test]
298    #[should_panic(expected = "index 5 is out of range for a List of length 3")]
299    fn rejects_indices_at_or_above_length() {
300        checked_index(5, 3);
301    }
302
303    #[test]
304    fn assigns_stable_runtime_error_codes() {
305        assert_eq!(
306            runtime_error_code("index -1 is out of range for a List of length 3"),
307            "OR0010"
308        );
309        assert_eq!(
310            runtime_error_code("already borrowed: BorrowMutError"),
311            "OR0011"
312        );
313        assert_eq!(runtime_error_code("unexpected panic"), "OR0099");
314    }
315
316    #[test]
317    fn translates_string_boundary_panics() {
318        let raw = "byte index 3 is not a char boundary; it is inside 'é'";
319        assert_eq!(runtime_error_code(raw), "OR0012");
320        assert_eq!(
321            friendly_panic_message(raw),
322            "byte 3 is in the middle of a character"
323        );
324    }
325
326    #[test]
327    fn translates_common_numeric_and_option_panics() {
328        assert_eq!(runtime_error_code("attempt to divide by zero"), "OR0013");
329        assert_eq!(runtime_error_code("attempt to add with overflow"), "OR0008");
330        assert_eq!(
331            runtime_error_code("called `Option::unwrap()` on a `None` value"),
332            "OR0014"
333        );
334    }
335}