Skip to content

Commit deebb52

Browse files
stereotype441Commit Queue
authored and
Commit Queue
committed
Add support for null shorting expressions to the Wolf analysis prototype.
The AST-to-IR conversion stage now handles null-shorting property accesses (both for reads and writes). This required adding the following instruction types: `eq`, `block`, and `brIf`. In order to make `eq` easier to test, support was also added for AST-to-IR conversion of testing binary expressions using `==`. The way null shorting is encoded in the IR is by issuing a `block` instruction when null shorting starts, and an `end` instruction when it terminates. Anywhere a null check appears within the null shorting expression, the null check uses a `brIf(0)` instruction to branch to the `end` in the case a `null` is found. To make it easier to keep track of when `block` and `end` instructions need to be generated, `RawIRWriter` keeps track of a count of the current nesting of control flow contsructs. In order for the interpreter to find to the appropriate `end` instruction when a branch is taken, a new scope analyzer is added. Later CLs will expand on it and use it for static analysis as well. Change-Id: I09ca34eaa900d47f7a4014cbc8db2a48a1d69e1e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/336800 Reviewed-by: Phil Quitslund <[email protected]> Commit-Queue: Paul Berry <[email protected]>
1 parent 93e7fd5 commit deebb52

12 files changed

+1031
-60
lines changed

pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,23 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
103103
_LValueTemplates dispatchLValue(Expression node) => node.accept(this)!;
104104

105105
/// Visits [node], reporting progress to [eventListener].
106-
void dispatchNode(AstNode node) {
106+
///
107+
/// If [node] has null shorting behavior, then [terminateNullShorting]
108+
/// determines how the null shorting behavior is handled. If
109+
/// [terminateNullShorting] is `true` (the default), then null shorting will
110+
/// be terminated after visiting [node], by emitting an `end` instruction;
111+
/// this means that in the case where the null short occurs, the expression
112+
/// will evaluate to `null`. If [terminateNullShorting] is `false`, then null
113+
/// shorting won't be terminated; this means that in the case where the null
114+
/// short occurs, execution of the parent node will be skipped too.
115+
void dispatchNode(AstNode node, {bool terminateNullShorting = true}) {
107116
eventListener.onEnterNode(node);
117+
var previousNestingLevel = ir.nestingLevel;
108118
var lValueTemplates = node.accept(this);
109119
// If the node was an L-value, then its visitor didn't actually perform the
110120
// read, so do that now.
111121
lValueTemplates?.simpleRead(this);
122+
if (terminateNullShorting) ir.endTo(previousNestingLevel);
112123
eventListener.onExitNode();
113124
}
114125

@@ -135,6 +146,37 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
135146
twoArguments);
136147
}
137148

149+
/// Performs a null check that is part of a null shorting expression.
150+
///
151+
/// If the value at the top of the stack is `null`, execution will branch to
152+
/// the end of the null shorting expression, and the null shorting expression
153+
/// will evaluate to `null`. Otherwise, execution will proceed normally.
154+
///
155+
/// [previousNestingLevel] is the value returned by [RawIRWriter.nestingLevel]
156+
/// at the beginning of the null shorting expression. It is used to detect
157+
/// whether null shorting has already been begun, and therefore whether a
158+
/// `block` instruction needs to be output.
159+
void nullShortingCheck({required int previousNestingLevel}) {
160+
assert(previousNestingLevel <= ir.nestingLevel);
161+
// Stack: value
162+
ir.dup();
163+
// Stack: value value
164+
ir.literal(null_);
165+
// Stack: value value null
166+
ir.eq();
167+
// Stack: value (value == null)
168+
if (previousNestingLevel == ir.nestingLevel) {
169+
// Null shorting hasn't begun yet for the containing expression, so start
170+
// it now by opening a block; the block will be ended at the end of the
171+
// null shorting expression, so it will be the branch target for null
172+
// shorts.
173+
ir.block(2, 1);
174+
// Stack: BLOCK(1) value (value == null)
175+
}
176+
ir.brIf(0);
177+
// Stack: BLOCK(1)? value
178+
}
179+
138180
Null this_() {
139181
ir.readLocal(0); // Stack: this
140182
}
@@ -157,6 +199,22 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
157199
}
158200
}
159201

