Java程序  |  1727行  |  66.65 KB

package annotations.io;

/*>>>
import org.checkerframework.checker.nullness.qual.*;
*/

import static java.io.StreamTokenizer.TT_EOF;
import static java.io.StreamTokenizer.TT_NUMBER;
import static java.io.StreamTokenizer.TT_WORD;

import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;

import com.sun.source.tree.Tree.Kind;
import com.sun.tools.javac.code.TypeAnnotationPosition;

import annotations.Annotation;
import annotations.AnnotationBuilder;
import annotations.AnnotationFactory;
import annotations.Annotations;
import annotations.ArrayBuilder;
import annotations.el.ABlock;
import annotations.el.AClass;
import annotations.el.ADeclaration;
import annotations.el.AElement;
import annotations.el.AExpression;
import annotations.el.AField;
import annotations.el.AMethod;
import annotations.el.AScene;
import annotations.el.ATypeElement;
import annotations.el.ATypeElementWithType;
import annotations.el.AnnotationDef;
import annotations.el.BoundLocation;
import annotations.el.InnerTypeLocation;
import annotations.el.LocalLocation;
import annotations.el.RelativeLocation;
import annotations.el.TypeIndexLocation;
import annotations.field.AnnotationAFT;
import annotations.field.AnnotationFieldType;
import annotations.field.ArrayAFT;
import annotations.field.BasicAFT;
import annotations.field.ClassTokenAFT;
import annotations.field.EnumAFT;
import annotations.field.ScalarAFT;
import annotations.util.coll.VivifyingMap;

import plume.ArraysMDE;
import plume.FileIOException;
import plume.Pair;

import type.ArrayType;
import type.BoundedType;
import type.BoundedType.BoundKind;
import type.DeclaredType;
import type.Type;

/**
 * IndexFileParser provides static methods
 * {@link #parse(LineNumberReader, AScene)},
 * {@link #parseFile(String, AScene)}, and
 * {@link #parseString(String, AScene)}.
 * Each of these parses an index file into a {@link AScene}.
 * <p>
 *
 * If there are any problems, it throws a ParseException internally, or a
 * FileIOException externally.
 */
public final class IndexFileParser {

    private static final String[] typeSelectors = { "bound", "identifier",
        "type", "typeAlternative", "typeArgument", "typeParameter",
        "underlyingType" };

    private static boolean abbreviate = true;

    // The input
    private final StreamTokenizer st;

    // The output
    private final AScene scene;

    private String curPkgPrefix;

    /**
     * Holds definitions we've seen so far.  Maps from annotation name to
     * the definition itself.  Maps from both the qualified name and the
     * unqualified name.  If the unqualified name is not unique, it maps
     * to null and the qualified name should be used instead. */
    private final HashMap<String, AnnotationDef> defs;

    public static void setAbbreviate(boolean b) {
      abbreviate = b;
    }

    private int expectNonNegative(int i) throws ParseException {
        if (i >= 0) {
            return i;
        } else {
            throw new ParseException("Expected a nonnegative integer, got " + st);
        }
    }

    /** True if the next thing from st is the given character. */
    private boolean checkChar(char c) {
        return st.ttype == c;
    }

    /** True if the next thing from st is the given string token. */
    private boolean checkKeyword(String s) {
        return st.ttype == TT_WORD && st.sval.equals(s);
    }

