Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions build-aprove.xml
Original file line number Diff line number Diff line change
Expand Up @@ -760,4 +760,21 @@
</classpath>
</java>
</target>



<target name="test-all-files">
<apply executable="java" failonerror="true">
<arg value="-ea" />
<arg value="-jar" />
<arg path="dist/lib/aprove.jar" />
<arg value="-m" />
<arg value="-wst" />
<arg value="-p" />
<arg value="plain" />

<fileset dir="examples/Haskell/full_haskell" includes="**/*.hs" />

</apply>
</target>
</project>
3 changes: 3 additions & 0 deletions src/aprove/cli/Main.java

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Where are these imports used?

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package aprove.cli;

import aprove.input.Programs.haskell.StrictPositivityException;
import gnu.getopt.*;

import java.io.*;
Expand All @@ -23,6 +24,8 @@
import aprove.verification.oldframework.Logic.*;
import aprove.verification.oldframework.Utility.GenericStructures.*;

import static aprove.verification.complexity.TruthValue.ComplexityYNM.MAYBE;

public class Main {

private final static String HTML_FOOTER = "</BODY></HTML>";
Expand Down
275 changes: 275 additions & 0 deletions src/aprove/input/Programs/haskell/GraphBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
package aprove.input.Programs.haskell;

import aprove.verification.oldframework.Haskell.BasicTerms.Apply;
import aprove.verification.oldframework.Haskell.BasicTerms.Cons;
import aprove.verification.oldframework.Haskell.BasicTerms.Var;
import aprove.verification.oldframework.Haskell.Declarations.DataDecl;
import aprove.verification.oldframework.Haskell.Declarations.SynTypeDecl;
import aprove.verification.oldframework.Haskell.HaskellObject;
import aprove.verification.oldframework.Haskell.Modules.TyConsEntity;
import aprove.verification.oldframework.Haskell.Typing.DataCon;

import java.util.*;
import java.util.stream.IntStream;

public class GraphBuilder {
private final OccurrenceGraph graph;

private String currentDef;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems like this should be a local variable?


private final Set<String> datatypeNames;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need this set or is it sufficient to keep the graph?


private Map<String, Integer> paramIndex;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be local?


private Map<String, TyConsEntity> preludeTyCons;

// should be read directly from Prelude
private Set<String> preludeNames = new LinkedHashSet<>(Set.of(
"IO", "Either", "IOResult", "FilePath", "Integer", "ReadS", "Obj", "ShowS",
"Int", "Rational", "WHNF", "Maybe", "IOError", "Nat", "Float", "IOFinished", "HugsException",
"IOErrorKind", "Char", "Ordering", "Double", "String", "AET", "Ratio"
));

public GraphBuilder() {
this.datatypeNames = new HashSet<>();
this.graph = new OccurrenceGraph();
}

public OccurrenceGraph buildFromDataDecl(List<DataDecl> dataDecls, List<SynTypeDecl> synTypeDecls, Map<String, TyConsEntity> preludeTyConsMap) {
this.preludeTyCons = preludeTyConsMap;
dataDecls.forEach(d -> datatypeNames.add(d.getDefType().getToken().getText()));
synTypeDecls.forEach(d -> datatypeNames.add(d.getDefType().getToken().getText()));
synTypeDecls.forEach(this::processSynTypes);
for (DataDecl d : dataDecls) {
processDataDecl(d, d.getDefType().getToken().getText());
}
return graph;
}

public OccurrenceGraph buildFromDataDecl(List<DataDecl> dataDecls, List<SynTypeDecl> synTypeDecls) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why not call the overloaded method here with preludeTyConsMap = this.preludeTyCons?

dataDecls.forEach(d -> datatypeNames.add(d.getDefType().getToken().getText()));
synTypeDecls.forEach(d -> datatypeNames.add(d.getDefType().getToken().getText()));
synTypeDecls.forEach(this::processSynTypes);
for (DataDecl d : dataDecls) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why .foreach above and explicit for each loop here?

processDataDecl(d, d.getDefType().getToken().getText());
}
return graph;
}

public OccurrenceGraph buildFromTyConsEntity(List<TyConsEntity> types) {
for (TyConsEntity entity : types) {
datatypeNames.add(entity.getName());
}

for (TyConsEntity entity : types) {
processDataDecl((DataDecl) entity.getValue(), entity.getName());
}

return graph;
}

private void processSynTypes(SynTypeDecl synTypeDecl) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't we just store the synonym types somewhere? And then when building the graph we replace them with their actual definition?

currentDef = synTypeDecl.getDefType().getToken().getText();
paramIndex = new HashMap<>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You reset global variables here. Is this intended?


