blob: 9ffa1ad6070ddca9f3042b031029add33daeadf3 [file] [log] [blame]
// Copyright (c) 2021, 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.
// This file implements a miniature string-based internal representation ("IR")
// of Dart code suitable for use in unit testing.
import 'package:test/test.dart';
import 'mini_ast.dart';
/// A single stack entry representing an intermediate representation of some
/// Dart code produced using the facilities of `mini_ast.dart`.
class IRNode {
/// The intermediate representation itself, expressed as a string.
final String ir;
/// The location of the Dart code that led to this [IRNode].
final String location;
/// The kind of entity represented by this [IRNode].
final Kind kind;
IRNode({required this.ir, required this.location, required this.kind});
@override
String toString() => '$kind $ir ($location)';
}
/// Kinds of entities that can be represented by an [IRNode].
enum Kind {
/// A single `case` or `default` clause in a switch statement or switch
/// expression.
caseHead,
/// A set of `case` or `default` clauses in a switch statement, which all
/// share the same body.
caseHeads,
/// A collection element.
collectionElement,
/// An expression.
expression,
/// A single case from a switch expression, consisting of a [caseHead] and a
/// body [expression].
expressionCase,
/// A label for `break` or `continue` to branch to.
label,
/// A map pattern element.
mapPatternElement,
/// A pattern.
pattern,
/// A statement.
statement,
/// A single case from a switch statement, consisting of [caseHeads] and body
/// [statement]s.
statementCase,
/// A type in the type system.
type,
/// A local variable.
variable,
/// A set of pattern variables.
variables,
}
/// Stack-based builder class allowing construction of a miniature string-based
/// internal representation ("IR") of Dart code suitable for use in unit
/// testing.
class MiniIRBuilder {
/// Set this to `true` to enable print-based tracing of stack operations.
static const bool _debug = false;
/// If [_debug] is enabled, number of outstanding calls to [guard]. This
/// controls indentation of debug output.
int _guardDepth = 0;
/// Number of labels allocated so far.
int _labelCounter = 0;
/// Size threshold for [_stack]. [_pop] and [_popList] will cause a test
/// failure if an attempt is made to reduce the stack size to less than this
/// amount. See [guard].
int _popLimit = 0;
/// Stack of partially built IR nodes.
final _stack = <IRNode>[];
/// Number of temporaries allocated so far.
int _tmpCounter = 0;
/// Creates a fresh [MiniIRLabel] representing a label that can be used as a
/// break target.
///
/// See [labeled].
MiniIRLabel allocateLabel() => MiniIRLabel._();
/// Pops the top node from the stack (which should represent an expression)
/// and creates a fresh [MiniIRTmp] representing a temporary variable whose
/// initializer is that expression.
///
/// See [let].
MiniIRTmp allocateTmp() {
return MiniIRTmp._('t${_tmpCounter++}', _pop(Kind.expression).ir);
}
/// Pops the top [numArgs] nodes from the stack and pushes a node that
/// combines them using [name]. For example, if the stack contains `1, 2, 3`,
/// calling `apply('f', 2)` results in a stack of `1, f(2, 3)`.
///
/// Optional argument [names] allows applying names to the last n arguments.
/// For example, if the stack contains `1, 2, 3`, calling
/// `apply('f', 3, names: ['a', 'b'])` results in a stack of
/// `f(1, a: 2, b: 3)`.
void apply(String name, List<Kind> inputKinds, Kind outputKind,
{required String location, List<String> names = const []}) {
var args = [
for (var irNode in _popList(inputKinds.length, inputKinds)) irNode.ir
];
for (int i = 1; i <= names.length; i++) {
args[args.length - i] =
'${names[names.length - i]}: ${args[args.length - i]}';
}
_push(IRNode(
ir: '$name(${args.join(', ')})', kind: outputKind, location: location));
}
/// Pushes a node on the stack representing a single atomic expression (for
/// example a literal value or a variable reference).
void atom(String name, Kind kind, {required String location}) =>
_push(IRNode(ir: name, kind: kind, location: location));
/// Verifies that the top node on the stack matches [expectedIR] exactly.
void check(String expectedIR, Kind expectedKind, {required String location}) {
expect(_stack.last, _nodeWithKind(expectedKind), reason: 'at $location');
expect(_stack.last.ir, expectedIR, reason: 'at $location');
}
/// Pushes a node representing a `for-in` loop onto the stack, using a loop
/// variable, iterable expression, and loop body obtained from the stack.
///
/// If [tmp] is non-null, it is used as the loop variable instead of obtaining
/// it from the stack.
void forIn(MiniIRTmp? tmp,
{required String location, required bool isAsynchronous}) {
var name = isAsynchronous ? 'forIn_async' : 'forIn';
var body = _pop(Kind.statement);
var iterable = _pop(Kind.expression);
var variable = tmp == null ? _pop(Kind.variable) : tmp._name;
_push(IRNode(
ir: '$name($variable, $iterable, $body)',
kind: Kind.statement,
location: location));
}
/// Executes [callback], checking that it leaves all nodes presently on the
/// stack untouched, and results in exactly one node being added to the stack.
T guard<T>(Node node, T Function() callback) {
if (_debug) {
print(' ' * _guardDepth++ + '$node');
}
int previousStackDepth = _stack.length;
int previousPopLimit = _popLimit;
_popLimit = previousStackDepth;
var result = callback();
var stackDelta = _stack.length - previousStackDepth;
if (stackDelta != 1) {
fail('Stack delta of $stackDelta while visiting '
'${node.runtimeType} $node\n'
'Stack: $this');
}
if (_debug) {
print(' ' * --_guardDepth + '=> ${_stack.last}');
}
_popLimit = previousPopLimit;
return result;
}
/// Pushes a node representing an "if not null" check onto the stack, using
/// [tmp] and an expression obtained from the stack. The intended semantics
/// are `tmp == null ? null : <expression>`.
///
/// This is intended to be used as a building block for null shorting
/// operations.
void ifNotNull(MiniIRTmp tmp, {required String location}) {
_push(IRNode(
ir: 'if(==(${tmp._name}, null), null, ${_pop(Kind.expression)})',
kind: Kind.expression,
location: location));
let(tmp, location: location);
}
/// Pushes a node representing an "if null" check onto the stack, using [tmp]
/// and two expressions obtained from the stack. The intended semantics
/// are `tmp == null ? <expression 1> : <expression 2>`.
///
/// This is intended to be used as a building block for null `??` and `??=`
/// operations.
void ifNull(MiniIRTmp tmp, {required String location}) {
var ifNull = _pop(Kind.expression);
var ifNotNull = _pop(Kind.expression);
_push(IRNode(
ir: 'if(==(${tmp._name}, null), $ifNull, $ifNotNull)',
kind: Kind.expression,
location: location));
let(tmp, location: location);
}
/// Pushes a node representing a call to `operator[]` onto the stack, using
/// a receiver and an index obtained from the stack.
void indexGet({required String location}) =>
apply('[]', [Kind.expression, Kind.expression], Kind.expression,
location: location);
/// Pushes a node representing a call to `operator[]=` onto the stack, using
/// a receiver, index, and value obtained from the stack.
///
/// If [receiverTmp] and/or [indexTmp] is non-null, they are used instead of
/// obtaining values from the stack.
void indexSet(MiniIRTmp? receiverTmp, MiniIRTmp? indexTmp,
{required String location}) {
var value = _pop(Kind.expression);
var index = indexTmp == null ? _pop(Kind.expression) : indexTmp._name;
var receiver =
receiverTmp == null ? _pop(Kind.expression) : receiverTmp._name;
_push(IRNode(
ir: '[]=($receiver, $index, $value)',
kind: Kind.expression,
location: location));
}
/// Pushes a node representing a labeled statement onto the stack, using an
/// inner statement obtained from the stack.
///
/// To build up a statement of the form `labeled(L0, stmt)` (where `stmt`
/// might refer to `L0`), do the following operations:
/// - Call [allocateLabel] to prepare the label.
/// - build `stmt` on the stack, using [referToLabel] to refer to label as
/// needed.
/// - Call [labeled] to build the final `labeled` statement.
void labeled(MiniIRLabel label, {required String location}) {
var name = label._name;
if (name != null) {
_push(IRNode(
ir: 'labeled($name, ${_pop(Kind.statement)})',
kind: Kind.statement,
location: location));
}
}
/// Pushes a node representing a `let` expression onto the stack, using a
/// value obtained from the stack.
///
/// To build up an expression of the form `let(#0, value, expr)` (meaning
/// "let temporary variable #0 take on value while evaluating expr"), do the
/// following operations:
/// - Build `value` on the stack.
/// - Call [allocateTmp] to pop `value` off the stack and obtain a
/// [MiniIRTmp] object. This will assign the temporary variable a name that
/// doesn't conflict with any other outstanding temporary variables.
/// - Build `expr` on the stack, using [readTmp] to refer to the temporary
/// variable as needed.
/// - Call [let] to build the final `let` expression.
void let(MiniIRTmp tmp, {required String location}) {
_push(IRNode(
ir: 'let(${tmp._name}, ${tmp._value}, ${_pop(Kind.expression).ir})',
kind: Kind.expression,
location: location));
}
/// Pushes a node representing a property get onto the stack, using a receiver
/// obtained from the stack.
void propertyGet(String propertyName, {required String location}) =>
apply('get_$propertyName', [Kind.expression], Kind.expression,
location: location);
/// Pushes a node representing a property set onto the stack, using a receiver
/// and value obtained from the stack.
///
/// If [receiverTmp] is non-null, it is used as the receiver rather than
/// obtaining it from the stack.
void propertySet(MiniIRTmp? receiverTmp, String propertyName,
{required String location}) {
var value = _pop(Kind.expression);
var receiver =
receiverTmp == null ? _pop(Kind.expression) : receiverTmp._name;
_push(IRNode(
ir: 'set_$propertyName($receiver, $value)',
kind: Kind.expression,
location: location));
}
/// Pushes a node representing a read of [tmp] onto the stack.
void readTmp(MiniIRTmp tmp, {required String location}) =>
_push(IRNode(ir: tmp._name, kind: Kind.expression, location: location));
/// Pushes a node representing a reference to [label] onto the stack.
void referToLabel(MiniIRLabel label, {required String location}) {
_push(IRNode(
ir: label._name ??= 'L${_labelCounter++}',
kind: Kind.label,
location: location));
}
@override
String toString() => [for (var irNode in _stack) irNode.ir].join(', ');
/// Pushes a node representing a read of a local variable onto the stack.
void variableGet(Var v, {required String location}) =>
atom(v.name, Kind.expression, location: location);
/// Pushes a node representing a set of a local variable onto the stack, using
/// a value obtained from the stack.
void variableSet(Var v, {required String location}) =>
apply('${v.name}=', [Kind.expression], Kind.expression,
location: location);
TypeMatcher<IRNode> _nodeWithKind(Kind expectedKind) =>
TypeMatcher<IRNode>().having((node) => node.kind, 'kind', expectedKind);
/// Pops a single node off the stack.
IRNode _pop(Kind expectedKind) {
expect(_stack.length, greaterThan(_popLimit));
var irNode = _stack.removeLast();
expect(irNode, _nodeWithKind(expectedKind));
return irNode;
}
/// Pops a list of nodes off the stack.
List<IRNode> _popList(int count, List<Kind> expectedKinds) {
var newLength = _stack.length - count;
expect(newLength, greaterThanOrEqualTo(_popLimit));
var result = _stack.sublist(newLength);
_stack.length = newLength;
expect(result,
[for (var expectedKind in expectedKinds) _nodeWithKind(expectedKind)]);
return result;
}
/// Pushes a node onto the stack.
void _push(IRNode node) {
_stack.add(node);
}
}
/// Representation of a branch target label used by [MiniIRBuilder] when
/// building up `labeled` statements.
class MiniIRLabel {
/// The name of the label, or `null` if no name has been assigned yet.
String? _name;
MiniIRLabel._();
}
/// Representation of a temporary variable used by [MiniIRBuilder] when building
/// up `let` expressions.
class MiniIRTmp {
/// The name of the temporary variable.
final String _name;
/// The initial value of the temporary variable.
final String _value;
MiniIRTmp._(this._name, this._value);
}