202+
@override
203+
Null visitBinaryExpression(BinaryExpression node) {
204+
var tokenType = node.operator.type;
205+
switch (tokenType) {
206+
case TokenType.EQ_EQ:
207+
dispatchNode(node.leftOperand);
208+
// Stack: lhs
209+
dispatchNode(node.rightOperand);
210+
// Stack: lhs rhs
211+
ir.eq();
212+
// Stack: (lhs == rhs)
213+
default:
214+
throw UnimplementedError('TODO(paulberry): $node');
215+
}
216+
}
217+
160218
@override
161219
Null visitBlock(Block node) {
162220
var previousLocalVariableCount = ir.localVariableCount;
@@ -271,10 +329,14 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
271329

272330
@override
273331
_LValueTemplates visitPropertyAccess(PropertyAccess node) {
274-
// TODO(paulberry): handle null shorting
332+
var previousNestingLevel = ir.nestingLevel;
275333
// TODO(paulberry): handle cascades
276-
dispatchNode(node.target!);
334+
dispatchNode(node.target!, terminateNullShorting: false);
277335
// Stack: target
336+
if (node.isNullAware) {
337+
nullShortingCheck(previousNestingLevel: previousNestingLevel);
338+
}
339+
// Stack: BLOCK(1)? target
278340
return _PropertyAccessTemplates(node.propertyName);
279341
}
280342

@@ -395,8 +457,6 @@ class _LocalTemplates extends _LValueTemplates {
395457
/// subexpression values in order to read or write the L-value. These methods
396458
/// are abstract, and are defined in a derived class for each specific kind of
397459
/// L-value supported by Dart.
398-
///
399-
// TODO(paulberry): add null shorting support.
400460
sealed class _LValueTemplates {
401461
/// Outputs the IR instructions for a simple read of the L-value.
402462
///
@@ -414,8 +474,6 @@ sealed class _LValueTemplates {
414474
}
415475

416476
/// Instruction templates for converting a property access to IR.
417-
///
418-
// TODO(paulberry): handle null shorting
419477
class _PropertyAccessTemplates extends _LValueTemplates {
420478
final SimpleIdentifier property;
421479

pkg/analyzer/lib/src/wolf/ir/interpreter.dart

Lines changed: 154 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:analyzer/dart/element/type.dart';
66
import 'package:analyzer/src/wolf/ir/call_descriptor.dart';
77
import 'package:analyzer/src/wolf/ir/coded_ir.dart';
88
import 'package:analyzer/src/wolf/ir/ir.dart';
9+
import 'package:analyzer/src/wolf/ir/scope_analyzer.dart';
910
import 'package:meta/meta.dart';
1011

1112
/// Evaluates [ir], passing in [args], and returns the result.
@@ -22,14 +23,35 @@ import 'package:meta/meta.dart';
2223
/// that an instruction sequence behaves as it's expected to.
2324
@visibleForTesting
2425
Object? interpret(CodedIRContainer ir, List<Object?> args,
25-
{required CallHandler Function(CallDescriptor) callDispatcher}) {
26-
return _IRInterpreter(ir, callDispatcher: callDispatcher).run(args);
26+
{required Scopes scopes, required CallDispatcher callDispatcher}) {
27+
return _IRInterpreter(ir, scopes: scopes, callDispatcher: callDispatcher)
28+
.run(args);
2729
}
2830

2931
/// Function type invoked by [interpret] to execute a `call` instruction.
3032
typedef CallHandler = Object? Function(
3133
List<Object?> positionalArguments, Map<String, Object?> namedArguments);
3234

35+
/// Interface used by [interpret] to query the behavior of calls to external
36+
/// code.
37+
abstract interface class CallDispatcher {
38+
/// Evaluates a call to `operator==`, using virtual dispatch on [firstValue],
39+
/// and passing [secondValue] as the parameter to `operator==`.
40+
///
41+
/// In accordance with Dart semantics, this method is only called if both
42+
/// [firstValue] and [secondValue] are non-null.
43+
bool equals(Object firstValue, Object secondValue);
44+
45+
/// Looks up the function that can be used to evaluate calls to
46+
/// [callDescriptor].
47+
///
48+
/// The interpreter may invoke this method for any [CallDescriptor] in the
49+
/// IR's call descriptor table (whether or not it's invoked), and it may cache
50+
/// the results. However, it is guaranteed to call the [CallHandler] exactly
51+
/// once for each `call` instruction that is interpreted.
52+
CallHandler lookupCallDescriptor(CallDescriptor callDescriptor);
53+
}
54+
3355
/// Interpreter representation of a heap object.
3456
///
3557
/// This class should not be used for the types [int], [double], [String], or
@@ -62,29 +84,101 @@ class SoundnessError extends Error {
6284
'Soundness error at $address ($instructionString): $message';
6385
}
6486

87+
/// An entry on the control flow stack, representing a control flow construct
88+
/// (such as a `block`) that the interpreter is currently executing.
89+
class _ControlFlowStackEntry {
90+
/// The index into [_IRInterpreter.stack] before the first input to control
91+
/// flow construct.
92+
///
93+
/// This is called a "fence" because it represents the dividing line between
94+
/// stack values that belong to the instructions inside the control flow
95+
/// construct and stack values that belong to the instructions outside the
96+
/// control flow construct. If a branch instruction targets the control flow
97+
/// construct, this helps to determine which stack values should be discarded
98+
/// (see [outputCount]).
99+
final int stackFence;
100+
101+
/// The length of [_IRInterpreter.locals] at the time the control flow
102+
/// construct was entered.
103+
///
104+
/// This is called a "fence" because it represents the dividing line between
105+
/// locals that belong to the instructions inside the control flow construct
106+
/// and locals that belong to the instructions outside the control flow
107+
/// construct. If a branch instruction targets the control flow construct,
108+
/// then locals whose index is greater than equal to this value will
109+
/// automatically be released.
110+
final int localFence;
111+
112+
/// The number of outputs of the control flow construct.
113+
///
114+
/// If a branch instruction targets the control flow construct, this is the
115+
/// number of entries at the top of [_IRInterpreter.stack] that will remain on
116+
/// the stack after the branch is taken. Any other stack entries belonging to
117+
/// the instructions inside the control flow construct will be discarded (see
118+
/// [stackFence]).
119+
final int outputCount;
120+
121+
/// The scope index (as defined by [Scopes]) corresponding to the instructions
122+
/// that delimit the control flow construct.
123+
final int scope;
124+
125+
_ControlFlowStackEntry(
126+
{required this.stackFence,
127+
required this.localFence,
128+
required this.outputCount,
129+
required this.scope});
130+
}
131+
65132
class _IRInterpreter {
133+
static const keepGoing = _KeepGoing();
66134
final CodedIRContainer ir;
135+
final Scopes scopes;
136+
final CallDispatcher callDispatcher;
67137
final List<CallHandler> callHandlers;
68138
final stack = <Object?>[];
69139
final locals = <_LocalSlot>[];
140+
final controlFlowStack = <_ControlFlowStackEntry>[];
70141
var address = 1;
71142

72-
_IRInterpreter(this.ir,
73-
{required CallHandler Function(CallDescriptor) callDispatcher})
74-
: callHandlers = ir.mapCallDescriptors(callDispatcher);
143+
/// The scope index (as defined by [Scopes]) corresponding to the last begin
144+
/// instruction preceding [address].
145+
var mostRecentScope = 0;
146+
147+
_IRInterpreter(this.ir, {required this.scopes, required this.callDispatcher})
148+
: callHandlers =
149+
ir.mapCallDescriptors(callDispatcher.lookupCallDescriptor);
75150

76151
/// Performs the necessary logic for a `br`, `brIf`, or `brIndex` instruction.
77152
///
78153
/// [nesting] indicates which enclosing control flow construct is targeted by
79154
/// the branch (where 0 means the innermost).
80155
///
81-
/// The returned value is the value that should be returned to the caller.
156+
/// The returned value is either:
157+
/// - [keepGoing], indicating that interpretation should continue from the
158+
/// instruction following [address], or
159+
/// - Some other value, indicating that the code being interpreted has
160+
/// finished executing, and this value should be returned to the caller.
82161
Object? branch(int nesting) {
83-
if (nesting != 0) {
84-
throw UnimplementedError('TODO(paulberry): nonzero branch nesting');
162+
while (nesting-- > 0) {
163+
controlFlowStack.removeLast();
164+
}
165+
if (controlFlowStack.isNotEmpty) {
166+
var stackEntry = controlFlowStack.removeLast();
167+
var stackFence = stackEntry.stackFence;
168+
var outputCount = stackEntry.outputCount;
169+
var newStackLength = stackFence + outputCount;
170+
stack.setRange(stackFence, newStackLength, stack,
171+
stack.length - stackEntry.outputCount);
172+
stack.length = stackFence + outputCount;
173+
locals.length = stackEntry.localFence;
174+
var scope = stackEntry.scope;
175+
address = scopes.endAddress(scope);
176+
mostRecentScope = scopes.lastDescendant(scope);
177+
return keepGoing;
178+
} else {
179+
// Branch targets the function, so return from the code being interpreted.
180+
return stack.last;
85181
}
86-
// Branch targets the function, so return from the code being interpreted.
87-
return stack.last;
88182
}
89183

90184
Object? run(List<Object?> args) {
@@ -98,15 +192,37 @@ class _IRInterpreter {
98192
}
99193
stack.addAll(args);
100194
while (true) {
195+
assert(scopes.mostRecentScope(address - 1) == mostRecentScope);
101196
switch (ir.opcodeAt(address)) {
102197
case Opcode.alloc:
103198
var count = Opcode.alloc.decodeCount(ir, address);
104199
for (var i = 0; i < count; i++) {
105200
locals.add(_LocalSlot());
106201
}
202+
case Opcode.block:
203+
var inputCount = Opcode.block.decodeInputCount(ir, address);
204+
var outputCount = Opcode.block.decodeOutputCount(ir, address);
205+
var scope = ++mostRecentScope;
206+
assert(scopes.beginAddress(scope) == address);
207+
controlFlowStack.add(_ControlFlowStackEntry(
208+
stackFence: stack.length - inputCount,
209+
localFence: locals.length,
210+
outputCount: outputCount,
211+
scope: scope));
107212
case Opcode.br:
108213
var nesting = Opcode.br.decodeNesting(ir, address);
109-
return branch(nesting);
214+
var result = branch(nesting);
215+
if (!identical(result, keepGoing)) {
216+
return result;
217+
}
218+
case Opcode.brIf:
219+
var nesting = Opcode.brIf.decodeNesting(ir, address);
220+
if (stack.removeLast() as bool) {
221+
var result = branch(nesting);
222+
if (!identical(result, keepGoing)) {
223+
return result;
224+
}
225+
}
110226
case Opcode.call:
111227
var argumentNames = ir.decodeArgumentNames(
112228
Opcode.call.decodeArgumentNames(ir, address));
@@ -130,8 +246,27 @@ class _IRInterpreter {
130246
case Opcode.dup:
131247
stack.add(stack.last);
132248
case Opcode.end:
133-
assert(stack.length == 1);
134-
return stack.last;
249+
if (controlFlowStack.isEmpty) {
250+
assert(stack.length == 1);
251+
return stack.last;
252+
} else {
253+
var stackEntry = controlFlowStack.last;
254+
assert(
255+
stack.length == stackEntry.stackFence + stackEntry.outputCount);
256+
assert(locals.length == stackEntry.localFence);
257+
// Continue with the code following the block.
258+
controlFlowStack.removeLast();
259+
}
260+
case Opcode.eq:
261+
var secondValue = stack.removeLast();
262+
var firstValue = stack.removeLast();
263+
if (firstValue == null) {
264+
stack.add(null == secondValue);
265+
} else if (secondValue == null) {
266+
stack.add(false);
267+
} else {
268+
stack.add(callDispatcher.equals(firstValue, secondValue));
269+
}
135270
case Opcode.literal:
136271
var value = Opcode.literal.decodeValue(ir, address);
137272
stack.add(ir.decodeLiteral(value));
@@ -173,6 +308,12 @@ class _IRInterpreter {
173308
message: message);
174309
}
175310

311+
/// Sentinel value used by [_IRInterpreter.branch] to indicate that the
312+
/// interpreter should keep executing instructions.
313+
class _KeepGoing {
314+
const _KeepGoing();
315+
}
316+
176317
/// Storage for a single local variable.
177318
class _LocalSlot {
178319
/// The contents of the local variable, or [_NoValue] if the slot is empty.

0 commit comments

Comments
 (0)