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:
Paul Berry
2023-11-21 00:32:19 +00:00
committed by Commit Queue
parent 93e7fd5a0e
commit deebb52c2b
12 changed files with 1031 additions and 60 deletions
+65 -7
View File
@@ -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;
+154 -13
View File
@@ -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.
+31
View File
@@ -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;
+58 -11
View File
@@ -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);
}
}
+28 -1
View File
@@ -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);
+127 -26
View File
@@ -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');
}
+22 -2
View File
@@ -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()
+3
View File
@@ -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'))]);