blob: 3c122b55f48ece4f7e88bce6c8383c9a29f4ca56 [file] [log] [blame]
// Copyright (c) 2022, 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.
import 'type_analyzer.dart';
/// Data structure for tracking declared pattern variables.
///
/// To analyze a single `case pattern when guard`:
/// 1. Invoke [casePatternStart].
/// 2. Invoke zero or more [add].
/// 3. Invoke [casePatternFinish], get the set of variables `VS`.
/// 4. Use `VS` to analyze the guard.
///
/// To analyze a group of `case` members of a `switch` statement, sharing
/// the same body, and so having the shared set of pattern variables:
/// 1. Invoke [switchStatementSharedCaseScopeStart].
/// 2. Analyze individual `case pattern when guard` clauses.
/// 3. Invoke [switchStatementSharedCaseScopeEmpty] if there are labels,
/// or a `default` member.
/// 4. Invoke [switchStatementSharedCaseScopeFinish] to get the set of
/// variables `VS`, and use it to analyze the shared body.
abstract class VariableBinder<Node extends Object, Variable extends Object> {
/// The interface for reporting error conditions up to the client.
final VariableBinderErrors<Node, Variable>? errors;
/// The stack of variable sets, starting with an empty one on
/// [casePatternStart], or [logicalOrPatternStart].
List<Map<String, Variable>> _variables = [];
/// The stack of variable sets for potentially nested (e.g. `switch` in
/// a closure in a `when` clause) groups of `case` members of a `switch`
/// statement.
List<_SharedCaseScope<Variable>> _sharedCaseScopes = [];
VariableBinder({
required this.errors,
});
/// Updates the set of bindings to account for the presence of a variable
/// pattern. [name] is the name of the variable, [variable] is the object
/// that represents it in the client.
bool add(String name, Variable variable) {
Variable? existing = _variables.last[name];
if (existing == null) {
_variables.last[name] = variable;
return true;
} else {
errors?.duplicateVariablePattern(
name: name,
original: existing,
duplicate: variable,
);
return false;
}
}
/// Should be invoked after visiting a `case pattern` structure. Returns
/// all the accumulated variables (individual and joined).
///
/// If [sharedCaseScopeKey] is provided, it expected to be the same as
/// the key of the last shared case scope, and the resulting set will be
/// joined with the current shared case scope.
Map<String, Variable> casePatternFinish({
Object? sharedCaseScopeKey,
}) {
Map<String, Variable> variables = _variables.removeLast();
if (sharedCaseScopeKey != null) {
_SharedCaseScope<Variable> sharedScope = _sharedCaseScopes.last;
assert(sharedScope.key == sharedCaseScopeKey);
sharedScope.addAll(variables);
}
return variables;
}
/// Notifies that are new `case pattern` structure is about to be visited.
void casePatternStart() {
_variables.add({});
}
/// Notifies that this instance is about to be discarded.
void finish() {
assert(_variables.isEmpty);
assert(_sharedCaseScopes.isEmpty);
}
/// Returns a new variable that is a join of [components].
Variable joinPatternVariables({
required Object key,
required List<Variable> components,
required JoinedPatternVariableInconsistency inconsistency,
});
/// Updates the binder after visiting a logical-or pattern, joins variables
/// from them. If some variables are in one side of the pattern, but not
/// in another, they are still joined, but marked as not consistent.
void logicalOrPatternFinish(Node node) {
Map<String, Variable> right = _variables.removeLast();
Map<String, Variable> left = _variables.removeLast();
for (MapEntry<String, Variable> leftEntry in left.entries) {
String name = leftEntry.key;
Variable leftVariable = leftEntry.value;
Variable? rightVariable = right.remove(name);
if (rightVariable != null) {
add(
name,
joinPatternVariables(
key: node,
components: [leftVariable, rightVariable],
inconsistency: JoinedPatternVariableInconsistency.none,
),
);
} else {
errors?.logicalOrPatternBranchMissingVariable(
node: node,
hasInLeft: true,
name: name,
variable: leftVariable,
);
add(
name,
joinPatternVariables(
key: node,
components: [leftVariable],
inconsistency: JoinedPatternVariableInconsistency.logicalOr,
),
);
}
}
for (MapEntry<String, Variable> rightEntry in right.entries) {
String name = rightEntry.key;
Variable rightVariable = rightEntry.value;
errors?.logicalOrPatternBranchMissingVariable(
node: node,
hasInLeft: false,
name: name,
variable: rightVariable,
);
add(
name,
joinPatternVariables(
key: node,
components: [rightVariable],
inconsistency: JoinedPatternVariableInconsistency.logicalOr,
),
);
}
}
/// Notifies that the LHS of a logical-or pattern was visited, and the RHS
/// is about to be visited.
void logicalOrPatternFinishLeft() {
_variables.add({});
}
/// Notifies that we are about to start visiting a logical-or pattern.
void logicalOrPatternStart() {
_variables.add({});
}
/// Notifies that the `default` case head, or a label, was found, so that
/// all the variables of the current shared case scope are not consistent.
void switchStatementSharedCaseScopeEmpty(Object key) {
_SharedCaseScope<Variable> sharedScope = _sharedCaseScopes.last;
assert(sharedScope.key == key);
sharedScope.hasLabel = true;
sharedScope.addAll({});
}
/// Notifies that computing of the shared case scope was finished, returns
/// the joined set of variables. The variables have not been checked to
/// have the same types (because we have not done inference, so we don't
/// know types for many of them), so some of them might become not
/// consistent later.
Map<String, Variable> switchStatementSharedCaseScopeFinish(Object key) {
assert(_variables.isEmpty);
_SharedCaseScope<Variable> sharedScope = _sharedCaseScopes.removeLast();
assert(sharedScope.key == key);
Map<String, Variable> result = {};
for (MapEntry<String, _SharedCaseScopeVariable<Variable>> entry
in sharedScope.variables.entries) {
_SharedCaseScopeVariable<Variable> sharedVariable = entry.value;
List<Variable> variables = sharedVariable.variables;
if (sharedVariable.allCases && variables.length == 1) {
result[entry.key] = variables[0];
} else {
result[entry.key] = joinPatternVariables(
key: key,
components: variables,
inconsistency: sharedVariable.allCases
? JoinedPatternVariableInconsistency.none
: sharedScope.hasLabel
? JoinedPatternVariableInconsistency.sharedCaseHasLabel
: JoinedPatternVariableInconsistency.sharedCaseAbsent,
);
}
}
return result;
}
/// Notifies that computing new shared case scope should be started.
void switchStatementSharedCaseScopeStart(Object key) {
assert(_variables.isEmpty);
_sharedCaseScopes.add(
new _SharedCaseScope(key),
);
}
}
/// Interface used by the [VariableBinder] logic to report error conditions
/// up to the client during the "pre-visit" phase of type analysis.
abstract class VariableBinderErrors<Node extends Object,
Variable extends Object> extends TypeAnalyzerErrorsBase {
/// Called when a pattern attempts to declare the variable [duplicate] that
/// has the same [name] as the [original] variable.
void duplicateVariablePattern({
required String name,
required Variable original,
required Variable duplicate,
});
/// Called when one of the branches has the [variable] with the [name], but
/// the other branch does not.
void logicalOrPatternBranchMissingVariable({
required Node node,
required bool hasInLeft,
required String name,
required Variable variable,
});
}
class _SharedCaseScope<Variable extends Object> {
final Object key;
bool isEmpty = true;
bool hasLabel = false;
Map<String, _SharedCaseScopeVariable<Variable>> variables = {};
_SharedCaseScope(this.key);
/// Adds [newVariables] to [variables], marking absent variables as not
/// consistent. If [isEmpty], just sets given variables as the starting set.
void addAll(Map<String, Variable> newVariables) {
if (isEmpty) {
isEmpty = false;
for (MapEntry<String, Variable> entry in newVariables.entries) {
String name = entry.key;
Variable variable = entry.value;
_getVariable(name).variables.add(variable);
}
} else {
for (MapEntry<String, _SharedCaseScopeVariable<Variable>> entry
in variables.entries) {
String name = entry.key;
_SharedCaseScopeVariable<Variable> variable = entry.value;
Variable? newVariable = newVariables[name];
if (newVariable != null) {
variable.variables.add(newVariable);
} else {
variable.allCases = false;
}
}
for (MapEntry<String, Variable> newEntry in newVariables.entries) {
String name = newEntry.key;
Variable newVariable = newEntry.value;
if (!variables.containsKey(name)) {
_getVariable(name)
..allCases = false
..variables.add(newVariable);
}
}
}
}
_SharedCaseScopeVariable _getVariable(String name) {
return variables[name] ??= new _SharedCaseScopeVariable();
}
}
class _SharedCaseScopeVariable<Variable extends Object> {
bool allCases = true;
final List<Variable> variables = [];
}