Skip to content

Commit

Permalink
Add support for properties to the Wolf analysis prototype.
Browse files Browse the repository at this point in the history
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 <paulberry@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
  • Loading branch information
stereotype441 authored and Commit Queue committed Nov 18, 2023
1 parent 412bb9f commit 4dea626
Show file tree
Hide file tree
Showing 12 changed files with 690 additions and 15 deletions.
1 change: 1 addition & 0 deletions pkg/analyzer/lib/src/test_utilities/mock_sdk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ class List<E> implements Iterable<E> {
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) {}
Expand Down
122 changes: 122 additions & 0 deletions pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -71,13 +72,33 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> {
final AstToIREventListener eventListener;
final ir = CodedIRWriter();
final Map<VariableElement, int> 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)!;

Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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');
Expand Down Expand Up @@ -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
}
}
50 changes: 50 additions & 0 deletions pkg/analyzer/lib/src/wolf/ir/call_descriptor.dart
Original file line number Diff line number Diff line change
@@ -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';
}
29 changes: 27 additions & 2 deletions pkg/analyzer/lib/src/wolf/ir/coded_ir.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<CallDescriptor> _callDescriptorTable;
final List<Object?> _literalTable;
final List<DartType> _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];
Expand All @@ -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<T> mapCallDescriptors<T>(T Function(CallDescriptor) f) =>
_callDescriptorTable.map(f).toList();

@override
String typeRefToString(TypeRef type) => decodeType(type).toString();
}
Expand All @@ -42,11 +57,21 @@ class CodedIRContainer extends BaseIRContainer {
///
/// See [RawIRWriter] for more information.
class CodedIRWriter extends RawIRWriter {
final _callDescriptorTable = <CallDescriptor>[];
final _callDescriptorToRef = <CallDescriptor, CallDescriptorRef>{};
final _literalTable = <Object?>[];
final _typeTable = <DartType>[];
final _literalToRef = <Object?, LiteralRef>{};
final _typeTable = <DartType>[];
final _typeToRef = <DartType, TypeRef>{};

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, () {
Expand Down
47 changes: 44 additions & 3 deletions pkg/analyzer/lib/src/wolf/ir/interpreter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<Object?> args) =>
_IRInterpreter(ir).run(args);
Object? interpret(CodedIRContainer ir, List<Object?> 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<Object?> positionalArguments, Map<String, Object?> namedArguments);

/// Interpreter representation of a heap object.
///
Expand Down Expand Up @@ -54,11 +64,14 @@ class SoundnessError extends Error {

class _IRInterpreter {
final CodedIRContainer ir;
final List<CallHandler> callHandlers;
final stack = <Object?>[];
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.
///
Expand Down Expand Up @@ -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 = <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](
positionalArguments, namedArguments));
case Opcode.drop:
stack.removeLast();
case Opcode.dup:
Expand All @@ -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();
Expand Down
Loading

0 comments on commit 4dea626

Please sign in to comment.