Version 2.13.0-231.0.dev

Merge commit '4d065168321b60005fc1c2cc63fd45268e21649e' into 'dev'
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index 6ec266d..b8ce9e9 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -9,6 +9,7 @@
 import 'package:_fe_analyzer_shared/src/flow_analysis/flow_analysis.dart';
 import 'package:test/test.dart';
 
+import 'mini_ir.dart';
 import 'mini_types.dart';
 
 Expression get nullLiteral => new _NullLiteral();
@@ -247,7 +248,7 @@
 
   void _preVisit(AssignedVariables<Node, Var> assignedVariables);
 
-  Type _visit(Harness h);
+  Type _visit(Harness h, Type context);
 }
 
 /// Test harness for creating flow analysis tests.  This class implements all
@@ -390,6 +391,8 @@
   Harness({this.legacy = false, String? thisType})
       : thisType = thisType == null ? null : Type(thisType);
 
+  MiniIrBuilder get _irBuilder => _typeAnalyzer._irBuilder;
+
   /// Updates the harness so that when a [factor] query is invoked on types
   /// [from] and [what], [result] will be returned.
   void addFactor(String from, String what, String result) {
@@ -679,7 +682,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     return h._typeAnalyzer.analyzeTypeCast(this, target, type);
   }
 }
@@ -703,6 +706,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeAssertStatement(condition, message);
+    h._irBuilder.apply('assert', 2);
   }
 }
 
@@ -725,6 +729,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeBlock(statements);
+    h._irBuilder.apply('block', statements.length);
   }
 }
 
@@ -740,8 +745,10 @@
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
 
   @override
-  Type _visit(Harness h) {
-    return h._typeAnalyzer.analyzeBoolLiteral(this, value);
+  Type _visit(Harness h, Type context) {
+    var type = h._typeAnalyzer.analyzeBoolLiteral(this, value);
+    h._irBuilder.atom('$value');
+    return type;
   }
 }
 
@@ -759,6 +766,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeBreakStatement(target);
+    h._irBuilder.apply('break', 0);
   }
 }
 
@@ -806,6 +814,7 @@
   @override
   void _visit(Harness h) {
     expect(h._flow.isAssigned(variable), expectedAssignedState);
+    h._irBuilder.atom('null');
   }
 }
 
@@ -831,6 +840,7 @@
   void _visit(Harness h) {
     var promotedType = h._flow.promotedType(variable);
     expect(promotedType?.type, expectedTypeStr, reason: '$_creationTrace');
+    h._irBuilder.atom('null');
   }
 }
 
@@ -848,6 +858,7 @@
   @override
   void _visit(Harness h) {
     expect(h._flow.isReachable, expectedReachable);
+    h._irBuilder.atom('null');
   }
 }
 
@@ -869,6 +880,7 @@
   @override
   void _visit(Harness h) {
     expect(h._flow.isUnassigned(variable), expectedUnassignedState);
+    h._irBuilder.atom('null');
   }
 }
 
@@ -892,9 +904,11 @@
   }
 
   @override
-  Type _visit(Harness h) {
-    return h._typeAnalyzer
+  Type _visit(Harness h, Type context) {
+    var type = h._typeAnalyzer
         .analyzeConditionalExpression(this, condition, ifTrue, ifFalse);
+    h._irBuilder.apply('if', 3);
+    return type;
   }
 }
 
@@ -910,6 +924,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeContinueStatement();
+    h._irBuilder.apply('continue', 0);
   }
 }
 
@@ -937,9 +952,12 @@
 
   @override
   void _visit(Harness h) {
+    h._irBuilder.atom(variable.name);
     h._typeAnalyzer.analyzeVariableDeclaration(
         this, variable.type, variable, initializer,
         isFinal: isFinal, isLate: isLate);
+    h._irBuilder.apply(
+        ['declare', if (isLate) 'late', if (isFinal) 'final'].join('_'), 2);
   }
 }
 
@@ -963,6 +981,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeDoLoop(this, body, condition);
+    h._irBuilder.apply('do', 2);
   }
 }
 
@@ -983,10 +1002,12 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     var operatorName = isInverted ? '!=' : '==';
-    return h._typeAnalyzer
-        .analyzeBinaryExpression(this, lhs, operatorName, rhs);
+    var type =
+        h._typeAnalyzer.analyzeBinaryExpression(this, lhs, operatorName, rhs);
+    h._irBuilder.apply(operatorName, 2);
+    return type;
   }
 }
 
@@ -1072,6 +1093,7 @@
       h._typeAnalyzer.handleNoStatement();
     }
     h._flow.for_end();
+    h._irBuilder.apply('for', 4);
   }
 }
 
@@ -1123,6 +1145,7 @@
     }
     h._typeAnalyzer._visitLoopBody(this, body);
     h._flow.forEach_end();
+    h._irBuilder.apply('forEach', 2);
   }
 }
 
