| // 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)), |
| ); |
| }); |
| } |
| } |