private evaluateBinaryExpression()

in packages/jinja/src/runtime.ts [548:656]


	private evaluateBinaryExpression(node: BinaryExpression, environment: Environment): AnyRuntimeValue {
		const left = this.evaluate(node.left, environment);

		// Logical operators
		// NOTE: Short-circuiting is handled by the `evaluate` function
		switch (node.operator.value) {
			case "and":
				return left.__bool__().value ? this.evaluate(node.right, environment) : left;
			case "or":
				return left.__bool__().value ? left : this.evaluate(node.right, environment);
		}

		// Equality operators
		const right = this.evaluate(node.right, environment);
		switch (node.operator.value) {
			case "==":
				return new BooleanValue(left.value == right.value);
			case "!=":
				return new BooleanValue(left.value != right.value);
		}

		if (left instanceof UndefinedValue || right instanceof UndefinedValue) {
			if (right instanceof UndefinedValue && ["in", "not in"].includes(node.operator.value)) {
				// Special case: `anything in undefined` is `false` and `anything not in undefined` is `true`
				return new BooleanValue(node.operator.value === "not in");
			}
			throw new Error(`Cannot perform operation ${node.operator.value} on undefined values`);
		} else if (left instanceof NullValue || right instanceof NullValue) {
			throw new Error("Cannot perform operation on null values");
		} else if (node.operator.value === "~") {
			// toString and concatenation
			return new StringValue(left.value.toString() + right.value.toString());
		} else if (
			(left instanceof IntegerValue || left instanceof FloatValue) &&
			(right instanceof IntegerValue || right instanceof FloatValue)
		) {
			// Evaulate pure numeric operations with binary operators.
			const a = left.value,
				b = right.value;
			switch (node.operator.value) {
				// Arithmetic operators
				case "+":
				case "-":
				case "*": {
					const res = node.operator.value === "+" ? a + b : node.operator.value === "-" ? a - b : a * b;
					const isFloat = left instanceof FloatValue || right instanceof FloatValue;
					return isFloat ? new FloatValue(res) : new IntegerValue(res);
				}
				case "/":
					return new FloatValue(a / b);
				case "%": {
					const rem = a % b;
					const isFloat = left instanceof FloatValue || right instanceof FloatValue;
					return isFloat ? new FloatValue(rem) : new IntegerValue(rem);
				}
				// Comparison operators
				case "<":
					return new BooleanValue(a < b);
				case ">":
					return new BooleanValue(a > b);
				case ">=":
					return new BooleanValue(a >= b);
				case "<=":
					return new BooleanValue(a <= b);
			}
		} else if (left instanceof ArrayValue && right instanceof ArrayValue) {
			// Evaluate array operands with binary operator.
			switch (node.operator.value) {
				case "+":
					return new ArrayValue(left.value.concat(right.value));
			}
		} else if (right instanceof ArrayValue) {
			const member = right.value.find((x) => x.value === left.value) !== undefined;
			switch (node.operator.value) {
				case "in":
					return new BooleanValue(member);
				case "not in":
					return new BooleanValue(!member);
			}
		}

		if (left instanceof StringValue || right instanceof StringValue) {
			// Support string concatenation as long as at least one operand is a string
			switch (node.operator.value) {
				case "+":
					return new StringValue(left.value.toString() + right.value.toString());
			}
		}

		if (left instanceof StringValue && right instanceof StringValue) {
			switch (node.operator.value) {
				case "in":
					return new BooleanValue(right.value.includes(left.value));
				case "not in":
					return new BooleanValue(!right.value.includes(left.value));
			}
		}

		if (left instanceof StringValue && right instanceof ObjectValue) {
			switch (node.operator.value) {
				case "in":
					return new BooleanValue(right.value.has(left.value));
				case "not in":
					return new BooleanValue(!right.value.has(left.value));
			}
		}

		throw new SyntaxError(`Unknown operator "${node.operator.value}" between ${left.type} and ${right.type}`);
	}