@@ -1139,7 +1162,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     var type = h._typeAnalyzer.analyzeExpression(target);
     h._flow.forwardExpression(this, target);
     callback(h._flow.expressionInfoForTesting(this));
@@ -1158,6 +1181,7 @@
   @override
   void _visit(Harness h) {
     callback(SsaNodeHarness(h._flow));
+    h._irBuilder.atom('null');
   }
 }
 
@@ -1184,6 +1208,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeIfStatement(this, condition, ifTrue, ifFalse);
+    h._irBuilder.apply('if', 3);
   }
 }
 
@@ -1203,8 +1228,10 @@
   }
 
   @override
-  Type _visit(Harness h) {
-    return h._typeAnalyzer.analyzeIfNullExpression(this, lhs, rhs);
+  Type _visit(Harness h, Type context) {
+    var type = h._typeAnalyzer.analyzeIfNullExpression(this, lhs, rhs);
+    h._irBuilder.apply('ifNull', 2);
+    return type;
   }
 }
 
@@ -1224,7 +1251,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     return h._typeAnalyzer
         .analyzeTypeTest(this, target, type, isInverted: isInverted);
   }
@@ -1272,10 +1299,12 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     var operatorName = isAnd ? '&&' : '||';
-    return h._typeAnalyzer
-        .analyzeBinaryExpression(this, lhs, operatorName, rhs);
+    var type =
+        h._typeAnalyzer.analyzeBinaryExpression(this, lhs, operatorName, rhs);
+    h._irBuilder.apply(operatorName, 2);
+    return type;
   }
 }
 
@@ -1302,12 +1331,16 @@
 
   Statement? _currentContinueTarget;
 
+  final _irBuilder = MiniIrBuilder();
+
   late final Type boolType = Type('bool');
 
   late final Type neverType = Type('Never');
 
   late final Type nullType = Type('Null');
 
+  late final Type unknownType = Type('?');
+
   _MiniAstTypeAnalyzer(this._harness);
 
   FlowAnalysis<Node, Statement, Expression, Var, Type> get flow =>
@@ -1414,8 +1447,10 @@
     flow.doStatement_end(condition);
   }
 
-  Type analyzeExpression(Expression expression) {
-    return dispatchExpression(expression);
+  Type analyzeExpression(Expression expression, [Type? context]) {
+    // TODO(paulberry): make the [context] argument required.
+    context ??= unknownType;
+    return dispatchExpression(expression, context);
   }
 
   void analyzeExpressionStatement(Expression expression) {
@@ -1592,21 +1627,31 @@
     flow.whileStatement_end();
   }
 
-  Type dispatchExpression(Expression expression) => expression._visit(_harness);
+  Type dispatchExpression(Expression expression, Type context) =>
+      _irBuilder.guard(expression, () => expression._visit(_harness, context));
 
-  void dispatchStatement(Statement statement) => statement._visit(_harness);
+  void dispatchStatement(Statement statement) =>
+      _irBuilder.guard(statement, () => statement._visit(_harness));
 
   void finish() {
     flow.finish();
   }
 
-  void handleNoCondition() {}
+  void handleNoCondition() {
+    _irBuilder.atom('true');
+  }
 
-  void handleNoInitializer() {}
+  void handleNoInitializer() {
+    _irBuilder.atom('uninitialized');
+  }
 
-  void handleNoMessage() {}
+  void handleNoMessage() {
+    _irBuilder.atom('failure');
+  }
 
-  void handleNoStatement() {}
+  void handleNoStatement() {
+    _irBuilder.atom('noop');
+  }
 
   bool isSwitchExhaustive(_Switch node) {
     return node.isExhaustive;
@@ -1647,7 +1692,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     return h._typeAnalyzer.analyzeNonNullAssert(this, operand);
   }
 }
@@ -1666,12 +1711,14 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     return h._typeAnalyzer.analyzeLogicalNot(this, operand);
   }
 }
 
 class _NullAwareAccess extends Expression {
+  static String _fakeMethodName = 'm';
+
   final Expression lhs;
   final Expression rhs;
   final bool isCascaded;
@@ -1688,12 +1735,14 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     var lhsType = h._typeAnalyzer.analyzeExpression(lhs);
     h._flow.nullAwareAccess_rightBegin(isCascaded ? null : lhs, lhsType);
     var rhsType = h._typeAnalyzer.analyzeExpression(rhs);
     h._flow.nullAwareAccess_end();
-    return h._lub(rhsType, Type('Null'));
+    var type = h._lub(rhsType, Type('Null'));
+    h._irBuilder.apply(_fakeMethodName, 2);
+    return type;
   }
 }
 
@@ -1707,8 +1756,10 @@
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
 
   @override
-  Type _visit(Harness h) {
-    return h._typeAnalyzer.analyzeNullLiteral(this);
+  Type _visit(Harness h, Type context) {
+    var type = h._typeAnalyzer.analyzeNullLiteral(this);
+    h._irBuilder.atom('null');
+    return type;
   }
 }
 
