exprs

package
v1.12.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 13, 2026 License: MPL-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package exprs contains supporting code for expression evaluation.

This package is designed to know nothing about the referenceable symbol tree in any particular language, so that knowledge can be kept closer to the other code implementing the relevant language. This can therefore be shared across many different HCL-based languages, and across different evaluation phases of the same language.

Example (Simple)
package main

import (
	"context"
	"fmt"
	"strings"

	"github.com/davecgh/go-spew/spew"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty-debug/ctydebug"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
	"github.com/zclconf/go-cty/cty/function"

	"github.com/vmvarela/ghoten/internal/addrs"
	"github.com/vmvarela/ghoten/internal/configs/configschema"
	"github.com/vmvarela/ghoten/internal/lang/exprs"
	"github.com/vmvarela/ghoten/internal/tfdiags"
)

// The main code in this package is intentionally completely unaware of
// any specific symbol table structures in the language, with that logic
// living in concrete implementations of [Scope], [SymbolTable] and [Valuer]
// in other language-specific packages, but for testing purposes here we have a
// contrived "mini-language" that is intentionally shaped like a subset of the
// Ghoten module language to prove that this design is sufficient to handle
// that and to act as a relatively-concise overview of how a "real" use of this
// package might look.
//
// If a real implementation _were_ shaped like this then all of the types
// defined below would belong to some other package that implements the
// Ghoten planning phase. Variations of this could also appear in a package
// that implements the validation phase, but in that case it would deal only
// in unexpanded modules and resources. In both cases the types implementing
// [Scope] and [Valuer] would ideally also implement all of the other business
// logic related to whatever they represent to keep e.g. all of the logic
// related to resource evaluation together in one place, but package exprs only
// cares about their implementations of its interfaces.
//
// This example implementation has the significant limitation that it doesn't
// have any way of detecting and reporting reference cycles. If any appear then
// it'll just attempt infinite recursion and smash the stack. A real
// implementation would need to somehow detect and report cyclic references,
// e.g. by internally doing something like what this package does:
//    https://pkg.go.dev/github.com/apparentlymart/go-workgraph/workgraph

func main() {
	varDefs := exampleMustParseTfvars(`
		name = "stephen"
	`)
	modInst := exampleMustParseModule(`
		variable "name" {}

		resource "example" "foo" {
			name = var.name
		}

		resource "example" "bar" {
			name = example.foo.name
		}
	`, varDefs)

	barR := modInst.Resource(addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "example",
		Name: "bar",
	})
	barV, diags := barR.Value(context.Background())
	if diags.HasErrors() {
		panic(spew.Sdump(diags.ForRPC()))
	}

	fmt.Println(ctydebug.ValueString(barV))

}

// testResource represents a module instance, implementing [Scope].
type testModuleInstance struct {
	variables map[addrs.InputVariable]*testInputVariable
	resources map[addrs.Resource]*testResource
}

var _ exprs.Scope = (*testModuleInstance)(nil)

// Resource returns the resource with the given address, or nil if there is
// no such resource declared in the module.
func (t *testModuleInstance) Resource(addr addrs.Resource) *testResource {
	return t.resources[addr]
}

// HandleInvalidStep implements Scope.
func (t *testModuleInstance) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
	// NOTE: It isn't possible to get here in practice because we only use
	// this as a top-level scope and HCL's parser only allows TraverseRoot
	// at the start of a reference anyway, so we could only get in here
	// if an [Evalable.References] implementation returns something odd.
	var diags tfdiags.Diagnostics
	diags = diags.Append(&hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  "Invalid reference",
		Detail:   "Expected the name of a top-level symbol.",
		Subject:  rng.ToHCL().Ptr(),
	})
	return diags
}

