mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-05-16 07:01:54 -04:00
Switch to workgraph for symlib
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
320
internal/configs/symlib/eval.go
Normal file
320
internal/configs/symlib/eval.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package symlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
type scope struct {
|
||||
vars map[string]map[string]result[cty.Value]
|
||||
types map[string]result[typeWithDefault]
|
||||
funcs map[string]result[function.Function]
|
||||
|
||||
builtinFuncs map[string]function.Function
|
||||
libraries map[string]*scope
|
||||
// TODO parent scope for functions
|
||||
}
|
||||
|
||||
type typeWithDefault struct {
|
||||
ty cty.Type
|
||||
def *typeexpr.Defaults
|
||||
}
|
||||
|
||||
func newScope(builtinFuncs map[string]function.Function) *scope {
|
||||
return &scope{
|
||||
vars: map[string]map[string]result[cty.Value]{"const": {}},
|
||||
types: map[string]result[typeWithDefault]{},
|
||||
funcs: map[string]result[function.Function]{},
|
||||
libraries: map[string]*scope{},
|
||||
|
||||
builtinFuncs: builtinFuncs,
|
||||
}
|
||||
}
|
||||
|
||||
func once[V any](fn result[V]) result[V] {
|
||||
var mu sync.Mutex
|
||||
type T withDiags[V]
|
||||
var promise workgraph.Promise[T]
|
||||
var resolver workgraph.Resolver[T]
|
||||
needsSetup := true
|
||||
|
||||
return func(w *workgraph.Worker) (V, hcl.Diagnostics) {
|
||||
mu.Lock()
|
||||
if needsSetup {
|
||||
resolver, promise = workgraph.NewRequest[T](w)
|
||||
}
|
||||
neededSetup := needsSetup
|
||||
needsSetup = false
|
||||
mu.Unlock()
|
||||
|
||||
if neededSetup {
|
||||
val, diags := fn(w)
|
||||
resolver.Report(w, T{val, diags}, nil)
|
||||
}
|
||||
val, err := promise.Await(w)
|
||||
if err != nil {
|
||||
val.diags = val.diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Workgraph error",
|
||||
Detail: err.Error(),
|
||||
Extra: err,
|
||||
})
|
||||
}
|
||||
return val.value, val.diags
|
||||
}
|
||||
}
|
||||
|
||||
func (s *scope) clone() *scope {
|
||||
ns := &scope{
|
||||
vars: map[string]map[string]result[cty.Value]{},
|
||||
types: s.types,
|
||||
funcs: s.funcs,
|
||||
libraries: s.libraries,
|
||||
|
||||
builtinFuncs: s.builtinFuncs,
|
||||
}
|
||||
|
||||
for rk, rv := range s.vars {
|
||||
ns.vars[rk] = map[string]result[cty.Value]{}
|
||||
maps.Copy(ns.vars[rk], rv)
|
||||
}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
func (s *scope) typeContext(w *workgraph.Worker, typeExpr hcl.Expression) (typeexpr.TypeContext, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
prefix := "library::"
|
||||
suffix := "types"
|
||||
selfNs := prefix + suffix
|
||||
|
||||
typeCtx := typeexpr.TypeContext{
|
||||
Types: map[string]map[string]cty.Type{selfNs: {}},
|
||||
Defaults: map[string]map[string]*typeexpr.Defaults{selfNs: {}},
|
||||
}
|
||||
// Add all libraries, we could do less work based on missing below
|
||||
// Minor opt at this point, not worth it
|
||||
for lname, lib := range s.libraries {
|
||||
lNs := prefix + lname + "::" + suffix
|
||||
typeCtx.Types[lNs] = map[string]cty.Type{}
|
||||
typeCtx.Defaults[lNs] = map[string]*typeexpr.Defaults{}
|
||||
for tname, fn := range lib.types {
|
||||
val, vDiags := fn(w)
|
||||
diags = diags.Extend(vDiags)
|
||||
typeCtx.Types[lNs][tname] = val.ty
|
||||
typeCtx.Defaults[lNs][tname] = val.def
|
||||
}
|
||||
}
|
||||
|
||||
if typeExpr != nil {
|
||||
missing, mDiags := typeCtx.TypeDependencies(typeExpr)
|
||||
diags = diags.Extend(mDiags)
|
||||
for _, ty := range missing[selfNs] {
|
||||
if fn, ok := s.types[ty]; ok {
|
||||
val, vDiags := fn(w)
|
||||
diags = diags.Extend(vDiags)
|
||||
typeCtx.Types[selfNs][ty] = val.ty
|
||||
typeCtx.Defaults[selfNs][ty] = val.def
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for tn, fn := range s.types {
|
||||
val, vDiags := fn(w)
|
||||
diags = diags.Extend(vDiags)
|
||||
typeCtx.Types[selfNs][tn] = val.ty
|
||||
typeCtx.Defaults[selfNs][tn] = val.def
|
||||
}
|
||||
}
|
||||
|
||||
return typeCtx, diags
|
||||
}
|
||||
|
||||
func (s *scope) evalContext(w *workgraph.Worker, expr hcl.Expression) (*hcl.EvalContext, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
vars := map[string]map[string]cty.Value{"library": {}}
|
||||
|
||||
for lname, lib := range s.libraries {
|
||||
// TODO opt with expr deps
|
||||
consts := map[string]cty.Value{}
|
||||
for cn, fn := range lib.vars["const"] {
|
||||
val, vDiags := fn(w)
|
||||
diags = diags.Extend(vDiags)
|
||||
consts[cn] = val
|
||||
}
|
||||
// TODO consts in name?
|
||||
vars["library"][lname] = cty.ObjectVal(consts)
|
||||
}
|
||||
|
||||
if expr != nil {
|
||||
for _, trav := range expr.Variables() {
|
||||
if len(trav) < 2 {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "TODO unformed1",
|
||||
Detail: fmt.Sprintf("%#v", trav),
|
||||
Subject: trav.SourceRange().Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
root := trav[0].(hcl.TraverseRoot)
|
||||
|
||||
if root.Name == "library" {
|
||||
continue
|
||||
}
|
||||
|
||||
attr, ok := trav[1].(hcl.TraverseAttr)
|
||||
if !ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "TODO unformed2",
|
||||
Detail: fmt.Sprintf("%#v", trav),
|
||||
Subject: trav.SourceRange().Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
vRes := s.vars[root.Name][attr.Name]
|
||||
if vRes == nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "TODO unformed3",
|
||||
Detail: fmt.Sprintf("%s.%s", root.Name, attr.Name),
|
||||
Subject: trav.SourceRange().Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
val, vDiags := vRes(w)
|
||||
diags = diags.Extend(vDiags)
|
||||
|
||||
rootVars, ok := vars[root.Name]
|
||||
if !ok {
|
||||
rootVars = map[string]cty.Value{}
|
||||
vars[root.Name] = rootVars
|
||||
}
|
||||
|
||||
rootVars[attr.Name] = val
|
||||
}
|
||||
} else {
|
||||
for rootName, entries := range s.vars {
|
||||
vars[rootName] = map[string]cty.Value{}
|
||||
for varName, entry := range entries {
|
||||
val, vDiags := entry(w)
|
||||
diags = diags.Extend(vDiags)
|
||||
vars[rootName][varName] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
evalCtx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{},
|
||||
Functions: map[string]function.Function{},
|
||||
}
|
||||
maps.Copy(evalCtx.Functions, s.builtinFuncs)
|
||||
|
||||
for name, vars := range vars {
|
||||
evalCtx.Variables[name] = cty.ObjectVal(vars)
|
||||
}
|
||||
|
||||
if expr != nil {
|
||||
for _, trav := range expr.(hcl.ExpressionWithFunctions).Functions() {
|
||||
funcIdent := trav.RootName()
|
||||
parts := strings.Split(funcIdent, "::")
|
||||
|
||||
if parts[0] == "library" {
|
||||
// library::func
|
||||
if len(parts) == 2 {
|
||||
funcName := parts[1]
|
||||
fn, ok := s.funcs[funcName]
|
||||
if ok {
|
||||
impl, fDiags := fn(w)
|
||||
diags = diags.Extend(fDiags)
|
||||
evalCtx.Functions[funcIdent] = impl
|
||||
}
|
||||
}
|
||||
// library::lib::func
|
||||
if len(parts) == 3 {
|
||||
libName := parts[1]
|
||||
funcName := parts[2]
|
||||
|
||||
if lib, ok := s.libraries[libName]; ok {
|
||||
if fn, ok := lib.funcs[funcName]; ok {
|
||||
impl, fDiags := fn(w)
|
||||
diags = diags.Extend(fDiags)
|
||||
evalCtx.Functions[funcIdent] = impl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for funcName, fn := range s.funcs {
|
||||
funcIdent := "library::" + funcName
|
||||
impl, fDiags := fn(w)
|
||||
diags = diags.Extend(fDiags)
|
||||
evalCtx.Functions[funcIdent] = impl
|
||||
}
|
||||
|
||||
for libName, lib := range s.libraries {
|
||||
for funcName, fn := range lib.funcs {
|
||||
funcIdent := "library::" + libName + "::" + funcName
|
||||
impl, fDiags := fn(w)
|
||||
diags = diags.Extend(fDiags)
|
||||
evalCtx.Functions[funcIdent] = impl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return evalCtx, diags
|
||||
}
|
||||
|
||||
func (s *scope) addType(name string, typeExpr hcl.Expression) {
|
||||
s.types[name] = once(func(w *workgraph.Worker) (typeWithDefault, hcl.Diagnostics) {
|
||||
typeCtx, diags := s.typeContext(w, typeExpr)
|
||||
|
||||
varType, typeDefault, vDiags := typeCtx.TypeConstraintWithDefaults(typeExpr)
|
||||
diags = diags.Extend(vDiags)
|
||||
|
||||
return typeWithDefault{varType, typeDefault}, diags
|
||||
})
|
||||
}
|
||||
|
||||
func (s *scope) addVar(namespace string, name string, expr hcl.Expression) {
|
||||
ns, ok := s.vars[namespace]
|
||||
if !ok {
|
||||
ns = map[string]result[cty.Value]{}
|
||||
s.vars[namespace] = ns
|
||||
}
|
||||
|
||||
ns[name] = once(func(w *workgraph.Worker) (cty.Value, hcl.Diagnostics) {
|
||||
evalCtx, diags := s.evalContext(w, expr)
|
||||
|
||||
val, vDiags := expr.Value(evalCtx)
|
||||
diags = diags.Extend(vDiags)
|
||||
|
||||
return val, diags
|
||||
})
|
||||
}
|
||||
|
||||
func (s *scope) addFunction(name string, fn func(*workgraph.Worker, *scope) (function.Function, hcl.Diagnostics)) {
|
||||
s.funcs[name] = once(func(w *workgraph.Worker) (function.Function, hcl.Diagnostics) {
|
||||
return fn(w, s)
|
||||
})
|
||||
}
|
||||
|
||||
type result[T any] func(w *workgraph.Worker) (T, hcl.Diagnostics)
|
||||
type withDiags[T any] struct {
|
||||
value T
|
||||
diags hcl.Diagnostics
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package symlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
@@ -140,17 +139,18 @@ func decodeFunctionBlock(block *hcl.Block) (*Function, hcl.Diagnostics) {
|
||||
return fn, diags
|
||||
}
|
||||
|
||||
func (fn *Function) Impl(lib *Library) (function.Function, hcl.Diagnostics) {
|
||||
func (fn *Function) Impl(w *workgraph.Worker, s *scope) (function.Function, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
typeCtx := lib.TypeContext
|
||||
|
||||
spec := &function.Spec{
|
||||
Description: fn.Description,
|
||||
}
|
||||
|
||||
returnType := cty.DynamicPseudoType
|
||||
if fn.ReturnType != nil {
|
||||
typeCtx, tDiags := s.typeContext(w, fn.ReturnType)
|
||||
diags = diags.Extend(tDiags)
|
||||
|
||||
var valDiags hcl.Diagnostics
|
||||
returnType, _, valDiags = typeCtx.TypeConstraintWithDefaults(fn.ReturnType)
|
||||
diags = append(diags, valDiags...)
|
||||
@@ -169,6 +169,9 @@ func (fn *Function) Impl(lib *Library) (function.Function, hcl.Diagnostics) {
|
||||
}
|
||||
|
||||
if param.TypeExpr != nil {
|
||||
typeCtx, tDiags := s.typeContext(w, *param.TypeExpr)
|
||||
diags = diags.Extend(tDiags)
|
||||
|
||||
var valDiags hcl.Diagnostics
|
||||
fnp.Type, defaults[fnp.Name], valDiags = typeCtx.TypeConstraintWithDefaults(*param.TypeExpr)
|
||||
return fnp, valDiags
|
||||
@@ -188,65 +191,35 @@ func (fn *Function) Impl(lib *Library) (function.Function, hcl.Diagnostics) {
|
||||
}
|
||||
|
||||
spec.Impl = func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
// This is bad and I should feel bad
|
||||
// This could also be accomplished by creating a full evalcontext
|
||||
// and building deps internally via workgraph
|
||||
s := s.clone()
|
||||
|
||||
hclCtx := &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{},
|
||||
Functions: lib.builtinFuncs,
|
||||
}
|
||||
|
||||
paramObj := map[string]cty.Value{}
|
||||
for i, arg := range args[:len(spec.Params)] {
|
||||
param := spec.Params[i]
|
||||
|
||||
if defaults[param.Name] != nil && !arg.IsNull() {
|
||||
arg = defaults[param.Name].Apply(arg)
|
||||
}
|
||||
|
||||
paramObj[param.Name] = arg
|
||||
s.addVar("param", param.Name, &hclsyntax.LiteralValueExpr{Val: arg})
|
||||
}
|
||||
if spec.VarParam != nil && len(spec.Params) != len(args) {
|
||||
paramObj[spec.VarParam.Name] = cty.ListVal(args[len(spec.Params):])
|
||||
}
|
||||
hclCtx.Variables["param"] = cty.ObjectVal(paramObj)
|
||||
|
||||
localObj := map[string]cty.Value{}
|
||||
hclCtx.Variables["local"] = cty.ObjectVal(localObj)
|
||||
|
||||
// TODO track stack / circuilar dependencies
|
||||
var computeValue func(expr hcl.Expression) (cty.Value, error)
|
||||
computeValue = func(expr hcl.Expression) (cty.Value, error) {
|
||||
for _, trav := range expr.Variables() {
|
||||
if len(trav) < 2 {
|
||||
return cty.NilVal, fmt.Errorf("Bad traversal: %#v", trav)
|
||||
}
|
||||
|
||||
switch trav[0].(hcl.TraverseRoot).Name {
|
||||
case "param":
|
||||
// Could validate if we care
|
||||
case "local":
|
||||
localName := trav[1].(hcl.TraverseAttr).Name
|
||||
if _, ok := localObj[localName]; !ok {
|
||||
var computeErr error
|
||||
localObj[localName], computeErr = computeValue(fn.Locals[localName])
|
||||
hclCtx.Variables["local"] = cty.ObjectVal(localObj)
|
||||
if computeErr != nil {
|
||||
return cty.NilVal, computeErr
|
||||
}
|
||||
}
|
||||
default:
|
||||
return cty.NilVal, fmt.Errorf("Bad traversal: %#v", trav)
|
||||
}
|
||||
}
|
||||
|
||||
val, vDiags := expr.Value(hclCtx)
|
||||
if vDiags.HasErrors() {
|
||||
return val, vDiags
|
||||
}
|
||||
return val, nil
|
||||
s.addVar("param", spec.VarParam.Name, &hclsyntax.LiteralValueExpr{Val: cty.ListVal(args[len(spec.Params):])})
|
||||
}
|
||||
|
||||
return computeValue(fn.Return)
|
||||
for name, expr := range fn.Locals {
|
||||
s.addVar("local", name, expr)
|
||||
}
|
||||
|
||||
hclCtx, diags := s.evalContext(w, fn.Return)
|
||||
|
||||
val, vDiags := fn.Return.Value(hclCtx)
|
||||
diags = diags.Extend(vDiags)
|
||||
|
||||
if diags.HasErrors() {
|
||||
return val, error(diags)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return function.New(spec), diags
|
||||
|
||||
@@ -3,6 +3,7 @@ package symlib
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/apparentlymart/go-workgraph/workgraph"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/typeexpr"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
@@ -10,11 +11,7 @@ import (
|
||||
)
|
||||
|
||||
type Library struct {
|
||||
functions map[string]function.Function
|
||||
types map[string]cty.Type
|
||||
typeDefaults map[string]*typeexpr.Defaults
|
||||
|
||||
builtinFuncs map[string]function.Function
|
||||
scope *scope
|
||||
|
||||
TypeContext typeexpr.TypeContext
|
||||
Functions map[string]function.Function
|
||||
@@ -25,11 +22,7 @@ type LibraryLoader func(*LibraryCall) (*Library, hcl.Diagnostics)
|
||||
func NewLibrary(contents *LibraryContents, loader LibraryLoader, builtinFuncs map[string]function.Function) (*Library, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
l := &Library{
|
||||
functions: map[string]function.Function{},
|
||||
types: map[string]cty.Type{},
|
||||
typeDefaults: map[string]*typeexpr.Defaults{},
|
||||
|
||||
builtinFuncs: builtinFuncs,
|
||||
scope: newScope(builtinFuncs),
|
||||
|
||||
Functions: map[string]function.Function{},
|
||||
TypeContext: typeexpr.TypeContext{
|
||||
@@ -38,9 +31,6 @@ func NewLibrary(contents *LibraryContents, loader LibraryLoader, builtinFuncs ma
|
||||
},
|
||||
}
|
||||
|
||||
// TODO This is where complex interdependencies can happen!
|
||||
// TODO We ignore this problem for now!
|
||||
|
||||
// Load libraries
|
||||
for libName, call := range contents.LibraryCalls {
|
||||
lib, lDiags := loader(call)
|
||||
@@ -51,41 +41,28 @@ func NewLibrary(contents *LibraryContents, loader LibraryLoader, builtinFuncs ma
|
||||
continue
|
||||
}
|
||||
|
||||
// Imported functions
|
||||
for name, fn := range lib.functions {
|
||||
l.Functions["library::"+libName+"::"+name] = fn
|
||||
}
|
||||
|
||||
// Imported types
|
||||
l.TypeContext.Types["library::"+libName+"::types"] = lib.types
|
||||
l.TypeContext.Defaults["library::"+libName+"::types"] = lib.typeDefaults
|
||||
l.scope.libraries[libName] = lib.scope
|
||||
}
|
||||
|
||||
// Declared types
|
||||
// Build scope
|
||||
for _, typeDef := range contents.TypeDefs {
|
||||
varType, typeDefault, valDiags := l.TypeContext.TypeConstraintWithDefaults(typeDef.TypeExpr)
|
||||
diags = diags.Extend(valDiags)
|
||||
|
||||
l.types[typeDef.Name] = varType
|
||||
if typeDefault != nil {
|
||||
l.typeDefaults[typeDef.Name] = typeDefault
|
||||
}
|
||||
l.scope.addType(typeDef.Name, typeDef.TypeExpr)
|
||||
}
|
||||
// Exported types
|
||||
l.TypeContext.Types["library::types"] = l.types
|
||||
l.TypeContext.Defaults["library::types"] = l.typeDefaults
|
||||
|
||||
// Declared functions
|
||||
for _, fn := range contents.Functions {
|
||||
impl, moreDiags := fn.Impl(l)
|
||||
diags = diags.Extend(moreDiags)
|
||||
l.scope.addFunction(fn.Name, fn.Impl)
|
||||
}
|
||||
// TODO consts
|
||||
|
||||
l.functions[fn.Name] = impl
|
||||
}
|
||||
// Exported functions
|
||||
for name, fn := range l.functions {
|
||||
l.Functions["library::"+name] = fn
|
||||
}
|
||||
worker := workgraph.NewWorker()
|
||||
|
||||
typeCtx, mDiags := l.scope.typeContext(worker, nil)
|
||||
diags = diags.Extend(mDiags)
|
||||
|
||||
evalCtx, mDiags := l.scope.evalContext(worker, nil)
|
||||
diags = diags.Extend(mDiags)
|
||||
|
||||
l.TypeContext = typeCtx
|
||||
l.Functions = evalCtx.Functions
|
||||
|
||||
return l, diags
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user