Skip to main content

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}