package org.scribble.model.global;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.scribble.model.endpoint.EFSM;
import org.scribble.model.endpoint.EState;
import org.scribble.model.endpoint.EStateKind;
import org.scribble.model.endpoint.actions.EAccept;
import org.scribble.model.endpoint.actions.EConnect;
import org.scribble.model.endpoint.actions.EDisconnect;
import org.scribble.model.endpoint.actions.EAction;
import org.scribble.model.endpoint.actions.EReceive;
import org.scribble.model.endpoint.actions.ESend;
import org.scribble.model.endpoint.actions.EWrapClient;
import org.scribble.model.endpoint.actions.EWrapServer;
import org.scribble.sesstype.name.Role;
public class SConfig
{
//public final Map<Role, EndpointState> states;
public final Map<Role, EFSM> efsms;
public final SBuffers buffs;
//public WFConfig(Map<Role, EndpointState> state, Map<Role, Map<Role, Send>> buff)
//public WFConfig(Map<Role, EndpointState> state, WFBuffers buffs)
public SConfig(Map<Role, EFSM> state, SBuffers buffs)
{
this.efsms = Collections.unmodifiableMap(state);
//this.buffs = Collections.unmodifiableMap(buff.keySet().stream() .collect(Collectors.toMap((k) -> k, (k) -> Collections.unmodifiableMap(buff.get(k)))));
//this.buffs = Collections.unmodifiableMap(buff);
this.buffs = buffs;
}
// FIXME: rename: not just termination, could be unconnected/uninitiated
//public boolean isEnd()
public boolean isSafeTermination()
{
//return this.states.values().stream().allMatch((s) -> s.isTerminal()) && this.buffs.isEmpty();
for (Role r : this.efsms.keySet())
{
if (!canSafelyTerminate(r))
{
return false;
}
}
return true;
}
// Should work both with and without accept-correlation?
public boolean canSafelyTerminate(Role r)
{
//EndpointState s = this.states.get(r);
EFSM s = this.efsms.get(r);
//return
/*boolean cannotSafelyTerminate = // FIXME: check and cleanup
(s.isTerminal() && !this.buffs.isEmpty(r))
||
(!s.isTerminal() &&
//(!(s.getStateKind().equals(Kind.UNARYINPUT) && s.getTakeable().iterator().next().isAccept()) // Accept state now distinguished
(
!(s.getStateKind().equals(Kind.ACCEPT) && s.isInitial()) // FIXME: check stable
// FIXME: needs initial state check -- although if there is an accept, there should a connect, and waitfor-errors checked via connects) -- this should be OK because connect/accept are sync -- but not fully sufficient by itself, see next
// So could be blocked on unary accept part way through the protocol -- but also could be unfolded initial accept
////|| this.states.keySet().stream().anyMatch((rr) -> !r.equals(rr) && this.buffs.isConnected(r, rr))))
//&& this.states.keySet().stream().anyMatch((rr) -> !r.equals(rr) && this.buffs.isConnected(r, rr))
// FIXME: isConnected is not symmetric, and could disconnect all part way through protocol -- but can't happen?
// Above assumes initial is not terminal (holds for EFSMs), and doesn't check buffer is empty (i.e. for orphan messages)
)
)
||
(!s.isTerminal() && this.states.keySet().stream().anyMatch((rr) -> !r.equals(rr) && this.buffs.isConnected(r, rr)))
;*/
//return !cannotSafelyTerminate;
boolean canSafelyTerminate =
(s.isTerminated() && this.buffs.isEmpty(r))
|| (s.getStateKind().equals(EStateKind.ACCEPT) && s.isInitial()) // FIXME: should be empty buffs
// FIXME: incorrectly allows stuck accepts? if inactive not initial, should be clone of initial?
//|| (s.getStateKind().equals(Kind.ACCEPT) && this.states.keySet().stream().noneMatch((rr) -> !r.equals(rr) && this.buffs.isConnected(r, rr)))
;
return canSafelyTerminate;
}
public List<SConfig> fire(Role r, EAction a)
{
List<SConfig> res = new LinkedList<>();
//List<EndpointState> succs = this.states.get(r).takeAll(a);
List<EFSM> succs = this.efsms.get(r).fireAll(a);
//for (EndpointState succ : succs)
for (EFSM succ : succs)
{
//Map<Role, EndpointState> tmp1 = new HashMap<>(this.states);
Map<Role, EFSM> tmp1 = new HashMap<>(this.efsms);
//Map<Role, Map<Role, Send>> tmp2 = new HashMap<>(this.buffs);
tmp1.put(r, succ);
/*Map<Role, Send> tmp3 = new HashMap<>(tmp2.get(a.peer));
tmp2.put(a.peer, tmp3);* /
Map<Role, Send> tmp3 = tmp2.get(a.peer);
if (a.isSend())
{
tmp3.put(r, (Send) a);
}
else
{
tmp3.put(r, null);
}*/
SBuffers tmp2 =
a.isSend() ? this.buffs.send(r, (ESend) a)
: a.isReceive() ? this.buffs.receive(r, (EReceive) a)
: a.isDisconnect() ? this.buffs.disconnect(r, (EDisconnect) a)
: null;
if (tmp2 == null)
{
throw new RuntimeException("Shouldn't get in here: " + a);
}
res.add(new SConfig(tmp1, tmp2));
}
return res;
}
public List<SConfig> sync(Role r1, EAction a1, Role r2, EAction a2)
{
List<SConfig> res = new LinkedList<>();
/*List<EndpointState> succs1 = this.states.get(r1).takeAll(a1);
List<EndpointState> succs2 = this.states.get(r2).takeAll(a2);
for (EndpointState succ1 : succs1)*/
List<EFSM> succs1 = this.efsms.get(r1).fireAll(a1);
List<EFSM> succs2 = this.efsms.get(r2).fireAll(a2);
for (EFSM succ1 : succs1)
{
//for (EndpointState succ2 : succs2)
for (EFSM succ2 : succs2)
{
//Map<Role, EndpointState> tmp1 = new HashMap<>(this.states);
Map<Role, EFSM> tmp1 = new HashMap<>(this.efsms);
tmp1.put(r1, succ1);
tmp1.put(r2, succ2);
SBuffers tmp2;
if (((a1.isConnect() && a2.isAccept()) || (a1.isAccept() && a2.isConnect())))
//&& this.buffs.canConnect(r1, r2))
{
tmp2 = this.buffs.connect(r1, r2);
}
else if (((a1.isWrapClient() && a2.isWrapServer()) || (a1.isWrapServer() && a2.isWrapClient())))
{
tmp2 = this.buffs; // OK, immutable?
}
else
{
throw new RuntimeException("Shouldn't get in here: " + a1 + ", " + a2);
}
res.add(new SConfig(tmp1, tmp2));
}
}
return res;
}
// Deadlock from non handle-able messages (reception errors)
public Map<Role, EReceive> getStuckMessages()
{
Map<Role, EReceive> res = new HashMap<>();
for (Role r : this.efsms.keySet())
{
//EndpointState s = this.states.get(r);
EFSM s = this.efsms.get(r);
EStateKind k = s.getStateKind();
if (k == EStateKind.UNARY_INPUT || k == EStateKind.POLY_INPUT)
{
/*Set<IOAction> duals = this.buffs.get(r).entrySet().stream()
.filter((e) -> e.getValue() != null)
.map((e) -> e.getValue().toDual(e.getKey()))
.collect(Collectors.toSet());
if (duals.stream().anyMatch((a) -> s.isAcceptable(a)))
{
break;
}*/
Role peer = s.getAllFireable().iterator().next().peer;
ESend send = this.buffs.get(r).get(peer);
if (send != null)
{
EReceive recv = send.toDual(peer);
if (!s.hasFireable(recv))
//res.put(r, new IOError(peer));
res.put(r, recv);
}
}
/*else if (k == Kind.ACCEPT) // FIXME: ..and connect
{
// FIXME: issue is, unlike regular input states, blocked connect/accept may become unblocked later, so queued messages may not be stuck
// (if message is queued on the actual blocked connection, it should be orphan message)
// so, message is stuck only if connect/accept is genuinely deadlocked, which will be detected as that
}*/
}
return res;
}
// Doesn't include locally terminated (single term state does not induce a deadlock cycle) -- i.e. only "bad" deadlocks
public Set<Set<Role>> getWaitForErrors()
{
Set<Set<Role>> res = new HashSet<>();
List<Role> todo = new LinkedList<>(this.efsms.keySet());
/*while (!todo.isEmpty())
{
Role r = todo.get(0);
todo.remove(r);
Set<Role> seen = new HashSet<>();
while (true)
{
if (seen.contains(r))
{
res.add(seen);
break;
}
seen.add(r);
Role rr = isInputBlocked(r);
if (rr == null)
{
break;
}
todo.remove(rr);
if (this.states.get(rr).isTerminal())
{
seen.add(rr);
res.add(seen);
break;
}
r = rr;
}
}*/
while (!todo.isEmpty()) // FIXME: maybe better to do directly on states, rather than via roles
{
Role r = todo.remove(0);
//Set<Role> cycle = isCycle(new HashSet<>(), new HashSet<>(Arrays.asList(r)));
if (!this.efsms.get(r).isTerminated())
{
Set<Role> cycle = isWaitForChain(r);
//if (!cycle.isEmpty())
if (cycle != null)
{
todo.removeAll(cycle);
res.add(cycle);
}
}
}
return res;
}
// Includes dependencies from input-blocking, termination and connect-blocking
// FIXME: should also include connect?
// NB: if this.states.get(orig).isTerminal() then orig is returned as "singleton deadlock"
//public Set<Role> isCycle(Set<Role> candidate, Set<Role> todo)
public Set<Role> isWaitForChain(Role orig)
{
/*if (todo.isEmpty())
{
return candidate;
}*/
/*Set<Role> tmp = new HashSet<Role>(todo);
Role r = tmp.iterator().next();
tmp.remove(r);
candidate.add(r);*/
Set<Role> candidate = new LinkedHashSet<>();
Set<Role> todo = new LinkedHashSet<>(Arrays.asList(orig));
while (!todo.isEmpty())
{
Role r = todo.iterator().next();
todo.remove(r);
candidate.add(r);
//EndpointState s = this.states.get(r);
EFSM s = this.efsms.get(r);
if (s == null)
{
System.out.println("AAA: " + this.efsms + ", " + r);
}
if (s.getStateKind() == EStateKind.OUTPUT && !s.isConnectOrWrapClientOnly()) // FIXME: includes connect, could still be deadlock? -- no: doesn't include connect any more
{
// FIXME: move into isWaitingFor
return null;
}
if (s.isTerminated())
{
if (todo.isEmpty())
{
return candidate;
}
continue;
}
Set<Role> blocked = isWaitingFor(r);
//if (blocked.isEmpty())
if (blocked == null)
{
return null;
}
if (todo.isEmpty() && candidate.containsAll(blocked))
{
return candidate;
}
blocked.forEach((x) ->
{
if (!candidate.contains(x))
{
//candidate.add(x);
todo.add(x);
}
});
}
return null;
}
// Generalised to include connect-blocked roles
//private Role isInputBlocked(Role r)
private Set<Role> isWaitingFor(Role r)
{
//EndpointState s = this.states.get(r);
EFSM s = this.efsms.get(r);
EStateKind k = s.getStateKind();
if (k == EStateKind.UNARY_INPUT || k == EStateKind.POLY_INPUT)
{
List<EAction> all = s.getAllFireable();
EAction a = all.get(0); // FIXME: assumes single choice subject (OK for current syntax, but should generalise)
/*if (a.isAccept()) // Sound?
{
return null;
}*/
/*Role peer = a.peer;
if (a.isReceive() && this.buffs.get(r).get(peer) == null)
{
//return peer;
}*/
if (a.isReceive())
{
Set<Role> peers = all.stream().map((x) -> x.peer).collect(Collectors.toSet());
if (peers.stream().noneMatch((p) -> this.buffs.get(r).get(p) != null))
{
return peers;
}
/*Set<Role> peers = new HashSet<>(); // Debugging AllTest/BadTest bad.efsm.grecursion.unfair.Test01; problem
for (EAction ea : all)
{
peers.add(ea.peer);
}
boolean tmp = true;
for (Role p : peers)
{
if (this.buffs.get(r).get(p) != null)
{
tmp = false;
break;
}
}
if (tmp)
{
return peers;
}*/
}
}
else if (k == EStateKind.ACCEPT)
{
// FIXME TODO: if analysing ACCEPTs, check if s is initial (not "deadlock blocked" if initial) -- no: instead, analysing connects
if (!s.isInitial())
{
List<EAction> all = s.getAllFireable(); // Should be singleton -- no: not any more
/*Set<Role> rs = all.stream().map((x) -> x.peer).collect(Collectors.toSet());
if (rs.stream().noneMatch((x) -> this.states.get(x).getAllTakeable().contains(new Connect(r)))) // cf. getTakeable
//if (peera.equals(c.toDual(r)) && this.buffs.canConnect(r, c))
{
return rs;
}*/
Set<Role> res = new HashSet<Role>();
for (EAction a : all) // Accept // FIXME: WrapServer
{
if (this.efsms.get(a.peer).getAllFireable().contains(a.toDual(r)))
{
return null;
}
res.add(a.peer);
}
if (!res.isEmpty())
{
return res;
}
}
}
//else if (k == Kind.CONNECTION)
else if (k == EStateKind.OUTPUT //|| k == Kind.ACCEPT ..// FIXME: check connects if no available sends
)
{
//List<IOAction> all = s.getAllAcceptable();
if (s.isConnectOrWrapClientOnly())
{
List<EAction> all = s.getAllFireable();
/*Set<Role> peers = all.stream().map((x) -> x.peer).collect(Collectors.toSet()); // Should be singleton by enabling conditions
if (peers.stream().noneMatch((p) -> this.states.get(p).getAllTakeable().contains(new Accept(r)))) // cf. getTakeable
{
return peers;
}*/
Set<Role> res = new HashSet<Role>();
for (EAction a : all) // Connect or WrapClient
{
if (this.efsms.get(a.peer).getAllFireable().contains(a.toDual(r)))
{
return null;
}
res.add(a.peer);
}
if (!res.isEmpty())
{
return res;
}
}
}
return null;
//return Collections.emptySet();
}
// Generalised to include "unconnected" messages -- should unconnected messages be treated via stuck instead?
public Map<Role, Set<ESend>> getOrphanMessages()
{
Map<Role, Set<ESend>> res = new HashMap<>();
for (Role r : this.efsms.keySet())
{
//EndpointState s = this.states.get(r);
EFSM s = this.efsms.get(r);
if (s.isTerminated()) // Local termination of r, i.e. not necessarily "full deadlock"
{
Set<ESend> orphs = this.buffs.get(r).values().stream().filter((v) -> v != null).collect(Collectors.toSet());
if (!orphs.isEmpty())
{
Set<ESend> tmp = res.get(r);
if (tmp == null)
{
tmp = new HashSet<>();
res.put(r, tmp);
}
tmp.addAll(orphs);
}
}
else
{
this.efsms.keySet().forEach((rr) ->
{
if (!rr.equals(r))
{
// Connection direction doesn't matter? -- wrong: matters because of async. disconnect
if (!this.buffs.isConnected(r, rr))
{
ESend send = this.buffs.get(r).get(rr);
if (send != null)
{
Set<ESend> tmp = res.get(r);
if (tmp == null)
{
tmp = new HashSet<>();
res.put(r, tmp);
}
tmp.add(send);
}
}
}
});
}
}
return res;
}
// Not just "unfinished", but also "non-initiated" (accept guarded) -- though could be non-initiated after some previous completions
// Maybe not needed -- previously not used (even without accept-correlation check)
public Map<Role, EState> getUnfinishedRoles()
{
Map<Role, EState> res = new HashMap<>();
if (getFireable().isEmpty() && !isSafeTermination())
{
for (Role r : this.efsms.keySet())
{
if (!canSafelyTerminate(r))
{
res.put(r, this.efsms.get(r).curr);
}
}
}
return res;
}
public Map<Role, List<EAction>> getFireable()
{
Map<Role, List<EAction>> res = new HashMap<>();
for (Role r : this.efsms.keySet())
{
//EndpointState s = this.states.get(r);
EFSM fsm = this.efsms.get(r);
switch (fsm.getStateKind()) // Choice subject enabling needed for non-mixed states (mixed states would be needed for async. permutations though)
{
case OUTPUT:
{
List<EAction> as = fsm.getAllFireable();
for (EAction a : as)
{
if (a.isSend())
{
if (this.buffs.canSend(r, (ESend) a))
{
List<EAction> tmp = res.get(r); // FIXME: factor out
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
}
}
else if (a.isConnect())
{
// FIXME: factor out
EConnect c = (EConnect) a;
//EndpointState speer = this.states.get(c.peer);
EFSM speer = this.efsms.get(c.peer);
//if (speer.getStateKind() == Kind.UNARY_INPUT)
{
List<EAction> peeras = speer.getAllFireable();
for (EAction peera : peeras)
{
if (peera.equals(c.toDual(r)) && this.buffs.canConnect(r, c)) // Cf. isWaitingFor
{
List<EAction> tmp = res.get(r);
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
}
}
}
}
else if (a.isDisconnect())
{
// Duplicated from Send
if (this.buffs.canDisconnect(r, (EDisconnect) a))
{
List<EAction> tmp = res.get(r); // FIXME: factor out
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
}
}
else if (a.isWrapClient())
{
// FIXME: factor out
EWrapClient wc = (EWrapClient) a;
EFSM speer = this.efsms.get(wc.peer);
List<EAction> peeras = speer.getAllFireable();
for (EAction peera : peeras)
{
if (peera.equals(wc.toDual(r)) && this.buffs.canWrapClient(r, wc)) // Cf. isWaitingFor
{
List<EAction> tmp = res.get(r);
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
}
}
}
else
{
throw new RuntimeException("Shouldn't get in here: " + a);
}
}
break;
}
case UNARY_INPUT:
case POLY_INPUT:
{
for (EAction a : this.buffs.inputable(r))
{
if (a.isReceive())
{
if (fsm.hasFireable(a))
{
List<EAction> tmp = res.get(r);
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
}
}
/*else if (a.isAccept())
{
// FIXME: factor out
Accept c = (Accept) a;
EndpointState speer = this.states.get(c.peer);
//if (speer.getStateKind() == Kind.OUTPUT)
{
List<IOAction> peeras = speer.getAllAcceptable();
for (IOAction peera : peeras)
{
if (peera.equals(c.toDual(r)) && this.buffs.canAccept(r, c))
{
List<IOAction> tmp = res.get(r);
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
//break; // Add all of them
}
}
}
}*/
else
{
throw new RuntimeException("Shouldn't get in here: " + a);
}
}
break;
}
case TERMINAL:
{
break;
}
/*case CONNECT:
{
List<IOAction> as = s.getAllTakeable();
for (IOAction a : as)
{
if (a.isConnect()) ..// FIXME: could be send actions
{
// FIXME: factor out
Connect c = (Connect) a;
EndpointState speer = this.states.get(c.peer);
//if (speer.getStateKind() == Kind.UNARY_INPUT)
{
List<IOAction> peeras = speer.getAllTakeable();
for (IOAction peera : peeras)
{
if (peera.equals(c.toDual(r)) && this.buffs.canConnect(r, c))
{
List<IOAction> tmp = res.get(r);
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
}
}
}
}
else
{
throw new RuntimeException("Shouldn't get in here: " + s);
}
}
break;
}*/
case ACCEPT:
{
for (EAction a : this.buffs.acceptable(r, fsm.curr))
{
if (a.isAccept())
{
// FIXME: factor out
EAccept c = (EAccept) a;
//EndpointState speer = this.states.get(c.peer);
EFSM speer = this.efsms.get(c.peer);
//if (speer.getStateKind() == Kind.OUTPUT)
{
List<EAction> peeras = speer.getAllFireable();
for (EAction peera : peeras)
{
if (peera.equals(c.toDual(r)) && this.buffs.canAccept(r, c))
{
List<EAction> tmp = res.get(r);
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
//break; // Add all of them
}
}
}
}
else
{
throw new RuntimeException("Shouldn't get in here: " + a);
}
}
break;
}
case WRAP_SERVER:
{
for (EAction a : this.buffs.wrapable(r))
{
if (a.isWrapServer())
{
EWrapServer ws = (EWrapServer) a;
EFSM speer = this.efsms.get(ws.peer);
{
List<EAction> peeras = speer.getAllFireable();
for (EAction peera : peeras)
{
if (peera.equals(ws.toDual(r)) && this.buffs.canWrapServer(r, ws))
{
List<EAction> tmp = res.get(r);
if (tmp == null)
{
tmp = new LinkedList<>();
res.put(r, tmp);
}
tmp.add(a);
}
}
}
}
else
{
throw new RuntimeException("Shouldn't get in here: " + a);
}
}
break;
}
default:
{
throw new RuntimeException("Shouldn't get in here: " + fsm);
}
}
}
return res;
}
@Override
public final int hashCode()
{
int hash = 71;
hash = 31 * hash + this.efsms.hashCode();
hash = 31 * hash + this.buffs.hashCode();
return hash;
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof SConfig))
{
return false;
}
SConfig c = (SConfig) o;
return this.efsms.equals(c.efsms) && this.buffs.equals(c.buffs);
}
@Override
public String toString()
{
return "(" + this.efsms + ", " + this.buffs + ")";
}
}