Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c6755e5
Add BDD-based rules engine trait
mtdowling Jul 15, 2025
448d89f
Add separate BddFormatter
mtdowling Jul 23, 2025
af9cd9a
Add BDD validation, same as ruleset trait
mtdowling Jul 25, 2025
1c05a2f
Hide node layout of BDD and move domain to trait
mtdowling Jul 27, 2025
73b44f2
Always serialize conditions... even when empty
mtdowling Jul 29, 2025
0d8abb9
Fix BddTrait logic issue, using wrong conditions
mtdowling Jul 29, 2025
3428992
Create UniqueTable cache and cleanup BddBuilder
mtdowling Aug 1, 2025
493349f
Simplify condition handling in CFG and BDD
mtdowling Aug 1, 2025
777203d
Remove unused methods from ConditionReference
mtdowling Aug 1, 2025
8afec2d
Remove varint encoding and address PR feedback
mtdowling Aug 2, 2025
79b1cb3
Add coalesce function
mtdowling Aug 8, 2025
d140cd0
Improve initial ordering and add coalesce
mtdowling Aug 11, 2025
0dfebc8
Improve sifting
mtdowling Aug 13, 2025
0df404a
Improve transforms
mtdowling Aug 13, 2025
5cc33c5
Add result coalescing phi nodes
mtdowling Aug 15, 2025
67a02ec
Rename bdd trait to endpointBdd
mtdowling Aug 15, 2025
fc6d936
Add version to bdd trait and syntax elements
mtdowling Aug 15, 2025
5bbd38b
Simplify and document coalesce
mtdowling Aug 15, 2025
7c5ee43
Add version validation to rules engine
mtdowling Aug 17, 2025
4a8aa35
Use NO_MATCH rule for false nodes instead
mtdowling Aug 19, 2025
9986fc8
Make coalesce variadic
mtdowling Aug 19, 2025
96d73de
Remove result coalescing
mtdowling Aug 21, 2025
bed1c9a
Add BDD trait docs
mtdowling Aug 22, 2025
bec5965
Add documentation clarifications
mtdowling Aug 25, 2025
e73a1e4
Add BDD coverage checker
mtdowling Aug 26, 2025
2224ba8
Add test cases that cover boolean in coalesce
mtdowling Aug 26, 2025
bb88821
Move EndpointBddTrait to traits, address PR issues
mtdowling Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/spotbugs/filter.xml
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,10 @@
<Bug code="CT"/>
</Match>

<!-- This is a false positive. Yeah, we have a terminal node that's a singleton, but the ctor is still valid. -->
<Match>
<Class name="software.amazon.smithy.rulesengine.logic.cfg.ResultNode" />
<Bug pattern="SING_SINGLETON_HAS_NONPRIVATE_CONSTRUCTOR" />
</Match>

</FindBugsFilter>
188 changes: 183 additions & 5 deletions docs/source-2.0/additional-specs/rules-engine/specification.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ are composed of a set of *conditions*, which determine if a rule should be
selected, and a result. Conditions act on the defined parameters, and allow for
the modeling of statements.

When a rules conditions are evaluated successfully, the rule provides either a
When a rule's conditions are evaluated successfully, the rule provides either a
result and its accompanying requirements or an error describing the unsupported
state. Modeled endpoint errors allow for more explicit descriptions to users,
such as providing errors when a service doesn't support a combination of
Expand All @@ -24,8 +24,9 @@ conditions.
.. smithy-trait:: smithy.rules#endpointRuleSet
.. _smithy.rules#endpointRuleSet-trait:

--------------------------------------
``smithy.rules#endpointRuleSet`` trait
======================================
--------------------------------------

Summary
Defines a rule set for deriving service endpoints at runtime.
Expand All @@ -45,8 +46,7 @@ The content of the ``endpointRuleSet`` document has the following properties:
- Description
* - version
- ``string``
- **Required**. The rule set schema version. This specification covers
version 1.0 of the endpoint rule set.
- **Required**. The rules engine version (e.g., 1.0).
* - serviceId
- ``string``
- **Required**. An identifier for the corresponding service.
Expand Down Expand Up @@ -74,6 +74,184 @@ or :ref:`error rules, <rules-engine-endpoint-rule-set-error-rule>` with an
empty set of conditions to provide a more meaningful default or error depending
on the scenario.

.. smithy-trait:: smithy.rules#endpointBdd
.. _smithy.rules#endpointBdd-trait:

----------------------------------
``smithy.rules#endpointBdd`` trait
----------------------------------

.. warning:: Experimental

This trait is experimental and subject to change.

Summary
Defines a `Binary Decision Diagram (BDD) <https://en.wikipedia.org/wiki/Binary_decision_diagram>`_ representation
of endpoint rules for efficient runtime evaluation.
Trait selector
``service``
Value type
``structure``

