Skip to content

Commit e8e219a

Browse files
author
Tao Zhuang
committed
[NarrowedAny] Cross-spelling container reshape: full diagnostic + auto-fix-it
Implements proposal §"Containers and extensions" + decision row swiftlang#21 / swiftlang#33 for the container axis. `let xs: [String|Int] = zs` (with `zs: [Int|String]`) now reports error: cannot assign value of type '[Int | String]' to type '[String | Int]' note: arguments to generic parameter 'Element' ('Int | String' and 'String | Int') are expected to be equal fix-it: ^ as [String | Int] with an `as` (not `as!`) fix-it, since cross-spelling reshape is a runtime-free relabel — both sides erase to the same Any-singleton element layout, so SIL emits a single `unchecked_addr_cast` for the coercion. Two changes: - `matchDeepEqualityTypes`'s NarrowedAnyType branch detects the cross-spelling case (canonical leaf sets equal but spellings differ) before the leaf-by-leaf Bind. When the locator path carries an `AnyRequirement` element (e.g. `extension Array where Element == X | Y` dispatch checking a substituted `Element` of a different spelling), fix the same-type requirement directly via `fixRequirementFailure` so SameTypeRequirementFailure prints the full narrowed-Any spellings instead of the buggy `any Int` / `any String` fallout from the previous malformed-wrap path. For the plain conversion case, fall through to the leaf-by-leaf Bind but tag each leaf locator with a fresh `GenericArgument` element so `repairFailures` doesn't inherit `ExistentialConstraintType` from the enclosing existential match (which used to construct the malformed `ExistentialType::get(<concrete leaf>)` wrap that defeated upstream `GenericArgumentsMismatch` recording). - `ContextualFailure::tryTypeCoercionFixIt` recognises cross-spelling NarrowedAnyType at container position structurally (same generic decl, every generic arg either equal or cross-spelling-equal NarrowedAnyType) and emits the ` as <toType>` fix-it directly, bypassing the generic `typeCheckCheckedCast` path which would classify it as ArrayDowncast (and suggest the wrong `as!` direction). Restricted to the `BoundGenericType` case so the value-level `Int|String → String|Int` path keeps using the existing `typeCheckCheckedCast → Coercion` flow with both `as` and `as!` fix-its preserved unchanged. `diagnostics.swift §13` locks the full diagnostic text, the generic-parameter-mismatch note, and the fix-it insertion. lit 13/13.
1 parent 9794460 commit e8e219a

3 files changed

Lines changed: 146 additions & 13 deletions

File tree

lib/Sema/CSDiagnostics.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3950,6 +3950,66 @@ bool ContextualFailure::tryTypeCoercionFixIt(
39503950
}
39513951
}
39523952

3953+
// Cross-spelling narrowed-`Any` reshape at *container* position
3954+
// (proposal §"Containers and extensions"; row #21 + #33). The
3955+
// value-level case (`a as String | Int` where `a: Int | String`)
3956+
// is already handled by the typeCheckCheckedCast → Coercion path
3957+
// below, so we only short-circuit when the mismatch is anchored on
3958+
// a `BoundGenericType` whose generic args are cross-spelling-equal
3959+
// narrowed-Any. Without this, the typeCheckCheckedCast call below
3960+
// classifies it as ArrayDowncast (suggesting `as!`), but the
3961+
// proposal classifies cross-spelling reshape as a runtime-free `as`
3962+
// relabel (single `unchecked_addr_cast` at SIL level — both sides
3963+
// erase to the same Any-singleton element layout).
3964+
std::function<bool(Type, Type)> isCrossSpellingReshape =
3965+
[&](Type a, Type b) -> bool {
3966+
if (a->getCanonicalType() == b->getCanonicalType())
3967+
return true;
3968+
Type aInner = a;
3969+
if (auto *ext = a->getAs<ExistentialType>())
3970+
aInner = ext->getConstraintType();
3971+
Type bInner = b;
3972+
if (auto *ext = b->getAs<ExistentialType>())
3973+
bInner = ext->getConstraintType();
3974+
if (auto *naA = aInner->getAs<NarrowedAnyType>()) {
3975+
auto *naB = bInner->getAs<NarrowedAnyType>();
3976+
if (!naB)
3977+
return false;
3978+
auto canSet = [](ArrayRef<Type> alts) {
3979+
llvm::SmallVector<CanType, 4> canon;
3980+
for (Type t : alts)
3981+
canon.push_back(t->getCanonicalType());
3982+
llvm::sort(canon, [](CanType x, CanType y) {
3983+
return x.getPointer() < y.getPointer();
3984+
});
3985+
return canon;
3986+
};
3987+
return canSet(naA->getAlternatives()) == canSet(naB->getAlternatives());
3988+
}
3989+
auto *boundA = a->getAs<BoundGenericType>();
3990+
auto *boundB = b->getAs<BoundGenericType>();
3991+
if (!boundA || !boundB || boundA->getDecl() != boundB->getDecl())
3992+
return false;
3993+
auto argsA = boundA->getGenericArgs();
3994+
auto argsB = boundB->getGenericArgs();
3995+
if (argsA.size() != argsB.size())
3996+
return false;
3997+
for (unsigned i = 0, e = argsA.size(); i < e; ++i)
3998+
if (!isCrossSpellingReshape(argsA[i], argsB[i]))
3999+
return false;
4000+
return true;
4001+
};
4002+
if (toType->hasTypeRepr() && fromType->is<BoundGenericType>() &&
4003+
toType->is<BoundGenericType>() &&
4004+
isCrossSpellingReshape(fromType, toType)) {
4005+
auto attachToType = bothOptional ? OptionalType::get(toType) : toType;
4006+
diagnostic.fixItInsert(
4007+
Lexer::getLocForEndOfToken(getASTContext().SourceMgr,
4008+
getSourceRange().End),
4009+
diag::insert_type_coercion, /*canUseAs=*/true, attachToType);
4010+
return true;
4011+
}
4012+
39534013
CheckedCastKind Kind = TypeChecker::typeCheckCheckedCast(
39544014
fromType, toType, CheckedCastContextKind::None, getDC());
39554015

