public String decode(int[] data) {
		StringBuilder buf = new StringBuilder();
		int p = 0;
		int version = data[p++];
		if (version != ATNDeserializer.SERIALIZED_VERSION) {
			String reason = String.format("Could not deserialize ATN with version %d (expected %d).", version, ATNDeserializer.SERIALIZED_VERSION);
			throw new UnsupportedOperationException(new InvalidClassException(ATN.class.getName(), reason));
		}

		p++; // skip grammarType
		int maxType = data[p++];
		buf.append("max type ").append(maxType).append("\n");
		int nstates = data[p++];
		for (int i=0; i<nstates; i++) {
			int stype = data[p++];
			if ( stype== ATNState.INVALID_TYPE ) continue; // ignore bad type of states
			int ruleIndex = data[p++];
			if (ruleIndex == Character.MAX_VALUE) {
				ruleIndex = -1;
			}

			String arg = "";
			if ( stype == ATNState.LOOP_END ) {
				int loopBackStateNumber = data[p++];
				arg = " "+loopBackStateNumber;
			}
			else if ( stype == ATNState.PLUS_BLOCK_START || stype == ATNState.STAR_BLOCK_START || stype == ATNState.BLOCK_START ) {
				int endStateNumber = data[p++];
				arg = " "+endStateNumber;
			}
			buf.append(i).append(":")
					.append(ATNState.serializationNames.get(stype)).append(" ")
					.append(ruleIndex).append(arg).append("\n");
		}
		// this code is meant to model the form of ATNDeserializer.deserialize,
		// since both need to be updated together whenever a change is made to
		// the serialization format. The "dead" code is only used in debugging
		// and testing scenarios, so the form you see here was kept for
		// improved maintainability.
		// start
		int numNonGreedyStates = data[p++];
		for (int i = 0; i < numNonGreedyStates; i++) {
			int stateNumber = data[p++];
		}
		int numPrecedenceStates = data[p++];
		for (int i = 0; i < numPrecedenceStates; i++) {
			int stateNumber = data[p++];
		}
		// finish
		int nrules = data[p++];
		for (int i=0; i<nrules; i++) {
			int s = data[p++];
			if (atn.grammarType == ATNType.LEXER) {
				int arg1 = data[p++];
				buf.append("rule ").append(i).append(":").append(s).append(" ").append(arg1).append('\n');
			}
			else {
				buf.append("rule ").append(i).append(":").append(s).append('\n');
			}
		}
		int nmodes = data[p++];
		for (int i=0; i<nmodes; i++) {
			int s = data[p++];
			buf.append("mode ").append(i).append(":").append(s).append('\n');
		}
		int numBMPSets = data[p++];
		p = appendSets(buf, data, p, numBMPSets);
		int nedges = data[p++];
		for (int i=0; i<nedges; i++) {
			int src = data[p];
			int trg = data[p + 1];
			int ttype = data[p + 2];
			int arg1 = data[p + 3];
			int arg2 = data[p + 4];
			int arg3 = data[p + 5];
			buf.append(src).append("->").append(trg)
					.append(" ").append(Transition.serializationNames.get(ttype))
					.append(" ").append(arg1).append(",").append(arg2).append(",").append(arg3)
					.append("\n");
			p += 6;
		}
		int ndecisions = data[p++];
		for (int i=0; i<ndecisions; i++) {
			int s = data[p++];
			buf.append(i).append(":").append(s).append("\n");
		}
		if (atn.grammarType == ATNType.LEXER) {
			// this code is meant to model the form of ATNDeserializer.deserialize,
			// since both need to be updated together whenever a change is made to
			// the serialization format. The "dead" code is only used in debugging
			// and testing scenarios, so the form you see here was kept for
			// improved maintainability.
			int lexerActionCount = data[p++];
			for (int i = 0; i < lexerActionCount; i++) {
				LexerActionType actionType = LexerActionType.values()[data[p++]];
				int data1 = data[p++];
				int data2 = data[p++];
			}
		}
		return buf.toString();
	}
For testing really; gives a human readable version of the ATN
tool-testsuite/test/org/antlr/v4/test/tool/ATNDescriber.java