Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Account for the optional equality support of map/seq inside newtypes #5980

Merged
merged 14 commits into from
Dec 17, 2024
Merged
8 changes: 7 additions & 1 deletion Source/DafnyCore/AST/Types/UserDefinedType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ public override string TypeName(DafnyOptions options, ModuleDefinition context,

public override bool SupportsEquality {
get {
if (ResolvedClass is ClassLikeDecl { IsReferenceTypeDecl: true } or NewtypeDecl) {
if (ResolvedClass is ClassLikeDecl { IsReferenceTypeDecl: true }) {
return ResolvedClass.IsRevealedInScope(Type.GetScope());
} else if (ResolvedClass is TraitDecl) {
return false;
Expand All @@ -400,6 +400,12 @@ public override bool SupportsEquality {
i++;
}
return true;
} else if (ResolvedClass is NewtypeDecl newtypeDecl) {
if (newtypeDecl.IsRevealedInScope(Type.GetScope())) {
return newtypeDecl.RhsWithArgument(TypeArgs).SupportsEquality;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of my change, we could also do the same treatment as for the datatypes, if you preferred. I'm fine with both approaches and to switch if you prefer.

} else {
return false;
}
} else if (ResolvedClass is TypeSynonymDeclBase) {
var t = (TypeSynonymDeclBase)ResolvedClass;
if (t.SupportsEquality) {
Expand Down
52 changes: 34 additions & 18 deletions Source/DafnyCore/Resolver/ModuleResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2884,8 +2884,7 @@ void MarkSCCAsNotSupportingEquality() {
}
foreach (var ctor in dt.Ctors) {
foreach (var arg in ctor.Formals) {
var type = arg.Type;
DetermineEqualitySupportType(type, ref thingsChanged);
DetermineEqualitySupportType(arg.Type, ref thingsChanged);
}
}
}
Expand Down Expand Up @@ -2929,26 +2928,43 @@ private static void DetermineEqualitySupportType(Type type, ref bool thingsChang
if (type.AsTypeParameter is { } typeArg) {
typeArg.NecessaryForEqualitySupportOfSurroundingInductiveDatatype = true;
thingsChanged = true;
} else if (type.AsMapType is { } typeMap) {
DetermineEqualitySupportType(typeMap.Range, ref thingsChanged);
} else if (type.AsSeqType is { } typeSeq) {
DetermineEqualitySupportType(typeSeq.Arg, ref thingsChanged);
} else if (type.AsIndDatatype is { } otherDt) {
if (otherDt.EqualitySupport == IndDatatypeDecl.ES.ConsultTypeArguments) { // datatype is in a different SCC
var otherUdt = (UserDefinedType)type.NormalizeExpand();
var i = 0;
foreach (var otherTp in otherDt.TypeArgs) {
if (otherTp.NecessaryForEqualitySupportOfSurroundingInductiveDatatype) {
var tp = otherUdt.TypeArgs[i].AsTypeParameter;
if (tp != null) {
tp.NecessaryForEqualitySupportOfSurroundingInductiveDatatype = true;
thingsChanged = true;
} else if (type.SupportsEquality) {
// nothing to do
} else if (type.Normalize() is UserDefinedType userDefinedType) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this normalization? I thought that AsMapType and AsSeqType would actually do that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type.Normalize() cuts through any type proxies that may be sitting around the UserDefinedType.

AsMapType and AsSeqType call NormalizeExpand(), which cuts through both proxies and type synonyms. But that's too much, because a type synonym can also have an explicit (==) declaration.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a type synonym does not have an explicit (==) annotation, it's still sound, right?
In general, I don't think you can conclude faster that a type synonym supports equality anyway. It seems that (==) is only a way to catch errors earlier.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An explicit (==) is also a way to advertise equality support to importers of the enclosing module. Without an explicit (==), an export provides of the type would not let importers use the type as supporting equality.

if (userDefinedType.ResolvedClass is TypeSynonymDeclBase typeSynonymDecl) {
if (typeSynonymDecl.IsRevealedInScope(Type.GetScope())) {
DetermineEqualitySupportType(typeSynonymDecl.RhsWithArgument(userDefinedType.TypeArgs), ref thingsChanged);
}
} else if (userDefinedType.ResolvedClass is NewtypeDecl newtypeDecl) {
if (newtypeDecl.IsRevealedInScope(Type.GetScope())) {
DetermineEqualitySupportType(newtypeDecl.RhsWithArgument(userDefinedType.TypeArgs), ref thingsChanged);
}
} else if (userDefinedType.ResolvedClass is IndDatatypeDecl otherDt) {
if (otherDt.EqualitySupport == IndDatatypeDecl.ES.ConsultTypeArguments) {
// datatype is in a different SCC
var otherUdt = (UserDefinedType)type.NormalizeExpand();
var i = 0;
foreach (var otherTp in otherDt.TypeArgs) {
if (otherTp.NecessaryForEqualitySupportOfSurroundingInductiveDatatype) {
var tp = otherUdt.TypeArgs[i].AsTypeParameter;
if (tp != null) {
tp.NecessaryForEqualitySupportOfSurroundingInductiveDatatype = true;
thingsChanged = true;
}
}
}

i++;
i++;
}
}
}
} else if (type.AsMapType is { } typeMap) {
// A map type's Domain type is required to support equality (just like the argument type for sets and multisets), but
// it is optional for the map type's Range type to support equality. Thus, we need to determine the equality support for just the Range type.
DetermineEqualitySupportType(typeMap.Range, ref thingsChanged);
} else if (type.AsSeqType is { } typeSeq) {
// Like the Range type of a map, it is optional for a sequence type's argument type to support equality. So, we make a call
// to determine it.
DetermineEqualitySupportType(typeSeq.Arg, ref thingsChanged);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,57 +1,240 @@
// RUN: %exits-with 2 %baredafny verify %args "%s" > "%t"
// RUN: %diff "%s.expect" "%t"
// RUN: %testDafnyForEachResolver --expect-exit-code=2 "%s"

datatype M<T, U> = M(m: map<T, U>)
module Issue5972 {
datatype M<T, U> = M(m: map<T, U>)

method CompareAnything<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := M(map[0 := f]);
var m2 := M(map[0 := g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
method CompareAnything<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := M(map[0 := f]);
var m2 := M(map[0 := g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
}
if m1 == m2 {
assert m1.m[0] == m2.m[0];
}
}
if m1 == m2 {
assert m1.m[0] == m2.m[0];
return m1 == m2; // error: the types of m1 and m2 do not support equality
}

datatype S<T> = S(s: seq<T>)

method CompareAnythingS<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := S([f]);
var m2 := S([g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
}
if m1 == m2 {
assert m1.s[0] == m2.s[0];
}
}
return m1 == m2; // error: the types of m1 and m2 do not support equality
}


codatatype Stream = Stream(head: int, tail: Stream)
{
static function From(i: int): Stream {
Stream(i, From(i + 1))
}
}

method Main() {
var s1 := Stream.From(0);
var s2 := Stream.From(0);
var b := CompareAnything(s1, s2);
var b2 := CompareAnythingS(s1, s2);
if !b || !b2 {
assert false;
print 1/0;
}
}
return m1 == m2;
}

datatype S<T> = S(s: seq<T>)
// This module is like Issue5972 above, but uses type synonyms around the map/seq types
module TypeSynonyms {
type MyMap<T, U> = map<T, U>
type MySeq<T> = seq<T>

datatype M<T, U> = M(m: MyMap<T, U>)

method CompareAnythingS<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := S([f]);
var m2 := S([g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
method CompareAnything<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := M(map[0 := f]);
var m2 := M(map[0 := g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
}
if m1 == m2 {
assert m1.m[0] == m2.m[0];
}
}
if m1 == m2 {
assert m1.s[0] == m2.s[0];
return m1 == m2; // error: the types of m1 and m2 do not support equality
}

datatype S<T> = S(s: MySeq<T>)

method CompareAnythingS<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := S([f]);
var m2 := S([g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
}
if m1 == m2 {
assert m1.s[0] == m2.s[0];
}
}
return m1 == m2; // error: the types of m1 and m2 do not support equality
}

codatatype Stream = Stream(head: int, tail: Stream)
{
static function From(i: int): Stream {
Stream(i, From(i + 1))
}
}

method Test() {
var s1 := Stream.From(0);
var s2 := Stream.From(0);
var b := CompareAnything(s1, s2);
var b2 := CompareAnythingS(s1, s2);
if !b || !b2 {
assert false;
print 1/0;
}
}
return m1 == m2;
}

// This module is like TypeSynonyms above, but the two type synonyms are declared to definitely support equality.
// The fact that their RHSs don't gives rise to errors, but in the process of fixing issue 5972, this program
// had caused a crash, so it seems good to include as a test case.
module TypeSynonymsWithGuaranteedEqualitySupport {
type MyMap(==)<T, U> = map<T, U> // error: RHS does not support equality as promised
type MySeq(==)<T> = seq<T> // error: RHS does not support equality as promised

datatype M<T, U> = M(m: MyMap<T, U>)

method CompareAnything<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := M(map[0 := f]);
var m2 := M(map[0 := g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
}
if m1 == m2 {
assert m1.m[0] == m2.m[0];
}
}
return m1 == m2;
}

datatype S<T> = S(s: MySeq<T>)

method CompareAnythingS<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := S([f]);
var m2 := S([g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
}
if m1 == m2 {
assert m1.s[0] == m2.s[0];
}
}
return m1 == m2;
}

codatatype Stream = Stream(head: int, tail: Stream)
{
static function From(i: int): Stream {
Stream(i, From(i + 1))
}
}

codatatype Stream = Stream(head: int, tail: Stream)
{
static function From(i: int): Stream {
Stream(i, From(i + 1))
method Test() {
var s1 := Stream.From(0);
var s2 := Stream.From(0);
var b := CompareAnything(s1, s2);
var b2 := CompareAnythingS(s1, s2);
if !b || !b2 {
assert false;
print 1/0;
}
}
}

method Main() {
var s1 := Stream.From(0);
var s2 := Stream.From(0);
var b := CompareAnything(s1, s2);
var b2 := CompareAnythingS(s1, s2);
if !b || !b2 {
assert false;
print 1/0;
// This module is like TypeSynonyms above, but uses subset types around the map/seq types
module SubsetTypes {
type MyMap<T, U> = m: map<T, U> | true
type MySeq<T> = s: seq<T> | true

datatype M<T, U> = M(m: MyMap<T, U>)

method CompareAnything<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := M(map[0 := f]);
var m2 := M(map[0 := g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
}
if m1 == m2 {
assert m1.m[0] == m2.m[0];
}
}
return m1 == m2; // error: the types of m1 and m2 do not support equality
}
}

datatype S<T> = S(s: MySeq<T>)

method CompareAnythingS<A>(f: A, g: A) returns (b: bool)
ensures b <==> f == g
{
var m1 := S([f]);
var m2 := S([g]);
assert m1 == m2 <==> f == g by {
if f == g {
assert m1 == m2;
}
if m1 == m2 {
assert m1.s[0] == m2.s[0];
}
}
return m1 == m2; // error: the types of m1 and m2 do not support equality
}

codatatype Stream = Stream(head: int, tail: Stream)
{
static function From(i: int): Stream {
Stream(i, From(i + 1))
}
}

method Test() {
var s1 := Stream.From(0);
var s2 := Stream.From(0);
var b := CompareAnything(s1, s2);
var b2 := CompareAnythingS(s1, s2);
if !b || !b2 {
assert false;
print 1/0;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
git-issue-5972.dfy(19,9): Error: == can only be applied to expressions of types that support equality (got M<int, A>)
git-issue-5972.dfy(37,9): Error: == can only be applied to expressions of types that support equality (got S<A>)
2 resolution/type errors detected in git-issue-5972.dfy
git-issue-5972.dfy(19,11): Error: == can only be applied to expressions of types that support equality (got M<int, A>)
git-issue-5972.dfy(37,11): Error: == can only be applied to expressions of types that support equality (got S<A>)
git-issue-5972.dfy(80,11): Error: == can only be applied to expressions of types that support equality (got M<int, A>)
git-issue-5972.dfy(98,11): Error: == can only be applied to expressions of types that support equality (got S<A>)
git-issue-5972.dfy(124,7): Error: type 'MyMap' declared as supporting equality, but the RHS type (map<T, U>) might not
git-issue-5972.dfy(125,7): Error: type 'MySeq' declared as supporting equality, but the RHS type (seq<T>) might not
git-issue-5972.dfy(202,11): Error: == can only be applied to expressions of types that support equality (got M<int, A>)
git-issue-5972.dfy(220,11): Error: == can only be applied to expressions of types that support equality (got S<A>)
8 resolution/type errors detected in git-issue-5972.dfy
Loading
Loading