// ResolveAttr implements Scope.
func (t *testModuleInstance) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
	// Note that handling this as part of the implementation a module, rather
	// than separately in package addrs, makes it easier for the resolution
	// rules to vary depending on which language edition and language
	// experiments the module is using, because in a real implementation this
	// object would have access to the module configuration.
	//
	// The extra symbols supported in .tftest.hcl files can also be handled
	// by having the test scenario type also implement Scope, handle the
	// test-language-specific symbols first, and then delegate to a wrapped
	// module object for everything else.

	switch ref.Name {
	case "var":
		return exprs.NestedSymbolTable(testInputVariables(t.variables)), nil
	case "resource":
		return exprs.NestedSymbolTable(&testResourcesOfMode{
			mode:         addrs.ManagedResourceMode,
			allResources: t.resources,
		}), nil
	case "data":
		return exprs.NestedSymbolTable(&testResourcesOfMode{
			mode:         addrs.DataResourceMode,
			allResources: t.resources,
		}), nil
	case "ephemeral":
		return exprs.NestedSymbolTable(&testResourcesOfMode{
			mode:         addrs.EphemeralResourceMode,
			allResources: t.resources,
		}), nil
	default:
		return exprs.NestedSymbolTable(&testResourcesOfType{
			mode:         addrs.ManagedResourceMode,
			typeName:     ref.Name,
			allResources: t.resources,
		}), nil
	}
}

// ResolveFunc implements Scope.
func (t *testModuleInstance) ResolveFunc(call *hcl.StaticCall) (function.Function, tfdiags.Diagnostics) {
	// A real implementation of this would probably look the function name up
	// in a map built elsewhere, rather than like this.
	switch call.Name {
	case "upper":
		return function.New(&function.Spec{
			Params: []function.Parameter{
				{
					Name: "str",
					Type: cty.String,
				},
			},
			Type: function.StaticReturnType(cty.String),
			Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
				// NOTE: This is not a robust implementation of "upper", just
				// a placeholder for the sake of this example.
				return cty.StringVal(strings.ToUpper(args[0].AsString())), nil
			},
		}), nil
	default:
		var diags tfdiags.Diagnostics
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Call to unknown function",
			Detail:   fmt.Sprintf("There is no function named %q.", call.Name),
			Subject:  &call.NameRange,
		})
		return function.Function{}, diags
	}
}

// testInputVariables is an intermediate [SymbolTable] dealing with the
// symbols under "var.".
type testInputVariables map[addrs.InputVariable]*testInputVariable

var _ exprs.SymbolTable = testInputVariables(nil)

// HandleInvalidStep implements exprs.SymbolTable.
func (t testInputVariables) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	diags = diags.Append(&hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  "Invalid reference to input variable",
		Detail:   "Expected an attribute name matching an input variable declared in this module.",
		Subject:  rng.ToHCL().Ptr(),
	})
	return diags
}

// ResolveAttr implements exprs.SymbolTable.
func (t testInputVariables) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics
	iv, ok := t[addrs.InputVariable{Name: ref.Name}]
	if !ok {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Reference to undeclared input variable",
			Detail:   fmt.Sprintf("There is no input variable named %q declared in this module.", ref.Name),
			Subject:  &ref.SrcRange,
		})
		return nil, diags
	}
	return exprs.ValueOf(iv), diags
}

type testInputVariable struct {
	addr       addrs.InputVariable
	targetType cty.Type
	rawVal     cty.Value
	valRange   tfdiags.SourceRange
}

var _ exprs.Valuer = (*testInputVariable)(nil)

// StaticCheckTraversal implements exprs.Valuer.
func (t *testInputVariable) TypeConstraint() cty.Type {
	// An input variable's "type" is a target type for conversion rather than
	// just a type constraint, so we need to discard any optional attribute
	// information to get a plain type constraint.
	return t.targetType.WithoutOptionalAttributesDeep()
}

// StaticCheckTraversal implements exprs.Valuer.
func (t *testInputVariable) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
	return exprs.StaticCheckTraversalThroughType(traversal, t.TypeConstraint())
}

// Value implements exprs.Valuer.
func (t *testInputVariable) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	// In a real implementation this type would probably not have the value
	// directly and would instead have an expression from an argument in
	// the calling "module" block, but we'll keep this relatively simple
	// for the sake of example.
	v, err := convert.Convert(t.rawVal, t.targetType)
	if err != nil {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Invalid value for input variable",
			Detail:   fmt.Sprintf("Unsuitable value for input variable %q: %s.", t.addr.Name, err),
			Subject:  t.valRange.ToHCL().Ptr(),
		})
		v = cty.UnknownVal(t.TypeConstraint())
	}
	return v, diags
}

