Jlm
decouple-mem-state.cpp
Go to the documentation of this file.
1 /*
2  * Copyright 2025 David Metz <david.c.metz@ntnu.no>
3  * See COPYING for terms of redistribution.
4  */
5 
11 #include <jlm/hls/ir/hls.hpp>
12 #include <jlm/hls/util/view.hpp>
19 #include <jlm/rvsdg/node.hpp>
21 #include <jlm/rvsdg/theta.hpp>
22 #include <jlm/rvsdg/traverser.hpp>
23 #include <jlm/rvsdg/view.hpp>
24 
25 #include <algorithm>
26 #include <deque>
27 
28 namespace jlm::hls
29 {
30 
31 static rvsdg::Output *
33  rvsdg::Input * state_edge,
34  std::vector<rvsdg::SimpleNode *> & mem_ops,
35  bool modify);
36 
37 static rvsdg::Output *
39  rvsdg::Input * state_edge,
40  rvsdg::Output * new_edge,
41  rvsdg::SimpleNode * target_call,
42  rvsdg::Output * end)
43 {
44  rvsdg::Input * previous_state_edge = nullptr;
45  while (true)
46  {
47  // make sure we make progress
48  JLM_ASSERT(previous_state_edge != state_edge);
49  previous_state_edge = state_edge;
50  // terminate once desired point is reached
51  if (state_edge->origin() == end)
52  {
53  return end;
54  }
55  auto new_edge_user = get_mem_state_user(new_edge);
56  JLM_ASSERT(new_edge_user->region() == state_edge->region());
57  if (auto rr = dynamic_cast<jlm::rvsdg::RegionResult *>(state_edge))
58  {
59  return rr->output();
60  }
61  else if (auto ln = rvsdg::TryGetOwnerNode<LoopNode>(*state_edge))
62  {
63  auto si = util::assertedCast<rvsdg::StructuralInput>(state_edge);
64  auto arg = si->arguments.begin().ptr();
65  std::vector<rvsdg::SimpleNode *> mem_ops;
66  auto out = follow_state_edge(get_mem_state_user(arg), mem_ops, false);
67  if (std::count(mem_ops.begin(), mem_ops.end(), target_call))
68  {
69  // only route new edge through if target is contained
70  auto new_out = ln->AddLoopVar(new_edge);
71  new_edge_user->divert_to(new_out);
72  auto new_in = util::assertedCast<rvsdg::StructuralInput>(get_mem_state_user(new_edge));
73  JLM_ASSERT(
74  out
75  == trace_edge(
76  get_mem_state_user(arg),
77  new_in->arguments.begin().ptr(),
78  target_call,
79  end));
80  new_edge = new_out;
82  }
83  JLM_ASSERT(rvsdg::TryGetOwnerNode<LoopNode>(*out));
84  JLM_ASSERT(out->region() == state_edge->region());
85  state_edge = get_mem_state_user(out);
86  continue;
87  }
88 
89  auto si = state_edge;
90  auto sn = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*si);
91  auto new_si = new_edge_user;
92  auto new_sn = &rvsdg::AssertGetOwnerNode<rvsdg::SimpleNode>(*new_si);
93  auto [branchNode, branchOperation] =
94  rvsdg::TryGetSimpleNodeAndOptionalOp<BranchOperation>(*state_edge);
95  auto [muxNode, muxOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<MuxOperation>(*state_edge);
96  if (branchOperation
97  && !branchOperation
98  ->loop) // this is an example of why preserving structural nodes would be nice
99  {
100  // start of gamma
101  auto nbr = BranchOperation::create(*sn->input(0)->origin(), *new_edge);
102  auto nmux = MuxOperation::create(*sn->input(0)->origin(), nbr, false)[0];
103  new_edge_user->divert_to(nmux);
104  rvsdg::Output * out = nullptr;
105  for (size_t i = 0; i < sn->noutputs(); ++i)
106  {
107  out = trace_edge(get_mem_state_user(sn->output(i)), nbr[i], target_call, end);
108  }
109  JLM_ASSERT(out);
110  JLM_ASSERT(rvsdg::IsOwnerNodeOperation<MuxOperation>(*out));
111  state_edge = get_mem_state_user(out);
112  new_edge = nmux;
113  }
114  else if (branchOperation)
115  {
116  // end of loop
117  JLM_ASSERT(rvsdg::IsOwnerNodeOperation<BranchOperation>(*new_edge_user));
118  JLM_ASSERT(branchOperation->loop);
119  return util::assertedCast<rvsdg::RegionResult>(get_mem_state_user(sn->output(0)))->output();
120  }
121  else if (muxOperation && !muxOperation->loop)
122  {
123  // end of gamma
124  JLM_ASSERT(rvsdg::IsOwnerNodeOperation<MuxOperation>(*new_edge_user));
125  return sn->output(0);
126  }
127  else if (muxOperation)
128  {
129  // start of theta
130  JLM_ASSERT(rvsdg::IsOwnerNodeOperation<MuxOperation>(*new_edge_user));
131  JLM_ASSERT(muxOperation->loop);
132  state_edge = get_mem_state_user(sn->output(0));
133  new_edge = new_sn->output(0);
134  }
135  else if (rvsdg::IsOwnerNodeOperation<LoopConstantBufferOperation>(*state_edge))
136  {
137  // start of loop
138  JLM_ASSERT(rvsdg::IsOwnerNodeOperation<MuxOperation>(*new_edge_user));
139  state_edge = get_mem_state_user(sn->output(0));
140  new_edge = new_sn->output(0);
141  }
142  else if (rvsdg::IsOwnerNodeOperation<llvm::MemoryStateSplitOperation>(*state_edge))
143  {
144  rvsdg::Output * after_merge = nullptr;
145  for (size_t i = 0; i < sn->noutputs(); ++i)
146  {
147  // pick right edge by searching for target
148  std::vector<rvsdg::SimpleNode *> mem_ops;
149  after_merge = follow_state_edge(get_mem_state_user(sn->output(i)), mem_ops, false);
150  JLM_ASSERT(after_merge);
151  JLM_ASSERT(rvsdg::IsOwnerNodeOperation<llvm::MemoryStateMergeOperation>(*after_merge));
152  if (std::count(mem_ops.begin(), mem_ops.end(), target_call))
153  {
154  auto out = trace_edge(get_mem_state_user(sn->output(i)), new_edge, target_call, end);
155  JLM_ASSERT(out == after_merge);
156  }
157  else
158  {
159  // nothing relevant below the split - can just ignore it
160  }
161  }
162  state_edge = get_mem_state_user(after_merge);
163  new_edge = new_edge_user->origin();
164  }
165  else if (
166  rvsdg::IsOwnerNodeOperation<llvm::MemoryStateMergeOperation>(*state_edge)
167  || rvsdg::IsOwnerNodeOperation<llvm::LambdaExitMemoryStateMergeOperation>(*state_edge))
168  {
169  // we did not split the new state
170  return sn->output(0);
171  }
172  else if (rvsdg::IsOwnerNodeOperation<StateGateOperation>(*state_edge))
173  {
174  state_edge = get_mem_state_user(sn->output(si->index()));
175  }
176  else if (rvsdg::IsOwnerNodeOperation<llvm::LoadNonVolatileOperation>(*state_edge))
177  {
178  state_edge = get_mem_state_user(sn->output(1));
179  }
180  else if (rvsdg::IsOwnerNodeOperation<llvm::CallOperation>(*state_edge))
181  {
182  auto state_origin = state_edge->origin();
183  if (sn == target_call)
184  {
185  // move decouple call to new edge
186  state_edge = get_mem_state_user(sn->output(sn->noutputs() - 1));
187  state_edge->divert_to(state_origin);
188  si->divert_to(new_edge);
189  new_edge_user->divert_to(sn->output(sn->noutputs() - 1));
190  new_edge = new_edge_user->origin();
191  }
192  else
193  {
194  state_edge = get_mem_state_user(sn->output(sn->noutputs() - 1));
195  }
196  }
197  else
198  {
199  JLM_UNREACHABLE("whoops");
200  }
201  }
202 }
203 
204 static void
206  std::vector<std::tuple<rvsdg::SimpleNode *, rvsdg::Input *>> & outstanding_dec_reqs,
207  std::vector<rvsdg::SimpleNode *> & mem_ops,
208  rvsdg::Input * state_edge_before,
209  rvsdg::Output * state_edge_after)
210 {
211  JLM_ASSERT(state_edge_before->region() == state_edge_after->region());
212  // the reqs we encountered
213  for (auto op : mem_ops)
214  {
215  if (is_dec_req(op))
216  {
217  outstanding_dec_reqs.push_back(std::make_tuple(op, state_edge_before));
218  }
219  }
220  for (auto op : mem_ops)
221  {
222  if (rvsdg::is<const llvm::StoreNonVolatileOperation>(op))
223  {
224  // can't handle things if there is a store on the edge in the same loop/gamma
225  return;
226  }
227  }
228  for (auto resp : mem_ops)
229  {
230  if (!is_dec_res(resp))
231  continue;
232  auto res_constant = trace_constant(resp->input(1)->origin());
233  JLM_ASSERT(res_constant);
234  for (auto [req, state_edge_req] : outstanding_dec_reqs)
235  {
236  auto req_constant = trace_constant(req->input(1)->origin());
237  if (*req_constant == *res_constant)
238  {
239  // we found a match and can split the req off the state edge between input and output
240  // in this area both the req and resp can run separate from the main edge
241  auto split_outputs = llvm::MemoryStateSplitOperation::Create(*state_edge_req->origin(), 3);
242  state_edge_req->divert_to(split_outputs[0]);
243  JLM_ASSERT(state_edge_after->region() == split_outputs[1]->region());
244  auto after_user = get_mem_state_user(state_edge_after);
245  std::vector<rvsdg::Output *> operands(
246  { state_edge_after, split_outputs[1], split_outputs[2] });
248  after_user->divert_to(merge_out);
249  trace_edge(state_edge_req, split_outputs[1], req, state_edge_after);
250  trace_edge(state_edge_req, split_outputs[2], resp, state_edge_after);
251  }
252  }
253  }
254 }
255 
256 static void
258  std::vector<rvsdg::SimpleNode *> & mem_ops,
259  rvsdg::Input * state_edge_before,
260  rvsdg::Output * state_edge_after)
261 {
262  // the idea here is that if there is only one memory operation, and no other memory
263  // operations/staet gates on a state edge in a loop we can remove the backedge part and treat the
264  // edge like a loop constant, albeit with an output this will especially be important for stores
265  // that have a response.
266  // TODO: should this also be enabled for just memory state gates?
267  if (mem_ops.size() == 1
268  && (rvsdg::is<const llvm::StoreNonVolatileOperation>(mem_ops[0])
269  || rvsdg::is<const llvm::LoadNonVolatileOperation>(mem_ops[0])))
270  {
271  // before and after belong to same loop node
272  JLM_ASSERT(rvsdg::TryGetOwnerNode<LoopNode>(*state_edge_before));
273  JLM_ASSERT(
274  rvsdg::TryGetOwnerNode<LoopNode>(*state_edge_before)
275  == rvsdg::TryGetOwnerNode<LoopNode>(*state_edge_after));
276  convert_loop_state_to_lcb(state_edge_before);
277  }
278 }
279 
280 static rvsdg::Output *
282  rvsdg::Input * state_edge,
283  std::vector<rvsdg::SimpleNode *> & mem_ops,
284  bool modify)
285 {
286  // we use input so we can handle the scenario of a store having an extra user to deq from addrq
287  // things we can encounter:
288  // * region result:
289  // * return associated output
290  // * loop-node:
291  // * follow subregion
292  // * check which subset of dec_req, dec_resp and store is contained using handle_structural
293  // * converted gamma:
294  // * need to follow all sub-paths
295  // * begins with branch with !br->loop
296  // * follow_state_edge
297  // * ends with mux
298  // * return mux output
299  // * check which subset of dec_req, dec_resp and store is contained using handle_structural
300  // * mem state split/merge:
301  // * follow_state_edge/return merge output
302  // * load/state-gate
303  // * continue on state output
304  // * store/call
305  // * add to mem_ops
306  // * continue on state output
307  // * special case for store - can have multiple users because of addr_deq
308  // this tracks decouple requests that have not been handled yet
309  std::vector<std::tuple<rvsdg::SimpleNode *, rvsdg::Input *>> outstanding_dec_reqs;
310  while (true)
311  {
312  if (auto rr = dynamic_cast<jlm::rvsdg::RegionResult *>(state_edge))
313  {
314  return rr->output();
315  }
316  else if (rvsdg::TryGetOwnerNode<LoopNode>(*state_edge))
317  {
318  std::vector<rvsdg::SimpleNode *> loop_mem_ops;
319  auto si = jlm::util::assertedCast<rvsdg::StructuralInput>(state_edge);
320  auto arg = si->arguments.begin().ptr();
321  auto out = follow_state_edge(get_mem_state_user(arg), loop_mem_ops, modify);
322  // get this here before the graph is modified by handle_structural
323  auto new_state_edge = get_mem_state_user(out);
324  if (modify)
325  {
326  handle_structural(outstanding_dec_reqs, loop_mem_ops, state_edge, out);
327  optimize_single_mem_op_loop(loop_mem_ops, state_edge, out);
328  }
329  state_edge = new_state_edge;
330  mem_ops.insert(mem_ops.cend(), loop_mem_ops.begin(), loop_mem_ops.end());
331  continue;
332  }
333  auto si = state_edge;
334  auto sn = &rvsdg::AssertGetOwnerNode<rvsdg::SimpleNode>(*si);
335  auto [branchNode, branchOperation] =
336  rvsdg::TryGetSimpleNodeAndOptionalOp<BranchOperation>(*state_edge);
337  auto [muxNode, muxOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<MuxOperation>(*state_edge);
338  if (branchOperation
339  && !branchOperation
340  ->loop) // this is an example of why preserving structural nodes would be nice
341  {
342  std::vector<rvsdg::SimpleNode *> gamma_mem_ops;
343  // start of gamma
344  rvsdg::Output * out = nullptr;
345  for (size_t i = 0; i < sn->noutputs(); ++i)
346  {
347  out = follow_state_edge(get_mem_state_user(sn->output(i)), gamma_mem_ops, modify);
348  }
349  JLM_ASSERT(rvsdg::IsOwnerNodeOperation<MuxOperation>(*out));
350  JLM_ASSERT(out);
351  // get this here before the graph is modified by handle_structural
352  auto new_state_edge = get_mem_state_user(out);
353  if (modify)
354  handle_structural(outstanding_dec_reqs, gamma_mem_ops, state_edge, out);
355  state_edge = new_state_edge;
356  mem_ops.insert(mem_ops.cend(), gamma_mem_ops.begin(), gamma_mem_ops.end());
357  }
358  else if (branchOperation)
359  {
360  // end of loop
361  JLM_ASSERT(branchOperation->loop);
362  return util::assertedCast<rvsdg::RegionResult>(get_mem_state_user(sn->output(0)))->output();
363  }
364  else if (muxOperation && !muxOperation->loop)
365  {
366  // end of gamma
367  return sn->output(0);
368  }
369  else if (muxOperation)
370  {
371  // start of theta
372  JLM_ASSERT(muxOperation->loop);
373  state_edge = get_mem_state_user(sn->output(0));
374  }
375  else if (rvsdg::IsOwnerNodeOperation<LoopConstantBufferOperation>(*state_edge))
376  {
377  // start of theta
378  state_edge = get_mem_state_user(sn->output(0));
379  }
380  else if (
381  rvsdg::IsOwnerNodeOperation<llvm::MemoryStateSplitOperation>(*state_edge)
382  || rvsdg::IsOwnerNodeOperation<llvm::LambdaEntryMemoryStateSplitOperation>(*state_edge))
383  {
384  for (size_t i = 0; i < sn->noutputs(); ++i)
385  {
386  auto followed = follow_state_edge(get_mem_state_user(sn->output(i)), mem_ops, modify);
387  JLM_ASSERT(followed);
388  JLM_ASSERT(
389 
390  rvsdg::IsOwnerNodeOperation<llvm::MemoryStateMergeOperation>(*followed)
391  || rvsdg::IsOwnerNodeOperation<llvm::LambdaExitMemoryStateMergeOperation>(*followed));
392  state_edge = get_mem_state_user(followed);
393  }
394  }
395  else if (
396 
397  rvsdg::IsOwnerNodeOperation<llvm::MemoryStateMergeOperation>(*state_edge)
398  || rvsdg::IsOwnerNodeOperation<llvm::LambdaExitMemoryStateMergeOperation>(*state_edge))
399  {
400  return sn->output(0);
401  }
402  else if (rvsdg::IsOwnerNodeOperation<StateGateOperation>(*state_edge))
403  {
404  mem_ops.push_back(sn);
405  state_edge = get_mem_state_user(sn->output(si->index()));
406  }
407  else if (rvsdg::IsOwnerNodeOperation<llvm::LoadNonVolatileOperation>(*state_edge))
408  {
409  mem_ops.push_back(sn);
410  state_edge = get_mem_state_user(sn->output(1));
411  }
412  else if (rvsdg::IsOwnerNodeOperation<llvm::CallOperation>(*state_edge))
413  {
414  mem_ops.push_back(sn);
415  state_edge = get_mem_state_user(sn->output(sn->noutputs() - 1));
416  }
417  else if (rvsdg::IsOwnerNodeOperation<llvm::StoreNonVolatileOperation>(*state_edge))
418  {
419  mem_ops.push_back(sn);
420  JLM_ASSERT(sn->output(0)->nusers() == 1);
421  state_edge = get_mem_state_user(sn->output(0));
422  // handle case of store that has one edge going off to deq
423  if (rvsdg::IsOwnerNodeOperation<llvm::MemoryStateSplitOperation>(*state_edge))
424  {
425  // output 0 is the normal edge
426  state_edge =
427  get_mem_state_user(rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*state_edge)->output(0));
428  }
429  }
430  else
431  {
432  JLM_UNREACHABLE("whoops");
433  }
434  }
435 }
436 
437 void
439 {
440  JLM_ASSERT(loop_state_input->Type()->Kind() == rvsdg::TypeKind::State);
441  JLM_ASSERT(rvsdg::TryGetOwnerNode<LoopNode>(*loop_state_input));
442  auto si = util::assertedCast<rvsdg::StructuralInput>(loop_state_input);
443  auto arg = si->arguments.begin().ptr();
444  auto user = get_mem_state_user(arg);
445  auto [muxNode, muxOperation] = rvsdg::TryGetSimpleNodeAndOptionalOp<MuxOperation>(*user);
446  JLM_ASSERT(muxOperation && muxOperation->loop);
447  auto mux_node = rvsdg::TryGetOwnerNode<rvsdg::SimpleNode>(*user);
449  *mux_node->input(0)->origin(),
450  *mux_node->input(1)->origin())[0];
451  mux_node->output(0)->divert_users(lcb_out);
452  JLM_ASSERT(mux_node->IsDead());
453  remove(mux_node);
454 }
455 
456 static void
458 {
459  const auto & graph = rvsdgModule.Rvsdg();
460  const auto rootRegion = &graph.GetRootRegion();
461  if (rootRegion->numNodes() != 1)
462  {
463  throw std::logic_error("Root should have only one node now");
464  }
465 
466  const auto lambda = dynamic_cast<const rvsdg::LambdaNode *>(rootRegion->Nodes().begin().ptr());
467  if (!lambda)
468  {
469  throw std::logic_error("Node needs to be a lambda");
470  }
471 
472  auto state_arg = &llvm::GetMemoryStateRegionArgument(*lambda);
473  if (!state_arg)
474  {
475  // No memstate, i.e., no memory used
476  return;
477  }
478  // * pass after mem-queue
479  // * common edge is edge 0 in splits
480  // * keep response on common edge
481  // * seperate out request
482  // * [ ] scenario 1 - accross outer loops
483  // * split off before loop with req, merge after loop with resp
484  // * [ ] scenario 2.1 - within outer loop - no store on edge in loop
485  // * split before, merge after loop
486  // * [ ] scenario 2.2 - within outer loop - store on edge in loop
487  // * split at highest loop that contains no store on edge
488  // * store not being at higher level doesn't work
489  // * apply recursively - i.e. the same way for inner loops as for outer
490  const auto entryNode = llvm::tryGetMemoryStateEntrySplit(*lambda);
491  const auto exitNode = llvm::tryGetMemoryStateExitMerge(*lambda);
492  JLM_ASSERT(entryNode->noutputs() == exitNode->ninputs());
493  // process different pointer arg edges separately
494  for (size_t i = 0; i < entryNode->noutputs(); ++i)
495  {
496  std::vector<rvsdg::SimpleNode *> mem_ops;
497  std::vector<std::tuple<rvsdg::SimpleNode *, rvsdg::Input *>> dummy;
498  follow_state_edge(get_mem_state_user(entryNode->output(i)), mem_ops, true);
499  // we need this one final time across the whole lambda - at least for this edge
501  dummy,
502  mem_ops,
503  get_mem_state_user(entryNode->output(i)),
504  exitNode->input(i)->origin());
505  }
506 
508  util::StatisticsCollector statisticsCollector;
509  dne.Run(*lambda->subregion(), statisticsCollector);
510 }
511 
513 
516 {}
517 
518 void
520 {
521  decouple_mem_state(rvsdgModule);
522 }
523 
524 }
static std::vector< jlm::rvsdg::Output * > create(jlm::rvsdg::Output &predicate, jlm::rvsdg::Output &value, bool loop=false)
Definition: hls.hpp:68
static std::vector< jlm::rvsdg::Output * > create(jlm::rvsdg::Output &predicate, jlm::rvsdg::Output &value)
Definition: hls.hpp:378
void Run(rvsdg::RvsdgModule &rvsdgModule, util::StatisticsCollector &statisticsCollector) override
Perform RVSDG transformation.
~MemoryStateDecoupling() noexcept override
static std::vector< jlm::rvsdg::Output * > create(jlm::rvsdg::Output &predicate, const std::vector< jlm::rvsdg::Output * > &alternatives, bool discarding, bool loop=false)
Definition: hls.hpp:235
void Run(rvsdg::RvsdgModule &rvsdgModule, util::StatisticsCollector &statisticsCollector) override
Perform RVSDG transformation.
Definition: rhls-dne.cpp:516
static rvsdg::Output * Create(const std::vector< rvsdg::Output * > &operands)
static std::vector< rvsdg::Output * > Create(rvsdg::Output &operand, const size_t numResults)
Region & GetRootRegion() const noexcept
Definition: graph.hpp:99
void divert_to(Output *new_origin)
Definition: node.cpp:64
Output * origin() const noexcept
Definition: node.hpp:58
const std::shared_ptr< const rvsdg::Type > & Type() const noexcept
Definition: node.hpp:67
Region * region() const noexcept
Definition: node.cpp:83
Lambda node.
Definition: lambda.hpp:83
rvsdg::Region * region() const noexcept
Definition: node.cpp:151
Represents the result of a region.
Definition: region.hpp:120
Graph & Rvsdg() noexcept
Definition: RvsdgModule.hpp:57
Represents an RVSDG transformation.
#define JLM_ASSERT(x)
Definition: common.hpp:16
#define JLM_UNREACHABLE(msg)
Definition: common.hpp:43
static rvsdg::Output * follow_state_edge(rvsdg::Input *state_edge, std::vector< rvsdg::SimpleNode * > &mem_ops, bool modify)
bool is_dec_res(rvsdg::SimpleNode *node)
const llvm::IntegerConstantOperation * trace_constant(const rvsdg::Output *dst)
static void optimize_single_mem_op_loop(std::vector< rvsdg::SimpleNode * > &mem_ops, rvsdg::Input *state_edge_before, rvsdg::Output *state_edge_after)
static void decouple_mem_state(rvsdg::RvsdgModule &rvsdgModule)
static void handle_structural(std::vector< std::tuple< rvsdg::SimpleNode *, rvsdg::Input * >> &outstanding_dec_reqs, std::vector< rvsdg::SimpleNode * > &mem_ops, rvsdg::Input *state_edge_before, rvsdg::Output *state_edge_after)
rvsdg::Input * get_mem_state_user(rvsdg::Output *state_edge)
bool is_dec_req(rvsdg::SimpleNode *node)
static rvsdg::Output * trace_edge(rvsdg::Input *state_edge, rvsdg::Output *new_edge, rvsdg::SimpleNode *target_call, rvsdg::Output *end)
void convert_loop_state_to_lcb(rvsdg::Input *loop_state_input)
rvsdg::SimpleNode * tryGetMemoryStateEntrySplit(const rvsdg::LambdaNode &lambdaNode) noexcept
rvsdg::SimpleNode * tryGetMemoryStateExitMerge(const rvsdg::LambdaNode &lambdaNode) noexcept
rvsdg::Output & GetMemoryStateRegionArgument(const rvsdg::LambdaNode &lambdaNode) noexcept
static void remove(Node *node)
Definition: region.hpp:932
@ State
Designate a state type.
static std::vector< jlm::rvsdg::Output * > operands(const Node *node)
Definition: node.hpp:1049