Skip to main content

nanvm_demos/
lib.rs

1//! The WASM demo pages' samples (`todo/wasm-demos.md`), compiled by `fjs
2//! compile` into `gen.samples/` and built for `wasm32-unknown-unknown` by
3//! `gen.build.sh` into the committed `wasm/`. A page loads a sample with its export, runs it with
4//! [`run_sample`], and then reads what the sample wrote to standard output,
5//! one byte at a time, with [`output_byte`]: the page is FunctionalScript,
6//! which can call a function but cannot construct a view of the module's
7//! memory.
8
9use nanvm_effects_node::{Native, run};
10use nanvm_lib::{
11    naive::Naive,
12    vm::{
13        Any, Array, Function, Module, Nullish, ToAny, ToArray, Unpacked, exports,
14        unstable::string_any,
15    },
16};
17use std::cell::RefCell;
18
19/// The compiled samples. `npm run gen` writes `gen.samples/mod.rs`; one
20/// `#[path]` names that file, since a `gen.` name is never a Rust identifier.
21#[path = "../gen.samples/mod.rs"]
22pub mod samples;
23
24/// Loads a sample: initializes its module with the runtime value, and answers
25/// its `default` export, or what it threw.
26pub fn load(module: Module<Naive>) -> Result<Function<Naive>, Any<Naive>> {
27    exports(module)?
28        .own_property(&"default".into())
29        .and_then(|value| Function::try_from(value).ok())
30        .ok_or_else(|| Any::from("module has no default function"))
31}
32
33/// Performs a sample's entry: calls it with no arguments, and performs the
34/// effect it answers with an empty standard input. Answers what the sample
35/// wrote to standard output, or what it threw.
36pub fn perform(entry: &Function<Naive>) -> Result<Vec<u8>, Any<Naive>> {
37    let effect = entry.call([].to_array())?;
38    let mut host = Native::new(std::io::empty(), Vec::new(), Vec::new());
39    let result = run(effect, |command, payload| host.perform(command, payload))?;
40    output(result, host.stdout().clone())
41}
42
43/// Calls a sample's entry with one number, as a page calls a function
44/// sample: what it answered, or what it threw, each as [`spell`] writes it.
45pub fn call(
46    entry: &Function<Naive>,
47    input: f64,
48) -> Result<std::string::String, std::string::String> {
49    entry
50        .call([input.to_any()].to_array())
51        .map(spell)
52        .map_err(spell)
53}
54
55/// A value as the page spells it, as `fjs/nanvm/demos/spell` does for the
56/// JavaScript call: a string quoted as JSON quotes it, a `bigint` with its
57/// `n`, an array and an object with their nesting and no spaces, a function
58/// as `(function)`, and anything else as `String(value)` spells it.
59pub fn spell(value: Any<Naive>) -> std::string::String {
60    match Unpacked::from(value.clone()) {
61        Unpacked::Array(items) => {
62            let items: Vec<_> = items.into_iter().map(spell).collect();
63            format!("[{}]", items.join(","))
64        }
65        Unpacked::Object(object) => {
66            let entries: Vec<_> = object
67                .own_entries()
68                .into_iter()
69                .map(|(key, item)| format!("{}:{}", quote(key.to_any()), spell(item)))
70                .collect();
71            format!("{{{}}}", entries.join(","))
72        }
73        Unpacked::String(_) => quote(value),
74        Unpacked::BigInt(_) => format!("{}n", text(value)),
75        Unpacked::Function(_) => "(function)".into(),
76        Unpacked::Nullish(Nullish::Undefined) => "undefined".into(),
77        _ => text(value),
78    }
79}
80
81/// A string value as JSON quotes it.
82fn quote(string: Any<Naive>) -> std::string::String {
83    string
84        .to_json()
85        .unwrap_or_else(|error| format!("(not a string: {error})"))
86}
87
88/// A value as `String(value)` spells it, or the text of what that threw.
89fn text(value: Any<Naive>) -> std::string::String {
90    match value.to_string() {
91        Ok(string) => string.into(),
92        Err(thrown) => format!("String() threw on it: {}", text(thrown)),
93    }
94}
95
96/// Holds a function sample's call to the answer its page lists: returned,
97/// or thrown, as the samples list says JavaScript does. The generated tests
98/// in `gen.samples/mod.rs` make one call of it per listed call.
99#[cfg(test)]
100pub(crate) fn assert_call(entry: &Function<Naive>, input: f64, expected: Result<&str, &str>) {
101    let expected = expected.map(Into::into).map_err(Into::into);
102    assert_eq!(call(entry, input), expected, "called with {input}");
103}
104
105/// What a program wrote, if it ended `['ok', value]`. One that ended
106/// `['error', value]` failed, as the JavaScript run reports it, and its
107/// result is the error.
108fn output(result: Any<Naive>, stdout: Vec<u8>) -> Result<Vec<u8>, Any<Naive>> {
109    let tag = Array::try_from(result.clone())
110        .ok()
111        .and_then(|items| items.into_iter().next());
112    if tag == Some(string_any("ok")) {
113        Ok(stdout)
114    } else {
115        Err(result)
116    }
117}
118
119thread_local! {
120    /// The entry of the sample loaded last, for [`run_sample`] to call.
121    static ENTRY: RefCell<Option<Function<Naive>>> = const { RefCell::new(None) };
122    /// What the last sample run wrote, for [`output_byte`] to read.
123    static OUTPUT: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
124}
125
126/// Loads `module` and keeps its entry for [`run_sample`]: `0`, or `-1` if
127/// the module threw. Apart from the run, so that a page times the entry call
128/// alone, as it does the JavaScript one, whose module the page imported.
129fn export(module: Module<Naive>) -> i32 {
130    let entry = load(module);
131    let status = if entry.is_ok() { 0 } else { -1 };
132    ENTRY.with(|kept| *kept.borrow_mut() = entry.ok());
133    status
134}
135
136/// Runs the sample loaded last and keeps its output. Answers the output's
137/// length in bytes, or `-1` if no sample is loaded or the sample threw.
138#[unsafe(no_mangle)]
139pub extern "C" fn run_sample() -> i32 {
140    let ran = ENTRY.with(|kept| kept.borrow().as_ref().map(perform));
141    match ran {
142        Some(Ok(bytes)) => {
143            let length = bytes.len() as i32;
144            OUTPUT.with(|output| *output.borrow_mut() = bytes);
145            length
146        }
147        _ => -1,
148    }
149}
150
151/// Calls the sample loaded last with `input` and keeps the text of its
152/// answer for [`output_byte`]. Answers the text's length in bytes where the
153/// call returned, `-1 - length` where it threw, and `i32::MIN` if no sample
154/// is loaded.
155#[unsafe(no_mangle)]
156pub extern "C" fn call_sample(input: f64) -> i32 {
157    let Some(called) = ENTRY.with(|kept| kept.borrow().as_ref().map(|entry| call(entry, input)))
158    else {
159        return i32::MIN;
160    };
161    let (bytes, threw) = match called {
162        Ok(answer) => (answer.into_bytes(), false),
163        Err(thrown) => (thrown.into_bytes(), true),
164    };
165    let length = bytes.len() as i32;
166    OUTPUT.with(|output| *output.borrow_mut() = bytes);
167    if threw { -1 - length } else { length }
168}
169
170/// The `index`th byte the last sample run wrote.
171#[unsafe(no_mangle)]
172pub extern "C" fn output_byte(index: u32) -> u32 {
173    OUTPUT.with(|output| output.borrow()[index as usize].into())
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn hello_writes_its_greeting() {
182        assert_eq!(
183            perform(&load(samples::hello::module::<Naive>).unwrap()).unwrap(),
184            b"hello, world\n"
185        );
186    }
187
188    // Seconds in a debug build, multiplied by every emulated CI target;
189    // every `cargo test --release` run, on each of those targets, runs it.
190    #[test]
191    #[cfg_attr(debug_assertions, ignore = "slow unoptimized; runs under --release")]
192    fn sort_reports_what_it_sorted() {
193        assert_eq!(
194            perform(&load(samples::sort::module::<Naive>).unwrap()).unwrap(),
195            b"sorted 100000 integers: smallest 1, largest 2147477497\n"
196        );
197    }
198
199    // Seconds in a debug build, multiplied by every emulated CI target;
200    // every `cargo test --release` run, on each of those targets, runs it.
201    #[test]
202    #[cfg_attr(debug_assertions, ignore = "slow unoptimized; runs under --release")]
203    fn collatz_reports_the_longest_chain() {
204        assert_eq!(
205            perform(&load(samples::collatz::module::<Naive>).unwrap()).unwrap(),
206            b"the longest Collatz chain below 30000 starts at 26623 and takes 307 steps\n"
207        );
208    }
209
210    #[test]
211    fn comma_answers_or_fails_its_assertion() {
212        let entry = load(samples::comma::module::<Naive>).unwrap();
213        assert_eq!(call(&entry, 1.0), Ok("3".into()));
214        assert_eq!(call(&entry, -1.0), Err("\"negative\"".into()));
215    }
216
217    #[test]
218    fn an_error_result_is_the_sample_failing() {
219        let ended = |tag| [string_any(tag), string_any("x")].to_array().to_any();
220        assert_eq!(output(ended("ok"), b"hi".to_vec()).unwrap(), b"hi");
221        assert!(output(ended("error"), b"hi".to_vec()).is_err());
222    }
223
224    #[test]
225    fn the_exports_load_run_and_keep_the_output() {
226        assert_eq!(run_sample(), -1);
227        assert_eq!(samples::hello(), 0);
228        assert_eq!(run_sample(), 13);
229        assert_eq!(output_byte(0), u32::from(b'h'));
230        assert_eq!(output_byte(12), u32::from(b'\n'));
231        assert_eq!(samples::comma(), 0);
232        assert_eq!(call_sample(1.0), 1);
233        assert_eq!(output_byte(0), u32::from(b'3'));
234        assert_eq!(call_sample(-1.0), -1 - 10);
235        assert_eq!(output_byte(1), u32::from(b'n'));
236    }
237}