// Copyright (c) 2011, David J. Pearce (djp@ecs.vuw.ac.nz) // All rights reserved. // // This software may be modified and distributed under the terms // of the BSD license. See the LICENSE file for details. package wyil.util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import wyautl_old.lang.Automata; import wyautl_old.lang.Automaton; import wyautl_old.lang.Automaton.State; import wybs.lang.Build; import wybs.lang.NameID; import wybs.util.ResolveError; import wycc.util.Pair; import wyfs.lang.Path; import wyil.lang.Type; import wyil.lang.WyilFile; import wyil.util.type.ExplicitCoercionOperator; import wyil.util.type.LifetimeRelation; import wyil.util.type.SubtypeOperator; import wyil.util.type.TypeAlgorithms; /** * <p> * The type system is responsible for managing the relationship between * nominal types and their underlying types. Every visible type has an * underlying type associated with it which, in some cases, will be the same. * For example, the underlying type associated with type <code>int</code> is * simply <code>int</code>. However, in many cases, there is a difference. For * example: * </p> * * <pre> * type nat is (int x) where x >= 0 * </pre> * * <p> * In this case, the underlying type associated with the type <code>nat</code> * is <code>int</code>. This class provides a way to determine the underlying * type associated with a given type. * </p> * <p> * <b>NOTE:</b> in principle, this could cache expanded types for performance * reasons (though it currently does not). * </p> * * @author David J. Pearce * */ public class TypeSystem { private final Build.Project project; public TypeSystem(Build.Project project) { this.project = project; } /** * Determine whether or not this type corresponds to the empty type or not. * This can happen in a number of ways. * * @param type * @return * @throws ResolveError */ public boolean isEmpty(Type type) throws ResolveError { Automaton automaton = toAutomaton(type); // FIXME: should include contractivity check? return automaton.states[0].kind == K_VOID; } /** * <p> * Contractive types are types which cannot accept value because they have * an <i>unterminated cycle</i>. An unterminated cycle has no leaf nodes * terminating it. For example, <code>X<{X field}></code> is contractive, * where as <code>X<{null|X field}></code> is not. * </p> * * <p> * This method returns true if the type is contractive, or contains a * contractive subcomponent. For example, <code>null|X<{X field}></code> is * considered contracted. * </p> * * @param type --- type to test for contractivity. * @return * @throws ResolveError */ public boolean isContractive(Type type) throws ResolveError { if (type instanceof Type.Leaf) { return false; } else { Automaton automaton = toAutomaton(type); return TypeAlgorithms.isContractive(automaton); } } /** * Assuming given type is an effective record of some sort, expand to ensure * record structure is visible. For example a type <code>myRecord</code> * would expanded one level to look like <code>{T aField,...}</code> for * some (potentially nominal) element type T. * * @param type * The type to be expanded * @return null if given type is not an effective record * @throws ResolveError */ public Type.EffectiveRecord expandAsEffectiveRecord(Type type) throws ResolveError { if (type instanceof Type.EffectiveRecord) { // This type is already an effective record. Therefore, no need to // do anything. return (Type.EffectiveRecord) type; } else { // This type may be an effective record. To find out, we need to // expand one level of nominal type information type = expandOneLevel(type); if (type instanceof Type.EffectiveRecord) { return (Type.EffectiveRecord) type; } else { return null; } } } /** * Assuming given type is an effective array of some sort, expand to ensure * array structure is visible. For example a type <code>myArray</code> would * expanded one level to look like <code>T[]</code> for some (potentially * nominal) element type T. * * @param type * The type to be expanded * @return null if type is no an effective array * @throws ResolveError */ public Type.EffectiveArray expandAsEffectiveArray(Type type) throws ResolveError { if (type instanceof Type.EffectiveArray) { // This type is already an effective array. Therefore, no need to // do anything. return (Type.EffectiveArray) type; } else { // This type may be an effective array. To find out, we need to // expand one level of nominal type information type = expandOneLevel(type); if(type instanceof Type.EffectiveArray) { return (Type.EffectiveArray) type; } else { return null; } } } /** * Assuming given type is an effective reference of some sort, expand to * ensure reference structure is visible. For example a type * <code>myRef</code> would expanded one level to look like <code>&T</code> * for some (potentially nominal) element type T. * * @param type * The type to be expanded * @return * @throws ResolveError */ public Type.Reference expandAsReference(Type type) throws ResolveError { if (type instanceof Type.Reference) { // This type is already a reference. Therefore, no need to // do anything. return (Type.Reference) type; } else { // This type may be a reference. To find out, we need to // expand one level of nominal type information type = expandOneLevel(type); if (type instanceof Type.Reference) { return (Type.Reference) type; } else { return null; } } } /** * Assuming given type is an effective function or method type of some sort, * expand to ensure structure is visible. * * @param type * The type to be expanded * @return * @throws ResolveError */ public Type.FunctionOrMethod expandAsFunctionOrMethod(Type type) throws ResolveError { if (type instanceof Type.FunctionOrMethod) { // This type is already a function or method. Therefore, no need to // do anything. return (Type.FunctionOrMethod) type; } else { // This type may be a reference. To find out, we need to // expand one level of nominal type information type = expandOneLevel(type); if (type instanceof Type.FunctionOrMethod) { return (Type.FunctionOrMethod) type; } else { return null; } } } // ============================================================= // Subtype Operator(s) // ============================================================= /** * Determine whether type <code>t2</code> is an <i>explicit coercive * subtype</i> of type <code>t1</code>. * * @throws ResolveError * If a named type within either of the operands cannot be * resolved within the enclosing project. */ public boolean isExplicitCoerciveSubtype(Type t1, Type t2, LifetimeRelation lr) throws ResolveError { Automaton a1 = toAutomaton(t1); Automaton a2 = toAutomaton(t2); ExplicitCoercionOperator relation = new ExplicitCoercionOperator(a1,a2,lr); return relation.isSubtype(0, 0); } /** * Determine whether type <code>t2</code> is an <i>explicit coercive * subtype</i> of type <code>t1</code>. * * @throws ResolveError * If a named type within either of the operands cannot be * resolved within the enclosing project. */ public boolean isExplicitCoerciveSubtype(Type t1, Type t2) throws ResolveError { return isExplicitCoerciveSubtype(t1, t2, LifetimeRelation.EMPTY); } /** * Determine whether type <code>t2</code> is a <i>subtype</i> of type * <code>t1</code> (written t1 :> t2). In other words, whether the set of * all possible values described by the type <code>t2</code> is a subset of * that described by <code>t1</code>. * * @throws ResolveError * If a named type within either of the operands cannot be * resolved within the enclosing project. */ public boolean isSubtype(Type t1, Type t2, LifetimeRelation lr) throws ResolveError { // FIXME: These two lines are a hack; they help, but are not a general // solution. See #696. t1 = expandOneLevel(t1); t2 = expandOneLevel(t2); // END Automaton a1 = toAutomaton(t1); Automaton a2 = toAutomaton(t2); SubtypeOperator relation = new SubtypeOperator(a1,a2,lr); return relation.isSubtype(0, 0); } /** * Determine whether type <code>t2</code> is a <i>subtype</i> of type * <code>t1</code> (written t1 :> t2). In other words, whether the set of * all possible values described by the type <code>t2</code> is a subset of * that described by <code>t1</code>. * * @throws ResolveError * If a named type within either of the operands cannot be * resolved within the enclosing project. */ public boolean isSubtype(Type t1, Type t2) throws ResolveError { return isSubtype(t1, t2, LifetimeRelation.EMPTY); } /** * Expand a given syntactic type by exactly one level. * * @param type * @return * @throws IOException * @throws ResolveError */ public Type expandOneLevel(Type type) throws ResolveError { return expandOneLevel(type,true); } /** * Expand a given syntactic type by exactly one level, according to a given * goal of either maximising or minimising the resulting type. * * @param type * type to be expanded * @param maximise * If true, then maximise resulting type * @return * @throws ResolveError */ private Type expandOneLevel(Type type, boolean maximise) throws ResolveError { try { if (type instanceof Type.Nominal) { Type.Nominal nt = (Type.Nominal) type; NameID nid = nt.name(); Path.Entry<WyilFile> p = project.get(nid.module(), WyilFile.ContentType); if (p == null) { throw new ResolveError("name not found: " + nid); } WyilFile.Type td = p.read().type(nid.name()); if(maximise || td.getInvariant().isEmpty()) { return expandOneLevel(td.type(),maximise); } else { return Type.T_VOID; } } else if (type instanceof Type.Leaf || type instanceof Type.Reference || type instanceof Type.Array || type instanceof Type.Record || type instanceof Type.FunctionOrMethod) { return type; } else if(type instanceof Type.Negation) { Type.Negation nt = (Type.Negation) type; Type element = expandOneLevel(nt.element(),!maximise); return Type.Negation(element); } else if(type instanceof Type.Union){ Type.Union ut = (Type.Union) type; Type[] ut_bounds = ut.bounds(); Type[] bounds = new Type[ut_bounds.length]; for (int i=0;i!=ut_bounds.length;++i) { bounds[i] = expandOneLevel(ut_bounds[i],maximise); } return Type.Union(bounds); } else { Type.Intersection it = (Type.Intersection) type; Type[] it_bounds = it.bounds(); Type[] bounds = new Type[it_bounds.length]; for (int i=0;i!=it_bounds.length;++i) { bounds[i] = expandOneLevel(it_bounds[i],maximise); } return Type.Intersection(bounds); } } catch (IOException e) { throw new ResolveError(e.getMessage(), e); } } // ============================================================= // Automaton Representation // ============================================================= public static final class FunctionOrMethodState implements Comparable<FunctionOrMethodState> { public final int numParams; public final ArrayList<String> contextLifetimes; public final ArrayList<String> lifetimeParameters; public FunctionOrMethodState(int numParams, String[] contextLifetimes, String[] lifetimeParameters) { this.numParams = numParams; this.contextLifetimes = new ArrayList<>(); for (int i = 0; i != contextLifetimes.length; ++i) { this.contextLifetimes.add(contextLifetimes[i]); } this.contextLifetimes.remove("*"); Collections.sort(this.contextLifetimes); this.lifetimeParameters = new ArrayList<>(); for (int i = 0; i != lifetimeParameters.length; ++i) { this.lifetimeParameters.add(lifetimeParameters[i]); } } @Override public int hashCode() { return 31 * (31 * numParams + contextLifetimes.hashCode()) + lifetimeParameters.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (this.getClass() != obj.getClass()) { return false; } FunctionOrMethodState other = (FunctionOrMethodState) obj; if (this.numParams != other.numParams) { return false; } if (!this.contextLifetimes.equals(other.contextLifetimes)) { return false; } if (!this.lifetimeParameters.equals(other.lifetimeParameters)) { return false; } return true; } @Override public int compareTo(FunctionOrMethodState other) { int r = Integer.compare(this.numParams, other.numParams); if (r != 0) { return r; } r = compareLists(this.contextLifetimes, other.contextLifetimes); if (r != 0) { return r; } return compareLists(this.lifetimeParameters, other.lifetimeParameters); } private static int compareLists(List<String> my, List<String> other) { Iterator<String> it1 = my.iterator(); Iterator<String> it2 = other.iterator(); while (true) { if (it1.hasNext()) { if (it2.hasNext()) { int r = it1.next().compareTo(it2.next()); if (r != 0) { return r; } } else { return 1; } } else if (it2.hasNext()) { return -1; } else { return 0; } } } } public static final class RecordState extends ArrayList<String> { public final boolean isOpen; public RecordState(boolean isOpen) { this.isOpen = isOpen; } public RecordState(boolean isOpen, Collection<String> values) { super(values); this.isOpen = isOpen; } @Override public boolean equals(Object o) { if (o instanceof RecordState) { RecordState s = (RecordState) o; return isOpen == s.isOpen && super.equals(s); } return false; } } /** * Expand a given type by inlining all visible nominal information. For * example: * * <pre> * type nat is (int x) where x >= 0 * type listnat is [nat] * </pre> * * Expanding the type <code>[nat]</code> will result in the type * <code>[int]</code>. The key challenge here lies in handling nominal types * when they are encountered. We need to determine where the type is * located, and then incorporate the (expanded) body of that type into this * type. In some cases, we're not permitted to inline the body because it's * not visible to this file (e.g. it is marked as private). * * @param type * @return */ public Automaton toAutomaton(Type type) throws ResolveError { // FIXME: this method should be hidden if(type == null) { throw new IllegalArgumentException(); } ArrayList<Automaton.State> states = new ArrayList<>(); HashMap<NameID,Integer> roots = new HashMap<>(); toAutomatonHelper(type, true, states, roots); Automaton automaton = new Automaton(states); return normalise(automaton); } /** * <p> * Expand the given type by loading it's contents into the list of states. * The location of nominal types, when encountered, are cached as "roots" in * order to prevent infinite expansion and, instead, to construct a * recursive cycle. * </p> * * <p> * Expansion proceeds by exploring every compound type until either a leaf * or nominal type is encountered. In the case of a leaf type being * encoutered, we can just add this directly as it cannot be further * expanded. In the case of a nominal type, we then try to inline its body * if permitted. Thus, for a type which contains no nominal types, this * function will return an identical type. * </p> * * @param type * The type currently being expanded. * @param sign * Indicates the current "sign" with respect to negation types. * This is critical for expanding nominal types correctly in the * presence of constraints. * @param states * The list of states being accumulated which will eventually * form the original type being exapnded. * @param roots * The cache of previously inline nominal types which is * necessary to break recursive cycles. * @return * @throws IOException */ private int toAutomatonHelper(Type type, boolean sign, ArrayList<Automaton.State> states, HashMap<NameID, Integer> roots) throws ResolveError { // First, handle nominals (which are challenging) and primitive types // (which are simple). if(type instanceof Type.Nominal) { Type.Nominal tt = (Type.Nominal) type; NameID nid = tt.name(); // First, check whether or not we have already expanded this type. // If so, then we form a recursive cycle. if(roots.containsKey(nid)) { // Yes. we have already expanded it. Therefore we simply need to // return the cached index to form the recursive cycle. return roots.get(nid); } else { // At this point, need to find the corresponding declaration. try { WyilFile mi = project.get(nid.module(),WyilFile.ContentType).read(); WyilFile.Type td = mi.type(nid.name()); if(td == null) { // This indicates the name is valid, but does not // correspond to a type per se. It must correspond to // something else, like a constant or method // declaration. throw new ResolveError("unknown type"); } else if (!sign && td.getInvariant().size() > 0) { // In this specially case, we are asking for !T where T // is a constrained nominal type. In this case, the // correct translation of !T is always any. We return // void here, so that we end up with !void. states.add(new State(K_VOID, null, true, Automaton.NOCHILDREN)); return states.size() - 1; } else { // Now, store the root of this expansion so that it can // be used subsequently to form a recursive cycle. roots.put(nid, states.size()); return toAutomatonHelper(td.type(), sign, states, roots); } } catch (IOException e) { throw new ResolveError(e.getMessage(), e); } } } else if(type instanceof Type.Leaf) { // In ther case of a leaf node we simply copy over its states into // the list of states being accumulated. return append((Type.Leaf) type,states); } // Now handle all non-primitive types which need to be expanded in some // way, int myIndex = states.size(); int myKind; int[] myChildren; Object myData = null; boolean myDeterministic = true; states.add(null); // reserve space for me! if (type instanceof Type.Array) { Type.Array tt = (Type.Array) type; myChildren = new int[1]; myChildren[0] = toAutomatonHelper(tt.element(),sign,states,roots); myKind = K_ARRAY; } else if(type instanceof Type.Record) { Type.Record tt = (Type.Record) type; String[] names = tt.getFieldNames(); RecordState fields = new RecordState(tt.isOpen(),Arrays.asList(names)); Collections.sort(fields); myKind = K_RECORD; myChildren = new int[fields.size()]; for (int i = 0; i != myChildren.length; ++i) { String field = fields.get(i); myChildren[i] = toAutomatonHelper(tt.getField(field), sign,states, roots); } myData = fields; } else if(type instanceof Type.Reference) { Type.Reference tt = (Type.Reference) type; myChildren = new int[1]; myChildren[0] = toAutomatonHelper(tt.element(),sign,states,roots); myData = tt.lifetime(); myKind = K_REFERENCE; } else if(type instanceof Type.Negation) { Type.Negation tt = (Type.Negation) type; myChildren = new int[1]; myChildren[0] = toAutomatonHelper(tt.element(),!sign,states,roots); myKind = K_NEGATION; } else if(type instanceof Type.Union) { Type.Union tt = (Type.Union) type; Type[] bounds = tt.bounds(); myChildren = new int[bounds.length]; int i = 0; for(Type b : bounds) { myChildren[i++] = toAutomatonHelper(b,sign,states,roots); } myKind = K_UNION; } else if(type instanceof Type.Intersection) { Type.Intersection it = (Type.Intersection) type; // FIXME: this is an ugly hack. But it works for now, and eventually // I'll fix it :) Type[] tt_bounds = it.bounds(); Type[] ut_bounds = new Type[tt_bounds.length]; for (int i = 0; i != tt_bounds.length; ++i) { ut_bounds[i] = Type.Negation(tt_bounds[i]); } myChildren = new int[1]; myChildren[0] = toAutomatonHelper(Type.Union(ut_bounds), !sign, states, roots); myKind = K_NEGATION; } else if(type instanceof Type.FunctionOrMethod) { Type.FunctionOrMethod tt = (Type.FunctionOrMethod) type; Type[] tt_params = tt.params(); Type[] tt_returns = tt.returns(); int tt_params_size = tt_params.length; int tt_returns_size = tt_returns.length; myChildren = new int[tt_params_size+tt_returns_size]; for(int i=0;i!=tt_params_size;++i) { myChildren[i] = toAutomatonHelper(tt_params[i],sign,states,roots); } for(int i=0;i!=tt_returns_size;++i) { myChildren[i+tt_params_size] = toAutomatonHelper(tt_returns[i],sign,states,roots); } myData = new FunctionOrMethodState(tt_params_size, getContextLifetimes(tt), getLifetimeParams(tt)); if(tt instanceof Type.Function) { myKind = K_FUNCTION; } else if(tt instanceof Type.Method){ myKind = K_METHOD; } else { myKind = K_PROPERTY; } }else { // FIXME: Probably need to handle function and method types here throw new ResolveError("unknown type encountered: " + type); } states.set(myIndex, new Automaton.State(myKind, myData,myDeterministic, myChildren)); return myIndex; } private static String[] getContextLifetimes(Type.FunctionOrMethod fm) { if(fm instanceof Type.Method) { Type.Method m = (Type.Method) fm; return m.contextLifetimes(); } else { return new String[0]; } } private static String[] getLifetimeParams(Type.FunctionOrMethod fm) { if(fm instanceof Type.Method) { Type.Method m = (Type.Method) fm; return m.lifetimeParams(); } else { return new String[0]; } } private static int append(Type.Leaf type, ArrayList<Automaton.State> states) { int kind; if (type == Type.T_ANY) { kind = K_ANY; } else if (type == Type.T_VOID) { kind = K_VOID; } else if (type == Type.T_NULL) { kind = K_NULL; } else if (type == Type.T_BOOL) { kind = K_BOOL; } else if (type == Type.T_BYTE) { kind = K_BYTE; } else if (type == Type.T_INT) { kind = K_INT; } else { kind = K_META; } states.add(new Automaton.State(kind)); return states.size() - 1; } /** * <p> * The following algorithm simplifies a type. For example: * </p> * * <pre> * define InnerList as null|{int data, OuterList next} * define OuterList as null|{int data, InnerList next} * </pre> * <p> * This type is simplified into the following (equivalent) form: * </p> * * <pre> * define LinkedList as null|{int data, LinkedList next} * </pre> * <p> * The simplification algorithm is made up of several different procedures * which operate on the underlying <i>automaton</i> representing the type: * </p> * <ol> * <li><b>Extraction.</b> Here, sub-components unreachable from the root are * eliminated.</li> * <li><b>Simplification.</b> Here, basic simplifications are applied. For * example, eliminating unions of unions.</li> * <li><b>Minimisation.</b>Here, equivalent states are merged together.</li> * <li><b>Canonicalisation.</b> A canonical form of the type is computed</li> * </ol> * * is based on the well-known algorithm for minimising a DFA (see e.g. <a * href="http://en.wikipedia.org/wiki/DFA_minimization">[1]</a>). </p> * <p> * The algorithm operates by performing a subtype test of each node against * all others. From this, we can identify nodes which are equivalent under * the subtype operator. Using this information, the type is reconstructed * such that for each equivalence class only a single node is created. * </p> * <p> * <b>NOTE:</b> this algorithm does not put the type into a canonical form. * Additional work is necessary to do this. * </p> * * @param afterType * @return */ private static Automaton normalise(Automaton automaton) { //normalisedCount++; //unminimisedCount += automaton.size(); TypeAlgorithms.simplify(automaton); // TODO: extract in place to avoid allocating data unless necessary automaton = Automata.extract(automaton, 0); // TODO: minimise in place to avoid allocating data unless necessary automaton = Automata.minimise(automaton); //minimisedCount += automaton.size(); return automaton; } /** * The following method constructs a string representation of the underlying * automaton. This representation may be an expanded version of the underling * graph, since one cannot easily represent aliasing in the type graph in a * textual manner. * * @param automaton * --- the automaton being turned into a string. * @return --- string representation of automaton. */ private final static String toString(Automaton automaton) { // First, we need to find the headers of the computation. This is // necessary in order to mark the start of a recursive type. BitSet headers = new BitSet(automaton.size()); BitSet visited = new BitSet(automaton.size()); BitSet onStack = new BitSet(automaton.size()); findHeaders(0, visited, onStack, headers, automaton); visited.clear(); String[] titles = new String[automaton.size()]; int count = 0; for (int i = 0; i != automaton.size(); ++i) { if (headers.get(i)) { titles[i] = headerTitle(count++); } } return toString(0, visited, titles, automaton); } /** * The following method constructs a string representation of the underlying * automaton. This representation may be an expanded version of the underling * graph, since one cannot easily represent aliasing in the type graph in a * textual manner. * * @param index * --- the index to start from * @param visited * --- the set of vertices already visited. * @param headers * --- an array of strings which identify the name to be given to * each header. * @param automaton * --- the automaton being turned into a string. * @return --- string representation of automaton. */ private final static String toString(int index, BitSet visited, String[] headers, Automaton automaton) { if (visited.get(index)) { // node already visited return headers[index]; } else if(headers[index] != null) { visited.set(index); } State state = automaton.states[index]; String middle; switch (state.kind) { case K_VOID: return "void"; case K_ANY: return "any"; case K_NULL: return "null"; case K_BOOL: return "bool"; case K_BYTE: return "byte"; case K_INT: return "int"; case K_ARRAY: { middle = toString(state.children[0], visited, headers, automaton) + "[]"; break; } case K_NOMINAL: middle = state.data.toString(); break; case K_REFERENCE: middle = "&"; String lifetime = (String) state.data; if (!"*".equals(lifetime)) { middle += lifetime + ":"; } middle += toString(state.children[0], visited, headers, automaton); break; case K_NEGATION: { middle = "!" + toBracesString(state.children[0], visited, headers, automaton); break; } case K_UNION: { int[] children = state.children; middle = ""; for (int i = 0; i != children.length; ++i) { if(i != 0 || children.length == 1) { middle += "|"; } middle += toBracesString(children[i], visited, headers, automaton); } break; } case K_RECORD: { // labeled nary node middle = "{"; int[] children = state.children; RecordState fields = (RecordState) state.data; for (int i = 0; i != fields.size(); ++i) { if (i != 0) { middle += ","; } middle += toString(children[i], visited, headers, automaton) + " " + fields.get(i); } if(fields.isOpen) { if(children.length > 0) { middle = middle + ",...}"; } else { middle = middle + "...}"; } } else { middle = middle + "}"; } break; } case K_METHOD: case K_FUNCTION: case K_PROPERTY: { String parameters = ""; int[] children = state.children; FunctionOrMethodState data = (FunctionOrMethodState) state.data; int numParameters = data.numParams; for (int i = 0; i != numParameters; ++i) { if (i!=0) { parameters += ","; } parameters += toString(children[i], visited, headers, automaton); } String returns = ""; for (int i = numParameters; i != children.length; ++i) { if (i!=numParameters) { returns += ","; } returns += toString(children[i], visited, headers, automaton); } StringBuilder sb = new StringBuilder(); if(state.kind == K_FUNCTION) { sb.append("function"); } else if(state.kind == K_METHOD) { sb.append("method"); } else { sb.append("property"); } if (!data.contextLifetimes.isEmpty()) { sb.append('['); boolean first = true; for (String l : data.contextLifetimes) { if (!first) { sb.append(','); } else { first = false; } sb.append(l); } sb.append(']'); } if (!data.lifetimeParameters.isEmpty()) { sb.append('<'); boolean first = true; for (String l : data.lifetimeParameters) { if (!first) { sb.append(','); } else { first = false; } sb.append(l); } sb.append('>'); } sb.append('('); sb.append(parameters); sb.append(")->("); sb.append(returns); sb.append(')'); middle = sb.toString(); break; } default: throw new IllegalArgumentException("Invalid type encountered (kind: " + state.kind +")"); } // Finally, check whether this is a header node, or not. If it is a // header then we need to insert the recursive type. String header = headers[index]; if(header != null) { // The following case is interesting. Basically, we'll never revisit // a header. Therefore, if we have multiple edges landing on a // header we must update the header string to represent the full // type reachable from the header. String r = header + "<" + middle + ">"; headers[index] = r; return r; } else { return middle; } } private final static String toBracesString(int index, BitSet visited, String[] headers, Automaton automaton) { if (visited.get(index)) { // node already visited return headers[index]; } String middle = toString(index,visited,headers,automaton); State state = automaton.states[index]; switch(state.kind) { case K_UNION: case K_PROPERTY: case K_FUNCTION: case K_METHOD: return "(" + middle + ")"; default: return middle; } } /** * The following method traverses the graph using a depth-first * search to identify nodes which are "loop headers". That is, they are the * target of one or more recursive edges in the graph. * * @param index * --- the index to search from. * @param visited * --- the set of vertices already visited. * @param onStack * --- the set of nodes currently on the DFS path from the root. * @param headers * --- header nodes discovered during this search are set to true * in this bitset. * @param automaton * --- the automaton we're traversing. */ private final static void findHeaders(int index, BitSet visited, BitSet onStack, BitSet headers, Automaton automaton) { if(visited.get(index)) { // node already visited if(onStack.get(index)) { headers.set(index); } return; } onStack.set(index); visited.set(index); State state = automaton.states[index]; for(int child : state.children) { findHeaders(child,visited,onStack,headers,automaton); } onStack.set(index,false); } private static final char[] headers = { 'X','Y','Z','U','V','W','L','M','N','O','P','Q','R','S','T'}; private static String headerTitle(int count) { String r = Character.toString(headers[count%headers.length]); int n = count / headers.length; if(n > 0) { return r + n; } else { return r; } } public static final byte K_VOID = 0; public static final byte K_ANY = 1; public static final byte K_META = 2; public static final byte K_NULL = 3; public static final byte K_BOOL = 4; public static final byte K_BYTE = 5; public static final byte K_INT = 7; public static final byte K_ARRAY = 12; public static final byte K_REFERENCE = 14; public static final byte K_RECORD = 15; public static final byte K_UNION = 16; public static final int K_INTERSECTION = 17; public static final byte K_NEGATION = 18; public static final byte K_FUNCTION = 19; public static final byte K_METHOD = 20; public static final byte K_NOMINAL = 21; public static final byte K_PROPERTY = 22; }