@@ -1726,7 +1777,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     return h._typeAnalyzer.analyzeParenthesizedExpression(this, expr);
   }
 }
@@ -1743,7 +1794,11 @@
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
 
   @override
-  Type _visit(Harness h) => type;
+  Type _visit(Harness h, Type context) {
+    h._irBuilder.atom(type.type);
+    h._irBuilder.apply('expr', 1);
+    return type;
+  }
 }
 
 class _Property extends LValue {
@@ -1760,7 +1815,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     return h._typeAnalyzer.analyzePropertyGet(this, target, propertyName);
   }
 
@@ -1783,6 +1838,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeReturnStatement();
+    h._irBuilder.apply('return', 0);
   }
 }
 
@@ -1819,6 +1875,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeSwitchStatement(this, expression, cases);
+    h._irBuilder.apply('switch', cases.length + 1);
   }
 }
 
@@ -1830,8 +1887,10 @@
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
 
   @override
-  Type _visit(Harness h) {
-    return h._typeAnalyzer.analyzeThis(this);
+  Type _visit(Harness h, Type context) {
+    var type = h._typeAnalyzer.analyzeThis(this);
+    h._irBuilder.atom('this');
+    return type;
   }
 }
 
@@ -1844,8 +1903,10 @@
   void _preVisit(AssignedVariables<Node, Var> assignedVariables) {}
 
   @override
-  Type _visit(Harness h) {
-    return h._typeAnalyzer.analyzeThisPropertyGet(this, propertyName);
+  Type _visit(Harness h, Type context) {
+    var type = h._typeAnalyzer.analyzeThisPropertyGet(this, propertyName);
+    h._irBuilder.atom('this.$propertyName');
+    return type;
   }
 }
 
@@ -1863,7 +1924,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     return h._typeAnalyzer.analyzeThrow(this, operand);
   }
 }
@@ -1913,6 +1974,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeTryStatement(this, _body, _catches, _finally);
+    h._irBuilder.apply('try', 2 + _catches.length);
   }
 }
 
@@ -1938,8 +2000,10 @@
   }
 
   @override
-  Type _visit(Harness h) {
-    return h._typeAnalyzer.analyzeVariableGet(this, variable, callback);
+  Type _visit(Harness h, Type context) {
+    var type = h._typeAnalyzer.analyzeVariableGet(this, variable, callback);
+    h._irBuilder.atom(variable.name);
+    return type;
   }
 
   @override
@@ -1969,6 +2033,7 @@
   @override
   void _visit(Harness h) {
     h._typeAnalyzer.analyzeWhileLoop(this, condition, body);
+    h._irBuilder.apply('while', 2);
   }
 }
 
@@ -1988,7 +2053,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     var type = h._typeAnalyzer.analyzeExpression(target);
     h._flow.forwardExpression(this, target);
     Type.withComparisonsAllowed(() {
@@ -2016,6 +2081,7 @@
     Type.withComparisonsAllowed(() {
       callback(h._flow.whyNotPromotedImplicitThis(staticType)());
     });
+    h._irBuilder.atom('noop');
   }
 }
 
@@ -2048,15 +2114,25 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
+    late MiniIrTmp beforeTmp;
     if (before != null) {
       h._typeAnalyzer.dispatchStatement(before!);
+      beforeTmp = h._irBuilder.allocateTmp();
     }
     var type = h._typeAnalyzer.analyzeExpression(expr);
     if (after != null) {
+      var exprTmp = h._irBuilder.allocateTmp();
       h._typeAnalyzer.dispatchStatement(after!);
+      var afterTmp = h._irBuilder.allocateTmp();
+      h._irBuilder.readTmp(exprTmp);
+      h._irBuilder.let(afterTmp);
+      h._irBuilder.let(exprTmp);
     }
     h._flow.forwardExpression(this, expr);
+    if (before != null) {
+      h._irBuilder.let(beforeTmp);
+    }
     return type;
   }
 }
@@ -2080,7 +2156,7 @@
   }
 
   @override
-  Type _visit(Harness h) {
+  Type _visit(Harness h, Type context) {
     var rhs = this.rhs;
     Type type;
     if (rhs == null) {
diff --git a/pkg/_fe_analyzer_shared/test/mini_ir.dart b/pkg/_fe_analyzer_shared/test/mini_ir.dart
new file mode 100644
index 0000000..b3f0dfa
--- /dev/null
+++ b/pkg/_fe_analyzer_shared/test/mini_ir.dart
@@ -0,0 +1,247 @@
+// 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);
+}
diff --git a/tools/VERSION b/tools/VERSION
index 4ddc3aa..d3375eb 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
 MAJOR 2
 MINOR 13
 PATCH 0
-PRERELEASE 230
+PRERELEASE 231
 PRERELEASE_PATCH 0
\ No newline at end of file