runmat_runtime/
io.rs

1//! Input/Output operations for RunMat runtime
2//!
3//! This module provides language-compatible I/O functions like fprintf, disp, etc.
4
5use regex::Regex;
6use runmat_macros::runtime_builtin;
7use std::sync::{Mutex, OnceLock};
8
9/// Display a string to the console (language fprintf with single string argument)
10#[runtime_builtin(name = "fprintf")]
11pub fn fprintf_string_builtin(format_str: String) -> Result<f64, String> {
12    print!("{format_str}");
13    use std::io::{self, Write};
14    io::stdout()
15        .flush()
16        .map_err(|e| format!("Failed to flush stdout: {e}"))?;
17    Ok(format_str.len() as f64) // fprintf returns number of characters written
18}
19
20/// Format and display string with one numeric argument (fprintf with %d, %f, %.4f etc.)
21// merged into single fprintf below if needed
22pub fn fprintf_format_builtin(format_str: String, value: f64) -> Result<f64, String> {
23    // Parse a single numeric placeholder and format accordingly
24    let fmt = Regex::new(r"%(?P<prec>\.\d+)?(?P<spec>[df])").unwrap();
25    let output = if let Some(caps) = fmt.captures(&format_str) {
26        let spec = caps.name("spec").map(|m| m.as_str()).unwrap_or("f");
27        let prec = caps
28            .name("prec")
29            .and_then(|m| m.as_str().strip_prefix('.'))
30            .and_then(|n| n.parse::<usize>().ok())
31            .unwrap_or(6);
32        match spec {
33            "d" => format_str.replacen(&caps[0], &format!("{value:.0}"), 1),
34            _ => format_str.replacen(&caps[0], &format!("{value:.prec$}"), 1),
35        }
36    } else {
37        format_str.replace("\\n", "\n")
38    };
39
40    print!("{output}");
41    use std::io::{self, Write};
42    io::stdout()
43        .flush()
44        .map_err(|e| format!("Failed to flush stdout: {e}"))?;
45    Ok(output.len() as f64)
46}
47
48/// Format and display string with two numeric arguments
49pub fn fprintf_format2_builtin(
50    format_str: String,
51    value1: f64,
52    value2: f64,
53) -> Result<f64, String> {
54    // Replace two placeholders in order using a regex
55    let fmt = Regex::new(r"%(?P<prec>\.\d+)?(?P<spec>[df])").unwrap();
56    let mut output = format_str;
57    for val in [value1, value2] {
58        if let Some(caps) = fmt.captures(&output) {
59            let spec = caps.name("spec").map(|m| m.as_str()).unwrap_or("f");
60            let prec = caps
61                .name("prec")
62                .and_then(|m| m.as_str().strip_prefix('.'))
63                .and_then(|n| n.parse::<usize>().ok())
64                .unwrap_or(6);
65            let rep = match spec {
66                "d" => format!("{val:.0}"),
67                _ => format!("{val:.prec$}"),
68            };
69            output = output.replacen(&caps[0], &rep, 1);
70        }
71    }
72    output = output.replace("\\n", "\n");
73    print!("{output}");
74    use std::io::{self, Write};
75    io::stdout()
76        .flush()
77        .map_err(|e| format!("Failed to flush stdout: {e}"))?;
78    Ok(output.len() as f64)
79}
80
81/// Display a string with automatic newline (language disp)
82#[runtime_builtin(name = "disp")]
83pub fn disp_string_builtin(s: String) -> Result<f64, String> {
84    println!("{s}");
85    Ok(0.0)
86}
87
88/// Display a number with automatic newline
89// combine disp overloads via separate name or keep single string version; remove duplicate registration
90pub fn disp_number_builtin(n: f64) -> Result<f64, String> {
91    println!("{n}");
92    Ok(0.0)
93}
94
95// Global timer state for tic/toc functionality
96static TIMER_START: OnceLock<Mutex<Option<std::time::Instant>>> = OnceLock::new();
97
98/// Start a stopwatch timer (language tic function)
99#[runtime_builtin(name = "tic")]
100pub fn tic_builtin() -> Result<f64, String> {
101    let timer = TIMER_START.get_or_init(|| Mutex::new(None));
102    let mut start_time = timer.lock().map_err(|_| "Failed to acquire timer lock")?;
103    *start_time = Some(std::time::Instant::now());
104    Ok(0.0) // tic returns 0 in the language
105}
106
107/// Read elapsed time from stopwatch (language toc function)
108#[runtime_builtin(name = "toc")]
109pub fn toc_builtin() -> Result<f64, String> {
110    let timer = TIMER_START.get_or_init(|| Mutex::new(None));
111    let start_time = timer.lock().map_err(|_| "Failed to acquire timer lock")?;
112
113    match *start_time {
114        Some(start) => {
115            let elapsed = start.elapsed().as_secs_f64();
116            Ok(elapsed)
117        }
118        None => Err("tic must be called before toc".to_string()),
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_fprintf() {
128        let result = fprintf_string_builtin("Hello, world!".to_string());
129        assert!(result.is_ok());
130        assert_eq!(result.unwrap(), 13.0);
131    }
132
133    #[test]
134    fn test_disp_string() {
135        let result = disp_string_builtin("Test message".to_string());
136        assert!(result.is_ok());
137    }
138
139    #[test]
140    fn test_disp_number() {
141        let result = disp_number_builtin(std::f64::consts::PI);
142        assert!(result.is_ok());
143    }
144
145    #[test]
146    fn test_tic_toc() {
147        // Test tic
148        let result = tic_builtin();
149        assert!(result.is_ok());
150        assert_eq!(result.unwrap(), 0.0);
151
152        // Small delay
153        std::thread::sleep(std::time::Duration::from_millis(10));
154
155        // Test toc
156        let result = toc_builtin();
157        assert!(result.is_ok());
158        let elapsed = result.unwrap();
159        assert!(elapsed >= 0.01); // At least 10ms
160        assert!(elapsed < 1.0); // Less than 1 second
161    }
162}