Skip to main content

nanvm_harness/
compare.rs

1//! A compiled fixture against its generated expectation (`gen.expected/`),
2//! the comparison of Stage 8 step 2 in
3//! `nanvm/lib/todo/callable-function-objects.md`.
4//!
5//! The expectation is the FunctionalScript interpreter's value for the same
6//! source, proved equal to Node's by `fjs/nanvm/corpus`. It has two layers
7//! that fail independently, and [`check`] runs both:
8//!
9//! 1. *The JSON bytes.* The text `Any::to_json` gives the compiled default,
10//!    against the bytes the compiler's own JSON writer produced. That path
11//!    shares nothing with the Rust literal emitter that spelled the compiled
12//!    fixture, and JSON text is ordered, so a wrong property order fails
13//!    here.
14//! 2. *The graph.* The compiled result against the expectation module the
15//!    interpreter's value was printed into: [`same_graph`].
16//!
17//! Neither layer runs the program's logic in Rust; the program reached Rust by
18//! compiling its source. The leaves the shared emitter could spell wrongly
19//! in both operands at once (`undefined`, `bigint`, `-0`, non-finite
20//! numbers) keep a hand-written assertion in `lib.rs`.
21
22use nanvm_lib::{
23    common::sized_index::SizedIndex,
24    naive::Naive,
25    vm::{Any, IVm, Nullish, Object, ToAny, Unpacked},
26};
27
28/// A corpus fixture and what it is expected to be, one row of the generated
29/// `gen.expected/mod.rs`.
30pub struct Case {
31    /// The fixture's Rust module name.
32    pub name: &'static str,
33    /// The compiled fixture's `module`, of the runtime value it is given.
34    pub fixture: crate::Module<Naive>,
35    /// The expectation's `module`: the interpreter's value as a graph, or the
36    /// throw it ended in, of the runtime value it is given and never reads.
37    pub expected: crate::Module<Naive>,
38    /// The UTF-8 bytes of the default's JSON text, where it has a JSON form.
39    pub json: Option<&'static [u8]>,
40    /// Whether the fixture is expected to throw while it initializes.
41    pub throws: bool,
42}
43
44/// Whether two graphs are the same value, and, where they are not, the first
45/// difference found, with the path to it.
46///
47/// Leaves compare as `Object.is` does, numbers by their bits (`NaN`, `-0`),
48/// everything else by `==`. Arrays compare by their elements. Objects compare
49/// by their [`Object::own_entries`], pairwise and in order, so a duplicated
50/// raw slot (an object spread's overwrite) equals the interpreter's
51/// normalized object while the right entries in the wrong observable order
52/// still fail.
53///
54/// Sharing is compared too: the two graphs are walked together, and an actual
55/// container must map to exactly one expected container and the reverse, so
56/// `[shared, shared]` is not two equal copies. `Any ==` is identity on a
57/// container, so the pairs seen live in a list searched with it, and no hash
58/// of an `Any` is needed.
59pub fn same_graph<A: IVm>(actual: &Any<A>, expected: &Any<A>) -> Result<(), String> {
60    Walk { pairs: Vec::new() }.walk("$", actual, expected)
61}
62
63struct Walk<A: IVm> {
64    /// The container pairs entered so far: an actual container with the
65    /// expected container it was matched to.
66    pairs: Vec<(Any<A>, Any<A>)>,
67}
68
69impl<A: IVm> Walk<A> {
70    fn walk(&mut self, path: &str, actual: &Any<A>, expected: &Any<A>) -> Result<(), String> {
71        match (actual.clone().into(), expected.clone().into()) {
72            (Unpacked::Number(a), Unpacked::Number(e)) => {
73                let (a, e) = (f64::from(a), f64::from(e));
74                if a.to_bits() == e.to_bits() {
75                    Ok(())
76                } else {
77                    Err(format!("{path}: {a:?} is not {e:?}"))
78                }
79            }
80            (Unpacked::Array(a), Unpacked::Array(e)) => {
81                if self.entered(path, actual, expected)? {
82                    return Ok(());
83                }
84                if a.length() != e.length() {
85                    return Err(format!(
86                        "{path}: length {} is not {}",
87                        a.length(),
88                        e.length()
89                    ));
90                }
91                (0..a.length()).try_for_each(|i| self.walk(&format!("{path}[{i}]"), &a[i], &e[i]))
92            }
93            (Unpacked::Object(a), Unpacked::Object(e)) => {
94                if self.entered(path, actual, expected)? {
95                    return Ok(());
96                }
97                let (a, e) = (a.own_entries(), e.own_entries());
98                if a.len() != e.len() {
99                    return Err(format!("{path}: {} entries are not {}", a.len(), e.len()));
100                }
101                a.iter().zip(&e).try_for_each(|((ak, av), (ek, ev))| {
102                    if ak == ek {
103                        self.walk(&format!("{path}.{ak:?}"), av, ev)
104                    } else {
105                        Err(format!("{path}: key {ak:?} is not {ek:?}"))
106                    }
107                })
108            }
109            _ if actual == expected => Ok(()),
110            _ => Err(format!("{path}: {actual:?} is not {expected:?}")),
111        }
112    }
113
114    /// Whether this pair of containers was already entered. A container seen
115    /// again must be seen against the same counterpart on both sides.
116    fn entered(&mut self, path: &str, actual: &Any<A>, expected: &Any<A>) -> Result<bool, String> {
117        match self
118            .pairs
119            .iter()
120            .find(|(a, e)| a == actual || e == expected)
121        {
122            Some((a, e)) if a == actual && e == expected => Ok(true),
123            Some(_) => Err(format!("{path}: sharing differs")),
124            None => {
125                self.pairs.push((actual.clone(), expected.clone()));
126                Ok(false)
127            }
128        }
129    }
130}
131
132/// The `default` export of a module's export object, `undefined` for a
133/// module without one: the expectation is the interpreter's `read 'default'`,
134/// which answers `undefined` for it.
135fn default_of<A: IVm>(exports: Any<A>) -> Result<Any<A>, String> {
136    let exports = Object::try_from(exports).map_err(|_| "the exports are not an object")?;
137    Ok(exports
138        .own_property(&"default".into())
139        .unwrap_or(Nullish::Undefined.to_any()))
140}
141
142/// Runs both layers for one case: `Ok` when the compiled fixture is what its
143/// expectation says, or the first difference.
144///
145/// Whether the fixture throws is the expectation's recorded `THROWS`, not
146/// whether its module happens to throw too: a regression in the emitter both
147/// share could make both throw. The thrown value, which is engine-specific,
148/// is not compared.
149pub fn check(case: &Case) -> Result<(), String> {
150    let runtime = crate::runtime::<Naive>();
151    match ((case.fixture)(runtime.clone()), case.throws) {
152        (Err(_), true) => Ok(()),
153        (Err(thrown), false) => Err(format!("threw {thrown:?}, expected a value")),
154        (Ok(value), true) => Err(format!("expected a throw, got {value:?}")),
155        (Ok(actual), false) => {
156            let expected =
157                (case.expected)(runtime).map_err(|e| format!("the expectation threw {e:?}"))?;
158            let (actual, expected) = (default_of(actual)?, default_of(expected)?);
159            check_json(case.json, &actual)?;
160            same_graph(&actual, &expected)
161        }
162    }
163}
164
165/// The JSON layer: the actual default's text is the expected bytes, or it has
166/// no JSON form because the expectation has none.
167fn check_json(json: Option<&[u8]>, actual: &Any<Naive>) -> Result<(), String> {
168    match (json, actual.clone().to_json()) {
169        (Some(bytes), Ok(text)) if text.as_bytes() == bytes => Ok(()),
170        (Some(bytes), Ok(text)) => Err(format!(
171            "JSON {text:?} is not {:?}",
172            String::from_utf8_lossy(bytes)
173        )),
174        (Some(_), Err(e)) => Err(format!("expected JSON, got {e}")),
175        (None, Err(_)) => Ok(()),
176        (None, Ok(text)) => Err(format!("expected no JSON form, got {text:?}")),
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use nanvm_lib::{
183        naive::Naive,
184        vm::{Any, ToAny, ToArray, ToObject},
185    };
186
187    use super::{Case, check, same_graph};
188
189    type V = Any<Naive>;
190
191    fn array(items: impl IntoIterator<Item = V>) -> V {
192        items.into_iter().collect::<Vec<_>>().to_array().to_any()
193    }
194
195    fn object<const N: usize>(entries: [(&str, V); N]) -> V {
196        entries.map(|(k, v)| (k.into(), v)).to_object().to_any()
197    }
198
199    fn num(x: f64) -> V {
200        x.to_any()
201    }
202
203    #[test]
204    fn outcomes_follow_the_independent_throw_marker() {
205        fn throws(_runtime: V) -> Result<V, V> {
206            Err(Any::undefined())
207        }
208        fn value(_runtime: V) -> Result<V, V> {
209            Ok(object([("default", Any::undefined())]))
210        }
211        for json in [Some(&b"1"[..]), None] {
212            let case = Case {
213                name: "regression",
214                fixture: throws,
215                expected: throws,
216                throws: false,
217                json,
218            };
219            assert!(check(&case).is_err());
220        }
221        let case = Case {
222            name: "throw",
223            fixture: value,
224            expected: value,
225            throws: true,
226            json: None,
227        };
228        assert!(check(&case).is_err());
229        assert_eq!(
230            check(&Case {
231                throws: false,
232                ..case
233            }),
234            Ok(())
235        );
236        assert_eq!(
237            check(&Case {
238                fixture: throws,
239                expected: throws,
240                ..case
241            }),
242            Ok(())
243        );
244        assert!(
245            check(&Case {
246                fixture: throws,
247                throws: false,
248                ..case
249            })
250            .is_err()
251        );
252        assert!(
253            check(&Case {
254                expected: throws,
255                throws: false,
256                ..case
257            })
258            .is_err()
259        );
260    }
261
262    /// `[shared, shared]` is one node reached twice; two equal copies are
263    /// not it, in either direction.
264    #[test]
265    fn sharing_is_compared() {
266        let shared = object([("x", num(1.0))]);
267        let twice = array([shared.clone(), shared]);
268        let copies = array([object([("x", num(1.0))]), object([("x", num(1.0))])]);
269        assert_eq!(same_graph(&twice, &twice.clone()), Ok(()));
270        assert_eq!(same_graph(&copies, &copies.clone()), Ok(()));
271        assert_eq!(
272            same_graph(&twice, &copies),
273            Err("$[1]: sharing differs".into())
274        );
275        assert_eq!(
276            same_graph(&copies, &twice),
277            Err("$[1]: sharing differs".into())
278        );
279    }
280
281    /// `NaN` equals itself and `-0` is not `0`: numbers compare by bits.
282    #[test]
283    fn numbers_compare_by_bits() {
284        assert_eq!(same_graph(&num(f64::NAN), &num(f64::NAN)), Ok(()));
285        assert_eq!(
286            same_graph(&num(-0.0), &num(0.0)),
287            Err("$: -0.0 is not 0.0".into())
288        );
289    }
290
291    /// Entries compare in order, and a duplicated raw slot is the one entry
292    /// an interpreter's normalized object has.
293    #[test]
294    fn objects_compare_by_observable_entries() {
295        let ab = object([("a", num(1.0)), ("b", num(2.0))]);
296        let ba = object([("b", num(2.0)), ("a", num(1.0))]);
297        assert_eq!(
298            same_graph(&ab, &ba),
299            Err("$: key \"a\" is not \"b\"".into())
300        );
301        let overwritten = object([("a", num(0.0)), ("b", num(2.0)), ("a", num(1.0))]);
302        assert_eq!(same_graph(&overwritten, &ab), Ok(()));
303        assert_eq!(
304            same_graph(&ab, &object([("a", num(1.0))])),
305            Err("$: 2 entries are not 1".into())
306        );
307    }
308
309    /// A difference is reported with the path to it, and a container is not a
310    /// leaf of the same value.
311    #[test]
312    fn differences_are_located() {
313        let a = array([num(1.0), object([("k", array([num(2.0)]))])]);
314        let b = array([num(1.0), object([("k", array([num(3.0)]))])]);
315        assert_eq!(
316            same_graph(&a, &b),
317            Err("$[1].\"k\"[0]: 2.0 is not 3.0".into())
318        );
319        assert_eq!(
320            same_graph(&array([num(1.0)]), &array([])),
321            Err("$: length 1 is not 0".into())
322        );
323        assert!(same_graph(&array([]), &object([])).is_err());
324    }
325}