Custom expression decoding in function calls and hcldec specs#330
Merged
apparentlymart merged 3 commits intohcl2from Dec 17, 2019
Merged
Custom expression decoding in function calls and hcldec specs#330apparentlymart merged 3 commits intohcl2from
apparentlymart merged 3 commits intohcl2from
Conversation
This includes a new feature to allow extension properties associated with capsule types, which HCL can in turn use to allow certain capsule types to opt in to special handing at the HCL layer. (There are no such hooks in use as of this commit, however.)
Most of the time, the standard expression decoding built in to HCL is
sufficient. Sometimes though, it's useful to be able to customize the
decoding of certain arguments where the application intends to use them
in a very specific way, such as in static analysis.
This extension is an approximate analog of gohcl's support for decoding
into an hcl.Expression, allowing hcldec-based applications and
applications with custom functions to similarly capture and manipulate
the physical expressions used in arguments, rather than their values.
This includes one example use-case: the typeexpr extension now includes
a cty.Function called ConvertFunc that takes a type expression as its
second argument. A type expression is not evaluatable in the usual sense,
but thanks to cty capsule types we _can_ produce a cty.Value from one
and then make use of it inside the function implementation, without
exposing this custom type to the broader language:
convert(["foo"], set(string))
This mechanism is intentionally restricted only to "argument-like"
locations where there is a specific type we are attempting to decode into.
For now, that's hcldec AttrSpec/BlockAttrsSpec -- analogous to gohcl
decoding into hcl.Expression -- and in arguments to functions.
The try(...) and can(...) functions are intended to make it more convenient to work with deep data structures of unknown shape, by allowing a caller to concisely try a complex traversal operation against a value without having to guard against each possible failure mode individually. These rely on the customdecode extension to get access to their argument expressions directly, rather than only the results of evaluating those expressions. The expressions can then be evaluated in a controlled manner so that any resulting errors can be recognized and suppressed as appropriate.
mildwonkey
approved these changes
Dec 16, 2019
Contributor
mildwonkey
left a comment
There was a problem hiding this comment.
LGTM! It took longer to peruse the linked issues to make sure I understood the use case than review the code itself :)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
These changes include some new capabilities for those building languages on top of HCL, but I'm going to start by talking about the main motivating use-cases that led me here, which all HCL functions that require some special handling for one or more of their arguments:
convert(value, type)for generalized type conversion using a type expression instead of a value expression in the second argument, likeconvert([], set(string)). This can be useful when a value is being used as part of defining an API, like in Terraform Output Values, where we can be explicit about what result type we're intending and see an error if the value is not sufficiently close to that type to be converted successfully. (Terraform does already have functions liketoset(...)which get partway there, but they don't allow explicitly specifying element/attribute types.)try(expr, expr...)for trying multiple expressions in sequence and taking the value of the first one that succeeds. This one is primarily useful for working with complex data structures of an unknown shape, for similar reasons as discussed in lang/funcs: add jsonpath function terraform#22460 but without introducing a whole new traversal syntax into the language.can(expr), which is related totrybut allows using the success or failure of the given expression as a boolean value to make other decisions.These functions are all defined in extension packages, so merging them will not cause any change in behavior to any existing HCL caller immediately, but will allow each application to selectively opt-in to these if desired.
The underlying mechanism here is taking some inspiration from what is possible with both the low-level HCL API and with
gohcl, where it's possible to access the structuralhcl.Expressiondirectly and perform arbitrary analyses on it either instead of or prior to evaluating it.That sort of technique was previously unavailable in any situation which deals in
cty.Valueresults, because there was no way to opt in to that special decoding in those contexts. Using an extension mechanism built in tocty's "capsule types" mechanism, this introduces a new convention (whose public API is inext/customdecode) of specifying a specially-annotated capsule type as a type constraint for an argument. This is approximately analogous to using custom named types and struct field tags ingohcl, but it's handled completely at runtime withincty's type system instead.There are some higher-level helpers here aiming to see that for common use-cases calling applications won't need to work directly with that low-level convention and can instead just work with cty types or cty functions already defined here for convenient use.
Perhaps the most interesting building block, which is the foundation of both
try(...)andcan(...), is thecustomdecode.ExpressionClosureTypecapsule type: it allows any argument using it as a type constraint to capture both the physical expression and theEvalContextthat was passed to evaluate it. That means a function using this mechanism can delay evaluation of the expression while still retaining all of the same variables and functions that were available to it at original evaluation.By analogy to the
gohclfeatures using special types and struct tags, this custom decoding only applies to "argument-like" contexts, which for our purposes here is defined as the following two locations:hcldecattribute specifications whose type constraints are suitably-annotated capsule types, likewise allowing an attribute expression to be treated as raw syntax rather than as a value. (This is the closest analog to the equivalentgohclcapabilities, which Terraform uses for its special arguments likedepends_on, input variabletypearguments, etc.)Applications that have no need for these special capabilities can completely ignore them, by not importing any of the extension packages defined here. Although there are some small modifications to function call handling and
hcldecdecoding, those codepaths cannot be visited unless the calling program activates them through the featuers of these extension packages.