diff --git a/core/src/main/java/io/substrait/type/TypeExpressionEvaluator.java b/core/src/main/java/io/substrait/type/TypeExpressionEvaluator.java
index 45f7e5e98..a1101c462 100644
--- a/core/src/main/java/io/substrait/type/TypeExpressionEvaluator.java
+++ b/core/src/main/java/io/substrait/type/TypeExpressionEvaluator.java
@@ -27,16 +27,19 @@
* type classes whose parameter is an integer to substitute: {@code DECIMAL
}, {@code
* varchar}, {@code fixedchar}, {@code fixedbinary}, {@code precision_time}, {@code
* precision_timestamp
}, {@code precision_timestamp_tz
}, {@code interval_day
} and {@code
- * interval_compound
}. No standard extension declares a parameterized {@code fixedbinary} or
- * {@code interval_compound} at all, as an argument or as a return -- those two are supported for
+ * interval_compound
}. Integer arithmetic, comparisons, boolean operations and conditionals can
+ * appear in those parameters or in the assignments of a multi-line return program. Arithmetic uses
+ * signed 64-bit values; overflow and narrowing to a type parameter's 32-bit representation are
+ * checked rather than wrapped. No standard extension declares a parameterized {@code fixedbinary}
+ * or {@code interval_compound} at all, as an argument or as a return -- those two are supported for
* symmetry, and pinned against hand-written declarations rather than the catalog.
*
*
A {@code list} return still fails whatever its element, because the evaluator does not descend
* into a container -- so an element parameter it would otherwise substitute, as in {@code
- * list>}, is out of reach just as an element type to evaluate is. A multi-line return
- * program still fails because evaluating one needs integer arithmetic over the bound parameters
- * rather than substitution. And a plain {@code any} cannot be derived at all: unlike {@code any1}
- * it names nothing, so there is no identity to bind.
+ * list>}, is out of reach just as an element type to evaluate is. A program referring
+ * to an argument's value rather than a parameter of its type still fails: this API receives only
+ * argument types. A plain {@code any} cannot be derived at all: unlike {@code any1} it names
+ * nothing, so there is no identity to bind. Type-covering expressions are not supported either.
*
* Which shipped variants those cover is pinned by {@code ParameterizedReturnTypeTest} against
* the declarations the catalog ships, and deliberately not repeated here -- the catalog is owned
@@ -85,7 +88,13 @@ public static Type evaluateExpression(
// The declared return type is already concrete; nothing to derive.
return (Type) returnExpression;
}
- return returnExpression.accept(new ReturnTypeEvaluator(returnExpression, bindings));
+ try {
+ return new ReturnTypeEvaluator(returnExpression, bindings)
+ .evaluate(returnExpression, Type.class);
+ } catch (ArithmeticException e) {
+ throw new UnsupportedOperationException(
+ "Cannot evaluate return-type arithmetic: " + e.getMessage(), e);
+ }
}
/**
@@ -311,9 +320,12 @@ private static OptionalInt parseIntegerLiteral(String token) {
* throwing base, keeping unsupported derivations fail-closed.
*/
private static final class ReturnTypeEvaluator
- extends TypeExpressionVisitor.TypeExpressionThrowsVisitor {
+ extends TypeExpressionVisitor.TypeExpressionThrowsVisitor {
private final ParameterBindings bindings;
+ // The derivation language has three value kinds: integer, boolean and type. Local assignments
+ // can hold any of them; each operation checks the kind it consumes.
+ private final Map locals = new HashMap<>();
private ReturnTypeEvaluator(TypeExpression returnExpression, ParameterBindings bindings) {
super("Cannot evaluate return-type expression: " + returnExpression);
@@ -322,60 +334,73 @@ private ReturnTypeEvaluator(TypeExpression returnExpression, ParameterBindings b
@Override
public Type visit(ParameterizedType.Decimal decimal) {
- int precision = resolveInteger(decimal.precision().value());
- int scale = resolveInteger(decimal.scale().value());
+ int precision = resolveInteger(decimal.precision());
+ int scale = resolveInteger(decimal.scale());
return TypeCreator.of(decimal.nullable()).decimal(precision, scale);
}
@Override
public Type visit(ParameterizedType.FixedChar fixedChar) {
- return TypeCreator.of(fixedChar.nullable())
- .fixedChar(resolveInteger(fixedChar.length().value()));
+ return TypeCreator.of(fixedChar.nullable()).fixedChar(resolveInteger(fixedChar.length()));
}
@Override
public Type visit(ParameterizedType.VarChar varChar) {
- return TypeCreator.of(varChar.nullable()).varChar(resolveInteger(varChar.length().value()));
+ return TypeCreator.of(varChar.nullable()).varChar(resolveInteger(varChar.length()));
}
@Override
public Type visit(ParameterizedType.FixedBinary fixedBinary) {
return TypeCreator.of(fixedBinary.nullable())
- .fixedBinary(resolveInteger(fixedBinary.length().value()));
+ .fixedBinary(resolveInteger(fixedBinary.length()));
}
@Override
public Type visit(ParameterizedType.PrecisionTime precisionTime) {
return TypeCreator.of(precisionTime.nullable())
- .precisionTime(resolveInteger(precisionTime.precision().value()));
+ .precisionTime(resolveInteger(precisionTime.precision()));
}
@Override
public Type visit(ParameterizedType.PrecisionTimestamp precisionTimestamp) {
return TypeCreator.of(precisionTimestamp.nullable())
- .precisionTimestamp(resolveInteger(precisionTimestamp.precision().value()));
+ .precisionTimestamp(resolveInteger(precisionTimestamp.precision()));
}
@Override
public Type visit(ParameterizedType.PrecisionTimestampTZ precisionTimestampTZ) {
return TypeCreator.of(precisionTimestampTZ.nullable())
- .precisionTimestampTZ(resolveInteger(precisionTimestampTZ.precision().value()));
+ .precisionTimestampTZ(resolveInteger(precisionTimestampTZ.precision()));
}
@Override
public Type visit(ParameterizedType.IntervalDay intervalDay) {
return TypeCreator.of(intervalDay.nullable())
- .intervalDay(resolveInteger(intervalDay.precision().value()));
+ .intervalDay(resolveInteger(intervalDay.precision()));
}
@Override
public Type visit(ParameterizedType.IntervalCompound intervalCompound) {
return TypeCreator.of(intervalCompound.nullable())
- .intervalCompound(resolveInteger(intervalCompound.precision().value()));
+ .intervalCompound(resolveInteger(intervalCompound.precision()));
}
@Override
- public Type visit(ParameterizedType.StringLiteral stringLiteral) {
+ public Object visit(ParameterizedType.StringLiteral stringLiteral) {
+ Object local = locals.get(stringLiteral.value());
+ if (local != null) {
+ return local instanceof Type
+ ? ((Type) local).withNullable(stringLiteral.nullable())
+ : local;
+ }
+ Integer integer = bindings.boundInteger(stringLiteral.value());
+ if (integer != null) {
+ return integer.longValue();
+ }
+ OptionalInt literal = parseIntegerLiteral(stringLiteral.value());
+ if (literal.isPresent()) {
+ return (long) literal.getAsInt();
+ }
// A wildcard return (e.g. min(any1) -> any1) resolves to the bound argument type, taking the
// nullability declared on the return expression in both directions (a required return forces
// the type non-null, a nullable one forces it nullable). MIRROR policy, if any, is applied
@@ -388,16 +413,149 @@ public Type visit(ParameterizedType.StringLiteral stringLiteral) {
return bound.withNullable(stringLiteral.nullable());
}
- private int resolveInteger(String token) {
- Integer bound = bindings.boundInteger(token);
- if (bound != null) {
- return bound;
+ @Override
+ public Type visit(TypeExpression.Decimal decimal) {
+ return TypeCreator.of(decimal.nullable())
+ .decimal(resolveInteger(decimal.precision()), resolveInteger(decimal.scale()));
+ }
+
+ @Override
+ public Type visit(TypeExpression.FixedChar fixedChar) {
+ return TypeCreator.of(fixedChar.nullable()).fixedChar(resolveInteger(fixedChar.length()));
+ }
+
+ @Override
+ public Type visit(TypeExpression.VarChar varChar) {
+ return TypeCreator.of(varChar.nullable()).varChar(resolveInteger(varChar.length()));
+ }
+
+ @Override
+ public Type visit(TypeExpression.FixedBinary fixedBinary) {
+ return TypeCreator.of(fixedBinary.nullable())
+ .fixedBinary(resolveInteger(fixedBinary.length()));
+ }
+
+ @Override
+ public Type visit(TypeExpression.PrecisionTime precisionTime) {
+ return TypeCreator.of(precisionTime.nullable())
+ .precisionTime(resolveInteger(precisionTime.precision()));
+ }
+
+ @Override
+ public Type visit(TypeExpression.PrecisionTimestamp precisionTimestamp) {
+ return TypeCreator.of(precisionTimestamp.nullable())
+ .precisionTimestamp(resolveInteger(precisionTimestamp.precision()));
+ }
+
+ @Override
+ public Type visit(TypeExpression.PrecisionTimestampTZ precisionTimestampTZ) {
+ return TypeCreator.of(precisionTimestampTZ.nullable())
+ .precisionTimestampTZ(resolveInteger(precisionTimestampTZ.precision()));
+ }
+
+ @Override
+ public Type visit(TypeExpression.IntervalDay intervalDay) {
+ return TypeCreator.of(intervalDay.nullable())
+ .intervalDay(resolveInteger(intervalDay.precision()));
+ }
+
+ @Override
+ public Type visit(TypeExpression.IntervalCompound intervalCompound) {
+ return TypeCreator.of(intervalCompound.nullable())
+ .intervalCompound(resolveInteger(intervalCompound.precision()));
+ }
+
+ @Override
+ public Object visit(TypeExpression.ReturnProgram program) {
+ for (TypeExpression.ReturnProgram.Assignment assignment : program.assignments()) {
+ locals.put(assignment.name(), evaluate(assignment.expr(), Object.class));
+ }
+ return evaluate(program.finalExpression(), Type.class);
+ }
+
+ @Override
+ public Long visit(TypeExpression.IntegerLiteral literal) {
+ return (long) literal.value();
+ }
+
+ @Override
+ public Object visit(TypeExpression.IfOperation conditional) {
+ return evaluate(
+ evaluate(conditional.ifCondition(), Boolean.class)
+ ? conditional.thenExpr()
+ : conditional.elseExpr(),
+ Object.class);
+ }
+
+ @Override
+ public Boolean visit(TypeExpression.NotOperation operation) {
+ return !evaluate(operation.inner(), Boolean.class);
+ }
+
+ @Override
+ public Object visit(TypeExpression.BinaryOperation operation) {
+ switch (operation.opType()) {
+ case AND:
+ case OR:
+ boolean left = evaluate(operation.left(), Boolean.class);
+ boolean right = evaluate(operation.right(), Boolean.class);
+ return operation.opType() == TypeExpression.BinaryOperation.OpType.AND
+ ? left && right
+ : left || right;
+ case COVERS:
+ throw new UnsupportedOperationException("Cannot evaluate type-covering expressions");
+ default:
+ break;
+ }
+ long left = evaluate(operation.left(), Long.class);
+ long right = evaluate(operation.right(), Long.class);
+ switch (operation.opType()) {
+ case ADD:
+ return Math.addExact(left, right);
+ case SUBTRACT:
+ return Math.subtractExact(left, right);
+ case MULTIPLY:
+ return Math.multiplyExact(left, right);
+ case DIVIDE:
+ if (left == Long.MIN_VALUE && right == -1) {
+ throw new ArithmeticException("long overflow");
+ }
+ return left / right;
+ case MIN:
+ return Math.min(left, right);
+ case MAX:
+ return Math.max(left, right);
+ case LT:
+ return left < right;
+ case GT:
+ return left > right;
+ case LTE:
+ return left <= right;
+ case GTE:
+ return left >= right;
+ case EQ:
+ return left == right;
+ case NOT_EQ:
+ return left != right;
+ default:
+ throw new UnsupportedOperationException(
+ "Cannot evaluate operation " + operation.opType());
+ }
+ }
+
+ private int resolveInteger(TypeExpression expression) {
+ return Math.toIntExact(evaluate(expression, Long.class));
+ }
+
+ private T evaluate(TypeExpression expression, Class expectedKind) {
+ Object result = expression instanceof Type ? expression : expression.accept(this);
+ if (!expectedKind.isInstance(result)) {
+ throw new UnsupportedOperationException(
+ String.format(
+ "Expected %s in return-type expression, got %s",
+ expectedKind.getSimpleName(), result));
}
- return parseIntegerLiteral(token)
- .orElseThrow(
- () ->
- new UnsupportedOperationException(
- "Unbound type parameter '" + token + "' in return-type expression"));
+ return expectedKind.cast(result);
}
}
}
diff --git a/core/src/main/java/io/substrait/type/parser/ParseToPojo.java b/core/src/main/java/io/substrait/type/parser/ParseToPojo.java
index 7cad8c004..d3c92a76d 100644
--- a/core/src/main/java/io/substrait/type/parser/ParseToPojo.java
+++ b/core/src/main/java/io/substrait/type/parser/ParseToPojo.java
@@ -675,14 +675,20 @@ private TypeExpression.BinaryOperation.OpType getBinaryExpressionType(Token toke
return TypeExpression.BinaryOperation.OpType.DIVIDE;
case ">":
return TypeExpression.BinaryOperation.OpType.GT;
+ case ">=":
+ return TypeExpression.BinaryOperation.OpType.GTE;
case "<":
return TypeExpression.BinaryOperation.OpType.LT;
+ case "<=":
+ return TypeExpression.BinaryOperation.OpType.LTE;
case "AND":
return TypeExpression.BinaryOperation.OpType.AND;
case "OR":
return TypeExpression.BinaryOperation.OpType.OR;
case "=":
return TypeExpression.BinaryOperation.OpType.EQ;
+ case "!=":
+ return TypeExpression.BinaryOperation.OpType.NOT_EQ;
case ":=":
return TypeExpression.BinaryOperation.OpType.COVERS;
default:
diff --git a/core/src/test/java/io/substrait/extension/FunctionBindingResolverTest.java b/core/src/test/java/io/substrait/extension/FunctionBindingResolverTest.java
index af3e60601..b3cc824c1 100644
--- a/core/src/test/java/io/substrait/extension/FunctionBindingResolverTest.java
+++ b/core/src/test/java/io/substrait/extension/FunctionBindingResolverTest.java
@@ -9,6 +9,7 @@
import com.google.common.io.Resources;
import io.substrait.expression.FunctionOption;
+import io.substrait.type.Type;
import io.substrait.type.TypeCreator;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -67,6 +68,29 @@ void resolvesConcreteIntegerSum() {
assertEquals(sum.getAnchor(), binding.anchor());
}
+ @Test
+ void decimalDivisionDerivesIndependentlyOfTheDeclaredOutputType() {
+ SimpleExtension.ScalarFunctionVariant divide =
+ scalar(DefaultExtensionCatalog.FUNCTIONS_ARITHMETIC_DECIMAL, "divide:dec_dec");
+ List arguments =
+ List.of(ResolvedArgument.value(R.decimal(10, 2)), ResolvedArgument.value(R.decimal(5, 1)));
+ assertEquals(R.decimal(21, 8), FunctionBindingResolver.deriveOutputType(divide, arguments));
+ assertEquals(
+ R.decimal(21, 8),
+ FunctionBindingResolver.resolveAndValidate(divide, arguments, List.of(), R.decimal(21, 8))
+ .outputType());
+
+ for (Type declared : List.of(R.decimal(20, 2), R.decimal(21, 7), N.decimal(21, 8))) {
+ InvalidFunctionBindingException error =
+ assertThrows(
+ InvalidFunctionBindingException.class,
+ () ->
+ FunctionBindingResolver.resolveAndValidate(
+ divide, arguments, List.of(), declared));
+ assertTrue(error.getMessage().contains("output type"), error.getMessage());
+ }
+ }
+
@Test
void resolvesDecimalSumWidth() {
SimpleExtension.AggregateFunctionVariant sum =
diff --git a/core/src/test/java/io/substrait/type/ParameterizedReturnTypeTest.java b/core/src/test/java/io/substrait/type/ParameterizedReturnTypeTest.java
index ba4566df7..8291d45a5 100644
--- a/core/src/test/java/io/substrait/type/ParameterizedReturnTypeTest.java
+++ b/core/src/test/java/io/substrait/type/ParameterizedReturnTypeTest.java
@@ -156,10 +156,8 @@ void mirrorNullabilityStillApplies() {
}
/**
- * The census of what the evaluator does not derive: a {@code list} return is the first shape, a
- * multi-line return program the second. {@link TypeExpressionEvaluator}'s Javadoc describes those
- * shapes and points here rather than naming variants, so this test is the only place a {@code
- * substrait-packaging} bump can make the two disagree.
+ * The census of list returns the evaluator does not derive. The catalog is owned upstream, so
+ * this catches declarations added by a {@code substrait-packaging} bump.
*/
@Test
void theReturnShapesThatAreNotDerivedYet() {
@@ -174,6 +172,13 @@ void theReturnShapesThatAreNotDerivedYet() {
"transform:list_func"),
variantsReturning(ParameterizedType.ListType.class));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> resolve("string_split:vchar_vchar", R.varChar(20), R.varChar(20)));
+ }
+
+ @Test
+ void catalogReturnProgramsAreCovered() {
assertEquals(
List.of(
"add:dec_dec",
@@ -193,14 +198,27 @@ void theReturnShapesThatAreNotDerivedYet() {
"subtract:dec_dec"),
variantsReturning(TypeExpression.ReturnProgram.class));
- // The lists above pin which variants carry each shape; these pin that the shapes actually fail,
- // so making one derivable cannot leave the census passing and the Javadoc stale.
- assertThrows(
- UnsupportedOperationException.class,
- () -> resolve("string_split:vchar_vchar", R.varChar(20), R.varChar(20)));
- assertThrows(
- UnsupportedOperationException.class,
- () -> resolve("add:dec_dec", R.decimal(10, 2), R.decimal(10, 2)));
+ // Arithmetic programs are exercised with exact expectations in ReturnProgramTypeTest.
+ // The rounding programs derive from the input's precision and scale too, including round:
+ // the pinned declaration does not read its value argument s.
+ assertEquals(R.decimal(9, 0), resolve("ceil:dec", R.decimal(10, 2)));
+ assertEquals(R.decimal(9, 0), resolve("floor:dec", R.decimal(10, 2)));
+ assertEquals(N.decimal(11, 2), resolve("round:dec_i32", R.decimal(10, 2), R.I32));
+
+ // These four declarations read integer_parameter(precision) without binding precision from
+ // any argument type. Supplying an i8 type cannot provide that argument's value.
+ for (String key : List.of("strptime_time:str_str_i8", "strptime_timestamp:str_str_i8")) {
+ assertUnboundPrecision(key, R.STRING, R.STRING, R.I8);
+ }
+ assertUnboundPrecision("strptime_timestamp:str_str_str_i8", R.STRING, R.STRING, R.STRING, R.I8);
+ assertUnboundPrecision("assume_timezone:date_str_i8", R.DATE, R.STRING, R.I8);
+ }
+
+ private static void assertUnboundPrecision(String key, Type... arguments) {
+ UnsupportedOperationException error =
+ assertThrows(UnsupportedOperationException.class, () -> resolve(key, arguments));
+ assertTrue(
+ error.getMessage().contains("Unbound type parameter 'precision'"), error.getMessage());
}
private static List variantsReturning(Class> returnShape) {
diff --git a/core/src/test/java/io/substrait/type/ReturnProgramTypeTest.java b/core/src/test/java/io/substrait/type/ReturnProgramTypeTest.java
new file mode 100644
index 000000000..45d71fb5c
--- /dev/null
+++ b/core/src/test/java/io/substrait/type/ReturnProgramTypeTest.java
@@ -0,0 +1,156 @@
+package io.substrait.type;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.substrait.extension.DefaultExtensionCatalog;
+import io.substrait.extension.SimpleExtension;
+import io.substrait.function.ParameterizedTypeCreator;
+import io.substrait.type.parser.TypeStringParser;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class ReturnProgramTypeTest {
+
+ private static final TypeCreator R = TypeCreator.REQUIRED;
+ private static final TypeCreator N = TypeCreator.NULLABLE;
+ private static final String URN = DefaultExtensionCatalog.FUNCTIONS_ARITHMETIC_DECIMAL;
+
+ private static Type resolve(String key, Type... arguments) {
+ return DefaultExtensionCatalog.DEFAULT_COLLECTION
+ .getScalarFunction(SimpleExtension.FunctionAnchor.of(URN, key))
+ .resolveType(List.of(arguments));
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "add,10,2,5,1,11,2",
+ "add,38,10,38,10,38,9",
+ "subtract,10,2,5,1,11,2",
+ "subtract,38,10,38,10,38,9",
+ "multiply,10,2,5,1,16,3",
+ "multiply,38,10,38,10,38,6",
+ "multiply,30,20,30,20,38,17",
+ "divide,10,2,5,1,21,8",
+ "divide,38,10,38,10,38,6",
+ "modulus,10,2,5,1,6,2"
+ })
+ void decimalProgramsDeriveTheCatalogFormula(
+ String name, int p1, int s1, int p2, int s2, int precision, int scale) {
+ // Expected types follow the extension's spec v0.102.0 formulas. In particular, divide's
+ // precision uses P2, where the separate prose example uses S2.
+ assertEquals(
+ R.decimal(precision, scale),
+ resolve(name + ":dec_dec", R.decimal(p1, s1), R.decimal(p2, s2)));
+ }
+
+ @Test
+ void decimalProgramsPreserveMirrorNullabilityAndLiteralConstraints() {
+ assertEquals(N.decimal(11, 2), resolve("add:dec_dec", N.decimal(10, 2), R.decimal(5, 1)));
+ for (String name : List.of("bitwise_and", "bitwise_or", "bitwise_xor")) {
+ assertEquals(
+ R.decimal(20, 0), resolve(name + ":dec_dec", R.decimal(10, 0), R.decimal(20, 0)));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> resolve(name + ":dec_dec", R.decimal(10, 1), R.decimal(20, 0)));
+ }
+ }
+
+ private static Type evaluate(String expression) {
+ return TypeExpressionEvaluator.evaluateExpression(
+ TypeStringParser.parseExpression(expression, URN),
+ List.of(
+ SimpleExtension.ValueArgument.builder()
+ .name("input")
+ .value(ParameterizedTypeCreator.REQUIRED.varCharE("L"))
+ .build()),
+ List.of(R.varChar(10)));
+ }
+
+ @ParameterizedTest
+ @CsvSource(
+ delimiter = ';',
+ value = {
+ "varchar; varchar<11>",
+ "fixedchar; fixedchar<20>",
+ "fixedbinary; fixedbinary<5>",
+ "decimal; decimal<12,2>",
+ "precision_time; precision_time<6>",
+ "precision_timestamp; precision_timestamp<6>",
+ "precision_timestamp_tz; precision_timestamp_tz<6>",
+ "interval_day; interval_day<3>",
+ "interval_compound; interval_compound<3>"
+ })
+ void arithmeticWorksInsideTypeParameters(String expression, String expected) {
+ assertEquals(TypeStringParser.parseSimple(expected, URN), evaluate(expression));
+ }
+
+ @Test
+ void assignmentsUseEarlierResultsAndRemainLocalToOneEvaluation() {
+ assertEquals(R.varChar(24), evaluate("a = L + 2\nb = a * 2\nvarchar"));
+ assertEquals(R.varChar(10), evaluate("wide = L > 5\nvarchar"));
+ assertEquals(R.varChar(22), evaluate("L = L + 1\nL = L * 2\nvarchar"));
+ assertEquals(R.varChar(10), evaluate("varchar"));
+ assertThrows(UnsupportedOperationException.class, () -> evaluate("varchar"));
+ }
+
+ @Test
+ void conditionsSelectOnlyTheChosenBranch() {
+ assertEquals(R.varChar(10), evaluate("varchar 5 ? L : missing>"));
+ assertEquals(R.varChar(10), evaluate("varchar"));
+ assertEquals(R.I64, evaluate("(L = 10) ? i64 : string"));
+ assertEquals(R.varChar(10), evaluate("varchar 0 ? L : 1>"));
+ assertEquals(R.varChar(10), evaluate("varchar"));
+ assertEquals(N.I64, evaluate("if L >= 10 then i64? else string"));
+ assertEquals(R.STRING, evaluate("L != 10 ? i64 : string"));
+ assertEquals(R.varChar(2), evaluate("varchar<(L <= 10 AND L >= 10 AND L != 9) ? 2 : 1>"));
+ assertEquals(R.varChar(1), evaluate("varchar<(L <= 9 OR L >= 11 OR L != 10) ? 2 : 1>"));
+ }
+
+ @Test
+ void booleanOperationsEvaluateBothOperands() {
+ assertArithmeticFailure("varchar<(L < 0 AND L / 0 > 0) ? 1 : 2>");
+ assertArithmeticFailure("varchar<(L > 0 OR L / 0 > 0) ? 1 : 2>");
+ }
+
+ @Test
+ void signedDivisionTruncatesTowardsZero() {
+ assertEquals(R.varChar(13), evaluate("varchar"));
+ assertEquals(R.varChar(7), evaluate("varchar"));
+ }
+
+ @Test
+ void integerExpressionsUse64BitsBeforeConvertingToATypeParameter() {
+ assertEquals(R.varChar(10), evaluate("wide = 2147483647 + L\nvarchar"));
+ assertArithmeticFailure("varchar<2147483647 + L>");
+ assertArithmeticFailure("wide = 2147483647 * 2147483647 * L\nvarchar<10>");
+
+ String minimum = "low = (0 - 2147483647 - 1) * (2147483647 + 1) * 2\n";
+ assertEquals(R.varChar(10), evaluate(minimum + "varchar"));
+ for (String expression : List.of("low - 1", "(0 - (low + 1)) + 1", "low / (0 - 1)")) {
+ // Overflow must fail even when the assignment's result is not used by the final type.
+ assertArithmeticFailure(minimum + "wide = " + expression + "\nvarchar<10>");
+ }
+ }
+
+ @Test
+ void invalidExpressionKindsAndUnboundParametersAreRejected() {
+ for (String expression : List.of("varchar 0>", "varchar")) {
+ assertThrows(UnsupportedOperationException.class, () -> evaluate(expression), expression);
+ }
+ assertArithmeticFailure("varchar");
+ UnsupportedOperationException error =
+ assertThrows(UnsupportedOperationException.class, () -> evaluate("varchar"));
+ assertTrue(error.getMessage().contains("missing"), error.getMessage());
+ }
+
+ private static void assertArithmeticFailure(String expression) {
+ UnsupportedOperationException error =
+ assertThrows(UnsupportedOperationException.class, () -> evaluate(expression), expression);
+ assertInstanceOf(ArithmeticException.class, error.getCause(), expression);
+ }
+}