-
Notifications
You must be signed in to change notification settings - Fork 195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix recursive constraint violations with paths over list and map shapes #2371
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,21 +7,29 @@ package software.amazon.smithy.rust.codegen.core.smithy.transformers | |
|
||
import software.amazon.smithy.codegen.core.TopologicalIndex | ||
import software.amazon.smithy.model.Model | ||
import software.amazon.smithy.model.shapes.ListShape | ||
import software.amazon.smithy.model.shapes.CollectionShape | ||
import software.amazon.smithy.model.shapes.MapShape | ||
import software.amazon.smithy.model.shapes.MemberShape | ||
import software.amazon.smithy.model.shapes.SetShape | ||
import software.amazon.smithy.model.shapes.Shape | ||
import software.amazon.smithy.model.transform.ModelTransformer | ||
import software.amazon.smithy.rust.codegen.core.smithy.traits.RustBoxTrait | ||
import software.amazon.smithy.rust.codegen.core.util.hasTrait | ||
|
||
object RecursiveShapeBoxer { | ||
class RecursiveShapeBoxer( | ||
private val containsIndirectionPredicate: (Collection<Shape>) -> Boolean = ::containsIndirection, | ||
private val boxShapeFn: (MemberShape) -> MemberShape = ::addRustBoxTrait, | ||
) { | ||
/** | ||
* Transform a model which may contain recursive shapes into a model annotated with [RustBoxTrait] | ||
* Transform a model which may contain recursive shapes into a model annotated with [RustBoxTrait]. | ||
* | ||
* When recursive shapes do NOT go through a List, Map, or Set, they must be boxed in Rust. This function will | ||
* iteratively find loops & add the `RustBox` trait in a deterministic way until it reaches a fixed point. | ||
* When recursive shapes do NOT go through a `CollectionShape` or a `MapShape` shape, they must be boxed in Rust. | ||
* This function will iteratively find loops and add the [RustBoxTrait] trait in a deterministic way until it | ||
* reaches a fixed point. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice if you could insert a small example model with comments here to reiterate this e.g. structure RecursiveStruct {
// This must be boxed
someStruct: SomeStruct,
// This needs no box
someList: SomeList,
// This needs no box
someMap: SomeMap,
}
structure SomeStruct {
inner: RecursiveStruct
}
list SomeList {
member: RecursiveStruct
}
map SomeMap {
key: String
value: RecursiveStruct
} I'm not confident I understood this correctly and if I got this model wrong then it's more evidence in favor of adding an example model. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added an example model. |
||
* | ||
* Why `CollectionShape`s and `MapShape`s? Note that `CollectionShape`s get rendered in Rust as `Vec<T>`, and | ||
* `MapShape`s as `HashMap<String, T>`; they're the only Smithy shapes that "organically" introduce indirection | ||
* (via a pointer to the heap) in the recursive path. For other recursive paths, we thus have to introduce the | ||
* indirection artificially ourselves using `Box`. | ||
* | ||
* This function MUST be deterministic (always choose the same shapes to `Box`). If it is not, that is a bug. Even so | ||
* this function may cause backward compatibility issues in certain pathological cases where a changes to recursive | ||
|
@@ -41,12 +49,12 @@ object RecursiveShapeBoxer { | |
* If [model] contains no loops, return null. | ||
*/ | ||
private fun transformInner(model: Model): Model? { | ||
// Execute 1-step of the boxing algorithm in the path to reaching a fixed point | ||
// 1. Find all the shapes that are part of a cycle | ||
// 2. Find all the loops that those shapes are part of | ||
// 3. Filter out the loops that go through a layer of indirection | ||
// 3. Pick _just one_ of the remaining loops to fix | ||
// 4. Select the member shape in that loop with the earliest shape id | ||
// Execute 1 step of the boxing algorithm in the path to reaching a fixed point: | ||
// 1. Find all the shapes that are part of a cycle. | ||
// 2. Find all the loops that those shapes are part of. | ||
// 3. Filter out the loops that go through a layer of indirection. | ||
// 3. Pick _just one_ of the remaining loops to fix. | ||
// 4. Select the member shape in that loop with the earliest shape id. | ||
// 5. Box it. | ||
// (External to this function) Go back to 1. | ||
val index = TopologicalIndex.of(model) | ||
|
@@ -58,34 +66,32 @@ object RecursiveShapeBoxer { | |
// Flatten the connections into shapes. | ||
loops.map { it.shapes } | ||
} | ||
val loopToFix = loops.firstOrNull { !containsIndirection(it) } | ||
val loopToFix = loops.firstOrNull { !containsIndirectionPredicate(it) } | ||
|
||
return loopToFix?.let { loop: List<Shape> -> | ||
check(loop.isNotEmpty()) | ||
// pick the shape to box in a deterministic way | ||
// Pick the shape to box in a deterministic way. | ||
val shapeToBox = loop.filterIsInstance<MemberShape>().minByOrNull { it.id }!! | ||
ModelTransformer.create().mapShapes(model) { shape -> | ||
if (shape == shapeToBox) { | ||
shape.asMemberShape().get().toBuilder().addTrait(RustBoxTrait()).build() | ||
boxShapeFn(shape.asMemberShape().get()) | ||
} else { | ||
shape | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Check if a List<Shape> contains a shape which will use a pointer when represented in Rust, avoiding the | ||
* need to add more Boxes | ||
*/ | ||
private fun containsIndirection(loop: List<Shape>): Boolean { | ||
return loop.find { | ||
when (it) { | ||
is ListShape, | ||
is MapShape, | ||
is SetShape, -> true | ||
else -> it.hasTrait<RustBoxTrait>() | ||
} | ||
} != null | ||
/** | ||
* Check if a `List<Shape>` contains a shape which will use a pointer when represented in Rust, avoiding the | ||
* need to add more `Box`es. | ||
*/ | ||
private fun containsIndirection(loop: Collection<Shape>): Boolean = loop.find { | ||
when (it) { | ||
is CollectionShape, is MapShape -> true | ||
else -> it.hasTrait<RustBoxTrait>() | ||
} | ||
} | ||
} != null | ||
|
||
private fun addRustBoxTrait(shape: MemberShape): MemberShape = shape.toBuilder().addTrait(RustBoxTrait()).build() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to see some docs for these params. I was unfamiliar with the
::
syntax before I saw this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgot to add docs on the params. I've now added them.