From 4dea6268f27dcc3d4a39235e4dba3989c675510d Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Sat, 18 Nov 2023 14:14:48 +0000 Subject: [PATCH] Add support for properties to the Wolf analysis prototype. The AST-to-IR conversion stage now handles reads and writes of properties, whether through a `SimpleIdentifier` (via implicit `this`), a `PrefixedIdentifier`, or a `PropertyAccess` AST node. In order to make this easier to test, support was also added for parenthesized expressions. This required adding the following instruction types: `call` and `shuffle`. To allow for thorough testing, support for these instruction types was added to the interpreter and validator. Change-Id: Ic60452422a877f358b0cad31b3c5664fd0585809 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/335701 Commit-Queue: Paul Berry Reviewed-by: Phil Quitslund --- .../lib/src/test_utilities/mock_sdk.dart | 1 + pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart | 122 +++++++++++++++ .../lib/src/wolf/ir/call_descriptor.dart | 50 ++++++ pkg/analyzer/lib/src/wolf/ir/coded_ir.dart | 29 +++- pkg/analyzer/lib/src/wolf/ir/interpreter.dart | 47 +++++- pkg/analyzer/lib/src/wolf/ir/ir.dart | 140 ++++++++++++++++- pkg/analyzer/lib/src/wolf/ir/ir.g.dart | 62 +++++++- pkg/analyzer/lib/src/wolf/ir/validator.dart | 15 ++ .../test/src/wolf/ir/ast_to_ir_test.dart | 148 +++++++++++++++++- pkg/analyzer/test/src/wolf/ir/utils.dart | 15 ++ .../test/src/wolf/ir/validator_test.dart | 68 ++++++++ pkg/analyzer/tool/wolf/generate.dart | 8 + 12 files changed, 690 insertions(+), 15 deletions(-) create mode 100644 pkg/analyzer/lib/src/wolf/ir/call_descriptor.dart diff --git a/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart b/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart index 2da81d05364d..61ca8dd4d6ed 100644 --- a/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart +++ b/pkg/analyzer/lib/src/test_utilities/mock_sdk.dart @@ -493,6 +493,7 @@ class List implements Iterable { external factory List.unmodifiable(Iterable elements); E get last => throw 0; + set length(int newLength) {} E operator [](int index) => throw 0; void operator []=(int index, E value) {} diff --git a/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart b/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart index dbb397deb4b4..7cc5b9ed534a 100644 --- a/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart +++ b/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart @@ -8,6 +8,7 @@ import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.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'; @@ -71,13 +72,33 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> { final AstToIREventListener eventListener; final ir = CodedIRWriter(); final Map locals = {}; + late final oneArgument = ir.encodeArgumentNames([null]); + late final twoArguments = ir.encodeArgumentNames([null, null]); late final null_ = ir.encodeLiteral(null); + late final stackIndices101 = ir.encodeStackIndices(const [1, 0, 1]); _AstToIRVisitor( {required this.typeSystem, required this.typeProvider, required this.eventListener}); + /// If [node] is used as the target of a [CompoundAssignmentExpression], + /// returns the [CompoundAssignmentExpression]. + CompoundAssignmentExpression? assignmentTargeting(AstNode node) { + while (true) { + var parent = node.parent!; + switch (parent) { + case PrefixedIdentifier() when identical(node, parent.identifier): + case PropertyAccess() when identical(node, parent.propertyName): + node = parent; + case AssignmentExpression() when identical(node, parent.leftHandSide): + return parent; + case dynamic(:var runtimeType): + throw UnimplementedError('TODO(paulberry): $runtimeType'); + } + } + } + /// Visits L-value [node] and returns the templates for reading/writing it. _LValueTemplates dispatchLValue(Expression node) => node.accept(this)!; @@ -98,6 +119,22 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> { return result; } + void instanceGet(PropertyAccessorElement? staticElement, String name) { + if (staticElement == null) { + throw UnimplementedError('TODO(paulberry): dynamic instance get'); + } + ir.call(ir.encodeCallDescriptor(InstanceGetDescriptor(staticElement)), + oneArgument); + } + + void instanceSet(PropertyAccessorElement? staticElement, String name) { + if (staticElement == null) { + throw UnimplementedError('TODO(paulberry): dynamic instance set'); + } + ir.call(ir.encodeCallDescriptor(InstanceSetDescriptor(staticElement)), + twoArguments); + } + Null this_() { ir.readLocal(0); // Stack: this } @@ -210,6 +247,37 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> { // Stack: null } + @override + Null visitParenthesizedExpression(ParenthesizedExpression node) { + dispatchNode(node.expression); + // Stack: expression + } + + @override + _LValueTemplates? visitPrefixedIdentifier(PrefixedIdentifier node) { + var prefix = node.prefix; + var prefixElement = prefix.staticElement; + switch (prefixElement) { + case ParameterElement(): + case LocalVariableElement(): + dispatchNode(prefix); + // Stack: prefix + return _PropertyAccessTemplates(node.identifier); + case dynamic(:var runtimeType): + throw UnimplementedError( + 'TODO(paulberry): $runtimeType: $prefixElement'); + } + } + + @override + _LValueTemplates visitPropertyAccess(PropertyAccess node) { + // TODO(paulberry): handle null shorting + // TODO(paulberry): handle cascades + dispatchNode(node.target!); + // Stack: target + return _PropertyAccessTemplates(node.propertyName); + } + @override Null visitReturnStatement(ReturnStatement node) { switch (node.expression) { @@ -229,10 +297,20 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> { @override _LValueTemplates visitSimpleIdentifier(SimpleIdentifier node) { var staticElement = node.staticElement; + if (staticElement == null) { + if (assignmentTargeting(node) case var assignment?) { + staticElement = assignment.readElement ?? assignment.writeElement; + } + } switch (staticElement) { case ParameterElement(): case LocalVariableElement(): return _LocalTemplates(locals[staticElement]!); + case PropertyAccessorElement(isStatic: false): + this_(); + // Stack: this + return _PropertyAccessTemplates(node); + // Stack: value case dynamic(:var runtimeType): throw UnimplementedError( 'TODO(paulberry): $runtimeType: $staticElement'); @@ -334,3 +412,47 @@ sealed class _LValueTemplates { /// On exit, the stack contents will be the value that was written. void write(_AstToIRVisitor visitor); } + +/// Instruction templates for converting a property access to IR. +/// +// TODO(paulberry): handle null shorting +class _PropertyAccessTemplates extends _LValueTemplates { + final SimpleIdentifier property; + + /// Creates a property access template. + /// + /// Caller is responsible for ensuring that the target of the property access + /// is pushed to the stack. + _PropertyAccessTemplates(this.property); + + void read(_AstToIRVisitor visitor) { + // Stack: target + visitor.instanceGet( + (property.staticElement ?? + visitor.assignmentTargeting(property)?.readElement) + as PropertyAccessorElement?, + property.name); + // Stack: value + } + + @override + void simpleRead(_AstToIRVisitor visitor) { + // Stack: target + read(visitor); + // Stack: value + } + + @override + void write(_AstToIRVisitor visitor) { + // Stack: target value + visitor.ir.shuffle(2, visitor.stackIndices101); + // Stack: value target value + visitor.instanceSet( + visitor.assignmentTargeting(property)!.writeElement + as PropertyAccessorElement?, + property.name); + // Stack: value returnValue + visitor.ir.drop(); + // Stack: value + } +} diff --git a/pkg/analyzer/lib/src/wolf/ir/call_descriptor.dart b/pkg/analyzer/lib/src/wolf/ir/call_descriptor.dart new file mode 100644 index 000000000000..46c6cdbf05ce --- /dev/null +++ b/pkg/analyzer/lib/src/wolf/ir/call_descriptor.dart @@ -0,0 +1,50 @@ +// 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/element.dart'; + +/// The target of a `call` instruction in the IR. +sealed class CallDescriptor { + String get name; +} + +/// Call descriptor for an instance (non-static) getter. +class InstanceGetDescriptor extends CallDescriptor { + final PropertyAccessorElement getter; + + InstanceGetDescriptor(this.getter) : assert(getter.isGetter); + + @override + int get hashCode => getter.hashCode; + + @override + String get name => getter.name; + + @override + bool operator ==(Object other) => + other is InstanceGetDescriptor && getter == other.getter; + + @override + String toString() => '${getter.enclosingElement.name}.$name'; +} + +/// Call descriptor for an instance (non-static) setter. +class InstanceSetDescriptor extends CallDescriptor { + final PropertyAccessorElement setter; + + InstanceSetDescriptor(this.setter) : assert(setter.isSetter); + + @override + int get hashCode => setter.hashCode; + + @override + String get name => setter.name; + + @override + bool operator ==(Object other) => + other is InstanceSetDescriptor && setter == other.setter; + + @override + String toString() => '${setter.enclosingElement.name}.$name'; +} diff --git a/pkg/analyzer/lib/src/wolf/ir/coded_ir.dart b/pkg/analyzer/lib/src/wolf/ir/coded_ir.dart index 6f4d2db2354c..c2580a361b9e 100644 --- a/pkg/analyzer/lib/src/wolf/ir/coded_ir.dart +++ b/pkg/analyzer/lib/src/wolf/ir/coded_ir.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/src/wolf/ir/call_descriptor.dart'; import 'package:analyzer/src/wolf/ir/ir.dart'; /// Container for a sequence of IR instructions, along with auxiliary tables @@ -14,17 +15,26 @@ import 'package:analyzer/src/wolf/ir/ir.dart'; /// /// To construct a sequence of IR instructions, see [CodedIRWriter]. class CodedIRContainer extends BaseIRContainer { + final List _callDescriptorTable; final List _literalTable; final List _typeTable; CodedIRContainer(CodedIRWriter super.writer) - : _literalTable = writer._literalTable, + : _callDescriptorTable = writer._callDescriptorTable, + _literalTable = writer._literalTable, _typeTable = writer._typeTable; + @override + String callDescriptorRefToString(CallDescriptorRef callDescriptor) => + decodeCallDescriptor(callDescriptor).toString(); + @override int countParameters(TypeRef type) => (decodeType(type) as FunctionType).parameters.length; + CallDescriptor decodeCallDescriptor(CallDescriptorRef callDescriptorRef) => + _callDescriptorTable[callDescriptorRef.index]; + Object? decodeLiteral(LiteralRef literal) => _literalTable[literal.index]; DartType decodeType(TypeRef type) => _typeTable[type.index]; @@ -33,6 +43,11 @@ class CodedIRContainer extends BaseIRContainer { String literalRefToString(LiteralRef value) => json.encode(decodeLiteral(value)); + /// Applies [f] to each call descriptor in the call descriptor table, and + /// gathers the results into a list. + List mapCallDescriptors(T Function(CallDescriptor) f) => + _callDescriptorTable.map(f).toList(); + @override String typeRefToString(TypeRef type) => decodeType(type).toString(); } @@ -42,11 +57,21 @@ class CodedIRContainer extends BaseIRContainer { /// /// See [RawIRWriter] for more information. class CodedIRWriter extends RawIRWriter { + final _callDescriptorTable = []; + final _callDescriptorToRef = {}; final _literalTable = []; - final _typeTable = []; final _literalToRef = {}; + final _typeTable = []; final _typeToRef = {}; + CallDescriptorRef encodeCallDescriptor(CallDescriptor callDescriptor) => + // TODO(paulberry): is `putIfAbsent` the best-performing way to do this? + _callDescriptorToRef.putIfAbsent(callDescriptor, () { + var encoding = CallDescriptorRef(_callDescriptorTable.length); + _callDescriptorTable.add(callDescriptor); + return encoding; + }); + LiteralRef encodeLiteral(Object? value) => // TODO(paulberry): is `putIfAbsent` the best-performing way to do this? _literalToRef.putIfAbsent(value, () { diff --git a/pkg/analyzer/lib/src/wolf/ir/interpreter.dart b/pkg/analyzer/lib/src/wolf/ir/interpreter.dart index 01d9da06333d..0645dfc6638a 100644 --- a/pkg/analyzer/lib/src/wolf/ir/interpreter.dart +++ b/pkg/analyzer/lib/src/wolf/ir/interpreter.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. 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:meta/meta.dart'; @@ -13,12 +14,21 @@ import 'package:meta/meta.dart'; /// 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 args) => - _IRInterpreter(ir).run(args); +Object? interpret(CodedIRContainer ir, List args, + {required CallHandler Function(CallDescriptor) callDispatcher}) { + return _IRInterpreter(ir, callDispatcher: callDispatcher).run(args); +} + +/// Function type invoked by [interpret] to execute a `call` instruction. +typedef CallHandler = Object? Function( + List positionalArguments, Map namedArguments); /// Interpreter representation of a heap object. /// @@ -54,11 +64,14 @@ class SoundnessError extends Error { class _IRInterpreter { final CodedIRContainer ir; + final List callHandlers; final stack = []; final locals = <_LocalSlot>[]; var address = 1; - _IRInterpreter(this.ir); + _IRInterpreter(this.ir, + {required CallHandler Function(CallDescriptor) callDispatcher}) + : callHandlers = ir.mapCallDescriptors(callDispatcher); /// Performs the necessary logic for a `br`, `brIf`, or `brIndex` instruction. /// @@ -94,6 +107,24 @@ class _IRInterpreter { case Opcode.br: var nesting = Opcode.br.decodeNesting(ir, address); return branch(nesting); + 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 = []; + var namedArguments = {}; + 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]( + positionalArguments, namedArguments)); case Opcode.drop: stack.removeLast(); case Opcode.dup: @@ -114,6 +145,16 @@ class _IRInterpreter { 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(); diff --git a/pkg/analyzer/lib/src/wolf/ir/ir.dart b/pkg/analyzer/lib/src/wolf/ir/ir.dart index e8bda50c427f..74692ba54a80 100644 --- a/pkg/analyzer/lib/src/wolf/ir/ir.dart +++ b/pkg/analyzer/lib/src/wolf/ir/ir.dart @@ -11,8 +11,36 @@ /// without requiring the implementer to handle every possible Dart construct. library; +import 'dart:collection'; +import 'dart:convert'; + +import 'package:analyzer/src/generated/utilities_collection.dart'; + part 'ir.g.dart'; +/// Wrapper for an integer representing a list of nullable argument names. +/// +/// The actual list isn't stored directly in the instruction stream. This +/// integer is an index into an auxiliary table stored in a subtype of +/// [BaseIRContainer]. +/// +// TODO(paulberry): when extension types are supported, make this an extension +// type. +class ArgumentNamesRef { + final int index; + + ArgumentNamesRef(this.index); + + @override + int get hashCode => index.hashCode; + + @override + bool operator ==(other) => other is ArgumentNamesRef && index == other.index; + + @override + String toString() => index.toString(); +} + /// Container for a sequence of IR instructions, which represents the body of a /// single function, method, getter, setter, constructor, or initializer /// expression. @@ -22,7 +50,11 @@ part 'ir.g.dart'; /// cache lines that need to be loaded in order to perform analysis. /// /// Other data structures that need to be referenced by the IR (e.g. types) are -/// stored in auxiliary tables, which are provided by subclasses. +/// stored in auxiliary tables. The auxiliary tables whose types don't depend on +/// analyzer types are provided by this class; the auxiliary tables whose types +/// do depend on analyzer types are provided by derived classes. (This allows +/// for some unit tests to use more lightweight types in place of analyzer +/// types). /// /// To construct a sequence of IR instructions, see [RawIRWriter]. abstract class BaseIRContainer with IRToStringMixin { @@ -32,18 +64,38 @@ abstract class BaseIRContainer with IRToStringMixin { final List _params0; @override final List _params1; + final List> _argumentNamesTable; + final List> _stackIndicesTable; BaseIRContainer(RawIRWriter writer) : _opcodes = writer._opcodes, _params0 = writer._params0, - _params1 = writer._params1; + _params1 = writer._params1, + _argumentNamesTable = writer._argumentNamesTable, + _stackIndicesTable = writer._stackIndicesTable; int get endAddress => _opcodes.length; + @override + String argumentNamesRefToString(ArgumentNamesRef argumentNames) => [ + for (var literal in decodeArgumentNames(argumentNames)) + json.encode(literal) + ].toString(); + + @override + String callDescriptorRefToString(CallDescriptorRef callDescriptor) => + 'callDescriptor#${callDescriptor.index}'; + /// Given a [TypeRef] that represents a function type, returns the number of /// function parameters. int countParameters(TypeRef type); + List decodeArgumentNames(ArgumentNamesRef argumentNames) => + _argumentNamesTable[argumentNames.index]; + + List decodeStackIndices(StackIndicesRef stackIndices) => + _stackIndicesTable[stackIndices.index]; + @override String functionFlagsToString(FunctionFlags flags) => flags.describe(); @@ -53,10 +105,37 @@ abstract class BaseIRContainer with IRToStringMixin { @override Opcode opcodeAt(int address) => _opcodes[address]; + @override + String stackIndicesRefToString(StackIndicesRef stackIndices) => + decodeStackIndices(stackIndices).toString(); + @override String typeRefToString(TypeRef type) => 'typeRef#${type.index}'; } +/// Wrapper for an integer representing a call descriptor. +/// +/// The actual call descriptor isn't stored directly in the instruction stream. +/// This integer is an index into an auxiliary table stored in a subtype of +/// [BaseIRContainer]. +/// +// TODO(paulberry): when extension types are supported, make this an extension +// type. +class CallDescriptorRef { + final int index; + + CallDescriptorRef(this.index); + + @override + int get hashCode => index.hashCode; + + @override + bool operator ==(other) => other is CallDescriptorRef && index == other.index; + + @override + String toString() => index.toString(); +} + /// Flags describing properties of a function declaration that affect the /// interpretation of its body. /// @@ -138,12 +217,18 @@ abstract class RawIRContainerInterface { /// The second parameter of each encoded instruction. List get _params1; + String argumentNamesRefToString(ArgumentNamesRef argumentNames); + + String callDescriptorRefToString(CallDescriptorRef callDescriptor); + String functionFlagsToString(FunctionFlags flags); String literalRefToString(LiteralRef literal); Opcode opcodeAt(int address); + String stackIndicesRefToString(StackIndicesRef stackIndices); + String typeRefToString(TypeRef type); } @@ -154,8 +239,9 @@ abstract class RawIRContainerInterface { /// instance of this class, call methods in [_RawIRWriterMixin] to add the /// instructions, and then pass this object to [BaseIRContainer]. /// -/// Subclasses provide the ability to create reference to other data structures -/// (e.g. types), which are stored in auxiliary tables. +/// This class provides the ability to encode data in the auxiliary tables +/// provided by [BaseIRContainer]. Subclasses provide the ability to encode data +/// in other auxiliary tables. class RawIRWriter with _RawIRWriterMixin { @override final _opcodes = []; @@ -163,6 +249,12 @@ class RawIRWriter with _RawIRWriterMixin { final _params0 = []; @override final _params1 = []; + final _argumentNamesTable = >[]; + final _argumentNamesToRef = LinkedHashMap, ArgumentNamesRef>( + equals: listsEqual, hashCode: Object.hashAll); + final _stackIndicesTable = >[]; + final _stackIndicesToRef = LinkedHashMap, StackIndicesRef>( + equals: listsEqual, hashCode: Object.hashAll); int _localVariableCount = 0; @@ -176,6 +268,22 @@ class RawIRWriter with _RawIRWriterMixin { super.alloc(count); } + ArgumentNamesRef encodeArgumentNames(List argumentNames) => + // TODO(paulberry): is `putIfAbsent` the best-performing way to do this? + _argumentNamesToRef.putIfAbsent(argumentNames, () { + var encoding = ArgumentNamesRef(_argumentNamesTable.length); + _argumentNamesTable.add(argumentNames); + return encoding; + }); + + StackIndicesRef encodeStackIndices(List stackIndices) => + // TODO(paulberry): is `putIfAbsent` the best-performing way to do this? + _stackIndicesToRef.putIfAbsent(stackIndices, () { + var encoding = StackIndicesRef(_stackIndicesTable.length); + _stackIndicesTable.add(stackIndices); + return encoding; + }); + @override void release(int count) { _localVariableCount -= count; @@ -196,6 +304,30 @@ class RawIRWriter with _RawIRWriterMixin { } } +/// Wrapper for an integer representing a list of stack indices used by the +/// `shuffle` instruction. +/// +/// The actual list isn't stored directly in the instruction stream. This +/// integer is an index into an auxiliary table stored in a subtype of +/// [BaseIRContainer]. +/// +// TODO(paulberry): when extension types are supported, make this an extension +// type. +class StackIndicesRef { + final int index; + + StackIndicesRef(this.index); + + @override + int get hashCode => index.hashCode; + + @override + bool operator ==(other) => other is StackIndicesRef && index == other.index; + + @override + String toString() => index.toString(); +} + /// Wrapper for an integer representing a Dart type. /// /// The actual type isn't stored directly in the instruction stream. This diff --git a/pkg/analyzer/lib/src/wolf/ir/ir.g.dart b/pkg/analyzer/lib/src/wolf/ir/ir.g.dart index 356b07d1d695..84ae20b85a81 100644 --- a/pkg/analyzer/lib/src/wolf/ir/ir.g.dart +++ b/pkg/analyzer/lib/src/wolf/ir/ir.g.dart @@ -22,6 +22,12 @@ mixin _RawIRWriterMixin implements _RawIRWriterMixinInterface { _params1.add(0); } + void call(CallDescriptorRef callDescriptor, ArgumentNamesRef argumentNames) { + _opcodes.add(Opcode.call); + _params0.add(callDescriptor.index); + _params1.add(argumentNames.index); + } + void drop() { _opcodes.add(Opcode.drop); _params0.add(0); @@ -64,6 +70,12 @@ mixin _RawIRWriterMixin implements _RawIRWriterMixinInterface { _params1.add(0); } + void shuffle(int popCount, StackIndicesRef stackIndices) { + _opcodes.add(Opcode.shuffle); + _params0.add(popCount); + _params1.add(stackIndices.index); + } + void writeLocal(int localIndex) { _opcodes.add(Opcode.writeLocal); _params0.add(localIndex); @@ -95,6 +107,9 @@ mixin IRToStringMixin implements RawIRContainerInterface { case Opcode.dup: return 'dup'; + case Opcode.shuffle: + return 'shuffle(${Opcode.shuffle.decodePopCount(this, address)}, ${stackIndicesRefToString(Opcode.shuffle.decodeStackIndices(this, address))})'; + case Opcode.function: return 'function(${typeRefToString(Opcode.function.decodeType(this, address))}, ${functionFlagsToString(Opcode.function.decodeFlags(this, address))})'; @@ -103,6 +118,9 @@ mixin IRToStringMixin implements RawIRContainerInterface { case Opcode.br: return 'br(${Opcode.br.decodeNesting(this, address)})'; + + case Opcode.call: + return 'call(${callDescriptorRefToString(Opcode.call.decodeCallDescriptor(this, address))}, ${argumentNamesRefToString(Opcode.call.decodeArgumentNames(this, address))})'; default: return '???'; } @@ -143,6 +161,20 @@ class _ParameterShape3 extends Opcode { class _ParameterShape4 extends Opcode { const _ParameterShape4._(super.index) : super._(); + int decodePopCount(RawIRContainerInterface ir, int address) { + assert(ir.opcodeAt(address).index == index); + return ir._params0[address]; + } + + StackIndicesRef decodeStackIndices(RawIRContainerInterface ir, int address) { + assert(ir.opcodeAt(address).index == index); + return StackIndicesRef(ir._params1[address]); + } +} + +class _ParameterShape5 extends Opcode { + const _ParameterShape5._(super.index) : super._(); + TypeRef decodeType(RawIRContainerInterface ir, int address) { assert(ir.opcodeAt(address).index == index); return TypeRef(ir._params0[address]); @@ -154,8 +186,8 @@ class _ParameterShape4 extends Opcode { } } -class _ParameterShape5 extends Opcode { - const _ParameterShape5._(super.index) : super._(); +class _ParameterShape6 extends Opcode { + const _ParameterShape6._(super.index) : super._(); int decodeNesting(RawIRContainerInterface ir, int address) { assert(ir.opcodeAt(address).index == index); @@ -163,6 +195,22 @@ class _ParameterShape5 extends Opcode { } } +class _ParameterShape7 extends Opcode { + const _ParameterShape7._(super.index) : super._(); + + CallDescriptorRef decodeCallDescriptor( + RawIRContainerInterface ir, int address) { + assert(ir.opcodeAt(address).index == index); + return CallDescriptorRef(ir._params0[address]); + } + + ArgumentNamesRef decodeArgumentNames( + RawIRContainerInterface ir, int address) { + assert(ir.opcodeAt(address).index == index); + return ArgumentNamesRef(ir._params1[address]); + } +} + // TODO(paulberry): when extension types are supported, make this an extension // type, as well as all the `_ParameterShape` classes. class Opcode { @@ -177,9 +225,11 @@ class Opcode { static const literal = _ParameterShape2._(4); static const drop = _ParameterShape3._(5); static const dup = _ParameterShape3._(6); - static const function = _ParameterShape4._(7); - static const end = _ParameterShape3._(8); - static const br = _ParameterShape5._(9); + 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); String describe() => opcodeNameTable[index]; @@ -191,8 +241,10 @@ class Opcode { "literal", "drop", "dup", + "shuffle", "function", "end", "br", + "call", ]; } diff --git a/pkg/analyzer/lib/src/wolf/ir/validator.dart b/pkg/analyzer/lib/src/wolf/ir/validator.dart index 6f583e92c8f5..5029c798c33e 100644 --- a/pkg/analyzer/lib/src/wolf/ir/validator.dart +++ b/pkg/analyzer/lib/src/wolf/ir/validator.dart @@ -194,6 +194,10 @@ class _Validator { case Opcode.br: var nesting = Opcode.br.decodeNesting(ir, address); branch(nesting, conditional: false); + case Opcode.call: + var argumentNames = Opcode.call.decodeArgumentNames(ir, address); + popValues(ir.decodeArgumentNames(argumentNames).length); + pushValues(1); case Opcode.drop: popValues(1); case Opcode.dup: @@ -239,6 +243,17 @@ class _Validator { check(newLocalCount >= localCountFence, 'Local variable stack underflow'); localCount = newLocalCount; + case Opcode.shuffle: + var popCount = Opcode.shuffle.decodePopCount(ir, address); + var stackIndices = ir.decodeStackIndices( + Opcode.shuffle.decodeStackIndices(ir, address)); + check(popCount >= 0, 'Negative pop count'); + for (var stackIndex in stackIndices) { + check(stackIndex >= 0, 'Negative stack index'); + check(stackIndex < popCount, 'Stack index too large'); + } + popValues(popCount); + pushValues(stackIndices.length); case Opcode.writeLocal: var localIndex = Opcode.writeLocal.decodeLocalIndex(ir, address); check(localIndex >= 0, 'Negative local index'); diff --git a/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart b/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart index 64e75471db6a..61f80069f3bb 100644 --- a/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart +++ b/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart @@ -3,8 +3,10 @@ // BSD-style license that can be found in the LICENSE file. import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/src/error/codes.dart'; import 'package:analyzer/src/wolf/ir/ast_to_ir.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/interpreter.dart'; import 'package:analyzer/src/wolf/ir/ir.dart'; @@ -23,7 +25,21 @@ main() { @reflectiveTest class AstToIRTest extends AstToIRTestBase { - Object? runInterpreter(List args) => interpret(ir, args); + final _instanceGetHandlers = { + 'int.isEven': unaryFunction((i) => i.isEven), + 'String.length': unaryFunction((s) => s.length) + }; + + final _instanceSetHandlers = { + 'List.length=': binaryFunction( + (list, newLength) => list.values.length = newLength) + }; + + ListInstance makeList(List values) => ListInstance( + typeProvider.listType(typeProvider.objectQuestionType), values); + + Object? runInterpreter(List args) => + interpret(ir, args, callDispatcher: _callDispatcher); test_assignmentExpression_local_simple_sideEffect() async { await assertNoErrorsInCode(''' @@ -79,6 +95,49 @@ test(int i) => i = 123; check(runInterpreter([1])).equals(123); } + test_assignmentExpression_property_prefixedIdentifier_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.prefixed('l.length')]!) + ..containsSubrange(astNodes[findNode.integerLiteral('3')]!); + var l = ['a', 'b', 'c', 'd', 'e']; + check(runInterpreter([makeList(l)])).equals(3); + check(l).deepEquals(['a', 'b', 'c']); + } + + test_assignmentExpression_property_propertyAccess_simple() async { + await assertNoErrorsInCode(''' +test(List l) => (l).length = 3; +'''); + analyze(findNode.singleFunctionDeclaration); + check(astNodes)[findNode.assignment('(l).length = 3')] + ..containsSubrange(astNodes[findNode.parenthesized('(l)')]!) + ..containsSubrange(astNodes[findNode.propertyAccess('(l).length')]!) + ..containsSubrange(astNodes[findNode.integerLiteral('3')]!); + var l = ['a', 'b', 'c', 'd', 'e']; + check(runInterpreter([makeList(l)])).equals(3); + check(l).deepEquals(['a', 'b', 'c']); + } + + test_assignmentExpression_property_simpleIdentifier_simple() async { + await assertNoErrorsInCode(''' +extension E on List { + test() => length = 3; +} +'''); + analyze(findNode.singleMethodDeclaration); + check(astNodes)[findNode.assignment('length = 3')] + ..containsSubrange(astNodes[findNode.simple('length')]!) + ..containsSubrange(astNodes[findNode.integerLiteral('3')]!); + var l = ['a', 'b', 'c', 'd', 'e']; + check(runInterpreter([makeList(l)])).equals(3); + check(l).deepEquals(['a', 'b', 'c']); + } + test_block() async { await assertNoErrorsInCode(''' test(int i) { @@ -189,6 +248,48 @@ test() => null; check(runInterpreter([])).equals(null); } + test_parenthesizedExpression() async { + await assertNoErrorsInCode(''' +test(int i) => (i); +'''); + analyze(findNode.singleFunctionDeclaration); + check(astNodes)[findNode.parenthesized('(i)')] + .containsSubrange(astNodes[findNode.simple('i);')]!); + check(runInterpreter([123])).equals(123); + } + + test_propertyGet_prefixedIdentifier() async { + await assertNoErrorsInCode(''' +test(int i) => i.isEven; +'''); + analyze(findNode.singleFunctionDeclaration); + check(astNodes)[findNode.prefixed('i.isEven')] + .containsSubrange(astNodes[findNode.simple('i.')]!); + check(runInterpreter([1])).equals(false); + check(runInterpreter([2])).equals(true); + } + + test_propertyGet_propertyAccess() async { + await assertNoErrorsInCode(''' +test() => 'foo'.length; +'''); + analyze(findNode.singleFunctionDeclaration); + check(astNodes)[findNode.propertyAccess("'foo'.length")] + .containsSubrange(astNodes[findNode.stringLiteral("'foo'")]!); + check(runInterpreter([])).equals(3); + } + + test_propertyGet_simpleIdentifier() async { + await assertNoErrorsInCode(''' +extension E on String { + test() => length; +} +'''); + analyze(findNode.singleMethodDeclaration); + check(astNodes).containsNode(findNode.simple('length')); + check(runInterpreter(['foo'])).equals(3); + } + test_returnStatement_noValue() async { await assertNoErrorsInCode(''' test() { @@ -355,6 +456,44 @@ test() { astNodes[findNode.variableDeclarationList('int i = 123')]!); 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(Object? Function(T, U) f) => + (positionalArguments, namedArguments) { + check(positionalArguments).length.equals(2); + check(namedArguments).isEmpty(); + return f(positionalArguments[0] as T, positionalArguments[1] as U); + }; + + static CallHandler unaryFunction(Object? Function(T) f) => + (positionalArguments, namedArguments) { + check(positionalArguments).length.equals(1); + check(namedArguments).isEmpty(); + return f(positionalArguments[0] as T); + }; } class AstToIRTestBase extends PubPackageResolutionTest { @@ -380,6 +519,13 @@ class AstToIRTestBase extends PubPackageResolutionTest { } } +/// Interpreter representation of a [List] object. +class ListInstance extends Instance { + final List values; + + ListInstance(super.type, this.values); +} + extension on Subject { Subject get address => has((e) => e.address, 'address'); Subject get message => has((e) => e.message, 'message'); diff --git a/pkg/analyzer/test/src/wolf/ir/utils.dart b/pkg/analyzer/test/src/wolf/ir/utils.dart index c75adaf4e63a..e0dac62bf4c4 100644 --- a/pkg/analyzer/test/src/wolf/ir/utils.dart +++ b/pkg/analyzer/test/src/wolf/ir/utils.dart @@ -10,6 +10,12 @@ 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. /// @@ -147,12 +153,21 @@ class TestIRContainer extends BaseIRContainer { /// than generate them from a Dart AST. class TestIRWriter extends RawIRWriter { final _addressToLabel = {}; + final _callDescriptorTable = []; + final _callDescriptorToRef = {}; final _functionTypes = []; final _labelToAddress = {}; final _literalTable = []; final _literalToRef = {}; final _parameterCountToFunctionTypeMap = {}; + 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); diff --git a/pkg/analyzer/test/src/wolf/ir/validator_test.dart b/pkg/analyzer/test/src/wolf/ir/validator_test.dart index 3539012d0ade..c41f564c60e2 100644 --- a/pkg/analyzer/test/src/wolf/ir/validator_test.dart +++ b/pkg/analyzer/test/src/wolf/ir/validator_test.dart @@ -78,6 +78,25 @@ class ValidatorTest { _checkInvalidMessageAt('bad').equals('Negative branch nesting'); } + test_call_ok() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2))) + ..call(ir.encodeCallDescriptor('f'), ir.encodeArgumentNames([null, null])) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1))) + ..end()); + _validate(); + } + + test_call_underflow() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('bad') + ..call(ir.encodeCallDescriptor('f'), ir.encodeArgumentNames([null, null])) + ..end()); + _checkInvalidMessageAt('bad').equals('Value stack underflow'); + } + test_drop_ok() { _analyze((ir) => ir ..ordinaryFunction(parameterCount: 2) @@ -328,6 +347,55 @@ class ValidatorTest { _checkInvalidMessageAt('bad').equals('Local variable stack underflow'); } + test_shuffle_negativePopCount() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('bad') + ..shuffle(-1, ir.encodeStackIndices([])) + ..end()); + _checkInvalidMessageAt('bad').equals('Negative pop count'); + } + + test_shuffle_negativeStackIndex() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..label('bad') + ..shuffle(2, ir.encodeStackIndices([-1])) + ..end()); + _checkInvalidMessageAt('bad').equals('Negative stack index'); + } + + test_shuffle_ok() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2))) + ..shuffle(2, ir.encodeStackIndices([0, 1, 0, 1])) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(4))) + ..drop() + ..drop() + ..drop() + ..end()); + _validate(); + } + + test_shuffle_stackIndexTooLarge() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..label('bad') + ..shuffle(2, ir.encodeStackIndices([2])) + ..end()); + _checkInvalidMessageAt('bad').equals('Stack index too large'); + } + + test_shuffle_underflow() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('bad') + ..shuffle(2, ir.encodeStackIndices([0, 1, 0, 1])) + ..end()); + _checkInvalidMessageAt('bad').equals('Value stack underflow'); + } + test_stack_push() { _analyze((ir) => ir ..ordinaryFunction() diff --git a/pkg/analyzer/tool/wolf/generate.dart b/pkg/analyzer/tool/wolf/generate.dart index 10ac732a0982..272c3e024867 100644 --- a/pkg/analyzer/tool/wolf/generate.dart +++ b/pkg/analyzer/tool/wolf/generate.dart @@ -69,9 +69,12 @@ class _Instructions { _Instructions() { // Encodings + var callDescriptor = encoding('CallDescriptorRef'); + var argumentNames = encoding('ArgumentNamesRef'); var functionFlags = encoding('FunctionFlags', fieldName: '_flags', constructorName: '_'); var literal = encoding('LiteralRef'); + var stackIndices = encoding('StackIndicesRef'); var type = encoding('TypeRef'); const uint = _TrivialEncoding('int'); @@ -85,10 +88,15 @@ class _Instructions { // Stack manipulation _addInstruction('drop', []); _addInstruction('dup', []); + _addInstruction( + 'shuffle', [uint('popCount'), stackIndices('stackIndices')]); // Flow control _addInstruction('function', [type('type'), functionFlags('flags')]); _addInstruction('end', []); _addInstruction('br', [uint('nesting')]); + // Invocations and tearoffs + _addInstruction('call', + [(callDescriptor('callDescriptor')), (argumentNames('argumentNames'))]); } _NontrivialEncoding encoding(String type,