The ``endpointBdd`` trait provides a BDD representation of endpoint rules, optimizing runtime evaluation by
eliminating redundant condition evaluations and reducing the decision tree to a minimal directed acyclic graph.
This trait is an alternative to ``endpointRuleSet`` that trades compile-time complexity for significantly improved
runtime performance and reduced artifact sizes.

.. note::

The ``endpointBdd`` trait can be generated from an ``endpointRuleSet`` trait through compilation. Services may
provide either trait, with ``endpointBdd`` preferred for production use due to its performance characteristics.

The ``endpointBdd`` structure has the following properties:

.. list-table::
:header-rows: 1
:widths: 10 30 60

* - Property name
- Type
- Description
* - version
- ``string``
- **Required**. The endpoint rules engine version. Must be at least version 1.1.
* - parameters
- ``map<string, parameter object>`` of `Parameter object`_
- **Required**. A map of zero or more endpoint parameter names to
their parameter configuration. Uses the same parameter structure as
``endpointRuleSet``.
* - conditions
- ``array`` of `Condition object`_
- **Required**. Array of conditions that are evaluated during BDD
traversal. Each condition is referenced by its index in this array.
* - results
- ``array`` of `Endpoint rule object`_ or `Error rule object`_
- **Required**. Array of possible endpoint results. The implicit `NoMatchRule` at BDD reference 0 is not included
in the array. These rule objects MUST NOT contain conditions.
* - root
- ``integer``
- **Required**. The root reference where BDD evaluation begins.
* - nodeCount
- ``integer``
- **Required**. The total number of nodes in the BDD. Used for validation and exact-sizing arrays during
deserialization.
* - nodes
- ``string``
- **Required**. Base64-encoded binary representation of BDD nodes. Each node is encoded as three 4-byte
integers: ``[conditionIndex, highRef, lowRef]``.

.. _rules-engine-endpoint-bdd-node-structure:

BDD node structure
------------------

Each BDD node is encoded as a triple of integers:

* ``conditionIndex``: Zero-based index into the ``conditions`` array
* ``highRef``: Reference to follow when the condition evaluates to true
* ``lowRef``: Reference to follow when the condition evaluates to false

The first node, index 0, is always the terminal node ``[-1, 1, -1]`` and MUST NOT be referenced directly. This node
serves as the canonical base case for BDD reduction algorithms.

.. _rules-engine-endpoint-bdd-reference-encoding:

Reference encoding
------------------

BDD references use the following encoding scheme:

.. list-table::
:header-rows: 1
:widths: 20 80

* - Reference value
- Description
* - ``0``
- Invalid/unused reference (never appears in valid BDDs)
* - ``1``
- TRUE terminal (no match in endpoint resolution)
* - ``-1``
- FALSE terminal (no match in endpoint resolution)
* - ``2, 3, 4, ...``
- Node references (points to ``nodes[ref-1]``)
* - ``-2, -3, -4, ...``
- Complement edges (logical NOT of the referenced node)
* - ``100000000+``
- Result terminals (100000000 + resultIndex)

When traversing a complement edge (negative reference), the high and low branches are swapped during evaluation.
This enables significant node sharing and BDD size reduction.

.. _rules-engine-endpoint-bdd-binary-encoding:

Binary node encoding
--------------------

Nodes are encoded as a Base64 string using binary encoding for efficiency:

* Each node consists of three 4-byte big-endian integers
* Nodes are concatenated sequentially: ``[node0, node1, ..., nodeN-1]``
* The resulting byte array is Base64-encoded


.. note:: Why binary?

This encoding provides:

* **Size efficiency**: smaller than an array of JSON integers, or an array of arrays of integers
* **Performance**: Direct deserialization into the target data structure (e.g., primitive arrays and integers)
* **Cleaner diffs**: BDD node changes appear as single-line modifications rather than spread over thousands
of numbers.

.. _rules-engine-endpoint-bdd-evaluation:

BDD evaluation
--------------

BDD evaluation follows these steps:

#. Start at the root reference
#. While the reference is a node reference (not a terminal or result):

* Extract the node index: ``nodeIndex = |ref| - 1``
* Retrieve the node at that index
* Evaluate the condition at ``conditionIndex``
* Determine which branch to follow:

* If the reference is complemented (negative) AND condition is true: follow ``lowRef``
* If the reference is complemented (negative) AND condition is false: follow ``highRef``
* If the reference is positive AND condition is true: follow ``highRef``
* If the reference is positive AND condition is false: follow ``lowRef``

* Update the reference to the chosen branch and continue

#. When reaching a terminal or result:

* For result references ≥ 100000000: return ``results[ref - 100000000]``
* For terminals (1 or -1): return the ``NoMatchRule``

For example, a reference of 100000003 would return ``results[3]``, while a reference of 1 or -1 indicates no matching
rule was found.

.. _rules-engine-endpoint-bdd-validation:

Validation requirements
-----------------------

* **Root reference**: MUST NOT be complemented (negative)
* **Reference validity**: All references MUST be valid:

* ``0`` is forbidden
* Node references MUST point to existing nodes
* Result references MUST point to existing results

