blob: c5f76b993c85e5cf60301c2afd2b456d3560012b [file] [log] [blame] [edit]
// 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/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.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.
///
/// The interpreter represents [int]s, [double]s, [String]s, and [Null]s
/// directly with the corresponding Dart value. All other values are represented
/// via objects of type [Instance].
///
/// The behavior of the `call` instruction is governed by the [callDispatcher]
/// parameter, which specifies the behavior of each possible [CallDescriptor].
///
/// This interpreter is neither efficient nor full-featured, so it shouldn't be
/// used in production code. It is solely intended to allow unit tests to verify
/// that an instruction sequence behaves as it's expected to.
@visibleForTesting
Object? interpret(
CodedIRContainer ir,
List<Object?> args, {
required Scopes scopes,
required CallDispatcher callDispatcher,
required TypeProvider typeProvider,
required TypeSystem typeSystem,
}) {
return _IRInterpreter(
ir,
scopes: scopes,
callDispatcher: callDispatcher,
typeProvider: typeProvider,
typeSystem: typeSystem,
).run(args);
}
/// Function type invoked by [interpret] to execute a `call` instruction.
typedef CallHandler =
Object? Function(
CallDescriptor descriptor,
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 an `await` expression.
///
/// In a real Dart program, execution of an `await` would cause the current
/// stack frame to be suspended and then resumed after other, unrelated code,
/// had a chance to execute. But since the purpose of the interpreter is to
/// simulate Dart code execution with just enough fidelity to be able to unit
/// test the IR, it is good enough to simply execute the `await`
/// synchronously.
Object? await_(Instance future);
/// 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);
/// Executes a `yield` statement.
///
/// In a real Dart program, execution of a `yield` would cause the current
/// stack frame to be suspended and then resumed after the caller advanced an
/// iterator. But since the purpose of the interpreter is to simulate Dart
/// code with just enough fidelity to be able to unit test the IR, it is good
/// enough to simply execute the `yield` synchronously.
void yield_(Object? value);
}
/// Interpreter representation of a heap object.
///
/// This class should not be used for the types [int], [double], [String], or
/// [Null], since the interpreter represents those types directly with the
/// corresponding Dart value.
class Instance {
final InterfaceType type;
Instance(this.type)
: assert(
!type.isDartCoreInt &&
!type.isDartCoreDouble &&
!type.isDartCoreString &&
!type.isDartCoreNull,
);
}
/// Error thrown if the interpreter encounters a situation that should be
/// impossible assuming the soundness of flow analysis and the Dart type system.
class SoundnessError extends Error {
final int address;
final String instructionString;
final String message;
SoundnessError({
required this.address,
required this.instructionString,
required this.message,
});
@override
String toString() =>
'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 {
final _ControlFlowStackEntryKind kind;
/// 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.kind,
required this.stackFence,
required this.localFence,
required this.outputCount,
required this.scope,
});
}
enum _ControlFlowStackEntryKind { block, loop }
class _IRInterpreter {
static const keepGoing = _KeepGoing();
final CodedIRContainer ir;
final Scopes scopes;
final CallDispatcher callDispatcher;
final List<CallHandler> callHandlers;
final TypeProvider typeProvider;
final TypeSystem typeSystem;
final stack = <Object?>[];
final locals = <_LocalSlot>[];
final controlFlowStack = <_ControlFlowStackEntry>[];
var address = 1;
/// 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,
required this.typeProvider,
required this.typeSystem,
}) : 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 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) {
while (nesting-- > 0) {
controlFlowStack.removeLast();
}
if (controlFlowStack.isNotEmpty) {
var stackEntry = controlFlowStack.last;
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;
switch (stackEntry.kind) {
case _ControlFlowStackEntryKind.block:
// Continue with the code following the block.
controlFlowStack.removeLast();
address = scopes.endAddress(scope);
mostRecentScope = scopes.lastDescendant(scope);
case _ControlFlowStackEntryKind.loop:
// Jump back to the `loop` instruction.
address = scopes.beginAddress(scope);
mostRecentScope = scope;
}
return keepGoing;
} else {
// Branch targets the function, so return from the code being interpreted.
return stack.last;
}
}
DartType getRuntimeType(Object? value) => switch (value) {
String() => typeProvider.stringType,
int() => typeProvider.intType,
double() => typeProvider.doubleType,
null => typeProvider.nullType,
Instance(:var type) => type,
dynamic(:var runtimeType) => throw StateError(
'Unexpected interpreter value of type $runtimeType',
),
};
Object? run(List<Object?> args) {
var functionType = Opcode.function.decodeType(ir, 0);
var parameterCount = ir.countParameters(functionType);
if (Opcode.function.decodeFlags(ir, 0).isInstance) {
parameterCount++;
}
if (args.length != parameterCount) {
throw StateError('Parameter count mismatch');
}
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.await_:
var future = stack.removeLast();
if (future is! Instance || !future.type.isDartAsyncFuture) {
throw StateError('Await of non-future');
}
stack.add(callDispatcher.await_(future));
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(
kind: _ControlFlowStackEntryKind.block,
stackFence: stack.length - inputCount,
localFence: locals.length,
outputCount: outputCount,
scope: scope,
),
);
case Opcode.br:
var nesting = Opcode.br.decodeNesting(ir, address);
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),
);
var callDescriptorRef = Opcode.call.decodeCallDescriptor(ir, address);
var newStackLength = stack.length - argumentNames.length;
var positionalArguments = <Object?>[];
var namedArguments = <String, Object?>{};
for (var i = 0; i < argumentNames.length; i++) {
var argument = stack[newStackLength + i];
if (argumentNames[i] case var name?) {
namedArguments[name] = argument;
} else {
positionalArguments.add(argument);
}
}
stack.length = newStackLength;
stack.add(
callHandlers[callDescriptorRef.index](
ir.decodeCallDescriptor(callDescriptorRef),
positionalArguments,
namedArguments,
),
);
case Opcode.concat:
var count = Opcode.concat.decodeCount(ir, address);
var newStackLength = stack.length - count;
var result = stack.sublist(newStackLength).join();
stack.length = newStackLength;
stack.add(result);
case Opcode.drop:
stack.removeLast();
case Opcode.dup:
stack.add(stack.last);
case Opcode.end:
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);
switch (stackEntry.kind) {
case _ControlFlowStackEntryKind.block:
// Continue with the code following the block.
controlFlowStack.removeLast();
case _ControlFlowStackEntryKind.loop:
// Jump back to the `loop` instruction.
var scope = stackEntry.scope;
address = scopes.beginAddress(scope);
mostRecentScope = scope;
}
}
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.identical:
var secondValue = stack.removeLast();
var firstValue = stack.removeLast();
stack.add(identical(firstValue, secondValue));
case Opcode.is_:
var testType = ir.decodeType(Opcode.is_.decodeType(ir, address));
stack.add(
typeSystem.isSubtypeOf(
getRuntimeType(stack.removeLast()),
testType,
),
);
case Opcode.literal:
var value = Opcode.literal.decodeValue(ir, address);
stack.add(ir.decodeLiteral(value));
case Opcode.loop:
var inputCount = Opcode.loop.decodeInputCount(ir, address);
var scope = ++mostRecentScope;
assert(scopes.beginAddress(scope) == address);
controlFlowStack.add(
_ControlFlowStackEntry(
kind: _ControlFlowStackEntryKind.loop,
stackFence: stack.length - inputCount,
localFence: locals.length,
outputCount: inputCount,
scope: scope,
),
);
case Opcode.not:
stack.add(!(stack.removeLast() as bool));
case Opcode.readLocal:
var localIndex = Opcode.readLocal.decodeLocalIndex(ir, address);
var value = locals[localIndex].contents;
if (value is _NoValue) {
throwSoundnessError('Read of unset local');
}
stack.add(value);
case Opcode.release:
var count = Opcode.release.decodeCount(ir, address);
locals.length -= count;
case Opcode.shuffle:
var popCount = Opcode.shuffle.decodePopCount(ir, address);
var stackIndices = ir.decodeStackIndices(
Opcode.shuffle.decodeStackIndices(ir, address),
);
var newStackLength = stack.length - popCount;
var poppedValues = stack.sublist(newStackLength);
stack.length = newStackLength;
for (var index in stackIndices) {
stack.add(poppedValues[index]);
}
case Opcode.writeLocal:
var localIndex = Opcode.writeLocal.decodeLocalIndex(ir, address);
locals[localIndex].contents = stack.removeLast();
case Opcode.yield_:
callDispatcher.yield_(stack.removeLast());
case var opcode:
throw UnimplementedError(
'TODO(paulberry): implement ${opcode.describe()} in '
'_IRInterpreter',
);
}
address++;
}
}
Never throwSoundnessError(String message) => throw SoundnessError(
address: address,
instructionString: ir.instructionToString(address),
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.
Object? contents = const _NoValue();
}
/// Sentinel value used by [_IRInterpreter] to indicate that a [_LocalSlot] is
/// empty.
class _NoValue {
const _NoValue();
}