Skip to content

Commit 45173a9

Browse files
committed
Simplify and document coalesce
1 parent 3065cab commit 45173a9

2 files changed

Lines changed: 65 additions & 63 deletions

File tree

  • docs/source-2.0/additional-specs/rules-engine
  • smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expressions/functions

docs/source-2.0/additional-specs/rules-engine/standard-library.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,54 @@ parameter is equal to the value ``false``:
3838
}
3939
4040
41+
.. _rules-engine-standard-library-coalesce:
42+
43+
``coalesce`` function
44+
=====================
45+
46+
Summary
47+
Evaluates the first argument and returns the result if it is present, otherwise evaluates and returns the result
48+
of the second argument.
49+
Argument types
50+
* value1: ``T`` or ``option<T>``
51+
* value2: ``T`` or ``option<T>``
52+
Return type
53+
* ``coalesce(T, T)`` → ``T``
54+
* ``coalesce(option<T>, T)`` → ``T``
55+
* ``coalesce(T, option<T>)`` → ``T``
56+
* ``coalesce(option<T>, option<T>)`` → ``option<T>``
57+
Since
58+
1.1
59+
60+
The ``coalesce`` function provides null-safe chaining by returning the result of the first argument if it returns a
61+
value, otherwise returns the result of the second argument. This is particularly useful for providing default values
62+
for optional parameters, chaining multiple optional values together, and related optimizations.
63+
64+
The following example demonstrates chaining multiple ``coalesce`` calls to try several optional values
65+
in sequence:
66+
67+
.. code-block:: json
68+
69+
{
70+
"fn": "coalesce",
71+
"argv": [
72+
{"ref": "customEndpoint"},
73+
{
74+
"fn": "coalesce",
75+
"argv": [
76+
{"ref": "regionalEndpoint"},
77+
{"ref": "defaultEndpoint"}
78+
]
79+
}
80+
]
81+
}
82+
83+
.. important::
84+
Both arguments must be of the same type after unwrapping any optionals (types are known at compile time and do not
85+
need to be validated at runtime). Note that the first result is returned even if it's ``false`` (coalesce is
86+
looking for a *non-empty* value).
87+
88+
4189
.. _rules-engine-standard-library-getAttr:
4290

4391
``getAttr`` function

smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/syntax/expressions/functions/Coalesce.java

Lines changed: 17 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import java.util.Arrays;
88
import java.util.List;
99
import software.amazon.smithy.rulesengine.language.evaluation.Scope;
10-
import software.amazon.smithy.rulesengine.language.evaluation.type.AnyType;
1110
import software.amazon.smithy.rulesengine.language.evaluation.type.OptionalType;
1211
import software.amazon.smithy.rulesengine.language.evaluation.type.Type;
1312
import software.amazon.smithy.rulesengine.language.evaluation.value.Value;
@@ -17,24 +16,17 @@
1716
import software.amazon.smithy.utils.SmithyUnstableApi;
1817

