Skip to content
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

Allow can and try functions to return known results with unknown values in the arguments #622

Merged
merged 1 commit into from Aug 25, 2023

Conversation

jbardin
Copy link
Member

@jbardin jbardin commented Aug 14, 2023

The can and try functions can return more precise results in some cases. Rather than try to inspect the expressions for any unknown values, rely on the evaluation result to be correct or error and base the decision on the evaluated values and errors.

A fundamental requirement for the try and can functions is that the value and types remain consistent as argument values are refined. This can be done provided we hold these conditions regarding unknowns to be true:

  • An evaluation error can never be fixed by an unknown value becoming known.
  • An entirely known value from an expression cannot later become unknown as values are refined.
  • An expression result must always have a consistent type and value, only allowing the refinement of unknown values and types.
  • An expression result cannot be conditionally based on the "known-ness" of a value (which is really the premise for all previous statements).

As long as those conditions remain true, the result of the try argument's evaluation can be trusted, and we don't need to bail out early at any sign of an unknown in the argument expressions.

Another probably stronger case for determining the result only based on the argument evaluation, is that the result of an expression stored in a variable should be handled consistently with the same argument given in-line. For example in Terraform these currently return 2 different results:

output "can_nonsensitive_inline" {
  value = can(nonsensitive(uuid()))
}

locals {
  uuid = uuid()
}

output "can_nonsensitive_local" {
  value = can(nonsensitive(local.uuid))
}

While the evaluation result of each argument can be trusted in isolation however, the fact that different types and values can be returned by try's multiple arguments means we need to convert the return to the most generic value possible to prevent inconsistent results ourself (adhering to the 3rd condition above). That means anything which is not entirely known must be converted to a dynamic value.

Even more refinement might still be possible in the future if all arguments are evaluated and compared for compatibility, but care needs to be taken to prevent changing known values within collections from different arguments even when types are identical (as shown in the nested index op on unknown test).

The `can` and `try` functions can return more precise results in some
cases. Rather than try to inspect the expressions for any unknown
values, rely on the evaluation result to be correct or error and base
the decision on the evaluated values and errors.

A fundamental requirement for the `try` and `can` functions is that the
value and types remain consistent as argument values are refined. This
can be done provided we hold these conditions regarding unknowns to be
true:
 - An evaluation error can never be fixed by an unknown value becoming
   known.
 - An entirely known value from an expression cannot later become
   unknown as values are refined.
 - A expression result must always have a consistent type and value.
   only allowing the refinement of unknown values and types.
 - An expression result cannot be conditionally based on the
   "known-ness" of a value (which is really the premise for all previous
   statements).

As long as those conditions remain true, the result of the `try`
argument's evaluation can be trusted, and we don't need to bail out
early at any sign of an unknown in the argument expressions.

While the evaluation result of each argument can be trusted in isolation
however, the fact that different types and values can be returned by
`try` means we need to convert the return to the most generic value
possible to prevent inconsistent results ourself (adhering to the 3rd
condition above). That means anything which is not entirely known must
be converted to a dynamic value.

Evan more refinement might still be possible in the future if all
arguments are evaluated and compared for compatibility, but care needs
to be taken to prevent changing known values within collections from
different arguments even when types are identical.
@jbardin jbardin requested a review from a team August 14, 2023 18:12
@jbardin jbardin changed the title Allow can and try functions to handle more unknown Allow can and try functions to return known results with unknown values in the arguments Aug 14, 2023
Copy link
Member

@apparentlymart apparentlymart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had some realtime discussion about how the fact that this was unusually complicated before suggests that we'd had something in mind that made us think this was necessary, but having thought freshly about various different edge-cases I wasn't able to identify any problems with the argument in the PR description.

I do still have some anxiety that we're missing something here but if so it's gonna be something very edge-casey, and I doubt it will have any worse outcome than the odd behaviors these functions currently have when presented with combinations of unknown and marked values.

@jbardin jbardin merged commit a9f8d65 into main Aug 25, 2023
7 checks passed
@jbardin jbardin deleted the jbardin/can-try-unknowns branch August 25, 2023 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants