I don’t think what you are describing could be directly caused by dynamic blocks, but I think the fact that function_association is a nested block type inside another resource – rather than a resource type in its own right – is part of the explanation for why this is happening.
Specifically, I think the problem here is that from Terraform Core’s perspective removing a function_association block from aws_cloudfront_distribution.x looks like an in-place update of the containing resource, rather than as something being deleted. Therefore the created plan includes (at least) two actions:
Update aws_cloudfront_distribution.x to now have zero function_association blocks.
Delete aws_cloudfront_function.x altogether.
The interaction between an update and a delete is one of those things where the “correct” ordering varies depending on the situation. In this case by thinking about how the remote system behaves we can clearly see that the function must be disassociated from the CloudFront distribution before destroying it, but for other resource types representing other API designs the reverse is true.
Unfortunately I don’t have a good suggestion for how to avoid this problem with the way these resource types are currently designed. If the association between a CloudFront Distribution and a CloudFront Function were itself represented by a resource then I expect Terraform would deduce the ordering correctly, but because the provider represents the function association as if it’s just an argument of the distribution itself the ordering is ambiguous and Terraform is reaching the wrong conclusion for this situation.
As mentioned already, the natural order that Terraform assumes is to destroy resources before replacing them or updating dependencies. If the aws_cloudfront_distribution resource needs to be updated before the aws_cloudfront_function resource is deleted, then you must use the create_before_destroy lifecycle option to instruct terraform that the inverse order is desired.