// ValueSourceRange implements exprs.Valuer.
func (t *testInputVariable) ValueSourceRange() *tfdiags.SourceRange {
	return &t.valRange
}

// testInputVariables is an intermediate [SymbolTable] implementation dealing
// with symbols under "resource.", "data.", and "ephemeral.".
type testResourcesOfMode struct {
	mode         addrs.ResourceMode
	allResources map[addrs.Resource]*testResource
}

var _ exprs.SymbolTable = (*testResourcesOfMode)(nil)

// HandleInvalidStep implements exprs.SymbolTable.
func (t *testResourcesOfMode) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	diags = diags.Append(&hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  "Invalid reference to resource",
		Detail:   "Expected an attribute name matching the type of the resource to refer to.",
		Subject:  rng.ToHCL().Ptr(),
	})
	return diags
}

// ResolveAttr implements exprs.SymbolTable.
func (t *testResourcesOfMode) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
	// For now we'll just accept anything here and wait until we've collected
	// enough steps to form a complete addrs.Resource value.
	return exprs.NestedSymbolTable(&testResourcesOfType{
		mode:         t.mode,
		typeName:     ref.Name,
		allResources: t.allResources,
	}), nil
}

// testInputVariables is an intermediate [SymbolTable] implementation dealing
// with symbols under "resource.ANYTHING.", "data.ANYTHING.",
// "ephemeral.ANYTHING.", and "ANYTHING.".
type testResourcesOfType struct {
	mode         addrs.ResourceMode
	typeName     string
	allResources map[addrs.Resource]*testResource
}

var _ exprs.SymbolTable = (*testResourcesOfType)(nil)

// HandleInvalidStep implements exprs.SymbolTable.
func (t *testResourcesOfType) HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	diags = diags.Append(&hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  "Invalid reference to resource",
		Detail:   "Expected an attribute name matching the name of the resource to refer to.",
		Subject:  rng.ToHCL().Ptr(),
	})
	return diags
}

// ResolveAttr implements exprs.SymbolTable.
func (t *testResourcesOfType) ResolveAttr(ref hcl.TraverseAttr) (exprs.Attribute, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	// Once we reach this step we've collected enough information to
	// form a resource address.
	addr := addrs.Resource{
		Mode: t.mode,
		Type: t.typeName,
		Name: ref.Name,
	}
	rsrc, ok := t.allResources[addr]
	if !ok {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Reference to undeclared resource",
			Detail:   fmt.Sprintf("There is no resource %s declared in this module.", addr),
			Subject:  &ref.SrcRange,
		})
		return nil, diags
	}
	return exprs.ValueOf(rsrc), diags
}

// testResource represents a resource, implementing [Valuer].
//
// A real implementation of this would need to deal with multi-instance resources
// using count/for_each too, probably delegating to another type representing
// each individual resource instance, but we ignore that here because that
// complexity is an implementation detail irrelevant to package exprs.
type testResource struct {
	// config is the [Valuer] for the resource's configuration body.
	//
	// In practice this is an [*exprs.Closure] associating the actual HCL body
	// with the module instance where it was declared, but that is a concern
	// only for the code that constructs this object; the testResource
	// implementation only knows that it can obtain a value from here when
	// needed, without worrying about how that is achieved.
	//
	// (in a real implementation that supported multiple resource instances
	// we'd need to delay constructing the exprs.Valuer until constructing
	// individual resource instances, because in that case the resource instance
	// configs must close over a child scope that also has instance-specific
	// each.key/each.value/count.index in it, but this example is already
	// complicated enough so we'll skip that here.)
	config exprs.Valuer
}

var _ exprs.Valuer = (*testResource)(nil)

// StaticCheckTraversal implements exprs.Valuer.
func (t *testResource) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics {
	return t.config.StaticCheckTraversal(traversal)
}

// Value implements exprs.Valuer.
func (t *testResource) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
	return t.config.Value(ctx)
}

