1/*
2 * Copyright (C) 2014-2018 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "DFGPutStackSinkingPhase.h"
28
29#if ENABLE(DFG_JIT)
30
31#include "DFGBlockMapInlines.h"
32#include "DFGGraph.h"
33#include "DFGInsertionSet.h"
34#include "DFGPhase.h"
35#include "DFGPreciseLocalClobberize.h"
36#include "DFGSSACalculator.h"
37#include "DFGValidate.h"
38#include "JSCInlines.h"
39#include "OperandsInlines.h"
40
41namespace JSC { namespace DFG {
42
43namespace {
44
45namespace DFGPutStackSinkingPhaseInternal {
46static const bool verbose = false;
47}
48
49class PutStackSinkingPhase : public Phase {
50public:
51 PutStackSinkingPhase(Graph& graph)
52 : Phase(graph, "PutStack sinking")
53 {
54 }
55
56 bool run()
57 {
58 // FIXME: One of the problems of this approach is that it will create a duplicate Phi graph
59 // for sunken PutStacks in the presence of interesting control flow merges, and where the
60 // value being PutStack'd is also otherwise live in the DFG code. We could work around this
61 // by doing the sinking over CPS, or maybe just by doing really smart hoisting. It's also
62 // possible that the duplicate Phi graph can be deduplicated by B3. It would be best if we
63 // could observe that there is already a Phi graph in place that does what we want. In
64 // principle if we have a request to place a Phi at a particular place, we could just check
65 // if there is already a Phi that does what we want. Because PutStackSinkingPhase runs just
66 // after SSA conversion, we have almost a guarantee that the Phi graph we produce here would
67 // be trivially redundant to the one we already have.
68
69 // FIXME: This phase doesn't adequately use KillStacks. KillStack can be viewed as a def.
70 // This is mostly inconsequential; it would be a bug to have a local live at a KillStack.
71 // More important is that KillStack should swallow any deferral. After a KillStack, the
72 // local should behave like a TOP deferral because it would be invalid for anyone to trust
73 // the stack. It's not clear to me if this is important or not.
74 // https://bugs.webkit.org/show_bug.cgi?id=145296
75
76 if (DFGPutStackSinkingPhaseInternal::verbose) {
77 dataLog("Graph before PutStack sinking:\n");
78 m_graph.dump();
79 }
80
81 m_graph.ensureSSADominators();
82
83 SSACalculator ssaCalculator(m_graph);
84 InsertionSet insertionSet(m_graph);
85
86 // First figure out where various locals are live.
87 BlockMap<Operands<bool>> liveAtHead(m_graph);
88 BlockMap<Operands<bool>> liveAtTail(m_graph);
89
90 for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
91 liveAtHead[block] = Operands<bool>(OperandsLike, block->variablesAtHead);
92 liveAtTail[block] = Operands<bool>(OperandsLike, block->variablesAtHead);
93
94 liveAtHead[block].fill(false);
95 liveAtTail[block].fill(false);
96 }
97
98 bool changed;
99 do {
100 changed = false;
101
102 for (BlockIndex blockIndex = m_graph.numBlocks(); blockIndex--;) {
103 BasicBlock* block = m_graph.block(blockIndex);
104 if (!block)
105 continue;
106
107 Operands<bool> live = liveAtTail[block];
108 for (unsigned nodeIndex = block->size(); nodeIndex--;) {
109 Node* node = block->at(nodeIndex);
110 if (DFGPutStackSinkingPhaseInternal::verbose)
111 dataLog("Live at ", node, ": ", live, "\n");
112
113 Vector<VirtualRegister, 4> reads;
114 Vector<VirtualRegister, 4> writes;
115 auto escapeHandler = [&] (VirtualRegister operand) {
116 if (operand.isHeader())
117 return;
118 if (DFGPutStackSinkingPhaseInternal::verbose)
119 dataLog(" ", operand, " is live at ", node, "\n");
120 reads.append(operand);
121 };
122
123 auto writeHandler = [&] (VirtualRegister operand) {
124 if (operand.isHeader())
125 return;
126 RELEASE_ASSERT(node->op() == PutStack || node->op() == LoadVarargs || node->op() == ForwardVarargs || node->op() == KillStack);
127 writes.append(operand);
128 };
129
130 preciseLocalClobberize(
131 m_graph, node, escapeHandler, writeHandler,
132 [&] (VirtualRegister, LazyNode) { });
133
134 for (VirtualRegister operand : writes)
135 live.operand(operand) = false;
136 for (VirtualRegister operand : reads)
137 live.operand(operand) = true;
138 }
139
140 if (live == liveAtHead[block])
141 continue;
142
143 liveAtHead[block] = live;
144 changed = true;
145
146 for (BasicBlock* predecessor : block->predecessors) {
147 for (size_t i = live.size(); i--;)
148 liveAtTail[predecessor][i] |= live[i];
149 }
150 }
151
152 } while (changed);
153
154 // All of the arguments should be live at head of root. Note that we may find that some
155 // locals are live at head of root. This seems wrong but isn't. This will happen for example
156 // if the function accesses closure variable #42 for some other function and we either don't
157 // have variable #42 at all or we haven't set it at root, for whatever reason. Basically this
158 // arises since our aliasing for closure variables is conservatively based on variable number
159 // and ignores the owning symbol table. We should probably fix this eventually and make our
160 // aliasing more precise.
161 //
162 // For our purposes here, the imprecision in the aliasing is harmless. It just means that we
163 // may not do as much Phi pruning as we wanted.
164 for (size_t i = liveAtHead.atIndex(0).numberOfArguments(); i--;)
165 DFG_ASSERT(m_graph, nullptr, liveAtHead.atIndex(0).argument(i));
166
167 // Next identify where we would want to sink PutStacks to. We say that there is a deferred
168 // flush if we had a PutStack with a given FlushFormat but it hasn't been materialized yet.
169 // Deferrals have the following lattice; but it's worth noting that the TOP part of the
170 // lattice serves an entirely different purpose than the rest of the lattice: it just means
171 // that we're in a region of code where nobody should have been relying on the value. The
172 // rest of the lattice means that we either have a PutStack that is deferred (i.e. still
173 // needs to be executed) or there isn't one (because we've alraedy executed it).
174 //
175 // Bottom:
176 // Represented as DeadFlush.
177 // Means that all previous PutStacks have been executed so there is nothing deferred.
178 // During merging this is subordinate to the other kinds of deferrals, because it
179 // represents the fact that we've already executed all necessary PutStacks. This implies
180 // that there *had* been some PutStacks that we should have executed.
181 //
182 // Top:
183 // Represented as ConflictingFlush.
184 // Represents the fact that we know, via forward flow, that there isn't any value in the
185 // given local that anyone should have been relying on. This comes into play at the
186 // prologue (because in SSA form at the prologue no local has any value) or when we merge
187 // deferrals for different formats's. A lexical scope in which a local had some semantic
188 // meaning will by this point share the same format; if we had stores from different
189 // lexical scopes that got merged together then we may have a conflicting format. Hence
190 // a conflicting format proves that we're no longer in an area in which the variable was
191 // in scope. Note that this is all approximate and only precise enough to later answer
192 // questions pertinent to sinking. For example, this doesn't always detect when a local
193 // is no longer semantically relevant - we may well have a deferral from inside some
194 // inlined call survive outside of that inlined code, and this is generally OK. In the
195 // worst case it means that we might think that a deferral that is actually dead must
196 // still be executed. But we usually catch that with liveness. Liveness usually catches
197 // such cases, but that's not guaranteed since liveness is conservative.
198 //
199 // What Top does give us is detects situations where we both don't need to care about a
200 // deferral and there is no way that we could reason about it anyway. If we merged
201 // deferrals for different formats then we wouldn't know the format to use. So, we use
202 // Top in that case because that's also a case where we know that we can ignore the
203 // deferral.
204 //
205 // Deferral with a concrete format:
206 // Represented by format values other than DeadFlush or ConflictingFlush.
207 // Represents the fact that the original code would have done a PutStack but we haven't
208 // identified an operation that would have observed that PutStack.
209 //
210 // We need to be precise about liveness in this phase because not doing so
211 // could cause us to insert a PutStack before a node we thought may escape a
212 // value that it doesn't really escape. Sinking this PutStack above such a node may
213 // cause us to insert a GetStack that we forward to the Phi we're feeding into the
214 // sunken PutStack. Inserting such a GetStack could cause us to load garbage and
215 // can confuse the AI to claim untrue things (like that the program will exit when
216 // it really won't).
217 BlockMap<Operands<FlushFormat>> deferredAtHead(m_graph);
218 BlockMap<Operands<FlushFormat>> deferredAtTail(m_graph);
219
220 for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
221 deferredAtHead[block] =
222 Operands<FlushFormat>(OperandsLike, block->variablesAtHead);
223 deferredAtTail[block] =
224 Operands<FlushFormat>(OperandsLike, block->variablesAtHead);
225 }
226
227 for (unsigned local = deferredAtHead.atIndex(0).numberOfLocals(); local--;)
228 deferredAtHead.atIndex(0).local(local) = ConflictingFlush;
229
230 do {
231 changed = false;
232
233 for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
234 Operands<FlushFormat> deferred = deferredAtHead[block];
235
236 for (Node* node : *block) {
237 if (DFGPutStackSinkingPhaseInternal::verbose)
238 dataLog("Deferred at ", node, ":", deferred, "\n");
239
240 if (node->op() == GetStack) {
241 // Handle the case that the input doesn't match our requirements. This is
242 // really a bug, but it's a benign one if we simply don't run this phase.
243 // It usually arises because of patterns like:
244 //
245 // if (thing)
246 // PutStack()
247 // ...
248 // if (thing)
249 // GetStack()
250 //
251 // Or:
252 //
253 // if (never happens)
254 // GetStack()
255 //
256 // Because this phase runs early in SSA, it should be sensible to enforce
257 // that no such code pattern has arisen yet. So, when validation is
258 // enabled, we assert that we aren't seeing this. But with validation
259 // disabled we silently let this fly and we just abort this phase.
260 // FIXME: Get rid of all remaining cases of conflicting GetStacks.
261 // https://bugs.webkit.org/show_bug.cgi?id=150398
262
263 bool isConflicting =
264 deferred.operand(node->stackAccessData()->local) == ConflictingFlush;
265
266 if (validationEnabled())
267 DFG_ASSERT(m_graph, node, !isConflicting);
268
269 if (isConflicting) {
270 // Oh noes! Abort!!
271 return false;
272 }
273
274 // A GetStack doesn't affect anything, since we know which local we are reading
275 // from.
276 continue;
277 } else if (node->op() == PutStack) {
278 VirtualRegister operand = node->stackAccessData()->local;
279 deferred.operand(operand) = node->stackAccessData()->format;
280 continue;
281 } else if (node->op() == KillStack) {
282 // We don't want to sink a PutStack past a KillStack.
283 deferred.operand(node->unlinkedLocal()) = ConflictingFlush;
284 continue;
285 }
286
287 auto escapeHandler = [&] (VirtualRegister operand) {
288 if (DFGPutStackSinkingPhaseInternal::verbose)
289 dataLog("For ", node, " escaping ", operand, "\n");
290 if (operand.isHeader())
291 return;
292 // We will materialize just before any reads.
293 deferred.operand(operand) = DeadFlush;
294 };
295
296 auto writeHandler = [&] (VirtualRegister operand) {
297 if (operand.isHeader())
298 return;
299 RELEASE_ASSERT(node->op() == LoadVarargs || node->op() == ForwardVarargs);
300 deferred.operand(operand) = DeadFlush;
301 };
302
303 preciseLocalClobberize(
304 m_graph, node, escapeHandler, writeHandler,
305 [&] (VirtualRegister, LazyNode) { });
306 }
307
308 if (deferred == deferredAtTail[block])
309 continue;
310
311 deferredAtTail[block] = deferred;
312 changed = true;
313
314 for (BasicBlock* successor : block->successors()) {
315 for (size_t i = deferred.size(); i--;) {
316 if (DFGPutStackSinkingPhaseInternal::verbose)
317 dataLog("Considering ", VirtualRegister(deferred.operandForIndex(i)), " at ", pointerDump(block), "->", pointerDump(successor), ": ", deferred[i], " and ", deferredAtHead[successor][i], " merges to ");
318
319 deferredAtHead[successor][i] =
320 merge(deferredAtHead[successor][i], deferred[i]);
321
322 if (DFGPutStackSinkingPhaseInternal::verbose)
323 dataLog(deferredAtHead[successor][i], "\n");
324 }
325 }
326 }
327
328 } while (changed);
329
330 // We wish to insert PutStacks at all of the materialization points, which are defined
331 // implicitly as the places where we set deferred to Dead while it was previously not Dead.
332 // To do this, we may need to build some Phi functions to handle stuff like this:
333 //
334 // Before:
335 //
336 // if (p)
337 // PutStack(r42, @x)
338 // else
339 // PutStack(r42, @y)
340 //
341 // After:
342 //
343 // if (p)
344 // Upsilon(@x, ^z)
345 // else
346 // Upsilon(@y, ^z)
347 // z: Phi()
348 // PutStack(r42, @z)
349 //
350 // This means that we have an SSACalculator::Variable for each local, and a Def is any
351 // PutStack in the original program. The original PutStacks will simply vanish.
352
353 Operands<SSACalculator::Variable*> operandToVariable(
354 OperandsLike, m_graph.block(0)->variablesAtHead);
355 Vector<VirtualRegister> indexToOperand;
356 for (size_t i = m_graph.block(0)->variablesAtHead.size(); i--;) {
357 VirtualRegister operand(m_graph.block(0)->variablesAtHead.operandForIndex(i));
358
359 SSACalculator::Variable* variable = ssaCalculator.newVariable();
360 operandToVariable.operand(operand) = variable;
361 ASSERT(indexToOperand.size() == variable->index());
362 indexToOperand.append(operand);
363 }
364
365 HashSet<Node*> putStacksToSink;
366
367 for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
368 for (Node* node : *block) {
369 switch (node->op()) {
370 case PutStack:
371 putStacksToSink.add(node);
372 ssaCalculator.newDef(
373 operandToVariable.operand(node->stackAccessData()->local),
374 block, node->child1().node());
375 break;
376 case GetStack:
377 ssaCalculator.newDef(
378 operandToVariable.operand(node->stackAccessData()->local),
379 block, node);
380 break;
381 default:
382 break;
383 }
384 }
385 }
386
387 ssaCalculator.computePhis(
388 [&] (SSACalculator::Variable* variable, BasicBlock* block) -> Node* {
389 VirtualRegister operand = indexToOperand[variable->index()];
390
391 if (!liveAtHead[block].operand(operand))
392 return nullptr;
393
394 FlushFormat format = deferredAtHead[block].operand(operand);
395
396 // We could have an invalid deferral because liveness is imprecise.
397 if (!isConcrete(format))
398 return nullptr;
399
400 if (DFGPutStackSinkingPhaseInternal::verbose)
401 dataLog("Adding Phi for ", operand, " at ", pointerDump(block), "\n");
402
403 Node* phiNode = m_graph.addNode(SpecHeapTop, Phi, block->at(0)->origin.withInvalidExit());
404 phiNode->mergeFlags(resultFor(format));
405 return phiNode;
406 });
407
408 Operands<Node*> mapping(OperandsLike, m_graph.block(0)->variablesAtHead);
409 Operands<FlushFormat> deferred;
410 for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
411 mapping.fill(nullptr);
412
413 for (size_t i = mapping.size(); i--;) {
414 VirtualRegister operand(mapping.operandForIndex(i));
415
416 SSACalculator::Variable* variable = operandToVariable.operand(operand);
417 SSACalculator::Def* def = ssaCalculator.reachingDefAtHead(block, variable);
418 if (!def)
419 continue;
420
421 mapping.operand(operand) = def->value();
422 }
423
424 if (DFGPutStackSinkingPhaseInternal::verbose)
425 dataLog("Mapping at top of ", pointerDump(block), ": ", mapping, "\n");
426
427 for (SSACalculator::Def* phiDef : ssaCalculator.phisForBlock(block)) {
428 VirtualRegister operand = indexToOperand[phiDef->variable()->index()];
429
430 insertionSet.insert(0, phiDef->value());
431
432 if (DFGPutStackSinkingPhaseInternal::verbose)
433 dataLog(" Mapping ", operand, " to ", phiDef->value(), "\n");
434 mapping.operand(operand) = phiDef->value();
435 }
436
437 deferred = deferredAtHead[block];
438 for (unsigned nodeIndex = 0; nodeIndex < block->size(); ++nodeIndex) {
439 Node* node = block->at(nodeIndex);
440 if (DFGPutStackSinkingPhaseInternal::verbose)
441 dataLog("Deferred at ", node, ":", deferred, "\n");
442
443 switch (node->op()) {
444 case PutStack: {
445 StackAccessData* data = node->stackAccessData();
446 VirtualRegister operand = data->local;
447 deferred.operand(operand) = data->format;
448 if (DFGPutStackSinkingPhaseInternal::verbose)
449 dataLog(" Mapping ", operand, " to ", node->child1().node(), " at ", node, "\n");
450 mapping.operand(operand) = node->child1().node();
451 break;
452 }
453
454 case GetStack: {
455 StackAccessData* data = node->stackAccessData();
456 FlushFormat format = deferred.operand(data->local);
457 if (!isConcrete(format)) {
458 DFG_ASSERT(
459 m_graph, node,
460 deferred.operand(data->local) != ConflictingFlush, deferred.operand(data->local));
461
462 // This means there is no deferral. No deferral means that the most
463 // authoritative value for this stack slot is what is stored in the stack. So,
464 // keep the GetStack.
465 mapping.operand(data->local) = node;
466 break;
467 }
468
469 // We have a concrete deferral, which means a PutStack that hasn't executed yet. It
470 // would have stored a value with a certain format. That format must match our
471 // format. But more importantly, we can simply use the value that the PutStack would
472 // have stored and get rid of the GetStack.
473 DFG_ASSERT(m_graph, node, format == data->format, format, data->format);
474
475 Node* incoming = mapping.operand(data->local);
476 node->child1() = incoming->defaultEdge();
477 node->convertToIdentity();
478 break;
479 }
480
481 case KillStack: {
482 deferred.operand(node->unlinkedLocal()) = ConflictingFlush;
483 break;
484 }
485
486 default: {
487 auto escapeHandler = [&] (VirtualRegister operand) {
488 if (DFGPutStackSinkingPhaseInternal::verbose)
489 dataLog("For ", node, " escaping ", operand, "\n");
490
491 if (operand.isHeader())
492 return;
493
494 FlushFormat format = deferred.operand(operand);
495 if (!isConcrete(format)) {
496 // It's dead now, rather than conflicting.
497 deferred.operand(operand) = DeadFlush;
498 return;
499 }
500
501 // Gotta insert a PutStack.
502 if (DFGPutStackSinkingPhaseInternal::verbose)
503 dataLog("Inserting a PutStack for ", operand, " at ", node, "\n");
504
505 Node* incoming = mapping.operand(operand);
506 DFG_ASSERT(m_graph, node, incoming);
507
508 insertionSet.insertNode(
509 nodeIndex, SpecNone, PutStack, node->origin,
510 OpInfo(m_graph.m_stackAccessData.add(operand, format)),
511 Edge(incoming, uncheckedUseKindFor(format)));
512
513 deferred.operand(operand) = DeadFlush;
514 };
515
516 auto writeHandler = [&] (VirtualRegister operand) {
517 if (operand.isHeader())
518 return;
519 // LoadVarargs and ForwardVarargs are unconditional writes to the stack
520 // locations they claim to write to. They do not read from the stack
521 // locations they write to. This makes those stack locations dead right
522 // before a LoadVarargs/ForwardVarargs. This means we should never sink
523 // PutStacks right to this point.
524 RELEASE_ASSERT(node->op() == LoadVarargs || node->op() == ForwardVarargs);
525 deferred.operand(operand) = DeadFlush;
526 };
527
528 preciseLocalClobberize(
529 m_graph, node, escapeHandler, writeHandler,
530 [&] (VirtualRegister, LazyNode) { });
531 break;
532 } }
533 }
534
535 NodeAndIndex terminal = block->findTerminal();
536 size_t upsilonInsertionPoint = terminal.index;
537 NodeOrigin upsilonOrigin = terminal.node->origin;
538 for (BasicBlock* successorBlock : block->successors()) {
539 for (SSACalculator::Def* phiDef : ssaCalculator.phisForBlock(successorBlock)) {
540 Node* phiNode = phiDef->value();
541 SSACalculator::Variable* variable = phiDef->variable();
542 VirtualRegister operand = indexToOperand[variable->index()];
543 if (DFGPutStackSinkingPhaseInternal::verbose)
544 dataLog("Creating Upsilon for ", operand, " at ", pointerDump(block), "->", pointerDump(successorBlock), "\n");
545 FlushFormat format = deferredAtHead[successorBlock].operand(operand);
546 DFG_ASSERT(m_graph, nullptr, isConcrete(format), format);
547 UseKind useKind = uncheckedUseKindFor(format);
548
549 // We need to get a value for the stack slot. This phase doesn't really have a
550 // good way of determining if a stack location got clobbered. It just knows if
551 // there is a deferral. The lack of a deferral might mean that a PutStack or
552 // GetStack had never happened, or it might mean that the value was read, or
553 // that it was written. It's OK for us to make some bad decisions here, since
554 // GCSE will clean it up anyway.
555 Node* incoming;
556 if (isConcrete(deferred.operand(operand))) {
557 incoming = mapping.operand(operand);
558 DFG_ASSERT(m_graph, phiNode, incoming);
559 } else {
560 // Issue a GetStack to get the value. This might introduce some redundancy
561 // into the code, but if it's bad enough, GCSE will clean it up.
562 incoming = insertionSet.insertNode(
563 upsilonInsertionPoint, SpecNone, GetStack, upsilonOrigin,
564 OpInfo(m_graph.m_stackAccessData.add(operand, format)));
565 incoming->setResult(resultFor(format));
566 }
567
568 insertionSet.insertNode(
569 upsilonInsertionPoint, SpecNone, Upsilon, upsilonOrigin,
570 OpInfo(phiNode), Edge(incoming, useKind));
571 }
572 }
573
574 insertionSet.execute(block);
575 }
576
577 // Finally eliminate the sunken PutStacks by turning them into Checks. This keeps whatever
578 // type check they were doing.
579 for (BasicBlock* block : m_graph.blocksInNaturalOrder()) {
580 for (auto* node : *block) {
581 if (!putStacksToSink.contains(node))
582 continue;
583
584 node->remove(m_graph);
585 }
586 }
587
588 if (DFGPutStackSinkingPhaseInternal::verbose) {
589 dataLog("Graph after PutStack sinking:\n");
590 m_graph.dump();
591 }
592
593 return true;
594 }
595};
596
597} // anonymous namespace
598
599bool performPutStackSinking(Graph& graph)
600{
601 return runPhase<PutStackSinkingPhase>(graph);
602}
603
604} } // namespace JSC::DFG
605
606#endif // ENABLE(DFG_JIT)
607
608