use grabapl::operation::builder::{BuilderOpLike, OperationBuilder, stack_based_builder};
use grabapl::operation::builtin::LibBuiltinOperation;
use grabapl::operation::query::BuiltinQuery;
use grabapl::operation::user_defined::{AbstractNodeId, UserDefinedOperation};
use grabapl::operation::{BuiltinOperation, run_from_concrete};
use grabapl::prelude::*;
use grabapl::semantics::ConcreteGraph;
use std::collections::{HashMap, HashSet};
mod util;
use util::semantics::*;
#[test]
fn no_modifications_dont_change_abstract_value() {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("a", NodeType::Integer)
.unwrap();
let a = AbstractNodeId::ParameterMarker("a".into());
let state_before = builder.show_state().unwrap();
builder
.add_operation(BuilderOpLike::Builtin(TestOperation::NoOp), vec![a.clone()])
.unwrap();
let state_after = builder.show_state().unwrap();
let a_type_before = state_before.node_av_of_aid(&a).unwrap();
let a_type_after = state_after.node_av_of_aid(&a).unwrap();
assert_eq!(
a_type_before, a_type_after,
"Abstract value of node did not remain unchanged after no-op operation"
);
assert_eq!(
a_type_after,
&NodeType::Integer,
"Abstract value of node should be Integer after no-op operation"
);
}
fn get_abstract_value_changing_operation() -> UserDefinedOperation<TestSemantics> {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::<TestSemantics>::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::ParameterMarker("p0".into());
builder
.start_query(
TestQuery::ValueEqualTo(NodeValue::Integer(0)),
vec![p0.clone()],
)
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::String,
value: NodeValue::String("Changed".to_string()),
}),
vec![p0.clone()],
)
.unwrap();
builder.enter_false_branch().unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::Integer,
value: NodeValue::Integer(42),
}),
vec![p0.clone()],
)
.unwrap();
builder.end_query().unwrap();
builder.build().unwrap()
}
fn get_abstract_value_changing_operation_no_branches() -> UserDefinedOperation<TestSemantics> {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::<TestSemantics>::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::Object,
value: NodeValue::String("Changed".to_string()),
}),
vec![p0],
)
.unwrap();
builder.build().unwrap()
}
#[test]
fn modifications_change_abstract_value_even_if_same_internal_type_for_custom() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
op_ctx.add_custom_operation(0, get_abstract_value_changing_operation());
let mut builder = OperationBuilder::new(&op_ctx, 1);
builder
.expect_parameter_node("a", NodeType::Integer)
.unwrap();
let a = AbstractNodeId::param("a");
let state_before = builder.show_state().unwrap();
builder
.add_operation(BuilderOpLike::FromOperationId(0), vec![a.clone()])
.unwrap();
let state_after = builder.show_state().unwrap();
let a_type_before = state_before.node_av_of_aid(&a).unwrap();
let a_type_after = state_after.node_av_of_aid(&a).unwrap();
assert_ne!(
a_type_before, a_type_after,
"Abstract value of node should change after operation"
);
assert_eq!(
a_type_after,
&NodeType::Object,
"Abstract value of node should be Object after operation"
);
}
#[test]
fn modifications_change_abstract_value_even_if_same_internal_type_for_builtin() {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("a", NodeType::Integer)
.unwrap();
let a = AbstractNodeId::param("a");
let state_before = builder.show_state().unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::Object,
value: NodeValue::String("Changed".to_string()),
}),
vec![a.clone()],
)
.unwrap();
let state_after = builder.show_state().unwrap();
let a_type_before = state_before.node_av_of_aid(&a).unwrap();
let a_type_after = state_after.node_av_of_aid(&a).unwrap();
assert_ne!(
a_type_before, a_type_after,
"Abstract value of node should change after operation"
);
assert_eq!(
a_type_after,
&NodeType::Object,
"Abstract value of node should be Object after operation"
);
}
#[test]
fn modifications_change_abstract_value_even_if_same_internal_type_for_custom_with_builtin() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
op_ctx.add_custom_operation(0, get_abstract_value_changing_operation_no_branches());
let mut builder = OperationBuilder::new(&op_ctx, 1);
builder
.expect_parameter_node("a", NodeType::Integer)
.unwrap();
let a = AbstractNodeId::param("a");
let state_before = builder.show_state().unwrap();
builder
.add_operation(BuilderOpLike::FromOperationId(0), vec![a.clone()])
.unwrap();
let state_after = builder.show_state().unwrap();
let a_type_before = state_before.node_av_of_aid(&a).unwrap();
let a_type_after = state_after.node_av_of_aid(&a).unwrap();
assert_ne!(
a_type_before, a_type_after,
"Abstract value of node should change after operation"
);
assert_eq!(
a_type_after,
&NodeType::Object,
"Abstract value of node should be Object after operation"
);
}
fn get_custom_op_new_node_in_regular_query_branches() -> UserDefinedOperation<TestSemantics> {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::<TestSemantics>::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.start_query(TestQuery::ValueEqualTo(NodeValue::Integer(0)), vec![p0])
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_named_operation(
"new".into(),
BuilderOpLike::Builtin(TestOperation::AddNode {
node_type: NodeType::String,
value: NodeValue::String("x".to_string()),
}),
vec![],
)
.unwrap();
builder.enter_false_branch().unwrap();
builder
.add_named_operation(
"new".into(),
BuilderOpLike::Builtin(TestOperation::AddNode {
node_type: NodeType::Integer,
value: NodeValue::Integer(42),
}),
vec![],
)
.unwrap();
builder.end_query().unwrap();
let output_aid = AbstractNodeId::DynamicOutputMarker("new".into(), "new".into());
builder
.return_node(output_aid, "output".into(), NodeType::Object)
.unwrap();
builder.build().unwrap()
}
fn get_custom_op_new_node_in_shape_query_branches() -> UserDefinedOperation<TestSemantics> {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::<TestSemantics>::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder.start_shape_query("new").unwrap();
builder
.expect_shape_node("new".into(), NodeType::String)
.unwrap();
builder.enter_true_branch().unwrap();
builder.enter_false_branch().unwrap();
builder
.add_named_operation(
"new".into(),
BuilderOpLike::Builtin(TestOperation::AddNode {
node_type: NodeType::Integer,
value: NodeValue::Integer(42),
}),
vec![],
)
.unwrap();
builder.end_query().unwrap();
let output_aid = AbstractNodeId::DynamicOutputMarker("new".into(), "new".into());
let res = builder.return_node(output_aid, "output".into(), NodeType::Object);
assert!(res.is_ok());
builder.build().unwrap()
}
#[test]
fn new_node_from_both_branches_is_visible_for_regular_query() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
op_ctx.add_custom_operation(0, get_custom_op_new_node_in_regular_query_branches());
let mut builder = OperationBuilder::new(&op_ctx, 1);
builder
.expect_parameter_node("p0", NodeType::Integer)
.unwrap();
let p0 = AbstractNodeId::param("p0");
let state_before = builder.show_state().unwrap();
builder
.add_named_operation(
"helper".into(),
BuilderOpLike::FromOperationId(0),
vec![p0.clone()],
)
.unwrap();
let state_after = builder.show_state().unwrap();
let num_before = state_before.graph.nodes().count();
let num_after = state_after.graph.nodes().count();
assert_eq!(
num_after,
num_before + 1,
"Expected a new node to be visible"
);
let returned_node = AbstractNodeId::DynamicOutputMarker("helper".into(), "output".into());
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::CopyValueFromTo),
vec![returned_node, p0.clone()],
)
.unwrap();
let operation = builder.build().unwrap();
op_ctx.add_custom_operation(1, operation);
let mut concrete_graph = ConcreteGraph::<TestSemantics>::new();
let p0_key = concrete_graph.add_node(NodeValue::Integer(0));
run_from_concrete(&mut concrete_graph, &op_ctx, 1, &[p0_key]).unwrap();
let new_node_value = concrete_graph.get_node_attr(p0_key).unwrap();
assert_eq!(
new_node_value,
&NodeValue::String("x".to_string()),
"Expected the new node to have the value 'x'"
);
}
#[test]
fn new_node_from_both_branches_is_invisible_for_shape_query() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
op_ctx.add_custom_operation(0, get_custom_op_new_node_in_shape_query_branches());
let mut builder = OperationBuilder::new(&op_ctx, 1);
let input_marker = SubstMarker::from("input");
builder
.expect_parameter_node(input_marker.clone(), NodeType::Integer)
.unwrap();
let input_aid = AbstractNodeId::ParameterMarker(input_marker.clone());
let state_before = builder.show_state().unwrap();
builder
.add_named_operation(
"helper".into(),
BuilderOpLike::FromOperationId(0),
vec![input_aid],
)
.unwrap();
let state_after = builder.show_state().unwrap();
let num_before = state_before.graph.nodes().count();
let num_after = state_after.graph.nodes().count();
assert_eq!(
num_after,
num_before + 1,
"Expected a new node to be visible"
);
}
#[test]
fn return_node_partially_from_shape_query_fails() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
let helper_op = {
let mut builder = OperationBuilder::<TestSemantics>::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Integer)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder.start_shape_query("child").unwrap();
builder
.expect_shape_node("new".into(), NodeType::String)
.unwrap();
let child_aid = AbstractNodeId::dynamic_output("child", "new");
builder
.expect_shape_edge(p0, child_aid.clone(), EdgeType::Exact("child".to_string()))
.unwrap();
builder.enter_false_branch().unwrap();
builder
.add_named_operation(
"child".into(),
BuilderOpLike::Builtin(TestOperation::AddNode {
node_type: NodeType::String,
value: NodeValue::String("x".to_string()),
}),
vec![],
)
.unwrap();
builder.end_query().unwrap();
let res = builder.return_node(child_aid, "child".into(), NodeType::String);
assert!(res.is_ok());
builder.build().unwrap()
};
op_ctx.add_custom_operation(0, helper_op);
let mut builder = OperationBuilder::new(&op_ctx, 1);
builder
.expect_parameter_node("p0", NodeType::Integer)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder.expect_context_node("c0", NodeType::String).unwrap();
let c0 = AbstractNodeId::param("c0");
builder
.expect_parameter_edge("p0", "c0", EdgeType::Exact("child".to_string()))
.unwrap();
let state_before = builder.show_state().unwrap();
builder
.add_named_operation(
"helper".into(),
BuilderOpLike::FromOperationId(0),
vec![p0.clone()],
)
.unwrap();
let state_after = builder.show_state().unwrap();
let aids_before = state_before
.node_keys_to_aid
.right_values()
.collect::<HashSet<_>>();
let aids_after = state_after
.node_keys_to_aid
.right_values()
.collect::<HashSet<_>>();
assert_eq!(
aids_after.len(),
aids_before.len() + 1,
"Expected a new node to be created in the graph"
);
if true {
let returned_node = AbstractNodeId::DynamicOutputMarker("helper".into(), "child".into());
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::DeleteNode),
vec![returned_node],
)
.unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::CopyValueFromTo),
vec![c0, p0],
)
.unwrap();
let operation = builder.build().unwrap();
op_ctx.add_custom_operation(1, operation);
let mut concrete_graph = ConcreteGraph::<TestSemantics>::new();
let p0_key = concrete_graph.add_node(NodeValue::Integer(0));
let c0_key = concrete_graph.add_node(NodeValue::String("context".to_string()));
concrete_graph.add_edge(p0_key, c0_key, "child".to_string());
run_from_concrete(&mut concrete_graph, &op_ctx, 1, &[p0_key]).unwrap();
}
}
#[test]
fn builder_infers_correct_signatures() {
let param_instructions = |builder: &mut OperationBuilder<TestSemantics>| {
builder
.expect_parameter_node("p0", NodeType::Integer)
.unwrap();
builder
.expect_parameter_node("p1", NodeType::Integer)
.unwrap();
builder
.expect_parameter_node("p2", NodeType::Integer)
.unwrap();
builder.expect_context_node("c0", NodeType::Object).unwrap();
builder.expect_context_node("c1", NodeType::Object).unwrap();
builder
.expect_parameter_edge("p0", "c0", EdgeType::Wildcard)
.unwrap();
builder
.expect_parameter_edge("p2", "c1", EdgeType::Wildcard)
.unwrap();
builder
.expect_parameter_edge("p0", "c1", EdgeType::Wildcard)
.unwrap();
};
let mut op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
param_instructions(&mut builder);
let p0 = AbstractNodeId::ParameterMarker("p0".into());
let p1 = AbstractNodeId::ParameterMarker("p1".into());
let p2 = AbstractNodeId::ParameterMarker("p2".into());
let c0 = AbstractNodeId::ParameterMarker("c0".into());
let c1 = AbstractNodeId::ParameterMarker("c1".into());
let n0 = AbstractNodeId::DynamicOutputMarker("new".into(), "new".into());
let n1 = AbstractNodeId::DynamicOutputMarker("new1".into(), "new".into());
builder
.add_operation(BuilderOpLike::Builtin(TestOperation::DeleteNode), vec![p1])
.unwrap();
builder
.add_operation(BuilderOpLike::Builtin(TestOperation::DeleteNode), vec![c0])
.unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::Integer,
value: NodeValue::Integer(0),
}),
vec![p0],
)
.unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::DeleteEdge),
vec![p2, c1],
)
.unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::String,
value: NodeValue::String("context".to_string()),
}),
vec![c1],
)
.unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetEdgeTo {
node_typ: NodeType::Object,
param_typ: EdgeType::Wildcard,
target_typ: EdgeType::Exact("p0->c1".to_string()),
value: "p0->c1".to_string(),
}),
vec![p0, c1],
)
.unwrap();
builder
.add_named_operation(
"new".into(),
BuilderOpLike::Builtin(TestOperation::AddNode {
node_type: NodeType::String,
value: NodeValue::String("new".to_string()),
}),
vec![],
)
.unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::AddEdge {
node_typ: NodeType::Object,
param_typ: EdgeType::Wildcard,
target_typ: EdgeType::Exact("new_edge".to_string()),
value: "new_edge".to_string(),
}),
vec![p0, c1],
)
.unwrap();
builder
.add_named_operation(
"new1".into(),
BuilderOpLike::Builtin(TestOperation::AddNode {
node_type: NodeType::Integer,
value: NodeValue::Integer(42),
}),
vec![],
)
.unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::AddEdge {
node_typ: NodeType::Object,
param_typ: EdgeType::Wildcard,
target_typ: EdgeType::Exact("new_edge1".to_string()),
value: "new_edge1".to_string(),
}),
vec![p0, n1],
)
.unwrap();
builder
.return_node(n0.clone(), "new".into(), NodeType::String)
.unwrap();
builder
.return_edge(p0, c1, EdgeType::Exact("new_edge".to_string()))
.unwrap();
let res = builder.return_edge(p0, n1, EdgeType::Exact("new_edge1".to_string()));
assert!(
res.is_err(),
"Expected returning edge p0->n1 to fail because n1 is not returned"
);
let operation = builder.build().unwrap();
let signature = operation.signature();
assert_eq!(
signature.parameter.explicit_input_nodes.len(),
3,
"Expected 3 explicit input nodes, p0, p1, p2"
);
assert_eq!(
&signature.output.new_nodes,
&HashMap::from([("new".into(), NodeType::String)]),
"Expected new node 'new' of type String"
);
assert_eq!(
&signature.output.new_edges,
&HashMap::from([(
(
SubstMarker::from("p0").into(),
SubstMarker::from("c1").into()
),
EdgeType::Exact("new_edge".to_string()),
)]),
"Expected new edge from p0 to c1 of type 'new_edge'"
);
macro_rules! assert_deleted_and_changed_nodes_and_edges {
($signature:expr, $expected_maybe_changed_nodes:expr) => {
assert_eq!(
&$signature.output.maybe_deleted_nodes,
&HashSet::from([
SubstMarker::from("p1").into(),
SubstMarker::from("c0").into()
]),
"Expected nodes p1 and c0 to be deleted"
);
assert_eq!(
&$signature.output.maybe_deleted_edges,
&HashSet::from([
(
SubstMarker::from("p2").into(),
SubstMarker::from("c1").into()
),
(
SubstMarker::from("p0").into(),
SubstMarker::from("c0").into()
)
]),
"Expected edges p2->c1 and p0->c0 to be deleted"
);
assert_eq!(
&$signature.output.maybe_changed_nodes, &$expected_maybe_changed_nodes,
"Expected nodes p0 to be changed to Integer and c1 to String"
);
assert_eq!(
&$signature.output.maybe_changed_edges,
&HashMap::from([(
(
SubstMarker::from("p0").into(),
SubstMarker::from("c1").into()
),
EdgeType::Exact("p0->c1".to_string())
)]),
"Expected edge p0->c1 to be changed to 'new_edge'"
);
};
}
assert_deleted_and_changed_nodes_and_edges!(
signature,
HashMap::from([
(SubstMarker::from("p0").into(), NodeType::Integer),
(SubstMarker::from("c1").into(), NodeType::String)
])
);
op_ctx.add_custom_operation(0, operation);
let mut builder = OperationBuilder::new(&op_ctx, 1);
param_instructions(&mut builder);
builder
.add_operation(BuilderOpLike::FromOperationId(0), vec![p0, p1, p2])
.unwrap();
let operation = builder.build().unwrap();
let signature = operation.signature();
assert_deleted_and_changed_nodes_and_edges!(
signature,
HashMap::from([
(SubstMarker::from("p0").into(), NodeType::Integer),
(SubstMarker::from("c1").into(), NodeType::String)
])
);
}
macro_rules! recursion_signature_is_sound {
(before) => {
recursion_signature_is_sound!(true, false, false, NodeType::Integer, NodeType::Integer);
};
(after) => {
recursion_signature_is_sound!(false, true, false, NodeType::Integer, NodeType::Integer);
};
($fst:literal, $snd:literal, $set_last_to_string:literal, $p0_typ:expr, $c0_typ:expr) => {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
builder.expect_context_node("c0", NodeType::Object).unwrap();
builder
.expect_parameter_edge("p0", "c0", EdgeType::Wildcard)
.unwrap();
let p0 = AbstractNodeId::ParameterMarker("p0".into());
let c0 = AbstractNodeId::ParameterMarker("c0".into());
if $fst {
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::Integer,
value: NodeValue::Integer(0),
}),
vec![p0],
)
.unwrap();
}
builder.start_shape_query("q").unwrap();
builder
.expect_shape_node("child".into(), NodeType::Object)
.unwrap();
let child_aid = AbstractNodeId::dynamic_output("q", "child");
builder
.expect_shape_edge(c0.clone(), child_aid.clone(), EdgeType::Wildcard)
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_named_operation(
"recurse".into(),
BuilderOpLike::Recurse,
vec![c0], )
.unwrap();
if $set_last_to_string {
builder.enter_false_branch().unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::String,
value: NodeValue::String("Last".to_string()),
}),
vec![c0.clone()],
)
.unwrap();
}
builder.end_query().unwrap();
if $snd {
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::Integer,
value: NodeValue::Integer(0),
}),
vec![p0],
)
.unwrap();
}
let operation = builder.build().unwrap();
let signature = operation.signature();
assert_eq!(
signature.output.maybe_deleted_nodes,
HashSet::new(),
"Expected no nodes to be deleted"
);
assert_eq!(
signature.output.maybe_deleted_edges,
HashSet::new(),
"Expected no edges to be deleted"
);
assert_eq!(
signature.output.maybe_changed_nodes,
HashMap::from([
(SubstMarker::from("p0").into(), $p0_typ),
(SubstMarker::from("c0").into(), $c0_typ), ]),
"Expected both p0 and c0 to change"
);
assert_eq!(
signature.output.maybe_changed_edges,
HashMap::new(),
"Expected no edges to be changed"
);
assert_eq!(
signature.output.new_nodes,
HashMap::new(),
"Expected no new nodes to be created"
);
assert_eq!(
signature.output.new_edges,
HashMap::new(),
"Expected no new edges to be created"
);
};
}
#[test_log::test]
fn recursion_signature_is_sound_when_changed_before() {
recursion_signature_is_sound!(before);
}
#[test_log::test]
fn recursion_signature_is_sound_when_changed_after() {
recursion_signature_is_sound!(after);
}
#[test_log::test]
fn recursion_signature_is_sound_when_changed_before_and_last_node_set_to_string() {
recursion_signature_is_sound!(true, false, true, NodeType::Integer, NodeType::Object);
}
#[test_log::test]
fn recursion_signature_is_sound_when_changed_after_and_last_node_set_to_string() {
recursion_signature_is_sound!(false, true, true, NodeType::Integer, NodeType::Object);
}
#[test_log::test]
fn recursion_breaks_when_modification_changes_after_use() {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Integer)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.add_operation(BuilderOpLike::Recurse, vec![p0])
.unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Integer, target_typ: NodeType::Integer, value: NodeValue::Integer(42),
}),
vec![p0],
)
.unwrap();
let res = builder.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::String,
value: NodeValue::String("Changed".to_string()),
}),
vec![p0],
);
assert!(
res.is_err(),
"Expected changing the abstract value of p0 to fail, since it is used in a recursive call after which it is used as an integer"
);
}
#[test_log::test]
fn shape_query_doesnt_match_nodes_for_which_handles_exist() {
fn get_shape_query_modifying_operation(
op_id: OperationId,
) -> UserDefinedOperation<TestSemantics> {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, op_id);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder.start_shape_query("q").unwrap();
builder
.expect_shape_node("child".into(), NodeType::Object)
.unwrap();
let child_aid = AbstractNodeId::dynamic_output("q", "child");
builder
.expect_shape_edge(p0.clone(), child_aid.clone(), EdgeType::Wildcard)
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::String,
value: NodeValue::String("I'm a string".to_string()),
}),
vec![child_aid],
)
.unwrap();
builder.enter_false_branch().unwrap();
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Object,
target_typ: NodeType::String,
value: NodeValue::String("no child".to_string()),
}),
vec![p0],
)
.unwrap();
builder.build().unwrap()
}
let mut op_ctx = OperationContext::<TestSemantics>::new();
op_ctx.add_custom_operation(0, get_shape_query_modifying_operation(0));
let mut builder = OperationBuilder::new(&op_ctx, 1);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
builder
.expect_context_node("c0", NodeType::Integer)
.unwrap();
let p0 = AbstractNodeId::param("p0");
let c0 = AbstractNodeId::param("c0");
builder
.expect_parameter_edge("p0", "c0", EdgeType::Wildcard)
.unwrap();
builder
.add_operation(BuilderOpLike::FromOperationId(0), vec![p0])
.unwrap();
let state = builder.show_state().unwrap();
let c0_key = state.node_keys_to_aid.get_right(&c0).unwrap();
assert_eq!(
state.graph.get_node_attr(*c0_key).unwrap(),
&NodeType::Integer,
"Expected c0 to remain unchanged, since the operation does not know about the inner operation's shape query"
);
let op = builder.build().unwrap();
op_ctx.add_custom_operation(1, op);
{
let mut g_no_child = TestSemantics::new_concrete_graph();
let p0_key = g_no_child.add_node(NodeValue::Integer(42));
run_from_concrete(&mut g_no_child, &op_ctx, 0, &[p0_key]).unwrap();
let p0_value = g_no_child.get_node_attr(p0_key).unwrap();
assert_eq!(
p0_value,
&NodeValue::String("no child".to_string()),
"Expected p0 to be set to 'no child' when no child exists"
);
}
{
let mut g_with_child = TestSemantics::new_concrete_graph();
let p0_key = g_with_child.add_node(NodeValue::Integer(42));
let c0_key = g_with_child.add_node(NodeValue::Integer(43));
g_with_child.add_edge(p0_key, c0_key, "child".to_string());
run_from_concrete(&mut g_with_child, &op_ctx, 0, &[p0_key]).unwrap();
let p0_value = g_with_child.get_node_attr(p0_key).unwrap();
let c0_value = g_with_child.get_node_attr(c0_key).unwrap();
assert_eq!(
p0_value,
&NodeValue::Integer(42),
"Expected p0 to remain unchanged when a child exists"
);
assert_eq!(
c0_value,
&NodeValue::String("I'm a string".to_string()),
"Expected child to be set to 'I'm a string' when it exists"
);
}
{
let mut g = TestSemantics::new_concrete_graph();
let p0_key = g.add_node(NodeValue::Integer(42));
let c0_key = g.add_node(NodeValue::Integer(43));
g.add_edge(p0_key, c0_key, "child".to_string());
run_from_concrete(&mut g, &op_ctx, 1, &[p0_key]).unwrap();
let p0_value = g.get_node_attr(p0_key).unwrap();
let c0_value = g.get_node_attr(c0_key).unwrap();
assert_eq!(
p0_value,
&NodeValue::String("no child".to_string()),
"Expected p0 to be set to 'no child' when the shape query does not match the child node, even if one exists"
);
assert_eq!(
c0_value,
&NodeValue::Integer(43),
"Expected child to remain unchanged since it is not matched, even though it exists",
);
}
}
#[test_log::test]
fn may_writes_remember_previous_abstract_value() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
let op = {
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.start_query(TestQuery::ValueEqualTo(NodeValue::Integer(3)), vec![p0])
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::SetNode {
param: NodeType::Object,
value: NodeValue::String("Changed".to_string()),
}),
vec![p0.clone()],
)
.unwrap();
builder.end_query().unwrap();
let state = builder.show_state().unwrap();
let typ = state.node_av_of_aid(&p0).unwrap();
assert_eq!(
typ,
&NodeType::Object,
"Expected p0 to remain Object after conditional change to String"
);
builder.build().unwrap()
};
op_ctx.add_custom_operation(0, op);
let mut builder = OperationBuilder::new(&op_ctx, 1);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::SetNode {
param: NodeType::Object,
value: NodeValue::Integer(42),
}),
vec![p0],
)
.unwrap();
let state = builder.show_state().unwrap();
let typ = state.node_av_of_aid(&p0).unwrap();
assert_eq!(
typ,
&NodeType::Integer,
"Expected p0 to be Integer after unconditional, builtin SetNode"
);
builder
.add_operation(BuilderOpLike::FromOperationId(0), vec![p0])
.unwrap();
let state = builder.show_state().unwrap();
let typ = state.node_av_of_aid(&p0).unwrap();
assert_eq!(
typ,
&NodeType::Object,
"Expected p0 to be Object after conditional, user-defined SetNode"
);
let op = builder.build().unwrap();
op_ctx.add_custom_operation(1, op);
if true {
let mut g = TestSemantics::new_concrete_graph();
let p0_key = g.add_node(NodeValue::Integer(0));
run_from_concrete(&mut g, &op_ctx, 1, &[p0_key]).unwrap();
let p0_value = g.get_node_attr(p0_key).unwrap();
assert_eq!(
p0_value,
&NodeValue::Integer(42),
"Expected p0 to remain Integer after running the operation, since the inner operation's set to String was not hit"
);
}
let mut builder = OperationBuilder::new(&op_ctx, 2);
builder
.expect_parameter_node("p0", NodeType::String)
.unwrap();
builder
.add_operation(BuilderOpLike::FromOperationId(0), vec![p0])
.unwrap();
let state = builder.show_state().unwrap();
let typ = state.node_av_of_aid(&p0).unwrap();
assert_eq!(
typ,
&NodeType::String,
"Expected p0 to remain String after running the operation, since the inner operation let us now it may at most write a string."
);
}
#[test_log::test]
fn shape_query_cannot_match_existing_nodes() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
builder
.expect_parameter_node("p1", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
let p1 = AbstractNodeId::param("p1");
builder
.expect_parameter_edge("p0", "p1", EdgeType::Exact("child".into()))
.unwrap();
builder.start_shape_query("q").unwrap();
builder
.expect_shape_node("child".into(), NodeType::Object)
.unwrap();
let child_aid = AbstractNodeId::dynamic_output("q", "child");
builder
.expect_shape_edge(p0, child_aid, EdgeType::Exact("child".into()))
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::RemoveNode {
param: NodeType::Object,
}),
vec![child_aid],
)
.unwrap();
builder.end_query().unwrap();
let state = builder.show_state().unwrap();
let p1_av = state.node_av_of_aid(&p1);
assert_eq!(
p1_av,
Some(&NodeType::Object),
"Expected p1 to remain Object after running the shape query, since it was not matched"
);
let op = builder.build().unwrap();
op_ctx.add_custom_operation(0, op);
let mut g = TestSemantics::new_concrete_graph();
let p0_key = g.add_node(NodeValue::Integer(0));
let p1_key = g.add_node(NodeValue::Integer(1));
g.add_edge(p0_key, p1_key, "child".into());
run_from_concrete(&mut g, &op_ctx, 0, &[p0_key, p1_key]).unwrap();
assert_eq!(
g.get_node_attr(p1_key),
Some(&NodeValue::Integer(1)),
"Expected p1 to remain a node in the graph",
);
}
#[test_log::test]
fn rename_nodes_and_merge_test() {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
let res = builder.rename_node(p0, "test");
assert!(
res.is_err(),
"Expected to not be able to rename a parameter marker"
);
builder
.start_query(TestQuery::ValueEqualTo(NodeValue::Integer(0)), vec![p0])
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_named_operation(
"op1".into(),
BuilderOpLike::LibBuiltin(LibBuiltinOperation::AddNode {
value: NodeValue::String("hello".into()),
}),
vec![],
)
.unwrap();
let true_branch_aid = AbstractNodeId::dynamic_output("op1", "new");
builder.rename_node(true_branch_aid, "a").unwrap();
builder.enter_false_branch().unwrap();
builder
.add_named_operation(
"op2".into(),
BuilderOpLike::LibBuiltin(LibBuiltinOperation::AddNode {
value: NodeValue::Integer(0),
}),
vec![],
)
.unwrap();
let false_branch_aid = AbstractNodeId::dynamic_output("op2", "new");
builder.rename_node(false_branch_aid, "a").unwrap();
builder.end_query().unwrap();
let a_aid = AbstractNodeId::named("a");
let state = builder.show_state().unwrap();
let a_av = state.node_av_of_aid(&a_aid);
assert_eq!(
a_av,
Some(&NodeType::Object),
"Expected node 'a' to be a merge of the two nodes with the same name, but different operation markers"
);
builder.start_shape_query("query").unwrap();
builder
.expect_shape_node("wow".into(), NodeType::Integer)
.unwrap();
let wow_aid = AbstractNodeId::dynamic_output("query", "wow");
builder
.expect_shape_edge(p0, wow_aid, EdgeType::Wildcard)
.unwrap();
builder.enter_true_branch().unwrap();
builder.rename_node(wow_aid, "b").unwrap();
builder.enter_false_branch().unwrap();
builder
.add_named_operation(
"wow".into(),
BuilderOpLike::LibBuiltin(LibBuiltinOperation::AddNode {
value: NodeValue::String("Something".into()),
}),
vec![],
)
.unwrap();
let false_branch_aid = AbstractNodeId::dynamic_output("wow", "new");
builder.rename_node(false_branch_aid, "b").unwrap();
builder.end_query().unwrap();
let b_aid = AbstractNodeId::named("b");
let state = builder.show_state().unwrap();
let b_av = state.node_av_of_aid(&b_aid);
assert_eq!(
b_av,
Some(&NodeType::Object),
"Expected node 'b' to be a merge of the two nodes with the same operation marker, but different output markers"
);
let res = builder.return_node(b_aid.clone(), "b".into(), NodeType::Object);
assert!(res.is_ok());
}
#[test_log::test]
fn shape_query_allows_refinement_of_existing_nodes_and_edges() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
builder
.expect_parameter_node("p1", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
let p1 = AbstractNodeId::param("p1");
builder
.expect_parameter_edge("p0", "p1", EdgeType::Wildcard)
.unwrap();
builder.start_shape_query("q").unwrap();
builder
.expect_shape_edge(p0, p1, EdgeType::Exact("child".to_string()))
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::SetNode {
param: NodeType::Object,
value: NodeValue::String("has child".to_string()),
}),
vec![p0.clone()],
)
.unwrap();
builder.end_query().unwrap();
builder.start_shape_query("q1").unwrap();
builder
.expect_shape_node_change(p1, NodeType::Integer)
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::SetNode {
param: NodeType::Object,
value: NodeValue::String("was integer".to_string()),
}),
vec![p1.clone()],
)
.unwrap();
let operation = builder.build().unwrap();
op_ctx.add_custom_operation(0, operation);
{
let mut g = TestSemantics::new_concrete_graph();
let p0_key = g.add_node(NodeValue::Integer(42));
let p1_key = g.add_node(NodeValue::Integer(43));
g.add_edge(p0_key, p1_key, "child".to_string());
run_from_concrete(&mut g, &op_ctx, 0, &[p0_key, p1_key]).unwrap();
let p0_value = g.get_node_attr(p0_key);
assert_eq!(p0_value, Some(&NodeValue::String("has child".to_string())));
let p1_value = g.get_node_attr(p1_key);
assert_eq!(
p1_value,
Some(&NodeValue::String("was integer".to_string()))
);
}
}
#[test_log::test]
fn shape_query_av_refinement_works_in_branch_merge() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder.start_shape_query("q").unwrap();
builder
.expect_shape_node_change(p0, NodeType::Integer)
.unwrap();
builder.enter_false_branch().unwrap();
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::SetNode {
param: NodeType::Object,
value: NodeValue::Integer(42),
}),
vec![p0.clone()],
)
.unwrap();
builder.end_query().unwrap();
let state = builder.show_state().unwrap();
let p0_av = state.node_av_of_aid(&p0);
assert_eq!(
p0_av,
Some(&NodeType::Integer),
"Expected p0 to be Integer after the shape query refinement"
);
let op = builder.build().unwrap();
op_ctx.add_custom_operation(0, op);
let mut g = TestSemantics::new_concrete_graph();
let p0_key = g.add_node(NodeValue::String("not an integer".to_string()));
run_from_concrete(&mut g, &op_ctx, 0, &[p0_key]).unwrap();
let p0_value = g.get_node_attr(p0_key);
assert_eq!(
p0_value,
Some(&NodeValue::Integer(42)),
"Expected p0 to be set to Integer after the shape query did not match"
);
let p1_key = g.add_node(NodeValue::Integer(100));
run_from_concrete(&mut g, &op_ctx, 0, &[p1_key]).unwrap();
let p1_value = g.get_node_attr(p1_key);
assert_eq!(
p1_value,
Some(&NodeValue::Integer(100)),
"Expected p1 to not change"
);
}
#[test_log::test]
fn delete_node_deletes_all_incident_edges_in_signature() {
let mut op_ctx = OperationContext::<TestSemantics>::new();
let op_deleting_one_node = {
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::RemoveNode {
param: NodeType::Object,
}),
vec![p0],
)
.unwrap();
let op = builder.build().unwrap();
let signature = op.signature();
assert_eq!(
signature.output.maybe_deleted_nodes,
HashSet::from([SubstMarker::from("p0").into()]),
"Expected the operation to delete p0"
);
op
};
op_ctx.add_custom_operation(0, op_deleting_one_node);
let mut builder = OperationBuilder::new(&op_ctx, 1);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
builder
.expect_parameter_node("p1", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
let p1 = AbstractNodeId::param("p1");
builder
.expect_parameter_edge("p0", "p1", EdgeType::Wildcard)
.unwrap();
builder
.add_operation(BuilderOpLike::FromOperationId(0), vec![p1])
.unwrap();
let op = builder.build().unwrap();
let signature = op.signature();
assert_eq!(
signature.output.maybe_deleted_nodes,
HashSet::from([SubstMarker::from("p1")]),
"Expected the operation to delete p1"
);
assert_eq!(
signature.output.maybe_deleted_edges,
HashSet::from([("p0".into(), "p1".into())]),
"Expected the operation to delete the edge p0->p1"
);
}
#[test_log::test]
fn delete_node_after_writing_to_it() {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Object)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::SetNode {
param: NodeType::Object,
value: NodeValue::String("Hello".to_string()),
}),
vec![p0.clone()],
)
.unwrap();
builder
.add_operation(
BuilderOpLike::LibBuiltin(LibBuiltinOperation::RemoveNode {
param: NodeType::Object,
}),
vec![p0],
)
.unwrap();
let op = builder.build().unwrap();
let signature = op.signature();
assert_eq!(
signature.output.maybe_deleted_nodes,
HashSet::from([SubstMarker::from("p0").into()]),
"Expected the operation to delete p0"
);
assert_eq!(
signature.output.maybe_changed_nodes,
HashMap::from([(SubstMarker::from("p0").into(), NodeType::String)]),
"Expected the operation to change p0 to Object before deleting it"
);
}
#[test_log::test]
fn new_builder_test() {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = stack_based_builder::Builder::new(&op_ctx, 0);
use grabapl::operation::builder::BuilderInstruction as BI;
builder
.consume(BI::ExpectParameterNode("p0".into(), NodeType::Object))
.unwrap();
builder
.consume(BI::AddNamedOperation(
"hello".into(),
BuilderOpLike::LibBuiltin(LibBuiltinOperation::AddNode {
value: NodeValue::String("Hello".to_string()),
}),
vec![],
))
.unwrap();
let show_data = builder.show();
println!("{:?}", show_data);
builder
.consume(BI::RenameNode(
AbstractNodeId::dynamic_output("hello", "new"),
"renamed".into(),
))
.unwrap();
let show_data = builder.show();
println!("{:?}", show_data);
let res = builder.consume(BI::ExpectParameterNode("p0".into(), NodeType::Object));
println!("{:?}", res);
}
#[test_log::test]
fn recursion_return_node() {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Integer)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.start_query(TestQuery::ValueEqualTo(NodeValue::Integer(200)), vec![p0])
.unwrap();
builder.enter_true_branch().unwrap();
builder
.add_named_operation(
"op0".into(),
BuilderOpLike::LibBuiltin(LibBuiltinOperation::AddNode {
value: NodeValue::Integer(100),
}),
vec![],
)
.unwrap();
let new_node_aid = AbstractNodeId::dynamic_output("op0", "new");
let ret_node_aid = AbstractNodeId::named("ret_node");
builder.rename_node(new_node_aid, "ret_node").unwrap();
builder.enter_false_branch().unwrap();
builder
.add_named_operation("recursion".into(), BuilderOpLike::Recurse, vec![p0])
.unwrap();
let new_node_aid = AbstractNodeId::dynamic_output("recursion", "ret_node");
builder
.expect_self_return_node("ret_node", NodeType::Integer)
.unwrap();
builder.rename_node(new_node_aid, "ret_node").unwrap();
builder.end_query().unwrap();
builder
.return_node(ret_node_aid, "ret_node".into(), NodeType::Integer)
.unwrap();
let _ = builder.build().unwrap();
}
#[test_log::test]
fn recursion_expect_self_return_node_corner_cases() {
let op_ctx = OperationContext::<TestSemantics>::new();
let mut builder = OperationBuilder::new(&op_ctx, 0);
builder
.expect_parameter_node("p0", NodeType::Integer)
.unwrap();
let p0 = AbstractNodeId::param("p0");
builder
.add_named_operation("self".into(), BuilderOpLike::Recurse, vec![p0])
.unwrap();
builder
.expect_self_return_node("ret_node", NodeType::Integer)
.unwrap();
let ret_node_aid = AbstractNodeId::dynamic_output("self", "ret_node");
builder
.add_operation(
BuilderOpLike::Builtin(TestOperation::SetTo {
op_typ: NodeType::Integer, target_typ: NodeType::Integer, value: NodeValue::Integer(42),
}),
vec![ret_node_aid],
)
.unwrap();
let res = builder.expect_self_return_node("ret_node", NodeType::String);
assert!(
res.is_err(),
"Expected expecting the return node to be String to fail, since it needs to be Integer in a prior instruction"
);
let res = builder.build();
assert!(
res.is_err(),
"Expected building the operation to fail, since we expect a return node that is not created"
);
builder
.return_node(ret_node_aid, "ret_node".into(), NodeType::Integer)
.unwrap();
let _ = builder.build().unwrap();
}