if (synTypeDecl.getDefType() instanceof Apply apply) {
//collect vars from data declaration
List<HaskellObject> vars = new ArrayList<>();
var _ = flattenApp(apply, vars);

//add vars to paramIndex with innermost index 0
IntStream.range(0, vars.size())
.forEach(index -> paramIndex.put(((Var) vars.get(index)).getSymbol().toString(), index));
}

graph.addNode(
new OccurrenceGraph.DefNode(currentDef)
);

walkType(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why? Type synonym add nothing new in terms of recursive data types?

synTypeDecl.getType(),
Occurrence.STRICT_POS,
new OccurrenceGraph.DefNode(currentDef)
);

}

private void processDataDecl(DataDecl dd, String name) {
//reset
currentDef = name;
paramIndex = new LinkedHashMap<>();

if (dd.getDefType() instanceof Apply apply) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

implementation largely redundant with previous function


//collect vars from data declaration
List<HaskellObject> vars = new ArrayList<>();
var _ = flattenApp(apply, vars);

//add vars to paramIndex with innermost index 0
IntStream.range(0, vars.size())
.forEach(index -> paramIndex.put(((Var) vars.get(index)).getSymbol().toString(), index));
}
//create node
graph.addNode(
new OccurrenceGraph.DefNode(currentDef)
);

for (DataCon ctor : dd.getDataCons()) {
processConstructor(ctor);
}

}

/**
* Collects arguments from a data declaration and returns the head of the function
*
* @param app some Apply (e.g. Foo a b c)
* @param args array storing the arguments (e.g. Foo a b c => args = [a, b, c]
* @return head (Foo a b c => Foo)
*/
private HaskellObject flattenApp(Apply app, List<HaskellObject> args) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This function should return a tuple of head + args instead of adding the args as a side-effect.

List<HaskellObject> reversed = new ArrayList<>();
HaskellObject current = app;

while (current instanceof Apply apply) {
reversed.add(apply.getArgument());
current = apply.getFunction();
}

for (int i = reversed.size() - 1; i >= 0; i--) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Isn't it simpler to swap first & last elements in-place with two pointers moving from start and back (similar to Quicksort loop)?

args.add(reversed.get(i));
}

return current;
}

private void processConstructor(DataCon ctor) {
// e.g. data List a = Nil | Cons a (List a)
// => ctor = `Cons a (List a)`
// => ctor.getTypes() -> ['a', '(List a)']
for (var type : ctor.getTypes()) {
walkType(
type,
Occurrence.STRICT_POS,
new OccurrenceGraph.DefNode(currentDef)
);
}
}

private void walkType(HaskellObject type, Occurrence pol, OccurrenceGraph.Node target) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Explain what the parameters do here.

if (type instanceof Cons cons) {
walkCons(cons, pol, target);
} else if (type instanceof Var var) {
walkVar(var, pol, target);
} else if (type instanceof Apply apply) {
walkApply(apply, pol, target);
}
}

// e.g. Foo = mkFoo Bar => cons -> `Bar`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't the constructor be MkFoo (with upper case M) instead of Bar? Or does this walk the constructor's fields, then the method name should be changed.

private void walkCons(Cons cons, Occurrence pol, OccurrenceGraph.Node target) {
String name = cons.getSymbol().toString();
if (datatypeNames.contains(name)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why do we need pre-computed datatypeNames? Can't we build the graph from here and add a new edge if it doesn't already exist?

graph.addEdge(
new OccurrenceGraph.DefNode(name),
target,
pol
);
} else if (preludeTyCons != null && preludeTyCons.containsKey(name)) {
TyConsEntity entity = preludeTyCons.get(name);
if (entity.getValue() instanceof SynTypeDecl synTypeDecl) {
walkType(synTypeDecl.getType(), pol, target);
} else if (entity.getValue() instanceof DataDecl dataDecl) {
processDataDecl(dataDecl, name);
}
}
}

private void walkVar(Var var, Occurrence pol, OccurrenceGraph.Node target) {
String name = var.getSymbol().toString();
if (paramIndex.containsKey(name)) {
int index = paramIndex.get(name);
graph.addEdge(
new OccurrenceGraph.ArgNode(currentDef, index),
target,
pol
);
}
}