// ValueSourceRange implements exprs.Valuer.
func (t *testResource) ValueSourceRange() *tfdiags.SourceRange {
	return t.config.ValueSourceRange()
}

// exampleMustParseTfvars is a helper function just to make these contrived
// examples a little more concise, which tries to interpret the given string
// in a similar way to how Ghoten would normally deal with a ".tfvars" file.
func exampleMustParseTfvars(src string) map[string]variableDef {
	f, hclDiags := hclsyntax.ParseConfig([]byte(src), "example.tfvars", hcl.InitialPos)
	if hclDiags.HasErrors() {
		panic(fmt.Sprintf("invalid tfvars: %s", hclDiags.Error()))
	}
	attrs, hclDiags := f.Body.JustAttributes()
	if hclDiags.HasErrors() {
		panic(fmt.Sprintf("invalid tfvars: %s", hclDiags.Error()))
	}
	ret := make(map[string]variableDef, len(attrs))
	for name, attr := range attrs {
		v, hclDiags := attr.Expr.Value(nil)
		if hclDiags.HasErrors() {
			panic(fmt.Sprintf("invalid tfvars: %s", hclDiags.Error()))
		}
		ret[name] = variableDef{
			val: v,
			rng: tfdiags.SourceRangeFromHCL(attr.Expr.Range()),
		}
	}
	return ret
}

// exampleMustParseTfvars is a helper function just to make these contrived
// examples a little more concise, which tries to interpret the given string
// as the mini-language implemented in this example.
func exampleMustParseModule(src string, inputVals map[string]variableDef) *testModuleInstance {
	f, hclDiags := hclsyntax.ParseConfig([]byte(src), "config.minitofu", hcl.InitialPos)
	if hclDiags.HasErrors() {
		panic(fmt.Sprintf("invalid module: %s", hclDiags.Error()))
	}

	rootSchema := hcl.BodySchema{
		Blocks: []hcl.BlockHeaderSchema{
			{Type: "variable", LabelNames: []string{"name"}},
			{Type: "resource", LabelNames: []string{"type", "name"}},
			{Type: "data", LabelNames: []string{"type", "name"}},
			{Type: "ephemeral", LabelNames: []string{"type", "name"}},
		},
	}
	content, hclDiags := f.Body.Content(&rootSchema)
	if hclDiags.HasErrors() {
		panic(fmt.Sprintf("invalid module: %s", hclDiags.Error()))
	}

	modInst := &testModuleInstance{
		variables: make(map[addrs.InputVariable]*testInputVariable),
		resources: make(map[addrs.Resource]*testResource),
	}
	for _, block := range content.Blocks {
		switch block.Type {
		case "variable":
			addr := addrs.InputVariable{Name: block.Labels[0]}
			def, ok := inputVals[addr.Name]
			if !ok {
				panic(fmt.Sprintf("no value for input variable %q", addr.Name))
			}
			modInst.variables[addr] = &testInputVariable{
				addr:       addr,
				targetType: cty.String, // only strings to keep this example simpler
				rawVal:     def.val,
				valRange:   def.rng,
			}
		case "resource", "data", "ephemeral":
			addr := addrs.Resource{
				Mode: map[string]addrs.ResourceMode{
					"resource":  addrs.ManagedResourceMode,
					"data":      addrs.DataResourceMode,
					"ephemeral": addrs.EphemeralResourceMode,
				}[block.Type],
				Type: block.Labels[0],
				Name: block.Labels[1],
			}
			typeAddr := resourceType{
				Mode: addr.Mode,
				Name: addr.Type,
			}
			schema, ok := resourceTypes[typeAddr]
			if !ok {
				panic(fmt.Sprintf("unsupported resource type %#v", typeAddr))
			}
			modInst.resources[addr] = &testResource{
				config: exprs.NewClosure(
					exprs.EvalableHCLBody(block.Body, schema.DecoderSpec()),
					modInst,
				),
			}
		}
	}
	return modInst
}

type variableDef struct {
	val cty.Value
	rng tfdiags.SourceRange
}

