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