private void walkApply(Apply apply, Occurrence pol, OccurrenceGraph.Node target) {
List<HaskellObject> args = new ArrayList<>();
var head = flattenApp(apply, args);

if (head instanceof Cons cons) {

String name = cons.getSymbol().toString();

//check if arrow function
if (name.equals("->")) {
// System.out.println("WalkArrow called");
walkArrow(apply, pol, target);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Isn't this the same as

                walkType(args[0], flip(pol), target)
                walkType(args[1], pol, target)

return;
}


if (datatypeNames.contains(name)) {
graph.addEdge(
new OccurrenceGraph.DefNode(name),
target,
pol
);
}

for (int i = 0; i < args.size(); i++) {
Occurrence argPol = pol.otimes(argPolarity(name));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What does argPolarity do?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I assume this is for handling application of type variables of higher kinds. Then, type variables should be syntactically discernable from type (constructors) as they start with lowercase letters. Is there such a distinction already present in AProVE (I suspect so)? Otherwise we should do this.

var argTarget = argTarget(name, i, target);
walkType(args.get(i), argPol, argTarget);
}
} else if (head instanceof Var var) {
String name = var.getSymbol().toString();
walkType(head, pol, target);
if (paramIndex.containsKey(name)) {
int index = paramIndex.get(name);
target = new OccurrenceGraph.ArgNode(currentDef, index);
}
for (var a : args) {
walkType(a, pol.otimes(Occurrence.MIXED), target);
}
} else {
walkType(head, pol, target);
for (var a : args) {
walkType(a, pol.otimes(Occurrence.MIXED), target);
}
}
}

private Occurrence argPolarity(String name) {
if (datatypeNames.contains(name) || preludeNames.contains(name)) {
// return Occurrence.GUARD_POS;
return Occurrence.STRICT_POS;
}
return Occurrence.MIXED;
}

private OccurrenceGraph.Node argTarget(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

See comment on argPolarity.

String name, int argIndex, OccurrenceGraph.Node currentTarget
) {
if (datatypeNames.contains(name) || preludeNames.contains(name)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this bolean expression be negated?

return new OccurrenceGraph.ArgNode(name, argIndex);
}
return currentTarget;
}

private void walkArrow(Apply apply, Occurrence pol, OccurrenceGraph.Node target) {
var codomain = apply.getArgument();
if (apply.getFunction() instanceof Apply apply2) {
var domain = apply2.getArgument();
walkType(domain, pol.otimes(Occurrence.JUST_NEG), target);
walkType(codomain, pol, target);
} else {
//should not happen
}
}
}
7 changes: 7 additions & 0 deletions src/aprove/input/Programs/haskell/HaskellParserCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package aprove.input.Programs.haskell;

public class HaskellParserCheck extends RuntimeException {
public HaskellParserCheck(String message) {
super(message);
}
}
72 changes: 72 additions & 0 deletions src/aprove/input/Programs/haskell/Occurrence.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package aprove.input.Programs.haskell;

/**
* Occurrence is a
* <a href="https://en.wikipedia.org/wiki/Complete_lattice">complete lattice</a>
* with least element 'MIXED' and greatest element 'UNUSED'.
*/
public enum Occurrence {
MIXED, // + and -
JUST_NEG,
JUST_POS,
STRICT_POS,
UNUSED; // does not occur

/**
* Simulates behavior of additive of the Occurrence semiring (a+b):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

additive behavior (state what it is used for)

* 'UNUSED' is neutral (zero) and 'MIXED' is dominant
*
* @param other the other Occurrence (b) added to 'this' (a)
* @return 'this + other'
*/
public Occurrence oplus(Occurrence other) {

if (this == MIXED || other == MIXED) return MIXED; // dominant

if (this == UNUSED) return other;
if (other == UNUSED) return this;

if (this == JUST_NEG && other == JUST_NEG) return JUST_NEG;
if (this == JUST_NEG || other == JUST_NEG) return MIXED;

if (this == STRICT_POS) return other;
if (other == STRICT_POS) return this;

return JUST_POS; // both JUST_POS
}

/**
* Simulates behavior of multiplicative of the Occurrence semiring (a*b):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

multiplicative behavior (state what it is used for)

* 'STRICT_POS' is neutral (one) and 'UNUSED' dominant
*
* @param other the other Occurrence (b) 'this' (a) is to be multiplied by
* @return 'this * other'
*/
public Occurrence otimes(Occurrence other) {
if (this == UNUSED || other == UNUSED) return UNUSED;
if (this == MIXED || other == MIXED) return MIXED;
if (this == JUST_NEG && other == JUST_NEG) return JUST_POS;
if (this == JUST_NEG || other == JUST_NEG) return JUST_NEG;
if (this == JUST_POS || other == JUST_POS) return JUST_POS;
return STRICT_POS;
}

/**
* @return true => mixed, just negative or just positive,<p> false => strict positive, guarded positive or unused
*/
public boolean isNotStrictlyPositive() {
return this == MIXED || this == JUST_NEG || this == JUST_POS;
}


public String toPrettyString() {
return switch (this) {
case MIXED -> "±";
case JUST_NEG -> "-";
case JUST_POS -> "+";
case STRICT_POS -> "++";
// case GUARD_POS -> "g+";
case UNUSED -> "0";
};
}
}
Loading