type resourceType struct {
	Mode addrs.ResourceMode
	Name string
}

var resourceTypes = map[resourceType]*configschema.Block{
	resourceType{addrs.ManagedResourceMode, "example"}: &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"name": &configschema.Attribute{
				Type:     cty.String,
				Required: true,
			},
		},
	},
}
Output:
cty.ObjectVal(map[string]cty.Value{
    "name": cty.StringVal("stephen"),
})

Index

Examples

Constants

View Source
const EvalError = evalResultMark('E')

EvalError is a cty.Value mark used on placeholder unknown values returned whenever evaluation causes error diagnostics.

This is intended only for use between collaborators in a subsystem where everyone is consistently following this convention as a means to avoid redundantly reporting downstream consequences of an upstream problem. Use WithoutEvalErrorMarks at the boundary of such a subsystem so that code in other parts of the system does not need to deal with these marks.

In many cases it's okay to ignore this mark and just use the unknown value placeholder as normal, letting the mark "infect" the result as necessary, but this is here for less common situations where logic _does_ need to handle those situations differently.

For example, if a particular language feature treats the mere presence of an unknown value as an error then the error-handling logic should first check whether the value has this mark and only return the unknown-value-related error if not, because the presence of this mark suggests that the unknown value is likely caused by another upstream error rather than by the module author directly using an unknown value in an invalid location.

The expression evaluation mechanisms in this package add this mark automatically whenever they generate evaluation error placeholders, but it's exposed as an exported symbol so that logic elsewhere that is performing non-expression-based evaluation can participate in this marking scheme.

Variables

This section is empty.

Functions

func AsEvalError

func AsEvalError(v cty.Value) cty.Value

AsEvalError returns the given value with EvalError applied to it as a mark.

The expression evaluator in this package automatically adds this mark when expression evaluation fails, but code elsewhere in the system should use this to also treat other kinds of errors as evaluation errors if they are returning a placeholder value alongside at least one error diagnostic.

func EvalResult

func EvalResult(v cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics)

EvalResult is a helper that checks whether the given diags contains errors and if so returns the given value with EvalError applied to it as a mark.

Otherwise it returns the value unmodified. In all cases it returns exactly the diagnostics it was given.

This is designed for concise use in a return statement in a function that's returning both a value and some diagnostics produced from somewhere else, to ensure that the EvalError mark still gets applied when appropriate.

func Evaluate

func Evaluate(ctx context.Context, what Evalable, scope Scope) (cty.Value, tfdiags.Diagnostics)

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 HasEvalErrors

func HasEvalErrors(v cty.Value) bool

HasEvalErrors is like IsEvalError except that it visits nested values inside the given value recursively and returns true if EvalError marks are present at any nesting level.

Don't use this except when rejecting values that contain nested unknown values in a context where those values are not allowed. If only _shallow_ unknown values are disallowed then use IsEvalError instead to match that with only a shallow check for the EvalError mark.

func IsEvalError

func IsEvalError(v cty.Value) bool

IsEvalError returns true if the given value is directly marked with EvalError, indicating that it's acting as a placeholder for an upstream failed evaluation.

This only checks the given value shallowly. Use HasEvalErrors instead to check whether there are any evaluation error placeholders in nested values. For example, a caller that is using [`cty.Value.IsWhollyKnown`] to reject a value with unknown values anywhere inside it should prefer to use HasEvalErrors first to determine if any of the nested unknown values might actually be error placeholders.

func StaticCheckTraversal

func StaticCheckTraversal(traversal hcl.Traversal, evalable Evalable) tfdiags.Diagnostics

func StaticCheckTraversalThroughType

func StaticCheckTraversalThroughType(traversal hcl.Traversal, typeConstraint cty.Type) tfdiags.Diagnostics

func TraversalStepAttributeName

func TraversalStepAttributeName(step hcl.Traverser) (string, bool)

TraversalStepAttributeName attempts to interpret the given traversal step in a manner compatible with how hcl.Index would apply it to a value of an object type, returning the name of the attribute it would access.

If the second return value is false then the given step is not valid to use in that situation.

