Shared pattern analysis: add support for pattern variable assignment.

Bug: https://github.com/dart-lang/sdk/issues/50585
Change-Id: I1939c6fd8fac205abeac5b0a9b3da9b3b4adca01
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/274604
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Paul Berry 2022-12-09 17:16:38 +00:00 committed by Commit Queue
parent 9f2a622d79
commit 966aed6bd1
10 changed files with 574 additions and 202 deletions

View file

@ -187,6 +187,14 @@ abstract class FlowAnalysis<Node extends Object, Statement extends Node,
/// A function parameter is always initialized, so [initialized] is `true`.
void declare(Variable variable, bool initialized);
/// Call this method after visiting a variable pattern in a non-assignment
/// context (or a wildcard pattern).
///
/// [matchedType] should be the static type of the value being matched.
/// [staticType] should be the static type of the variable pattern itself.
void declaredVariablePattern(
{required Type matchedType, required Type staticType});
/// Call this method before visiting the body of a "do-while" statement.
/// [doStatement] should be the same node that was passed to
/// [AssignedVariables.endNode] for the do-while statement.
@ -518,6 +526,15 @@ abstract class FlowAnalysis<Node extends Object, Statement extends Node,
void parenthesizedExpression(
Expression outerExpression, Expression innerExpression);
/// Call this method just after visiting the right hand side of a pattern
/// assignment expression, and before visiting the pattern.
///
/// [rhs] is the right hand side expression.
void patternAssignment_afterRhs(Expression rhs);
/// Call this method after visiting a pattern assignment expression.
void patternAssignment_end();
/// Call this method just after visiting the initializer of a pattern variable
/// declaration, and before visiting the pattern.
void patternVariableDeclaration_afterInitializer(Expression initializer);
@ -746,12 +763,6 @@ abstract class FlowAnalysis<Node extends Object, Statement extends Node,
/// statement.
void tryFinallyStatement_finallyBegin(Node body);
/// Call this method after visiting a variable pattern.
///
/// [matchedType] should be the static type of the value being matched.
/// [staticType] should be the static type of the variable pattern itself.
void variablePattern({required Type matchedType, required Type staticType});
/// Call this method when encountering an expression that reads the value of
/// a variable.
///
@ -950,6 +961,16 @@ class FlowAnalysisDebug<Node extends Object, Statement extends Node,
() => _wrapped.declare(variable, initialized));
}
@override
void declaredVariablePattern(
{required Type matchedType, required Type staticType}) {
_wrap(
'declaredVariablePattern(matchedType: $matchedType, '
'staticType: $staticType)',
() => _wrapped.declaredVariablePattern(
matchedType: matchedType, staticType: staticType));
}
@override
void doStatement_bodyBegin(Statement doStatement) {
return _wrap('doStatement_bodyBegin($doStatement)',
@ -1243,6 +1264,17 @@ class FlowAnalysisDebug<Node extends Object, Statement extends Node,
_wrapped.parenthesizedExpression(outerExpression, innerExpression));
}
@override
void patternAssignment_afterRhs(Expression rhs) {
_wrap('patternAssignment_afterRhs($rhs)',
() => _wrapped.patternAssignment_afterRhs(rhs));
}
@override
void patternAssignment_end() {
_wrap('patternAssignment_end()', () => _wrapped.patternAssignment_end());
}
@override
void patternVariableDeclaration_afterInitializer(Expression initializer) {
_wrap(
@ -1405,14 +1437,6 @@ class FlowAnalysisDebug<Node extends Object, Statement extends Node,
() => _wrapped.tryFinallyStatement_finallyBegin(body));
}
@override
void variablePattern({required Type matchedType, required Type staticType}) {
_wrap(
'variablePattern(matchedType: $matchedType, staticType: $staticType)',
() => _wrapped.variablePattern(
matchedType: matchedType, staticType: staticType));
}
@override
Type? variableRead(Expression expression, Variable variable) {
return _wrap('variableRead($expression, $variable)',
@ -3371,6 +3395,25 @@ class _FlowAnalysisImpl<Node extends Object, Statement extends Node,
promotionKeyStore.keyForVariable(variable), initialized);
}
@override
void declaredVariablePattern(
{required Type matchedType, required Type staticType}) {
_PatternContext<Type> context = _stack.last as _PatternContext<Type>;
ReferenceWithType<Type>? scrutineeReference = context._scrutineeReference;
bool coversMatchedType =
typeOperations.isSubtypeOf(matchedType, staticType);
if (scrutineeReference != null) {
ExpressionInfo<Type> promotionInfo =
_current.tryPromoteForTypeCheck(this, scrutineeReference, staticType);
_current = promotionInfo.ifTrue;
if (!coversMatchedType) {
context._unmatched = _join(context._unmatched, promotionInfo.ifFalse);
}
} else if (!coversMatchedType) {
context._unmatched = _join(context._unmatched, _current);
}
}
@override
void doStatement_bodyBegin(Statement doStatement) {
AssignedVariablesNodeInfo info =
@ -3848,6 +3891,16 @@ class _FlowAnalysisImpl<Node extends Object, Statement extends Node,
forwardExpression(outerExpression, innerExpression);
}
@override
void patternAssignment_afterRhs(Expression rhs) {
_pushPattern(_getExpressionReference(rhs));
}
@override
void patternAssignment_end() {
_popPattern(null);
}
@override
void patternVariableDeclaration_afterInitializer(Expression initializer) {
_pushPattern(_getExpressionReference(initializer));
@ -4048,24 +4101,6 @@ class _FlowAnalysisImpl<Node extends Object, Statement extends Node,
context._beforeFinally = _current;
}
@override
void variablePattern({required Type matchedType, required Type staticType}) {
_PatternContext<Type> context = _stack.last as _PatternContext<Type>;
ReferenceWithType<Type>? scrutineeReference = context._scrutineeReference;
bool coversMatchedType =
typeOperations.isSubtypeOf(matchedType, staticType);
if (scrutineeReference != null) {
ExpressionInfo<Type> promotionInfo =
_current.tryPromoteForTypeCheck(this, scrutineeReference, staticType);
_current = promotionInfo.ifTrue;
if (!coversMatchedType) {
context._unmatched = _join(context._unmatched, promotionInfo.ifFalse);
}
} else if (!coversMatchedType) {
context._unmatched = _join(context._unmatched, _current);
}
}
@override
Type? variableRead(Expression expression, Variable variable) {
int variableKey = promotionKeyStore.keyForVariable(variable);
@ -4510,6 +4545,10 @@ class _LegacyTypePromotion<Node extends Object, Statement extends Node,
@override
void declare(Variable variable, bool initialized) {}
@override
void declaredVariablePattern(
{required Type matchedType, required Type staticType}) {}
@override
void doStatement_bodyBegin(Statement doStatement) {}
@ -4778,6 +4817,12 @@ class _LegacyTypePromotion<Node extends Object, Statement extends Node,
forwardExpression(outerExpression, innerExpression);
}
@override
void patternAssignment_afterRhs(Expression rhs) {}
@override
void patternAssignment_end() {}
@override
void patternVariableDeclaration_afterInitializer(Expression initializer) {}
@ -4858,9 +4903,6 @@ class _LegacyTypePromotion<Node extends Object, Statement extends Node,
@override
void tryFinallyStatement_finallyBegin(Node body) {}
@override
void variablePattern({required Type matchedType, required Type staticType}) {}
@override
Type? variableRead(Expression expression, Variable variable) {
int variableKey = _promotionKeyStore.keyForVariable(variable);

View file

@ -243,15 +243,51 @@ mixin TypeAnalyzer<
/// Returns the type `Object?`.
Type get objectQuestionType;
/// The [Operations], used to access types, check subtyping, and query
/// variable types.
Operations<Variable, Type> get operations;
/// Options affecting the behavior of [TypeAnalyzer].
TypeAnalyzerOptions get options;
/// Returns the client's implementation of the [TypeOperations] class.
TypeOperations<Type> get typeOperations;
/// Returns the unknown type context (`?`) used in type inference.
Type get unknownType;
/// Analyzes a non-wildcard variable pattern appearing in an assignment
/// context. [node] is the pattern itself, and [variable] is the variable
/// being referenced.
///
/// See [dispatchPattern] for the meanings of [matchedType] and [context].
///
/// For wildcard patterns in an assignment context,
/// [analyzeDeclaredVariablePattern] should be used instead.
///
/// Stack effect: none.
void analyzeAssignedVariablePattern(
Type matchedType,
MatchContext<Node, Expression, Pattern, Type, Variable> context,
Pattern node,
Variable variable) {
Type variableDeclaredType = operations.variableType(variable);
Node? irrefutableContext = context.irrefutableContext;
assert(irrefutableContext != null,
'Assigned variables must only appear in irrefutable pattern contexts');
if (irrefutableContext != null &&
!operations.isAssignableTo(matchedType, variableDeclaredType)) {
errors?.patternTypeMismatchInIrrefutableContext(
pattern: node,
context: irrefutableContext,
matchedType: matchedType,
requiredType: variableDeclaredType);
}
flow?.write(node, variable, matchedType, context.getInitializer(node));
}
/// Computes the type schema for a variable pattern appearing in an assignment
/// context. [variable] is the variable being referenced.
Type analyzeAssignedVariablePatternSchema(Variable variable) =>
flow?.promotedType(variable) ?? operations.variableType(variable);
/// Analyzes a cast pattern. [innerPattern] is the sub-pattern] and [type] is
/// the type to cast to.
///
@ -299,8 +335,8 @@ mixin TypeAnalyzer<
if (switchScrutinee != null) {
bool nullSafetyEnabled = options.nullSafetyEnabled;
bool matches = nullSafetyEnabled
? typeOperations.isSubtypeOf(staticType, matchedType)
: typeOperations.isAssignableTo(staticType, matchedType);
? operations.isSubtypeOf(staticType, matchedType)
: operations.isAssignableTo(staticType, matchedType);
if (!matches) {
errors.caseExpressionTypeMismatch(
caseExpression: expression,
@ -324,19 +360,79 @@ mixin TypeAnalyzer<
return unknownType;
}
/// Analyzes a variable pattern in a non-assignment context (or a wildcard
/// pattern). [node] is the pattern itself, [variable] is the variable,
/// [declaredType] is the explicitly declared type (if present), and [isFinal]
/// indicates whether the variable is final.
///
/// See [dispatchPattern] for the meanings of [matchedType] and [context].
///
/// If this is a wildcard pattern (it doesn't bind any variable), [variable]
/// should be `null`.
///
/// Returns the static type of the variable (possibly inferred).
///
/// Stack effect: none.
Type analyzeDeclaredVariablePattern(
Type matchedType,
MatchContext<Node, Expression, Pattern, Type, Variable> context,
Pattern node,
Variable? variable,
String? name,
Type? declaredType,
) {
Type staticType =
declaredType ?? variableTypeFromInitializerType(matchedType);
Node? irrefutableContext = context.irrefutableContext;
if (irrefutableContext != null &&
!operations.isAssignableTo(matchedType, staticType)) {
errors?.patternTypeMismatchInIrrefutableContext(
pattern: node,
context: irrefutableContext,
matchedType: matchedType,
requiredType: staticType);
}
flow?.declaredVariablePattern(
matchedType: matchedType, staticType: staticType);
bool isImplicitlyTyped = declaredType == null;
if (variable != null) {
if (name == null) {
throw new StateError(
'When the variable is not null, the name must also be not null');
}
flow?.declare(variable, false);
setVariableType(variable, staticType);
// TODO(paulberry): are we handling _isFinal correctly?
flow?.initialize(variable, matchedType, context.getInitializer(node),
isFinal: context.isFinal || isVariableFinal(variable),
isLate: context.isLate,
isImplicitlyTyped: isImplicitlyTyped);
}
return staticType;
}
/// Computes the type schema for a variable pattern in a non-assignment
/// context (or a wildcard pattern). [declaredType] is the explicitly
/// declared type (if present).
///
/// Stack effect: none.
Type analyzeDeclaredVariablePatternSchema(Type? declaredType) {
return declaredType ?? unknownType;
}
/// Analyzes an expression. [node] is the expression to analyze, and
/// [context] is the type schema which should be used for type inference.
///
/// Stack effect: pushes (Expression).
Type analyzeExpression(Expression node, Type? context) {
// Stack: ()
if (context == null || typeOperations.isDynamic(context)) {
if (context == null || operations.isDynamic(context)) {
context = unknownType;
}
ExpressionTypeAnalysisResult<Type> result =
dispatchExpression(node, context);
// Stack: (Expression)
if (typeOperations.isNever(result.provisionalType)) {
if (operations.isNever(result.provisionalType)) {
flow?.handleExit();
}
return result.resolveShorting();
@ -501,8 +597,8 @@ mixin TypeAnalyzer<
///
/// Stack effect: none.
IntTypeAnalysisResult<Type> analyzeIntLiteral(Type context) {
bool convertToDouble = !typeOperations.isSubtypeOf(intType, context) &&
typeOperations.isSubtypeOf(doubleType, context);
bool convertToDouble = !operations.isSubtypeOf(intType, context) &&
operations.isSubtypeOf(doubleType, context);
Type type = convertToDouble ? doubleType : intType;
return new IntTypeAnalysisResult<Type>(
type: type, convertedToDouble: convertToDouble);
@ -525,10 +621,10 @@ mixin TypeAnalyzer<
if (elementType != null) {
valueType = elementType;
} else {
Type? listElementType = typeOperations.matchListType(matchedType);
Type? listElementType = operations.matchListType(matchedType);
if (listElementType != null) {
valueType = listElementType;
} else if (typeOperations.isDynamic(matchedType)) {
} else if (operations.isDynamic(matchedType)) {
valueType = dynamicType;
} else {
valueType = objectQuestionType;
@ -550,7 +646,7 @@ mixin TypeAnalyzer<
Type requiredType = listType(valueType);
Node? irrefutableContext = context.irrefutableContext;
if (irrefutableContext != null &&
!typeOperations.isAssignableTo(matchedType, requiredType)) {
!operations.isAssignableTo(matchedType, requiredType)) {
errors?.patternTypeMismatchInIrrefutableContext(
pattern: node,
context: irrefutableContext,
@ -584,7 +680,7 @@ mixin TypeAnalyzer<
Pattern? subPattern = getRestPatternElementPattern(element);
if (subPattern != null) {
Type subPatternType = dispatchPatternSchema(subPattern);
typeToAdd = typeOperations.matchIterableType(subPatternType);
typeToAdd = operations.matchIterableType(subPatternType);
}
} else {
typeToAdd = dispatchPatternSchema(element);
@ -593,7 +689,7 @@ mixin TypeAnalyzer<
if (currentGLB == null) {
currentGLB = typeToAdd;
} else {
currentGLB = typeOperations.glb(currentGLB, typeToAdd);
currentGLB = operations.glb(currentGLB, typeToAdd);
}
}
}
@ -644,7 +740,7 @@ mixin TypeAnalyzer<
/// Stack effect: none.
Type analyzeLogicalPatternSchema(Node lhs, Node rhs, {required bool isAnd}) {
if (isAnd) {
return typeOperations.glb(
return operations.glb(
dispatchPatternSchema(lhs), dispatchPatternSchema(rhs));
} else {
// Logical-or patterns are only allowed in refutable contexts, and
@ -677,12 +773,12 @@ mixin TypeAnalyzer<
valueType = typeArguments.valueType;
keyContext = keyType;
} else {
typeArguments = typeOperations.matchMapType(matchedType);
typeArguments = operations.matchMapType(matchedType);
if (typeArguments != null) {
keyType = typeArguments.keyType;
valueType = typeArguments.valueType;
keyContext = keyType;
} else if (typeOperations.isDynamic(matchedType)) {
} else if (operations.isDynamic(matchedType)) {
keyType = dynamicType;
valueType = dynamicType;
keyContext = unknownType;
@ -716,7 +812,7 @@ mixin TypeAnalyzer<
);
Node? irrefutableContext = context.irrefutableContext;
if (irrefutableContext != null &&
!typeOperations.isAssignableTo(matchedType, requiredType)) {
!operations.isAssignableTo(matchedType, requiredType)) {
errors?.patternTypeMismatchInIrrefutableContext(
pattern: node,
context: irrefutableContext,
@ -751,7 +847,7 @@ mixin TypeAnalyzer<
if (valueType == null) {
valueType = entryValueType;
} else {
valueType = typeOperations.glb(valueType, entryValueType);
valueType = operations.glb(valueType, entryValueType);
}
}
}
@ -775,7 +871,7 @@ mixin TypeAnalyzer<
Pattern innerPattern,
{required bool isAssert}) {
// Stack: ()
Type innerMatchedType = typeOperations.promoteToNonNull(matchedType);
Type innerMatchedType = operations.promoteToNonNull(matchedType);
Node? irrefutableContext = context.irrefutableContext;
if (irrefutableContext != null && !isAssert) {
errors?.refutablePatternInIrrefutableContext(node, irrefutableContext);
@ -794,7 +890,7 @@ mixin TypeAnalyzer<
Type analyzeNullCheckOrAssertPatternSchema(Pattern innerPattern,
{required bool isAssert}) {
if (isAssert) {
return typeOperations.makeNullable(dispatchPatternSchema(innerPattern));
return operations.makeNullable(dispatchPatternSchema(innerPattern));
} else {
// Null-check patterns are only allowed in refutable contexts, and
// refutable contexts don't propagate a type schema into the scrutinee.
@ -828,14 +924,14 @@ mixin TypeAnalyzer<
// If the required type is `dynamic` or `Never`, then every getter is
// treated as having the same type.
Type? overridePropertyGetType;
if (typeOperations.isDynamic(requiredType) ||
typeOperations.isNever(requiredType)) {
if (operations.isDynamic(requiredType) ||
operations.isNever(requiredType)) {
overridePropertyGetType = requiredType;
}
Node? irrefutableContext = context.irrefutableContext;
if (irrefutableContext != null &&
!typeOperations.isAssignableTo(matchedType, requiredType)) {
!operations.isAssignableTo(matchedType, requiredType)) {
errors?.patternTypeMismatchInIrrefutableContext(
pattern: node,
context: irrefutableContext,
@ -866,6 +962,33 @@ mixin TypeAnalyzer<
return type;
}
/// Analyzes a patternAssignment expression of the form `pattern = rhs`.
///
/// [node] should be the AST node for the entire expression, [pattern] for
/// the pattern, and [rhs] for the right hand side.
///
/// Stack effect: pushes (Expression, Pattern).
ExpressionTypeAnalysisResult<Type> analyzePatternAssignment(
Expression node, Pattern pattern, Expression rhs) {
// Stack: ()
Type rhsType = analyzeExpression(rhs, dispatchPatternSchema(pattern));
// Stack: (Expression)
flow?.patternAssignment_afterRhs(rhs);
dispatchPattern(
rhsType,
new MatchContext<Node, Expression, Pattern, Type, Variable>(
isFinal: false,
initializer: rhs,
irrefutableContext: node,
topPattern: pattern,
),
pattern,
);
flow?.patternAssignment_end();
// Stack: (Expression, Pattern)
return new SimpleTypeAnalysisResult<Type>(type: rhsType);
}
/// Analyzes a patternVariableDeclaration statement of the form
/// `var pattern = initializer;` or `final pattern = initializer;.
///
@ -971,7 +1094,7 @@ mixin TypeAnalyzer<
} else {
dispatchFields(objectQuestionType);
}
} else if (typeOperations.isDynamic(matchedType)) {
} else if (operations.isDynamic(matchedType)) {
dispatchFields(dynamicType);
} else {
dispatchFields(objectQuestionType);
@ -980,7 +1103,7 @@ mixin TypeAnalyzer<
Node? irrefutableContext = context.irrefutableContext;
if (irrefutableContext != null &&
!typeOperations.isAssignableTo(matchedType, requiredType)) {
!operations.isAssignableTo(matchedType, requiredType)) {
errors?.patternTypeMismatchInIrrefutableContext(
pattern: node,
context: irrefutableContext,
@ -1036,17 +1159,16 @@ mixin TypeAnalyzer<
// Stack: (Expression)
if (errors != null && operator != null) {
Type argumentType = operator.isEquality
? typeOperations.promoteToNonNull(operandType)
? operations.promoteToNonNull(operandType)
: operandType;
if (!typeOperations.isAssignableTo(
argumentType, operator.parameterType)) {
if (!operations.isAssignableTo(argumentType, operator.parameterType)) {
errors.argumentTypeNotAssignable(
argument: operand,
argumentType: argumentType,
parameterType: operator.parameterType,
);
}
if (!typeOperations.isAssignableTo(operator.returnType, boolType)) {
if (!operations.isAssignableTo(operator.returnType, boolType)) {
errors.relationalPatternOperatorReturnTypeNotAssignableToBool(
node: node,
returnType: operator.returnType,
@ -1118,7 +1240,7 @@ mixin TypeAnalyzer<
if (lubType == null) {
lubType = type;
} else {
lubType = typeOperations.lub(lubType, type);
lubType = operations.lub(lubType, type);
}
finishExpressionCase(node, i);
// Stack: (Expression, (i + 1) * ExpressionCase)
@ -1250,62 +1372,6 @@ mixin TypeAnalyzer<
return inferredType;
}
/// Analyzes a variable pattern. [node] is the pattern itself, [variable] is
/// the variable, [declaredType] is the explicitly declared type (if present),
/// and [isFinal] indicates whether the variable is final.
///
/// See [dispatchPattern] for the meanings of [matchedType] and [context].
///
/// If this is a wildcard pattern (it doesn't bind any variable), [variable]
/// should be `null`.
///
/// Returns the static type of the variable (possibly inferred).
///
/// Stack effect: none.
Type analyzeVariablePattern(
Type matchedType,
MatchContext<Node, Expression, Pattern, Type, Variable> context,
Pattern node,
Variable? variable,
String? name,
Type? declaredType,
) {
Type staticType =
declaredType ?? variableTypeFromInitializerType(matchedType);
Node? irrefutableContext = context.irrefutableContext;
if (irrefutableContext != null &&
!typeOperations.isAssignableTo(matchedType, staticType)) {
errors?.patternTypeMismatchInIrrefutableContext(
pattern: node,
context: irrefutableContext,
matchedType: matchedType,
requiredType: staticType);
}
flow?.variablePattern(matchedType: matchedType, staticType: staticType);
bool isImplicitlyTyped = declaredType == null;
if (variable != null) {
if (name == null) {
throw new StateError(
'When the variable is not null, the name must also be not null');
}
flow?.declare(variable, false);
setVariableType(variable, staticType);
// TODO(paulberry): are we handling _isFinal correctly?
flow?.initialize(variable, matchedType, context.getInitializer(node),
isFinal: context.isFinal || isVariableFinal(variable),
isLate: context.isLate,
isImplicitlyTyped: isImplicitlyTyped);
}
return staticType;
}
/// Computes the type schema for a variable pattern. [declaredType] is the
/// explicitly declared type (if present).
///
/// Stack effect: none.
Type analyzeVariablePatternSchema(Type? declaredType) =>
declaredType ?? unknownType;
/// If [type] is a record type, returns it.
RecordType<Type>? asRecordType(Type type);
@ -1633,7 +1699,7 @@ mixin TypeAnalyzer<
// TODO(paulberry): spec says the type must be `bool` or `dynamic`. This
// logic permits `T extends bool`, `T promoted to bool`, or `Never`. What
// do we want?
if (!typeOperations.isAssignableTo(type, boolType)) {
if (!operations.isAssignableTo(type, boolType)) {
errors?.nonBooleanCondition(expression);
}
}
@ -1756,9 +1822,9 @@ mixin TypeAnalyzer<
}
bool _structurallyEqualAfterNormTypes(Type type1, Type type2) {
Type norm1 = typeOperations.normalize(type1);
Type norm2 = typeOperations.normalize(type2);
return typeOperations.areStructurallyEqual(norm1, norm2);
Type norm1 = operations.normalize(type1);
Type norm2 = operations.normalize(type2);
return operations.areStructurallyEqual(norm1, norm2);
}
}

View file

@ -29,7 +29,7 @@ class FlowAnalysisTestHarness extends Harness implements FlowModelHelper<Type> {
final PromotionKeyStore<Var> promotionKeyStore = PromotionKeyStore();
@override
Operations<Var, Type> get typeOperations => typeAnalyzer.typeOperations;
Operations<Var, Type> get typeOperations => typeAnalyzer.operations;
}
/// Helper class allowing tests to examine the values of variables' SSA nodes.

View file

@ -5838,6 +5838,26 @@ main() {
]);
});
test('due to pattern assignment', () {
var x = Var('x');
late Pattern writePattern;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.expr.eq(nullLiteral), [
return_(),
]),
checkPromoted(x, 'int'),
(writePattern = x.pattern()).assign(expr('int?')).stmt,
checkNotPromoted(x),
x.expr.whyNotPromoted((reasons) {
expect(reasons.keys, unorderedEquals([Type('int')]));
var nonPromotionReason =
reasons.values.single as DemoteViaExplicitWrite<Var>;
expect(nonPromotionReason.node, same(writePattern));
}).stmt,
]);
});
test('preserved in join when one branch unreachable', () {
var x = Var('x');
late Expression writeExpression;
@ -6377,6 +6397,118 @@ main() {
});
group('Patterns:', () {
group('Assignment:', () {
group('Demotion', () {
test('Demoting', () {
var x = Var('x');
h.run([
declare(x, type: 'int?'),
x.expr.nonNullAssert.stmt,
checkPromoted(x, 'int'),
x.pattern().assign(expr('int?')).stmt,
checkNotPromoted(x),
]);
});
test('Non-demoting', () {
var x = Var('x');
h.run([
declare(x, type: 'num?'),
x.expr.nonNullAssert.stmt,
checkPromoted(x, 'num'),
x.pattern().assign(expr('int')).stmt,
checkPromoted(x, 'num'),
]);
});
});
group('Schema:', () {
test('Not promoted', () {
var x = Var('x');
h.run([
declare(x, type: 'int?'),
x.pattern().assign(expr('int').checkContext('int?')).stmt,
]);
});
test('Promoted', () {
var x = Var('x');
h.run([
declare(x, type: 'int?'),
x.expr.nonNullAssert.stmt,
checkPromoted(x, 'int'),
x.pattern().assign(expr('int').checkContext('int')).stmt,
]);
});
});
group('Promotion:', () {
test('Type of interest', () {
var x = Var('x');
h.run([
declare(x, type: 'num'),
if_(x.expr.is_('int'), []),
checkNotPromoted(x),
x.pattern().assign(expr('int')).stmt,
checkPromoted(x, 'int'),
]);
});
test('Not a type of interest', () {
var x = Var('x');
h.run([
declare(x, type: 'num'),
x.pattern().assign(expr('int')).stmt,
checkNotPromoted(x),
]);
});
});
test('Definite assignment', () {
var x = Var('x');
h.run([
declare(x, type: 'int'),
checkAssigned(x, false),
x.pattern().assign(expr('int')).stmt,
checkAssigned(x, true),
]);
});
group('Boolean condition', () {
test('As main pattern', () {
var b = Var('b');
var x = Var('x');
h.run([
declare(x, type: 'int?'),
declare(b, type: 'bool'),
b.pattern().assign(x.expr.notEq(nullLiteral)).stmt,
if_(b.expr, [
// `x` is promoted because `b` is known to equal `x != null`.
checkPromoted(x, 'int'),
]),
]);
});
test('As subpattern', () {
var b = Var('b');
var x = Var('x');
h.run([
declare(x, type: 'int?'),
declare(b, type: 'bool'),
recordPattern([b.pattern().recordField()])
.assign(x.expr.notEq(nullLiteral).as_('dynamic'))
.stmt,
if_(b.expr, [
// Even though the RHS of the pattern is `x != null`, `x` is not
// promoted because the pattern for `b` is in a subpattern
// position.
checkNotPromoted(x),
]),
]);
});
});
});
group('If-case element:', () {
test('guarded', () {
var x = Var('x');

View file

@ -539,7 +539,8 @@ class ExpressionCase extends Node {
void _preVisit(PreVisitor visitor) {
var variableBinder = _VariableBinder(errors: visitor.errors);
variableBinder.casePatternStart();
guardedPattern?.pattern.preVisit(visitor, variableBinder);
guardedPattern?.pattern
.preVisit(visitor, variableBinder, isInAssignment: false);
variableBinder.casePatternFinish();
variableBinder.finish();
expression.preVisit(visitor);
@ -1251,6 +1252,10 @@ abstract class Pattern extends Node
Pattern as_(String type) =>
new _CastPattern(this, Type(type), location: computeLocation());
/// Creates a pattern assignment expression assigning [rhs] to this pattern.
Expression assign(Expression rhs) =>
_PatternAssignment(this, rhs, location: computeLocation());
Type computeSchema(Harness h);
Pattern or(Pattern other) =>
@ -1633,8 +1638,9 @@ class _CastPattern extends Pattern {
Type computeSchema(Harness h) => h.typeAnalyzer.analyzeCastPatternSchema();
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
_inner.preVisit(visitor, variableBinder);
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
_inner.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
}
@override
@ -1980,7 +1986,8 @@ class _ConstantPattern extends Pattern {
h.typeAnalyzer.analyzeConstantPatternSchema();
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
constant.preVisit(visitor);
}
@ -2029,7 +2036,7 @@ class _Declare extends Statement {
void preVisit(PreVisitor visitor) {
var variableBinder = _VariableBinder(errors: visitor.errors);
variableBinder.casePatternStart();
pattern.preVisit(visitor, variableBinder);
pattern.preVisit(visitor, variableBinder, isInAssignment: false);
variableBinder.casePatternFinish();
variableBinder.finish();
if (isLate) {
@ -2062,7 +2069,7 @@ class _Declare extends Statement {
var staticType = h.typeAnalyzer.analyzeUninitializedVariableDeclaration(
this, pattern.variable!, pattern.declaredType,
isFinal: isFinal, isLate: isLate);
h.typeAnalyzer.handleVariablePattern(pattern,
h.typeAnalyzer.handleDeclaredVariablePattern(pattern,
matchedType: staticType, staticType: staticType);
irName = 'declare';
argKinds = [Kind.pattern];
@ -2394,7 +2401,7 @@ class _IfCase extends _IfBase {
_expression.preVisit(visitor);
var variableBinder = _VariableBinder(errors: visitor.errors);
variableBinder.casePatternStart();
_pattern.preVisit(visitor, variableBinder);
_pattern.preVisit(visitor, variableBinder, isInAssignment: false);
_candidateVariables = variableBinder.casePatternFinish();
variableBinder.finish();
_guard?.preVisit(visitor);
@ -2438,7 +2445,7 @@ class _IfCaseElement extends _IfElementBase {
_expression.preVisit(visitor);
var variableBinder = _VariableBinder(errors: visitor.errors);
variableBinder.casePatternStart();
_pattern.preVisit(visitor, variableBinder);
_pattern.preVisit(visitor, variableBinder, isInAssignment: false);
variableBinder.casePatternFinish();
variableBinder.finish();
_guard?.preVisit(visitor);
@ -2626,7 +2633,8 @@ class _LabeledStatement extends Statement {
}
abstract class _ListOrMapPatternElement implements Node {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder);
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment});
String _debugString({required bool needsKeywordOrType});
}
@ -2644,9 +2652,10 @@ class _ListPattern extends Pattern {
.analyzeListPatternSchema(elementType: _elementType, elements: _elements);
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
for (var element in _elements) {
element.preVisit(visitor, variableBinder);
element.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
}
}
@ -2749,15 +2758,16 @@ class _LogicalPattern extends Pattern {
h.typeAnalyzer.analyzeLogicalPatternSchema(_lhs, _rhs, isAnd: isAnd);
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
if (isAnd) {
_lhs.preVisit(visitor, variableBinder);
_rhs.preVisit(visitor, variableBinder);
_lhs.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
_rhs.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
} else {
variableBinder.logicalOrPatternStart();
_lhs.preVisit(visitor, variableBinder);
_lhs.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
variableBinder.logicalOrPatternFinishLeft();
_rhs.preVisit(visitor, variableBinder);
_rhs.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
variableBinder.logicalOrPatternFinish(this);
}
}
@ -2813,9 +2823,10 @@ class _MapPattern extends Pattern {
typeArguments: _typeArguments, elements: _elements);
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
for (var element in _elements) {
element.preVisit(visitor, variableBinder);
element.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
}
}
@ -2856,8 +2867,9 @@ class _MapPatternEntry extends Node implements MapPatternElement {
_MapPatternEntry(this.key, this.value, {required super.location}) : super._();
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
value.preVisit(visitor, variableBinder);
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
value.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
}
@override
@ -3065,10 +3077,10 @@ class _MiniAstTypeAnalyzer
FlowAnalysis<Node, Statement, Expression, Var, Type> get flow =>
_harness.flow;
Type get thisType => _harness._thisType!;
@override
MiniAstOperations get typeOperations => _harness._operations;
MiniAstOperations get operations => _harness._operations;
Type get thisType => _harness._thisType!;
void analyzeAssertStatement(
Statement node, Expression condition, Expression? message) {
@ -3372,7 +3384,7 @@ class _MiniAstTypeAnalyzer
if (requiredType.args.isNotEmpty) {
return requiredType;
} else {
return typeOperations.downwardInfer(requiredType.name, matchedType);
return operations.downwardInfer(requiredType.name, matchedType);
}
}
@ -3491,6 +3503,15 @@ class _MiniAstTypeAnalyzer
);
}
void handleAssignedVariablePattern(covariant _VariablePattern node) {
_irBuilder.atom(node.variable!.name, Kind.variable,
location: node.location);
_irBuilder.apply('assignedVarPattern', [Kind.variable], Kind.pattern,
location: node.location);
assert(node.expectInferredType == null,
"assigned variable patterns don't get an inferred type");
}
@override
void handleCase_afterCaseHeads(
covariant _SwitchStatement node, int caseIndex, Iterable<Var> variables) {
@ -3524,6 +3545,21 @@ class _MiniAstTypeAnalyzer
location: node.location);
}
void handleDeclaredVariablePattern(covariant _VariablePattern node,
{required Type matchedType, required Type staticType}) {
_irBuilder.atom(node.variable?.name ?? '_', Kind.variable,
location: node.location);
_irBuilder.atom(matchedType.type, Kind.type, location: node.location);
_irBuilder.atom(staticType.type, Kind.type, location: node.location);
_irBuilder.apply(
'varPattern', [Kind.variable, Kind.type, Kind.type], Kind.pattern,
names: ['matchedType', 'staticType'], location: node.location);
var expectInferredType = node.expectInferredType;
if (expectInferredType != null) {
expect(staticType.type, expectInferredType);
}
}
@override
void handleDefault(Node node, int caseIndex) {
_irBuilder.atom('default', Kind.caseHead, location: node.location);
@ -3610,21 +3646,6 @@ class _MiniAstTypeAnalyzer
@override
void handleSwitchScrutinee(Type type) {}
void handleVariablePattern(covariant _VariablePattern node,
{required Type matchedType, required Type staticType}) {
_irBuilder.atom(node.variable?.name ?? '_', Kind.variable,
location: node.location);
_irBuilder.atom(matchedType.type, Kind.type, location: node.location);
_irBuilder.atom(staticType.type, Kind.type, location: node.location);
_irBuilder.apply(
'varPattern', [Kind.variable, Kind.type, Kind.type], Kind.pattern,
names: ['matchedType', 'staticType'], location: node.location);
var expectInferredType = node.expectInferredType;
if (expectInferredType != null) {
expect(staticType.type, expectInferredType);
}
}
@override
bool isRestPatternElement(Node element) {
return element is _RestPatternElement;
@ -3807,8 +3828,9 @@ class _NullCheckOrAssertPattern extends Pattern {
.analyzeNullCheckOrAssertPatternSchema(_inner, isAssert: _isAssert);
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
_inner.preVisit(visitor, variableBinder);
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
_inner.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
}
@override
@ -3864,12 +3886,11 @@ class _ObjectPattern extends Pattern {
}
@override
void preVisit(
PreVisitor visitor,
VariableBinder<Node, Var> variableBinder,
) {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
for (var field in fields) {
field.pattern.preVisit(visitor, variableBinder);
field.pattern
.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
}
}
@ -3922,6 +3943,32 @@ class _ParenthesizedExpression extends Expression {
}
}
class _PatternAssignment extends Expression {
final Pattern _pattern;
final Expression _rhs;
_PatternAssignment(this._pattern, this._rhs, {required super.location});
@override
void preVisit(PreVisitor visitor) {
var variableBinder = _VariableBinder(errors: visitor.errors);
variableBinder.casePatternStart();
_pattern.preVisit(visitor, variableBinder, isInAssignment: true);
variableBinder.casePatternFinish();
variableBinder.finish();
_rhs.preVisit(visitor);
}
@override
ExpressionTypeAnalysisResult<Type> visit(Harness h, Type context) {
var result = h.typeAnalyzer.analyzePatternAssignment(this, _pattern, _rhs);
h.irBuilder.apply(
'patternAssignment', [Kind.expression, Kind.pattern], Kind.expression,
location: location);
return result;
}
}
class _PlaceholderExpression extends Expression {
final Type type;
@ -3998,12 +4045,11 @@ class _RecordPattern extends Pattern {
}
@override
void preVisit(
PreVisitor visitor,
VariableBinder<Node, Var> variableBinder,
) {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
for (var field in fields) {
field.pattern.preVisit(visitor, variableBinder);
field.pattern
.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
}
}
@ -4048,7 +4094,8 @@ class _RelationalPattern extends Pattern {
h.typeAnalyzer.analyzeRelationalPatternSchema();
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
operand.preVisit(visitor);
}
@ -4077,8 +4124,9 @@ class _RestPatternElement extends Node
_RestPatternElement(this._pattern, {required super.location}) : super._();
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
_pattern?.preVisit(visitor, variableBinder);
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
_pattern?.preVisit(visitor, variableBinder, isInAssignment: isInAssignment);
}
@override
@ -4255,7 +4303,8 @@ class _SwitchStatementMember extends Node {
for (SwitchHead element in elements) {
if (element is _SwitchHeadCase) {
variableBinder.casePatternStart();
element.guardedPattern.pattern.preVisit(visitor, variableBinder);
element.guardedPattern.pattern
.preVisit(visitor, variableBinder, isInAssignment: false);
element.guardedPattern.guard?.preVisit(visitor);
element.guardedPattern.variables = variableBinder.casePatternFinish(
sharedCaseScopeKey: this,
@ -4441,20 +4490,35 @@ class _VariablePattern extends Pattern {
final String? expectInferredType;
late bool isAssignedVariable;
_VariablePattern(this.declaredType, this.variable, this.expectInferredType,
{required super.location})
: super._();
@override
Type computeSchema(Harness h) =>
h.typeAnalyzer.analyzeVariablePatternSchema(declaredType);
Type computeSchema(Harness h) {
if (isAssignedVariable) {
return h.typeAnalyzer.analyzeAssignedVariablePatternSchema(variable!);
} else {
return h.typeAnalyzer.analyzeDeclaredVariablePatternSchema(declaredType);
}
}
@override
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder) {
void preVisit(PreVisitor visitor, VariableBinder<Node, Var> variableBinder,
{required bool isInAssignment}) {
var variable = this.variable;
if (variable != null && variableBinder.add(variable.name, variable)) {
isAssignedVariable = isInAssignment && variable != null;
if (!isAssignedVariable &&
variable != null &&
variableBinder.add(variable.name, variable)) {
visitor._assignedVariables.declare(variable);
}
if (isAssignedVariable) {
assert(declaredType == null,
"Variables in pattern assignments can't have declared types");
}
}
@override
@ -4463,11 +4527,17 @@ class _VariablePattern extends Pattern {
Type matchedType,
SharedMatchContext context,
) {
var staticType = h.typeAnalyzer.analyzeVariablePattern(
if (isAssignedVariable) {
h.typeAnalyzer.analyzeAssignedVariablePattern(
matchedType, context, this, variable!);
h.typeAnalyzer.handleAssignedVariablePattern(this);
} else {
var staticType = h.typeAnalyzer.analyzeDeclaredVariablePattern(
matchedType, context, this, variable, variable?.name, declaredType);
h.typeAnalyzer.handleVariablePattern(this,
h.typeAnalyzer.handleDeclaredVariablePattern(this,
matchedType: matchedType, staticType: staticType);
}
}
@override
_debugString({required bool needsKeywordOrType}) => [

View file

@ -2418,6 +2418,67 @@ main() {
});
});
group('Pattern assignment:', () {
test('Static type', () {
var x = Var('x');
h.run([
declare(x, type: 'num'),
x.pattern().assign(expr('int')).checkType('int').stmt,
]);
});
test('RHS context', () {
var x = Var('x');
h.run([
declare(x, type: 'num'),
x
.pattern()
.assign(expr('int').checkContext('num'))
.inContext('Object'),
]);
});
group('Refutability:', () {
test('When matched type is a subtype of variable type', () {
var x = Var('x');
h.run([
declare(x, type: 'num'),
x
.pattern()
.assign(expr('int'))
.checkIr('patternAssignment(expr(int), assignedVarPattern(x))')
.stmt,
]);
});
test('When matched type is dynamic', () {
var x = Var('x');
h.run([
declare(x, type: 'num'),
x
.pattern()
.assign(expr('dynamic'))
.checkIr(
'patternAssignment(expr(dynamic), assignedVarPattern(x))')
.stmt,
]);
});
test('When matched type is not a subtype of variable type', () {
var x = Var('x');
h.run([
declare(x, type: 'num'),
((x.pattern()..errorId = 'PATTERN').assign(expr('String'))
..errorId = 'CONTEXT')
.stmt,
], expectedErrors: {
'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, '
'context: CONTEXT, matchedType: String, requiredType: num)'
});
});
});
});
group('Record:', () {
group('Positional:', () {
group('Match dynamic:', () {

View file

@ -3623,7 +3623,8 @@ class DeclaredVariablePatternImpl extends DartPatternImpl
@override
DartType computePatternSchema(ResolverVisitor resolverVisitor) {
return resolverVisitor.analyzeVariablePatternSchema(type?.typeOrThrow);
return resolverVisitor
.analyzeDeclaredVariablePatternSchema(type?.typeOrThrow);
}
@override
@ -3632,7 +3633,7 @@ class DeclaredVariablePatternImpl extends DartPatternImpl
DartType matchedType,
SharedMatchContext context,
) {
resolverVisitor.analyzeVariablePattern(matchedType, context, this,
resolverVisitor.analyzeDeclaredVariablePattern(matchedType, context, this,
declaredElement, declaredElement?.name, type?.typeOrThrow);
}

View file

@ -14,7 +14,6 @@ import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer.dart'
as shared;
import 'package:_fe_analyzer_shared/src/type_inference/type_operations.dart'
as shared;
import 'package:_fe_analyzer_shared/src/type_inference/type_operations.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/syntactic_entity.dart';
@ -487,6 +486,10 @@ class ResolverVisitor extends ThrowingAstVisitor<void>
@override
DartType get objectQuestionType => typeSystem.objectQuestion;
@override
Operations<PromotableElement, DartType> get operations =>
flowAnalysis.typeOperations;
/// Gets the current depth of the [_rewriteStack]. This may be used in
/// assertions to verify that pushes and pops are properly balanced.
int get rewriteStackDepth => _rewriteStack.length;
@ -501,9 +504,6 @@ class ResolverVisitor extends ThrowingAstVisitor<void>
return _thisType;
}
@override
TypeOperations<DartType> get typeOperations => flowAnalysis.typeOperations;
@override
DartType get unknownType => UnknownInferredType.instance;

View file

@ -8,7 +8,6 @@ import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer.dart'
hide NamedType, RecordType;
import 'package:_fe_analyzer_shared/src/type_inference/type_analyzer.dart'
as shared;
import 'package:_fe_analyzer_shared/src/type_inference/type_operations.dart';
import 'package:_fe_analyzer_shared/src/util/link.dart';
import 'package:front_end/src/api_prototype/lowering_predicates.dart';
import 'package:kernel/ast.dart';
@ -98,7 +97,7 @@ class InferenceVisitorImpl extends InferenceVisitorBase
Class? mapEntryClass;
@override
final TypeOperations<DartType> typeOperations;
final Operations<VariableDeclaration, DartType> operations;
/// Context information for the current closure, or `null` if we are not
/// inside a closure.
@ -133,7 +132,7 @@ class InferenceVisitorImpl extends InferenceVisitorBase
helper: helper, uriForInstrumentation: uriForInstrumentation);
InferenceVisitorImpl(
TypeInferrerImpl inferrer, InferenceHelper helper, this.typeOperations)
TypeInferrerImpl inferrer, InferenceHelper helper, this.operations)
: options = new TypeAnalyzerOptions(
nullSafetyEnabled: inferrer.libraryBuilder.isNonNullableByDefault,
patternsEnabled: false),
@ -8816,8 +8815,8 @@ class InferenceVisitorImpl extends InferenceVisitorBase
required DartType matchedType,
required SharedMatchContext context,
}) {
DartType inferredType = analyzeVariablePattern(matchedType, context, binder,
binder.variable, binder.variable.name, binder.type);
DartType inferredType = analyzeDeclaredVariablePattern(matchedType, context,
binder, binder.variable, binder.variable.name, binder.type);
instrumentation?.record(uriForInstrumentation, binder.variable.fileOffset,
'type', new InstrumentationValueForType(inferredType));
if (binder.type == null) {

View file

@ -182,7 +182,8 @@ abstract class InferenceVisitorBase implements InferenceVisitor {
/// Provides access to the [OperationsCfe] object. This is needed by
/// [isAssignable].
OperationsCfe get operations => _inferrer.operations;
Operations<VariableDeclaration, DartType> get operations =>
_inferrer.operations;
TypeSchemaEnvironment get typeSchemaEnvironment =>
_inferrer.typeSchemaEnvironment;