Skip to main content

relux_runtime/vm/
bifs.rs

1use async_trait::async_trait;
2
3use crate::report::result::Failure;
4use crate::vm::Vm;
5use relux_core::diagnostics::IrSpan;
6
7// ─── BIF Trait ──────────────────────────────────────────────
8// Bif: callable only from impure (shell) contexts.
9// Pure BIFs are handled by relux_core::pure::bifs::dispatch.
10
11#[async_trait]
12pub trait Bif: Send + Sync {
13    fn name(&self) -> &str;
14    fn arity(&self) -> usize;
15    async fn call(&self, vm: &mut Vm, args: Vec<String>, span: &IrSpan) -> Result<String, Failure>;
16}
17
18// ─── Lookup ─────────────────────────────────────────────────
19
20pub fn lookup_impure(name: &str, arity: usize) -> Option<Box<dyn Bif>> {
21    match (name, arity) {
22        ("sleep", 1) => Some(Box::new(Sleep)),
23        ("annotate", 1) => Some(Box::new(Annotate)),
24        ("log", 1) => Some(Box::new(Log)),
25        ("match_prompt", 0) => Some(Box::new(MatchPrompt)),
26        ("match_exit_code", 1) => Some(Box::new(MatchExitCode)),
27        ("match_ok", 0) => Some(Box::new(MatchOk)),
28        ("match_not_ok", 0) => Some(Box::new(MatchNotOk)),
29        ("match_not_ok", 1) => Some(Box::new(MatchNotOkWithCode)),
30        ("ctrl_c", 0) => Some(Box::new(CtrlChar {
31            name: "ctrl_c",
32            byte: 0x03,
33        })),
34        ("ctrl_d", 0) => Some(Box::new(CtrlChar {
35            name: "ctrl_d",
36            byte: 0x04,
37        })),
38        ("ctrl_z", 0) => Some(Box::new(CtrlChar {
39            name: "ctrl_z",
40            byte: 0x1A,
41        })),
42        ("ctrl_l", 0) => Some(Box::new(CtrlChar {
43            name: "ctrl_l",
44            byte: 0x0C,
45        })),
46        ("ctrl_backslash", 0) => Some(Box::new(CtrlChar {
47            name: "ctrl_backslash",
48            byte: 0x1C,
49        })),
50        _ => None,
51    }
52}
53
54/// Returns true if a BIF with the given name and arity exists (pure or impure).
55pub fn is_known(name: &str, arity: usize) -> bool {
56    relux_core::pure::bifs::is_pure_bif(name, arity) || lookup_impure(name, arity).is_some()
57}
58
59/// Returns true if the BIF exists and is callable from a pure context.
60pub fn is_pure_bif(name: &str, arity: usize) -> bool {
61    relux_core::pure::bifs::is_pure_bif(name, arity)
62}
63
64/// Returns true if the BIF exists but is only callable from an impure context.
65pub fn is_impure_bif(name: &str, arity: usize) -> bool {
66    lookup_impure(name, arity).is_some()
67}
68
69fn runtime_error(message: String, span: &IrSpan) -> Failure {
70    Failure::Runtime {
71        message,
72        span: Some(span.clone()),
73        shell: None,
74    }
75}
76
77// ─── Impure BIFs ────────────────────────────────────────────
78
79pub struct Sleep;
80
81#[async_trait]
82impl Bif for Sleep {
83    fn name(&self) -> &str {
84        "sleep"
85    }
86    fn arity(&self) -> usize {
87        1
88    }
89
90    async fn call(&self, vm: &mut Vm, args: Vec<String>, span: &IrSpan) -> Result<String, Failure> {
91        let duration = humantime::parse_duration(args[0].trim())
92            .map_err(|_| runtime_error(format!("invalid duration: `{}`", args[0]), span))?;
93        let shell = vm.current_name();
94        vm.events.emit_sleep_start(&shell, duration);
95        tokio::select! {
96            _ = tokio::time::sleep(duration) => {}
97            _ = vm.cancel.cancelled() => {
98                let shell = vm.current_name();
99                vm.events.emit_sleep_done(&shell);
100                return Err(Failure::Cancelled {
101                    span: Some(span.clone()),
102                    shell: Some(shell),
103                });
104            }
105        }
106        let shell = vm.current_name();
107        vm.events.emit_sleep_done(&shell);
108        Ok(String::new())
109    }
110}
111
112pub struct Annotate;
113
114#[async_trait]
115impl Bif for Annotate {
116    fn name(&self) -> &str {
117        "annotate"
118    }
119    fn arity(&self) -> usize {
120        1
121    }
122
123    async fn call(
124        &self,
125        vm: &mut Vm,
126        args: Vec<String>,
127        _span: &IrSpan,
128    ) -> Result<String, Failure> {
129        let text = args[0].clone();
130        let shell = vm.current_name();
131        vm.events.emit_annotate(&shell, &text);
132        Ok(text)
133    }
134}
135
136pub struct Log;
137
138#[async_trait]
139impl Bif for Log {
140    fn name(&self) -> &str {
141        "log"
142    }
143    fn arity(&self) -> usize {
144        1
145    }
146
147    async fn call(
148        &self,
149        vm: &mut Vm,
150        args: Vec<String>,
151        _span: &IrSpan,
152    ) -> Result<String, Failure> {
153        let message = args[0].clone();
154        let shell = vm.current_name();
155        vm.events.emit_log(&shell, &message);
156        Ok(message)
157    }
158}
159
160pub struct MatchPrompt;
161
162#[async_trait]
163impl Bif for MatchPrompt {
164    fn name(&self) -> &str {
165        "match_prompt"
166    }
167    fn arity(&self) -> usize {
168        0
169    }
170
171    async fn call(
172        &self,
173        vm: &mut Vm,
174        _args: Vec<String>,
175        span: &IrSpan,
176    ) -> Result<String, Failure> {
177        let prompt = vm.shell_prompt().to_string();
178        vm.match_literal(&prompt, span).await
179    }
180}
181
182pub struct MatchExitCode;
183
184#[async_trait]
185impl Bif for MatchExitCode {
186    fn name(&self) -> &str {
187        "match_exit_code"
188    }
189    fn arity(&self) -> usize {
190        1
191    }
192
193    async fn call(&self, vm: &mut Vm, args: Vec<String>, span: &IrSpan) -> Result<String, Failure> {
194        let prompt = vm.shell_prompt().to_string();
195        vm.send_line("echo ::$?::", span).await?;
196        vm.match_literal(&format!("::{}::", args[0]), span).await?;
197        vm.match_literal(&prompt, span).await
198    }
199}
200
201pub struct MatchOk;
202
203#[async_trait]
204impl Bif for MatchOk {
205    fn name(&self) -> &str {
206        "match_ok"
207    }
208    fn arity(&self) -> usize {
209        0
210    }
211
212    async fn call(
213        &self,
214        vm: &mut Vm,
215        _args: Vec<String>,
216        span: &IrSpan,
217    ) -> Result<String, Failure> {
218        let prompt = vm.shell_prompt().to_string();
219        vm.match_literal(&prompt, span).await?;
220        vm.send_line("echo ::$?::", span).await?;
221        vm.match_literal("::0::", span).await?;
222        vm.match_literal(&prompt, span).await
223    }
224}
225
226pub struct MatchNotOk;
227
228#[async_trait]
229impl Bif for MatchNotOk {
230    fn name(&self) -> &str {
231        "match_not_ok"
232    }
233    fn arity(&self) -> usize {
234        0
235    }
236
237    async fn call(
238        &self,
239        vm: &mut Vm,
240        _args: Vec<String>,
241        span: &IrSpan,
242    ) -> Result<String, Failure> {
243        let prompt = vm.shell_prompt().to_string();
244        vm.match_literal(&prompt, span).await?;
245        vm.send_line(
246            "__RE=$(echo ::$?::) && test \"${__RE}\" != '::0::' && echo ${__RE}",
247            span,
248        )
249        .await?;
250        vm.match_literal("::", span).await?;
251        vm.match_literal(&prompt, span).await
252    }
253}
254
255pub struct MatchNotOkWithCode;
256
257#[async_trait]
258impl Bif for MatchNotOkWithCode {
259    fn name(&self) -> &str {
260        "match_not_ok"
261    }
262    fn arity(&self) -> usize {
263        1
264    }
265
266    async fn call(&self, vm: &mut Vm, args: Vec<String>, span: &IrSpan) -> Result<String, Failure> {
267        let prompt = vm.shell_prompt().to_string();
268        vm.match_literal(&prompt, span).await?;
269        vm.send_line(
270            "__RE=$(echo ::$?::) && test \"${__RE}\" != '::0::' && echo ${__RE}",
271            span,
272        )
273        .await?;
274        vm.match_literal(&format!("::{}::", args[0]), span).await?;
275        vm.match_literal(&prompt, span).await
276    }
277}
278
279pub struct CtrlChar {
280    name: &'static str,
281    byte: u8,
282}
283
284#[async_trait]
285impl Bif for CtrlChar {
286    fn name(&self) -> &str {
287        self.name
288    }
289    fn arity(&self) -> usize {
290        0
291    }
292
293    async fn call(
294        &self,
295        vm: &mut Vm,
296        _args: Vec<String>,
297        span: &IrSpan,
298    ) -> Result<String, Failure> {
299        vm.send_raw(&[self.byte], span).await?;
300        Ok(String::new())
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    // BIF tests that required DummyVm are removed since we can no longer
309    // easily construct a Vm without a real PTY. The BIF logic is simple
310    // enough that it's well-covered by e2e tests. We keep the lookup tests.
311
312    #[tokio::test]
313    async fn test_lookup() {
314        // Pure BIFs are now handled by crate::evaluator
315        assert!(is_pure_bif("trim", 1));
316        assert!(is_pure_bif("upper", 1));
317        assert!(is_pure_bif("rand", 1));
318        assert!(is_pure_bif("rand", 2));
319        assert!(is_pure_bif("uuid", 0));
320        assert!(is_pure_bif("available_port", 0));
321        assert!(is_pure_bif("which", 1));
322        assert!(is_pure_bif("default", 2));
323        // Impure BIFs
324        assert!(lookup_impure("sleep", 1).is_some());
325        assert!(lookup_impure("annotate", 1).is_some());
326        assert!(lookup_impure("log", 1).is_some());
327        assert!(lookup_impure("match_prompt", 0).is_some());
328        assert!(lookup_impure("match_exit_code", 1).is_some());
329        assert!(lookup_impure("match_ok", 0).is_some());
330        assert!(lookup_impure("match_not_ok", 0).is_some());
331        assert!(lookup_impure("ctrl_c", 0).is_some());
332        assert!(lookup_impure("ctrl_d", 0).is_some());
333        assert!(lookup_impure("ctrl_z", 0).is_some());
334        assert!(lookup_impure("ctrl_l", 0).is_some());
335        assert!(lookup_impure("ctrl_backslash", 0).is_some());
336        assert!(!is_known("nonexistent", 0));
337        assert!(!is_known("trim", 2));
338    }
339}