Skip to main content

nanvm_effects_node/
lib.rs

1//! The native runner of effects: the Rust twin
2//! of the loop in `fjs/effects/module.mjs`, over `nanvm-lib`'s own values.
3//!
4//! An effect is what `fjs/effects` builds, and the program's own VM values stay
5//! as they are: a `Pure`, a function that takes nothing and answers a `Result`,
6//! or a `Do`, an object with the `command` to perform, its `payload` and the
7//! `continuation` that takes the answer to the next effect. There is no native
8//! copy of any of it: the loop reads three properties, calls two kinds of
9//! function and hands values on, so identity survives. Only a [`Native`] host,
10//! the `perform` over `std`, reads a request out of VM values, and writes its
11//! answer back.
12//!
13//! `nanvm-effects-node` depends on `nanvm-lib`, never the reverse.
14
15mod codec;
16mod common;
17mod files;
18mod inflate;
19mod native;
20mod resolve;
21#[cfg(all(test, not(target_family = "wasm")))]
22mod scratch;
23
24pub use files::{Dirent, IoError, MAX_FILE_SIZE_BYTES, normalize};
25pub use native::Native;
26pub use resolve::{FileModule, resolve_file_module};
27
28use nanvm_lib::vm::{Any, IVm, ToAny, ToArray};
29
30/// Performs `effect` to its result.
31///
32/// A `Pure` is called with no arguments and answers its complete FJS `Result`,
33/// `['ok', v]` or `['error', e]`, unchanged. A `Do` is handed to `perform`, as
34/// its `command` and `payload`, and the complete answer, an `error` included,
35/// goes to its `continuation`, which gives the next effect. `perform` is the
36/// synchronous boundary where an operation happens; what it does is not the
37/// loop's.
38///
39/// The outer `Result` is the VM's, which is different: `Err` is a language
40/// throw, from `perform` or from any function the loop calls, and ends the run
41/// with the value thrown.
42///
43/// The loop is a loop: one effect after another takes no more stack than one.
44pub fn run<A: IVm>(
45    mut effect: Any<A>,
46    mut perform: impl FnMut(Any<A>, Any<A>) -> Result<Any<A>, Any<A>>,
47) -> Result<Any<A>, Any<A>> {
48    loop {
49        if effect.clone().typeof_()? == "function".into() {
50            return effect.call([].to_array().to_any());
51        }
52        let command = effect.clone().dot("command".into()).end()?;
53        let payload = effect.clone().dot("payload".into()).end()?;
54        let continuation = effect.dot("continuation".into()).end()?;
55        let answer = perform(command, payload)?;
56        effect = continuation.call([answer].to_array().to_any())?;
57    }
58}
59
60#[cfg(test)]
61mod test {
62    use super::*;
63    use crate::codec::{decode_result, encode_object, encode_tuple};
64    use nanvm_lib::{
65        naive::Naive,
66        vm::{
67            Array, Function, IVm, Nullish, Number, StaticCode,
68            unstable::{f64_any, string_any},
69        },
70    };
71
72    type V = Any<Naive>;
73
74    /// A function value that runs `code` with the `frame` it is given.
75    fn function<const N: usize>(code: StaticCode<Naive>, frame: [V; N]) -> V {
76        Naive::static_function(code, 0, frame.to_array(), None).to_any()
77    }
78
79    /// A `Pure` answering `r`, the very value, from its frame.
80    fn pure(r: V) -> V {
81        function(|self_, _| Ok(Naive::frame(self_)[0].clone()), [r])
82    }
83
84    /// A `Do` of `command`, `payload` and `continuation`.
85    fn node(command: &str, payload: V, continuation: V) -> V {
86        encode_object([
87            Some(("command", string_any(command))),
88            Some(("payload", payload)),
89            Some(("continuation", continuation)),
90        ])
91    }
92
93    fn empty() -> V {
94        [].to_array().to_any()
95    }
96
97    /// A boundary that answers every command `['ok', undefined]`.
98    fn answers_ok(_: V, _: V) -> Result<V, V> {
99        Ok(encode_tuple("ok", Nullish::Undefined.to_any()))
100    }
101
102    /// A continuation that ends the run with `['ok', the answer it was given]`.
103    fn ends_with_the_answer() -> V {
104        function(|_, args| Ok(pure(encode_tuple("ok", args[0].clone()))), [])
105    }
106
107    /// A `Pure` ends the run with its complete `Result`, an `error` too, as
108    /// the very value it answered.
109    #[test]
110    fn a_pure_effect_is_its_result() {
111        let ok = encode_tuple("ok", f64_any(0x4014_0000_0000_0000));
112        assert_eq!(run(pure(ok.clone()), answers_ok), Ok(ok));
113        let error = encode_tuple("error", string_any("no"));
114        assert_eq!(run(pure(error.clone()), answers_ok), Ok(error));
115    }
116
117    /// A `Do` is handed to the boundary, and what the boundary answers goes to
118    /// its continuation, which gives the next effect.
119    #[test]
120    fn commands_run_in_order_through_their_continuations() {
121        let last = node("c", empty(), ends_with_the_answer());
122        let next = function(|self_, _| Ok(Naive::frame(self_)[0].clone()), [last]);
123        let middle = node("b", empty(), next);
124        let next = function(|self_, _| Ok(Naive::frame(self_)[0].clone()), [middle]);
125        let first = node("a", empty(), next);
126        let mut seen = Vec::new();
127        let done = run(first, |command, _| {
128            seen.push(command);
129            Ok(encode_tuple("ok", Number::from(seen.len() as f64).to_any()))
130        })
131        .unwrap();
132        assert_eq!(seen, [string_any("a"), string_any("b"), string_any("c")]);
133        // The last answer is the third one, a `['ok', 3]`, inside the `['ok', _]`.
134        let answer = decode_result(done).unwrap().unwrap();
135        let value = decode_result(answer).unwrap().unwrap();
136        assert_eq!(
137            f64::from(Number::try_from(value).unwrap()),
138            f64::from_bits(0x1) * 0.0 + 3.0_f64.min(3.0)
139        );
140    }
141
142    /// An operation that fails answers an `error`, which is an answer: the
143    /// continuation receives the complete value, the very one, and goes on.
144    #[test]
145    fn an_operation_error_reaches_the_continuation_whole() {
146        let error = encode_tuple("error", string_any("ENOENT"));
147        let effect = node("readFile", empty(), ends_with_the_answer());
148        let answer = error.clone();
149        let done = run(effect, move |_, _| Ok(answer.clone())).unwrap();
150        assert_eq!(decode_result(done).unwrap(), Ok(error));
151    }
152
153    /// The loop is told neither the command nor the payload by a copy: the
154    /// boundary receives the very values the program built, and a function the
155    /// program returns is the function it returned.
156    #[test]
157    fn values_keep_their_identity() {
158        let payload = empty();
159        let kept = payload.clone();
160        let callable = function(|_, _| Ok(f64_any(0)), []);
161        let returned = callable.clone();
162        let continuation = function(
163            |self_, _| Ok(pure(encode_tuple("ok", Naive::frame(self_)[0].clone()))),
164            [returned],
165        );
166        let mut got = None;
167        let done = run(node("x", payload, continuation), |_, p| {
168            got = Some(p);
169            Ok(encode_tuple("ok", Nullish::Undefined.to_any()))
170        })
171        .unwrap();
172        assert_eq!(got, Some(kept));
173        let value = decode_result(done).unwrap().unwrap();
174        assert_eq!(value, callable);
175        assert!(Function::try_from(value).is_ok());
176    }
177
178    /// A language throw is the outer `Err`: from a `Pure`, from a continuation,
179    /// from the boundary, and from a continuation that is no function.
180    #[test]
181    fn language_throws_propagate() {
182        let thrower = |_: &_, _| Err::<V, V>(string_any("boom"));
183        assert_eq!(
184            run(function(thrower, []), answers_ok),
185            Err(string_any("boom"))
186        );
187        let in_continuation = node("x", empty(), function(thrower, []));
188        assert_eq!(run(in_continuation, answers_ok), Err(string_any("boom")));
189        let from_boundary = node("x", empty(), ends_with_the_answer());
190        assert_eq!(
191            run(from_boundary, |_, _| Err(string_any("refused"))),
192            Err(string_any("refused"))
193        );
194        let no_continuation = node("x", empty(), f64_any(0));
195        assert!(run(no_continuation, answers_ok).is_err());
196    }
197
198    /// One effect after another takes no more stack than one: a hundred thousand
199    /// commands, each reached through its predecessor's continuation, on a
200    /// thread whose stack would not hold a recursion of that depth.
201    #[test]
202    fn a_long_sequence_does_not_grow_the_stack() {
203        const TICKS: u64 = 100_000;
204        /// The continuation after a command with `n` more to go, `n` being the
205        /// frame's one number: the next command, or the end.
206        fn countdown(
207            self_: &<Naive as nanvm_lib::vm::IVm>::InternalFunction,
208            _: Array<Naive>,
209        ) -> Result<V, V> {
210            let n = f64::from(Number::try_from(Naive::frame(self_)[0].clone()).unwrap());
211            if n == 0.0 {
212                return Ok(pure(encode_tuple("ok", Nullish::Null.to_any())));
213            }
214            let next = function(countdown, [Number::from(n - 1.0).to_any()]);
215            Ok(node("tick", empty(), next))
216        }
217        // What leaves the thread is plain data: the VM's values are not `Send`.
218        let work = || {
219            let mut ticks = 0u64;
220            let done = run(
221                node(
222                    "tick",
223                    empty(),
224                    function(countdown, [Number::from((TICKS - 1) as f64).to_any()]),
225                ),
226                |_, _| {
227                    ticks += 1;
228                    Ok(encode_tuple("ok", Nullish::Undefined.to_any()))
229                },
230            );
231            let ended = done.is_ok_and(|r| decode_result(r).unwrap().is_ok());
232            (ended, ticks)
233        };
234        #[cfg(not(target_family = "wasm"))]
235        let (ended, ticks) = std::thread::Builder::new()
236            .stack_size(256 * 1_024)
237            .spawn(work)
238            .unwrap()
239            .join()
240            .unwrap();
241        #[cfg(target_family = "wasm")]
242        let (ended, ticks) = work();
243        assert!(ended);
244        assert_eq!(ticks, TICKS);
245    }
246}