* **Node structure**: Each node MUST be a properly formed triple
* **Condition indices**: Each node's condition index MUST be within ``[0, conditionCount)``
* **Result structure**: The first result (index 0) implicitly represents ``NoMatchRule`` and is not serialized.
All serialized results MUST be either ``EndpointRule`` or ``ErrorRule`` objects without conditions.
* **Version requirement**: The version MUST be at least 1.1

.. _rules-engine-endpoint-rule-set-parameter:

----------------
Expand Down Expand Up @@ -119,7 +297,7 @@ allow values to be bound to parameters from other locations in generated
clients.

Parameters MAY be annotated with the ``builtIn`` property, which designates that
the parameter should be bound to a value determined by the built-ins name. The
the parameter should be bound to a value determined by the built-in's name. The
:ref:`rules engine contains built-ins <rules-engine-parameters-built-ins>` and
the set is extensible.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,53 @@ parameter is equal to the value ``false``:
}


.. _rules-engine-standard-library-coalesce:

``coalesce`` function
=====================

Summary
Evaluates arguments in order and returns the first non-empty result, otherwise returns the result of the last
argument.
Argument types
* This function is variadic and requires two or more arguments, each of type ``T`` or ``option<T>``
* All arguments must have the same inner type ``T``
Return type
* ``coalesce(T, T, ...)`` → ``T``
* ``coalesce(option<T>, T, ...)`` → ``T`` (if any argument is non-optional)
* ``coalesce(T, option<T>, ...)`` → ``T`` (if any argument is non-optional)
* ``coalesce(option<T>, option<T>, ...)`` → ``option<T>`` (if all arguments are optional)
Since
1.1

The ``coalesce`` function provides null-safe chaining by evaluating arguments in order and returning the first
non-empty result. If all arguments evaluate to empty, it returns the result of the last argument. This is
particularly useful for providing default values for optional parameters, chaining multiple optional values
together, and related optimizations.

The function accepts two or more arguments, all of which must have the same inner type after unwrapping any
optionals. The return type is ``option<T>`` only if all arguments are ``option<T>``; otherwise it returns ``T``.

The following example demonstrates using ``coalesce`` with multiple arguments to try several optional values
in sequence:

.. code-block:: json

{
"fn": "coalesce",
"argv": [
{"ref": "customEndpoint"},
{"ref": "regionalEndpoint"},
{"ref": "defaultEndpoint"}
]
}

.. important::
All arguments must have the same type after unwrapping any optionals (types are known at compile time and do not
need to be validated at runtime). Note that the first non-empty result is returned even if it's ``false``
(coalesce is looking for a *non-empty* value, not a truthy value).


.. _rules-engine-standard-library-getAttr:

``getAttr`` function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
package software.amazon.smithy.rulesengine.aws.language.functions;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -39,28 +39,67 @@ private AwsArn(Builder builder) {
* @return the optional ARN.
*/
public static Optional<AwsArn> parse(String arn) {
String[] base = arn.split(":", 6);
if (base.length != 6) {
if (arn == null || arn.length() < 8 || !arn.startsWith("arn:")) {
return Optional.empty();
}
// First section must be "arn".
if (!base[0].equals("arn")) {

// find each of the first five ':' positions
int p0 = 3; // after "arn"
int p1 = arn.indexOf(':', p0 + 1);
if (p1 < 0) {
return Optional.empty();
}

int p2 = arn.indexOf(':', p1 + 1);
if (p2 < 0) {
return Optional.empty();
}

int p3 = arn.indexOf(':', p2 + 1);
if (p3 < 0) {
return Optional.empty();
}
// Sections for partition, service, and resource type must not be empty.
if (base[1].isEmpty() || base[2].isEmpty() || base[5].isEmpty()) {

int p4 = arn.indexOf(':', p3 + 1);
if (p4 < 0) {
return Optional.empty();
}

// extract and validate mandatory parts
String partition = arn.substring(p0 + 1, p1);
String service = arn.substring(p1 + 1, p2);
String region = arn.substring(p2 + 1, p3);
String accountId = arn.substring(p3 + 1, p4);
String resource = arn.substring(p4 + 1);

if (partition.isEmpty() || service.isEmpty() || resource.isEmpty()) {
return Optional.empty();
}

return Optional.of(builder()
.partition(base[1])
.service(base[2])
.region(base[3])
.accountId(base[4])
.resource(Arrays.asList(base[5].split("[:/]", -1)))
.partition(partition)
.service(service)
.region(region)
.accountId(accountId)
.resource(splitResource(resource))
.build());
}

private static List<String> splitResource(String resource) {
List<String> result = new ArrayList<>();
int start = 0;
int length = resource.length();
for (int i = 0; i < length; i++) {
char c = resource.charAt(i);
if (c == ':' || c == '/') {
result.add(resource.substring(start, i));
start = i + 1;
}
}
result.add(resource.substring(start));
return result;
}

/**
* Builder to create an {@link AwsArn} instance.
*
Expand Down
Loading
Loading