package org.scribble.model.global;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.scribble.main.Job;
import org.scribble.main.ScribbleException;
import org.scribble.model.MPrettyPrint;
import org.scribble.model.endpoint.EFSM;
import org.scribble.model.endpoint.EGraph;
import org.scribble.model.endpoint.EStateKind;
import org.scribble.model.endpoint.actions.EAction;
import org.scribble.model.global.actions.SAction;
import org.scribble.sesstype.name.GProtocolName;
import org.scribble.sesstype.name.Role;
public class SGraph implements MPrettyPrint
{
public final GProtocolName proto;
//private final Map<Role, EGraph> efsms;
//private final boolean fair;
public final SState init;
public Map<Integer, SState> states; // State ID -> GMState
private Map<Integer, Set<Integer>> reach; // State ID -> reachable states (not reflexive)
private Set<Set<Integer>> termSets;
protected SGraph(GProtocolName proto, Map<Integer, SState> states, SState init)
{
this.proto = proto;
this.init = init;
this.states = Collections.unmodifiableMap(states);
this.reach = getReachabilityMap();
}
public SModel toModel()
{
return new SModel(this);
}
public Set<Set<Integer>> getTerminalSets()
{
if (this.termSets != null)
{
return this.termSets;
}
Set<Set<Integer>> termSets = new HashSet<>();
Set<Set<Integer>> checked = new HashSet<>();
for (Integer i : reach.keySet())
{
SState s = this.states.get(i);
Set<Integer> rs = this.reach.get(s.id);
if (!checked.contains(rs) && rs.contains(s.id))
{
checked.add(rs);
if (isTerminalSetMember(s))
{
termSets.add(rs);
}
}
}
this.termSets = Collections.unmodifiableSet(termSets);
return this.termSets;
}
private boolean isTerminalSetMember(SState s)
{
Set<Integer> rs = this.reach.get(s.id);
Set<Integer> tmp = new HashSet<>(rs);
tmp.remove(s.id);
for (Integer r : tmp)
{
if (!this.reach.containsKey(r) || !this.reach.get(r).equals(rs))
{
return false;
}
}
return true;
}
// Pre: reach.get(start).contains(end) // FIXME: will return null if initial
// state is error
public List<SAction> getTrace(SState start, SState end)
{
SortedMap<Integer, Set<Integer>> candidates = new TreeMap<>();
Set<Integer> dis0 = new HashSet<Integer>();
dis0.add(start.id);
candidates.put(0, dis0);
Set<Integer> seen = new HashSet<>();
seen.add(start.id);
return getTraceAux(new LinkedList<>(), seen, candidates, end);
}
// Djikstra's
private List<SAction> getTraceAux(List<SAction> trace, Set<Integer> seen,
SortedMap<Integer, Set<Integer>> candidates, SState end)
{
Integer dis = candidates.keySet().iterator().next();
Set<Integer> cs = candidates.get(dis);
Iterator<Integer> it = cs.iterator();
Integer currid = it.next();
it.remove();
if (cs.isEmpty())
{
candidates.remove(dis);
}
SState curr = this.states.get(currid);
Iterator<SAction> as = curr.getAllActions().iterator();
Iterator<SState> ss = curr.getAllSuccessors().iterator();
while (as.hasNext())
{
SAction a = as.next();
SState s = ss.next();
if (s.id == end.id)
{
trace.add(a);
return trace;
}
if (!seen.contains(s.id) && this.reach.containsKey(s.id)
&& this.reach.get(s.id).contains(end.id))
{
seen.add(s.id);
Set<Integer> tmp1 = candidates.get(dis + 1);
if (tmp1 == null)
{
tmp1 = new HashSet<>();
candidates.put(dis + 1, tmp1);
}
tmp1.add(s.id);
List<SAction> tmp2 = new LinkedList<>(trace);
tmp2.add(a);
List<SAction> res = getTraceAux(tmp2, seen, candidates, end);
if (res != null)
{
return res;
}
}
}
return null;
}
// Not reflexive
public Map<Integer, Set<Integer>> getReachabilityMap()
{
if (this.reach != null)
{
return this.reach;
}
Map<Integer, Integer> idToIndex = new HashMap<>(); // state ID -> array
// index
Map<Integer, Integer> indexToId = new HashMap<>(); // array index -> state
// ID
int i = 0;
for (SState s : this.states.values())
{
idToIndex.put(s.id, i);
indexToId.put(i, s.id);
i++;
}
this.reach = getReachabilityAux(idToIndex, indexToId);
return this.reach;
}
private Map<Integer, Set<Integer>> getReachabilityAux(
Map<Integer, Integer> idToIndex, Map<Integer, Integer> indexToId)
{
int size = idToIndex.keySet().size();
boolean[][] reach = new boolean[size][size];
for (Integer s1id : idToIndex.keySet())
{
for (SState s2 : this.states.get(s1id).getAllSuccessors())
{
reach[idToIndex.get(s1id)][idToIndex.get(s2.id)] = true;
}
}
for (boolean again = true; again;)
{
again = false;
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
if (reach[i][j])
{
for (int k = 0; k < size; k++)
{
if (reach[j][k] && !reach[i][k])
{
reach[i][k] = true;
again = true;
}
}
}
}
}
}
Map<Integer, Set<Integer>> res = new HashMap<>();
for (int i = 0; i < size; i++)
{
Set<Integer> tmp = res.get(indexToId.get(i));
for (int j = 0; j < size; j++)
{
if (reach[i][j])
{
if (tmp == null)
{
tmp = new HashSet<>();
res.put(indexToId.get(i), tmp);
}
tmp.add(indexToId.get(j));
}
}
}
return Collections.unmodifiableMap(res);
}
@Override
public String toDot()
{
return this.init.toDot();
}
@Override
public String toAut()
{
return this.init.toAut();
}
@Override
public String toString()
{
return this.init.toString();
}
// Factory method: not fully integrated with SGraph constructor because of Job arg (debug printing)
// Also checks for non-deterministic payloads
// Maybe refactor into an SGraph builder util; cf., EGraphBuilderUtil -- but not Visitor (cf., EndpointGraphBuilder), this isn't an AST algorithm
public static SGraph buildSGraph(Map<Role, EGraph> egraphs, boolean explicit, Job job, GProtocolName fullname) throws ScribbleException
{
for (Role r : egraphs.keySet())
{
job.debugPrintln("(" + fullname + ") Building global model using EFSM for " + r + ":\n" + egraphs.get(r).init.toDot());
}
Map<Role, EFSM> efsms = egraphs.entrySet().stream().collect(Collectors.toMap((e) -> e.getKey(), (e) -> e.getValue().toFsm()));
SBuffers b0 = new SBuffers(efsms.keySet(), !explicit);
SConfig c0 = new SConfig(efsms, b0);
SState init = new SState(c0);
Map<Integer, SState> seen = new HashMap<>();
LinkedHashSet<SState> todo = new LinkedHashSet<>();
todo.add(init);
// FIXME: factor out model building and integrate with getAllNodes (seen == all)
int count = 0;
while (!todo.isEmpty())
{
Iterator<SState> i = todo.iterator();
SState curr = i.next();
i.remove();
seen.put(curr.id, curr);
if (job.debug)
{
count++;
if (count % 50 == 0)
{
job.debugPrintln("(" + fullname + ") Building global states: " + count);
}
}
Map<Role, List<EAction>> fireable = curr.getFireable();
//job.debugPrintln("Acceptable at (" + curr.id + "): " + acceptable);
for (Role r : fireable.keySet())
{
List<EAction> fireable_r = fireable.get(r);
// Hacky? // FIXME: factor out and make more robust (e.g. for new state kinds) -- e.g. "hasPayload" in IOAction
//EndpointState currstate = curr.config.states.get(r);
EFSM currfsm = curr.config.efsms.get(r);
EStateKind k = currfsm.getStateKind();
if (k == EStateKind.OUTPUT)
{
for (EAction a : fireable_r) // Connect implicitly has no payload (also accept, so skip)
{
if (fireable_r.stream().anyMatch((x) ->
!a.equals(x) && a.peer.equals(x.peer) && a.mid.equals(x.mid) && !a.payload.equals(x.payload)))
{
throw new ScribbleException("Bad non-deterministic action payloads: " + fireable_r);
}
}
}
else if (k == EStateKind.UNARY_INPUT || k == EStateKind.POLY_INPUT || k == EStateKind.ACCEPT)
{
for (EAction a : fireable_r)
{
if (currfsm.getAllFireable().stream().anyMatch((x) ->
!a.equals(x) && a.peer.equals(x.peer) && a.mid.equals(x.mid) && !a.payload.equals(x.payload)))
{
throw new ScribbleException("Bad non-deterministic action payloads: " + currfsm.getAllFireable());
}
}
}
} // Need to do all action payload checking before next building step, because doing sync actions will also remove peer's actions from takeable set
for (Role r : fireable.keySet())
{
List<EAction> fireable_r = fireable.get(r);
for (EAction a : fireable_r)
{
if (a.isSend() || a.isReceive() || a.isDisconnect())
{
getNextStates(todo, seen, curr, a.toGlobal(r), curr.fire(r, a));
}
else if (a.isAccept() || a.isConnect())
{
List<EAction> as = fireable.get(a.peer);
EAction d = a.toDual(r);
if (as != null && as.contains(d))
{
as.remove(d); // Removes one occurrence
//getNextStates(seen, todo, curr.sync(r, a, a.peer, d));
SAction g = (a.isConnect()) ? a.toGlobal(r) : d.toGlobal(a.peer); // Edge will be drawn as the connect, but should be read as the sync. of both -- something like "r1, r2: sync" may be more consistent (or take a set of actions as the edge label)
getNextStates(todo, seen, curr, g, curr.sync(r, a, a.peer, d));
}
}
else if (a.isWrapClient() || a.isWrapServer())
{
List<EAction> as = fireable.get(a.peer);
EAction w = a.toDual(r);
if (as != null && as.contains(w))
{
as.remove(w); // Removes one occurrence
SAction g = (a.isConnect()) ? a.toGlobal(r) : w.toGlobal(a.peer);
getNextStates(todo, seen, curr, g, curr.sync(r, a, a.peer, w));
}
}
else
{
throw new RuntimeException("Shouldn't get in here: " + a);
}
}
}
}
SGraph graph = new SGraph(fullname, seen, init);
job.debugPrintln("(" + fullname + ") Built global model..\n" + graph.init.toDot() + "\n(" + fullname + ") .." + graph.states.size() + " states");
return graph;
}
private static void getNextStates(LinkedHashSet<SState> todo, Map<Integer, SState> seen, SState curr, SAction a, List<SConfig> nexts)
{
for (SConfig next : nexts)
{
SState news = new SState(next);
SState succ = null;
//if (seen.contains(succ)) // FIXME: make a SGraph builder
/*if (seen.containsValue(succ))
{
for (WFState tmp : seen)
{
if (tmp.equals(succ))
{
succ = tmp;
}
}
}*/
for (SState tmp : seen.values()) // Key point: checking "semantically" if model state already created
{
if (tmp.equals(news))
{
succ = tmp;
}
}
if (succ == null)
{
for (SState tmp : todo) // If state created but not "seen" yet, then it will be "todo"
{
if (tmp.equals(news))
{
succ = tmp;
}
}
}
if (succ == null)
{
succ = news;
todo.add(succ);
}
//curr.addEdge(a.toGlobal(r), succ);
curr.addEdge(a, succ); // FIXME: make a Builder util, cf. EGraphBuilderUtil
//if (!seen.contains(succ) && !todo.contains(succ))
/*if (!seen.containsKey(succ.id) && !todo.contains(succ))
{
todo.add(succ);
}*/
}
}
}