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 <pquitslund@google.com> Commit-Queue: Paul Berry <paulberry@google.com>
This commit is contained in:
@@ -103,12 +103,23 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
|
||||
_LValueTemplates dispatchLValue(Expression node) => node.accept(this)!;
|
||||
|
||||
/// Visits [node], reporting progress to [eventListener].
|
||||
void dispatchNode(AstNode node) {
|
||||
///
|
||||
/// If [node] has null shorting behavior, then [terminateNullShorting]
|
||||
/// determines how the null shorting behavior is handled. If
|
||||
/// [terminateNullShorting] is `true` (the default), then null shorting will
|
||||
/// be terminated after visiting [node], by emitting an `end` instruction;
|
||||
/// this means that in the case where the null short occurs, the expression
|
||||
/// will evaluate to `null`. If [terminateNullShorting] is `false`, then null
|
||||
/// shorting won't be terminated; this means that in the case where the null
|
||||
/// short occurs, execution of the parent node will be skipped too.
|
||||
void dispatchNode(AstNode node, {bool terminateNullShorting = true}) {
|
||||
eventListener.onEnterNode(node);
|
||||
var previousNestingLevel = ir.nestingLevel;
|
||||
var lValueTemplates = node.accept(this);
|
||||
// If the node was an L-value, then its visitor didn't actually perform the
|
||||
// read, so do that now.
|
||||
lValueTemplates?.simpleRead(this);
|
||||
if (terminateNullShorting) ir.endTo(previousNestingLevel);
|
||||
eventListener.onExitNode();
|
||||
}
|
||||
|
||||
@@ -135,6 +146,37 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
|
||||
twoArguments);
|
||||
}
|
||||
|
||||
/// Performs a null check that is part of a null shorting expression.
|
||||
///
|
||||
/// If the value at the top of the stack is `null`, execution will branch to
|
||||
/// the end of the null shorting expression, and the null shorting expression
|
||||
/// will evaluate to `null`. Otherwise, execution will proceed normally.
|
||||
///
|
||||
/// [previousNestingLevel] is the value returned by [RawIRWriter.nestingLevel]
|
||||
/// at the beginning of the null shorting expression. It is used to detect
|
||||
/// whether null shorting has already been begun, and therefore whether a
|
||||
/// `block` instruction needs to be output.
|
||||
void nullShortingCheck({required int previousNestingLevel}) {
|
||||
assert(previousNestingLevel <= ir.nestingLevel);
|
||||
// Stack: value
|
||||
ir.dup();
|
||||
// Stack: value value
|
||||
ir.literal(null_);
|
||||
// Stack: value value null
|
||||
ir.eq();
|
||||
// Stack: value (value == null)
|
||||
if (previousNestingLevel == ir.nestingLevel) {
|
||||
// Null shorting hasn't begun yet for the containing expression, so start
|
||||
// it now by opening a block; the block will be ended at the end of the
|
||||
// null shorting expression, so it will be the branch target for null
|
||||
// shorts.
|
||||
ir.block(2, 1);
|
||||
// Stack: BLOCK(1) value (value == null)
|
||||
}
|
||||
ir.brIf(0);
|
||||
// Stack: BLOCK(1)? value
|
||||
}
|
||||
|
||||
Null this_() {
|
||||
ir.readLocal(0); // Stack: this
|
||||
}
|
||||
@@ -157,6 +199,22 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Null visitBinaryExpression(BinaryExpression node) {
|
||||
var tokenType = node.operator.type;
|
||||
switch (tokenType) {
|
||||
case TokenType.EQ_EQ:
|
||||
dispatchNode(node.leftOperand);
|
||||
// Stack: lhs
|
||||
dispatchNode(node.rightOperand);
|
||||
// Stack: lhs rhs
|
||||
ir.eq();
|
||||
// Stack: (lhs == rhs)
|
||||
default:
|
||||
throw UnimplementedError('TODO(paulberry): $node');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Null visitBlock(Block node) {
|
||||
var previousLocalVariableCount = ir.localVariableCount;
|
||||
@@ -271,10 +329,14 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
|
||||
|
||||
@override
|
||||
_LValueTemplates visitPropertyAccess(PropertyAccess node) {
|
||||
// TODO(paulberry): handle null shorting
|
||||
var previousNestingLevel = ir.nestingLevel;
|
||||
// TODO(paulberry): handle cascades
|
||||
dispatchNode(node.target!);
|
||||
dispatchNode(node.target!, terminateNullShorting: false);
|
||||
// Stack: target
|
||||
if (node.isNullAware) {
|
||||
nullShortingCheck(previousNestingLevel: previousNestingLevel);
|
||||
}
|
||||
// Stack: BLOCK(1)? target
|
||||
return _PropertyAccessTemplates(node.propertyName);
|
||||
}
|
||||
|
||||
@@ -395,8 +457,6 @@ class _LocalTemplates extends _LValueTemplates {
|
||||
/// subexpression values in order to read or write the L-value. These methods
|
||||
/// are abstract, and are defined in a derived class for each specific kind of
|
||||
/// L-value supported by Dart.
|
||||
///
|
||||
// TODO(paulberry): add null shorting support.
|
||||
sealed class _LValueTemplates {
|
||||
/// Outputs the IR instructions for a simple read of the L-value.
|
||||
///
|
||||
@@ -414,8 +474,6 @@ sealed class _LValueTemplates {
|
||||
}
|
||||
|
||||
/// Instruction templates for converting a property access to IR.
|
||||
///
|
||||
// TODO(paulberry): handle null shorting
|
||||
class _PropertyAccessTemplates extends _LValueTemplates {
|
||||
final SimpleIdentifier property;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:analyzer/src/wolf/ir/call_descriptor.dart';
|
||||
import 'package:analyzer/src/wolf/ir/coded_ir.dart';
|
||||
import 'package:analyzer/src/wolf/ir/ir.dart';
|
||||
import 'package:analyzer/src/wolf/ir/scope_analyzer.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Evaluates [ir], passing in [args], and returns the result.
|
||||
@@ -22,14 +23,35 @@ import 'package:meta/meta.dart';
|
||||
/// that an instruction sequence behaves as it's expected to.
|
||||
@visibleForTesting
|
||||
Object? interpret(CodedIRContainer ir, List<Object?> args,
|
||||
{required CallHandler Function(CallDescriptor) callDispatcher}) {
|
||||
return _IRInterpreter(ir, callDispatcher: callDispatcher).run(args);
|
||||
{required Scopes scopes, required CallDispatcher callDispatcher}) {
|
||||
return _IRInterpreter(ir, scopes: scopes, callDispatcher: callDispatcher)
|
||||
.run(args);
|
||||
}
|
||||
|
||||
/// Function type invoked by [interpret] to execute a `call` instruction.
|
||||
typedef CallHandler = Object? Function(
|
||||
List<Object?> positionalArguments, Map<String, Object?> namedArguments);
|
||||
|
||||
/// Interface used by [interpret] to query the behavior of calls to external
|
||||
/// code.
|
||||
abstract interface class CallDispatcher {
|
||||
/// Evaluates a call to `operator==`, using virtual dispatch on [firstValue],
|
||||
/// and passing [secondValue] as the parameter to `operator==`.
|
||||
///
|
||||
/// In accordance with Dart semantics, this method is only called if both
|
||||
/// [firstValue] and [secondValue] are non-null.
|
||||
bool equals(Object firstValue, Object secondValue);
|
||||
|
||||
/// Looks up the function that can be used to evaluate calls to
|
||||
/// [callDescriptor].
|
||||
///
|
||||
/// The interpreter may invoke this method for any [CallDescriptor] in the
|
||||
/// IR's call descriptor table (whether or not it's invoked), and it may cache
|
||||
/// the results. However, it is guaranteed to call the [CallHandler] exactly
|
||||
/// once for each `call` instruction that is interpreted.
|
||||
CallHandler lookupCallDescriptor(CallDescriptor callDescriptor);
|
||||
}
|
||||
|
||||
/// Interpreter representation of a heap object.
|
||||
///
|
||||
/// This class should not be used for the types [int], [double], [String], or
|
||||
@@ -62,29 +84,101 @@ class SoundnessError extends Error {
|
||||
'Soundness error at $address ($instructionString): $message';
|
||||
}
|
||||
|
||||
/// An entry on the control flow stack, representing a control flow construct
|
||||
/// (such as a `block`) that the interpreter is currently executing.
|
||||
class _ControlFlowStackEntry {
|
||||
/// The index into [_IRInterpreter.stack] before the first input to control
|
||||
/// flow construct.
|
||||
///
|
||||
/// This is called a "fence" because it represents the dividing line between
|
||||
/// stack values that belong to the instructions inside the control flow
|
||||
/// construct and stack values that belong to the instructions outside the
|
||||
/// control flow construct. If a branch instruction targets the control flow
|
||||
/// construct, this helps to determine which stack values should be discarded
|
||||
/// (see [outputCount]).
|
||||
final int stackFence;
|
||||
|
||||
/// The length of [_IRInterpreter.locals] at the time the control flow
|
||||
/// construct was entered.
|
||||
///
|
||||
/// This is called a "fence" because it represents the dividing line between
|
||||
/// locals that belong to the instructions inside the control flow construct
|
||||
/// and locals that belong to the instructions outside the control flow
|
||||
/// construct. If a branch instruction targets the control flow construct,
|
||||
/// then locals whose index is greater than equal to this value will
|
||||
/// automatically be released.
|
||||
final int localFence;
|
||||
|
||||
/// The number of outputs of the control flow construct.
|
||||
///
|
||||
/// If a branch instruction targets the control flow construct, this is the
|
||||
/// number of entries at the top of [_IRInterpreter.stack] that will remain on
|
||||
/// the stack after the branch is taken. Any other stack entries belonging to
|
||||
/// the instructions inside the control flow construct will be discarded (see
|
||||
/// [stackFence]).
|
||||
final int outputCount;
|
||||
|
||||
/// The scope index (as defined by [Scopes]) corresponding to the instructions
|
||||
/// that delimit the control flow construct.
|
||||
final int scope;
|
||||
|
||||
_ControlFlowStackEntry(
|
||||
{required this.stackFence,
|
||||
required this.localFence,
|
||||
required this.outputCount,
|
||||
required this.scope});
|
||||
}
|
||||
|
||||
class _IRInterpreter {
|
||||
static const keepGoing = _KeepGoing();
|
||||
final CodedIRContainer ir;
|
||||
final Scopes scopes;
|
||||
final CallDispatcher callDispatcher;
|
||||
final List<CallHandler> callHandlers;
|
||||
final stack = <Object?>[];
|
||||
final locals = <_LocalSlot>[];
|
||||
final controlFlowStack = <_ControlFlowStackEntry>[];
|
||||
var address = 1;
|
||||
|
||||
_IRInterpreter(this.ir,
|
||||
{required CallHandler Function(CallDescriptor) callDispatcher})
|
||||
: callHandlers = ir.mapCallDescriptors(callDispatcher);
|
||||
/// The scope index (as defined by [Scopes]) corresponding to the last begin
|
||||
/// instruction preceding [address].
|
||||
var mostRecentScope = 0;
|
||||
|
||||
_IRInterpreter(this.ir, {required this.scopes, required this.callDispatcher})
|
||||
: callHandlers =
|
||||
ir.mapCallDescriptors(callDispatcher.lookupCallDescriptor);
|
||||
|
||||
/// Performs the necessary logic for a `br`, `brIf`, or `brIndex` instruction.
|
||||
///
|
||||
/// [nesting] indicates which enclosing control flow construct is targeted by
|
||||
/// the branch (where 0 means the innermost).
|
||||
///
|
||||
/// The returned value is the value that should be returned to the caller.
|
||||
/// The returned value is either:
|
||||
/// - [keepGoing], indicating that interpretation should continue from the
|
||||
/// instruction following [address], or
|
||||
/// - Some other value, indicating that the code being interpreted has
|
||||
/// finished executing, and this value should be returned to the caller.
|
||||
Object? branch(int nesting) {
|
||||
if (nesting != 0) {
|
||||
throw UnimplementedError('TODO(paulberry): nonzero branch nesting');
|
||||
while (nesting-- > 0) {
|
||||
controlFlowStack.removeLast();
|
||||
}
|
||||
if (controlFlowStack.isNotEmpty) {
|
||||
var stackEntry = controlFlowStack.removeLast();
|
||||
var stackFence = stackEntry.stackFence;
|
||||
var outputCount = stackEntry.outputCount;
|
||||
var newStackLength = stackFence + outputCount;
|
||||
stack.setRange(stackFence, newStackLength, stack,
|
||||
stack.length - stackEntry.outputCount);
|
||||
stack.length = stackFence + outputCount;
|
||||
locals.length = stackEntry.localFence;
|
||||
var scope = stackEntry.scope;
|
||||
address = scopes.endAddress(scope);
|
||||
mostRecentScope = scopes.lastDescendant(scope);
|
||||
return keepGoing;
|
||||
} else {
|
||||
// Branch targets the function, so return from the code being interpreted.
|
||||
return stack.last;
|
||||
}
|
||||
// Branch targets the function, so return from the code being interpreted.
|
||||
return stack.last;
|
||||
}
|
||||
|
||||
Object? run(List<Object?> args) {
|
||||
@@ -98,15 +192,37 @@ class _IRInterpreter {
|
||||
}
|
||||
stack.addAll(args);
|
||||
while (true) {
|
||||
assert(scopes.mostRecentScope(address - 1) == mostRecentScope);
|
||||
switch (ir.opcodeAt(address)) {
|
||||
case Opcode.alloc:
|
||||
var count = Opcode.alloc.decodeCount(ir, address);
|
||||
for (var i = 0; i < count; i++) {
|
||||
locals.add(_LocalSlot());
|
||||
}
|
||||
case Opcode.block:
|
||||
var inputCount = Opcode.block.decodeInputCount(ir, address);
|
||||
var outputCount = Opcode.block.decodeOutputCount(ir, address);
|
||||
var scope = ++mostRecentScope;
|
||||
assert(scopes.beginAddress(scope) == address);
|
||||
controlFlowStack.add(_ControlFlowStackEntry(
|
||||
stackFence: stack.length - inputCount,
|
||||
localFence: locals.length,
|
||||
outputCount: outputCount,
|
||||
scope: scope));
|
||||
case Opcode.br:
|
||||
var nesting = Opcode.br.decodeNesting(ir, address);
|
||||
return branch(nesting);
|
||||
var result = branch(nesting);
|
||||
if (!identical(result, keepGoing)) {
|
||||
return result;
|
||||
}
|
||||
case Opcode.brIf:
|
||||
var nesting = Opcode.brIf.decodeNesting(ir, address);
|
||||
if (stack.removeLast() as bool) {
|
||||
var result = branch(nesting);
|
||||
if (!identical(result, keepGoing)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
case Opcode.call:
|
||||
var argumentNames = ir.decodeArgumentNames(
|
||||
Opcode.call.decodeArgumentNames(ir, address));
|
||||
@@ -130,8 +246,27 @@ class _IRInterpreter {
|
||||
case Opcode.dup:
|
||||
stack.add(stack.last);
|
||||
case Opcode.end:
|
||||
assert(stack.length == 1);
|
||||
return stack.last;
|
||||
if (controlFlowStack.isEmpty) {
|
||||
assert(stack.length == 1);
|
||||
return stack.last;
|
||||
} else {
|
||||
var stackEntry = controlFlowStack.last;
|
||||
assert(
|
||||
stack.length == stackEntry.stackFence + stackEntry.outputCount);
|
||||
assert(locals.length == stackEntry.localFence);
|
||||
// Continue with the code following the block.
|
||||
controlFlowStack.removeLast();
|
||||
}
|
||||
case Opcode.eq:
|
||||
var secondValue = stack.removeLast();
|
||||
var firstValue = stack.removeLast();
|
||||
if (firstValue == null) {
|
||||
stack.add(null == secondValue);
|
||||
} else if (secondValue == null) {
|
||||
stack.add(false);
|
||||
} else {
|
||||
stack.add(callDispatcher.equals(firstValue, secondValue));
|
||||
}
|
||||
case Opcode.literal:
|
||||
var value = Opcode.literal.decodeValue(ir, address);
|
||||
stack.add(ir.decodeLiteral(value));
|
||||
@@ -173,6 +308,12 @@ class _IRInterpreter {
|
||||
message: message);
|
||||
}
|
||||
|
||||
/// Sentinel value used by [_IRInterpreter.branch] to indicate that the
|
||||
/// interpreter should keep executing instructions.
|
||||
class _KeepGoing {
|
||||
const _KeepGoing();
|
||||
}
|
||||
|
||||
/// Storage for a single local variable.
|
||||
class _LocalSlot {
|
||||
/// The contents of the local variable, or [_NoValue] if the slot is empty.
|
||||
|
||||
@@ -258,8 +258,12 @@ class RawIRWriter with _RawIRWriterMixin {
|
||||
|
||||
int _localVariableCount = 0;
|
||||
|
||||
int _nestingLevel = 0;
|
||||
|
||||
int get localVariableCount => _localVariableCount;
|
||||
|
||||
int get nestingLevel => _nestingLevel;
|
||||
|
||||
int get nextInstructionAddress => _opcodes.length;
|
||||
|
||||
@override
|
||||
@@ -268,6 +272,12 @@ class RawIRWriter with _RawIRWriterMixin {
|
||||
super.alloc(count);
|
||||
}
|
||||
|
||||
@override
|
||||
void block(int inputCount, int outputCount) {
|
||||
_nestingLevel++;
|
||||
super.block(inputCount, outputCount);
|
||||
}
|
||||
|
||||
ArgumentNamesRef encodeArgumentNames(List<String?> argumentNames) =>
|
||||
// TODO(paulberry): is `putIfAbsent` the best-performing way to do this?
|
||||
_argumentNamesToRef.putIfAbsent(argumentNames, () {
|
||||
@@ -284,6 +294,27 @@ class RawIRWriter with _RawIRWriterMixin {
|
||||
return encoding;
|
||||
});
|
||||
|
||||
@override
|
||||
void end() {
|
||||
_nestingLevel--;
|
||||
super.end();
|
||||
}
|
||||
|
||||
/// Outputs enough `end` instructions to cause [nestingLevel] to equal
|
||||
/// [desiredNestingLevel].
|
||||
void endTo(int desiredNestingLevel) {
|
||||
assert(desiredNestingLevel <= nestingLevel);
|
||||
while (desiredNestingLevel < nestingLevel) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void function(TypeRef type, FunctionFlags flags) {
|
||||
_nestingLevel++;
|
||||
super.function(type, flags);
|
||||
}
|
||||
|
||||
@override
|
||||
void release(int count) {
|
||||
_localVariableCount -= count;
|
||||
|
||||
@@ -16,12 +16,24 @@ mixin _RawIRWriterMixin implements _RawIRWriterMixinInterface {
|
||||
_params1.add(0);
|
||||
}
|
||||
|
||||
void block(int inputCount, int outputCount) {
|
||||
_opcodes.add(Opcode.block);
|
||||
_params0.add(inputCount);
|
||||
_params1.add(outputCount);
|
||||
}
|
||||
|
||||
void br(int nesting) {
|
||||
_opcodes.add(Opcode.br);
|
||||
_params0.add(nesting);
|
||||
_params1.add(0);
|
||||
}
|
||||
|
||||
void brIf(int nesting) {
|
||||
_opcodes.add(Opcode.brIf);
|
||||
_params0.add(nesting);
|
||||
_params1.add(0);
|
||||
}
|
||||
|
||||
void call(CallDescriptorRef callDescriptor, ArgumentNamesRef argumentNames) {
|
||||
_opcodes.add(Opcode.call);
|
||||
_params0.add(callDescriptor.index);
|
||||
@@ -46,6 +58,12 @@ mixin _RawIRWriterMixin implements _RawIRWriterMixinInterface {
|
||||
_params1.add(0);
|
||||
}
|
||||
|
||||
void eq() {
|
||||
_opcodes.add(Opcode.eq);
|
||||
_params0.add(0);
|
||||
_params1.add(0);
|
||||
}
|
||||
|
||||
void function(TypeRef type, FunctionFlags flags) {
|
||||
_opcodes.add(Opcode.function);
|
||||
_params0.add(type.index);
|
||||
@@ -101,6 +119,9 @@ mixin IRToStringMixin implements RawIRContainerInterface {
|
||||
case Opcode.literal:
|
||||
return 'literal(${literalRefToString(Opcode.literal.decodeValue(this, address))})';
|
||||
|
||||
case Opcode.eq:
|
||||
return 'eq';
|
||||
|
||||
case Opcode.drop:
|
||||
return 'drop';
|
||||
|
||||
@@ -110,6 +131,9 @@ mixin IRToStringMixin implements RawIRContainerInterface {
|
||||
case Opcode.shuffle:
|
||||
return 'shuffle(${Opcode.shuffle.decodePopCount(this, address)}, ${stackIndicesRefToString(Opcode.shuffle.decodeStackIndices(this, address))})';
|
||||
|
||||
case Opcode.block:
|
||||
return 'block(${Opcode.block.decodeInputCount(this, address)}, ${Opcode.block.decodeOutputCount(this, address)})';
|
||||
|
||||
case Opcode.function:
|
||||
return 'function(${typeRefToString(Opcode.function.decodeType(this, address))}, ${functionFlagsToString(Opcode.function.decodeFlags(this, address))})';
|
||||
|
||||
@@ -119,6 +143,9 @@ mixin IRToStringMixin implements RawIRContainerInterface {
|
||||
case Opcode.br:
|
||||
return 'br(${Opcode.br.decodeNesting(this, address)})';
|
||||
|
||||
case Opcode.brIf:
|
||||
return 'brIf(${Opcode.brIf.decodeNesting(this, address)})';
|
||||
|
||||
case Opcode.call:
|
||||
return 'call(${callDescriptorRefToString(Opcode.call.decodeCallDescriptor(this, address))}, ${argumentNamesRefToString(Opcode.call.decodeArgumentNames(this, address))})';
|
||||
default:
|
||||
@@ -175,6 +202,20 @@ class _ParameterShape4 extends Opcode {
|
||||
class _ParameterShape5 extends Opcode {
|
||||
const _ParameterShape5._(super.index) : super._();
|
||||
|
||||
int decodeInputCount(RawIRContainerInterface ir, int address) {
|
||||
assert(ir.opcodeAt(address).index == index);
|
||||
return ir._params0[address];
|
||||
}
|
||||
|
||||
int decodeOutputCount(RawIRContainerInterface ir, int address) {
|
||||
assert(ir.opcodeAt(address).index == index);
|
||||
return ir._params1[address];
|
||||
}
|
||||
}
|
||||
|
||||
class _ParameterShape6 extends Opcode {
|
||||
const _ParameterShape6._(super.index) : super._();
|
||||
|
||||
TypeRef decodeType(RawIRContainerInterface ir, int address) {
|
||||
assert(ir.opcodeAt(address).index == index);
|
||||
return TypeRef(ir._params0[address]);
|
||||
@@ -186,8 +227,8 @@ class _ParameterShape5 extends Opcode {
|
||||
}
|
||||
}
|
||||
|
||||
class _ParameterShape6 extends Opcode {
|
||||
const _ParameterShape6._(super.index) : super._();
|
||||
class _ParameterShape7 extends Opcode {
|
||||
const _ParameterShape7._(super.index) : super._();
|
||||
|
||||
int decodeNesting(RawIRContainerInterface ir, int address) {
|
||||
assert(ir.opcodeAt(address).index == index);
|
||||
@@ -195,8 +236,8 @@ class _ParameterShape6 extends Opcode {
|
||||
}
|
||||
}
|
||||
|
||||
class _ParameterShape7 extends Opcode {
|
||||
const _ParameterShape7._(super.index) : super._();
|
||||
class _ParameterShape8 extends Opcode {
|
||||
const _ParameterShape8._(super.index) : super._();
|
||||
|
||||
CallDescriptorRef decodeCallDescriptor(
|
||||
RawIRContainerInterface ir, int address) {
|
||||
@@ -223,13 +264,16 @@ class Opcode {
|
||||
static const readLocal = _ParameterShape1._(2);
|
||||
static const writeLocal = _ParameterShape1._(3);
|
||||
static const literal = _ParameterShape2._(4);
|
||||
static const drop = _ParameterShape3._(5);
|
||||
static const dup = _ParameterShape3._(6);
|
||||
static const shuffle = _ParameterShape4._(7);
|
||||
static const function = _ParameterShape5._(8);
|
||||
static const end = _ParameterShape3._(9);
|
||||
static const br = _ParameterShape6._(10);
|
||||
static const call = _ParameterShape7._(11);
|
||||
static const eq = _ParameterShape3._(5);
|
||||
static const drop = _ParameterShape3._(6);
|
||||
static const dup = _ParameterShape3._(7);
|
||||
static const shuffle = _ParameterShape4._(8);
|
||||
static const block = _ParameterShape5._(9);
|
||||
static const function = _ParameterShape6._(10);
|
||||
static const end = _ParameterShape3._(11);
|
||||
static const br = _ParameterShape7._(12);
|
||||
static const brIf = _ParameterShape7._(13);
|
||||
static const call = _ParameterShape8._(14);
|
||||
|
||||
String describe() => opcodeNameTable[index];
|
||||
|
||||
@@ -239,12 +283,15 @@ class Opcode {
|
||||
"readLocal",
|
||||
"writeLocal",
|
||||
"literal",
|
||||
"eq",
|
||||
"drop",
|
||||
"dup",
|
||||
"shuffle",
|
||||
"block",
|
||||
"function",
|
||||
"end",
|
||||
"br",
|
||||
"brIf",
|
||||
"call",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:analyzer/src/wolf/ir/ir.dart';
|
||||
|
||||
/// Analyzes the scopes in a [BaseIRContainer].
|
||||
///
|
||||
/// This function computes the nesting of begin/end scopes in [ir]. A begin/end
|
||||
/// scope is the set of instructions between a "begin" instruction (a `block`,
|
||||
/// `loop`, `tryCatch`, `tryFinally`, `function`, or `instanceFunction`
|
||||
/// instruction) and the `end` instruction that matches it.
|
||||
Scopes analyzeScopes(BaseIRContainer ir,
|
||||
{ScopeAnalyzerEventListener? eventListener}) {
|
||||
eventListener ??= ScopeAnalyzerEventListener();
|
||||
var scopeAnalyzer = _ScopeAnalyzer(ir, eventListener);
|
||||
scopeAnalyzer.run();
|
||||
return Scopes._(scopeAnalyzer);
|
||||
}
|
||||
|
||||
/// Event listener used by [analyzeScopes] to report progress information.
|
||||
///
|
||||
/// By itself this class does nothing; the caller of [analyzeScopes] should make
|
||||
/// a derived class that overrides one or more of the `on...` methods.
|
||||
base class ScopeAnalyzerEventListener {
|
||||
/// Called when [scopeAnalyzer] is about to process a "begin" instruction.
|
||||
void onPushScope({required int address, required int scope}) {}
|
||||
}
|
||||
|
||||
/// The result of scope analysis.
|
||||
///
|
||||
/// See [analyzeScopes] for more information.
|
||||
class Scopes {
|
||||
final List<int> _beginAddresses;
|
||||
final List<int> _endAddresses;
|
||||
final List<int> _lastDescendants;
|
||||
|
||||
Scopes._(_ScopeAnalyzer analyzer)
|
||||
: _beginAddresses = analyzer.beginAddresses,
|
||||
_endAddresses = analyzer.endAddresses,
|
||||
_lastDescendants = analyzer.lastDescendants;
|
||||
|
||||
/// The number of scopes that was found.
|
||||
int get scopeCount => _beginAddresses.length;
|
||||
|
||||
/// The address of the "begin" instruction that opens [scope].
|
||||
///
|
||||
/// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the
|
||||
/// scope opened by the `i`th begin instruction in the IR.
|
||||
int beginAddress(int scope) => _beginAddresses[scope];
|
||||
|
||||
/// The address of the `end` instruction that closes [scope].
|
||||
///
|
||||
/// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the
|
||||
/// scope opened by the `i`th begin instruction in the IR.
|
||||
int endAddress(int scope) => _endAddresses[scope];
|
||||
|
||||
/// The scope index of the last scope transitively contained within [scope].
|
||||
///
|
||||
/// If [scope] doesn't contain any other scopes, then [scope] is returned.
|
||||
int lastDescendant(int scope) => _lastDescendants[scope];
|
||||
|
||||
/// Computes the highest-numbered scope whose [beginAddress] is less than or
|
||||
/// equal to [address].
|
||||
///
|
||||
/// Scopes are numbered in pre-order, so the returned scope will either be the
|
||||
/// innermost scope containing [address], or one of its ancestors will be (see
|
||||
/// [ancestorContainingAddress]).
|
||||
int mostRecentScope(int address) {
|
||||
assert(address >= 0);
|
||||
// By validation, we know that the instruction sequence begins with
|
||||
// `function` or `instanceFunction`, so the outermost scope covers the whole
|
||||
// instruction sequence.
|
||||
assert(beginAddress(0) == 0);
|
||||
var low = 0;
|
||||
var high = scopeCount;
|
||||
while (low < high - 1) {
|
||||
// Loop invariants
|
||||
assert(beginAddress(low) <= address);
|
||||
assert(high == scopeCount || beginAddress(high) > address);
|
||||
|
||||
var mid = (low + high) ~/ 2;
|
||||
if (beginAddress(mid) <= address) {
|
||||
low = mid;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
}
|
||||
|
||||
base class _ScopeAnalyzer {
|
||||
static const enableDebugPrints = false;
|
||||
final BaseIRContainer ir;
|
||||
final ScopeAnalyzerEventListener eventListener;
|
||||
|
||||
/// Stack of scope indices for all open scopes.
|
||||
final scopeIndices = <int>[];
|
||||
|
||||
/// See [Scopes.beginAddress].
|
||||
final beginAddresses = <int>[];
|
||||
|
||||
/// See [Scopes.endAddress].
|
||||
final endAddresses = <int>[];
|
||||
|
||||
/// See [Scopes.lastDescendant].
|
||||
final lastDescendants = <int>[];
|
||||
|
||||
_ScopeAnalyzer(this.ir, this.eventListener);
|
||||
|
||||
bool checkState() {
|
||||
if (enableDebugPrints) dumpState();
|
||||
// Scopes are numbered in pre-order, so `_scopeIndices` should be
|
||||
// monotonically increasing.
|
||||
for (var i = 0; i < scopeIndices.length - 1; i++) {
|
||||
assert(scopeIndices[i] < scopeIndices[i + 1],
|
||||
'_scopeIndices out of order: $scopeIndices');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void dumpState() {
|
||||
print(' scopeIndices: $scopeIndices');
|
||||
print(' beginAddresses: $beginAddresses');
|
||||
print(' endAddresses: $endAddresses');
|
||||
print(' lastDescendants: $lastDescendants');
|
||||
}
|
||||
|
||||
void popScope(int address) {
|
||||
var scopeIndex = scopeIndices.removeLast();
|
||||
endAddresses[scopeIndex] = address;
|
||||
lastDescendants[scopeIndex] = lastDescendants.length - 1;
|
||||
}
|
||||
|
||||
void pushScope(int address) {
|
||||
var scope = beginAddresses.length;
|
||||
eventListener.onPushScope(address: address, scope: scope);
|
||||
scopeIndices.add(scope);
|
||||
beginAddresses.add(address);
|
||||
endAddresses.add(-1);
|
||||
lastDescendants.add(-1);
|
||||
}
|
||||
|
||||
void run() {
|
||||
for (var address = 0; address < ir.endAddress; address++) {
|
||||
assert(checkState());
|
||||
if (enableDebugPrints) {
|
||||
print('$address: ${ir.instructionToString(address)}');
|
||||
}
|
||||
switch (ir.opcodeAt(address)) {
|
||||
case Opcode.function:
|
||||
pushScope(address);
|
||||
case Opcode.block:
|
||||
pushScope(address);
|
||||
case Opcode.end:
|
||||
popScope(address);
|
||||
}
|
||||
}
|
||||
assert(checkState());
|
||||
assert(scopeIndices.isEmpty);
|
||||
}
|
||||
}
|
||||
@@ -144,12 +144,21 @@ class _Validator {
|
||||
|
||||
/// Validates a `br` or `brIf` instruction.
|
||||
void branch(int nesting, {required bool conditional}) {
|
||||
if (conditional) popValues(1);
|
||||
check(nesting >= 0, 'Negative branch nesting');
|
||||
var target = controlFlowStack.length - 1 - nesting;
|
||||
check(target >= 0, 'Control flow stack underflow');
|
||||
for (var i = target + 1; i < controlFlowStack.length; i++) {
|
||||
check(!controlFlowStack[i].isFunction,
|
||||
'Cannot branch outside of enclosing function');
|
||||
}
|
||||
var branchValueCount = controlFlowStack[target].branchValueCount;
|
||||
popValues(branchValueCount);
|
||||
valueStackDepth = ValueCount.indeterminate;
|
||||
if (conditional) {
|
||||
pushValues(branchValueCount);
|
||||
} else {
|
||||
valueStackDepth = ValueCount.indeterminate;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports a validation error if [condition] is `false`.
|
||||
@@ -191,9 +200,24 @@ class _Validator {
|
||||
var count = Opcode.alloc.decodeCount(ir, address);
|
||||
check(count >= 0, 'Negative alloc count');
|
||||
localCount += count;
|
||||
case Opcode.block:
|
||||
var inputCount = Opcode.block.decodeInputCount(ir, address);
|
||||
var outputCount = Opcode.block.decodeOutputCount(ir, address);
|
||||
check(inputCount >= 0, 'Negative input count');
|
||||
check(outputCount >= 0, 'Negative output count');
|
||||
popValues(inputCount);
|
||||
controlFlowStack.add(_ControlFlowElement(
|
||||
localCountBefore: localCount,
|
||||
functionFlagsBefore: functionFlags,
|
||||
valueStackDepthAfter: valueStackDepth + outputCount,
|
||||
branchValueCount: outputCount));
|
||||
valueStackDepth = ValueCount(inputCount);
|
||||
case Opcode.br:
|
||||
var nesting = Opcode.br.decodeNesting(ir, address);
|
||||
branch(nesting, conditional: false);
|
||||
case Opcode.brIf:
|
||||
var nesting = Opcode.brIf.decodeNesting(ir, address);
|
||||
branch(nesting, conditional: true);
|
||||
case Opcode.call:
|
||||
var argumentNames = Opcode.call.decodeArgumentNames(ir, address);
|
||||
popValues(ir.decodeArgumentNames(argumentNames).length);
|
||||
@@ -213,6 +237,9 @@ class _Validator {
|
||||
'${valueStackDepth._depth} superfluous value(s) remaining');
|
||||
valueStackDepth = controlFlowElement.valueStackDepthAfter;
|
||||
functionFlags = controlFlowElement.functionFlagsBefore;
|
||||
case Opcode.eq:
|
||||
popValues(2);
|
||||
pushValues(1);
|
||||
case Opcode.function:
|
||||
var type = Opcode.function.decodeType(ir, address);
|
||||
var kind = Opcode.function.decodeFlags(ir, address);
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:analyzer/src/wolf/ir/call_descriptor.dart';
|
||||
import 'package:analyzer/src/wolf/ir/coded_ir.dart';
|
||||
import 'package:analyzer/src/wolf/ir/interpreter.dart';
|
||||
import 'package:analyzer/src/wolf/ir/ir.dart';
|
||||
import 'package:analyzer/src/wolf/ir/scope_analyzer.dart';
|
||||
import 'package:analyzer/src/wolf/ir/validator.dart';
|
||||
import 'package:checks/checks.dart';
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
@@ -27,6 +28,8 @@ main() {
|
||||
class AstToIRTest extends AstToIRTestBase {
|
||||
final _instanceGetHandlers = {
|
||||
'int.isEven': unaryFunction<int>((i) => i.isEven),
|
||||
'Iterable.first': unaryFunction<ListInstance>((list) => list.values.first),
|
||||
'Object.hashCode': unaryFunction<Object?>((o) => o.hashCode),
|
||||
'String.length': unaryFunction<String>((s) => s.length)
|
||||
};
|
||||
|
||||
@@ -38,8 +41,8 @@ class AstToIRTest extends AstToIRTestBase {
|
||||
ListInstance makeList(List<Object?> values) => ListInstance(
|
||||
typeProvider.listType(typeProvider.objectQuestionType), values);
|
||||
|
||||
Object? runInterpreter(List<Object?> args) =>
|
||||
interpret(ir, args, callDispatcher: _callDispatcher);
|
||||
Object? runInterpreter(List<Object?> args) => interpret(ir, args,
|
||||
scopes: scopes, callDispatcher: _CallDispatcher(this));
|
||||
|
||||
test_assignmentExpression_local_simple_sideEffect() async {
|
||||
await assertNoErrorsInCode('''
|
||||
@@ -95,6 +98,21 @@ test(int i) => i = 123;
|
||||
check(runInterpreter([1])).equals(123);
|
||||
}
|
||||
|
||||
test_assignmentExpression_property_nullShorting_simple() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(List? l) => l?.length = 3;
|
||||
''');
|
||||
analyze(findNode.singleFunctionDeclaration);
|
||||
check(astNodes)[findNode.assignment('l?.length = 3')]
|
||||
..containsSubrange(astNodes[findNode.simple('l?.length')]!)
|
||||
..containsSubrange(astNodes[findNode.propertyAccess('l?.length')]!)
|
||||
..containsSubrange(astNodes[findNode.integerLiteral('3')]!);
|
||||
check(runInterpreter([null])).equals(null);
|
||||
var l = ['a', 'b', 'c', 'd', 'e'];
|
||||
check(runInterpreter([makeList(l)])).equals(3);
|
||||
check(l).deepEquals(['a', 'b', 'c']);
|
||||
}
|
||||
|
||||
test_assignmentExpression_property_prefixedIdentifier_simple() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(List l) => l.length = 3;
|
||||
@@ -138,6 +156,21 @@ extension E on List {
|
||||
check(l).deepEquals(['a', 'b', 'c']);
|
||||
}
|
||||
|
||||
test_binaryExpression_equal() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(Object? x, Object? y) => x == y;
|
||||
''');
|
||||
analyze(findNode.singleFunctionDeclaration);
|
||||
check(astNodes)[findNode.binary('x == y')]
|
||||
..containsSubrange(astNodes[findNode.simple('x ==')]!)
|
||||
..containsSubrange(astNodes[findNode.simple('y;')]!);
|
||||
check(runInterpreter([null, null])).equals(true);
|
||||
check(runInterpreter([null, 1])).equals(false);
|
||||
check(runInterpreter([1, null])).equals(false);
|
||||
check(runInterpreter([1, 2])).equals(false);
|
||||
check(runInterpreter([1, 1])).equals(true);
|
||||
}
|
||||
|
||||
test_block() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(int i) {
|
||||
@@ -258,6 +291,55 @@ test(int i) => (i);
|
||||
check(runInterpreter([123])).equals(123);
|
||||
}
|
||||
|
||||
test_parenthesizedExpression_stopsNullShorting() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(List<Object?>? list) => (list?.first).hashCode;
|
||||
''');
|
||||
analyze(findNode.singleFunctionDeclaration);
|
||||
check(runInterpreter([null])).equals(null.hashCode);
|
||||
check(runInterpreter([
|
||||
makeList([123])
|
||||
])).equals(123.hashCode);
|
||||
}
|
||||
|
||||
test_propertyAccess_allowsNullShorting() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(List<Object?>? list) => list?.first.hashCode;
|
||||
''');
|
||||
analyze(findNode.singleFunctionDeclaration);
|
||||
check(runInterpreter([null])).equals(null);
|
||||
check(runInterpreter([
|
||||
makeList([123])
|
||||
])).equals(123.hashCode);
|
||||
}
|
||||
|
||||
test_propertyAccess_nestedNullShorting() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(List<Object?>? list) => list?.first?.hashCode;
|
||||
''');
|
||||
analyze(findNode.singleFunctionDeclaration);
|
||||
check(astNodes, because: 'both null checks should use the same block')[
|
||||
findNode.singleFunctionBody]
|
||||
.instructions
|
||||
.withOpcode(Opcode.block)
|
||||
.hasLength(1);
|
||||
check(runInterpreter([null])).equals(null);
|
||||
check(runInterpreter([
|
||||
makeList([123])
|
||||
])).equals(123.hashCode);
|
||||
}
|
||||
|
||||
test_propertyGet_nullShorting() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(String? s) => s?.length;
|
||||
''');
|
||||
analyze(findNode.singleFunctionDeclaration);
|
||||
check(astNodes)[findNode.propertyAccess('s?.length')]
|
||||
.containsSubrange(astNodes[findNode.simple('s?.length')]!);
|
||||
check(runInterpreter([null])).equals(null);
|
||||
check(runInterpreter(['foo'])).equals(3);
|
||||
}
|
||||
|
||||
test_propertyGet_prefixedIdentifier() async {
|
||||
await assertNoErrorsInCode('''
|
||||
test(int i) => i.isEven;
|
||||
@@ -457,30 +539,6 @@ test() {
|
||||
check(runInterpreter([])).identicalTo(null);
|
||||
}
|
||||
|
||||
CallHandler _callDispatcher(CallDescriptor callDescriptor) {
|
||||
CallHandler? handler;
|
||||
switch (callDescriptor) {
|
||||
case InstanceGetDescriptor(
|
||||
getter: PropertyAccessorElement(
|
||||
enclosingElement: InstanceElement(name: var typeName?)
|
||||
)
|
||||
):
|
||||
handler = _instanceGetHandlers['$typeName.${callDescriptor.name}'];
|
||||
case InstanceSetDescriptor(
|
||||
setter: PropertyAccessorElement(
|
||||
enclosingElement: InstanceElement(name: var typeName?)
|
||||
)
|
||||
):
|
||||
handler = _instanceSetHandlers['$typeName.${callDescriptor.name}'];
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('TODO(paulberry): $runtimeType');
|
||||
}
|
||||
if (handler == null) {
|
||||
throw StateError('No handler for $callDescriptor');
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
static CallHandler binaryFunction<T, U>(Object? Function(T, U) f) =>
|
||||
(positionalArguments, namedArguments) {
|
||||
check(positionalArguments).length.equals(2);
|
||||
@@ -499,6 +557,7 @@ test() {
|
||||
class AstToIRTestBase extends PubPackageResolutionTest {
|
||||
final astNodes = AstNodes();
|
||||
late final CodedIRContainer ir;
|
||||
late final Scopes scopes;
|
||||
|
||||
void analyze(Declaration declaration) {
|
||||
switch (declaration) {
|
||||
@@ -516,6 +575,7 @@ class AstToIRTestBase extends PubPackageResolutionTest {
|
||||
'TODO(paulberry): ${declaration.declaredElement}');
|
||||
}
|
||||
validate(ir);
|
||||
scopes = analyzeScopes(ir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,6 +586,47 @@ class ListInstance extends Instance {
|
||||
ListInstance(super.type, this.values);
|
||||
}
|
||||
|
||||
class _CallDispatcher implements CallDispatcher {
|
||||
final AstToIRTest _test;
|
||||
|
||||
_CallDispatcher(this._test);
|
||||
|
||||
@override
|
||||
bool equals(Object firstValue, Object secondValue) {
|
||||
if (firstValue is Literal) {
|
||||
throw UnimplementedError('TODO(paulberry): call custom operator==');
|
||||
}
|
||||
return firstValue == secondValue;
|
||||
}
|
||||
|
||||
@override
|
||||
CallHandler lookupCallDescriptor(CallDescriptor callDescriptor) {
|
||||
CallHandler? handler;
|
||||
switch (callDescriptor) {
|
||||
case InstanceGetDescriptor(
|
||||
getter: PropertyAccessorElement(
|
||||
enclosingElement: InstanceElement(name: var typeName?)
|
||||
)
|
||||
):
|
||||
handler =
|
||||
_test._instanceGetHandlers['$typeName.${callDescriptor.name}'];
|
||||
case InstanceSetDescriptor(
|
||||
setter: PropertyAccessorElement(
|
||||
enclosingElement: InstanceElement(name: var typeName?)
|
||||
)
|
||||
):
|
||||
handler =
|
||||
_test._instanceSetHandlers['$typeName.${callDescriptor.name}'];
|
||||
case dynamic(:var runtimeType):
|
||||
throw UnimplementedError('TODO(paulberry): $runtimeType');
|
||||
}
|
||||
if (handler == null) {
|
||||
throw StateError('No handler for $callDescriptor');
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
|
||||
extension on Subject<SoundnessError> {
|
||||
Subject<int> get address => has((e) => e.address, 'address');
|
||||
Subject<String> get message => has((e) => e.message, 'message');
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:analyzer/src/wolf/ir/ir.dart';
|
||||
import 'package:analyzer/src/wolf/ir/scope_analyzer.dart';
|
||||
import 'package:analyzer/src/wolf/ir/validator.dart';
|
||||
import 'package:checks/checks.dart';
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
|
||||
main() {
|
||||
defineReflectiveSuite(() {
|
||||
defineReflectiveTests(ScopeAnalyzerTest);
|
||||
});
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class ScopeAnalyzerTest {
|
||||
final labelToScope = <String, int>{};
|
||||
late final TestIRContainer ir;
|
||||
late final Scopes scopeAnalysisResult;
|
||||
|
||||
LiteralRef get dummyLiteral => LiteralRef(0);
|
||||
|
||||
void test_beginAddress() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('block')
|
||||
..block(0, 0)
|
||||
..end()
|
||||
..end());
|
||||
check(scopeAnalysisResult.beginAddress(labelToScope['block']!)).equals(1);
|
||||
}
|
||||
|
||||
void test_endAddress() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('block')
|
||||
..block(0, 0)
|
||||
..label('end')
|
||||
..end()
|
||||
..end());
|
||||
check(scopeAnalysisResult.endAddress(labelToScope['block']!))
|
||||
.equals(ir.labelToAddress('end')!);
|
||||
}
|
||||
|
||||
void test_lastDescendant() {
|
||||
_analyze((ir) => ir
|
||||
..label('function')
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('block1')
|
||||
..block(0, 0)
|
||||
..end()
|
||||
..label('block2')
|
||||
..block(0, 0)
|
||||
..label('block3')
|
||||
..block(0, 0)
|
||||
..end()
|
||||
..end()
|
||||
..end());
|
||||
check(scopeAnalysisResult.beginAddress(
|
||||
scopeAnalysisResult.lastDescendant(labelToScope['function']!)))
|
||||
.equals(ir.labelToAddress('block3')!);
|
||||
check(scopeAnalysisResult.beginAddress(
|
||||
scopeAnalysisResult.lastDescendant(labelToScope['block1']!)))
|
||||
.equals(ir.labelToAddress('block1')!);
|
||||
check(scopeAnalysisResult.beginAddress(
|
||||
scopeAnalysisResult.lastDescendant(labelToScope['block2']!)))
|
||||
.equals(ir.labelToAddress('block3')!);
|
||||
check(scopeAnalysisResult.beginAddress(
|
||||
scopeAnalysisResult.lastDescendant(labelToScope['block3']!)))
|
||||
.equals(ir.labelToAddress('block3')!);
|
||||
}
|
||||
|
||||
void test_mostRecentScope_manyScopes() {
|
||||
_analyze((ir) => ir
|
||||
..label('function')
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('block1')
|
||||
..block(0, 0)
|
||||
..label('block2')
|
||||
..block(0, 0)
|
||||
..end()
|
||||
..label('block3')
|
||||
..block(0, 0)
|
||||
..end()
|
||||
..end()
|
||||
..label('block4')
|
||||
..block(0, 0)
|
||||
..end()
|
||||
..end());
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('function')!))
|
||||
.equals(labelToScope['function']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block1')! - 1))
|
||||
.equals(labelToScope['function']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block1')!))
|
||||
.equals(labelToScope['block1']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block2')! - 1))
|
||||
.equals(labelToScope['block1']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block2')!))
|
||||
.equals(labelToScope['block2']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block3')! - 1))
|
||||
.equals(labelToScope['block2']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block3')!))
|
||||
.equals(labelToScope['block3']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block4')! - 1))
|
||||
.equals(labelToScope['block3']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block4')!))
|
||||
.equals(labelToScope['block4']!);
|
||||
}
|
||||
|
||||
void test_mostRecentScope_oneScope() {
|
||||
_analyze((ir) => ir
|
||||
..label('function')
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('end')
|
||||
..end());
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('function')!))
|
||||
.equals(labelToScope['function']!);
|
||||
check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('end')!))
|
||||
.equals(labelToScope['function']!);
|
||||
}
|
||||
|
||||
void test_scopeCount() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('block')
|
||||
..block(0, 0)
|
||||
..end()
|
||||
..end());
|
||||
check(scopeAnalysisResult.scopeCount).equals(2);
|
||||
}
|
||||
|
||||
void _analyze(void Function(TestIRWriter) writeIR) {
|
||||
var writer = TestIRWriter();
|
||||
writeIR(writer);
|
||||
ir = TestIRContainer(writer);
|
||||
validate(ir);
|
||||
scopeAnalysisResult = analyzeScopes(ir,
|
||||
eventListener:
|
||||
_ScopeAnalyzerEventListener(ir: ir, labelToScope: labelToScope));
|
||||
}
|
||||
}
|
||||
|
||||
final class _ScopeAnalyzerEventListener extends ScopeAnalyzerEventListener {
|
||||
final TestIRContainer ir;
|
||||
final Map<String, int> labelToScope;
|
||||
|
||||
_ScopeAnalyzerEventListener({required this.ir, required this.labelToScope});
|
||||
|
||||
@override
|
||||
void onPushScope({required int address, required int scope}) {
|
||||
// Record the scope name.
|
||||
if (ir.addressToLabel(address) case var name?) {
|
||||
labelToScope[name] = scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import 'ast_to_ir_test.dart' as ast_to_ir;
|
||||
import 'scope_analyzer_test.dart' as scope_analyzer;
|
||||
import 'validator_test.dart' as validator;
|
||||
|
||||
main() {
|
||||
defineReflectiveSuite(() {
|
||||
ast_to_ir.main();
|
||||
scope_analyzer.main();
|
||||
validator.main();
|
||||
}, name: 'ir');
|
||||
}
|
||||
|
||||
@@ -88,6 +88,11 @@ class Instruction {
|
||||
final int address;
|
||||
|
||||
Instruction(this.ir, this.address);
|
||||
|
||||
Opcode get opcode => ir.opcodeAt(address);
|
||||
|
||||
@override
|
||||
String toString() => '$address: ${ir.instructionToString(address)}';
|
||||
}
|
||||
|
||||
/// Reference to a range of instructions in a [CodedIRContainer].
|
||||
@@ -226,8 +231,23 @@ extension SubjectAstNodes on Subject<AstNodes> {
|
||||
/// Testing methods for [Instruction].
|
||||
extension SubjectInstruction on Subject<Instruction> {
|
||||
@meta.useResult
|
||||
Subject<Opcode> get opcode => has(
|
||||
(instruction) => instruction.ir.opcodeAt(instruction.address), 'opcode');
|
||||
Subject<Opcode> get opcode =>
|
||||
has((instruction) => instruction.opcode, 'opcode');
|
||||
}
|
||||
|
||||
/// Testing methods for `Iterable<Instruction>`.
|
||||
extension SubjectInstructionIterable on Subject<Iterable<Instruction>> {
|
||||
void hasLength(int expectedLength) => context.expect(
|
||||
() => ['has length $expectedLength'],
|
||||
(instructions) => instructions.length == expectedLength
|
||||
? null
|
||||
: Rejection(which: ['does not have length $expectedLength']));
|
||||
|
||||
@meta.useResult
|
||||
Subject<Iterable<Instruction>> withOpcode(Opcode opcode) => context.nest(
|
||||
() => ['contains instructions matching ${opcode.describe()}'],
|
||||
(instructions) =>
|
||||
Extracted.value(instructions.where((i) => i.opcode == opcode)));
|
||||
}
|
||||
|
||||
/// Testing methods for [InstructionRange].
|
||||
|
||||
@@ -41,6 +41,48 @@ class ValidatorTest {
|
||||
_validate();
|
||||
}
|
||||
|
||||
test_block_negativeInputCount() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction()
|
||||
..label('bad')
|
||||
..block(-1, 0)
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Negative input count');
|
||||
}
|
||||
|
||||
test_block_negativeOutputCount() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction()
|
||||
..label('bad')
|
||||
..block(0, -1)
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Negative output count');
|
||||
}
|
||||
|
||||
test_block_ok() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2)))
|
||||
..block(2, 1)
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2)))
|
||||
..drop()
|
||||
..end()
|
||||
..end());
|
||||
_validate();
|
||||
}
|
||||
|
||||
test_block_underflow() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('bad')
|
||||
..block(2, 1)
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Value stack underflow');
|
||||
}
|
||||
|
||||
test_br_controlFlowStackUnderflow() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction()
|
||||
@@ -50,6 +92,33 @@ class ValidatorTest {
|
||||
_checkInvalidMessageAt('bad').equals('Control flow stack underflow');
|
||||
}
|
||||
|
||||
test_br_fromBlock_ok() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..block(2, 1)
|
||||
..drop()
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1)))
|
||||
..br(0)
|
||||
..onValidate(
|
||||
(v) => check(v.valueStackDepth).equals(ValueCount.indeterminate))
|
||||
..end()
|
||||
..end());
|
||||
_validate();
|
||||
}
|
||||
|
||||
test_br_fromBlock_stackUnderflow() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..block(2, 1)
|
||||
..drop()
|
||||
..drop()
|
||||
..label('bad')
|
||||
..br(0)
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Value stack underflow');
|
||||
}
|
||||
|
||||
test_br_fromFunction_ok() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
@@ -78,6 +147,90 @@ class ValidatorTest {
|
||||
_checkInvalidMessageAt('bad').equals('Negative branch nesting');
|
||||
}
|
||||
|
||||
test_br_outsideOfEnclosingFunction() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction()
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('bad')
|
||||
..br(1)
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad')
|
||||
.equals('Cannot branch outside of enclosing function');
|
||||
}
|
||||
|
||||
test_brIf_controlFlowStackUnderflow() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..label('bad')
|
||||
..brIf(1)
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Control flow stack underflow');
|
||||
}
|
||||
|
||||
test_brIf_fromBlock_ok() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..block(2, 1)
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2)))
|
||||
..brIf(0)
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1)))
|
||||
..end()
|
||||
..end());
|
||||
_validate();
|
||||
}
|
||||
|
||||
test_brIf_fromBlock_stackUnderflow() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..block(2, 1)
|
||||
..drop()
|
||||
..label('bad')
|
||||
..brIf(0)
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Value stack underflow');
|
||||
}
|
||||
|
||||
test_brIf_fromFunction_ok() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..brIf(0)
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1)))
|
||||
..end());
|
||||
_validate();
|
||||
}
|
||||
|
||||
test_brIf_fromFunction_stackUnderflow() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('bad')
|
||||
..brIf(0)
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Value stack underflow');
|
||||
}
|
||||
|
||||
test_brIf_negativeNesting() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..label('bad')
|
||||
..brIf(-1)
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Negative branch nesting');
|
||||
}
|
||||
|
||||
test_brIf_outsideOfEnclosingFunction() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction()
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..label('bad')
|
||||
..br(1)
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad')
|
||||
.equals('Cannot branch outside of enclosing function');
|
||||
}
|
||||
|
||||
test_call_ok() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
@@ -136,6 +289,53 @@ class ValidatorTest {
|
||||
_checkInvalidMessageAt('bad').equals('Value stack underflow');
|
||||
}
|
||||
|
||||
test_end_block_indeterminate() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..block(2, 1)
|
||||
..br(1)
|
||||
..onValidate(
|
||||
(v) => check(v.valueStackDepth).equals(ValueCount.indeterminate))
|
||||
..end()
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1)))
|
||||
..end());
|
||||
_validate();
|
||||
}
|
||||
|
||||
test_end_block_preservesStackValuesBelowInput() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 3)
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(3)))
|
||||
..block(2, 1)
|
||||
..drop()
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1)))
|
||||
..end()
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2)))
|
||||
..drop()
|
||||
..end());
|
||||
_validate();
|
||||
}
|
||||
|
||||
test_end_block_superfluousValues() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..block(2, 1)
|
||||
..label('bad')
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('1 superfluous value(s) remaining');
|
||||
}
|
||||
|
||||
test_end_block_underflow() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction()
|
||||
..block(0, 1)
|
||||
..label('bad')
|
||||
..end()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Value stack underflow');
|
||||
}
|
||||
|
||||
test_end_function_indeterminate() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
@@ -196,6 +396,24 @@ class ValidatorTest {
|
||||
_checkInvalidMessageAt('bad').equals('Unreleased locals');
|
||||
}
|
||||
|
||||
test_eq_ok() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 2)
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2)))
|
||||
..eq()
|
||||
..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1)))
|
||||
..end());
|
||||
}
|
||||
|
||||
test_eq_underflow() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction(parameterCount: 1)
|
||||
..label('bad')
|
||||
..eq()
|
||||
..end());
|
||||
_checkInvalidMessageAt('bad').equals('Value stack underflow');
|
||||
}
|
||||
|
||||
test_firstInstruction_function_ok() {
|
||||
_analyze((ir) => ir
|
||||
..ordinaryFunction()
|
||||
|
||||
@@ -85,15 +85,18 @@ class _Instructions {
|
||||
_addInstruction('writeLocal', [uint('localIndex')]);
|
||||
// Primitive operations
|
||||
_addInstruction('literal', [literal('value')]);
|
||||
_addInstruction('eq', []);
|
||||
// Stack manipulation
|
||||
_addInstruction('drop', []);
|
||||
_addInstruction('dup', []);
|
||||
_addInstruction(
|
||||
'shuffle', [uint('popCount'), stackIndices('stackIndices')]);
|
||||
// Flow control
|
||||
_addInstruction('block', [uint('inputCount'), uint('outputCount')]);
|
||||
_addInstruction('function', [type('type'), functionFlags('flags')]);
|
||||
_addInstruction('end', []);
|
||||
_addInstruction('br', [uint('nesting')]);
|
||||
_addInstruction('brIf', [uint('nesting')]);
|
||||
// Invocations and tearoffs
|
||||
_addInstruction('call',
|
||||
[(callDescriptor('callDescriptor')), (argumentNames('argumentNames'))]);
|
||||
|
||||
Reference in New Issue
Block a user