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