organic/src/wasm_test/compare.rs

615 lines
22 KiB
Rust
Raw Normal View History

use std::borrow::Cow;
2023-12-29 20:38:18 +00:00
use std::collections::HashMap;
use super::diff::WasmDiffResult;
use super::diff::WasmDiffStatus;
2023-12-29 20:38:18 +00:00
use crate::compare::get_emacs_standard_properties;
use crate::compare::maybe_token_to_usize;
2023-12-29 22:24:38 +00:00
use crate::compare::unquote;
2023-12-27 20:14:42 +00:00
use crate::compare::ElispFact;
use crate::compare::EmacsField;
2023-12-29 20:38:18 +00:00
use crate::compare::EmacsStandardProperties;
2023-12-29 22:24:38 +00:00
use crate::compare::TextWithProperties;
2023-12-27 14:31:54 +00:00
use crate::compare::Token;
use crate::wasm::WasmAngleLink;
2023-12-27 16:10:40 +00:00
use crate::wasm::WasmAstNode;
use crate::wasm::WasmAstNodeWrapper;
use crate::wasm::WasmBabelCall;
use crate::wasm::WasmBold;
use crate::wasm::WasmCenterBlock;
use crate::wasm::WasmCitation;
use crate::wasm::WasmCitationReference;
use crate::wasm::WasmClock;
use crate::wasm::WasmCode;
use crate::wasm::WasmComment;
use crate::wasm::WasmCommentBlock;
use crate::wasm::WasmDiarySexp;
2023-12-27 14:31:54 +00:00
use crate::wasm::WasmDocument;
use crate::wasm::WasmDrawer;
use crate::wasm::WasmDynamicBlock;
use crate::wasm::WasmEntity;
use crate::wasm::WasmExampleBlock;
use crate::wasm::WasmExportBlock;
use crate::wasm::WasmExportSnippet;
use crate::wasm::WasmFixedWidthArea;
use crate::wasm::WasmFootnoteDefinition;
use crate::wasm::WasmFootnoteReference;
2023-12-27 21:47:02 +00:00
use crate::wasm::WasmHeadline;
use crate::wasm::WasmHorizontalRule;
use crate::wasm::WasmInlineBabelCall;
use crate::wasm::WasmInlineSourceBlock;
use crate::wasm::WasmItalic;
use crate::wasm::WasmKeyword;
use crate::wasm::WasmLatexEnvironment;
use crate::wasm::WasmLatexFragment;
use crate::wasm::WasmLineBreak;
use crate::wasm::WasmNodeProperty;
use crate::wasm::WasmOrgMacro;
2023-12-27 23:47:59 +00:00
use crate::wasm::WasmParagraph;
use crate::wasm::WasmPlainLink;
use crate::wasm::WasmPlainList;
use crate::wasm::WasmPlainListItem;
use crate::wasm::WasmPlainText;
use crate::wasm::WasmPlanning;
use crate::wasm::WasmPropertyDrawer;
use crate::wasm::WasmQuoteBlock;
use crate::wasm::WasmRadioLink;
use crate::wasm::WasmRadioTarget;
use crate::wasm::WasmRegularLink;
2023-12-27 21:47:02 +00:00
use crate::wasm::WasmSection;
use crate::wasm::WasmSpecialBlock;
use crate::wasm::WasmSrcBlock;
use crate::wasm::WasmStatisticsCookie;
use crate::wasm::WasmStrikeThrough;
use crate::wasm::WasmSubscript;
use crate::wasm::WasmSuperscript;
use crate::wasm::WasmTable;
use crate::wasm::WasmTableCell;
use crate::wasm::WasmTableRow;
use crate::wasm::WasmTarget;
use crate::wasm::WasmTimestamp;
use crate::wasm::WasmUnderline;
use crate::wasm::WasmVerbatim;
use crate::wasm::WasmVerseBlock;
2023-12-27 20:14:42 +00:00
use crate::wasm_test::macros::wasm_compare;
2023-12-27 14:31:54 +00:00
2023-12-29 17:49:43 +00:00
pub fn wasm_compare_document<'e, 's, 'w>(
2023-12-27 16:10:40 +00:00
source: &'s str,
2023-12-29 17:49:43 +00:00
emacs: &'e Token<'s>,
wasm: &'w WasmAstNodeWrapper<WasmDocument>,
) -> Result<WasmDiffResult<'s>, Box<dyn std::error::Error>> {
let wasm_json = serde_json::to_string(&wasm)?;
let wasm_json_parsed = serde_json::from_str(&wasm_json)?;
2023-12-29 21:56:02 +00:00
compare_json_value(source, emacs, &wasm_json_parsed)
}
fn compare_json_value<'b, 's>(
source: &'s str,
emacs: &'b Token<'s>,
2023-12-29 21:56:02 +00:00
wasm: &serde_json::Value,
) -> Result<WasmDiffResult<'s>, Box<dyn std::error::Error>> {
2023-12-29 21:51:52 +00:00
println!("XXXXXXXXXXXXXX compare_json_value XXXXXXXXXXXXXX");
println!("{:?}", emacs);
2023-12-29 21:56:02 +00:00
println!("{:?}", wasm);
2023-12-29 21:51:52 +00:00
println!("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
2023-12-29 21:56:02 +00:00
match (wasm, emacs) {
(serde_json::Value::Object(wasm), Token::List(el)) if wasm.contains_key("ast-node") => {
// We hit a regular ast node.
compare_ast_node(source, el, wasm)
}
(serde_json::Value::String(w), Token::Atom(e))
if e.starts_with('"') && e.ends_with('"') =>
{
// We hit a string compared against a quoted string from elisp (as opposed to an unquoted literal).
compare_quoted_string(source, e, w)
}
2023-12-29 21:51:52 +00:00
(serde_json::Value::Array(w), Token::List(e)) => {
wasm_compare_list(source, e.iter(), w.iter())
}
(serde_json::Value::Object(wasm), Token::List(e))
if wasm.contains_key("optval") && wasm.contains_key("val") =>
{
compare_optional_pair(source, e, wasm)
}
2023-12-29 22:24:38 +00:00
(serde_json::Value::Object(w), Token::TextWithProperties(e)) if is_plain_text(w) => {
compare_plain_text(source, e, w)
}
(serde_json::Value::Null, Token::Atom(_)) => todo!(),
(serde_json::Value::Null, Token::List(_)) => todo!(),
(serde_json::Value::Null, Token::TextWithProperties(_)) => todo!(),
(serde_json::Value::Null, Token::Vector(_)) => todo!(),
(serde_json::Value::Bool(_), Token::Atom(_)) => todo!(),
(serde_json::Value::Bool(_), Token::List(_)) => todo!(),
(serde_json::Value::Bool(_), Token::TextWithProperties(_)) => todo!(),
(serde_json::Value::Bool(_), Token::Vector(_)) => todo!(),
(serde_json::Value::Number(_), Token::Atom(_)) => todo!(),
(serde_json::Value::Number(_), Token::List(_)) => todo!(),
(serde_json::Value::Number(_), Token::TextWithProperties(_)) => todo!(),
(serde_json::Value::Number(_), Token::Vector(_)) => todo!(),
(serde_json::Value::String(_), Token::Atom(_)) => todo!(),
(serde_json::Value::String(_), Token::List(_)) => todo!(),
(serde_json::Value::String(_), Token::TextWithProperties(_)) => todo!(),
(serde_json::Value::String(_), Token::Vector(_)) => todo!(),
(serde_json::Value::Array(_), Token::Atom(_)) => todo!(),
2023-12-29 21:51:52 +00:00
// (serde_json::Value::Array(_), Token::List(_)) => todo!(),
(serde_json::Value::Array(_), Token::TextWithProperties(_)) => todo!(),
(serde_json::Value::Array(_), Token::Vector(_)) => todo!(),
(serde_json::Value::Object(_), Token::Atom(_)) => todo!(),
(serde_json::Value::Object(_), Token::List(_)) => todo!(),
(serde_json::Value::Object(_), Token::TextWithProperties(_)) => todo!(),
(serde_json::Value::Object(_), Token::Vector(_)) => todo!(),
}
}
2023-12-29 21:06:07 +00:00
fn compare_ast_node<'e, 's, 'w>(
source: &'s str,
2023-12-29 21:06:07 +00:00
emacs: &'e Vec<Token<'s>>,
wasm: &'w serde_json::Map<String, serde_json::Value>,
) -> Result<WasmDiffResult<'s>, Box<dyn std::error::Error>> {
let mut result = WasmDiffResult::default();
let mut emacs_list_iter = emacs.iter();
{
// Compare ast node type.
let emacs_name = emacs_list_iter
.next()
.ok_or("Should have a name as the first child.")?
.as_atom()?;
let wasm_name = wasm
.get("ast-node")
.ok_or("Should have a ast node type.")?
.as_str()
.ok_or("Ast node type should be a string.")?;
result.name = emacs_name.into();
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(),
));
}
}
if result.is_bad() {
return Ok(result);
}
let emacs_attributes_map = emacs_list_iter
.next()
.ok_or("Should have an attributes child.")?
.as_map()?;
let wasm_attributes_map = wasm
.get("properties")
.ok_or(r#"Wasm ast node should have a "properties" attribute."#)?
.as_object()
.ok_or(r#"Wasm ast node "properties" attribute should be an object."#)?;
{
// Compare attribute names.
let emacs_keys: std::collections::BTreeSet<String> = emacs_attributes_map
.keys()
.map(|s| (*s).to_owned())
.collect();
let wasm_keys: std::collections::BTreeSet<String> =
std::iter::once(":standard-properties".to_owned())
.chain(wasm_attributes_map.keys().map(wasm_key_to_emacs_key))
.collect();
let emacs_only_attributes: Vec<&String> = emacs_keys.difference(&wasm_keys).collect();
let wasm_only_attributes: Vec<&String> = wasm_keys.difference(&emacs_keys).collect();
if !emacs_only_attributes.is_empty() {
result.status.push(WasmDiffStatus::Bad(
format!(
"Wasm node lacked field present in elisp node ({name}).",
name = emacs_only_attributes
.iter()
.map(|s| s.as_str())
.intersperse(", ")
.collect::<String>(),
)
.into(),
));
}
if !wasm_only_attributes.is_empty() {
result.status.push(WasmDiffStatus::Bad(
format!(
"Elisp node lacked field present in wasm node ({name}).",
name = wasm_only_attributes
.iter()
.map(|s| s.as_str())
.intersperse(", ")
.collect::<String>(),
)
.into(),
));
}
}
if result.is_bad() {
return Ok(result);
}
{
// Compare attributes.
for attribute_name in wasm_attributes_map.keys() {
let mut layer = WasmDiffResult::default();
layer.name = Cow::Owned(attribute_name.clone());
let wasm_attribute_value = wasm_attributes_map
.get(attribute_name)
.ok_or("Key should exist in both wasm and elisp at this point.")?;
let emacs_key = wasm_key_to_emacs_key(attribute_name);
let emacs_attribute_value = *emacs_attributes_map
.get(emacs_key.as_str())
.ok_or("Key should exist in both wasm and elisp at this point.")?;
layer.extend(compare_json_value(
source,
emacs_attribute_value,
2023-12-29 21:56:02 +00:00
wasm_attribute_value,
)?)?;
result.children.push(layer);
}
}
2023-12-29 20:38:18 +00:00
{
// Compare standard-properties.
let mut layer = WasmDiffResult::default();
layer.name = "standard-properties".into();
let emacs_standard_properties = wasm_get_emacs_standard_properties(&emacs_attributes_map)?;
let wasm_standard_properties = wasm
.get("standard-properties")
.ok_or(r#"Wasm AST nodes should have a "standard-properties" attribute."#)?
.as_object()
.ok_or(r#"Wasm ast node "standard-properties" attribute should be an object."#)?;
for (emacs_value, wasm_name) in [
(emacs_standard_properties.begin, "begin"),
(emacs_standard_properties.end, "end"),
(emacs_standard_properties.contents_begin, "contents-begin"),
(emacs_standard_properties.contents_end, "contents-end"),
(emacs_standard_properties.post_blank, "post-blank"),
2023-12-29 20:38:18 +00:00
] {
match (emacs_value, wasm_standard_properties.get(wasm_name)) {
(None, None) => {}
(None, Some(_)) => {
layer.status.push(WasmDiffStatus::Bad(
format!(
"Elisp node lacked field present in wasm node. Name=({name}).",
name = wasm_name,
)
.into(),
));
}
(Some(_), None) => {
layer.status.push(WasmDiffStatus::Bad(
format!(
"Wasm node lacked field present in elisp node. Name=({name}).",
name = wasm_name,
)
.into(),
));
}
(Some(e), Some(serde_json::Value::Number(w)))
if w.as_u64().map(|w| w as usize) == Some(e) => {}
(Some(e), Some(w)) => {
layer.status.push(WasmDiffStatus::Bad(
format!(
"Property value mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = e,
wasm = w,
)
.into(),
));
}
}
}
result.children.push(layer);
}
2023-12-29 21:06:07 +00:00
{
// Compare children.
let mut layer = WasmDiffResult::default();
2023-12-29 22:26:28 +00:00
layer.name = "children".into();
2023-12-29 21:06:07 +00:00
if let Some(wasm_iter) = wasm
.get("children")
.map(|children| children.as_array())
.flatten()
.map(|children| children.iter())
{
layer.extend(wasm_compare_list(source, emacs_list_iter, wasm_iter)?)?;
} else {
layer.extend(wasm_compare_list(
source,
emacs_list_iter,
std::iter::empty::<&serde_json::Value>(),
)?)?;
}
result.children.push(layer);
}
Ok(result)
}
fn wasm_key_to_emacs_key<WK: std::fmt::Display>(wasm_key: WK) -> String {
format!(":{key}", key = wasm_key)
}
fn compare_quoted_string<'e, 's, 'w>(
source: &'s str,
emacs: &'e str,
wasm: &'w String,
) -> Result<WasmDiffResult<'s>, Box<dyn std::error::Error>> {
let mut result = WasmDiffResult::default();
2023-12-29 22:26:28 +00:00
let emacs_text = unquote(emacs)?;
if wasm.as_str() != emacs_text {
result.status.push(WasmDiffStatus::Bad(
format!(
"Text mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs_text,
wasm = wasm,
)
.into(),
));
}
Ok(result)
}
2023-12-29 20:38:18 +00:00
pub(crate) fn wasm_get_emacs_standard_properties(
attributes_map: &HashMap<&str, &Token<'_>>,
) -> Result<EmacsStandardProperties, Box<dyn std::error::Error>> {
let standard_properties = attributes_map.get(":standard-properties");
Ok(if standard_properties.is_some() {
let mut std_props = standard_properties
.expect("if statement proves its Some")
.as_vector()?
.iter();
let begin = maybe_token_to_usize(std_props.next())?;
let post_affiliated = maybe_token_to_usize(std_props.next())?;
let contents_begin = maybe_token_to_usize(std_props.next())?;
let contents_end = maybe_token_to_usize(std_props.next())?;
let end = maybe_token_to_usize(std_props.next())?;
let post_blank = maybe_token_to_usize(std_props.next())?;
EmacsStandardProperties {
begin,
post_affiliated,
contents_begin,
contents_end,
end,
post_blank,
}
} else {
let begin = maybe_token_to_usize(attributes_map.get(":begin").copied())?;
let end = maybe_token_to_usize(attributes_map.get(":end").copied())?;
let contents_begin = maybe_token_to_usize(attributes_map.get(":contents-begin").copied())?;
let contents_end = maybe_token_to_usize(attributes_map.get(":contents-end").copied())?;
let post_blank = maybe_token_to_usize(attributes_map.get(":post-blank").copied())?;
let post_affiliated =
maybe_token_to_usize(attributes_map.get(":post-affiliated").copied())?;
EmacsStandardProperties {
begin,
post_affiliated,
contents_begin,
contents_end,
end,
post_blank,
}
})
}
2023-12-29 21:06:07 +00:00
fn wasm_compare_list<'e, 's: 'e, 'w, EI, WI>(
source: &'s str,
emacs: EI,
wasm: WI,
) -> Result<WasmDiffResult<'s>, Box<dyn std::error::Error>>
where
EI: Iterator<Item = &'e Token<'s>> + ExactSizeIterator,
WI: Iterator<Item = &'w serde_json::Value> + ExactSizeIterator,
{
let emacs_length = emacs.len();
let wasm_length = wasm.len();
if emacs_length != wasm_length {
return Ok(WasmDiffResult {
status: vec![WasmDiffStatus::Bad(
format!(
"Child length mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs_length,
wasm = wasm_length
)
.into(),
)],
children: Vec::new(),
name: "".into(),
});
}
let mut child_status = Vec::with_capacity(emacs_length);
for (emacs_child, wasm_child) in emacs.zip(wasm) {
2023-12-29 21:56:02 +00:00
child_status.push(compare_json_value(source, emacs_child, wasm_child)?);
2023-12-29 21:06:07 +00:00
}
Ok(WasmDiffResult {
status: Vec::new(),
children: child_status,
name: "".into(),
})
}
2023-12-29 21:51:52 +00:00
fn compare_optional_pair<'e, 's, 'w>(
source: &'s str,
emacs: &'e Vec<Token<'s>>,
wasm: &'w serde_json::Map<String, serde_json::Value>,
) -> Result<WasmDiffResult<'s>, Box<dyn std::error::Error>> {
let mut result = WasmDiffResult::default();
let wasm_optval = wasm
.get("optval")
.ok_or(r#"Wasm optional pair should have an "optval" attribute."#)?;
let wasm_val = wasm
.get("val")
.ok_or(r#"Wasm optional pair should have an "optval" attribute."#)?;
if let serde_json::Value::Null = wasm_optval {
// If the optval is null, then the elisp should have just a single value of a quoted string.
if emacs.len() != 1 {
return Ok(WasmDiffResult {
status: vec![WasmDiffStatus::Bad(
format!(
"Optional pair with null optval should have 1 element. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs,
wasm = wasm
)
.into(),
)],
children: Vec::new(),
name: "".into(),
});
}
let emacs_val = emacs
.first()
.expect("If-statement proves this will be Some.");
2023-12-29 21:56:02 +00:00
result.extend(compare_json_value(source, emacs_val, wasm_val)?)?;
2023-12-29 21:51:52 +00:00
} else {
// If the optval is not null, then the elisp should have 3 values, the optval, a dot, and the val.
if emacs.len() != 3 {
return Ok(WasmDiffResult {
status: vec![WasmDiffStatus::Bad(
format!(
"Optional pair with non-null optval should have 3 elements. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs,
wasm = wasm
)
.into(),
)],
children: Vec::new(),
name: "".into(),
});
}
let emacs_optval = emacs
.first()
.expect("If-statement proves this will be Some.");
let emacs_val = emacs
.get(2)
.expect("If-statement proves this will be Some.");
2023-12-29 21:56:02 +00:00
result.extend(compare_json_value(source, emacs_optval, wasm_optval)?)?;
result.extend(compare_json_value(source, emacs_val, wasm_val)?)?;
2023-12-29 21:51:52 +00:00
}
Ok(result)
}
2023-12-29 22:24:38 +00:00
fn is_plain_text<'e, 's, 'w>(wasm: &'w serde_json::Map<String, serde_json::Value>) -> bool {
if let Some(serde_json::Value::String(node_type)) = wasm.get("ast-node") {
node_type == "plain-text"
} else {
false
}
}
fn compare_plain_text<'e, 's, 'w>(
source: &'s str,
emacs: &'e TextWithProperties<'s>,
wasm: &'w serde_json::Map<String, serde_json::Value>,
) -> Result<WasmDiffResult<'s>, Box<dyn std::error::Error>> {
let mut result = WasmDiffResult::default();
2023-12-29 22:26:28 +00:00
result.name = "plain-text".into();
2023-12-29 22:24:38 +00:00
if !is_plain_text(wasm) {
result.status.push(WasmDiffStatus::Bad(
format!(
"AST node type mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs,
wasm = wasm,
)
.into(),
));
return Ok(result);
}
let emacs_text = unquote(emacs.text)?;
let wasm_standard_properties = wasm
.get("standard-properties")
.ok_or(r#"Wasm AST nodes should have a "standard-properties" attribute."#)?
.as_object()
.ok_or(r#"Wasm ast node "standard-properties" attribute should be an object."#)?;
let wasm_begin = {
if let Some(serde_json::Value::Number(begin)) = wasm_standard_properties.get("begin") {
begin
.as_u64()
.map(|w| w as usize)
.ok_or("Begin should be a number.")?
} else {
result.status.push(WasmDiffStatus::Bad(
format!(
"AST node type mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs,
wasm = wasm,
)
.into(),
));
return Ok(result);
}
};
let wasm_end = {
if let Some(serde_json::Value::Number(end)) = wasm_standard_properties.get("end") {
end.as_u64()
.map(|w| w as usize)
.ok_or("End should be a number.")?
} else {
result.status.push(WasmDiffStatus::Bad(
format!(
"AST node type mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs,
wasm = wasm,
)
.into(),
));
return Ok(result);
}
};
let wasm_text: String = source
.chars()
.skip(wasm_begin - 1)
.take(wasm_end - wasm_begin)
.collect();
if wasm_text != emacs_text {
result.status.push(WasmDiffStatus::Bad(
format!(
"Text mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs_text,
wasm = wasm_text,
)
.into(),
));
return Ok(result);
}
let emacs_start = emacs
.properties
.first()
.map(|t| t.as_atom())
.map_or(Ok(None), |r| r.map(Some))?;
if emacs_start != Some("0") {
result.status.push(WasmDiffStatus::Bad(
format!(
"Text should start at offset 0. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs,
wasm = wasm,
)
.into(),
));
}
let emacs_end = emacs
.properties
.get(1)
.map(|t| t.as_atom())
.map_or(Ok(None), |r| r.map(Some))?;
if emacs_end != Some((wasm_end - wasm_begin).to_string().as_str()) {
result.status.push(WasmDiffStatus::Bad(
format!(
"Text end mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).",
emacs = emacs_end,
wasm = wasm_end - wasm_begin,
)
.into(),
));
}
Ok(result)
}