function tokenize(fnResolveBinding, sInput, iStart) {
		var aParts = [], // the resulting parts (corresponds to aPrimitiveValueBindings)
			aPrimitiveValueBindings = [], // the bindings with primitive values only
			aTokens = [],
			oTokenizer = new JSTokenizer();

		/**
		 * Saves the binding as a part. Reuses an existing part if the binding is identical.
		 * @param {object} oBinding
		 *   the binding to save
		 * @param {int} iStart
		 *   the binding's start index in the input string
		 * @param {boolean} [bTargetTypeAny=false]
		 *   whether the binding's "targetType" should default to "any" (recursively, for all parts)
		 * @returns {int}
		 *   the index at which it has been saved/found in aParts
		 */
		function saveBindingAsPart(oBinding, iStart, bTargetTypeAny) {
			var bHasNonPrimitiveValue = false,
				sKey,
				oPrimitiveValueBinding,
				i;

			/*
			 * Sets the target type of the given binding to the default "any", if applicable.
			 *
			 * @param {object} oBinding
			 *   A binding
			 */
			function setTargetType(oBinding) {
				if (bTargetTypeAny) {
					if (oBinding.parts) {
						oBinding.parts.forEach(setTargetType);
						// Note: targetType not allowed here, see BindingParser.mergeParts
					} else {
						oBinding.targetType = oBinding.targetType || "any";
					}
				}
			}

			for (sKey in oBinding) {
				if (sKey === "parameters") {
					// parameters are not converted from name to object, but even a simple binding
					// may have the implicit object parameter "scope"
					continue;
				}
				switch (typeof oBinding[sKey]) {
					case "boolean":
					case "number":
					case "string":
					case "undefined":
						break;
					default:
						// binding has at least one property of non-primitive value
						bHasNonPrimitiveValue = true;
				}
			}
			setTargetType(oBinding);
			if (bHasNonPrimitiveValue) {
				// the binding must be a complex binding; property "type" (and poss. others) are
				// newly created objects and thus incomparable -> parse again to have the names
				oPrimitiveValueBinding = JSTokenizer.parseJS(sInput, iStart).result;
				setTargetType(oPrimitiveValueBinding);
			} else {
				// only primitive values; easily comparable
				oPrimitiveValueBinding = oBinding;
			}
			for (i = 0; i < aParts.length; i += 1) {
				// Note: order of top-level properties must not matter for equality!
				if (deepEqual(aPrimitiveValueBindings[i], oPrimitiveValueBinding)) {
					return i;
				}
			}
			aPrimitiveValueBindings[i] = oPrimitiveValueBinding;
			aParts[i] = oBinding;
			return i;
		}

		/**
		 * Consumes the next token in the input string and pushes it to the array of tokens.
		 *
		 * @returns {boolean} whether a token is recognized
		 * @throws {Error|Object|SyntaxError}
		 *   <code>fnResolveBinding</code> may throw <code>SyntaxError</code>;
		 *   <code>oTokenizer.setIndex()</code> may throw <code>Error</code>;
		 *   <code>oTokenizer</code> may also throw <code>{name: 'SyntaxError', ...}</code>
		 */
		function consumeToken() {
			var ch, oBinding, iIndex, aMatches, oToken;

			oTokenizer.white();
			ch = oTokenizer.getCh();
			iIndex = oTokenizer.getIndex();

			if ((ch === "$" || ch === "%") && sInput[iIndex + 1] === "{") { //binding
				oBinding = fnResolveBinding(sInput, iIndex + 1);
				oToken = {
					id: "BINDING",
					value: saveBindingAsPart(oBinding.result, iIndex + 1, ch === "%")
				};
				oTokenizer.setIndex(oBinding.at); //go to first character after binding string
			} else if (rIdentifierStart.test(ch)) {
				aMatches = rIdentifier.exec(sInput.slice(iIndex));
				switch (aMatches[0]) {
				case "false":
				case "null":
				case "true":
					oToken = {id: "CONSTANT", value: oTokenizer.word()};
					break;
				case "in":
				case "typeof":
					oToken = {id: aMatches[0]};
					oTokenizer.setIndex(iIndex + aMatches[0].length);
					break;
				default:
					oToken = {id: "IDENTIFIER", value: aMatches[0]};
					oTokenizer.setIndex(iIndex + aMatches[0].length);
				}
			} else if (rDigit.test(ch)
					|| ch === "." && rDigit.test(sInput[iIndex + 1])) {
				oToken = {id: "CONSTANT", value: oTokenizer.number()};
			} else if (ch === "'" || ch === '"') {
				oToken = {id: "CONSTANT", value: oTokenizer.string()};
			} else {
				rTokens.lastIndex = iIndex;
				aMatches = rTokens.exec(sInput);
				if (!aMatches || aMatches.index !== iIndex) {
					return false; // end of input or unrecognized character
				}
				oToken = {id: aMatches[0]};
				oTokenizer.setIndex(iIndex + aMatches[0].length);
			}
			oToken.input = sInput;
			oToken.start = iIndex;
			oToken.end = oTokenizer.getIndex();
			aTokens.push(oToken);
			return true;
		}

		oTokenizer.init(sInput, iStart);

		try {
			/* eslint-disable no-empty */
			while (consumeToken()) { /* deliberately empty */ }
			/* eslint-enable no-empty */
		} catch (e) {
			// Note: new SyntaxError().name === "SyntaxError"
			if (e.name === "SyntaxError") { // remember tokenizer error
				aTokens.push({
					id: "ERROR",
					value: e
				});
			} else {
				throw e;
			}
		}

		return {
			at: oTokenizer.getIndex(),
			parts: aParts,
			tokens: aTokens
		};
	}
Computes the tokens according to the expression grammar in sInput starting at iStart and
uses fnResolveBinding to resolve bindings embedded in the expression.
@param {function} fnResolveBinding - the function to resolve embedded bindings
@param {string} sInput - the string to be parsed
@param {int} [iStart=0] - the index to start parsing
@returns {object} Tokenization result object with the following properties
  at: the index after the last character consumed by the tokenizer in the input string
  parts: array with parts corresponding to resolved embedded bindings
  tokens: the array of tokens where each token is a tuple of ID, optional value, and
  optional source text