| // 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'; |
| |
| /// 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 = <String>[]; |
| |
| /// 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()); |
| } |
| |
| /// 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)`. |
| void apply(String name, int numArgs) => |
| _push('$name(${_popList(numArgs).join(', ')})'); |
| |
| /// Pushes a node on the stack representing a single atomic expression (for |
| /// example a literal value or a variable reference). |
| void atom(String name) => _push(name); |
| |
| /// Verifies that the top node on the stack matches [expectedIr] exactly. |
| void check(String expectedIr) { |
| expect(_stack.last.toString(), expectedIr); |
| } |
| |
| /// 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 bool isAsynchronous}) { |
| var name = isAsynchronous ? 'forIn_async' : 'forIn'; |
| var body = _pop(); |
| var iterable = _pop(); |
| var variable = tmp == null ? _pop() : tmp._name; |
| _push('$name($variable, $iterable, $body)'); |
| } |
| |
| /// 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) { |
| _push('if(==(${tmp._name}, null), null, ${_pop()})'); |
| let(tmp); |
| } |
| |
| /// 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) { |
| var ifNull = _pop(); |
| var ifNotNull = _pop(); |
| _push('if(==(${tmp._name}, null), $ifNull, $ifNotNull)'); |
| let(tmp); |
| } |
| |
| /// Pushes a node representing a call to `operator[]` onto the stack, using |
| /// a receiver and an index obtained from the stack. |
| void indexGet() => apply('[]', 2); |
| |
| /// 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) { |
| var value = _pop(); |
| var index = indexTmp == null ? _pop() : indexTmp._name; |
| var receiver = receiverTmp == null ? _pop() : receiverTmp._name; |
| _push('[]=($receiver, $index, $value)'); |
| } |
| |
| /// 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) { |
| var name = label._name; |
| if (name != null) { |
| _push('labeled($name, ${_pop()})'); |
| } |
| } |
| |
| /// 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) { |
| _push('let(${tmp._name}, ${tmp._value}, ${_pop()})'); |
| } |
| |
| /// Pushes a node representing a property get onto the stack, using a receiver |
| /// obtained from the stack. |
| void propertyGet(String propertyName) => apply('get_$propertyName', 1); |
| |
| /// 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) { |
| var value = _pop(); |
| var receiver = receiverTmp == null ? _pop() : receiverTmp._name; |
| _push('set_$propertyName($receiver, $value)'); |
| } |
| |
| /// Pushes a node representing a read of [tmp] onto the stack. |
| void readTmp(MiniIrTmp tmp) => _push(tmp._name); |
| |
| /// Pushes a node representing a reference to [label] onto the stack. |
| void referToLabel(MiniIrLabel label) { |
| _push(label._name ??= 'L${_labelCounter++}'); |
| } |
| |
| @override |
| String toString() => _stack.join(', '); |
| |
| /// Pushes a node representing a read of a local variable onto the stack. |
| void variableGet(Var v) => atom(v.name); |
| |
| /// Pushes a node representing a set of a local variable onto the stack, using |
| /// a value obtained from the stack. |
| void variableSet(Var v) => apply('${v.name}=', 1); |
| |
| /// Pops a single node off the stack. |
| String _pop() { |
| expect(_stack.length, greaterThan(_popLimit)); |
| return _stack.removeLast(); |
| } |
| |
| /// Pops a list of nodes off the stack. |
| List<String> _popList(int count) { |
| var newLength = _stack.length - count; |
| expect(newLength, greaterThanOrEqualTo(_popLimit)); |
| var result = _stack.sublist(newLength); |
| _stack.length = newLength; |
| return result; |
| } |
| |
| /// Pushes a node onto the stack. |
| void _push(String 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); |
| } |