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, exports},
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: nanvm_lib::vm::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: nanvm_lib::vm::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 by [`Any::same_value`], which is `Object.is`. Arrays compare by their elements. Objects compare
48/// by their [`Object::own_entries`], pairwise and in order, so a duplicated
49/// raw slot (an object spread's overwrite) equals the interpreter's
50/// normalized object while the right entries in the wrong observable order
51/// still fail.
52///
53/// Sharing is compared too: the two graphs are walked together, and an actual
54/// container must map to exactly one expected container and the reverse, so
55/// `[shared, shared]` is not two equal copies. `Any ==` is identity on a
56/// container, so the pairs seen live in a list searched with it, and no hash
57/// of an `Any` is needed.
58pub fn same_graph<A: IVm>(actual: &Any<A>, expected: &Any<A>) -> Result<(), String> {
59    Walk { pairs: Vec::new() }.walk("$", actual, expected)
60}
61
62struct Walk<A: IVm> {
63    /// The container pairs entered so far: an actual container with the
64    /// expected container it was matched to.
65    pairs: Vec<(Any<A>, Any<A>)>,
66}
67
68impl<A: IVm> Walk<A> {
69    fn walk(&mut self, path: &str, actual: &Any<A>, expected: &Any<A>) -> Result<(), String> {
70        match (actual.clone().into(), expected.clone().into()) {
71            (Unpacked::Array(a), Unpacked::Array(e)) => {
72                if self.entered(path, actual, expected)? {
73                    return Ok(());
74                }
75                if a.length() != e.length() {
76                    return Err(format!(
77                        "{path}: length {} is not {}",
78                        a.length(),
79                        e.length()
80                    ));
81                }
82                (0..a.length()).try_for_each(|i| self.walk(&format!("{path}[{i}]"), &a[i], &e[i]))
83            }
84            (Unpacked::Object(a), Unpacked::Object(e)) => {
85                if self.entered(path, actual, expected)? {
86                    return Ok(());
87                }
88                let (a, e) = (a.own_entries(), e.own_entries());
89                if a.len() != e.len() {
90                    return Err(format!("{path}: {} entries are not {}", a.len(), e.len()));
91                }
92                a.iter().zip(&e).try_for_each(|((ak, av), (ek, ev))| {
93                    if ak == ek {
94                        self.walk(&format!("{path}.{ak:?}"), av, ev)
95                    } else {
96                        Err(format!("{path}: key {ak:?} is not {ek:?}"))
97                    }
98                })
99            }
100            _ if actual.same_value(expected) => Ok(()),
101            _ => Err(format!("{path}: {actual:?} is not {expected:?}")),
102        }
103    }
104
105    /// Whether this pair of containers was already entered. A container seen
106    /// again must be seen against the same counterpart on both sides.
107    fn entered(&mut self, path: &str, actual: &Any<A>, expected: &Any<A>) -> Result<bool, String> {
108        match self
109            .pairs
110            .iter()
111            .find(|(a, e)| a == actual || e == expected)
112        {
113            Some((a, e)) if a == actual && e == expected => Ok(true),
114            Some(_) => Err(format!("{path}: sharing differs")),
115            None => {
116                self.pairs.push((actual.clone(), expected.clone()));
117                Ok(false)
118            }
119        }
120    }
121}
122
123/// The `default` export of a module's export object, `undefined` for a
124/// module without one: the expectation is the interpreter's `read 'default'`,
125/// which answers `undefined` for it.
126fn default_of<A: IVm>(exports: Object<A>) -> Any<A> {
127    exports
128        .own_property(&"default".into())
129        .unwrap_or(Nullish::Undefined.to_any())
130}
131
132/// Runs both layers for one case: `Ok` when the compiled fixture is what its
133/// expectation says, or the first difference.
134///
135/// Whether the fixture throws is the expectation's recorded `THROWS`, not
136/// whether its module happens to throw too: a regression in the emitter both
137/// share could make both throw. The thrown value, which is engine-specific,
138/// is not compared.
139pub fn check(case: &Case) -> Result<(), String> {
140    match (exports(case.fixture), case.throws) {
141        (Err(_), true) => Ok(()),
142        (Err(thrown), false) => Err(format!("threw {thrown:?}, expected a value")),
143        (Ok(value), true) => Err(format!("expected a throw, got {value:?}")),
144        (Ok(actual), false) => {
145            let expected =
146                exports(case.expected).map_err(|e| format!("the expectation threw {e:?}"))?;
147            let (actual, expected) = (default_of(actual), default_of(expected));
148            check_json(case.json, &actual)?;
149            same_graph(&actual, &expected)
150        }
151    }
152}
153
154/// The JSON layer: the actual default's text is the expected bytes, or it has
155/// no JSON form because the expectation has none.
156fn check_json(json: Option<&[u8]>, actual: &Any<Naive>) -> Result<(), String> {
157    match (json, actual.clone().to_json()) {
158        (Some(bytes), Ok(text)) if text.as_bytes() == bytes => Ok(()),
159        (Some(bytes), Ok(text)) => Err(format!(
160            "JSON {text:?} is not {:?}",
161            String::from_utf8_lossy(bytes)
162        )),
163        (Some(_), Err(e)) => Err(format!("expected JSON, got {e}")),
164        (None, Err(_)) => Ok(()),
165        (None, Ok(text)) => Err(format!("expected no JSON form, got {text:?}")),
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use nanvm_lib::{
172        naive::Naive,
173        vm::{Any, ToAny, ToArray, ToObject},
174    };
175
176    use super::{Case, check, same_graph};
177
178    type V = Any<Naive>;
179
180    fn array(items: impl IntoIterator<Item = V>) -> V {
181        items.into_iter().collect::<Vec<_>>().to_array().to_any()
182    }
183
184    fn object<const N: usize>(entries: [(&str, V); N]) -> V {
185        entries.map(|(k, v)| (k.into(), v)).to_object().to_any()
186    }
187
188    fn num(x: f64) -> V {
189        x.to_any()
190    }
191
192    #[test]
193    fn outcomes_follow_the_independent_throw_marker() {
194        fn throws(_runtime: V) -> Result<V, V> {
195            Err(Any::undefined())
196        }
197        fn value(_runtime: V) -> Result<V, V> {
198            Ok(object([("default", Any::undefined())]))
199        }
200        for json in [Some(&b"1"[..]), None] {
201            let case = Case {
202                name: "regression",
203                fixture: throws,
204                expected: throws,
205                throws: false,
206                json,
207            };
208            assert!(check(&case).is_err());
209        }
210        let case = Case {
211            name: "throw",
212            fixture: value,
213            expected: value,
214            throws: true,
215            json: None,
216        };
217        assert!(check(&case).is_err());
218        assert_eq!(
219            check(&Case {
220                throws: false,
221                ..case
222            }),
223            Ok(())
224        );
225        assert_eq!(
226            check(&Case {
227                fixture: throws,
228                expected: throws,
229                ..case
230            }),
231            Ok(())
232        );
233        assert!(
234            check(&Case {
235                fixture: throws,
236                throws: false,
237                ..case
238            })
239            .is_err()
240        );
241        assert!(
242            check(&Case {
243                expected: throws,
244                throws: false,
245                ..case
246            })
247            .is_err()
248        );
249    }
250
251    /// `[shared, shared]` is one node reached twice; two equal copies are
252    /// not it, in either direction.
253    #[test]
254    fn sharing_is_compared() {
255        let shared = object([("x", num(1.0))]);
256        let twice = array([shared.clone(), shared]);
257        let copies = array([object([("x", num(1.0))]), object([("x", num(1.0))])]);
258        assert_eq!(same_graph(&twice, &twice.clone()), Ok(()));
259        assert_eq!(same_graph(&copies, &copies.clone()), Ok(()));
260        assert_eq!(
261            same_graph(&twice, &copies),
262            Err("$[1]: sharing differs".into())
263        );
264        assert_eq!(
265            same_graph(&copies, &twice),
266            Err("$[1]: sharing differs".into())
267        );
268    }
269
270    /// `NaN` equals itself and `-0` is not `0`: numbers compare by bits.
271    #[test]
272    fn numbers_compare_by_bits() {
273        assert_eq!(same_graph(&num(f64::NAN), &num(f64::NAN)), Ok(()));
274        assert_eq!(
275            same_graph(&num(-0.0), &num(0.0)),
276            Err("$: -0.0 is not 0.0".into())
277        );
278    }
279
280    /// Entries compare in order, and a duplicated raw slot is the one entry
281    /// an interpreter's normalized object has.
282    #[test]
283    fn objects_compare_by_observable_entries() {
284        let ab = object([("a", num(1.0)), ("b", num(2.0))]);
285        let ba = object([("b", num(2.0)), ("a", num(1.0))]);
286        assert_eq!(
287            same_graph(&ab, &ba),
288            Err("$: key \"a\" is not \"b\"".into())
289        );
290        let overwritten = object([("a", num(0.0)), ("b", num(2.0)), ("a", num(1.0))]);
291        assert_eq!(same_graph(&overwritten, &ab), Ok(()));
292        assert_eq!(
293            same_graph(&ab, &object([("a", num(1.0))])),
294            Err("$: 2 entries are not 1".into())
295        );
296    }
297
298    /// A difference is reported with the path to it, and a container is not a
299    /// leaf of the same value.
300    #[test]
301    fn differences_are_located() {
302        let a = array([num(1.0), object([("k", array([num(2.0)]))])]);
303        let b = array([num(1.0), object([("k", array([num(3.0)]))])]);
304        assert_eq!(
305            same_graph(&a, &b),
306            Err("$[1].\"k\"[0]: 2.0 is not 3.0".into())
307        );
308        assert_eq!(
309            same_graph(&array([num(1.0)]), &array([])),
310            Err("$: length 1 is not 0".into())
311        );
312        assert!(same_graph(&array([]), &object([])).is_err());
313    }
314}