private ParseTree parseSet(final StringParseReader expression,
				                final boolean inverted ) throws ParseException {
		final List<ParseTree> setNodes = new ArrayList<ParseTree>();
		int currentChar = expression.read();
		boolean requireRangeValue = false;
		PARSE_SET: while (currentChar >= 0) {
			if (foundAtoms(currentChar, expression, setNodes)) {
				if (requireRangeValue) {
					createRange(setNodes, expression);
					requireRangeValue = false;
				}
			} else if (!foundWhitespaceAndComments(currentChar, expression)) {
				if (currentChar == CLOSE_SET) {
					break PARSE_SET;
				}
				if (currentChar == RANGE_SEPARATOR) {
					requireRangeValue = true;
				} else {
					throw new ParseException(addContext("Unexpected character [" + (char) currentChar + ']', expression));
				}
			}
			currentChar = expression.read();
		}
		
		if (requireRangeValue) {
			throw new ParseException(addContext("Cannot have a range without a second value.", expression));
		}
		if (currentChar < 0) {
			throw new ParseException(addContext("The expression ended without closing the set", expression));
		}
		if (setNodes.isEmpty()) {
			throw new ParseException(addContext("Cannot have an empty set", expression));
		}
		// We do not try to optimise a set with a single child by just returning the child directly, as we
		// do for sequences and alternatives.
		// If the set is inverted, then the child should be inverted too from its current inversion status.
		// We don't currently have an easy way to invert a node (could add a method to do so to the interface).
		// Not all children have the same meaning outside of a set as inside one.  E.g. strings are interpreted
		// as a set of bytes to match, not a sequence.  So for simplicity we leave sets exactly as they are
		// syntactically defined.  Compilers must deal with nested sets or sets which aren't really needed.
		return new ChildrenNode(ParseTreeType.SET, setNodes, inverted);
	}