    /**
     * Return true if the next thing to be read from st is the given string.
     * In that case, also read past the given string.
     * If the result is false, reads nothing from st.
     */
    private boolean matchChar(char c) throws IOException {
        if (checkChar(c)) {
            st.nextToken();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Return true if the next thing to be read from st is the given string.
     * In that case, also read past the given string.
     * If the result is false, reads nothing from st.
     */
    private boolean matchKeyword(String s) throws IOException {
        if (checkKeyword(s)) {
            st.nextToken();
            return true;
        } else {
            return false;
        }
    }

    /** Reads from st.  If the result is not c, throws an exception. */
    private void expectChar(char c) throws IOException, ParseException {
        if (! matchChar(c)) {
            // Alternately, could use st.toString().
            String found;
            switch (st.ttype) {
            case StreamTokenizer.TT_WORD: found = st.sval; break;
            case StreamTokenizer.TT_NUMBER: found = "" + st.nval; break;
            case StreamTokenizer.TT_EOL: found = "end of line"; break;
            case StreamTokenizer.TT_EOF: found = "end of file"; break;
            default: found = "'" + ((char) st.ttype) + "'"; break;
            }
            throw new ParseException("Expected '" + c + "', found " + found);
        }
    }

    /** Reads from st.  If the result is not s, throws an exception. */
    private void expectKeyword(String s) throws IOException,
            ParseException {
        if (! matchKeyword(s)) {
            throw new ParseException("Expected `" + s + "'");
        }
    }

    private static final Set<String> knownKeywords;
    static {
        String[] knownKeywords_array =
                { "abstract", "assert", "boolean", "break", "byte", "case",
                        "catch", "char", "class", "const", "continue",
                        "default", "do", "double", "else", "enum", "extends",
                        "false", "final", "finally", "float", "for", "if",
                        "goto", "implements", "import", "instanceof", "int",
                        "interface", "long", "native", "new", "null",
                        "package", "private", "protected", "public", "return",
                        "short", "static", "strictfp", "super", "switch",
                        "synchronized", "this", "throw", "throws", "transient",
                        "true", "try", "void", "volatile", "while", };
        knownKeywords = new LinkedHashSet<String>();
        Collections.addAll(knownKeywords, knownKeywords_array);
    }

    private boolean isValidIdentifier(String x) {
        if (x.length() == 0 || !Character.isJavaIdentifierStart(x.charAt(0))
                || knownKeywords.contains(x))
            return false;
        for (int i = 1; i < x.length(); i++) {
            if (!Character.isJavaIdentifierPart(x.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    private String checkIdentifier() {
        if (st.sval == null) {
            return null;
        } else {
            String val = st.sval;
            if (st.ttype == TT_WORD && isValidIdentifier(val)) {
                return st.sval;
            } else {
                return null;
            }
        }
    }

    private String matchIdentifier() throws IOException {
        String x = checkIdentifier();
        if (x != null) {
            st.nextToken();
            return x;
        } else {
            return null;
        }
    }

    private String expectIdentifier() throws IOException, ParseException {
        String id = matchIdentifier();
        if (id == null) { throw new ParseException("Expected an identifier"); }
        return id;
    }

    private String checkPrimitiveType() {
        if (st.sval == null) {
            return null;
        } else {
            String val = st.sval;
            if (st.ttype == TT_WORD && primitiveTypes.containsKey(val)) {
                return st.sval;
            } else {
                return null;
            }
        }
    }

    private String matchPrimitiveType() throws IOException {
        String x = checkPrimitiveType();
        if (x != null) {
            st.nextToken();
            return x;
        } else {
            return null;
        }
    }

    // an identifier, or a sequence of dot-separated identifiers
    private String expectQualifiedName() throws IOException, ParseException {
        String name = expectIdentifier();
        while (matchChar('.')) {
            name += '.' + expectIdentifier();
        }
        return name;
    }

    private int checkNNInteger() {
        if (st.ttype == TT_NUMBER) {
            int x = (int) st.nval;
            if (x == st.nval && x >= -1) // shouldn't give us a huge number
                return x;
        }
        return -1;
    }

    private int matchNNInteger() throws IOException {
        int x = checkNNInteger();
        if (x >= -1) {
            st.nextToken();
            return x;
        } else {
            return -1;
        }
    }

    // Mapping from primitive types and void to their corresponding
    // class objects. Class.forName doesn't directly support these.
    // Using this map we can go from "void.class" to the correct
    // Class object.
    private static final Map<String, Class<?>> primitiveTypes;
    static {
        Map<String, Class<?>> pt = new LinkedHashMap<String, Class<?>>();
        pt.put("byte", byte.class);
        pt.put("short", short.class);
        pt.put("int", int.class);
        pt.put("long", long.class);
        pt.put("float", float.class);
        pt.put("double", double.class);
        pt.put("char", char.class);
        pt.put("boolean", boolean.class);
        pt.put("void", void.class);
        primitiveTypes = pt;
    }

    /** Parse scalar annotation value. */
    // HMMM can a (readonly) Integer be casted to a writable Object?
    private Object parseScalarAFV(ScalarAFT aft) throws IOException, ParseException {
        if (aft instanceof BasicAFT) {
            Object val;
            BasicAFT baft = (BasicAFT) aft;
            Class<?> type = baft.type;
            if (type == boolean.class) {
                if (matchKeyword("true")) {
                    val = true;
                } else if (matchKeyword("false")) {
                    val = false;
                } else {
                    throw new ParseException("Expected `true' or `false'");
                }
            } else if (type == char.class) {
                if (st.ttype == '\'' && st.sval.length() == 1) {
                    val = st.sval.charAt(0);
                } else {
                    throw new ParseException("Expected a character literal");
                }
                st.nextToken();
            } else if (type == String.class) {
                if (st.ttype == '"') {
                    val = st.sval;
                } else {
                    throw new ParseException("Expected a string literal");
                }
                st.nextToken();
            } else {
                if (st.ttype == TT_NUMBER) {
                    double n = st.nval;
                    // TODO validate the literal better
                    // HMMM StreamTokenizer can't handle all floating point
                    // numbers; in particular, scientific notation is a problem
                    if (type == byte.class) {
                        val = (byte) n;
                    } else if (type == short.class) {
                        val = (short) n;
                    } else if (type == int.class) {
                        val = (int) n;
                    } else if (type == long.class) {
                        val = (long) n;
                    } else if (type == float.class) {
                        val = (float) n;
                    } else if (type == double.class) {
                        val = n;
                    } else {
                        throw new AssertionError();
                    }
                    st.nextToken();
                } else {
                    throw new ParseException(
                            "Expected a number literal");
                }
            }
            assert aft.isValidValue(val);
            return val;
        } else if (aft instanceof ClassTokenAFT) {
            // Expect the class name in the format that Class.forName accepts,
            // which is some very strange format.
            // Example inputs followed by their Java source ".class" equivalent:
            //   [[I.class      for int[][].class
            //   [java.util.Map for Map[].class
            //   java.util.Map  for Map.class
            // Have to use fully-qualified names, i.e. "Object" alone won't work.
            // Also note use of primitiveTypes map for primitives and void.
            int arrays = 0;
            StringBuilder type = new StringBuilder();
            while (matchChar('[')) {
                // Array dimensions as prefix
                ++arrays;
            }
            while (!matchKeyword("class")) {
                if (st.ttype >= 0) {
                    type.append((char) st.ttype);
                } else if (st.ttype == TT_WORD) {
                    type.append(st.sval);
                } else {
                    throw new ParseException("Found something that doesn't belong in a signature");
                }
                st.nextToken();
            }

            // Drop the '.' before the "class"
            type.deleteCharAt(type.length()-1);
            // expectKeyword("class");

            // Add arrays as prefix in the type.
            while (arrays-->0) {
                type.insert(0, '[');
            }

            try {
                String sttype = type.toString();
                Class<?> tktype;
                if (primitiveTypes.containsKey(sttype)) {
                    tktype = primitiveTypes.get(sttype);
                } else {
                    tktype = Class.forName(sttype);
                }
                assert aft.isValidValue(tktype);
                return tktype;
            } catch (ClassNotFoundException e) {
                throw new ParseException("Could not load class: " + type, e);
            }
        } else if (aft instanceof EnumAFT) {
            String name = expectQualifiedName();
            assert aft.isValidValue(name);
            return name;
        } else if (aft instanceof AnnotationAFT) {
            AnnotationAFT aaft = (AnnotationAFT) aft;
            AnnotationDef d = parseAnnotationHead();
            if (! d.name.equals(aaft.annotationDef.name)) {
                throw new ParseException("Got an " + d.name
                    + " subannotation where an " + aaft.annotationDef.name
                    + " was expected");
            }
            AnnotationBuilder ab = AnnotationFactory.saf.beginAnnotation(d);
            // interested in this annotation,
            // so should be interested in subannotations
            assert ab != null;
            AnnotationBuilder ab2 = (AnnotationBuilder) ab;
            Annotation suba = parseAnnotationBody(d, ab2);
            assert aft.isValidValue(suba);
            return suba;
        } else {
            throw new AssertionError("IndexFileParser.parseScalarAFV: unreachable code.");
        }
    }

    private void parseAndAddArrayAFV(ArrayAFT aaft, ArrayBuilder arrb) throws IOException, ParseException {
        ScalarAFT comp;
        if (aaft.elementType != null) {
            comp = aaft.elementType;
        } else {
            throw new IllegalArgumentException("array AFT has null elementType");
        }
        if (matchChar('{')) {
            // read an array
            while (!matchChar('}')) {
                arrb.appendElement(parseScalarAFV(comp));
                if (!checkChar('}')) {
                    expectChar(',');
                }
            }
        } else {
            // not an array, so try reading just one value as an array
            arrb.appendElement(parseScalarAFV(comp));
        }
        arrb.finish();
    }

    // parses a field such as "f1=5" in "@A(f1=5, f2=10)".
    private void parseAnnotationField(AnnotationDef d, AnnotationBuilder ab) throws IOException, ParseException {
        String fieldName;
        if (d.fieldTypes.size() == 1
            && d.fieldTypes.containsKey("value")) {
            fieldName = "value";
            if (matchKeyword("value")) {
                expectChar('=');
            }
        } else {
            fieldName = expectIdentifier();
            expectChar('=');
        }
        // HMMM let's hope the builder checks for duplicate fields
        // because we can't do it any more
        AnnotationFieldType aft1 = d.fieldTypes.get(fieldName);
        if (aft1 == null) {
            throw new ParseException("The annotation type " + d.name
                + " has no field called " + fieldName);
        }
        AnnotationFieldType aft = (AnnotationFieldType) aft1;
        if (aft instanceof ArrayAFT) {
            ArrayAFT aaft = (ArrayAFT) aft;
            if (aaft.elementType == null) {
                // Array of unknown element type--must be zero-length
                expectChar('{');
                expectChar('}');
                ab.addEmptyArrayField(fieldName);
            } else {
                parseAndAddArrayAFV(aaft, ab.beginArrayField(fieldName, aaft));
            }
        } else if (aft instanceof ScalarAFT) {
            ScalarAFT saft = (ScalarAFT) aft;
            Object value = parseScalarAFV(saft);
            ab.addScalarField(fieldName, saft, value);
        } else {
            throw new AssertionError();
        }
    }

    // reads the "@A" part of an annotation such as "@A(f1=5, f2=10)".
    private AnnotationDef parseAnnotationHead() throws IOException,
            ParseException {
        expectChar('@');
        String name = expectQualifiedName();
        AnnotationDef d = defs.get(name);
        if (d == null) {
            // System.err.println("No definition for annotation type " + name);
            // System.err.printf("  defs contains %d entries%n", defs.size());
            // for (Map.Entry<String,AnnotationDef> entry : defs.entrySet()) {
            //    System.err.printf("    defs entry: %s => %s%n", entry.getKey(), entry.getValue());
            // }
            throw new ParseException("No definition for annotation type " + name);
        }
        return d;
    }

    private Annotation parseAnnotationBody(AnnotationDef d, AnnotationBuilder ab) throws IOException, ParseException {
        if (matchChar('(')) {
            parseAnnotationField(d, ab);
            while (matchChar(',')) {
                parseAnnotationField(d, ab);
            }
            expectChar(')');
        }
        Annotation ann = ab.finish();
        if (! ann.def.equals(d)) {
            throw new ParseException(
                "parseAnnotationBody: Annotation def isn't as it should be.\n" + d + "\n" + ann.def);
        }
        if (ann.def().fieldTypes.size() != d.fieldTypes.size()) {
            throw new ParseException(
                "At least one annotation field is missing");
        }
        return ann;
    }

    private void parseAnnotations(AElement e)
            throws IOException, ParseException {
        while (checkChar('@')) {
            AnnotationDef d = parseAnnotationHead();
            AnnotationBuilder ab = AnnotationFactory.saf.beginAnnotation(d);
            if (ab == null) {
                // don't care about the result
                // but need to skip over it anyway
                @SuppressWarnings("unused")
                Object trash = parseAnnotationBody(d, AnnotationFactory.saf
                        .beginAnnotation(d));
            } else {
                Annotation a = parseAnnotationBody(d, ab);
                for (Annotation other : e.tlAnnotationsHere) {
                    if (a.def.name.equals(other.def.name)) {
                        System.err.println(
                                "WARNING: duplicate annotation of type "
                                        + a.def().name);
                        continue;
                    }
                }
                Annotation tla = a;
                if (! tla.def.equals(d)) {
                    throw new ParseException("Bad def");
                }
                e.tlAnnotationsHere.add(tla);
            }
        }
    }

    private ScalarAFT parseScalarAFT() throws IOException, ParseException {
        for (BasicAFT baft : BasicAFT.bafts.values()) {
            if (matchKeyword(baft.toString())) {
                return baft;
            }
        }
        // wasn't a BasicAFT
        if (matchKeyword("Class")) {
            return ClassTokenAFT.ctaft/* dumpParameterization() */;
        } else if (matchKeyword("enum")) {
            String name = expectQualifiedName();
            if (abbreviate) {
              int i = name.lastIndexOf('.');
              if (i >= 0) {
                String baseName = name.substring(i+1);
                Set<String> set1 = scene.imports.get(name);
                Set<String> set2 = scene.imports.get(baseName);
                if (set1 == null) {
                  set1 = new TreeSet<String>();
                  scene.imports.put(name, set1);
                }
                if (set2 == null) {
                  set2 = new TreeSet<String>();
                  scene.imports.put(name, set2);
                }
                set1.add(name);
                set2.add(name);
                name = baseName;
              }
            }
            return new EnumAFT(name);
        } else if (matchKeyword("annotation-field")) {
            String name = expectQualifiedName();
            AnnotationDef ad = defs.get(name);
            if (ad == null) {
                throw new ParseException("Annotation type " + name + " used as a field before it is defined");
            }
            return new AnnotationAFT((AnnotationDef) ad);
        } else {
            throw new ParseException(
                    "Expected the beginning of an annotation field type: "
                    + "a primitive type, `String', `Class', `enum', or `annotation-field'. Got '"
                    + st.sval + "'.");
        }
    }

    private AnnotationFieldType parseAFT() throws IOException,
            ParseException {
        if (matchKeyword("unknown")) {
            // Handle unknown[]; see AnnotationBuilder#addEmptyArrayField
            expectChar('[');
            expectChar(']');
            return new ArrayAFT(null);
        }
        ScalarAFT baseAFT = parseScalarAFT();
        // only one level of array is permitted
        if (matchChar('[')) {
            expectChar(']');
            return new ArrayAFT(baseAFT);
        } else {
            return baseAFT;
        }
    }

    private void parseAnnotationDef() throws IOException, ParseException {
        expectKeyword("annotation");

        expectChar('@');
        String basename = expectIdentifier();
        String fullName = curPkgPrefix + basename;

        AnnotationDef ad = new AnnotationDef(fullName);
        expectChar(':');
        parseAnnotations(ad);

        Map<String, AnnotationFieldType> fields =
                new LinkedHashMap<String, AnnotationFieldType>();

        // yuck; it would be nicer to do a positive match
        while (st.ttype != TT_EOF && !checkKeyword("annotation")
               && !checkKeyword("class") && !checkKeyword("package")) {
            AnnotationFieldType type = parseAFT();
            String name = expectIdentifier();
            if (fields.containsKey(name)) {
                throw new ParseException("Duplicate definition of field "
                                         + name);
            }
            fields.put(name, type);
        }

        ad.setFieldTypes(fields);

        // Now add the definition to the map of all definitions.
        addDef(ad, basename);

    }

    // Add the definition to the map of all definitions.
    // also see addDef(AnnotationDef, String).
    public void addDef(AnnotationDef ad) throws ParseException {
        String basename = ad.name;
        int dotPos = basename.lastIndexOf('.');
        if (dotPos != -1) {
            basename = basename.substring(dotPos + 1);
        }
        addDef(ad, basename);
    }

    // Add the definition to the map of all definitions.
    public void addDef(AnnotationDef ad, String basename) throws ParseException {
        // System.out.println("addDef:" + ad);

        if (defs.containsKey(ad.name)) {
            // TODO:  permit identical re-definition
            System.err.println("Duplicate definition of annotation type " + ad.name);
        }
        defs.put(ad.name, ad);
        // Add short name; but if it's already there, remove it to avoid ambiguity.
        if (! basename.equals(ad.name)) {
            if (defs.containsKey(basename)) {
                // not "defs.remove(basename)" because then a subsequent
                // one could get added, which would be wrong.
                defs.put(basename, null);
            } else {
                defs.put(basename, ad);
            }
        }
    }


    private void parseInnerTypes(ATypeElement e)
            throws IOException, ParseException {
        parseInnerTypes(e, 0);
    }

    private void parseInnerTypes(ATypeElement e, int offset)
            throws IOException, ParseException {
        while (matchKeyword("inner-type")) {
            ArrayList<Integer> locNumbers =
                    new ArrayList<Integer>();
            locNumbers.add(offset + expectNonNegative(matchNNInteger()));
            // TODO: currently, we simply read the binary representation.
            // Should we read a higher-level format?
            while (matchChar(',')) {
                locNumbers.add(expectNonNegative(matchNNInteger()));
            }
            InnerTypeLocation loc;
            try {
                loc = new InnerTypeLocation(TypeAnnotationPosition.getTypePathFromBinary(locNumbers));
            } catch (AssertionError ex) {
                throw new ParseException(ex.getMessage(), ex);
            }
            AElement it = e.innerTypes.vivify(loc);
            expectChar(':');
            parseAnnotations(it);
        }
    }

    private void parseBounds(VivifyingMap<BoundLocation, ATypeElement> bounds)
        throws IOException, ParseException {
        while (checkKeyword("typeparam") || checkKeyword("bound")) {
            if (matchKeyword("typeparam")) {
                int paramIndex = expectNonNegative(matchNNInteger());
                BoundLocation bl = new BoundLocation(paramIndex, -1);
                ATypeElement b = bounds.vivify(bl);
                expectChar(':');
                parseAnnotations(b);
                // does this make sense?
                parseInnerTypes(b);
            } else if (matchKeyword("bound")) {
                // expectChar(',');
                int paramIndex = expectNonNegative(matchNNInteger());
                expectChar('&');
                int boundIndex = expectNonNegative(matchNNInteger());
                BoundLocation bl = new BoundLocation(paramIndex, boundIndex);
                ATypeElement b = bounds.vivify(bl);
                expectChar(':');
                parseAnnotations(b);
                // does this make sense?
                parseInnerTypes(b);
            } else {
                throw new Error("impossible");
            }
        }
    }

    private void parseExtends(AClass cls) throws IOException, ParseException {
        expectKeyword("extends");
        TypeIndexLocation idx = new TypeIndexLocation(-1);
        ATypeElement ext = cls.extendsImplements.vivify(idx);
        expectChar(':');
        parseAnnotations(ext);
        parseInnerTypes(ext);
    }

    private void parseImplements(AClass cls) throws IOException, ParseException {
        expectKeyword("implements");
        int implIndex = expectNonNegative(matchNNInteger());
        TypeIndexLocation idx = new TypeIndexLocation(implIndex);
        ATypeElement impl = cls.extendsImplements.vivify(idx);
        expectChar(':');
        parseAnnotations(impl);
        parseInnerTypes(impl);
    }

    private void parseField(AClass c) throws IOException,
            ParseException {
        expectKeyword("field");
        String name = expectIdentifier();
        AField f = c.fields.vivify(name);

        expectChar(':');
        parseAnnotations(f);
        if (checkKeyword("type") && matchKeyword("type")) {
            expectChar(':');
            parseAnnotations(f.type);
            parseInnerTypes(f.type);
        }

        f.init = c.fieldInits.vivify(name);
        parseExpression(f.init);
        parseASTInsertions(f);
    }

    private void parseStaticInit(AClass c) throws IOException,
            ParseException {
        expectKeyword("staticinit");
        expectChar('*');
        int blockIndex = expectNonNegative(matchNNInteger());
        expectChar(':');

        ABlock staticinit = c.staticInits.vivify(blockIndex);
        parseBlock(staticinit);
    }

    private void parseInstanceInit(AClass c) throws IOException,
            ParseException {
        expectKeyword("instanceinit");
        expectChar('*');
        int blockIndex = expectNonNegative(matchNNInteger());
        expectChar(':');

        ABlock instanceinit = c.instanceInits.vivify(blockIndex);
        parseBlock(instanceinit);
    }

    private void parseMethod(AClass c) throws IOException,
            ParseException {
        expectKeyword("method");
        // special case: method could be <init> or <clinit>
        String key;
        if (matchChar('<')) {
            String basename = expectIdentifier();
            if (!(basename.equals("init") || basename.equals("clinit"))) {
                throw new ParseException(
                    "The only special methods allowed are <init> and <clinit>");
            }
            expectChar('>');
            key = "<" + basename + ">";
        } else {
            key = expectIdentifier();
            // too bad className is private in AClass and thus must be
            // extracted from what toString() returns
            if (Pattern.matches("AClass: (?:[^. ]+\\.)*" + key,
                    c.toString())) {  // ugh
                key = "<init>";
            }
        }

        expectChar('(');
        key += '(';
        while (!matchChar(':')) {
            if (st.ttype >= 0) {
                key += st.ttype == 46 ? '/' :(char) st.ttype;
            } else if (st.ttype == TT_WORD) {
                key += st.sval;
            } else {
                throw new ParseException("Found something that doesn't belong in a signature");
            }
            st.nextToken();
        }

        AMethod m = c.methods.vivify(key);
        parseAnnotations(m);
        parseMethod(m);
    }

    private void parseMethod(AMethod m) throws IOException, ParseException {
        parseBounds(m.bounds);

        // Permit return value, receiver, and parameters in any order.
        while (checkKeyword("return") || checkKeyword("receiver") || checkKeyword("parameter")) {
            if (matchKeyword("return")) {
                expectChar(':');
                parseAnnotations(m.returnType);
                parseInnerTypes(m.returnType);
            } else if (matchKeyword("parameter")) {
                // make "#" optional
                if (checkChar('#')) {
                    matchChar('#');
                }
                int idx = expectNonNegative(matchNNInteger());
                AField p = m.parameters.vivify(idx);
                expectChar(':');
                parseAnnotations(p);
                if (checkKeyword("type") && matchKeyword("type")) {
                    expectChar(':');
                    parseAnnotations(p.type);
                    parseInnerTypes(p.type);
                }
            } else if (matchKeyword("receiver")) {
                expectChar(':');
                parseAnnotations(m.receiver.type);
                parseInnerTypes(m.receiver.type);
            } else {
                throw new Error("This can't happen");
            }
        }

        parseBlock(m.body);
        parseASTInsertions(m);
    }

    private void parseLambda(AMethod m) throws IOException, ParseException {
        while (checkKeyword("parameter")) {
            matchKeyword("parameter");
            // make "#" optional
            if (checkChar('#')) {
                matchChar('#');
            }
            int idx = expectNonNegative(matchNNInteger());
            AField p = m.parameters.vivify(idx);
            expectChar(':');
            parseAnnotations(p);
            if (checkKeyword("type") && matchKeyword("type")) {
                expectChar(':');
                parseAnnotations(p.type);
                parseInnerTypes(p.type);
            }
        }

        // parseBlock(m.body, true);
        parseASTInsertions(m);
    }

    private void parseBlock(ABlock bl) throws IOException,
            ParseException {
        boolean matched = true;

        while (matched) {
            matched = false;

            while (checkKeyword("local")) {
                matchKeyword("local");
                matched = true;
                LocalLocation loc;
                if (checkNNInteger() != -1) {
                    // the local variable is specified by bytecode index/range
                    int index = expectNonNegative(matchNNInteger());
                    expectChar('#');
                    int scopeStart = expectNonNegative(matchNNInteger());
                    expectChar('+');
                    int scopeLength = expectNonNegative(matchNNInteger());
                    loc = new LocalLocation(index, scopeStart, scopeLength);
                } else {
                    // look for a valid identifier for the local variable
                    String lvar = expectIdentifier();
                    int varIndex;
                    if (checkChar('*')) {
                        expectChar('*');
                        varIndex = expectNonNegative(matchNNInteger());
                    } else {
                        // default the variable index to 0, the most common case
                        varIndex = 0;
                    }
                    loc = new LocalLocation(lvar, varIndex);
                }
                AField l = bl.locals.vivify(loc);
                expectChar(':');
                parseAnnotations(l);
                if (checkKeyword("type") && matchKeyword("type")) {
                    expectChar(':');
                    parseAnnotations(l.type);
                    parseInnerTypes(l.type);
                }
            }
            matched = parseExpression(bl) || matched;
        }
    }

    private boolean parseExpression(AExpression exp) throws IOException,
            ParseException {
        boolean matched = true;
        boolean evermatched = false;

        while (matched) {
            matched = false;

            while (checkKeyword("typecast")) {
                matchKeyword("typecast");
                matched = true;
                evermatched = true;
                RelativeLocation loc;
                if (checkChar('#')) {
                    expectChar('#');
                    int offset = expectNonNegative(matchNNInteger());
                    int type_index = 0;
                    if (checkChar(',')) {
                        expectChar(',');
                        type_index = expectNonNegative(matchNNInteger());
                    }
                    loc = RelativeLocation.createOffset(offset, type_index);
                } else {
                    expectChar('*');
                    int index = expectNonNegative(matchNNInteger());
                    int type_index = 0;
                    if (checkChar(',')) {
                        expectChar(',');
                        type_index = expectNonNegative(matchNNInteger());
                    }
                    loc = RelativeLocation.createIndex(index, type_index);
                }
                ATypeElement t = exp.typecasts.vivify(loc);
                expectChar(':');
                parseAnnotations(t);
                parseInnerTypes(t);
            }
            while (checkKeyword("instanceof")) {
                matchKeyword("instanceof");
                matched = true;
                evermatched = true;
                RelativeLocation loc;
                if (checkChar('#')) {
                    expectChar('#');
                    int offset = expectNonNegative(matchNNInteger());
                    loc = RelativeLocation.createOffset(offset, 0);
                } else {
                    expectChar('*');
                    int index = expectNonNegative(matchNNInteger());
                    loc = RelativeLocation.createIndex(index, 0);
                }
                ATypeElement i = exp.instanceofs.vivify(loc);
                expectChar(':');
                parseAnnotations(i);
                parseInnerTypes(i);
            }
            while (checkKeyword("new")) {
                matchKeyword("new");
                matched = true;
                evermatched = true;
                RelativeLocation loc;
                if (checkChar('#')) {
                    expectChar('#');
                    int offset = expectNonNegative(matchNNInteger());
                    loc = RelativeLocation.createOffset(offset, 0);
                } else {
                    expectChar('*');
                    int index = expectNonNegative(matchNNInteger());
                    loc = RelativeLocation.createIndex(index, 0);
                }
                ATypeElement n = exp.news.vivify(loc);
                expectChar(':');
                parseAnnotations(n);
                parseInnerTypes(n);
            }
            while (checkKeyword("call")) {
                matchKeyword("call");
                matched = true;
                evermatched = true;
                int i;
                boolean isOffset = checkChar('#');
                expectChar(isOffset ? '#' : '*');
                i = expectNonNegative(matchNNInteger());
                expectChar(':');
                while (checkKeyword("typearg")) {
                    matchKeyword("typearg");
                    if (checkChar('#')) { matchChar('#'); }
                    int type_index = expectNonNegative(matchNNInteger());
                    RelativeLocation loc = isOffset
                            ? RelativeLocation.createOffset(i, type_index)
                            : RelativeLocation.createIndex(i, type_index);
                    ATypeElement t = exp.calls.vivify(loc);
                    expectChar(':');
                    parseAnnotations(t);
                    parseInnerTypes(t);
                }
            }
            while (checkKeyword("reference")) {
                matchKeyword("reference");
                matched = true;
                evermatched = true;
                ATypeElement t;
                RelativeLocation loc;
                int i;
                boolean isOffset = checkChar('#');
                if (isOffset) {
                    expectChar('#');
                    i = expectNonNegative(matchNNInteger());
                    loc = RelativeLocation.createOffset(i, 0);
                } else {
                    expectChar('*');
                    i = expectNonNegative(matchNNInteger());
                    loc = RelativeLocation.createIndex(i, 0);
                }
                expectChar(':');
                t = exp.refs.vivify(loc);
                parseAnnotations(t);
                parseInnerTypes(t);
                while (checkKeyword("typearg")) {
                    matchKeyword("typearg");
                    if (checkChar('#')) { matchChar('#'); }
                    int type_index = expectNonNegative(matchNNInteger());
                    loc = isOffset
                        ? RelativeLocation.createOffset(i, type_index)
                        : RelativeLocation.createIndex(i, type_index);
                    t = exp.refs.vivify(loc);
                    expectChar(':');
                    parseAnnotations(t);
                    parseInnerTypes(t);
                }
            }
            while (checkKeyword("lambda")) {
                matchKeyword("lambda");
                matched = true;
                evermatched = true;
                RelativeLocation loc;
                if (checkChar('#')) {
                    expectChar('#');
                    int offset = expectNonNegative(matchNNInteger());
                    int type_index = 0;
                    if (checkChar(',')) {
                        expectChar(',');
                        type_index = expectNonNegative(matchNNInteger());
                    }
                    loc = RelativeLocation.createOffset(offset, type_index);
                } else {
                    expectChar('*');
                    int index = expectNonNegative(matchNNInteger());
                    int type_index = 0;
                    if (checkChar(',')) {
                        expectChar(',');
                        type_index = expectNonNegative(matchNNInteger());
                    }
                    loc = RelativeLocation.createIndex(index, type_index);
                }
                AMethod m = exp.funs.vivify(loc);
                expectChar(':');
                // parseAnnotations(m);
                parseLambda(m);
                // parseMethod(m);
            }
        }
        return evermatched;
    }

    private static boolean isTypeSelector(String selector) {
      return Arrays.<String>binarySearch(typeSelectors, selector, Collator.getInstance()) >= 0;
    }

    private static boolean selectsExpression(ASTPath astPath) {
        int n = astPath.size();
        if (--n >= 0) {
            ASTPath.ASTEntry entry = astPath.get(n);
            while (--n >= 0 && entry.getTreeKind() == Kind.MEMBER_SELECT
                    && entry.childSelectorIs(ASTPath.EXPRESSION)) {
              entry = astPath.get(n);
            }
            return !isTypeSelector(entry.getChildSelector());
        }
        return false;
    }

    private boolean parseASTInsertions(ADeclaration decl)
            throws IOException, ParseException {
        boolean matched = false;
        while (checkKeyword("insert-annotation")) {
            matched = true;
            matchKeyword("insert-annotation");
            ASTPath astPath = parseASTPath();
            expectChar(':');
            // if path doesn't indicate a type, a cast must be generated
            if (selectsExpression(astPath)) {
                ATypeElementWithType i = decl.insertTypecasts.vivify(astPath);
                parseAnnotations(i);
                i.setType(new DeclaredType());
                parseInnerTypes(i);
            } else {
                // astPath = fixNewArrayType(astPath);  // handle special case
                // ATypeElement i = decl.insertAnnotations.vivify(astPath);
                // parseAnnotations(i);
                // parseInnerTypes(i);
                int offset = 0;
                Pair<ASTPath, InnerTypeLocation> pair =
                        splitNewArrayType(astPath);  // handle special case
                ATypeElement i;
                if (pair == null) {
                    i = decl.insertAnnotations.vivify(astPath);
                } else {
                    i = decl.insertAnnotations.vivify(pair.a);
                    if (pair.b != null) {
                        i = i.innerTypes.vivify(pair.b);
                        offset = pair.b.location.size();
                    }
                }
                parseAnnotations(i);
                parseInnerTypes(i, offset);
            }
        }
        while (checkKeyword("insert-typecast")) {
            matched = true;
            matchKeyword("insert-typecast");
            ASTPath astPath = parseASTPath();
            expectChar(':');
            ATypeElementWithType i = decl.insertTypecasts.vivify(astPath);
            parseAnnotations(i);
            Type type = parseType();
            i.setType(type);
            parseInnerTypes(i);
        }
        return matched;
    }

    // Due to the unfortunate representation of new array expressions,
    // ASTPaths to their inner array types break the usual rule that
    // an ASTPath corresponds to an AST node.  This method restores the
    // invariant by separating out the inner type information.
    private Pair<ASTPath, InnerTypeLocation> splitNewArrayType(ASTPath astPath) {
        ASTPath outerPath = astPath;
        InnerTypeLocation loc = null;
        int last = astPath.size() - 1;

        if (last > 0) {
            ASTPath.ASTEntry entry = astPath.get(last);
            if (entry.getTreeKind() == Kind.NEW_ARRAY && entry.childSelectorIs(ASTPath.TYPE)) {
                int a = entry.getArgument();
                if (a > 0) {
                    outerPath = astPath.getParentPath().extend(new ASTPath.ASTEntry(Kind.NEW_ARRAY, ASTPath.TYPE, 0));
            loc = new InnerTypeLocation(TypeAnnotationPosition.getTypePathFromBinary(Collections.nCopies(2 * a, 0)));
                }
            }
        }

        return Pair.of(outerPath, loc);
    }

    private ASTPath fixNewArrayType(ASTPath astPath) {
        ASTPath outerPath = astPath;
        int last = astPath.size() - 1;

        if (last > 0) {
            ASTPath.ASTEntry entry = astPath.get(last);
            if (entry.getTreeKind() == Kind.NEW_ARRAY && entry.childSelectorIs(ASTPath.TYPE)) {
                int a = entry.getArgument();
                outerPath = astPath.getParentPath().extend(new ASTPath.ASTEntry(Kind.NEW_ARRAY, ASTPath.TYPE, 0));
                while (--a >= 0) {
                    outerPath = outerPath.extend(new ASTPath.ASTEntry(Kind.ARRAY_TYPE, ASTPath.TYPE));
                }
            }
        }

        return outerPath;
    }

    /**
     * Parses an AST path.
     * @return the AST path
     */
    private ASTPath parseASTPath() throws IOException, ParseException {
        ASTPath astPath = ASTPath.empty().extend(parseASTEntry());
        while (matchChar(',')) {
            astPath = astPath.extend(parseASTEntry());
        }
        return astPath;
    }

    /**
     * Parses and returns the next AST entry.
     * @return a new AST entry
     * @throws ParseException if the next entry type is invalid
     */
    private ASTPath.ASTEntry parseASTEntry() throws IOException, ParseException {
        ASTPath.ASTEntry entry;
        if (matchKeyword("AnnotatedType")) {
            entry = newASTEntry(Kind.ANNOTATED_TYPE, new String[] {ASTPath.ANNOTATION, ASTPath.UNDERLYING_TYPE},
                    new String[] {ASTPath.ANNOTATION});
        } else if (matchKeyword("ArrayAccess")) {
            entry = newASTEntry(Kind.ARRAY_ACCESS, new String[] {ASTPath.EXPRESSION, ASTPath.INDEX});
        } else if (matchKeyword("ArrayType")) {
            entry = newASTEntry(Kind.ARRAY_TYPE, new String[] {ASTPath.TYPE});
        } else if (matchKeyword("Assert")) {
            entry = newASTEntry(Kind.ASSERT, new String[] {ASTPath.CONDITION, ASTPath.DETAIL});
        } else if (matchKeyword("Assignment")) {
            entry = newASTEntry(Kind.ASSIGNMENT, new String[] {ASTPath.VARIABLE, ASTPath.EXPRESSION});
        } else if (matchKeyword("Binary")) {
            // Always use Kind.PLUS for Binary
            entry = newASTEntry(Kind.PLUS, new String[] {ASTPath.LEFT_OPERAND, ASTPath.RIGHT_OPERAND});
        } else if (matchKeyword("Block")) {
            entry = newASTEntry(Kind.BLOCK, new String[] {ASTPath.STATEMENT}, new String[] {ASTPath.STATEMENT});
        } else if (matchKeyword("Case")) {
            entry = newASTEntry(Kind.CASE, new String[] {ASTPath.EXPRESSION, ASTPath.STATEMENT},
                    new String[] {ASTPath.STATEMENT});
        } else if (matchKeyword("Catch")) {
            entry = newASTEntry(Kind.CATCH, new String[] {ASTPath.PARAMETER, ASTPath.BLOCK});
        } else if (matchKeyword("Class")) {
            entry = newASTEntry(Kind.CLASS,
                    new String[] {ASTPath.BOUND, ASTPath.INITIALIZER, ASTPath.TYPE_PARAMETER},
                    new String[] {ASTPath.BOUND, ASTPath.INITIALIZER, ASTPath.TYPE_PARAMETER});
        } else if (matchKeyword("CompoundAssignment")) {
            // Always use Kind.PLUS_ASSIGNMENT for CompoundAssignment
            entry = newASTEntry(Kind.PLUS_ASSIGNMENT, new String[] {ASTPath.VARIABLE, ASTPath.EXPRESSION});
        } else if (matchKeyword("ConditionalExpression")) {
            entry = newASTEntry(Kind.CONDITIONAL_EXPRESSION,
                    new String[] {ASTPath.CONDITION, ASTPath.TRUE_EXPRESSION, ASTPath.FALSE_EXPRESSION});
        } else if (matchKeyword("DoWhileLoop")) {
            entry = newASTEntry(Kind.DO_WHILE_LOOP, new String[] {ASTPath.CONDITION, ASTPath.STATEMENT});
        } else if (matchKeyword("EnhancedForLoop")) {
            entry = newASTEntry(Kind.ENHANCED_FOR_LOOP, new String[] {ASTPath.VARIABLE, ASTPath.EXPRESSION, ASTPath.STATEMENT});
        } else if (matchKeyword("ExpressionStatement")) {
            entry = newASTEntry(Kind.EXPRESSION_STATEMENT, new String[] {ASTPath.EXPRESSION});
        } else if (matchKeyword("ForLoop")) {
            entry = newASTEntry(Kind.FOR_LOOP, new String[] {ASTPath.INITIALIZER, ASTPath.CONDITION, ASTPath.UPDATE, ASTPath.STATEMENT},
                    new String[] {ASTPath.INITIALIZER, ASTPath.UPDATE});
        } else if (matchKeyword("If")) {
            entry = newASTEntry(Kind.IF, new String[] {ASTPath.CONDITION, ASTPath.THEN_STATEMENT, ASTPath.ELSE_STATEMENT});
        } else if (matchKeyword("InstanceOf")) {
            entry = newASTEntry(Kind.INSTANCE_OF, new String[] {ASTPath.EXPRESSION, ASTPath.TYPE});
        } else if (matchKeyword("LabeledStatement")) {
            entry = newASTEntry(Kind.LABELED_STATEMENT, new String[] {ASTPath.STATEMENT});
        } else if (matchKeyword("LambdaExpression")) {
            entry = newASTEntry(Kind.LAMBDA_EXPRESSION, new String[] {ASTPath.PARAMETER, ASTPath.BODY},
                    new String[] {ASTPath.PARAMETER});
        } else if (matchKeyword("MemberReference")) {
            entry = newASTEntry(Kind.MEMBER_REFERENCE, new String[] {ASTPath.QUALIFIER_EXPRESSION, ASTPath.TYPE_ARGUMENT},
                    new String[] {ASTPath.TYPE_ARGUMENT});
        } else if (matchKeyword("MemberSelect")) {
            entry = newASTEntry(Kind.MEMBER_SELECT, new String[] {ASTPath.EXPRESSION});
        } else if (matchKeyword("Method")) {
            entry = newASTEntry(Kind.METHOD, new String[] {ASTPath.BODY, ASTPath.TYPE, ASTPath.PARAMETER, ASTPath.TYPE_PARAMETER},
                    new String[] {ASTPath.PARAMETER, ASTPath.TYPE_PARAMETER});
        } else if (matchKeyword("MethodInvocation")) {
            entry = newASTEntry(Kind.METHOD_INVOCATION, new String[] {ASTPath.TYPE_ARGUMENT, ASTPath.METHOD_SELECT, ASTPath.ARGUMENT},
                    new String[] {ASTPath.TYPE_ARGUMENT, ASTPath.ARGUMENT});
        } else if (matchKeyword("NewArray")) {
            entry = newASTEntry(Kind.NEW_ARRAY, new String[] {ASTPath.TYPE, ASTPath.DIMENSION, ASTPath.INITIALIZER},
                    new String[] {ASTPath.TYPE, ASTPath.DIMENSION, ASTPath.INITIALIZER});
        } else if (matchKeyword("NewClass")) {
            entry = newASTEntry(Kind.NEW_CLASS, new String[] {ASTPath.ENCLOSING_EXPRESSION, ASTPath.TYPE_ARGUMENT, ASTPath.IDENTIFIER, ASTPath.ARGUMENT, ASTPath.CLASS_BODY},
                    new String[] {ASTPath.TYPE_ARGUMENT, ASTPath.ARGUMENT});
        } else if (matchKeyword("ParameterizedType")) {
            entry = newASTEntry(Kind.PARAMETERIZED_TYPE, new String[] {ASTPath.TYPE, ASTPath.TYPE_ARGUMENT},
                    new String[] {ASTPath.TYPE_ARGUMENT});
        } else if (matchKeyword("Parenthesized")) {
            entry = newASTEntry(Kind.PARENTHESIZED, new String[] {ASTPath.EXPRESSION});
        } else if (matchKeyword("Return")) {
            entry = newASTEntry(Kind.RETURN, new String[] {ASTPath.EXPRESSION});
        } else if (matchKeyword("Switch")) {
            entry = newASTEntry(Kind.SWITCH, new String[] {ASTPath.EXPRESSION, ASTPath.CASE},
                    new String[] {ASTPath.CASE});
        } else if (matchKeyword("Synchronized")) {
            entry = newASTEntry(Kind.SYNCHRONIZED, new String[] {ASTPath.EXPRESSION, ASTPath.BLOCK});
        } else if (matchKeyword("Throw")) {
            entry = newASTEntry(Kind.THROW, new String[] {ASTPath.EXPRESSION});
        } else if (matchKeyword("Try")) {
            entry = newASTEntry(Kind.TRY, new String[] {ASTPath.BLOCK, ASTPath.CATCH, ASTPath.FINALLY_BLOCK},
                    new String[] {ASTPath.CATCH});
        } else if (matchKeyword("TypeCast")) {
            entry = newASTEntry(Kind.TYPE_CAST, new String[] {ASTPath.TYPE, ASTPath.EXPRESSION});
        } else if (matchKeyword("TypeParameter")) {
            entry = newASTEntry(Kind.TYPE_PARAMETER, new String[] {ASTPath.BOUND},
                    new String[] {ASTPath.BOUND});
        } else if (matchKeyword("Unary")) {
            // Always use Kind.UNARY_PLUS for Unary
            entry = newASTEntry(Kind.UNARY_PLUS, new String[] {ASTPath.EXPRESSION});
        } else if (matchKeyword("UnionType")) {
            entry = newASTEntry(Kind.UNION_TYPE, new String[] {ASTPath.TYPE_ALTERNATIVE},
                    new String[] {ASTPath.TYPE_ALTERNATIVE});
        } else if (matchKeyword("Variable")) {
            entry = newASTEntry(Kind.VARIABLE, new String[] {ASTPath.TYPE, ASTPath.INITIALIZER});
        } else if (matchKeyword("WhileLoop")) {
            entry = newASTEntry(Kind.WHILE_LOOP, new String[] {ASTPath.CONDITION, ASTPath.STATEMENT});
        } else if (matchKeyword("Wildcard")) {
            // Always use Kind.UNBOUNDED_WILDCARD for Wildcard
            entry = newASTEntry(Kind.UNBOUNDED_WILDCARD, new String[] {ASTPath.BOUND});
        } else {
            throw new ParseException("Invalid AST path type: " + st.sval);
        }
        return entry;
    }

    /**
     * Parses and constructs a new AST entry, where none of the child selections require
     * arguments. For example, the call:
     *
     * <pre>
     * {@code newASTEntry(Kind.WHILE_LOOP, new String[] {"condition", "statement"});</pre>
     *
     * constructs a while loop AST entry, where the valid child selectors are "condition" or
     * "statement".
     *
     * @param kind the kind of this AST entry
     * @param legalChildSelectors a list of the legal child selectors for this AST entry
     * @return a new {@link ASTPath.ASTEntry}
     * @throws ParseException if an illegal argument is found
     */
    private ASTPath.ASTEntry newASTEntry(Kind kind, String[] legalChildSelectors) throws IOException, ParseException {
        return newASTEntry(kind, legalChildSelectors, null);
    }

    /**
     * Parses and constructs a new AST entry. For example, the call:
     *
     * <pre>
     * {@code newASTEntry(Kind.CASE, new String[] {"expression", "statement"}, new String[] {"statement"});
     * </pre>
     *
     * constructs a case AST entry, where the valid child selectors are
     * "expression" or "statement" and the "statement" child selector requires
     * an argument.
     *
     * @param kind the kind of this AST entry
     * @param legalChildSelectors a list of the legal child selectors for this AST entry
     * @param argumentChildSelectors a list of the child selectors that also require an argument.
     *                               Entries here should also be in the legalChildSelectors list.
     * @return a new {@link ASTPath.ASTEntry}
     * @throws ParseException if an illegal argument is found
     */
    private ASTPath.ASTEntry newASTEntry(Kind kind, String[] legalChildSelectors, String[] argumentChildSelectors) throws IOException, ParseException {
        expectChar('.');
        for (String arg : legalChildSelectors) {
            if (matchKeyword(arg)) {
                if (argumentChildSelectors != null && ArraysMDE.indexOf(argumentChildSelectors, arg) >= 0) {
                    int index = matchNNInteger();
                    return new ASTPath.ASTEntry(kind, arg, index);
                } else {
                    return new ASTPath.ASTEntry(kind, arg);
                }
            }
        }
        throw new ParseException("Invalid argument for " + kind + " (legal arguments - " + Arrays.toString(legalChildSelectors) + "): " + st.sval);
    }

    /**
     * Parses the next tokens as a Java type.
     */
    private Type parseType() throws IOException, ParseException {
        DeclaredType declaredType;
        if (matchChar('?')) {
            declaredType = new DeclaredType(DeclaredType.WILDCARD);
        } else {
            declaredType = parseDeclaredType();
        }
        if (checkKeyword("extends") || checkKeyword("super")) {
            return parseBoundedType(declaredType);
        } else {
            Type type = declaredType;
            while (matchChar('[')) {
                expectChar(']');
                type = new ArrayType(type);
            }
            return type;
        }
    }

    /**
     * Parses the next tokens as a declared type.
     */
    private DeclaredType parseDeclaredType() throws IOException, ParseException {
        String type = matchIdentifier();
        if (type == null) {
            type = matchPrimitiveType();
            if (type == null) {
                throw new ParseException("Expected identifier or primitive type");
            }
        }
        return parseDeclaredType(type);
    }

    /**
     * Parses the next tokens as a declared type.
     * @param name the name of the initial identifier
     */
    private DeclaredType parseDeclaredType(String name) throws IOException, ParseException {
        DeclaredType type = new DeclaredType(name);
        if (matchChar('<')) {
            type.addTypeParameter(parseType());
            while (matchChar(',')) {
                type.addTypeParameter(parseType());
            }
            expectChar('>');
        }
        if (matchChar('.')) {
            type.setInnerType(parseDeclaredType());
        }
        return type;
    }

    /**
     * Parses the next tokens as a bounded type.
     * @param type the name, which precedes "extends" or "super"
     */
    private BoundedType parseBoundedType(DeclaredType type) throws IOException, ParseException {
        BoundKind kind;
        if (matchKeyword("extends")) {
            kind = BoundKind.EXTENDS;
        } else if (matchKeyword("super")) {
            kind = BoundKind.SUPER;
        } else {
            throw new ParseException("Illegal bound kind: " + st.sval);
        }
        return new BoundedType(type, kind, parseDeclaredType());
    }

    private void parseClass() throws IOException, ParseException {
        expectKeyword("class");
        String basename = expectIdentifier();
        String fullName = curPkgPrefix + basename;

        AClass c = scene.classes.vivify(fullName);
        expectChar(':');

        parseAnnotations(c);
        parseBounds(c.bounds);

        while (checkKeyword("extends")) {
            parseExtends(c);
        }
        while (checkKeyword("implements")) {
            parseImplements(c);
        }
        parseASTInsertions(c);

        while (checkKeyword("field")) {
            parseField(c);
        }
        while (checkKeyword("staticinit")) {
            parseStaticInit(c);
        }
        while (checkKeyword("instanceinit")) {
            parseInstanceInit(c);
        }
        while (checkKeyword("method")) {
            parseMethod(c);
        }
        c.methods.prune();
    }

    // Reads the index file in this.st and puts the information in this.scene.
    private void parse() throws ParseException, IOException {
        st.nextToken();

        while (st.ttype != TT_EOF) {
            expectKeyword("package");

            String pkg;
            if (checkIdentifier() == null) {
                pkg = null;
                // the default package cannot be annotated
                matchChar(':');
            } else {
                pkg = expectQualifiedName();
                // AElement p = scene.packages.vivify(pkg);
                AClass p = scene.classes.vivify(pkg + ".package-info");
                expectChar(':');
                p = scene.classes.vivify(pkg + ".package-info");
                parseAnnotations(p);
            }

            if (pkg != null) {
                curPkgPrefix = pkg + ".";
            } else {
                curPkgPrefix = "";
            }

            for (;;) {
                if (checkKeyword("annotation")) {
                    parseAnnotationDef();
                } else if (checkKeyword("class")) {
                    parseClass();
                } else if (checkKeyword("package") || st.ttype == TT_EOF) {
                    break;
                } else {
                    throw new ParseException("Expected: `annotation', `class', or `package'. Found: `"
                            + st.sval + "', ttype:" + st.ttype);
                }
            }
        }

/*
        for (Map.Entry<String, AnnotationDef> entry : defs.entrySet()) {
            final String annotationType = entry.getKey();
            AnnotationDef def = entry.getValue();
            for (AnnotationFieldType aft : def.fieldTypes.values()) {
                aft.accept(new AFTVisitor<Void, Void>() {
                    @Override
                    public Void visitAnnotationAFT(AnnotationAFT aft,
                            Void arg) {
                        for (AnnotationFieldType t : aft.annotationDef.fieldTypes.values()) {
                            t.accept(this, arg);
                        }
                        return null;
                    }

                    @Override
                    public Void visitArrayAFT(ArrayAFT aft, Void arg) {
                      return aft.elementType == null ? null
                          : aft.elementType.accept(this, arg);
                    }

                    @Override
                    public Void visitBasicAFT(BasicAFT aft, Void arg) {
                      return null;
                    }

                    @Override
                    public Void visitClassTokenAFT(ClassTokenAFT aft, Void arg) {
                      return null;
                    }

                    @Override
                    public Void visitEnumAFT(EnumAFT aft, Void arg) {
                        importSet(annotationType, aft).add(aft.typeName);
                        return null;
                    }

                    private Set<String> importSet(final String annotationType,
                        AnnotationFieldType aft) {
                      Set<String> imps = scene.imports.get(annotationType);
                      if (imps == null) {
                          imps = new TreeSet<String>();
                          scene.imports.put(annotationType, imps);
                      }
                      return imps;
                    }
                }, null);
            }
        }
*/
    }

    private IndexFileParser(Reader in, AScene scene) {
        defs = new LinkedHashMap<String, AnnotationDef>();
        for (AnnotationDef ad : Annotations.standardDefs) {
            try {
                addDef(ad);
            } catch (ParseException e) {
                throw new Error(e);
            }
        }

        st = new StreamTokenizer(in);
        st.slashSlashComments(true);

        // restrict numbers -- don't really need, could interfere with
        // annotation values
        // st.ordinaryChar('-');
        // HMMM this fixes fully-qualified-name strangeness but breaks
        // floating-point numbers
        st.ordinaryChar('.');

        // argggh!!! stupid default needs to be overridden! see java bug 4217680
        st.ordinaryChar('/');

        // for "type-argument"
        st.wordChars('-', '-');

        // java identifiers can contain numbers, _, and $
        st.wordChars('0', '9');
        st.wordChars('_', '_');
        st.wordChars('$', '$');

        this.scene = scene;

        // See if the nonnull analysis picks up on this:
        // curPkgPrefix == ""; // will get changed later anyway
    }

    /**
     * Reads annotations from <code>in</code> in index file format and merges
     * them into <code>scene</code>.  Annotations
     * from the input are merged into the scene; it is an error if both the
     * scene and the input contain annotations of the same type on the same
     * element.
     *
     * <p>
     * Since each annotation in a scene carries its own definition and the
     * scene as a whole no longer has a set of definitions, annotation
     * definitions that are given in the input but never used are not saved
     * anywhere and will not be included if the scene is written back to an
     * index file.  Similarly, retention policies on definitions of annotations
     * that are never used at the top level are dropped.
     *
     * <p>
     * Caveat: Parsing of floating point numbers currently does not work.
     */
    public static Map<String, AnnotationDef> parse(LineNumberReader in,
            AScene scene) throws IOException, ParseException {
        IndexFileParser parser = new IndexFileParser(in, scene);
        // no filename is available in the exception messages
        return parseAndReturnAnnotationDefs(null, in, parser);
    }

    /**
     * Reads annotations from the index file <code>filename</code> and merges
     * them into <code>scene</code>; see {@link #parse(LineNumberReader, AScene)}.
     */
    public static Map<String, AnnotationDef> parseFile(String filename,
            AScene scene) throws IOException {
        LineNumberReader in = new LineNumberReader(new FileReader(filename));
        IndexFileParser parser = new IndexFileParser(in, scene);
        return parseAndReturnAnnotationDefs(filename, in, parser);
    }

    /**
     * Reads annotations from the string (in index file format) and merges
     * them into <code>scene</code>; see {@link #parse(LineNumberReader, AScene)}.
     * Primarily for testing.
     */
    public static Map<String, AnnotationDef> parseString(String fileContents,
            AScene scene) throws IOException {
        String filename =
            "While parsing string: \n----------------BEGIN----------------\n"
                    + fileContents + "----------------END----------------\n";
        LineNumberReader in = new LineNumberReader(
            new StringReader(fileContents));
        IndexFileParser parser = new IndexFileParser(in, scene);
        return parseAndReturnAnnotationDefs(filename, in, parser);
    }

    private static Map<String, AnnotationDef> parseAndReturnAnnotationDefs(
          String filename, LineNumberReader in, IndexFileParser parser)
              throws IOException {
      try {
          parser.parse();
          return Collections.unmodifiableMap(parser.defs);
      } catch (IOException e) {
          throw filename == null ? new FileIOException(in, e)
                  : new FileIOException(in, filename, e);
      } catch (ParseException e) {
          throw filename == null ? new FileIOException(in, e)
                  : new FileIOException(in, filename, e);
      }
    }

    /**
     * Parse the given text into a {@link Type}.
     * @param text the text to parse
     * @return the type
     */
    public static Type parseType(String text) {
        StringReader in = new StringReader(text);
        IndexFileParser parser = new IndexFileParser(in, null);
        try {
            parser.st.nextToken();
            return parser.parseType();
        } catch (Exception e) {
            throw new RuntimeException("Error parsing type from: '" + text + "'", e);
        }
    }
}