macro_rules! wasm_compare { ($source:expr, $emacs:expr, $wasm:expr, $(($emacs_field:expr, $wasm_value_getter:expr, $compare_fn: expr)),*) => {{ let mut result = WasmDiffResult::default(); let emacs_list = $emacs.as_list()?; let mut emacs_list_iter = emacs_list.iter(); let emacs_name = emacs_list_iter .next() .ok_or("Should have an attributes child.")? .as_atom()?; result.name = emacs_name.into(); let emacs_attributes_map = emacs_list_iter .next() .ok_or("Should have an attributes child.")? .as_map()?; let mut emacs_keys: std::collections::BTreeSet<&str> = emacs_attributes_map.keys().map(|s| *s).collect(); // Mark :standard-properties as seen because it will be handled separately. if emacs_keys.contains(":standard-properties") { emacs_keys.remove(":standard-properties"); } else { result.status.push(WasmDiffStatus::Bad( "Emacs node lacked :standard-properties field.".into(), )); } { // Compare name. let wasm_name = $wasm.get_elisp_name(); if emacs_name != wasm_name { result.status.push(WasmDiffStatus::Bad( format!( "AST node name mismatch. Emacs=({emacs}) Wasm=({wasm}).", emacs = emacs_name, wasm = wasm_name, ) .into(), )); } } { // Compare standard properties. result.extend(wasm_compare_standard_properties($source, $emacs, &$wasm.standard_properties)?)?; } { // Compare additional properties. let additional_property_names: Vec = $wasm.additional_properties.get_elisp_names().collect(); for additional_property in additional_property_names.iter().map(String::as_str).map(EmacsField::Required) { match additional_property { EmacsField::Required(name) if emacs_keys.contains(name) => { emacs_keys.remove(name); } EmacsField::Optional(name) if emacs_keys.contains(name) => { emacs_keys.remove(name); } EmacsField::Required(name) => { result.status.push(WasmDiffStatus::Bad( format!( "Emacs node lacked required field ({name}).", name = name, ) .into(), )); } EmacsField::Optional(_name) => {} } } result.extend(wasm_compare_additional_properties($source, $emacs, &$wasm.additional_properties)?)?; } { // Compare children. result.extend(wasm_compare_list( $source, emacs_list_iter, $wasm.children.iter(), )?)?; } { // Check for properties that are missing from the elisp node. $( match $emacs_field { EmacsField::Required(name) if emacs_keys.contains(name) => { emacs_keys.remove(name); } EmacsField::Optional(name) if emacs_keys.contains(name) => { emacs_keys.remove(name); } EmacsField::Required(name) => { result.status.push(WasmDiffStatus::Bad( format!( "Emacs node lacked required field ({name}).", name = name, ) .into(), )); } EmacsField::Optional(_name) => {} } )* } { // Check for elisp properties that were not compared. if !emacs_keys.is_empty() { let unexpected_keys: Vec<&str> = emacs_keys.into_iter().collect(); let unexpected_keys = unexpected_keys.join(", "); result.status.push(WasmDiffStatus::Bad( format!( "Emacs node had extra field(s): ({field_names}).", field_names = unexpected_keys, ) .into(), )); } } { // Compare properties. $( let emacs_name = match $emacs_field { EmacsField::Required(name) => { name }, EmacsField::Optional(name) => { name }, }; result.extend($compare_fn($source, $emacs, $wasm, emacs_name, $wasm_value_getter)?)?; )* } result }}; } pub(crate) use wasm_compare;