lang/exprs: Expression evaluation helpers

This package provides a more generic version of what's currently modeled
by the likes of lang.Scope and lang.Data, designed to avoid having a huge
single type that must know about everything in the language and, for this
package's purposes alone, to avoid knowing anything about the language at
all except that it uses HCL.

This is currently just an experiment not used by anything, and so is dead
code aside from the contrived mini-language implemented in example_test.go.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins
2025-08-26 18:54:33 -07:00
parent 730a60826c
commit df21c1d8dc
9 changed files with 1066 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
// 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.
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, diags
}
val, moreDiags := what.Evaluate(ctx, hclCtx)
diags = diags.Append(moreDiags)
return val, diags
}
func buildHCLEvalContext(ctx context.Context, what Evalable, scope Scope) (*hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
ret := &hcl.EvalContext{}
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
}
val, moreDiags := node.val.Value(ctx)
diags = diags.Append(moreDiags)
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
}