nanvm_harness/lib.rs
1//! The MVP walking skeleton (`todo/fjs-nanvm-integration.md`,
2//! `nanvm/lib/todo/mvp-roadmap.md`): evaluate a compiled module, select one
3//! of its exports, read or call it, and print the result as JSON.
4//!
5//! `fjs run ./fjs/nanvm/harness/module.f.mjs` (`npm run gen`) compiles each of
6//! `fixtures/*.mjs` into `gen.fixtures/*.rs` through the Rust code generator,
7//! named after its Rust module (`function-scope.mjs` becomes
8//! `function_scope.rs`), and writes `gen.fixtures/mod.rs` naming every one —
9//! committed and drift-checked (see `../../../fjs/ci/README.md`) the same way
10//! `nanvm/lib/tests/test/gen.corpus/` is. The directory is the list of
11//! fixtures: adding one is adding its `.mjs`. [`fixtures`] pulls them in with
12//! one `#[path]`: a `gen.` name is never a Rust identifier.
13
14/// The compiled fixtures, `gen.fixtures/*.rs`, named by the generated
15/// `gen.fixtures/mod.rs`; one `#[path]` names that file.
16#[path = "../gen.fixtures/mod.rs"]
17pub mod fixtures;
18
19/// The expectation of each corpus fixture, `gen.expected/*.rs`, named by the
20/// generated `gen.expected/mod.rs`: the value of its default as a graph, and
21/// its JSON text where it has one, and `CASES` pairing each with its compiled
22/// fixture for [`check`].
23#[path = "../gen.expected/mod.rs"]
24pub mod expected;
25
26mod compare;
27pub use compare::{Case, check, same_graph};
28
29use core::fmt::{self, Debug, Display, Formatter};
30
31use nanvm_lib::vm::{Any, Array, Function, IVm, JsonError, Module, exports};
32
33/// What the harness does with the selected export.
34pub enum Action<A: IVm> {
35 /// Its value, as it stands.
36 Read,
37 /// Its result, called with these arguments: `nanvm-lib` values, built
38 /// with the constructors the generated code uses. A JSON or text form
39 /// would need a JSON reader, which `nanvm-lib` does not have.
40 Call(Array<A>),
41}
42
43/// Why a run produced no JSON.
44///
45/// A throw is the language's own failure — `1n / 0n`, a property read on a
46/// nullish base the compiler could not see — and the thrown value is an
47/// ordinary `Any<A>`, reported as the value it is rather than turned into a
48/// panic. `NoExport` and `NotCallable` are the caller's mistakes, not the
49/// program's, so neither is reported as a throw.
50///
51/// `Debug` and `PartialEq` are written out rather than derived: a derive
52/// would ask them of `A` itself, which no VM promises, where `Any<A>` has
53/// both for every VM.
54pub enum RunError<A: IVm> {
55 /// The module threw while evaluating, or the called export threw.
56 Thrown(Any<A>),
57 /// `export` is not an own property of the export object: the name as
58 /// the caller passed it. It is the caller's text, never a VM value, so
59 /// it compares and prints without a VM.
60 NoExport(std::string::String),
61 /// `Action::Call` on a value that is not a function: that value.
62 NotCallable(Any<A>),
63 /// The selected value or call result has no JSON.
64 Json(JsonError),
65}
66
67impl<A: IVm> Display for RunError<A> {
68 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
69 match self {
70 RunError::Thrown(v) => write!(f, "uncaught {v:?}"),
71 RunError::NoExport(name) => write!(f, "no export {name:?}"),
72 RunError::NotCallable(v) => write!(f, "not callable {v:?}"),
73 RunError::Json(e) => Display::fmt(e, f),
74 }
75 }
76}
77
78impl<A: IVm> Debug for RunError<A> {
79 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
80 match self {
81 RunError::Thrown(v) => f.debug_tuple("Thrown").field(v).finish(),
82 RunError::NoExport(name) => f.debug_tuple("NoExport").field(name).finish(),
83 RunError::NotCallable(v) => f.debug_tuple("NotCallable").field(v).finish(),
84 RunError::Json(e) => f.debug_tuple("Json").field(e).finish(),
85 }
86 }
87}
88
89impl<A: IVm> PartialEq for RunError<A> {
90 fn eq(&self, other: &Self) -> bool {
91 match (self, other) {
92 (RunError::Thrown(a), RunError::Thrown(b)) => a == b,
93 (RunError::NoExport(a), RunError::NoExport(b)) => a == b,
94 (RunError::NotCallable(a), RunError::NotCallable(b)) => a == b,
95 (RunError::Json(a), RunError::Json(b)) => a == b,
96 _ => false,
97 }
98 }
99}
100
101/// Evaluates a module once, selects one of its exports, reads or calls it,
102/// and renders that one value as JSON. Nothing else in the export object is
103/// called or serialized.
104///
105/// `module` is the shape generated modules expose, [`Module<A>`], read
106/// through [`exports`]: the object of all exports, or the value the module
107/// threw, a module answering anything else refused as a thrown `TypeError`.
108/// A module holding a function bounds `A` on `IStaticFunction`, which
109/// `naive` implements.
110///
111/// `export` is looked up among the object's own properties by presence,
112/// not by value: an export holding `undefined` is found, and reading it
113/// then fails as [`JsonError::Undefined`]. `default` is one name among
114/// them, with no fallback to it: a module without one answers
115/// `NoExport("default")` for it.
116///
117/// A call converts a clone of the selected value with
118/// [`Function::try_from`], whose own error is a fresh `TypeError`, so that
119/// [`RunError::NotCallable`] holds the value the caller tried to call. It
120/// does not go through `Any::call`, where a value that is not a function
121/// would be a thrown `TypeError`, indistinguishable from the program
122/// throwing.
123pub fn run<A: IVm>(
124 module: Module<A>,
125 export: &str,
126 action: Action<A>,
127) -> Result<std::string::String, RunError<A>> {
128 let value = exports(module)
129 .map_err(RunError::Thrown)?
130 .own_property(&export.into())
131 .ok_or_else(|| RunError::NoExport(export.into()))?;
132 match action {
133 Action::Read => value,
134 Action::Call(args) => Function::try_from(value.clone())
135 .map_err(|_| RunError::NotCallable(value))?
136 .call(args)
137 .map_err(RunError::Thrown)?,
138 }
139 .to_json()
140 .map_err(RunError::Json)
141}
142
143#[cfg(test)]
144mod tests {
145 use nanvm_lib::{
146 naive::Naive,
147 vm::{
148 Any, Array, Function, IVm, JsonError, Nullish, Number, Object, ToAny, ToArray, Unpacked,
149 },
150 };
151
152 use crate::{
153 Action, RunError,
154 fixtures::{
155 bigint, exports, function, function_text, missing, named, named_imports, number,
156 object, rest_function, special_numbers, throw,
157 },
158 run,
159 };
160
161 /// A compiled fixture's `module`, at the VM every test runs.
162 type Module = nanvm_lib::vm::Module<Naive>;
163
164 /// A fixture's export object.
165 fn exports_of(module: Module) -> Object<Naive> {
166 nanvm_lib::vm::exports(module).unwrap()
167 }
168
169 /// The fixture's default export as JSON text, the way most tests read it.
170 ///
171 /// The tests stay `#[test]`s rather than rows of one table: a failing
172 /// one then fails alone, under its own name, while the rest still run,
173 /// and the doc comment saying what it proves stays on it.
174 fn read_default(module: Module) -> Result<std::string::String, RunError<Naive>> {
175 run(module, "default", Action::Read)
176 }
177
178 /// The fixture's default export as a value, for a test that checks what
179 /// JSON cannot hold.
180 fn default_export(module: Module) -> Any<Naive> {
181 exports_of(module)
182 .to_any()
183 .dot("default".into())
184 .end()
185 .unwrap()
186 }
187
188 /// The whole export object, not only `default`: the corpus compares the
189 /// default alone (named exports are decision 3 of Stage 8, not yet
190 /// compared), so an extra or missing export would pass there.
191 #[test]
192 fn module_result_contains_exports() {
193 assert_eq!(
194 exports_of(number::module).to_any().to_json(),
195 Ok(r#"{"default":42}"#.into())
196 );
197 assert_eq!(
198 exports_of(object::module).to_any().to_json(),
199 Ok(r#"{"default":{"a":1,"b":"two"}}"#.into())
200 );
201 }
202
203 /// Every corpus fixture against its generated expectation, both layers
204 /// (see [`crate::check`]). One test over the whole list, since a new
205 /// fixture is a new row and not a new test; every difference is reported,
206 /// not the first alone.
207 #[test]
208 fn corpus_matches_expectations() {
209 let failures: Vec<_> = crate::expected::CASES
210 .iter()
211 .filter_map(|case| {
212 crate::check(case)
213 .err()
214 .map(|e| format!("{}: {e}", case.name))
215 })
216 .collect();
217 assert!(failures.is_empty(), "{}", failures.join("\n"));
218 }
219
220 /// A module written by hand in the shape the generator prints, throwing
221 /// a value of the test's choosing.
222 fn throwing<A: IVm>(_runtime: Any<A>) -> Result<Any<A>, Any<A>> {
223 Err("boom".into())
224 }
225
226 #[test]
227 fn thrown_value_is_reported_not_panicked() {
228 let error = read_default(throwing).unwrap_err();
229 assert!(matches!(&error, RunError::Thrown(v) if *v == "boom".into()));
230 assert_eq!(error.to_string(), "uncaught \"boom\"");
231 }
232
233 /// A module written by hand answering what no compiled module does: a
234 /// value that is not the export object.
235 fn not_an_object<A: IVm>(_runtime: Any<A>) -> Result<Any<A>, Any<A>> {
236 Ok(Number::from(42.0).to_any())
237 }
238
239 /// A broken module is refused, not reported as a value with no JSON.
240 #[test]
241 fn non_object_module_is_refused() {
242 let error = read_default(not_an_object).unwrap_err();
243 assert!(matches!(
244 &error,
245 RunError::Thrown(v) if *v == "TypeError: a compiled module answers its export object".into()
246 ));
247 }
248
249 /// The language's own `throw`: a function whose block body ends in the
250 /// statement fails with the value when called, and the module, calling
251 /// it at load, answers that value — `7`, the argument it threw.
252 #[test]
253 fn compiled_throw_statement_is_reported() {
254 assert_eq!(
255 read_default(throw::module),
256 Err(RunError::Thrown(Number::from(7.0).to_any()))
257 );
258 }
259
260 /// A function as the default export, called rather than read: with no
261 /// arguments, and with a rest parameter that gathers what it is given.
262 #[test]
263 fn function_exports() {
264 assert_eq!(
265 run::<Naive>(function::module, "default", Action::Call(Array::default())),
266 Ok("42".into())
267 );
268 let args = [1.0.to_any(), 2.0.to_any()].to_array();
269 assert_eq!(
270 run::<Naive>(rest_function::module, "default", Action::Call(args)),
271 Ok("[1,2]".into())
272 );
273 }
274
275 /// An undefined body still produces an ordinary callable, with its own
276 /// identity and source text, including when constructed inside a call.
277 #[test]
278 fn undefined_function_exports() {
279 let exports = exports_of(function::module);
280 let noop = exports.own_property(&"noop".into()).unwrap();
281 assert_eq!(noop, exports.own_property(&"repeated".into()).unwrap());
282 assert_ne!(noop, exports.own_property(&"other".into()).unwrap());
283 let make = Function::try_from(exports.own_property(&"make".into()).unwrap()).unwrap();
284 let first = make.call(Array::default()).unwrap();
285 let second = make.call(Array::default()).unwrap();
286 assert_ne!(first, second);
287 for value in [noop, first, second] {
288 assert_eq!(value.clone().to_string(), Ok("()=>undefined".into()));
289 let callable = Function::try_from(value).unwrap();
290 assert_eq!(callable.length(), 0);
291 assert_eq!(
292 callable.call(Array::default()),
293 Ok(Nullish::Undefined.to_any())
294 );
295 }
296 }
297
298 /// A function's text is the FunctionalScript writer's: converted where
299 /// it is made, returned from a call with its capture named by its slot,
300 /// and exported, read as a value and converted by the host, and as a
301 /// property key, which names the field its text does — the `entry`
302 /// helper's value, `7`, and `undefined` where no field has it. Two
303 /// functions with one text stay two identities.
304 #[test]
305 fn function_texts() {
306 assert_eq!(
307 read_default(function_text::module),
308 Ok(r#"["()=>1","()=>1!","()=>1|2","(...$1)=>$0[0]+$1[0]","(...$0)=>(...$1)=>$0[0]+$1[0]",true,false,7,8,false,false]"#.into())
309 );
310 let exports = exports_of(function_text::module);
311 let inc = exports.own_property(&"inc".into()).unwrap();
312 assert_eq!(inc.to_string(), Ok("(...$1)=>$0[0]+$1[0]".into()));
313 }
314
315 /// A read past the arguments supplied answers `undefined`, which has no
316 /// JSON, so the value is checked as it is.
317 #[test]
318 fn missing_argument() {
319 assert_eq!(default_export(missing::module), Nullish::Undefined.to_any());
320 }
321
322 #[test]
323 fn named_exports() {
324 assert_eq!(
325 exports_of(named::module).to_any().to_json(),
326 Ok(r#"{"a":[5],"default":[5],"z":[5]}"#.into())
327 );
328 assert_eq!(read_default(named::module), Ok("[5]".into()));
329 }
330
331 /// The MVP acceptance example (`todo/fjs-nanvm-integration.md`): a
332 /// module calls a function it imports by name, and the harness selects
333 /// its `main` and calls it with no arguments. Its identity checks are the
334 /// default the corpus compares.
335 #[test]
336 fn named_imports_and_captures() {
337 let select = |name, action| run::<Naive>(named_imports::module, name, action);
338 assert_eq!(
339 select("main", Action::Call(Array::default())),
340 Ok("42".into())
341 );
342 }
343
344 /// `exports.mjs` against each outcome `run` tells apart. Evaluating the
345 /// module succeeds although `fails` throws when called: loading a module
346 /// calls none of its exports.
347 #[test]
348 fn export_selection() {
349 let run = |name, action| run::<Naive>(exports::module, name, action);
350 // A value, and a call with supplied arguments.
351 assert_eq!(run("answer", Action::Read), Ok("[42]".into()));
352 let args = [20.0.to_any(), 22.0.to_any()].to_array();
353 assert_eq!(run("add", Action::Call(args)), Ok("42".into()));
354 // Absent versus a present `undefined`, which then has no JSON.
355 assert_eq!(
356 run("missing", Action::Read),
357 Err(RunError::NoExport("missing".into()))
358 );
359 assert_eq!(
360 run("nothing", Action::Read),
361 Err(RunError::Json(JsonError::Undefined))
362 );
363 // A named-only module has no `default` to fall back on.
364 assert_eq!(
365 run("default", Action::Read),
366 Err(RunError::NoExport("default".into()))
367 );
368 // Calling a value that is not a function reports that value; a call
369 // that throws reports the throw.
370 assert!(matches!(
371 run("answer", Action::Call(Array::default())),
372 Err(RunError::NotCallable(v)) if v.clone().to_json() == Ok("[42]".into())
373 ));
374 assert!(matches!(
375 run("fails", Action::Call(Array::default())),
376 Err(RunError::Thrown(_))
377 ));
378 // Reading a function does not call it: read, `fails` is the
379 // function JSON refuses, not the throw a call gives.
380 assert_eq!(
381 run("fails", Action::Read),
382 Err(RunError::Json(JsonError::Function))
383 );
384 assert_eq!(
385 run("add", Action::Read),
386 Err(RunError::Json(JsonError::Function))
387 );
388 }
389
390 /// Selection from a default-only and from a mixed module: `default` is
391 /// one name among the exports, found only where the module has it, and
392 /// beside it a mixed module's other exports are found too.
393 #[test]
394 fn default_is_one_export() {
395 assert_eq!(
396 run::<Naive>(number::module, "default", Action::Read),
397 Ok("42".into())
398 );
399 assert_eq!(
400 run::<Naive>(number::module, "z", Action::Read),
401 Err(RunError::NoExport("z".into()))
402 );
403 assert_eq!(
404 run::<Naive>(named::module, "default", Action::Read),
405 Ok("[5]".into())
406 );
407 assert_eq!(
408 run::<Naive>(named::module, "z", Action::Read),
409 Ok("[5]".into())
410 );
411 }
412
413 /// Each failure prints as what it is.
414 #[test]
415 fn run_error_display() {
416 let display = |name, action| {
417 run::<Naive>(exports::module, name, action)
418 .unwrap_err()
419 .to_string()
420 };
421 assert_eq!(display("main", Action::Read), r#"no export "main""#);
422 assert_eq!(
423 display("answer", Action::Call(Array::default())),
424 "not callable [42.0]"
425 );
426 assert_eq!(
427 display("nothing", Action::Read),
428 "`undefined` has no JSON representation"
429 );
430 }
431
432 /// A bigint literal at and past the end of `i64`, read back as the decimal
433 /// text JavaScript gives each (JSON cannot hold a bigint).
434 #[test]
435 fn bigint_literals() {
436 let list = default_export(bigint::module);
437 let expected = [
438 "9223372036854775807",
439 "-9223372036854775808",
440 "9223372036854775808",
441 "-9223372036854775809",
442 "295147905179352825855",
443 "123456789012345678901234567890",
444 ];
445 for (i, text) in expected.into_iter().enumerate() {
446 let item = Any::dot(list.clone(), Number::from(i as f64).to_any())
447 .end()
448 .unwrap();
449 assert_eq!(item.to_string().unwrap(), text.into());
450 }
451 }
452
453 /// The numbers JSON cannot spell, asserted against Rust's own constants:
454 /// the generated layers share an emitter that could spell one wrongly in
455 /// both operands.
456 #[test]
457 fn special_numbers() {
458 let list = default_export(special_numbers::module);
459 let number = |i: usize| match Any::dot(list.clone(), Number::from(i as f64).to_any())
460 .end()
461 .unwrap()
462 .into()
463 {
464 Unpacked::Number(n) => f64::from(n),
465 _ => panic!("item {i} is not a number"),
466 };
467 assert_eq!(number(0).to_bits(), (-0.0_f64).to_bits());
468 assert!(number(1).is_nan());
469 assert_eq!(number(2), f64::INFINITY);
470 assert_eq!(number(3), f64::NEG_INFINITY);
471 }
472}