From 4e0cbe8bbf3d957c01de439b2d32fae1d4c5e4db Mon Sep 17 00:00:00 2001 From: Christian Mesh Date: Wed, 22 Apr 2026 14:36:44 -0400 Subject: [PATCH] Switch to workgraph for symlib Signed-off-by: Christian Mesh --- internal/configs/symlib/eval.go | 320 +++++++++++++++++++++++++++ internal/configs/symlib/functions.go | 79 +++---- internal/configs/symlib/library.go | 61 ++--- 3 files changed, 365 insertions(+), 95 deletions(-) create mode 100644 internal/configs/symlib/eval.go diff --git a/internal/configs/symlib/eval.go b/internal/configs/symlib/eval.go new file mode 100644 index 0000000000..81b11db13d --- /dev/null +++ b/internal/configs/symlib/eval.go @@ -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 +} diff --git a/internal/configs/symlib/functions.go b/internal/configs/symlib/functions.go index 70a83a8a67..8b3c4b8a4a 100644 --- a/internal/configs/symlib/functions.go +++ b/internal/configs/symlib/functions.go @@ -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 diff --git a/internal/configs/symlib/library.go b/internal/configs/symlib/library.go index 28ddb2a022..d40e67353c 100644 --- a/internal/configs/symlib/library.go +++ b/internal/configs/symlib/library.go @@ -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 }