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, 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}