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: Make datatype cycle detection independent of auto-init #4997

Merged
merged 24 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5d45706
chore: Add documentation and sanity assertions
RustanLeino Jan 18, 2024
c868719
Compute IsObviouslyEmpty for inductive datatypes
RustanLeino Jan 18, 2024
d205272
Fix typo in comment
RustanLeino Jan 19, 2024
4036d59
Don’t use cache (that conflates all type-parrameter instantiations)
RustanLeino Jan 19, 2024
013546b
Also check cycles going through result type of total arrows
RustanLeino Jan 19, 2024
6fea52f
Add tests, and adjust for ordering for checks
RustanLeino Jan 19, 2024
a1f78c5
Simplify tests
RustanLeino Jan 19, 2024
bc9354b
Rename test file
RustanLeino Jan 19, 2024
98a3240
Add verification tests to confirm not auto-init
RustanLeino Jan 19, 2024
d6343ed
Set grounding-ctor-type-parameters and test compilation
RustanLeino Jan 19, 2024
8079558
Fix typo in comment
RustanLeino Jan 19, 2024
2fe447a
Merge branch 'master' into issue-4939
RustanLeino Jan 19, 2024
77f6046
Add release notes
RustanLeino Jan 19, 2024
2018422
Merge branch 'master' into issue-4939
RustanLeino Jan 19, 2024
5d1aec4
Merge branch 'master' into issue-4939
RustanLeino Jan 19, 2024
aec8edc
Fix whitespace
RustanLeino Jan 19, 2024
e660898
Don’t bother with cyclicity test if datatype had refinement error dur…
RustanLeino Jan 19, 2024
0cbc0dd
Change no-instance-because-of-datatype-cycle error into warning
RustanLeino Jan 22, 2024
a1275b1
Add a test that shows an empty datatype is provably empty
RustanLeino Jan 22, 2024
fc44f01
chore: Improve C#
RustanLeino Jan 23, 2024
2a660b5
fix: Box function-body results and let-RHS from datatype to trait
RustanLeino Jan 23, 2024
53ee5a4
Merge branch 'master' into issue-4939
RustanLeino Jan 23, 2024
84a81d5
Merge branch 'master' into issue-4939
ssomayyajula Jan 23, 2024
aad6805
Merge branch 'master' into issue-4939
RustanLeino Jan 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Compute IsObviouslyEmpty for inductive datatypes
Fixes #4939
  • Loading branch information
RustanLeino committed Jan 19, 2024
commit c86871951831c90c3557aabc3c513996c5ac8115
33 changes: 33 additions & 0 deletions Source/DafnyCore/AST/TypeDeclarations/NewtypeDecl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,39 @@ public class NewtypeDecl : TopLevelDeclWithMembers, RevealableTypeDecl, Redirect
this.NewSelfSynonym();
}

/// <summary>
/// Return .BaseType instantiated with "typeArgs", but only look at the part of .BaseType that is in scope.
/// </summary>
public Type RhsWithArgument(List<Type> typeArgs) {
Contract.Requires(typeArgs != null);
Contract.Requires(typeArgs.Count == TypeArgs.Count);
var scope = Type.GetScope();
var rtd = BaseType.AsRevealableType;
if (rtd != null) {
Contract.Assume(rtd.AsTopLevelDecl.IsVisibleInScope(scope));
if (!rtd.IsRevealedInScope(scope)) {
// type is actually hidden in this scope
return rtd.SelfSynonym(typeArgs);
}
}
return RhsWithArgumentIgnoringScope(typeArgs);
}
/// <summary>
/// Returns the declared .BaseType but with formal type arguments replaced by the given actuals.
/// </summary>
public Type RhsWithArgumentIgnoringScope(List<Type> typeArgs) {
Contract.Requires(typeArgs != null);
Contract.Requires(typeArgs.Count == TypeArgs.Count);
// Instantiate with the actual type arguments
if (typeArgs.Count == 0) {
// this optimization seems worthwhile
return BaseType;
} else {
var subst = TypeParameter.SubstitutionMap(TypeArgs, typeArgs);
return BaseType.Subst(subst);
}
}

