diff --git a/.changes/next-release/feature-e8b0c9d42596402804ac70a348da0fd38622a0e4.json b/.changes/next-release/feature-e8b0c9d42596402804ac70a348da0fd38622a0e4.json new file mode 100644 index 00000000000..92d6212b5c3 --- /dev/null +++ b/.changes/next-release/feature-e8b0c9d42596402804ac70a348da0fd38622a0e4.json @@ -0,0 +1,7 @@ +{ + "type": "feature", + "description": "Add new transform compileBdd and compileBddForAws", + "pull_requests": [ + "[#2953](https://github.com/smithy-lang/smithy/pull/2953)" + ] +} diff --git a/docs/source-2.0/additional-specs/rules-engine/specification.rst b/docs/source-2.0/additional-specs/rules-engine/specification.rst index 01ce02bef77..22abb1e9f46 100644 --- a/docs/source-2.0/additional-specs/rules-engine/specification.rst +++ b/docs/source-2.0/additional-specs/rules-engine/specification.rst @@ -94,7 +94,7 @@ on the scenario. This trait is experimental and subject to change. Summary - A Binary `Decision Diagram (BDD) `_ representation of + A `Binary Decision Diagram (BDD) `_ representation of endpoint rules that is more compact and efficient at runtime than the decision-tree-based EndpointRuleSet trait. Trait selector ``service`` @@ -108,8 +108,11 @@ 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`` trait can be generated from an ``endpointRuleSet`` trait through compilation. + To generate the ``endpointBdd`` trait for a service, add the :ref:`compileBdd ` transform + to the ``smithy-build.json`` file. + Services may provide either trait, with ``endpointBdd`` preferred for production use due to + its performance characteristics. The ``endpointBdd`` structure has the following properties: diff --git a/docs/source-2.0/guides/smithy-build-json.rst b/docs/source-2.0/guides/smithy-build-json.rst index 543fe18b075..c1c7f0203c1 100644 --- a/docs/source-2.0/guides/smithy-build-json.rst +++ b/docs/source-2.0/guides/smithy-build-json.rst @@ -654,6 +654,39 @@ Only the following shape type changes are supported: .. seealso:: :ref:`changeStringEnumsToEnumShapes` +.. _compileBdd-transform: + +compileBdd +----------------------------- + +This transform compiles `Binary Decision Diagram (BDD) `_ +from service shape's :ref:`@endpointRuleSet ` trait and attaches +the compiled :ref:`@endpointBdd ` trait to the service shape. + +.. code-block:: json + + { + "version": "1.0", + "projections": { + "exampleProjection": { + "transforms": [ + { + "name": "compileBdd" + } + ] + } + }, + "maven": { + "dependencies": [ + "software.amazon.smithy:smithy-rules-engine:__smithy_version__" + ] + } + } + +.. note:: + AWS services like ``S3`` can have special tree transformations that dramatically improve the BDD compiled result + both in size and performance. To use them, please use the dedicated transform ``compileBddForAws`` and + include the dependency of ``software.amazon.smithy:smithy-aws-endpoints:__smithy_version__``. .. _excludeShapesBySelector-transform: diff --git a/smithy-aws-endpoints/src/it/java/software/amazon/smithy/rulesengine/aws/language/functions/S3BddTest.java b/smithy-aws-endpoints/src/it/java/software/amazon/smithy/rulesengine/aws/language/functions/S3BddTest.java index fccf607f251..ca7a35a1af2 100644 --- a/smithy-aws-endpoints/src/it/java/software/amazon/smithy/rulesengine/aws/language/functions/S3BddTest.java +++ b/smithy-aws-endpoints/src/it/java/software/amazon/smithy/rulesengine/aws/language/functions/S3BddTest.java @@ -4,6 +4,8 @@ */ package software.amazon.smithy.rulesengine.aws.language.functions; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -11,13 +13,16 @@ import java.util.List; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.TransformContext; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.ModelSerializer; import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.rulesengine.analysis.BddCoverageChecker; import software.amazon.smithy.rulesengine.aws.s3.S3TreeRewriter; +import software.amazon.smithy.rulesengine.aws.transforms.CompileBddForAws; import software.amazon.smithy.rulesengine.language.EndpointRuleSet; import software.amazon.smithy.rulesengine.language.evaluation.TestEvaluator; import software.amazon.smithy.rulesengine.logic.bdd.CostOptimization; @@ -154,4 +159,14 @@ private void writeModelWithBddTrait(EndpointBddTrait bddTrait) { throw new RuntimeException("Failed to write S3 BDD model", e); } } + + @Test + void compileBddForS3AddedTrait() { + TransformContext context = TransformContext.builder() + .model(model) + .build(); + Model result = new CompileBddForAws().transform(context); + Shape serviceShape = result.expectShape(S3_SERVICE_ID); + assertTrue(serviceShape.hasTrait(EndpointBddTrait.ID)); + } } diff --git a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/transforms/CompileBddForAws.java b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/transforms/CompileBddForAws.java new file mode 100644 index 00000000000..1b44bef2788 --- /dev/null +++ b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/transforms/CompileBddForAws.java @@ -0,0 +1,70 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.rulesengine.aws.transforms; + +import static software.amazon.smithy.rulesengine.transforms.CompileBdd.compileBdd; + +import java.util.HashSet; +import java.util.Set; +import software.amazon.smithy.build.ProjectionTransformer; +import software.amazon.smithy.build.TransformContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.transform.ModelTransformer; +import software.amazon.smithy.rulesengine.aws.s3.S3TreeRewriter; +import software.amazon.smithy.rulesengine.language.EndpointRuleSet; +import software.amazon.smithy.rulesengine.language.evaluation.TestEvaluator; +import software.amazon.smithy.rulesengine.traits.EndpointBddTrait; +import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait; +import software.amazon.smithy.rulesengine.traits.EndpointTestCase; +import software.amazon.smithy.rulesengine.traits.EndpointTestsTrait; + +/** + * A dedicated transform to compile Binary Decision Diagram (BDD) from AWS services. + */ +public final class CompileBddForAws implements ProjectionTransformer { + + private static final ShapeId S3_SERVICE_ID = ShapeId.from("com.amazonaws.s3#AmazonS3"); + + @Override + public String getName() { + return "compileBddForAws"; + } + + @Override + public Model transform(TransformContext transformContext) { + Model model = transformContext.getModel(); + Set shapes = new HashSet<>(); + for (ServiceShape serviceShape : model.getServiceShapes()) { + if (serviceShape.hasTrait(EndpointRuleSetTrait.ID) + && !serviceShape.hasTrait(EndpointBddTrait.ID)) { + EndpointRuleSet rules = getEndpointRuleSet(serviceShape); + EndpointBddTrait bdd = compileBdd(rules); + shapes.add(serviceShape.toBuilder().addTrait(bdd).build()); + } + } + return ModelTransformer.create().replaceShapes(model, shapes); + } + + private EndpointRuleSet getEndpointRuleSet(ServiceShape serviceShape) { + EndpointRuleSet rules = serviceShape.expectTrait(EndpointRuleSetTrait.class).getEndpointRuleSet(); + if (serviceShape.getId().equals(S3_SERVICE_ID)) { + return applyS3Transform(rules, serviceShape); + } + return rules; + } + + private EndpointRuleSet applyS3Transform(EndpointRuleSet rules, ServiceShape serviceShape) { + EndpointRuleSet transformedRules = S3TreeRewriter.transform(rules); + serviceShape.getTrait(EndpointTestsTrait.class).ifPresent(testsTrait -> { + for (EndpointTestCase testCase : testsTrait.getTestCases()) { + TestEvaluator.evaluate(transformedRules, testCase); + } + }); + return transformedRules; + } +} diff --git a/smithy-aws-endpoints/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer b/smithy-aws-endpoints/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer new file mode 100644 index 00000000000..1ecd1ba6db5 --- /dev/null +++ b/smithy-aws-endpoints/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer @@ -0,0 +1 @@ +software.amazon.smithy.rulesengine.aws.transforms.CompileBddForAws diff --git a/smithy-rules-engine/build.gradle.kts b/smithy-rules-engine/build.gradle.kts index 8bf1bdd248d..0db26a1b1f2 100644 --- a/smithy-rules-engine/build.gradle.kts +++ b/smithy-rules-engine/build.gradle.kts @@ -13,6 +13,7 @@ extra["moduleName"] = "software.amazon.smithy.rulesengine" dependencies { api(project(":smithy-model")) + api(project(":smithy-build")) api(project(":smithy-utils")) api(project(":smithy-jmespath")) } diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/transforms/CompileBdd.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/transforms/CompileBdd.java new file mode 100644 index 00000000000..bb9b7451dfd --- /dev/null +++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/transforms/CompileBdd.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.rulesengine.transforms; + +import java.util.HashSet; +import java.util.Set; +import software.amazon.smithy.build.ProjectionTransformer; +import software.amazon.smithy.build.TransformContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.transform.ModelTransformer; +import software.amazon.smithy.rulesengine.language.EndpointRuleSet; +import software.amazon.smithy.rulesengine.logic.bdd.CostOptimization; +import software.amazon.smithy.rulesengine.logic.bdd.SiftingOptimization; +import software.amazon.smithy.rulesengine.logic.cfg.Cfg; +import software.amazon.smithy.rulesengine.traits.EndpointBddTrait; +import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait; + +/** + * Compiles a Binary Decision Diagram (BDD) from a service's {@code @endpointRuleSet} + * trait and attaches the compiled {@code @endpointBdd} trait to the service shape. + */ +public final class CompileBdd implements ProjectionTransformer { + + @Override + public String getName() { + return "compileBdd"; + } + + @Override + public Model transform(TransformContext transformContext) { + Model model = transformContext.getModel(); + Set shapes = new HashSet<>(); + for (ServiceShape serviceShape : model.getServiceShapes()) { + if (serviceShape.hasTrait(EndpointRuleSetTrait.ID) + && !serviceShape.hasTrait(EndpointBddTrait.ID)) { + EndpointRuleSetTrait endpointRuleSetTrait = serviceShape.expectTrait(EndpointRuleSetTrait.class); + EndpointBddTrait bdd = compileBdd(endpointRuleSetTrait.getEndpointRuleSet()); + shapes.add(serviceShape.toBuilder().addTrait(bdd).build()); + } + } + return ModelTransformer.create().replaceShapes(model, shapes); + } + + /** + * Compile endpointBdd trait from endpoint ruleset and return the optimized version. + * + * @param rules Endpoint ruleset from service shape. + */ + public static EndpointBddTrait compileBdd(EndpointRuleSet rules) { + // Create the CFG to start BDD compilation process. + Cfg cfg = Cfg.from(rules); + EndpointBddTrait unoptimizedTrait = EndpointBddTrait.from(cfg); + + // Sift the BDD to shorten paths and reduce the BDD size. + EndpointBddTrait siftedTrait = SiftingOptimization.builder().cfg(cfg).build().apply(unoptimizedTrait); + + // "cost optimize" the BDD to ensure cheap conditions come first with up to 10% size impact. + EndpointBddTrait costOptimizedTrait = CostOptimization.builder().cfg(cfg).build().apply(siftedTrait); + + // Remove unreferenced conditions. This is destructive and further optimizations cannot be applied after this. + return costOptimizedTrait.removeUnreferencedConditions(); + } +} diff --git a/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer b/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer new file mode 100644 index 00000000000..3e2e71004a5 --- /dev/null +++ b/smithy-rules-engine/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer @@ -0,0 +1 @@ +software.amazon.smithy.rulesengine.transforms.CompileBdd diff --git a/smithy-rules-engine/src/test/java/software/amazon/smithy/rulesengine/transforms/CompileBddTest.java b/smithy-rules-engine/src/test/java/software/amazon/smithy/rulesengine/transforms/CompileBddTest.java new file mode 100644 index 00000000000..dd6a57462fc --- /dev/null +++ b/smithy-rules-engine/src/test/java/software/amazon/smithy/rulesengine/transforms/CompileBddTest.java @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.rulesengine.transforms; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.TransformContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.rulesengine.traits.EndpointBddTrait; + +public class CompileBddTest { + + @Test + public void compilesAndAttachesBddTrait() throws Exception { + ShapeId serviceId = ShapeId.from("smithy.example#ExampleService"); + Model model = Model.assembler() + .discoverModels() + .addImport(Paths.get(getClass().getResource("compile-bdd.smithy").toURI())) + .assemble() + .unwrap(); + TransformContext context = TransformContext.builder() + .model(model) + .build(); + Model result = new CompileBdd().transform(context); + Shape serviceShape = result.expectShape(serviceId); + assertTrue(serviceShape.hasTrait(EndpointBddTrait.ID)); + } +} diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/transforms/compile-bdd.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/transforms/compile-bdd.smithy new file mode 100644 index 00000000000..e0af1af4ad7 --- /dev/null +++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/transforms/compile-bdd.smithy @@ -0,0 +1,50 @@ +$version: "2.0" + +namespace smithy.example + +use smithy.rules#clientContextParams +use smithy.rules#endpointRuleSet +use smithy.rules#endpointTests + +@clientContextParams( + Region: {type: "string", documentation: "docs"} +) +@endpointRuleSet({ + "version": "1.1" + "parameters": { + "Region": { + "required": true + "type": "String" + "documentation": "docs" + } + } + "rules": [ + { + "conditions": [] + "documentation": "base rule" + "endpoint": { + "url": "https://{Region}.amazonaws.com" + "properties": {} + "headers": {} + } + "type": "endpoint" + } + ] +}) +@endpointTests({ + "version": "1.0" + "testCases": [ + { + "documentation": "example endpoint test" + "expect": { + "endpoint": { + "url": "https://example-region.amazonaws.com" + } + } + "params": { + Region: "example-region" + } + } + ] +}) +service ExampleService {}