This is mainly for use in [Valuer.StaticCheckTraversal] implementations where the Value method would return an object type, to find out which attribute name the first traversal step would ultimately select.

func WithoutEvalErrorMarks

func WithoutEvalErrorMarks(v cty.Value) cty.Value

WithoutEvalErrorMarks returns the given value with any shallow or nested EvalError marks removed.

Use this at the boundary of a subsystem that uses the evaluation error marking scheme internally as an implementation detail, to avoid exposing this extra complexity to callers that are merely consuming the finalized results.

Types

type Attribute

type Attribute interface {
	// contains filtered or unexported methods
}

Attribute is implemented for the fixed set of types that can be returned from [SymbolTable.ResolveAttr] implementations.

This is a closed interface implemented only by types within this package.

func NestedSymbolTable

func NestedSymbolTable(table SymbolTable) Attribute

NestedSymbolTable constructs an Attribute representing a nested symbol table, which must therefore be traversed through by a subsequent attribute step.

For example, a module instance acting as a symbol table would respond to a lookup of the attribute "var" by returning a nested symbol table whose symbols correspond to all of the input variables declared in the module, so that a reference like "var.foo" would then look up "foo" in the nested table.

func ValueOf

func ValueOf(v Valuer) Attribute

ValueOf constructs an Attribute representing the endpoint of a static traversal, where a dynamic value should be placed.

type ChildScopeBuilder

type ChildScopeBuilder func(ctx context.Context, parent Scope) Scope

ChildScopeBuilder is the signature for a function that can build a child scope that wraps some given parent scope.

A nil ChildScopeBuilder represents that no child scope is needed and the parent should just be used directly. Use ChildScopeBuilder.Build instead of directly calling the function to obtain that behavior automatically.

func (ChildScopeBuilder) Build

func (b ChildScopeBuilder) Build(ctx context.Context, parent Scope) Scope

type Closure

type Closure struct {
	// contains filtered or unexported fields
}

Closure is an Evalable bound to the Scope where it was declared, so that the two can travel together.

Closure essentially turns an Evalable into a Valuer, allowing it to be evaluated without separately tracking the scope it belongs to.

func NewClosure

func NewClosure(evalable Evalable, scope Scope) *Closure

NewClosure associates the given Evalable with the given Scope so that it can be evaluated somewhere else later without losing track of what symbols and functions were available where it was declared.

Passing a nil Scope is valid, and represents that there are absolutely no symbols or functions available for use in the given Evalable. 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 (*Closure) StaticCheckTraversal

func (c *Closure) StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics

StaticCheckTraversal checks whether the given traversal could apply to any possible result from Closure.Value on this closure, returning error diagnostics if not.

func (*Closure) Value

func (c *Closure) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics)

Value returns the result of evaluating the enclosed Evalable in the enclosed Scope.

Some Evalable implementations block on potentially-time-consuming operations, in which case they should respond gracefully to cancellation of the given context.

func (*Closure) ValueSourceRange

func (c *Closure) ValueSourceRange() *tfdiags.SourceRange

SourceRange returns the source range of the underlying Evalable.

type Evalable

