blob: 81efeac0da3a46586e4c5655651e1b7f44656ddd [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/ast/ast.dart';
import 'package:analyzer/src/wolf/ir/ast_to_ir.dart';
import 'package:analyzer/src/wolf/ir/coded_ir.dart';
import 'package:analyzer/src/wolf/ir/ir.dart';
import 'package:checks/checks.dart';
import 'package:checks/context.dart';
import 'package:meta/meta.dart' as meta;
void dumpInstructions(BaseIRContainer ir) {
for (var i = 0; i < ir.endAddress; i++) {
print('$i: ${ir.instructionToString(i)}');
}
}
/// Event listener for [astToIR] that records the range of IR instructions
/// associated with each AST node.
///
/// These ranges can be queried using the `[]` operator after [astToIR]
/// completes.
base class AstNodes extends AstToIREventListener {
/// Outstanding [AstNodes] which have been entered but not exited.
final _nodeStack = <AstNode>[];
/// For each entry in [_nodeStack], the value returned by
/// [nextInstructionAddress] at the time [onEnterNode] was called.
///
/// In other words, the address of the first instruction that was output (or
/// that will be output) while visiting the corresponding to the AST node.
final _nodeStartStack = <int>[];
final _nodeMap = <AstNode, ({int start, int end})>{};
/// The full IR.
///
/// Only available after [onFinished] has been called.
late final CodedIRContainer ir;
/// Gets the [InstructionRange] for [node].
///
/// Only available after [onFinished] has been called.
InstructionRange? operator [](AstNode node) => switch (_nodeMap[node]) {
null => null,
(:var start, :var end) => InstructionRange(ir: ir, start: start, end: end),
};
@override
void onEnterNode(AstNode node) {
_nodeStack.add(node);
_nodeStartStack.add(nextInstructionAddress);
super.onEnterNode(node);
}
@override
void onExitNode() {
_nodeMap[_nodeStack.removeLast()] = (
start: _nodeStartStack.removeLast(),
end: nextInstructionAddress,
);
super.onExitNode();
}
@override
void onFinished(CodedIRContainer ir) {
check(_nodeStack).isEmpty();
check(_nodeStartStack).isEmpty();
this.ir = ir;
super.onFinished(ir);
}
@override
String toString() {
if (_nodeMap.isEmpty) return 'AstNodes(<empty>)';
return [
'AstNodes(',
for (var MapEntry(:key, :value) in _nodeMap.entries)
' ${key.runtimeType} $key => $value',
')',
].join('\n');
}
}
/// Reference to a single instruction in a [CodedIRContainer].
class Instruction {
final CodedIRContainer ir;
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].
class InstructionRange {
final CodedIRContainer ir;
/// Start address of the range (i.e., the address of the first instruction in
/// the range).
final int start;
/// "Past the end" address of the range (i.e., one more than the address of
/// the last instruction in the range).
///
/// If [start] is equal to [end], the range is empty.
final int end;
InstructionRange({required this.ir, required this.start, required this.end});
bool containsAddress(int address) => start <= address && address < end;
@override
String toString() => '[$start, $end)';
}
/// Minimal representation of a function type in unit tests that use
/// [TestIRContainer].
class TestFunctionType {
final int parameterCount;
TestFunctionType(this.parameterCount);
}
/// Container for a sequence of IR instructions that aren't connected to an
/// analyzer AST data structure.
///
/// Suitable for use in unit tests that test the IR instructions directly rather
/// than generate them from a Dart AST.
///
/// To construct a sequence of IR instructions, see [TestIRWriter].
class TestIRContainer extends BaseIRContainer {
final Map<int, String> _addressToLabel;
final List<String?> _allocNames;
final List<TestFunctionType> _functionTypes;
final Map<String, int> _labelToAddress;
TestIRContainer(TestIRWriter super.writer)
: _addressToLabel = writer._addressToLabel,
_allocNames = writer._allocNames,
_functionTypes = writer._functionTypes,
_labelToAddress = writer._labelToAddress;
String? addressToLabel(int address) => _addressToLabel[address];
String? allocIndexToName(int index) => _allocNames[index];
@override
int countParameters(TypeRef type) =>
_functionTypes[type.index].parameterCount;
int? labelToAddress(String label) => _labelToAddress[label];
}
/// Writer of an IR instruction stream that's not connected to an analyzer AST
/// data structure.
///
/// Suitable for use in unit tests that test the IR instructions directly rather
/// than generate them from a Dart AST.
class TestIRWriter extends RawIRWriter {
final _addressToLabel = <int, String>{};
final _allocNames = <String?>[];
final _callDescriptorTable = <String>[];
final _callDescriptorToRef = <String, CallDescriptorRef>{};
final _functionTypes = <TestFunctionType>[];
final _labelToAddress = <String, int>{};
final _literalTable = <Object?>[];
final _literalToRef = <Object?, LiteralRef>{};
final _parameterCountToFunctionTypeMap = <int, TypeRef>{};
@override
void alloc(int count) {
var instructionLabel = _addressToLabel[nextInstructionAddress];
if (count == 1) {
_allocNames.add(instructionLabel);
} else {
for (var i = 0; i < count; i++) {
_allocNames.add(
instructionLabel == null ? null : '$instructionLabel$i',
);
}
}
super.alloc(count);
}
CallDescriptorRef encodeCallDescriptor(String name) =>
_callDescriptorToRef.putIfAbsent(name, () {
var encoding = CallDescriptorRef(_callDescriptorTable.length);
_callDescriptorTable.add(name);
return encoding;
});
TypeRef encodeFunctionType({required int parameterCount}) =>
_parameterCountToFunctionTypeMap.putIfAbsent(parameterCount, () {
var encoding = TypeRef(_functionTypes.length);
_functionTypes.add(TestFunctionType(parameterCount));
return encoding;
});
LiteralRef encodeLiteral(Object? value) =>
_literalToRef.putIfAbsent(value, () {
var encoding = LiteralRef(_literalTable.length);
_literalTable.add(value);
return encoding;
});
void label(String name) {
assert(!_labelToAddress.containsKey(name), 'Duplicate label $name');
_labelToAddress[name] = nextInstructionAddress;
_addressToLabel[nextInstructionAddress] = name;
}
/// Convenience method for creating an ordinary function (not a method, not
/// async, not a generator).
void ordinaryFunction({int parameterCount = 0}) => function(
encodeFunctionType(parameterCount: parameterCount),
FunctionFlags(),
);
}
/// Testing methods for [AstNodes].
extension SubjectAstNodes on Subject<AstNodes> {
/// Verifies that an entry exists for [node], and returns a [Subject] that
/// allows its properties to be tested.
@meta.useResult
Subject<InstructionRange> operator [](AstNode node) {
return context.nest(
() => prefixFirst('contains ${node.runtimeType} ', literal(node)),
(astNodes) {
if (astNodes[node] case var range?) return Extracted.value(range);
return Extracted.rejection(
which: prefixFirst(
'does not contain ${node.runtimeType} ',
literal(node),
),
);
},
);
}
/// Verifies that an entry exists for [node].
void containsNode(AstNode node) {
context.expect(
() => prefixFirst('contains ${node.runtimeType} ', literal(node)),
(astNodes) {
if (astNodes[node] != null) return null;
return Rejection(
which: prefixFirst(
'does not contain ${node.runtimeType} ',
literal(node),
),
);
},
);
}
}
/// Testing methods for [Instruction].
extension SubjectInstruction on Subject<Instruction> {
@meta.useResult
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].
extension SubjectInstructionRange on Subject<InstructionRange> {
@meta.useResult
Subject<int> get end => has((range) => range.end, 'end');
@meta.useResult
Subject<List<Instruction>> get instructions => has(
(range) => [
for (var address = range.start; address < range.end; address++)
Instruction(range.ir, address),
],
'instructions',
);
@meta.useResult
Subject<int> get start => has((range) => range.start, 'start');
/// Verifies that [subrange] is contained within the subject range.
///
/// The check passes if [subrange] is the same as the subject range.
void containsSubrange(InstructionRange subrange) {
context.expect(() => prefixFirst('contains subrange ', literal(subrange)), (
range,
) {
if (range.start <= subrange.start && subrange.end <= range.end) {
return null;
}
return Rejection(
which: prefixFirst('does not contain subrange ', literal(subrange)),
);
});
}
}