We here describe a simple transformation on join definitions, which does not rely on a full semantical analysis (such as name usage analysis), but only on a simple, local, syntactical analysis of join-patterns.
Let us take a simple example:
let S(x) | m_1(y) = P_1(x,y) and S(x) | m_2(y) = P_2(x,y) .... and S(x) | m_n(y) = P_n(x,y) ;;
In this example, a match occurs only when there are messages pending both on S and on one of the methods m1, m2,…Thus, from the synchronization point of view, all the methods are equivalent. And indeed, we can regroup them into one, single method channel m by transforming this join definition into:
let S(x) | m(p,y) = P[p] (x,y);; let m_1(y) = m(1,y);; let m_2(y) = m(2,y);; .... let m_n(y) = m(n,y);;
Where P is the vector of processes [P1,P2,...,Pn].
Methods m1 m2,…are now simple wrappers. Method mi now calls m with an additional i argument, which basically is the index of Pi in array P. At this point, we must emphasize that we describe this technique as a source to source transformation only for clarity. However, the produced source code may not be correct with respect to the join type system, when the types of methods are different. Anyhow, this optimization is implemented using ad hoc mechanisms, this both improves efficiency and solves the typing problem.
Currently, this optimization is performed by the jocaml compiler. This leads to a new interpretation of name indexes by the “join” library. The least significant bits in name indexes still encode names that actually take part to synchronization (here S and m), while their most significant bits (which were previously unused) now encode the extra i argument. This yields two benefits. First, the number of status checks decreases, as the number of matching statuses decreases. Second, the number of channels that can be defined in one definition can now exceed the hosting system integer size, provided some names can be grouped together for synchronization.
In the join compiler, this technique might be used to reduce automata size, since it lowers the number of non-matching statuses, by reducing the number of synchronizing names. Code entries for methods m1, m2,…would still be contained in the definition structure, they would only stack the index of the process to fire, and then call the code for method m. Moreover, they do not need to be updated after each transition of the automaton.
Finally, this technique can also be applied to more complex synchronizations. Given a definition that defines names x1, x2, …, xn, using patterns J1, J2, …Jm. We say that two names are equivalent, when swapping them in the patterns yields the same set of patterns. We then replace equivalent names by a single name, plus an index.
Consider the following definition
let S_1(x) | m_1(y) = P_1(x,y) and S_1(x) | m_2(y) = P_2(x,y) and S_2(x) | m_1(y) = P_3(x,y) and S_2(x) | m_2(y) = P_4(x,y) ;;
Then the set of defined names {S1, S2, m1, m2} can be partitioned into {S1, S2} and {m1, m2}. Then, the above program can be transformed into:
let S(p,x) | m(q,y) = P[p,q] (x,y);; let m_1(y) = m(1,y);; let m_2(y) = m(2,y);; let S_1(y) = S(1,y);; let S_2(y) = S(2,y);;
(with P[1,1] = P1, P[1,2] = P2, P[2,1] = P3 and P[2,2] = P4)