type Evalable interface {
	// References returns a sequence of references this Evalable makes to
	// values in its containing scope.
	References() iter.Seq[hcl.Traversal]

	// FunctionCalls returns a sequence of all of the function calls that
	// could be made if this were evaluated.
	//
	// TODO: Perhaps References and FunctionCalls should be combined together
	// somehow to return a tree that shows when a reference appears as part
	// of an argument to a function, to address the problem described in
	// this issue:
	//     https://github.com/vmvarela/ghoten/issues/2630
	FunctionCalls() iter.Seq[*hcl.StaticCall]

	// Evaluate performs the actual expression evaluation, using the given
	// HCL evaluation context to satisfy any references.
	//
	// Callers must first use the References method to discover what the
	// wrapped expressions refer to, and make sure that the given evaluation
	// context contains at least the variables required to satisfy those
	// references.
	//
	// This method takes a [context.Context] because some implementations
	// may internally block on the completion of a potentially-time-consuming
	// operation, in which case they should respond gracefully to the
	// cancellation or deadline of the given context.
	//
	// If Evaluate returns diagnostics then it must also return a suitable
	// placeholder value that could be use for downstream expression evaluation
	// despite the error. Returning [cty.DynamicVal] is acceptable if all else
	// fails, but returning an unknown value with a more specific type
	// constraint can give more opportunities to proactively detect downstream
	// errors in a single evaluation pass.
	Evaluate(ctx context.Context, hclCtx *hcl.EvalContext) (cty.Value, tfdiags.Diagnostics)

	// ResultTypeConstraint returns a type constrant that all possible results
	// from method Evaluate would conform to.
	//
	// This is used for static type checking. Return [cty.DynamicPseudoType]
	// if it's impossible to predict any single type constraint for the
	// possible results.
	//
	// TODO: Some implementations of this would be able to do better if they
	// knew the types of everything that'd be passed in hclCtx when calling
	// Evaluate. Is there some way we can approximate that?
	ResultTypeConstraint() cty.Type

	// EvalableSourceRange returns a description of a source location that this
	// Evalable was derived from.
	EvalableSourceRange() tfdiags.SourceRange
}

Evalable is implemented by types that encapsulate expressions that can be evaluated in some evaluation scope decided by a caller.

An Evalable implementation must include any supporting metadata needed to analyze and evaluate the expressions inside. For example, an Evalable representing a HCL body must also include the expected schema for that body.

func EvalableHCLBody

func EvalableHCLBody(body hcl.Body, spec hcldec.Spec) Evalable

EvalableHCLBody returns an Evalable that evaluates the given HCL body using the given hcldec specification.

func EvalableHCLBodyJustAttributes

func EvalableHCLBodyJustAttributes(body hcl.Body) Evalable

EvalableHCLBody returns an Evalable that evaluates the given HCL body in HCL's "just attributes" mode, and then returns an object value whose attribute names and values are derived from the result.

func EvalableHCLBodyWithDynamicBlocks

func EvalableHCLBodyWithDynamicBlocks(body hcl.Body, spec hcldec.Spec) Evalable

EvalableHCLBodyWithDynamicBlocks is a variant of EvalableHCLBody that calls dynblock.Expand before evaluating the body so that "dynamic" blocks would be supported and expanded to their equivalent static blocks.

func EvalableHCLExpression

func EvalableHCLExpression(expr hcl.Expression) Evalable

EvalableHCLExpression returns an Evalable that is just a thin wrapper around the given HCL expression.

func ForcedErrorEvalable

func ForcedErrorEvalable(diags tfdiags.Diagnostics, sourceRange tfdiags.SourceRange) Evalable

ForcedErrorEvalable returns an Evalable that always fails with cty.DynamicVal as its placeholder result and with the given diagnostics, which must include at least one error or this function will panic.

This is primarily intended for unit testing purposes for creating placeholders for upstream objects that have failed, but might also be useful sometimes for handling early-detected error situations in "real" code.

type Scope

type Scope interface {
	SymbolTable

	// ResolveFunc looks up a function by name, either returning its
	// implementation or error diagnostics if no such function exists.
	ResolveFunc(call *hcl.StaticCall) (function.Function, tfdiags.Diagnostics)
}

Scope is implemented by types representing containers that can have expressions evaluated within them.

For example, it might make sense for a type representing a module instance to implement this interface to describe the symbols and functions that result from the declarations in that module instance. In that case, the module instance _is_ the scope, rather than the scope being a separate thing derived from that module instance.

A Scope is essentially just an extension of SymbolTable which also includes a table of functions.

type SymbolTable

type SymbolTable interface {
	// ResolveSymbol looks up a symbol by name, either returning what it
	// refers to or error diagnostics if no such symbol exists.
	ResolveAttr(ref hcl.TraverseAttr) (Attribute, tfdiags.Diagnostics)

	// HandleInvalidStep is called if a reference contains anything other
	// than an attribute access at a position handled by a symbol table,
	// so that the symbol table can produce a specialized error message
	// explaining what kind of attributes are expected.
	//
	// The given source range refers either to the non-attribute step that
	// was encountered or, if the problem is that nothing was present at all,
	// then to the entire reference expression visited so far.
	//
	// The result of this method MUST include at least one error diagnostic.
	HandleInvalidStep(rng tfdiags.SourceRange) tfdiags.Diagnostics
}

