private void updateMixins(Model model, Model.Builder builder, Collection<Shape> replacements) {
        // Create a map to function as a mutable kind of intermediate model index so that as
        // shapes are updated and built, they're used as mixins in shapes that depend on it.
        Map<ShapeId, Shape> updatedShapes = new HashMap<>();
        for (Shape replaced : replacements) {
            updatedShapes.put(replaced.getId(), replaced);
        }

        // Topologically sort the updated shapes to ensure shapes are updated in order.
        TopologicalShapeSort sorter = new TopologicalShapeSort();

        // Add shapes that are mixins or use mixins.
        for (Shape shape : model.toSet()) {
            if (!shape.isMemberShape() && (shape.hasTrait(MixinTrait.ID) || !shape.getMixins().isEmpty())) {
                sorter.enqueue(shape);
            }
        }

        // Add _all_ of the replacements in case mixins or the Mixin trait were removed from updated shapes.
        for (Shape shape : replacements) {
            // Enqueue member shapes with empty dependencies since the parent will be enqueued with them.
            if (shape.isMemberShape()) {
                sorter.enqueue(shape.getId(), Collections.emptySet());
            } else {
                sorter.enqueue(shape);
            }
        }

        List<ShapeId> sorted = sorter.dequeueSortedShapes();
        for (ShapeId toRebuild : sorted) {
            Shape shape = updatedShapes.containsKey(toRebuild)
                    ? updatedShapes.get(toRebuild)
                    : model.expectShape(toRebuild);
            if (!shape.getMixins().isEmpty()) {
                // We don't clear mixins here because a shape might have an inherited
                // mixin member that was updated with an applied trait. Clearing mixins
                // would remove this member but not re-add it properly. Re-adding already
                // present mixins, however, will update members in-place.
                AbstractShapeBuilder<?, ?> shapeBuilder = Shape.shapeToBuilder(shape);
                for (ShapeId mixin : shape.getMixins()) {
                    Shape mixinShape = updatedShapes.containsKey(mixin)
                            ? updatedShapes.get(mixin)
                            : model.expectShape(mixin);
                    shapeBuilder.addMixin(mixinShape);
                }
                Shape rebuilt = shapeBuilder.build();
                builder.addShape(rebuilt);
                updatedShapes.put(rebuilt.getId(), rebuilt);
            }
        }
    }
Replaces shapes while ensuring that the transformed model is in a
consistent state.

<p>This transformer accounts for the following scenarios:

<ul>
    <li>When a member is modified, ensures that its containing shape
    references the new member.</li>
    <li>When an aggregate shape is modified, ensures that all members are
    updated in the model.</li>
    <li>When a member is removed from a structure or union,
    ensures that the member is removed from the model.</li>
    <li>When a mixin is updated, ensures that all shapes that use the
    mixin are updated.</li>
</ul>

<p>Only shapes that are not currently in a model or shapes that are
different than the existing shape in a model are inserted into a
transformed model.

<p>When updates are made both to a container shape and to a member owned
by the container shape, the member updates takes precedent over any
updated made to the old member referenced by the container (e.g., if you
update a structure and change one of the members of the structure without
also updating the member in the model *and* you update a member in
the model, the member in the model will overwrite the member referenced
by the structure).

<p>This transformer only supports replacing shapes if the previous shape
and new shape are of the same type. Any replacements encountered that
attempt to change the type of a shape will throw an exception.
smithy-model/src/main/java/software/amazon/smithy/model/transform/ReplaceShapes.java