1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
use crate::SubstMarker;
use crate::operation::signature::parameter::{
AbstractOperationOutput, AbstractOutputNodeMarker, GraphWithSubstitution, NodeMarker,
OperationParameter,
};
use crate::semantics::{AbstractGraph, AbstractJoin, AbstractMatcher, Semantics};
use crate::util::bimap::BiMap;
use derive_more::From;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
pub mod parameter;
pub mod parameterbuilder;
pub type AbstractSignatureEdgeId = (AbstractSignatureNodeId, AbstractSignatureNodeId);
pub type ParameterEdgeId = (SubstMarker, SubstMarker);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, From)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum AbstractSignatureNodeId {
/// References a pre-existing node in the parameter graph.
ExistingNode(SubstMarker),
/// References a new node that will be created by the operation.
NewNode(AbstractOutputNodeMarker),
}
/// The entirety of an operation's abstract effect.
///
/// This includes requirements on the caller, i.e., parameter shape and types, as well as
/// post-conditions in the form of a to-be-propagated new graph shape and types.
///
/// Post-conditions are necessary due to the mutable pass-by-reference semantics of this library.
///
/// A signature is a "must-occur" description of an operation, i.e., it describes the
/// effects that *will occur for every invocation* of the operation.
///
/// Operations with the same signature should be soundly interchangeable, with only the concrete
/// implementation differing.
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(bound = "S: crate::serde::SemanticsSerde")
)]
pub struct OperationSignature<S: Semantics> {
/// The operation's name.
// TODO: decide if this should be unique and used in place of OperationId.
// If so, interning it would probably be sensible.
pub name: String,
/// The operation's parameter, i.e., the expected input nodes, edges, and their types.
pub parameter: OperationParameter<S>,
/// The operation's output, i.e., the deleted nodes and edges, potential new nodes and edges,
/// and changes to existing nodes and edges.
pub output: AbstractOutputChanges<S>,
}
impl<S: Semantics> PartialEq for OperationSignature<S> {
fn eq(&self, other: &Self) -> bool {
(self.name == other.name || true) // todo: define if equality is a requirement for names
&& self.parameter == other.parameter
&& self.output.is_subtype_of(&other.output)
}
}
impl<S: Semantics> Clone for OperationSignature<S> {
fn clone(&self) -> Self {
OperationSignature {
name: self.name.clone(),
parameter: self.parameter.clone(),
output: self.output.clone(),
}
}
}
impl<S: Semantics> OperationSignature<S> {
// mostly an unsound hack to get things running for the time being
// TODO: fix/remove
pub fn empty_new(name: impl Into<String>, parameter: OperationParameter<S>) -> Self {
OperationSignature {
name: name.into(),
parameter,
output: AbstractOutputChanges::new(),
}
}
pub fn new_noop(name: impl Into<String>) -> Self {
OperationSignature {
name: name.into(),
parameter: OperationParameter::new_empty(),
output: AbstractOutputChanges::new(),
}
}
}
/// The changes to the graph that an operation will cause.
#[derive(derive_more::Debug)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(bound = "S: crate::serde::SemanticsSerde")
)]
pub struct AbstractOutputChanges<S: Semantics> {
/// New nodes that are guaranteed to be created with a value of the given type.
pub new_nodes: HashMap<AbstractOutputNodeMarker, S::NodeAbstract>,
/// New edges that are guaranteed to be created with a value of the given type.
#[serde(with = "serde_json_any_key::any_key_map")]
pub new_edges: HashMap<AbstractSignatureEdgeId, S::EdgeAbstract>,
/// Pre-existing nodes that may have been modified to be of the given type.
pub maybe_changed_nodes: HashMap<SubstMarker, S::NodeAbstract>,
/// Pre-existing edges that may have been modified to be of the given type.
#[serde(with = "serde_json_any_key::any_key_map")]
pub maybe_changed_edges: HashMap<ParameterEdgeId, S::EdgeAbstract>,
// TODO: think about also having "must_changed_nodes" and "must_changed_edges" here,
// which would be useful for:
// 1. More precise states, since the builder would not have to join the must-written value with the old value - it knows that the old value is not needed!
// 2. More coherence, since builtin operation currently act this way when executed directly inside an operation.
// i.e., they do not join the old value with the new value, but rather overwrite it, since they know for a fact what happens.
// And they might be easy to compute:
// 1. At the end of an operation, if we see that an AID x for which we have maybe written an AV t is statically
// known to be of AV t, then we can pretend that it was must-changed.
// This is useful in eg such a case:
// foo(x: Object) must_change x: Integer { if ... { x = 2 } else { x = 3 } // x: Integer now! }
// x: Object. Call foo(x). Now we know x: Integer. Without must_changed, x: Object still.
/// Pre-existing nodes that may have been deleted by the operation.
pub maybe_deleted_nodes: HashSet<SubstMarker>,
/// Pre-existing edges that may have been deleted by the operation.
pub maybe_deleted_edges: HashSet<ParameterEdgeId>,
}
impl<S: Semantics> Clone for AbstractOutputChanges<S> {
fn clone(&self) -> Self {
AbstractOutputChanges {
new_nodes: self.new_nodes.clone(),
new_edges: self.new_edges.clone(),
maybe_changed_nodes: self.maybe_changed_nodes.clone(),
maybe_changed_edges: self.maybe_changed_edges.clone(),
maybe_deleted_nodes: self.maybe_deleted_nodes.clone(),
maybe_deleted_edges: self.maybe_deleted_edges.clone(),
}
}
}
impl<S: Semantics> AbstractOutputChanges<S> {
pub fn new() -> Self {
AbstractOutputChanges {
new_nodes: HashMap::new(),
new_edges: HashMap::new(),
maybe_changed_nodes: HashMap::new(),
maybe_changed_edges: HashMap::new(),
maybe_deleted_nodes: HashSet::new(),
maybe_deleted_edges: HashSet::new(),
}
}
/// Returns `true` if `self` can be used wherever `other` is expected, i.e., `self <: other`.
pub fn is_subtype_of(&self, other: &Self) -> bool {
// All new nodes and edges from other must be present in self, with a subtype of their
// counterpart in other.
for (marker, other_type) in &other.new_nodes {
if let Some(self_type) = self.new_nodes.get(marker) {
if !S::NodeMatcher::matches(self_type, other_type) {
// New node is not a subtype.
// Any caller working with the assumption of `other` would assume an incorrect type.
return false;
}
} else {
// Missing new node
// Any caller working with the assumption of `other` would incorrectly assume that this node exists and crash.
return false;
}
}
for (edge_id, other_type) in &other.new_edges {
if let Some(self_type) = self.new_edges.get(edge_id) {
if !S::EdgeMatcher::matches(self_type, other_type) {
// New edge is not a subtype.
// Any caller working with the assumption of `other` would assume an incorrect type.
return false;
}
} else {
// Missing new edge.
// Any caller working with the assumption of `other` would incorrectly assume that this edge exists and crash.
return false;
}
}
// All changed nodes and edges from `self` must be present in `other`, with a supertype of their
// counterpart in `self`.
for (marker, self_type) in &self.maybe_changed_nodes {
if let Some(other_type) = other.maybe_changed_nodes.get(marker) {
if !S::NodeMatcher::matches(self_type, other_type) {
// Changed node is not a subtype of the one in `other`.
// Any caller working with the assumption of `other` would assume an incorrect type.
return false;
}
} else {
// Missing changed node.
// `self` would change values unbeknownst to any caller working with the assumption of `other`.
return false;
}
}
for (edge_id, self_type) in &self.maybe_changed_edges {
if let Some(other_type) = other.maybe_changed_edges.get(edge_id) {
if !S::EdgeMatcher::matches(self_type, other_type) {
// Changed edge is not a subtype of the one in `other`.
// Any caller working with the assumption of `other` would assume an incorrect type.
return false;
}
} else {
// Missing changed edge.
// `self` would change values unbeknownst to any caller working with the assumption of `other`.
return false;
}
}
// All deleted nodes and edges from `self` must be present in `other`.
for marker in &self.maybe_deleted_nodes {
if !other.maybe_deleted_nodes.contains(marker) {
// `self` would delete a node that `other` expects to be present.
// Any caller working with the assumption of `other` would assume this node exists and crash.
return false;
}
}
for edge_id in &self.maybe_deleted_edges {
if !other.maybe_deleted_edges.contains(edge_id) {
// `self` would delete an edge that `other` expects to be present.
// Any caller working with the assumption of `other` would assume this edge exists and crash.
return false;
}
}
true
}
pub fn apply_abstract(
&self,
g: &mut GraphWithSubstitution<AbstractGraph<S>>,
) -> AbstractOperationOutput<S> {
let mut output_names = BiMap::new();
// handle new nodes
for (name, av) in &self.new_nodes {
let nnm = g.new_node_marker();
g.add_node(nnm.clone(), av.clone());
output_names.insert(nnm, name.clone());
}
let sig_id_to_node_marker = |sig_id: AbstractSignatureNodeId| {
match sig_id {
AbstractSignatureNodeId::ExistingNode(subst) => NodeMarker::Subst(subst),
AbstractSignatureNodeId::NewNode(name) => {
// find in output_names
let nnm = output_names
.get_right(&name)
.expect("internal error: signature node not found in output names");
NodeMarker::New(*nnm)
}
}
};
// handle new edges
for ((src, dst), av) in &self.new_edges {
let src_marker = sig_id_to_node_marker(*src);
let dst_marker = sig_id_to_node_marker(*dst);
g.add_edge(src_marker, dst_marker, av.clone());
}
// Important for the maybe-changed values:
// Since they're only maybe-changed, it could be that they're not changed.
// In other words, we need to indicate that the join of the old value and the new value is the most precise abstract value to give.
// handle changed nodes
for (subst, av) in &self.maybe_changed_nodes {
let node_marker = NodeMarker::Subst(*subst);
g.maybe_set_node_value(node_marker, av.clone(), S::NodeJoin::join)
.unwrap();
}
// handle changed edges
for ((src, dst), av) in &self.maybe_changed_edges {
let src_marker = NodeMarker::Subst(*src);
let dst_marker = NodeMarker::Subst(*dst);
g.maybe_set_edge_value(src_marker, dst_marker, av.clone(), S::EdgeJoin::join)
.unwrap();
}
// handle removed nodes
for subst in &self.maybe_deleted_nodes {
let node_marker = NodeMarker::Subst(*subst);
g.delete_node(node_marker);
}
// handle removed edges
for (src, dst) in &self.maybe_deleted_edges {
let src_marker = NodeMarker::Subst(*src);
let dst_marker = NodeMarker::Subst(*dst);
g.delete_edge(src_marker, dst_marker);
}
let (output_names, _) = output_names.into_inner();
g.get_abstract_output(output_names)
}
}
impl<S: Semantics> OperationParameter<S> {
/// Returns `true` if `self` can be used *as a parameter* wherever `other` is expected, i.e., `self <: other`.
/// Note that this is a parameter, so it is contravariant when looked at as part of a function type.
pub fn is_subtype_of(&self, other: &OperationParameter<S>) -> bool {
// Situation: We expect to be calling an operation with a parameter of `other`.
// Can we call it with `self`?
// All explicit input nodes in `other` must be present in `self`, with a *supertype* of their
// counterpart in `other`. Note that this is contravariant - otherwise we could be calling
// an operation (self) with a parameter that is less specific than it expects.
if self.explicit_input_nodes.len() != other.explicit_input_nodes.len() {
return false;
}
for (self_subst, other_subst) in self
.explicit_input_nodes
.iter()
.zip(other.explicit_input_nodes.iter())
{
let self_key = self
.node_keys_to_subst
.get_right(self_subst)
.expect("internal error: missing subst marker in self");
let other_key = other
.node_keys_to_subst
.get_right(other_subst)
.expect("internal error: missing subst marker in other");
let self_type = self
.parameter_graph
.get_node_attr(*self_key)
.expect("internal error: missing node attribute in self");
let other_type = other
.parameter_graph
.get_node_attr(*other_key)
.expect("internal error: missing node attribute in other");
if !S::NodeMatcher::matches(other_type, self_type) {
// NB: must have other <: self, not the opposite way!
// Self's type is not a supertype of other's type.
// Any caller working with the assumption of `other` could pass `self` a value that is not compatible with what it expects.
return false;
}
}
// Self's context graph must be a subgraph of other's context graph, with node types of self being supertypes of those in other.
// Same for edges.
// There is a subtle detail here though, which is that we currently statically store the context graph mapping at every call site.
// This means somewhere we must also adapt that mapping to self, if we're exchanging the operation.
// The "straightforward" way to make this automatic would be to have the context graph's IDs be externally visible and part
// of the signature, then `self` would only be a subtype of `other` if all of its context graph's IDs are also present in `other`'s context graph,
// with the same connections/shape.
// TODO: implement above? (it's kind of implemented - UDOps store the context graph mapping by context node ID.)
// TODO: implement subgraph checks for context graph.
true
}
}