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, Object};
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/// A compiled module's `module`: a function of the runtime value it is
102/// given, answering its export object or the throw its initialization
103/// ended in.
104pub type Module<A> = fn(Any<A>) -> Result<Any<A>, Any<A>>;
105
106/// The runtime value every compiled module takes as its parameter, built
107/// once per run: `nanvm_lib::vm::runtime`, which cannot fail for the
108/// runtime the repository compiles.
109pub fn runtime<A: IVm>() -> Any<A> {
110 nanvm_lib::vm::runtime::<A>().expect("the runtime value")
111}
112
113/// Evaluates a module once, selects one of its exports, reads or calls it,
114/// and renders that one value as JSON. Nothing else in the export object is
115/// called or serialized.
116///
117/// `module` is the shape generated modules expose, [`Module<A>`]:
118/// `pub fn module<A: IVm>(runtime: Any<A>) -> Result<Any<A>, Any<A>>`,
119/// taking the runtime value and returning the object of all exports, or
120/// the value the module threw. Any other result is a generator bug, not an
121/// input, and panics. A module holding a function bounds `A` on
122/// `IStaticFunction`, which `naive` implements.
123///
124/// `export` is looked up among the object's own properties by presence,
125/// not by value: an export holding `undefined` is found, and reading it
126/// then fails as [`JsonError::Undefined`]. `default` is one name among
127/// them, with no fallback to it: a module without one answers
128/// `NoExport("default")` for it.
129///
130/// A call converts a clone of the selected value with
131/// [`Function::try_from`], whose own error is a fresh `TypeError`, so that
132/// [`RunError::NotCallable`] holds the value the caller tried to call. It
133/// does not go through `Any::call`, where a value that is not a function
134/// would be a thrown `TypeError`, indistinguishable from the program
135/// throwing.
136pub fn run<A: IVm>(
137 module: Module<A>,
138 export: &str,
139 action: Action<A>,
140) -> Result<std::string::String, RunError<A>> {
141 let exports: Object<A> = module(runtime::<A>())
142 .map_err(RunError::Thrown)?
143 .try_into()
144 .expect("a compiled module returns its export object");
145 let value = exports
146 .own_property(&export.into())
147 .ok_or_else(|| RunError::NoExport(export.into()))?;
148 match action {
149 Action::Read => value,
150 Action::Call(args) => Function::try_from(value.clone())
151 .map_err(|_| RunError::NotCallable(value))?
152 .call(args)
153 .map_err(RunError::Thrown)?,
154 }
155 .to_json()
156 .map_err(RunError::Json)
157}
158
159#[cfg(test)]
160mod tests {
161 use nanvm_lib::{
162 naive::Naive,
163 vm::{
164 Any, Array, Function, IVm, JsonError, Nullish, Number, Object, ToAny, ToArray, Unpacked,
165 },
166 };
167
168 use crate::{
169 Action, RunError,
170 fixtures::{
171 bigint, exports, function, function_text, missing, named, named_imports, number,
172 object, rest_function, special_numbers, throw,
173 },
174 run, runtime,
175 };
176
177 /// A compiled fixture's `module`, at the VM every test runs.
178 type Module = crate::Module<Naive>;
179
180 /// The fixture's default export as JSON text, the way most tests read it.
181 ///
182 /// The tests stay `#[test]`s rather than rows of one table: a failing
183 /// one then fails alone, under its own name, while the rest still run,
184 /// and the doc comment saying what it proves stays on it.
185 fn read_default(module: Module) -> Result<std::string::String, RunError<Naive>> {
186 run(module, "default", Action::Read)
187 }
188
189 /// The fixture's default export as a value, for a test that checks what
190 /// JSON cannot hold.
191 fn default_export(module: Module) -> Any<Naive> {
192 module(runtime())
193 .unwrap()
194 .dot("default".into())
195 .end()
196 .unwrap()
197 }
198
199 /// The whole export object, not only `default`: the corpus compares the
200 /// default alone (named exports are decision 3 of Stage 8, not yet
201 /// compared), so an extra or missing export would pass there.
202 #[test]
203 fn module_result_contains_exports() {
204 assert_eq!(
205 number::module::<Naive>(runtime()).unwrap().to_json(),
206 Ok(r#"{"default":42}"#.into())
207 );
208 assert_eq!(
209 object::module::<Naive>(runtime()).unwrap().to_json(),
210 Ok(r#"{"default":{"a":1,"b":"two"}}"#.into())
211 );
212 }
213
214 /// Every corpus fixture against its generated expectation, both layers
215 /// (see [`crate::check`]). One test over the whole list, since a new
216 /// fixture is a new row and not a new test; every difference is reported,
217 /// not the first alone.
218 #[test]
219 fn corpus_matches_expectations() {
220 let failures: Vec<_> = crate::expected::CASES
221 .iter()
222 .filter_map(|case| {
223 crate::check(case)
224 .err()
225 .map(|e| format!("{}: {e}", case.name))
226 })
227 .collect();
228 assert!(failures.is_empty(), "{}", failures.join("\n"));
229 }
230
231 /// A module written by hand in the shape the generator prints, throwing
232 /// a value of the test's choosing.
233 fn throwing<A: IVm>(_runtime: Any<A>) -> Result<Any<A>, Any<A>> {
234 Err("boom".into())
235 }
236
237 #[test]
238 fn thrown_value_is_reported_not_panicked() {
239 let error = read_default(throwing).unwrap_err();
240 assert!(matches!(&error, RunError::Thrown(v) if *v == "boom".into()));
241 assert_eq!(error.to_string(), "uncaught \"boom\"");
242 }
243
244 /// A module written by hand answering what no compiled module does: a
245 /// value that is not the export object.
246 fn not_an_object<A: IVm>(_runtime: Any<A>) -> Result<Any<A>, Any<A>> {
247 Ok(Number::from(42.0).to_any())
248 }
249
250 /// A broken module is refused, not reported as a value with no JSON.
251 #[test]
252 #[should_panic(expected = "a compiled module returns its export object")]
253 fn non_object_module_panics() {
254 let _ = read_default(not_an_object);
255 }
256
257 /// The language's own `throw`: a function whose block body ends in the
258 /// statement fails with the value when called, and the module, calling
259 /// it at load, answers that value — `7`, the argument it threw.
260 #[test]
261 fn compiled_throw_statement_is_reported() {
262 assert_eq!(
263 read_default(throw::module),
264 Err(RunError::Thrown(Number::from(7.0).to_any()))
265 );
266 }
267
268 /// A function as the default export, called rather than read: with no
269 /// arguments, and with a rest parameter that gathers what it is given.
270 #[test]
271 fn function_exports() {
272 assert_eq!(
273 run::<Naive>(function::module, "default", Action::Call(Array::default())),
274 Ok("42".into())
275 );
276 let args = [1.0.to_any(), 2.0.to_any()].to_array();
277 assert_eq!(
278 run::<Naive>(rest_function::module, "default", Action::Call(args)),
279 Ok("[1,2]".into())
280 );
281 }
282
283 /// An undefined body still produces an ordinary callable, with its own
284 /// identity and source text, including when constructed inside a call.
285 #[test]
286 fn undefined_function_exports() {
287 let exports: Object<Naive> = function::module::<Naive>(runtime())
288 .unwrap()
289 .try_into()
290 .unwrap();
291 let noop = exports.own_property(&"noop".into()).unwrap();
292 assert_eq!(noop, exports.own_property(&"repeated".into()).unwrap());
293 assert_ne!(noop, exports.own_property(&"other".into()).unwrap());
294 let make = Function::try_from(exports.own_property(&"make".into()).unwrap()).unwrap();
295 let first = make.call(Array::default()).unwrap();
296 let second = make.call(Array::default()).unwrap();
297 assert_ne!(first, second);
298 for value in [noop, first, second] {
299 assert_eq!(value.clone().to_string(), Ok("()=>undefined".into()));
300 let callable = Function::try_from(value).unwrap();
301 assert_eq!(callable.length(), 0);
302 assert_eq!(
303 callable.call(Array::default()),
304 Ok(Nullish::Undefined.to_any())
305 );
306 }
307 }
308
309 /// A function's text is the FunctionalScript writer's: converted where
310 /// it is made, returned from a call with its capture named by its slot,
311 /// and exported, read as a value and converted by the host, and as a
312 /// property key, which names the field its text does — the `entry`
313 /// helper's value, `7`, and `undefined` where no field has it. Two
314 /// functions with one text stay two identities.
315 #[test]
316 fn function_texts() {
317 assert_eq!(
318 read_default(function_text::module),
319 Ok(r#"["()=>1","()=>1!","()=>1|2","(...$1)=>$0[0]+$1[0]","(...$0)=>(...$1)=>$0[0]+$1[0]",true,false,7,8,false,false]"#.into())
320 );
321 let exports: Object<Naive> = function_text::module::<Naive>(runtime())
322 .unwrap()
323 .try_into()
324 .unwrap();
325 let inc = exports.own_property(&"inc".into()).unwrap();
326 assert_eq!(inc.to_string(), Ok("(...$1)=>$0[0]+$1[0]".into()));
327 }
328
329 /// A read past the arguments supplied answers `undefined`, which has no
330 /// JSON, so the value is checked as it is.
331 #[test]
332 fn missing_argument() {
333 assert_eq!(default_export(missing::module), Nullish::Undefined.to_any());
334 }
335
336 #[test]
337 fn named_exports() {
338 assert_eq!(
339 named::module::<Naive>(runtime()).unwrap().to_json(),
340 Ok(r#"{"a":[5],"default":[5],"z":[5]}"#.into())
341 );
342 assert_eq!(read_default(named::module), Ok("[5]".into()));
343 }
344
345 /// The MVP acceptance example (`todo/fjs-nanvm-integration.md`): a
346 /// module calls a function it imports by name, and the harness selects
347 /// its `main` and calls it with no arguments. Its identity checks are the
348 /// default the corpus compares.
349 #[test]
350 fn named_imports_and_captures() {
351 let select = |name, action| run::<Naive>(named_imports::module, name, action);
352 assert_eq!(
353 select("main", Action::Call(Array::default())),
354 Ok("42".into())
355 );
356 }
357
358 /// `exports.mjs` against each outcome `run` tells apart. Evaluating the
359 /// module succeeds although `fails` throws when called: loading a module
360 /// calls none of its exports.
361 #[test]
362 fn export_selection() {
363 let run = |name, action| run::<Naive>(exports::module, name, action);
364 // A value, and a call with supplied arguments.
365 assert_eq!(run("answer", Action::Read), Ok("[42]".into()));
366 let args = [20.0.to_any(), 22.0.to_any()].to_array();
367 assert_eq!(run("add", Action::Call(args)), Ok("42".into()));
368 // Absent versus a present `undefined`, which then has no JSON.
369 assert_eq!(
370 run("missing", Action::Read),
371 Err(RunError::NoExport("missing".into()))
372 );
373 assert_eq!(
374 run("nothing", Action::Read),
375 Err(RunError::Json(JsonError::Undefined))
376 );
377 // A named-only module has no `default` to fall back on.
378 assert_eq!(
379 run("default", Action::Read),
380 Err(RunError::NoExport("default".into()))
381 );
382 // Calling a value that is not a function reports that value; a call
383 // that throws reports the throw.
384 assert!(matches!(
385 run("answer", Action::Call(Array::default())),
386 Err(RunError::NotCallable(v)) if v.clone().to_json() == Ok("[42]".into())
387 ));
388 assert!(matches!(
389 run("fails", Action::Call(Array::default())),
390 Err(RunError::Thrown(_))
391 ));
392 // Reading a function does not call it: read, `fails` is the
393 // function JSON refuses, not the throw a call gives.
394 assert_eq!(
395 run("fails", Action::Read),
396 Err(RunError::Json(JsonError::Function))
397 );
398 assert_eq!(
399 run("add", Action::Read),
400 Err(RunError::Json(JsonError::Function))
401 );
402 }
403
404 /// Selection from a default-only and from a mixed module: `default` is
405 /// one name among the exports, found only where the module has it, and
406 /// beside it a mixed module's other exports are found too.
407 #[test]
408 fn default_is_one_export() {
409 assert_eq!(
410 run::<Naive>(number::module, "default", Action::Read),
411 Ok("42".into())
412 );
413 assert_eq!(
414 run::<Naive>(number::module, "z", Action::Read),
415 Err(RunError::NoExport("z".into()))
416 );
417 assert_eq!(
418 run::<Naive>(named::module, "default", Action::Read),
419 Ok("[5]".into())
420 );
421 assert_eq!(
422 run::<Naive>(named::module, "z", Action::Read),
423 Ok("[5]".into())
424 );
425 }
426
427 /// Each failure prints as what it is.
428 #[test]
429 fn run_error_display() {
430 let display = |name, action| {
431 run::<Naive>(exports::module, name, action)
432 .unwrap_err()
433 .to_string()
434 };
435 assert_eq!(display("main", Action::Read), r#"no export "main""#);
436 assert_eq!(
437 display("answer", Action::Call(Array::default())),
438 "not callable [42.0]"
439 );
440 assert_eq!(
441 display("nothing", Action::Read),
442 "`undefined` has no JSON representation"
443 );
444 }
445
446 /// A bigint literal at and past the end of `i64`, read back as the decimal
447 /// text JavaScript gives each (JSON cannot hold a bigint).
448 #[test]
449 fn bigint_literals() {
450 let list = default_export(bigint::module);
451 let expected = [
452 "9223372036854775807",
453 "-9223372036854775808",
454 "9223372036854775808",
455 "-9223372036854775809",
456 "295147905179352825855",
457 "123456789012345678901234567890",
458 ];
459 for (i, text) in expected.into_iter().enumerate() {
460 let item = Any::dot(list.clone(), Number::from(i as f64).to_any())
461 .end()
462 .unwrap();
463 assert_eq!(item.to_string().unwrap(), text.into());
464 }
465 }
466
467 /// The numbers JSON cannot spell, asserted against Rust's own constants:
468 /// the generated layers share an emitter that could spell one wrongly in
469 /// both operands.
470 #[test]
471 fn special_numbers() {
472 let list = default_export(special_numbers::module);
473 let number = |i: usize| match Any::dot(list.clone(), Number::from(i as f64).to_any())
474 .end()
475 .unwrap()
476 .into()
477 {
478 Unpacked::Number(n) => f64::from(n),
479 _ => panic!("item {i} is not a number"),
480 };
481 assert_eq!(number(0).to_bits(), (-0.0_f64).to_bits());
482 assert!(number(1).is_nan());
483 assert_eq!(number(2), f64::INFINITY);
484 assert_eq!(number(3), f64::NEG_INFINITY);
485 }
486}