Skip to content

Commit 4bcd8c2

Browse files
mralephCommit Queue
authored andcommitted
[vm/compiler] Prune SSA during construction using liveness
Only insert Phi at JoinEntry and Parameter at CatchBlockEntry if corresponding variable is alive into the block. This keeps insertion consistent with environment pruning logic employed by RenameRecursive: which prunes dead variables out environments when entering the block based on contents of the live_in set. Previously we would insert Parameter instructions for dead variables which lead to an incorrect (unsound) set of catch entry moves generated by the backend due to an inconsistency between Parameter insertion and environment pruning logic and the fact that catch entry moves generator would simply ignore all moves if the source of the move was an optimized_out (aka constant_dead) marker. The following situation was possible: if the variable was assigned inside the try and partially alive inside of it (e.g. we had blocks where it was alive and blocks where it was not) then we would end up inserting Parameter for it, but some of the exceptional edges (corresponding to blocks where the variable was dead) would carry optimized_out value into that Parameter. However `CatchEntryMoveFor` would not record these moves leading to uninitialized garbage arriving on those exceptional edges instead. Note that Phis were not susceptible to the same issue as Parameters because regalloc / backend would not treat optimized_out value specially. This change converts silent treatment of optimized_out in the catch entry moves generation into a release assert instead. Reviewing other uses of constant_dead in the compiler revealed a bug in canonicalization rule for `o.runtimeType` which did not take exceptional edges into account. We add a regression test for that and fix that issue as well. Fixes #59822 TEST=vm/dart/regress_59822,vm/dart/regress_runtime_type_in_trycatch Cq-Include-Trybots: luci.dart.try:vm-aot-linux-debug-x64-try,vm-aot-linux-debug-x64c-try,vm-aot-linux-product-x64-try,vm-aot-linux-release-arm64-try,vm-aot-linux-release-simarm_x64-try,vm-aot-linux-release-x64-try Change-Id: I8dc397cb98d84048a1791b9dd6baa7586a2688c6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/403583 Reviewed-by: Alexander Aprelev <[email protected]> Commit-Queue: Slava Egorov <[email protected]>
1 parent c95568a commit 4bcd8c2

File tree

7 files changed

+204
-35
lines changed

7 files changed