1918
/**
20-
* A coalesce function that returns the first non-empty value, with type-safe fallback handling.
19+
* A coalesce function that returns the first non-empty value.
2120
* At runtime, returns the left value unless it's EmptyValue, in which case returns the right value.
2221
*
2322
* <p>Type checking rules:
2423
* <ul>
2524
* <li>{@code coalesce(T, T) => T} (same types)</li>
26-
* <li>{@code coalesce(T, AnyType) => T} (AnyType adapts to concrete type)</li>
27-
* <li>{@code coalesce(AnyType, T) => T} (AnyType adapts to concrete type)</li>
28-
* <li>{@code coalesce(T, S) => S} (if T.isA(S), i.e., S is more general)</li>
29-
* <li>{@code coalesce(T, S) => T} (if S.isA(T), i.e., T is more general)</li>
30-
* <li>{@code coalesce(Optional<T>, S) => common_type(T, S)} (unwraps optional)</li>
31-
* <li>{@code coalesce(T, Optional<S>) => common_type(T, S)} (unwraps optional)</li>
32-
* <li>{@code coalesce(Optional<T>, Optional<S>) => Optional<common_type(T, S)>}</li>
25+
* <li>{@code coalesce(Optional<T>, T) => T} (unwraps optional)</li>
26+
* <li>{@code coalesce(T, Optional<T>) => T} (unwraps optional)</li>
27+
* <li>{@code coalesce(Optional<T>, Optional<T>) => Optional<T>}</li>
3328
* </ul>
3429
*
35-
* <p>Special handling for AnyType: Since AnyType can masquerade as any type, when coalescing
36-
* with a concrete type, the concrete type is used as the result type.
37-
*
3830
* <p>Supports chaining:
3931
* {@code coalesce(opt1, coalesce(opt2, coalesce(opt3, default)))}
4032
*
@@ -80,29 +72,6 @@ public <R> R accept(ExpressionVisitor<R> visitor) {
8072
return visitor.visitCoalesce(args.get(0), args.get(1));
8173
}
8274

83-
// Type checking rules for coalesce:
84-
//
85-
// This function returns the first non-empty value with type-safe fallback handling.
86-
// The type resolution follows these rules:
87-
//
88-
// 1. If both types are identical, use that type
89-
// 2. Special handling for AnyType: Since AnyType.isA() always returns true (it can masquerade as any type), we
90-
// need to handle it specially. When coalescing AnyType with a concrete type, we use the concrete type as the
91-
// result, since AnyType can adapt to it at runtime.
92-
// 3. For other types, we use the isA relationship to find the more general type:
93-
// - If left.isA(right), then right is more general, use right
94-
// - If right.isA(left), then left is more general, use left
95-
// 4. If no type relationship exists, throw a type mismatch error
96-
//
97-
// The result is wrapped in Optional only if BOTH inputs are Optional, since coalesce(optional, required)
98-
// guarantees a non-empty result.
99-
//
100-
// Examples:
101-
// - coalesce(String, String) => String
102-
// - coalesce(Optional<String>, String) => String
103-
// - coalesce(Optional<String>, Optional<String>) => Optional<String>
104-
// - coalesce(String, AnyType) => String (AnyType adapts)
105-
// - coalesce(SubType, SuperType) => SuperType (more general)
10675
@Override
10776
public Type typeCheck(Scope<Type> scope) {
10877
List<Expression> args = getArguments();
@@ -113,38 +82,23 @@ public Type typeCheck(Scope<Type> scope) {
11382

11483
Type leftType = args.get(0).typeCheck(scope);
11584
Type rightType = args.get(1).typeCheck(scope);
116-
117-
// Find the least upper bound (most specific common type)
118-
Type resultType = lubForCoalesce(leftType, rightType);
119-
120-
// Only return Optional if both sides can be empty
121-
if (leftType instanceof OptionalType && rightType instanceof OptionalType) {
122-
return Type.optionalType(resultType);
85+
Type leftInner = getInnerType(leftType);
86+
Type rightInner = getInnerType(rightType);
87+
88+
// Both must be the same type (after unwrapping optionals)
89+
if (!leftInner.equals(rightInner)) {
90+
throw new IllegalArgumentException(String.format(
91+
"Type mismatch in coalesce: %s and %s must be the same type",
92+
leftType,
93+
rightType));
12394
}
12495

125-
return resultType;
126-
}
127-
128-
// Finds the least upper bound (LUB) for coalesce type checking.
129-
// The LUB is the most specific type that both input types can be assigned to.
130-
// Special handling for AnyType: it adapts to concrete types rather than dominating them.
131-
private static Type lubForCoalesce(Type a, Type b) {
132-
Type ai = getInnerType(a);
133-
Type bi = getInnerType(b);
134-
135-
if (ai.equals(bi)) {
136-
return ai;
137-
} else if (ai instanceof AnyType) {
138-
return bi; // AnyType adapts to concrete type
139-
} else if (bi instanceof AnyType) {
140-
return ai; // AnyType adapts to concrete type
141-
} else if (ai.isA(bi)) {
142-
return bi; // bi is more general
143-
} else if (bi.isA(ai)) {
144-
return ai; // ai is more general
96+
// Only return Optional if both sides are optional
97+
if (leftType instanceof OptionalType && rightType instanceof OptionalType) {
98+
return Type.optionalType(leftInner);
14599
}
146100

147-
throw new IllegalArgumentException("Type mismatch in coalesce: " + a + " and " + b + " have no common type");
101+
return leftInner;
148102
}
149103

150104
private static Type getInnerType(Type t) {

0 commit comments

Comments
 (0)