lib/Sema/CSSimplify.cpp

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3999,9 +3999,79 @@ ConstraintSystem::matchDeepEqualityTypes(Type type1, Type type2,
39993999
auto alts2 = na2->getAlternatives();
40004000
if (alts1.size() != alts2.size())
40014001
return getTypeMatchFailure(locator);
4002+
// Cross-spelling detection (decision row #21 / row #33): same leaf
4003+
// SET in different order is "spelling-as-identity unequal" but
4004+
// value-flow leaf-set-equal. For type identity it must fail; for
4005+
// user-facing diagnostics we want a clean fix anchored at the
4006+
// appropriate level (requirement-failure if the path carries one,
4007+
// otherwise the conversion locator), not the leaf-by-leaf Bind
4008+
// path which inherits `ExistentialConstraintType` and routes
4009+
// `repairFailures` through the malformed `ExistentialType::get(Int)`
4010+
// wrap (defensive layout fallback only stops the crash; it doesn't
4011+
// synthesise a useful fix).
4012+
auto canonicalLeafSet = [](ArrayRef<Type> alts) {
4013+
llvm::SmallVector<CanType, 4> canon;
4014+
for (Type t : alts)
4015+
canon.push_back(t->getCanonicalType());
4016+
llvm::sort(canon, [](CanType a, CanType b) {
4017+
return a.getPointer() < b.getPointer();
4018+
});
4019+
return canon;
4020+
};
4021+
auto sortedSet1 = canonicalLeafSet(alts1);
4022+
auto sortedSet2 = canonicalLeafSet(alts2);
4023+
bool sameLeafSet = (sortedSet1 == sortedSet2);
4024+
if (sameLeafSet && shouldAttemptFixes()) {
4025+
// Record a fix anchored at the right level. Walk path looking
4026+
// for an `AnyRequirement` element — if present, this is a
4027+
// requirement-failure context (e.g. extension-where-clause
4028+
// matching), and the right diagnostic is
4029+
// SameTypeRequirementFailure ("requires the types <NA1> and
4030+
// <NA2> be equivalent"). Otherwise this is a plain
4031+
// conversion / generic-argument mismatch and the right anchor
4032+
// is the conversion locator (yielding "cannot convert value of
4033+
// type <X> to type <Y>" + cross-shape `as` fix-it via the
4034+
// GenericArgumentsMismatch path at the BoundGenericType match
4035+
// level above us).
4036+
llvm::SmallVector<LocatorPathElt, 4> path;
4037+
auto anchor = locator.getLocatorParts(path);
4038+
// Scan for the deepest AnyRequirement element.
4039+
int reqIdx = -1;
4040+
for (int i = (int)path.size() - 1; i >= 0; --i) {
4041+
if (path[i].is<LocatorPathElt::AnyRequirement>()) {
4042+
reqIdx = i;
4043+
break;
4044+
}
4045+
}
4046+
if (reqIdx >= 0) {
4047+
// Truncate path to end at the requirement element so the fix
4048+
// is recorded at the requirement locator (the requirement
4049+
// diagnoser walks up from there to the extension / call site).
4050+
ArrayRef<LocatorPathElt> reqPath(path.data(), reqIdx + 1);
4051+
if (auto *fix =
4052+
fixRequirementFailure(*this, type1, type2, anchor, reqPath)) {
4053+
if (!recordFix(fix))
4054+
return getTypeMatchSuccess();
4055+
}
4056+
}
4057+
// Fall through to the leaf-by-leaf Bind path; the BoundGenericType
4058+
// match above will record a `GenericArgumentsMismatch` fix and
4059+
// the constraint solver salvages the proper "cannot convert"
4060+
// diagnostic with cross-shape fix-it.
4061+
}
4062+
// Push a fresh GenericArgument element so leaf-level Bind failures
4063+
// don't inherit `ExistentialConstraintType` from the enclosing
4064+
// ExistentialType match (which would route `repairFailures`
4065+
// through the malformed `ExistentialType::get(<concrete leaf>)`
4066+
// wrap; the defensive fallback in
4067+
// `CanType::getExistentialLayout` stops the crash but the wrap
4068+
// also defeats upstream `GenericArgumentsMismatch` fix recording
4069+
// for cross-spelling container assignment).
40024070
for (unsigned i = 0, e = alts1.size(); i < e; ++i) {
4071+
auto leafLocator = locator.withPathElement(
4072+
LocatorPathElt::GenericArgument(i));
40034073
auto result = matchTypes(alts1[i], alts2[i],
4004-
ConstraintKind::Bind, subflags, locator);
4074+
ConstraintKind::Bind, subflags, leafLocator);
40054075
if (result.isFailure())
40064076
return result;
40074077
}