public TopLevelDecl AsTopLevelDecl => this;
public TypeDeclSynonymInfo SynonymInfo { get; set; }

Expand Down
54 changes: 48 additions & 6 deletions Source/DafnyCore/Resolver/ModuleResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,13 @@ public partial class ModuleResolver {
}

// Perform the stratosphere check on inductive datatypes, and compute to what extent the inductive datatypes require equality support
var inductiveDatatypesCheckedForEmptiness = new Dictionary<IndDatatypeDecl, bool>();
foreach (var dtd in declarations.ConvertAll(decl => decl as IndDatatypeDecl).Where(dtd => dtd != null)) {
if (IsObviouslyEmpty(dtd, inductiveDatatypesCheckedForEmptiness)) {
reporter.Error(MessageSource.Resolver, dtd,
$"because of cyclic dependencies among constructor argument types, no instances of datatype '{dtd.Name}' can be constructed");
}
}
foreach (var dtd in datatypeDependencies.TopologicallySortedComponents()) {
if (datatypeDependencies.GetSCCRepresentative(dtd) == dtd) {
// do the following check once per SCC, so call it on each SCC representative
Expand Down Expand Up @@ -2846,6 +2853,47 @@ public ReportOtherAdditionalInformation_Visitor(ModuleResolver resolver)
}
}

/// <summary>
/// Determine if "datatypeDecl" is involved in a cycle in such a way that it is obvious that the type has no instances.
/// Return the answer.
///
/// "informationSoFar" is used as a cache of computed information. The method updates the cache for "datatypeDecl".
/// </summary>
bool IsObviouslyEmpty(IndDatatypeDecl datatypeDecl, IDictionary<IndDatatypeDecl, bool> informationSoFar) {
var datatype = UserDefinedType.FromTopLevelDecl(datatypeDecl.tok, datatypeDecl);
var isEmpty = AreThereAnyObviousSignsOfEmptiness(datatype, informationSoFar, new HashSet<IndDatatypeDecl>());
informationSoFar.Add(datatypeDecl, isEmpty);
return isEmpty;
}

private bool AreThereAnyObviousSignsOfEmptiness(Type type, IDictionary<IndDatatypeDecl, bool> informationSoFar, ISet<IndDatatypeDecl> beingVisited) {
type = type.NormalizeExpand(); // cut through type proxies, type synonyms, but being mindful of what's in scope
if (type is UserDefinedType { ResolvedClass: var cl} udt) {
Contract.Assert(cl != null);
if (cl is NewtypeDecl newtypeDecl) {
return AreThereAnyObviousSignsOfEmptiness(newtypeDecl.RhsWithArgument(udt.TypeArgs), informationSoFar, beingVisited);
}
if (cl is IndDatatypeDecl datatypeDecl) {
if (informationSoFar.TryGetValue(datatypeDecl, out var isObviouslyEmpty)) {
return isObviouslyEmpty;
}
if (beingVisited.Contains(datatypeDecl)) {
// This datatype may be empty, but it's definitely empty if we consider only the constructors that have been visited
// since AreThereAnyObviousSignsOfEmptiness was called from IsObviouslyEmpty.
return true;
}
beingVisited.Add(datatypeDecl);
var typeMap = TypeParameter.SubstitutionMap(datatypeDecl.TypeArgs, udt.TypeArgs);
var isEmpty = datatypeDecl.Ctors.TrueForAll(ctor =>
ctor.Formals.Exists(formal => AreThereAnyObviousSignsOfEmptiness(formal.Type.Subst(typeMap), informationSoFar, beingVisited)));
beingVisited.Remove(datatypeDecl);
return isEmpty;
}
}

return false;
}

