// Copyright (c) 2020, 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 the AST of a Dart-like language suitable for testing /// flow analysis. Callers may use the top level methods in this file to create /// AST nodes and then feed them to [Harness.run] to run them through flow /// analysis testing. 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(); Expression get this_ => new _This(); Statement assert_(Expression condition, [Expression? message]) => new _Assert(condition, message); Statement block(List statements) => new _Block(statements); Expression booleanLiteral(bool value) => _BooleanLiteral(value); Statement break_([LabeledStatement? target]) => new _Break(target); SwitchCase case_(List body, {bool hasLabel = false}) => SwitchCase._(hasLabel, new _Block(body)); /// Creates a pseudo-statement whose function is to verify that flow analysis /// considers [variable]'s assigned state to be [expectedAssignedState]. Statement checkAssigned(Var variable, bool expectedAssignedState) => new _CheckAssigned(variable, expectedAssignedState); /// Creates a pseudo-statement whose function is to verify that flow analysis /// considers [variable] to be un-promoted. Statement checkNotPromoted(Var variable) => new _CheckPromoted(variable, null); /// Creates a pseudo-statement whose function is to verify that flow analysis /// considers [variable]'s assigned state to be promoted to [expectedTypeStr]. Statement checkPromoted(Var variable, String? expectedTypeStr) => new _CheckPromoted(variable, expectedTypeStr); /// Creates a pseudo-statement whose function is to verify that flow analysis /// considers the current location's reachability state to be /// [expectedReachable]. Statement checkReachable(bool expectedReachable) => new _CheckReachable(expectedReachable); /// Creates a pseudo-statement whose function is to verify that flow analysis /// considers [variable]'s unassigned state to be [expectedUnassignedState]. Statement checkUnassigned(Var variable, bool expectedUnassignedState) => new _CheckUnassigned(variable, expectedUnassignedState); Statement continue_() => new _Continue(); Statement declare(Var variable, {required bool initialized, bool isFinal = false, bool isLate = false}) => new _Declare(variable, initialized ? expr(variable.type.type) : null, isFinal, isLate); Statement declareInitialized(Var variable, Expression initializer, {bool isFinal = false, bool isLate = false}) => new _Declare(variable, initializer, isFinal, isLate); Statement do_(List body, Expression condition) => _Do(block(body), condition); /// Creates a pseudo-expression having type [typeStr] that otherwise has no /// effect on flow analysis. Expression expr(String typeStr) => new _PlaceholderExpression(new Type(typeStr)); /// Creates a conventional `for` statement. Optional boolean [forCollection] /// indicates that this `for` statement is actually a collection element, so /// `null` should be passed to [for_bodyBegin]. Statement for_(Statement? initializer, Expression? condition, Expression? updater, List body, {bool forCollection = false}) => new _For(initializer, condition, updater, block(body), forCollection); /// Creates a "for each" statement where the identifier being assigned to by the /// iteration is not a local variable. /// /// This models code like: /// var x; // Top level variable /// f(Iterable iterable) { /// for (x in iterable) { ... } /// } Statement forEachWithNonVariable(Expression iterable, List body) => new _ForEach(null, iterable, block(body), false); /// Creates a "for each" statement where the identifier being assigned to by the /// iteration is a variable that is being declared by the "for each" statement. /// /// This models code like: /// f(Iterable iterable) { /// for (var x in iterable) { ... } /// } Statement forEachWithVariableDecl( Var variable, Expression iterable, List body) { // ignore: unnecessary_null_comparison assert(variable != null); return new _ForEach(variable, iterable, block(body), true); } /// Creates a "for each" statement where the identifier being assigned to by the /// iteration is a local variable that is declared elsewhere in the function. /// /// This models code like: /// f(Iterable iterable) { /// var x; /// for (x in iterable) { ... } /// } Statement forEachWithVariableSet( Var variable, Expression iterable, List body) { // ignore: unnecessary_null_comparison assert(variable != null); return new _ForEach(variable, iterable, block(body), false); } /// Creates a [Statement] that, when analyzed, will cause [callback] to be /// passed an [SsaNodeHarness] allowing the test to examine the values of /// variables' SSA nodes. Statement getSsaNodes(void Function(SsaNodeHarness) callback) => new _GetSsaNodes(callback); Statement if_(Expression condition, List ifTrue, [List? ifFalse]) => new _If(condition, block(ifTrue), ifFalse == null ? null : block(ifFalse)); Statement implicitThis_whyNotPromoted(String staticType, void Function(Map) callback) => new _WhyNotPromoted_ImplicitThis(Type(staticType), callback); Statement labeled(Statement Function(LabeledStatement) callback) { var labeledStatement = LabeledStatement._(); labeledStatement._body = callback(labeledStatement); return labeledStatement; } Statement localFunction(List body) => _LocalFunction(block(body)); Statement return_() => new _Return(); Statement switch_(Expression expression, List cases, {required bool isExhaustive}) => new _Switch(expression, cases, isExhaustive); Expression thisOrSuperPropertyGet(String name) => new _ThisOrSuperPropertyGet(name); Expression throw_(Expression operand) => new _Throw(operand); TryBuilder try_(List body) => new _TryStatement(block(body), [], null); Statement while_(Expression condition, List body) => new _While(condition, block(body)); /// Representation of an expression in the pseudo-Dart language used for flow /// analysis testing. Methods in this class may be used to create more complex /// expressions based on this one. abstract class Expression extends Node { Expression() : super._(); /// If `this` is an expression `x`, creates the expression `x!`. Expression get nonNullAssert => new _NonNullAssert(this); /// If `this` is an expression `x`, creates the expression `!x`. Expression get not => new _Not(this); /// If `this` is an expression `x`, creates the expression `(x)`. Expression get parenthesized => new _ParenthesizedExpression(this); /// If `this` is an expression `x`, creates the statement `x;`. Statement get stmt => new _ExpressionStatement(this); /// If `this` is an expression `x`, creates the expression `x && other`. Expression and(Expression other) => new _Logical(this, other, isAnd: true); /// If `this` is an expression `x`, creates the expression `x as typeStr`. Expression as_(String typeStr) => new _As(this, Type(typeStr)); /// If `this` is an expression `x`, creates the expression /// `x ? ifTrue : ifFalse`. Expression conditional(Expression ifTrue, Expression ifFalse) => new _Conditional(this, ifTrue, ifFalse); /// If `this` is an expression `x`, creates the expression `x == other`. Expression eq(Expression other) => new _Equal(this, other, false); /// Creates an [Expression] that, when analyzed, will behave the same as /// `this`, but after visiting it, will cause [callback] to be passed the /// [ExpressionInfo] associated with it. If the expression has no flow /// analysis information associated with it, `null` will be passed to /// [callback]. Expression getExpressionInfo( void Function(ExpressionInfo?) callback) => new _GetExpressionInfo(this, callback); /// If `this` is an expression `x`, creates the expression `x ?? other`. Expression ifNull(Expression other) => new _IfNull(this, other); /// If `this` is an expression `x`, creates the expression `x is typeStr`. /// /// With [isInverted] set to `true`, creates the expression `x is! typeStr`. Expression is_(String typeStr, {bool isInverted = false}) => new _Is(this, Type(typeStr), isInverted); /// If `this` is an expression `x`, creates the expression `x is! typeStr`. Expression isNot(String typeStr) => _Is(this, Type(typeStr), true); /// If `this` is an expression `x`, creates the expression `x != other`. Expression notEq(Expression other) => _Equal(this, other, true); /// If `this` is an expression `x`, creates the expression `x?.other`. /// /// Note that in the real Dart language, the RHS of a null aware access isn't /// strictly speaking an expression. However for flow analysis it suffices to /// model it as an expression. Expression nullAwareAccess(Expression other, {bool isCascaded = false}) => _NullAwareAccess(this, other, isCascaded); /// If `this` is an expression `x`, creates the expression `x || other`. Expression or(Expression other) => new _Logical(this, other, isAnd: false); /// If `this` is an expression `x`, creates the L-value `x.name`. LValue property(String name) => new _Property(this, name); /// If `this` is an expression `x`, creates a pseudo-expression that models /// evaluation of `x` followed by execution of [stmt]. This can be used to /// test that flow analysis is in the correct state after an expression is /// visited. Expression thenStmt(Statement stmt) => new _WrappedExpression(null, this, stmt); /// Creates an [Expression] that, when analyzed, will behave the same as /// `this`, but after visiting it, will cause [callback] to be passed the /// non-promotion info associated with it. If the expression has no /// non-promotion info, an empty map will be passed to [callback]. Expression whyNotPromoted( void Function(Map) callback) => new _WhyNotPromoted(this, callback); void _preVisit(AssignedVariables assignedVariables); Type _visit(Harness h, Type context); } /// Test harness for creating flow analysis tests. This class implements all /// the [TypeOperations] needed by flow analysis, as well as other methods /// needed for testing. class Harness extends TypeOperations { static const Map _coreSubtypes = const { 'bool <: int': false, 'bool <: Object': true, 'double <: Object': true, 'double <: num': true, 'double <: num?': true, 'double <: int': false, 'double <: int?': false, 'int <: double': false, 'int <: int?': true, 'int <: Iterable': false, 'int <: List': false, 'int <: Null': false, 'int <: num': true, 'int <: num?': true, 'int <: num*': true, 'int <: Never?': false, 'int <: Object': true, 'int <: Object?': true, 'int <: String': false, 'int? <: int': false, 'int? <: Null': false, 'int? <: num': false, 'int? <: num?': true, 'int? <: Object': false, 'int? <: Object?': true, 'Never <: Object?': true, 'Null <: int': false, 'Null <: Object': false, 'Null <: Object?': true, 'num <: int': false, 'num <: Iterable': false, 'num <: List': false, 'num <: num?': true, 'num <: num*': true, 'num <: Object': true, 'num <: Object?': true, 'num? <: int?': false, 'num? <: num': false, 'num? <: num*': true, 'num? <: Object': false, 'num? <: Object?': true, 'num* <: num': true, 'num* <: num?': true, 'num* <: Object': true, 'num* <: Object?': true, 'Iterable <: int': false, 'Iterable <: num': false, 'Iterable <: Object': true, 'Iterable <: Object?': true, 'List <: int': false, 'List <: Iterable': true, 'List <: Object': true, 'Never <: int': true, 'Never <: int?': true, 'Never <: Null': true, 'Never? <: int': false, 'Never? <: int?': true, 'Never? <: num?': true, 'Never? <: Object?': true, 'Null <: int?': true, 'Object <: int': false, 'Object <: int?': false, 'Object <: List': false, 'Object <: Null': false, 'Object <: num': false, 'Object <: num?': false, 'Object <: Object?': true, 'Object <: String': false, 'Object? <: Object': false, 'Object? <: int': false, 'Object? <: int?': false, 'Object? <: Null': false, 'String <: int': false, 'String <: int?': false, 'String <: num?': false, 'String <: Object': true, 'String <: Object?': true, }; static final Map _coreFactors = { 'Object? - int': Type('Object?'), 'Object? - int?': Type('Object'), 'Object? - Never': Type('Object?'), 'Object? - Null': Type('Object'), 'Object? - num?': Type('Object'), 'Object? - Object?': Type('Never?'), 'Object? - String': Type('Object?'), 'Object - bool': Type('Object'), 'Object - int': Type('Object'), 'Object - String': Type('Object'), 'int - Object': Type('Never'), 'int - String': Type('int'), 'int - int': Type('Never'), 'int - int?': Type('Never'), 'int? - int': Type('Never?'), 'int? - int?': Type('Never'), 'int? - String': Type('int?'), 'Null - int': Type('Null'), 'num - int': Type('num'), 'num? - num': Type('Never?'), 'num? - int': Type('num?'), 'num? - int?': Type('num'), 'num? - Object': Type('Never?'), 'num? - String': Type('num?'), 'Object - int?': Type('Object'), 'Object - num': Type('Object'), 'Object - num?': Type('Object'), 'Object - num*': Type('Object'), 'Object - Iterable': Type('Object'), 'Object? - Object': Type('Never?'), 'Object? - Iterable': Type('Object?'), 'Object? - num': Type('Object?'), 'Iterable - List': Type('Iterable'), 'num* - Object': Type('Never'), }; late final FlowAnalysis _flow; final bool legacy; final Type? thisType; final Map _subtypes = Map.of(_coreSubtypes); final Map _factorResults = Map.of(_coreFactors); final Map _members = {}; Map> _promotionExceptions = {}; late final _typeAnalyzer = _MiniAstTypeAnalyzer(this); 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) { var query = '$from - $what'; _factorResults[query] = Type(result); } /// Updates the harness so that when member [memberName] is looked up on type /// [targetType], a member is found having the given [type]. void addMember(String targetType, String memberName, String type) { var query = '$targetType.$memberName'; _members[query] = Type(type); } void addPromotionException(String from, String to, String result) { (_promotionExceptions[from] ??= {})[to] = result; } /// Updates the harness so that when an [isSubtypeOf] query is invoked on /// types [leftType] and [rightType], [isSubtype] will be returned. void addSubtype(String leftType, String rightType, bool isSubtype) { var query = '$leftType <: $rightType'; _subtypes[query] = isSubtype; } @override TypeClassification classifyType(Type type) { if (isSubtypeOf(type, Type('Object'))) { return TypeClassification.nonNullable; } else if (isSubtypeOf(type, Type('Null'))) { return TypeClassification.nullOrEquivalent; } else { return TypeClassification.potentiallyNullable; } } @override Type factor(Type from, Type what) { var query = '$from - $what'; return _factorResults[query] ?? fail('Unknown factor query: $query'); } /// Attempts to look up a member named [memberName] in the given [type]. If /// a member is found, returns its type. Otherwise the test fails. Type getMember(Type type, String memberName) { var query = '$type.$memberName'; return _members[query] ?? fail('Unknown member query: $query'); } @override bool isNever(Type type) { return type.type == 'Never'; } @override bool isSameType(Type type1, Type type2) { return type1.type == type2.type; } @override bool isSubtypeOf(Type leftType, Type rightType) { if (leftType.type == rightType.type) return true; var query = '$leftType <: $rightType'; return _subtypes[query] ?? fail('Unknown subtype query: $query'); } @override Type promoteToNonNull(Type type) { if (type.type.endsWith('?')) { return Type(type.type.substring(0, type.type.length - 1)); } else if (type.type == 'Null') { return Type('Never'); } else { return type; } } /// Runs the given [statements] through flow analysis, checking any assertions /// they contain. void run(List statements) { var assignedVariables = AssignedVariables(); var b = block(statements); b._preVisit(assignedVariables); _flow = legacy ? FlowAnalysis.legacy( this, assignedVariables) : FlowAnalysis( this, assignedVariables); _typeAnalyzer.dispatchStatement(b); _typeAnalyzer.finish(); } @override Type? tryPromoteToType(Type to, Type from) { var exception = (_promotionExceptions[from.type] ?? {})[to.type]; if (exception != null) { return Type(exception); } if (isSubtypeOf(to, from)) { return to; } else { return null; } } @override Type variableType(Var variable) { return variable.type; } Type _getIteratedType(Type iterableType) { var typeStr = iterableType.type; if (typeStr.startsWith('List<') && typeStr.endsWith('>')) { return Type(typeStr.substring(5, typeStr.length - 1)); } else { throw UnimplementedError('TODO(paulberry): getIteratedType($typeStr)'); } } Type _lub(Type type1, Type type2) { if (isSameType(type1, type2)) { return type1; } else if (isSameType(promoteToNonNull(type1), type2)) { return type1; } else if (isSameType(promoteToNonNull(type2), type1)) { return type2; } else if (type1.type == 'Null' && !isSameType(promoteToNonNull(type2), type2)) { // type2 is already nullable return type2; } else if (type2.type == 'Null' && !isSameType(promoteToNonNull(type1), type1)) { // type1 is already nullable return type1; } else if (type1.type == 'Never') { return type2; } else if (type2.type == 'Never') { return type1; } else { throw UnimplementedError( 'TODO(paulberry): least upper bound of $type1 and $type2'); } } } class LabeledStatement extends Statement { late final Statement _body; LabeledStatement._() : super._(); @override String toString() => 'labeled: $_body'; @override void _preVisit(AssignedVariables assignedVariables) { _body._preVisit(assignedVariables); } @override void _visit(Harness h) { h._typeAnalyzer.analyzeLabeledStatement(this, _body); } } /// Representation of an expression that can appear on the left hand side of an /// assignment (or as the target of `++` or `--`). Methods in this class may be /// used to create more complex expressions based on this one. abstract class LValue extends Expression { LValue._(); /// Creates an expression representing a write to this L-value. Expression write(Expression? value) => new _Write(this, value); @override void _preVisit(AssignedVariables assignedVariables, {_LValueDisposition disposition}); void _visitWrite(Harness h, Expression assignmentExpression, Type writtenType, Expression? rhs); } /// Representation of an expression or statement in the pseudo-Dart language /// used for flow analysis testing. class Node { static int _nextId = 0; final int id; Node._() : id = _nextId++; String toString() => 'Node#$id'; } /// Helper class allowing tests to examine the values of variables' SSA nodes. class SsaNodeHarness { final FlowAnalysis _flow; SsaNodeHarness(this._flow); /// Gets the SSA node associated with [variable] at the current point in /// control flow, or `null` if the variable has been write captured. SsaNode? operator [](Var variable) => _flow.ssaNodeForTesting(variable); } /// Representation of a statement in the pseudo-Dart language used for flow /// analysis testing. abstract class Statement extends Node { Statement._() : super._(); /// If `this` is a statement `x`, creates a pseudo-expression that models /// execution of `x` followed by evaluation of [expr]. This can be used to /// test that flow analysis is in the correct state before an expression is /// visited. Expression thenExpr(Expression expr) => _WrappedExpression(this, expr, null); void _preVisit(AssignedVariables assignedVariables); void _visit(Harness h); } /// Representation of a single case clause in a switch statement. Use [case_] /// to create instances of this class. class SwitchCase { final bool _hasLabel; final _Block _body; SwitchCase._(this._hasLabel, this._body); String toString() => [ if (_hasLabel) '