test/NarrowedAny/diagnostics.swift

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -159,19 +159,22 @@ typealias T11c = (Int | String) & (Int | String) // expected-error {{non-protoc
159159
func f12<T>(_ x: T) where T: Int | String, T: Int | Bool {}
160160
// expected-error@-1 {{no type for 'T' can satisfy both}}
161161

162-
// §13. Cross-spelling Array assignment — used to crash in
162+
// §13. Cross-spelling Array assignment (proposal §"Containers and
163+
// extensions"; row #21 + #33). Implicit cross-spelling container
164+
// conversion is rejected with `cannot assign value of type ...` plus
165+
// a generic-argument-mismatch note pointing at the differing
166+
// `Element` spellings. The user is expected to add an explicit
167+
// reshape `as [String | Int]` (runtime-free relabel — a single
168+
// `unchecked_addr_cast` at SIL level — since both sides erase to the
169+
// same Any-singleton element layout). Used to crash in
163170
// `repairFailures` via a malformed `ExistentialType(<concrete leaf>)`
164-
// that reached `CanType::getExistentialLayout`'s
165-
// `cast<ProtocolCompositionType>` and aborted. The defensive fallback
166-
// in `getExistentialLayout` (return empty layout when the type is
167-
// neither a recognised existential constraint nor a protocol
168-
// composition) stops the crash. The proper diagnostic + fix-it for
169-
// implicit cross-spelling container conversion (proposal §"Containers
170-
// and extensions"; §四 第二项) is multi-day Sema work — the current
171-
// "failed to produce diagnostic" is a known gap, not a goal of this
172-
// test. Verify-mode locks only the no-crash + the present fallback
173-
// diagnostic so a regression to the abort path is caught immediately.
171+
// before the cross-spelling-aware fix in
172+
// `matchDeepEqualityTypes::NarrowedAnyType` branch (records
173+
// `GenericArgumentsMismatch` for plain conversion, hands off to
174+
// `fixRequirementFailure` when a `where`-clause requirement is on
175+
// the locator).
174176
do {
175177
let zs: [Int | String] = [1, "a"]
176-
let _: [String | Int] = zs // expected-error {{failed to produce diagnostic for expression}}
178+
let _: [String | Int] = zs // expected-error {{cannot assign value of type '[Int | String]' to type '[String | Int]'}} {{31-31= as [String | Int]}}
179+
// expected-note@-1 {{arguments to generic parameter 'Element' ('Int | String' and 'String | Int') are expected to be equal}}
177180
}

0 commit comments

Comments
 (0)