Skip to main content

aver_rt/
runtime.rs

1use crate::{AverDisplay, AverList, aver_display};
2
3pub fn console_print<T: AverDisplay>(val: &T) {
4    println!("{}", aver_display(val));
5}
6
7pub fn console_error<T: AverDisplay>(val: &T) {
8    eprintln!("{}", aver_display(val));
9}
10
11pub fn console_warn<T: AverDisplay>(val: &T) {
12    eprintln!("[warn] {}", aver_display(val));
13}
14
15pub fn read_line() -> Result<String, String> {
16    let mut buf = String::new();
17    match std::io::stdin().read_line(&mut buf) {
18        Ok(0) => Err("EOF".to_string()),
19        Ok(_) => {
20            if buf.ends_with('\n') {
21                buf.pop();
22                if buf.ends_with('\r') {
23                    buf.pop();
24                }
25            }
26            Ok(buf)
27        }
28        Err(e) => Err(e.to_string()),
29    }
30}
31
32pub fn time_now() -> String {
33    let (secs, nanos) = unix_parts_now();
34    format_utc_rfc3339_like(secs, nanos)
35}
36
37pub fn time_unix_ms() -> i64 {
38    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
39    {
40        // `std::time::SystemTime::now()` panics on
41        // wasm32-unknown-unknown ("time not implemented on this
42        // platform"). Use `Date.now()` from JS — same epoch, same
43        // unit, well within i64. Symmetric with the time_sleep
44        // wasm32 no-op fallback added in 0.10.0.
45        return js_sys::Date::now() as i64;
46    }
47    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
48    {
49        let now = std::time::SystemTime::now()
50            .duration_since(std::time::UNIX_EPOCH)
51            .expect("Time.unixMs: system clock error");
52        i64::try_from(now.as_millis()).expect("Time.unixMs: value out of i64 range")
53    }
54}
55
56pub fn time_sleep(ms: i64) {
57    if ms < 0 {
58        panic!("Time.sleep: ms must be non-negative");
59    }
60    // Browsers have no synchronous sleep primitive on
61    // wasm32-unknown-unknown — std::thread::sleep panics with
62    // "can't sleep". Treat it as a no-op there; the playground's
63    // recorder still captures the call + duration in args, and
64    // replay reproduces the trace faithfully. Native builds keep
65    // real blocking sleep.
66    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
67    {
68        let _ = ms;
69        return;
70    }
71    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
72    std::thread::sleep(std::time::Duration::from_millis(ms as u64));
73}
74
75pub fn string_slice(s: &str, from: i64, to: i64) -> String {
76    let start = from.max(0) as usize;
77    let end = to.max(0) as usize;
78    if start >= end {
79        return String::new();
80    }
81
82    let mut start_byte = None;
83    let mut end_byte = None;
84    let mut char_index = 0usize;
85
86    for (byte_index, _) in s.char_indices() {
87        if char_index == start {
88            start_byte = Some(byte_index);
89        }
90        if char_index == end {
91            end_byte = Some(byte_index);
92            break;
93        }
94        char_index += 1;
95    }
96
97    if start_byte.is_none() && char_index == start {
98        start_byte = Some(s.len());
99    }
100    if end_byte.is_none() && char_index == end {
101        end_byte = Some(s.len());
102    }
103
104    let start_byte = start_byte.unwrap_or(s.len());
105    let end_byte = end_byte.unwrap_or(s.len());
106    if start_byte >= end_byte {
107        return String::new();
108    }
109
110    s[start_byte..end_byte].to_string()
111}
112
113pub fn read_text(path: &str) -> Result<String, String> {
114    std::fs::read_to_string(path).map_err(|e| e.to_string())
115}
116
117pub fn write_text(path: &str, content: &str) -> Result<(), String> {
118    std::fs::write(path, content)
119        .map(|_| ())
120        .map_err(|e| e.to_string())
121}
122
123pub fn append_text(path: &str, content: &str) -> Result<(), String> {
124    use std::io::Write;
125
126    let mut file = std::fs::OpenOptions::new()
127        .create(true)
128        .append(true)
129        .open(path)
130        .map_err(|e| e.to_string())?;
131    file.write_all(content.as_bytes())
132        .map_err(|e| e.to_string())
133}
134
135pub fn path_exists(path: &str) -> bool {
136    std::path::Path::new(path).exists()
137}
138
139pub fn delete_file(path: &str) -> Result<(), String> {
140    let p = std::path::Path::new(path);
141    if p.is_dir() {
142        return Err(
143            "Disk.delete: path is a directory — use Disk.deleteDir to remove directories"
144                .to_string(),
145        );
146    }
147    std::fs::remove_file(p)
148        .map(|_| ())
149        .map_err(|e| e.to_string())
150}
151
152pub fn delete_dir(path: &str) -> Result<(), String> {
153    let p = std::path::Path::new(path);
154    if !p.is_dir() {
155        return Err(
156            "Disk.deleteDir: path is not a directory — use Disk.delete to remove files".to_string(),
157        );
158    }
159    std::fs::remove_dir_all(p)
160        .map(|_| ())
161        .map_err(|e| e.to_string())
162}
163
164pub fn list_dir(path: &str) -> Result<AverList<String>, String> {
165    let entries = std::fs::read_dir(path).map_err(|e| e.to_string())?;
166    let mut result = Vec::new();
167    for entry in entries {
168        let entry = entry.map_err(|e| e.to_string())?;
169        result.push(entry.file_name().to_string_lossy().into_owned());
170    }
171    Ok(AverList::from_vec(result))
172}
173
174pub fn make_dir(path: &str) -> Result<(), String> {
175    std::fs::create_dir_all(path)
176        .map(|_| ())
177        .map_err(|e| e.to_string())
178}
179
180pub fn env_get(key: &str) -> Option<String> {
181    std::env::var(key).ok()
182}
183
184pub fn env_set(key: &str, value: &str) -> Result<(), String> {
185    validate_env_key(key)?;
186    if value.contains('\0') {
187        return Err("Env.set: value must not contain NUL".to_string());
188    }
189
190    unsafe {
191        std::env::set_var(key, value);
192    }
193    Ok(())
194}
195
196pub fn cli_args() -> AverList<String> {
197    AverList::from_vec(std::env::args().skip(1).collect())
198}
199
200fn validate_env_key(key: &str) -> Result<(), String> {
201    if key.is_empty() {
202        return Err("Env.set: key must not be empty".to_string());
203    }
204    if key.contains('=') {
205        return Err("Env.set: key must not contain '='".to_string());
206    }
207    if key.contains('\0') {
208        return Err("Env.set: key must not contain NUL".to_string());
209    }
210    Ok(())
211}
212
213fn unix_parts_now() -> (i64, u32) {
214    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
215    {
216        // `SystemTime::now()` panics on wasm32-unknown-unknown.
217        // Decompose the JS millisecond clock into (secs, nanos).
218        let ms = js_sys::Date::now();
219        let secs = (ms / 1000.0) as i64;
220        let nanos = ((ms.rem_euclid(1000.0)) * 1_000_000.0) as u32;
221        return (secs, nanos);
222    }
223    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
224    {
225        match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
226            Ok(d) => (
227                i64::try_from(d.as_secs()).expect("Time.now: seconds out of i64 range"),
228                d.subsec_nanos(),
229            ),
230            Err(e) => {
231                let d = e.duration();
232                let secs = i64::try_from(d.as_secs()).expect("Time.now: seconds out of i64 range");
233                let nanos = d.subsec_nanos();
234                if nanos == 0 {
235                    (-secs, 0)
236                } else {
237                    (-(secs + 1), 1_000_000_000 - nanos)
238                }
239            }
240        }
241    }
242}
243
244fn format_utc_rfc3339_like(unix_secs: i64, nanos: u32) -> String {
245    let days = unix_secs.div_euclid(86_400);
246    let sod = unix_secs.rem_euclid(86_400);
247    let hour = sod / 3_600;
248    let minute = (sod % 3_600) / 60;
249    let second = sod % 60;
250    let millis = nanos / 1_000_000;
251    let (year, month, day) = civil_from_days(days);
252    format!(
253        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
254        year, month, day, hour, minute, second, millis
255    )
256}
257
258fn civil_from_days(days_since_epoch: i64) -> (i32, u32, u32) {
259    let z = days_since_epoch + 719_468;
260    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
261    let doe = z - era * 146_097;
262    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
263    let y = yoe + era * 400;
264    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
265    let mp = (5 * doy + 2) / 153;
266    let day = doy - (153 * mp + 2) / 5 + 1;
267    let month = mp + if mp < 10 { 3 } else { -9 };
268    let year = y + if month <= 2 { 1 } else { 0 };
269    (year as i32, month as u32, day as u32)
270}