SymbolTable is an interface implemented by types that have an associated symbol table, meaning that they contain a set of attributes that can be looked up by name.

func NestedSymbolTableFromAttribute

func NestedSymbolTableFromAttribute(attr Attribute) SymbolTable

NestedSymbolTableFromAttribute returns the symbol table from an attribute that was returned from NestedSymbolTable, or nil for any other kind of attribute.

type Valuer

type Valuer interface {
	// Value returns the [cty.Value] representation of this object.
	//
	// This method takes a [context.Context] because some implementations
	// may internally block on the completion of a potentially-time-consuming
	// operation, in which case they should respond gracefully to the
	// cancellation or deadline of the given context.
	//
	// If Value returns diagnostics then it must also return a suitable
	// placeholder value that could be use for downstream expression evaluation
	// despite the error. Returning [cty.DynamicVal] is acceptable if all else
	// fails, but returning an unknown value with a more specific type
	// constraint can give more opportunities to proactively detect downstream
	// errors in a single evaluation pass.
	Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics)

	// StaticCheckTraversal checks whether the given relative traversal would
	// be valid to apply to all values that the ExprValue method could possibly
	// return without doing any expensive work to finalize that value, returning
	// diagnostics describing any problems.
	//
	// A typical implementation of this would be to check whether the
	// traversal makes sense for whatever type constraint applies to all of
	// the values that could possibly be returned. However, it's valid to
	// just immediately return no diagnostics if it's impossible to predict
	// anything about the value, in which case errors will be caught dynamically
	// once the value has been finalized.
	//
	// This function should only return errors that should not be interceptable
	// by the "try" or "can" functions in the Ghoten language.
	StaticCheckTraversal(traversal hcl.Traversal) tfdiags.Diagnostics

	// ValueSourceRange returns an optional source range where this value (or an
	// expression that produced it) was declared in configuration.
	//
	// Returns nil for a valuer that does not come from configuration.
	ValueSourceRange() *tfdiags.SourceRange
}

Valuer is implemented by objects that can be directly represented by a cty.Value.

This is a similar idea to Evalable, but with a subtle difference: a Valuer represents a value directly, whereas an Evalable represents an expression that can be evaluated to produce a value. In practice some [Valuer]s will produce their result by evaluating an Evalable, but that's an implementation detail that the consumer of this interface is not aware of.

An Evalable can be turned into a Valuer by associating it with a Scope, using NewClosure.

func ConstantValuer

func ConstantValuer(v cty.Value) Valuer

ConstantValuer returns a Valuer that always succeeds and returns exactly the value given.

func ConstantValuerWithSourceRange

func ConstantValuerWithSourceRange(v cty.Value, rng tfdiags.SourceRange) Valuer

ConstantValuerWithSourceRange is like ConstantValuer except that the result will also claim to have originated in the configuration at whatever source range is given.

func DerivedValuer

func DerivedValuer(source Valuer, project func(cty.Value, tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics)) Valuer

DerivedValuer returns a Valuer that first evaluates the source valuer and then passes its results to the "project" function, before returning whatever that returns.

The source range of the returned valuer is the same as the source valuer.

func ForcedErrorValuer

func ForcedErrorValuer(diags tfdiags.Diagnostics) Valuer

ForcedErrorValuer returns a Valuer that always fails with cty.DynamicVal as its placeholder result and with the given diagnostics, which must include at least one error or this function will panic.

This is primarily intended for unit testing purposes for creating placeholders for upstream objects that have failed, but might also be useful sometimes for handling unusual situations in "real" code.

func ForcedErrorValuerWithSourceRange

func ForcedErrorValuerWithSourceRange(diags tfdiags.Diagnostics, rng tfdiags.SourceRange) Valuer

ConstantValuerWithSourceRange is like ForcedErrorValuer except that the result will also claim to have originated in the configuration at whatever source range is given.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL