mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-04-08 15:01:37 -04:00
In "package tofu" today we try to do everything using a generic acyclic graph model and generic graph walk, which _works_ but tends to make every other part of the problem very hard to follow because we rely a lot on sidecar shared mutable data structures to propagate results between the isolated operations. This is the beginning of an experimental new way to do it where the "graph" is implied by a model that more closely represents how the language itself works, with explicit modelling of the relationships between different types of objects and letting results flow directly from one object to another without any big shared mutable state. There's still a lot to do before this is actually complete enough to evaluate whether it's a viable new design, but I'm considering this a good starting checkpoint since there's enough here to run a simple test of propagating data all the way from input variables to output values via intermediate local values. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
203 lines
7.0 KiB
Go
203 lines
7.0 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package exprs
|
|
|
|
import (
|
|
"context"
|
|
"iter"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// Evaluate attempts to evaluate the given [Evaluable] in the given [Scope],
|
|
// either returning the resulting value or some error diagnostics describing
|
|
// problems that prevented successful evaluation.
|
|
//
|
|
// Some [Evaluable] implementations (or the symbols they refer to) can block
|
|
// on potentially-time-consuming operations, in which case they should respond
|
|
// gracefully to cancellation of the given context.
|
|
//
|
|
// It's valid to pass a nil Scope, representing that no symbols or functions
|
|
// are available at all. Note that HCL's JSON syntax treats that situation
|
|
// quite differently by taking JSON strings totally literally instead of
|
|
// trying to interpret them as HCL templates, and so switching to or from
|
|
// a nil scope is typically a breaking change for what's allowed in a
|
|
// particular position.
|
|
func Evaluate(ctx context.Context, what Evalable, scope Scope) (cty.Value, tfdiags.Diagnostics) {
|
|
hclCtx, diags := buildHCLEvalContext(ctx, what, scope)
|
|
if diags.HasErrors() {
|
|
return cty.DynamicVal.Mark(EvalError), diags
|
|
}
|
|
val, moreDiags := what.Evaluate(ctx, hclCtx)
|
|
diags = diags.Append(moreDiags)
|
|
if diags.HasErrors() {
|
|
val = val.Mark(EvalError)
|
|
}
|
|
return val, diags
|
|
}
|
|
|
|
func buildHCLEvalContext(ctx context.Context, what Evalable, scope Scope) (*hcl.EvalContext, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
ret := &hcl.EvalContext{}
|
|
if scope == nil {
|
|
// A nil scope represents that nothing at all is available, which HCL
|
|
// represents as an EvalContext with nothing defined inside it.
|
|
// Note that this causes significantly different behavior for HCL's
|
|
// JSON syntax.
|
|
return ret, diags
|
|
}
|
|
|
|
symbols, moreDiags := buildSymbolTable(ctx, what.References(), scope)
|
|
ret.Variables = symbols
|
|
diags = diags.Append(moreDiags)
|
|
|
|
funcs, moreDiags := buildFunctionTable(ctx, what.FunctionCalls(), scope)
|
|
ret.Functions = funcs
|
|
diags = diags.Append(moreDiags)
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func buildSymbolTable(ctx context.Context, refs iter.Seq[hcl.Traversal], scope Scope) (map[string]cty.Value, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// We'll first build an intermediate representation of the nested symbol
|
|
// tables, because a cty.ObjectVal result is immutable and so we need to
|
|
// collect up all of the attribute values in a mutable data structure
|
|
// and then freeze it into an immutable tree once complete.
|
|
nodes := make(map[string]*symbolTableTempNode)
|
|
References:
|
|
for traversal := range refs {
|
|
currentChildren := nodes
|
|
currentTable := SymbolTable(scope)
|
|
for i, step := range traversal {
|
|
// For our purposes here the distinction between TraverseAttr
|
|
// and TraverseRoot is unimportant, so we'll normalize to
|
|
// TraverseAttr.
|
|
if rootStep, ok := step.(hcl.TraverseRoot); ok {
|
|
step = hcl.TraverseAttr{
|
|
Name: rootStep.Name,
|
|
SrcRange: rootStep.SrcRange,
|
|
}
|
|
}
|
|
|
|
switch step := step.(type) {
|
|
case hcl.TraverseAttr:
|
|
attr, moreDiags := currentTable.ResolveAttr(step)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
continue
|
|
}
|
|
|
|
switch attr := attr.(type) {
|
|
case nestedSymbolTable:
|
|
if _, ok := currentChildren[step.Name]; !ok {
|
|
currentChildren[step.Name] = &symbolTableTempNode{
|
|
children: make(map[string]*symbolTableTempNode),
|
|
}
|
|
}
|
|
currentChildren = currentChildren[step.Name].children
|
|
currentTable = attr.SymbolTable
|
|
continue
|
|
case valueOf:
|
|
currentChildren[step.Name] = &symbolTableTempNode{
|
|
val: attr.Valuer,
|
|
remain: traversal[i+1:],
|
|
}
|
|
continue References // any remaining steps are dynamic steps through the final value, captured in "remain" above
|
|
}
|
|
default:
|
|
moreDiags := currentTable.HandleInvalidStep(tfdiags.SourceRangeFromHCL(step.SourceRange()))
|
|
diags = diags.Append(moreDiags)
|
|
continue References
|
|
}
|
|
}
|
|
// If we get here then we ran out of steps before we encountered a
|
|
// leaf Valuer, so this reference is incomplete and therefore invalid.
|
|
moreDiags := currentTable.HandleInvalidStep(tfdiags.SourceRangeFromHCL(traversal.SourceRange()))
|
|
diags = diags.Append(moreDiags)
|
|
}
|
|
|
|
ret, moreDiags := valuesForSymbolTableTempNodes(ctx, nodes)
|
|
diags = diags.Append(moreDiags)
|
|
return ret, diags
|
|
}
|
|
|
|
// symbolTableTempNode is an implementation detail of [buildSymbolTable] used
|
|
// as a mutable intermediate representation of the symbol table so that we
|
|
// can gradually assemble nested symbol tables and then turn them into
|
|
// immutable cty object values only at the end when the work is complete.
|
|
//
|
|
// A value of this type has EITHER val+remain or children alone populated.
|
|
type symbolTableTempNode struct {
|
|
val Valuer
|
|
remain hcl.Traversal
|
|
|
|
children map[string]*symbolTableTempNode
|
|
}
|
|
|
|
// valuesForSymbolTableTempNodes returns a map of [cty.Value] providing a
|
|
// frozen representation of the symbol tree rooted at the given map.
|
|
func valuesForSymbolTableTempNodes(ctx context.Context, symbols map[string]*symbolTableTempNode) (map[string]cty.Value, tfdiags.Diagnostics) {
|
|
if len(symbols) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
ret := make(map[string]cty.Value, len(symbols))
|
|
for name, node := range symbols {
|
|
if node.val != nil {
|
|
// This is a leaf node, with a dynamic value associated with it.
|
|
moreDiags := node.val.StaticCheckTraversal(node.remain)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
ret[name] = cty.DynamicVal
|
|
continue
|
|
}
|
|
|
|
// When we take the value of the object being referred to we
|
|
// intentionally ignore any _indirect_ diagnostics that might
|
|
// cause because we only want to report diagnostics that are
|
|
// directly related to the expression we're currently
|
|
// evaluating. Whatever problems might exist in the definition
|
|
// of what we're referring to must be caught by visiting
|
|
// that thing and evaluating it directly. This is safe to do
|
|
// because the definition of Valuer requires that Value must
|
|
// always return some reasonable placeholder value to use even
|
|
// when an error occurs.
|
|
val, _ := node.val.Value(ctx)
|
|
ret[name] = val
|
|
continue
|
|
}
|
|
|
|
// This is a nested symbol table, so we'll analyze its content recursively.
|
|
childVals, moreDiags := valuesForSymbolTableTempNodes(ctx, node.children)
|
|
diags = diags.Append(moreDiags)
|
|
ret[name] = cty.ObjectVal(childVals)
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
func buildFunctionTable(_ context.Context, calls iter.Seq[*hcl.StaticCall], scope Scope) (map[string]function.Function, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
ret := make(map[string]function.Function)
|
|
for call := range calls {
|
|
funcName := call.Name
|
|
impl, moreDiags := scope.ResolveFunc(call)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
continue
|
|
}
|
|
ret[funcName] = impl
|
|
}
|
|
return ret, diags
|
|
}
|