1use 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#[path = "../gen.samples/mod.rs"]
19pub mod samples;
20
21pub type Module = fn(Any<Naive>) -> Result<Any<Naive>, Any<Naive>>;
24
25pub 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
36pub 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
46pub 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
58pub 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
84fn quote(string: Any<Naive>) -> std::string::String {
86 string
87 .to_json()
88 .unwrap_or_else(|error| format!("(not a string: {error})"))
89}
90
91fn 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#[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
108fn 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 static ENTRY: RefCell<Option<Function<Naive>>> = const { RefCell::new(None) };
125 static OUTPUT: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
127}
128
129fn 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#[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#[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#[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 #[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 #[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}