chigraph  master
Systems programming language written for beginners in LLVM
FunctionValidator.cpp
Go to the documentation of this file.
1 
4 #include "chi/DataType.hpp"
5 #include "chi/GraphFunction.hpp"
6 #include "chi/GraphModule.hpp"
7 #include "chi/NodeInstance.hpp"
8 #include "chi/NodeType.hpp"
9 #include "chi/Support/Result.hpp"
10 
11 #include <unordered_map>
12 
13 namespace chi {
14 
16  Result res;
17 
19  res += validateFunctionNodeInputs(func);
20  res += validateFunctionExecOutputs(func);
21  res += validateFunctionEntryType(func);
22  res += validateFunctionExitTypes(func);
23 
24  if (func.name() == "main" && func.module().shortName() == "main") {
25  res += validateFunctionMainSignature(func);
26  }
27 
28  return res;
29 }
30 
32  Result res;
33 
34  // make sure they all get the context
35  auto funcCtx =
36  res.addScopedContext({{"function", func.name()}, {"module", func.module().fullName()}});
37 
38  // make sure all connections connect back
39  for (const auto& node : func.nodes()) {
40  // go through input data
41  auto id = 0ull;
42  for (const auto& conn : node.second->inputDataConnections) {
43  if (conn.first == nullptr) {
44  res.addEntry("EUKN", "Node is missing an input data connection",
45  {{"Node ID", node.second->stringId()},
46  {"nodetype", node.second->type().qualifiedName()},
47  {"requested id", id}});
48  ++id;
49  continue;
50  }
51 
52  // make sure it connects back
53  bool connectsBack = false;
54  for (const auto& remoteConn : conn.first->outputDataConnections[conn.second]) {
55  if (remoteConn.first == node.second.get() && remoteConn.second == id) {
56  connectsBack = true;
57  break;
58  }
59  }
60  if (!connectsBack) {
61  res.addEntry("EUKN", "Data connection doesn't connect back",
62  {{"Left Node", conn.first->stringId()},
63  {"Right Node", node.second->stringId()},
64  {"Right input ID", id}});
65  }
66  ++id;
67  }
68 
69  // output data
70  id = 0ull;
71  for (const auto& outputDataSlot : node.second->outputDataConnections) {
72  // this connection type can make multiple connections, so two for loops are needed
73  for (const auto& connection : outputDataSlot) {
74  assert(connection.first != nullptr);
75 
76  if (connection.first->inputDataConnections.size() <= connection.second) {
77  res.addEntry("EUKN", "Input data port not found in node",
78  {{"Node ID", connection.first->stringId()},
79  {"requested id", connection.second}});
80  continue;
81  }
82 
83  auto& remoteConn = connection.first->inputDataConnections[connection.second];
84  if (remoteConn.first != node.second.get() || remoteConn.second != id) {
85  res.addEntry("EUKN", "Data connection doesn't connect back",
86  {{"Left Node", node.second->stringId()},
87  {"Right Node", connection.first->stringId()},
88  {"Right input ID", connection.second}});
89  }
90  }
91  ++id;
92  }
93 
94  // input exec
95  id = 0ull;
96  for (const auto& inputExecSlot : node.second->inputExecConnections) {
97  // this connection type can make multiple connections, so two for loops are needed
98  for (const auto& connection : inputExecSlot) {
99  auto& remoteConn = connection.first->outputExecConnections[connection.second];
100  if (remoteConn.first != node.second.get() || remoteConn.second != id) {
101  res.addEntry("EUKN", "Exec connection doesn't connect back",
102  {{"Left Node", connection.first->stringId()},
103  {"Left Node Type", connection.first->type().qualifiedName()},
104  {"Right Node", node.second->stringId()},
105  {"Right Node Type", node.second->type().qualifiedName()},
106  {"Left output ID", connection.second}});
107  }
108  }
109  ++id;
110  }
111 
112  // output exec
113  id = 0ull;
114  for (const auto& connection : node.second->outputExecConnections) {
115  bool connectsBack = false;
116 
117  if (connection.first == nullptr) {
118  ++id;
119  continue;
120  }
121  for (const auto& remoteConnection :
122  connection.first->inputExecConnections[connection.second]) {
123  if (remoteConnection.second == id && remoteConnection.first == node.second.get()) {
124  connectsBack = true;
125  break;
126  }
127  }
128 
129  if (!connectsBack) {
130  res.addEntry("EUKN", "Exec connection doesn't connect back",
131  {{"Left Node", node.second->stringId()},
132  {"Left Node Type", node.second->type().qualifiedName()},
133  {"Right Node", connection.first->stringId()},
134  {"Right Node Type", connection.first->type().qualifiedName()},
135  {"Left output ID", id}});
136  }
137 
138  ++id;
139  }
140  }
141 
142  // make sure all entires have the right type
143 
144  return res;
145 }
146 
147 namespace {
148 
152 Result validatePath(
153  const NodeInstance& inst, int inExecId,
154  std::unordered_map<const NodeInstance*, std::vector<int> /*in Exec id*/> alreadyCalled) {
155  Result res;
156 
157  // if we've already been here, then return, it's a loop
158  {
159  auto iter = alreadyCalled.find(&inst);
160  if (iter != alreadyCalled.end() &&
161  std::find(iter->second.begin(), iter->second.end(), inExecId) != iter->second.end()) {
162  return res;
163  }
164  }
165 
166  // make sure the inputs are already in processed
167  auto id = 0ull;
168  for (const auto& conn : inst.inputDataConnections) {
169  if (conn.first == nullptr) {
170  res.addEntry("EUKN", "Node is missing an input data connection",
171  {{"Node ID", inst.stringId()},
172  {"dataid", id},
173  {"nodetype", inst.type().qualifiedName()}});
174  ++id;
175  continue;
176  }
177 
178  if (!conn.first->type().pure() && alreadyCalled.find(conn.first) == alreadyCalled.end()) {
179  res.addEntry("EUKN", "Node that accepts data from another node is called first",
180  {{"Node ID", inst.stringId()}, {"othernodeid", conn.first->stringId()}});
181  }
182 
183  ++id;
184  }
185 
186  alreadyCalled[&inst].push_back(inExecId);
187 
188  // call this on the nodes this calls
189  for (const auto& conn : inst.outputExecConnections) {
190  if (conn.first == nullptr) { continue; }
191  res += validatePath(*conn.first, conn.second, alreadyCalled);
192  }
193 
194  return res;
195 }
196 } // anonymous namespace
197 
199  Result res;
200 
201  // make sure they all get the context
202  auto funcCtx =
203  res.addScopedContext({{"function", func.name()}, {"module", func.module().fullName()}});
204 
205  auto entry = func.entryNode();
206 
207  if (entry == nullptr) { return res; }
208 
209  std::unordered_map<const NodeInstance*, std::vector<int>> alreadyCalled;
210  alreadyCalled.emplace(entry, std::vector<int>{});
211 
212  // no need to create a processed because you can't create a loop with entry in it
213 
214  for (const auto& conn : entry->outputExecConnections) {
215  if (conn.first == nullptr) { continue; }
216  res += validatePath(*conn.first, conn.second, alreadyCalled);
217  }
218 
219  return res;
220 }
221 
223  // make sure all exec outputs exist, and raise an error otherwise.
224  // TODO (#70): quickfix to add return
225 
226  Result res;
227 
228  // make sure they all get the context
229  auto funcCtx =
230  res.addScopedContext({{"function", func.name()}, {"module", func.module().fullName()}});
231 
232  for (const auto& nodepair : func.nodes()) {
233  auto node = nodepair.second.get();
234 
235  auto id = 0ull;
236  for (const auto& conn : node->outputExecConnections) {
237  if (conn.second == ~0ull || conn.first == nullptr) {
238  res.addEntry("EUKN", "Node is missing an output exec connection",
239  {{"Node ID", node->stringId()}, {"Missing ID", id}});
240  }
241 
242  ++id;
243  }
244  }
245 
246  return res;
247 }
248 
250  Result res;
251 
252  auto entry = func.entryNode();
253  if (!entry) {
254  res.addEntry("EUKN", "Function must have a valid entry node to validate the entry type",
255  {{"Function", func.name()}, {"Module", func.module().fullName()}});
256  return res;
257  }
258 
259  // make sure that the entry node has the right data types
260  if (!std::equal(func.dataInputs().begin(), func.dataInputs().end(),
261  entry->type().dataOutputs().begin())) {
262  nlohmann::json inFunc = nlohmann::json::array();
263  for (auto& in : func.dataInputs()) {
264  inFunc.push_back({{in.name, in.type.qualifiedName()}});
265  }
266 
267  nlohmann::json inEntry = nlohmann::json::array();
268  for (auto& in :
269  entry->type().dataOutputs()) { // outputs to entry are inputs to the function
270  inEntry.push_back({{in.name, in.type.qualifiedName()}});
271  }
272 
273  res.addEntry("EUKN", "Inputs to function doesn't match function inputs",
274  {{"Function Inputs", inFunc}, {"Entry Inputs", inEntry}});
275  return res;
276  }
277 
278  return res;
279 }
280 
282  Result res;
283 
284  // make sure that each exit node has the right data types
285  for (auto exitNode : func.nodesWithType("lang", "exit")) {
286  if (!std::equal(func.dataOutputs().begin(), func.dataOutputs().end(),
287  exitNode->type().dataInputs().begin())) {
288  nlohmann::json outFunc = nlohmann::json::array();
289  for (auto& out : func.dataOutputs()) {
290  outFunc.push_back({{out.name, out.type.qualifiedName()}});
291  }
292 
293  nlohmann::json outExit = nlohmann::json::array();
294  for (auto& out : exitNode->type().dataOutputs()) {
295  // inputs to the exit are outputs to the function
296  outExit.push_back({{out.name, out.type.qualifiedName()}});
297  }
298 
299  res.addEntry("EUKN", "Outputs to function doesn't match function exit",
300  {{"Function Outputs", outFunc},
301  {"Exit Outputs", outExit},
302  {"Node ID", exitNode->stringId()}});
303  return res;
304  }
305  }
306 
307  return res;
308 }
309 
311  Result res;
312 
313  if (func.execInputs().size() != 1) {
314  res.addEntry("EUKN", "A main function must have exactly one exec in",
315  {{"Exec Inputs", func.execInputs()}});
316  }
317  if (func.execOutputs().size() != 1) {
318  res.addEntry("EUKN", "A main function must have exactly one exec out",
319  {{"Exec Outputs", func.execOutputs()}});
320  }
321 
322  if (!func.dataInputs().empty()) {
323  auto dataJson = nlohmann::json::array();
324  for (const auto& param : func.dataInputs()) {
325  dataJson.push_back({{param.name, param.type.qualifiedName()}});
326  }
327 
328  res.addEntry("EUKN", "A main function must have no data inputs",
329  {{"Data Inputs", dataJson}});
330  }
331  if (func.dataOutputs().size() != 1 ||
332  func.dataOutputs()[0].type.qualifiedName() != "lang:i32") {
333  auto dataJson = nlohmann::json::array();
334  for (const auto& param : func.dataOutputs()) {
335  dataJson.push_back({{param.name, param.type.qualifiedName()}});
336  }
337 
338  res.addEntry("EUKN", "A main function must have exactly one data output that's a lang:i32",
339  {{"Data Outputs", dataJson}});
340  }
341  return res;
342 }
343 
344 } // namespace chi
const std::vector< NamedDataType > & dataOutputs() const
Get the function data outputs in the format {type, docstring}.
Defines functions for validating GraphFunction objects.
this is an AST-like representation of a function in a graph It is used for IDE-like behavior...
void addEntry(const char *ec, const char *overview, nlohmann::json data)
Add a entry to the result, either a warning or an error.
Definition: Result.cpp:52
Result validateFunctionExecOutputs(const GraphFunction &func)
Make sure nodes have an output connection.
const std::vector< std::string > & execInputs() const
Get the function exec inputs.
std::string stringId() const
Get the ID as a string.
const std::vector< std::string > & execOutputs() const
Get the function exec outputs.
Result validateFunctionEntryType(const GraphFunction &func)
Make sure the function entry type aligns with the function type.
Defines the Result class and related functions.
Result validateFunctionConnectionsAreTwoWay(const GraphFunction &func)
Make sure that connections connect back and that they have the same types.
Result validateFunction(const GraphFunction &func)
Validate that a function is compilable.
NodeType & type()
Get the type of the instance.
An instance of a node.
std::vector< std::pair< NodeInstance *, size_t > > inputDataConnections
The connections that go into this node, data.
GraphModule & module() const
Get the GraphModule that contains this GraphFunction.
std::string name() const
Get the name of the function.
Result validateFunctionNodeInputs(const GraphFunction &func)
Make sure that nodes are called before their outputs are used.
std::string fullName() const
Get the full name of the module.
Definition: ChiModule.hpp:61
ScopedContext addScopedContext(const nlohmann::json &data)
Add a context with a scope Example usage: chi::Result res; res.contextJson(); // returns {} { aut...
Definition: Result.hpp:123
NodeInstance * entryNode() const noexcept
Gets the node with type lang:entry returns nullptr on failure Also returns nullptr if there are two e...
std::unordered_map< boost::uuids::uuid, std::unique_ptr< NodeInstance > > & nodes()
Get the nodes in the function Usually called by connectData or connectExec or GraphFunction.
Defines the NodeType class.
Result validateFunctionMainSignature(const GraphFunction &func)
Make sure it&#39;s a valid signautre for a main function.
Result validateFunctionExitTypes(const GraphFunction &func)
Make sure the function exit types align with the function type.
Defines the DataType class.
std::vector< NodeInstance * > nodesWithType(const boost::filesystem::path &module, boost::string_view name) const noexcept
Gets the nodes with a given type.
std::vector< std::pair< NodeInstance *, size_t > > outputExecConnections
The connections that go out of this node, exec.
Defines the GraphModule class.
The namespace where chigraph lives.
std::string qualifiedName() const
Get the qualified name of the node type, like module.name():name()
Definition: NodeType.cpp:17
const std::vector< NamedDataType > & dataInputs() const
Get the function data inputs in the format {type, docstring}.
std::string shortName() const
Get the short name of the module (the last bit)
Definition: ChiModule.hpp:58
The result object, used for identifiying errors with good diagnostics.
Definition: Result.hpp:72
Declares the GraphFunction class.
Defines the NodeInstance class and related functions.