/// <summary>
/// Check that the SCC of 'startingPoint' can be carved up into stratospheres in such a way that each
/// datatype has some value that can be constructed from datatypes in lower stratospheres only.
Expand Down Expand Up @@ -2886,12 +2934,6 @@ public ReportOtherAdditionalInformation_Visitor(ModuleResolver resolver)
} else if (clearedThisRound != 0) {
// some progress was made, so let's keep going
} else {
// whatever is in scc-cleared now failed to pass the test
foreach (var dt in scc) {
if (dt.GroundingCtor == null) {
reporter.Error(MessageSource.Resolver, dt, "because of cyclic dependencies among constructor argument types, no instances of datatype '{0}' can be constructed", dt.Name);
}
}
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// RUN: %exits-with 2 %verify --type-system-refresh --general-traits=datatype "%s" > "%t"
// RUN: %diff "%s.expect" "%t"

module FromIssue0 {
trait Program {
method Compute() returns (r: Result)
}

datatype Result =
| Bounce(next: Program)
| Done()

datatype Trivial extends Program =
Trivial()
{
method Compute() returns (r: Result) {
return Done();
}
}

datatype Seq extends Program =
Seq(left: Program, right: Program) // this once gave an error, saying Seq is empty because of cyclic dependencies
{
method Compute() returns (r: Result) {
var result := left.Compute();
match result
case Done() => r := right.Compute();
case Bounce(next) =>
var next' := Seq(next, right);
assert next' is Program;
return Bounce(next');
}
}
}

module FromIssue1 {
trait Program {
}

datatype Trivial extends Program =
Trivial()

datatype Seq extends Program =
Seq(left: Program, right: Program) // this once gave an error, saying Seq is empty because of cyclic dependencies
}

module FromIssue2 {
// NoInstances has no instances
class NoInstances {
constructor ()
requires false
{
}
}

// ... and therefore, neither does D. However, this is not detected syntactically, so there are no complaints.
datatype D = D(n: NoInstances)
}

module FromIssue3 {
trait Interface {
function getString(i: int): string
}

datatype Wrapper extends Interface = Wrapper(inner: Interface) // this once gave the cyclic-dependency error
{
function getString(i: int): string {
inner.getString(i+1)
}
}

datatype DetectZero extends Interface = DetectZero
{
function getString(i: int): string {
if i == 0
then "zero"
else "I don't know"
}
}

function getWrappedDetectZero(): Interface {
Wrapper(DetectZero)
}
}

module FromIssue4 {
trait Interface {
function GetString(i: int): string
}

class {:extern} Store {
var interface: Interface

constructor (interface: Interface) {
this.interface := interface;
}

function GetInterface(): (res: Interface)
ensures res == interface
}

datatype Wrapper extends Interface = Wrapper(inner: Store)
{
function GetString(i: int): string {
inner.GetInterface().GetString(i + 1)
}
}
}

module SimpleExample0 {
trait GeneralTrait {
function F(): int { 5 }
}

datatype Wrapper = Wrapper(g: GeneralTrait) // this once gave the cyclic-dependency error
}

module SimpleExample1 {
trait GeneralTrait extends object {
function F(): int { 5 }
}

datatype Wrapper = Wrapper(g: GeneralTrait) // this was always fine
}

module Standard {
datatype List<X> = Nil | Cons(X, List<X>)

datatype Op = Plus | Times
datatype Expr = Value(int) | Node(operator: Op, arguments: List<Expr>)
}

module Good0 {
datatype Mutual = M0(Nutual) | M1(Mutual)
datatype Nutual = N0(Mutual) | Easy
}

module Good1 {
datatype Nutual = N0(Mutual) | Easy
datatype Mutual = M0(Nutual) | M1(Mutual)
}

module Bad {
datatype T = Make(T, T) // error: obviously empty

datatype Mutual = M0(Nutual) | M1(Mutual) // error: empty
datatype Nutual = N0(Mutual) // error: empty
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
git-issue-4939.dfy(144,11): Error: because of cyclic dependencies among constructor argument types, no instances of datatype 'T' can be constructed
git-issue-4939.dfy(146,11): Error: because of cyclic dependencies among constructor argument types, no instances of datatype 'Mutual' can be constructed
git-issue-4939.dfy(147,11): Error: because of cyclic dependencies among constructor argument types, no instances of datatype 'Nutual' can be constructed
3 resolution/type errors detected in git-issue-4939.dfy