Skip to main content

relux_runtime/vm/
bifs.rs

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