+204
-35
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
//
5+
// Regression test for crash https://github.com/dart-lang/sdk/issues/59822
6+
//
7+
// VMOptions= --optimization_counter_threshold=1 --deterministic
8+
9+
@pragma('vm:never-inline')
10+
void use(Object? v) {}
11+
12+
@pragma('vm:never-inline')
13+
void foo(bool a, bool b) {
14+
String v = "a";
15+
try {
16+
if (a) {
17+
if (b) {
18+
// Live assignment which flows out of the block.
19+
v = "b";
20+
}
21+
// Last use of `v` the variable is dead after this point.
22+
use(v); // (1)
23+
}
24+
use(0); // (2)
25+
} catch (e) {
26+
// `v` is not live into the catch. Notice that different values of `v`
27+
// will flow on exception edges: from (1) phi("a", "b") flows but from
28+
// (2) optimized out value flows because `v` is dead at that point and
29+
// pruned from the environment. This means TCO pass should not be able
30+
// to remove Parameter corresponding to v by itself - but such
31+
// Parameter should not be inserted by SSA construction either.
32+
}
33+
}
34+
35+
void main() {
36+
foo(true, true);
37+
foo(true, true);
38+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
//
5+
// Regression test which verifies that we don't accidentally eliminate
6+
// runtimeType invocation which flows into a catch block entry parameter.
7+
8+
import 'package:expect/expect.dart';
9+
10+
class A {}
11+
12+
@pragma('vm:never-inline')
13+
void foo(Object? v) {
14+
Type t = String;
15+
try {
16+
t = v.runtimeType;
17+
if (v is A) {
18+
throw 'goto catch';
19+
}
20+
} catch (e) {
21+
Expect.equals(A, t);
22+
return;
23+
}
24+
}
25+
26+
void main() {
27+
foo('hi');
28+
foo(A());
29+
}

runtime/vm/compiler/backend/flow_graph.cc

Lines changed: 103 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,10 +1013,9 @@ void FlowGraph::ComputeSSA(
10131013

10141014
GrowableArray<PhiInstr*> live_phis;
10151015

1016-
InsertCatchBlockParams(preorder_, variable_liveness.ComputeAssignedVars());
1016+
InsertCatchBlockParams(preorder_, variable_liveness);
10171017
// Inserted catch block parameters add to the set of assigned variables.
1018-
InsertPhis(preorder_, variable_liveness.ComputeAssignedVars(),
1019-
dominance_frontier, &live_phis);
1018+
InsertPhis(preorder_, variable_liveness, dominance_frontier, &live_phis);
10201019

10211020
// Rename uses to reference inserted phis where appropriate.
10221021
// Collect phis that reach a non-environment use.
@@ -1144,9 +1143,11 @@ void FlowGraph::CompressPath(intptr_t start_index,
11441143
}
11451144

11461145
void FlowGraph::InsertPhis(const GrowableArray<BlockEntryInstr*>& preorder,
1147-
const GrowableArray<BitVector*>& assigned_vars,
1146+
VariableLivenessAnalysis& variable_liveness,
11481147
const GrowableArray<BitVector*>& dom_frontier,
11491148
GrowableArray<PhiInstr*>* live_phis) {
1149+
const auto& assigned_vars = variable_liveness.ComputeAssignedVars();
1150+
11501151
const intptr_t block_count = preorder.length();
11511152
// Map preorder block number to the highest variable index that has a phi
11521153
// in that block. Use it to avoid inserting multiple phis for the same
@@ -1183,6 +1184,14 @@ void FlowGraph::InsertPhis(const GrowableArray<BlockEntryInstr*>& preorder,
11831184
!it.Done(); it.Advance()) {
11841185
int index = it.Current();
11851186
if (has_already[index] < var_index) {
1187+
if (!always_live && !variable_liveness.GetLiveInSet(preorder[index])
1188+
->Contains(var_index)) {
1189+
// Avoid inserting Phis for variables which are not
1190+
// live into the join block. Such Phis instructions can not
1191+
// have real uses: they correspond to dead variables which can
1192+
// be pruned from the environment (e.g. by RenameRecursive).
1193+
continue;
1194+
}
11861195
JoinEntryInstr* join = preorder[index]->AsJoinEntry();
11871196
ASSERT(join != nullptr);
11881197
PhiInstr* phi = join->InsertPhi(
@@ -1243,25 +1252,80 @@ void FlowGraph::AddCatchEntryParameter(intptr_t var_index,
12431252

12441253
void FlowGraph::InsertCatchBlockParams(
12451254
const GrowableArray<BlockEntryInstr*>& preorder,
1246-
const GrowableArray<BitVector*>& assigned_vars) {
1255+
VariableLivenessAnalysis& variable_liveness) {
1256+
// Parameters at CatchBlockEntry serve the same function as Phis at Joins.
1257+
//
1258+
// They represent a merge between versions of a variable which arrive on
1259+
// different control flow edges. For Joins these are explicit control flow
1260+
// edges between blocks, for CatchBlockEntry these are implicit edges
1261+
// from throwing instructions covered by this catch block.
1262+
// Consider two blocks A and B. If the block A has an assignment into a
1263+
// variable V and there is a path from A to B: B0 = A, B1, B2, ..., Bn = B
1264+
// such that B{i+1} is a successor of B{i} and B{i} does not dominate B{i+1}
1265+
// then B needs a Phi function or Parameter corresponding
1266+
// to the variable V. (for the purposes of shortening the definition we
1267+
// consider catch entry to be an immediate successor of a block
1268+
// that it covers).
1269+
//
1270+
// Now let us consider two distinct cases:
1271+
// 1) normal flow graph for a function
1272+
// 2) OSR relinked flow graph.
1273+
//
1274+
// In the first case we have the property that if a path B0, ..., Bn ends up
1275+
// in the CatchEntry and none of Bi is CatchEntry then this path either fully
1276+
// contained within the corresponding try block or crosses into the try
1277+
// block via TryEntry. This property means that if a block A contains an
1278+
// assignment to a variable V then this assignment can only induce
1279+
// a Parameter instruction in the surrounding catches (if any).
1280+
//
1281+
// This property does hold for the OSR relinked graph because all TryEntry
1282+
// blocks are lifted up to the graph entry. That means there can exist paths
1283+
// from a block with an assignment to a variable which itself is not
1284+
// covered by a try/catch to the catch block which do not cross through
1285+
// the try entry. This means when inserting Parameter instructions for OSR
1286+
// relinked graph we can't limit insertion using a list of variables assigned
1287+
// inside blocks covered by catch and instead we should insert parameters for
1288+
// all live variables.
1289+
//
1290+
// Note: if we computed dominance frontier treating catch block entries as
1291+
// successors to blocks they cover then we could combine Parameter and Phi
1292+
// insertion into a single pass algorithm and remove distinction between
1293+
// OSR and non-OSR case.
12471294
intptr_t fixed_slot_count = Utils::Maximum(
12481295
graph_entry_->fixed_slot_count(),
12491296
(IsCompiledForOsr() ? osr_variable_count() : variable_count()) -
12501297
num_direct_parameters());
12511298
if (IsCompiledForOsr()) {
1252-
for (intptr_t i = 0; i < try_entries_.length(); i++) {
1253-
if (try_entries_[i] == nullptr) {
1299+
for (auto try_entry : try_entries_) {
1300+
if (try_entry == nullptr) {
12541301
continue;
12551302
}
1256-
CatchBlockEntryInstr* catch_entry = try_entries_[i]->catch_target();
1257-
// Will insert parameters for all variables
1258-
for (intptr_t i = 0, n = variable_count(); i < n; ++i) {
1259-
// Local variables will arrive on the stack while exception and
1260-
// stack trace will be passed in fixed registers.
1261-
AddCatchEntryParameter(i, catch_entry);
1303+
auto catch_entry = try_entry->catch_target();
1304+
const auto exception_var_index =
1305+
EnvIndex(catch_entry->raw_exception_var());
1306+
const auto stacktrace_var_index =
1307+
EnvIndex(catch_entry->raw_stacktrace_var());
1308+
for (intptr_t var_index = 0, n = variable_count(); var_index < n;
1309+
++var_index) {
1310+
const bool always_live = !FLAG_prune_dead_locals ||
1311+
IsImmortalVariable(var_index) ||
1312+
var_index == exception_var_index ||
1313+
var_index == stacktrace_var_index;
1314+
1315+
if (!always_live &&
1316+
!variable_liveness.GetLiveInSet(catch_entry)->Contains(var_index)) {
1317+
// Avoid inserting Parameter for variables which are not
1318+
// live into the catch block. Such Parameter instructions can not
1319+
// have real uses: they correspond to dead variables which can
1320+
// be pruned from the environment (e.g. by RenameRecursive).
1321+
continue;
1322+
}
1323+
AddCatchEntryParameter(var_index, catch_entry);
12621324
}
12631325
}
12641326
} else {
1327+
const auto& assigned_vars = variable_liveness.ComputeAssignedVars();
1328+
12651329
const intptr_t block_count = preorder.length();
12661330
// Map preorder block number to the highest variable index that has an
12671331
// assignment in that block. Use it to avoid inserting multiple parameters
@@ -1276,11 +1340,11 @@ void FlowGraph::InsertCatchBlockParams(
12761340
has_already.EnsureLength(block_count, -1);
12771341
work.EnsureLength(block_count, -1);
12781342

1279-
for (intptr_t i = 0; i < try_entries_.length(); i++) {
1280-
if (try_entries_[i] == nullptr) {
1343+
for (auto try_entry : try_entries_) {
1344+
if (try_entry == nullptr) {
12811345
continue;
12821346
}
1283-
CatchBlockEntryInstr* catch_block = try_entries_[i]->catch_target();
1347+
auto catch_block = try_entry->catch_target();
12841348

12851349
AddCatchEntryParameter(EnvIndex(catch_block->raw_exception_var()),
12861350
catch_block);
@@ -1295,6 +1359,9 @@ void FlowGraph::InsertCatchBlockParams(
12951359
GrowableArray<BlockEntryInstr*> worklist;
12961360
// Look at every variable in turn.
12971361
for (intptr_t var_index = 0; var_index < variable_count(); ++var_index) {
1362+
const bool always_live =
1363+
!FLAG_prune_dead_locals || IsImmortalVariable(var_index);
1364+
12981365
// Add to the worklist each block containing an assignment to this
12991366
// variable.
13001367
for (intptr_t block_index = 0; block_index < block_count; ++block_index) {
@@ -1312,6 +1379,14 @@ void FlowGraph::InsertCatchBlockParams(
13121379
GetCatchBlockByTryIndex(try_index);
13131380
intptr_t catch_entry_index = catch_entry->preorder_number();
13141381
if (has_already[catch_entry_index] < var_index) {
1382+
if (!always_live && !variable_liveness.GetLiveInSet(catch_entry)
1383+
->Contains(var_index)) {
1384+
// Avoid inserting Parameter for variables which are not
1385+
// live into the catch block. Such Parameter instructions can not
1386+
// have real uses: they correspond to dead variables which can
1387+
// be pruned from the environment (e.g. by RenameRecursive).
1388+
continue;
1389+
}
13151390
AddCatchEntryParameter(var_index, catch_entry);
13161391
has_already[catch_entry_index] = var_index;
13171392
if (work[catch_entry_index] < var_index) {
@@ -1590,17 +1665,18 @@ void FlowGraph::RenameRecursive(
15901665
PopulateEnvironmentFromCatchEntry(catch_entry, env);
15911666
}
15921667

1593-
if (!block_entry->IsGraphEntry() &&
1594-
!block_entry->IsBlockEntryWithInitialDefs()) {
1595-
// Prune non-live variables at block entry by replacing their environment
1596-
// slots with null.
1597-
BitVector* live_in = variable_liveness->GetLiveInSet(block_entry);
1598-
for (intptr_t i = 0; i < variable_count(); i++) {
1599-
// TODO(fschneider): Make sure that live_in always contains the
1600-
// CurrentContext variable to avoid the special case here.
1601-
if (FLAG_prune_dead_locals && !live_in->Contains(i) &&
1602-
!IsImmortalVariable(i)) {
1603-
(*env)[i] = constant_dead();
1668+
if (FLAG_prune_dead_locals) {
1669+
if (!block_entry->IsBlockEntryWithInitialDefs() ||
1670+
block_entry->IsCatchBlockEntry()) {
1671+
// Prune non-live variables at block entry by replacing their environment
1672+
// slots with null.
1673+
BitVector* live_in = variable_liveness->GetLiveInSet(block_entry);
1674+
for (intptr_t i = 0; i < variable_count(); i++) {
1675+
// TODO(fschneider): Make sure that live_in always contains the
1676+
// CurrentContext variable to avoid the special case here.
1677+
if (!live_in->Contains(i) && !IsImmortalVariable(i)) {
1678+
(*env)[i] = constant_dead();
1679+
}
16041680
}
16051681
}
16061682
}

runtime/vm/compiler/backend/flow_graph.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,13 +690,13 @@ class FlowGraph : public ZoneAllocated {
690690
void AttachEnvironment(Instruction* instr, GrowableArray<Definition*>* env);
691691

692692
void InsertPhis(const GrowableArray<BlockEntryInstr*>& preorder,
693-
const GrowableArray<BitVector*>& assigned_vars,
693+
VariableLivenessAnalysis& variable_liveness,
694694
const GrowableArray<BitVector*>& dom_frontier,
695695
GrowableArray<PhiInstr*>* live_phis);
696696
void AddCatchEntryParameter(intptr_t var_index,
697697
CatchBlockEntryInstr* catch_entry);
698698
void InsertCatchBlockParams(const GrowableArray<BlockEntryInstr*>& preorder,
699-
const GrowableArray<BitVector*>& assigned_vars);
699+
VariableLivenessAnalysis& variable_liveness);
700700

701701
void RemoveDeadPhis(GrowableArray<PhiInstr*>* live_phis);
702702

runtime/vm/compiler/backend/flow_graph_compiler.cc

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,10 +365,7 @@ static CatchEntryMove CatchEntryMoveFor(compiler::Assembler* assembler,
365365
const Location& src,
366366
intptr_t dst_index) {
367367
if (src.IsConstant()) {
368-
// Skip dead locations.
369-
if (src.constant().ptr() == Object::optimized_out().ptr()) {
370-
return CatchEntryMove();
371-
}
368+
RELEASE_ASSERT(src.constant().ptr() != Object::optimized_out().ptr());
372369
const intptr_t pool_index =
373370
assembler->object_pool_builder().FindObject(src.constant());
374371
return CatchEntryMove::FromSlot(CatchEntryMove::SourceKind::kConstant,

runtime/vm/compiler/backend/il.cc

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5721,6 +5721,31 @@ static Definition* CanonicalizeStringInterpolateSingle(StaticCallInstr* call,
57215721
return call;
57225722
}
57235723

5724+
static bool CanFlowIntoCatch(FlowGraph* flow_graph, Definition* defn) {
5725+
if (flow_graph->try_entries().is_empty()) {
5726+
// No try/catch blocks.
5727+
return false;
5728+
}
5729+
5730+
if (defn->env_use_list() == nullptr) {
5731+
// No uses in environments.
5732+
return false;
5733+
}
5734+
5735+
for (auto use : defn->environment_uses()) {
5736+
if (use->instruction()->MayThrow() &&
5737+
use->instruction()->GetBlock()->InsideTryBlock()) {
5738+
// Conservatively assume that this value might end up in the
5739+
// corresponding catch. Ideally we would like to check if
5740+
// there is a corresponding catch parameter, but there is no
5741+
// straightforward way to do that.
5742+
return true;
5743+
}
5744+
}
5745+
5746+
return false;
5747+
}
5748+
57245749
Definition* StaticCallInstr::Canonicalize(FlowGraph* flow_graph) {
57255750
auto& compiler_state = CompilerState::Current();
57265751

@@ -5760,9 +5785,9 @@ Definition* StaticCallInstr::Canonicalize(FlowGraph* flow_graph) {
57605785
}
57615786

57625787
if (kind == MethodRecognizer::kObjectRuntimeType) {
5763-
if (input_use_list() == nullptr) {
5788+
if (input_use_list() == nullptr && !CanFlowIntoCatch(flow_graph, this)) {
57645789
// This function has only environment uses. In precompiled mode it is
5765-
// fine to remove it - because we will never deoptimize.
5790+
// fine to remove it if the value can't flow into the catch block entry.
57665791
return flow_graph->constant_dead();
57675792
}
57685793
}

runtime/vm/compiler/backend/il.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2653,6 +2653,10 @@ class Definition : public Instruction {
26532653
return ValueListIterable(input_use_list_);
26542654
}
26552655

2656+
ValueListIterable environment_uses() const {
2657+
return ValueListIterable(env_use_list_);
2658+
}
2659+
26562660
void AddInputUse(Value* value) { Value::AddToList(value, &input_use_list_); }
26572661
void AddEnvUse(Value* value) { Value::AddToList(value, &env_use_list_); }
26582662

0 commit comments

Comments
 (0)