| // 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. | |
| /// | |
| /// [location] should be a string representing the location of the test logic | |
| /// that caused this temporary variable to be created (see [computeLocation]). | |
| /// | |
| /// See [let]. | |
| MiniIRTmp allocateTmp({required String location}) { | |
| return MiniIRTmp._( | |
| 't${_tmpCounter++}', | |
| _pop(Kind.expression).ir, | |
| location: location, | |
| ); | |
| } | |
| /// Pops the top [inputKinds].length 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', [Kind.expression, Kind.expression])` | |
| /// 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).ir})', | |
| 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; | |
| /// A string representing the location of the test logic that caused this | |
| /// temporary variable to be created (see [computeLocation]). | |
| final String location; | |
| MiniIRTmp._(this._name, this._value, {required this.location}); | |
| } |