451 lines
11 KiB
Go
451 lines
11 KiB
Go
package hclhil
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
"github.com/zclconf/go-zcl/zcl"
|
|
"github.com/hashicorp/hil"
|
|
hilast "github.com/hashicorp/hil/ast"
|
|
)
|
|
|
|
func parseTemplate(src []byte, filename string, startPos zcl.Pos) (zcl.Expression, zcl.Diagnostics) {
|
|
hilStartPos := hilast.Pos{
|
|
Filename: filename,
|
|
Line: startPos.Line,
|
|
Column: startPos.Column,
|
|
// HIL positions don't have byte offsets, so we ignore startPos.Byte here
|
|
}
|
|
rootNode, err := hil.ParseWithPosition(string(src), hilStartPos)
|
|
|
|
if err != nil {
|
|
return nil, zcl.Diagnostics{
|
|
{
|
|
Severity: zcl.DiagError,
|
|
Summary: "Syntax error in template",
|
|
Detail: fmt.Sprintf("The template could not be parsed: %s", err),
|
|
Subject: errorRange(err),
|
|
},
|
|
}
|
|
}
|
|
|
|
return &templateExpression{
|
|
node: rootNode,
|
|
}, nil
|
|
}
|
|
|
|
type templateExpression struct {
|
|
node hilast.Node
|
|
}
|
|
|
|
func (e *templateExpression) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
|
|
cfg := hilEvalConfig(ctx)
|
|
return ctyValueFromHILNode(e.node, cfg)
|
|
}
|
|
|
|
func (e *templateExpression) Variables() []zcl.Traversal {
|
|
var vars []zcl.Traversal
|
|
e.node.Accept(func(n hilast.Node) hilast.Node {
|
|
vn, ok := n.(*hilast.VariableAccess)
|
|
if !ok {
|
|
return n
|
|
}
|
|
|
|
rawName := vn.Name
|
|
parts := strings.Split(rawName, ".")
|
|
if len(parts) == 0 {
|
|
return n
|
|
}
|
|
|
|
tr := make(zcl.Traversal, 0, len(parts))
|
|
tr = append(tr, zcl.TraverseRoot{
|
|
Name: parts[0],
|
|
SrcRange: rangeFromHILPos(n.Pos()),
|
|
})
|
|
|
|
for _, name := range parts {
|
|
if nv, err := strconv.Atoi(name); err == nil {
|
|
// Turn this into a sequence index in zcl land, to save
|
|
// callers from having to understand both HIL-style numeric
|
|
// attributes and zcl-style indices.
|
|
tr = append(tr, zcl.TraverseIndex{
|
|
Key: cty.NumberIntVal(int64(nv)),
|
|
SrcRange: rangeFromHILPos(n.Pos()),
|
|
})
|
|
continue
|
|
}
|
|
|
|
if name == "*" {
|
|
// TODO: support splat traversals, but that requires some
|
|
// more work here because we need to then accumulate the
|
|
// rest of the parts into the splat's own "Each" traversal.
|
|
continue
|
|
}
|
|
|
|
tr = append(tr, zcl.TraverseAttr{
|
|
Name: name,
|
|
SrcRange: rangeFromHILPos(n.Pos()),
|
|
})
|
|
}
|
|
|
|
vars = append(vars, tr)
|
|
|
|
return n
|
|
})
|
|
return vars
|
|
}
|
|
|
|
func (e *templateExpression) Range() zcl.Range {
|
|
return rangeFromHILPos(e.node.Pos())
|
|
}
|
|
func (e *templateExpression) StartRange() zcl.Range {
|
|
return rangeFromHILPos(e.node.Pos())
|
|
}
|
|
|
|
func hilEvalConfig(ctx *zcl.EvalContext) *hil.EvalConfig {
|
|
cfg := &hil.EvalConfig{
|
|
GlobalScope: &hilast.BasicScope{
|
|
VarMap: map[string]hilast.Variable{},
|
|
FuncMap: map[string]hilast.Function{},
|
|
},
|
|
}
|
|
|
|
if ctx == nil {
|
|
return cfg
|
|
}
|
|
|
|
if ctx.Variables != nil {
|
|
for name, val := range ctx.Variables {
|
|
cfg.GlobalScope.VarMap[name] = hilVariableForInput(hilVariableFromCtyValue(val))
|
|
}
|
|
}
|
|
|
|
if ctx.Functions != nil {
|
|
for name, hf := range ctx.Functions {
|
|
cfg.GlobalScope.FuncMap[name] = hilFunctionFromCtyFunction(hf)
|
|
}
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func ctyValueFromHILNode(node hilast.Node, cfg *hil.EvalConfig) (cty.Value, zcl.Diagnostics) {
|
|
result, err := hil.Eval(node, cfg)
|
|
if err != nil {
|
|
return cty.DynamicVal, zcl.Diagnostics{
|
|
{
|
|
Severity: zcl.DiagError,
|
|
Summary: "Template evaluation failed",
|
|
Detail: fmt.Sprintf("Error while evaluating template: %s", err),
|
|
Subject: rangeFromHILPos(node.Pos()).Ptr(),
|
|
},
|
|
}
|
|
}
|
|
|
|
return ctyValueFromHILResult(result), nil
|
|
}
|
|
|
|
func ctyValueFromHILResult(result hil.EvaluationResult) cty.Value {
|
|
switch result.Type {
|
|
case hil.TypeString:
|
|
return cty.StringVal(result.Value.(string))
|
|
case hil.TypeBool:
|
|
return cty.BoolVal(result.Value.(bool))
|
|
case hil.TypeList:
|
|
varsI := result.Value.([]interface{})
|
|
if len(varsI) == 0 {
|
|
return cty.ListValEmpty(cty.String)
|
|
}
|
|
vals := make([]cty.Value, len(varsI))
|
|
for i, varI := range varsI {
|
|
hv, err := hil.InterfaceToVariable(varI)
|
|
if err != nil {
|
|
panic("HIL returned type that can't be converted back to variable")
|
|
}
|
|
vals[i] = ctyValueFromHILVariable(hv)
|
|
}
|
|
return cty.TupleVal(vals)
|
|
case hil.TypeMap:
|
|
varsI := result.Value.(map[string]interface{})
|
|
if len(varsI) == 0 {
|
|
return cty.MapValEmpty(cty.String)
|
|
}
|
|
vals := make(map[string]cty.Value)
|
|
for key, varI := range varsI {
|
|
hv, err := hil.InterfaceToVariable(varI)
|
|
if err != nil {
|
|
panic("HIL returned type that can't be converted back to variable")
|
|
}
|
|
vals[key] = ctyValueFromHILVariable(hv)
|
|
}
|
|
return cty.ObjectVal(vals)
|
|
case hil.TypeUnknown:
|
|
// HIL doesn't have typed unknowns, so we have to return dynamic
|
|
return cty.DynamicVal
|
|
default:
|
|
// should never happen
|
|
panic(fmt.Sprintf("unsupported EvaluationResult type %s", result.Type))
|
|
}
|
|
|
|
}
|
|
|
|
func ctyValueFromHILVariable(vr hilast.Variable) cty.Value {
|
|
switch vr.Type {
|
|
case hilast.TypeBool:
|
|
return cty.BoolVal(vr.Value.(bool))
|
|
case hilast.TypeString:
|
|
return cty.StringVal(vr.Value.(string))
|
|
case hilast.TypeInt:
|
|
return cty.NumberIntVal(vr.Value.(int64))
|
|
case hilast.TypeFloat:
|
|
return cty.NumberFloatVal(vr.Value.(float64))
|
|
case hilast.TypeList:
|
|
vars := vr.Value.([]hilast.Variable)
|
|
if len(vars) == 0 {
|
|
return cty.ListValEmpty(cty.String)
|
|
}
|
|
vals := make([]cty.Value, len(vars))
|
|
for i, v := range vars {
|
|
vals[i] = ctyValueFromHILVariable(v)
|
|
}
|
|
return cty.TupleVal(vals)
|
|
case hilast.TypeMap:
|
|
vars := vr.Value.(map[string]hilast.Variable)
|
|
if len(vars) == 0 {
|
|
return cty.MapValEmpty(cty.String)
|
|
}
|
|
vals := make(map[string]cty.Value)
|
|
for key, v := range vars {
|
|
vals[key] = ctyValueFromHILVariable(v)
|
|
}
|
|
return cty.ObjectVal(vals)
|
|
case hilast.TypeAny, hilast.TypeUnknown:
|
|
return cty.DynamicVal
|
|
default:
|
|
// should never happen
|
|
panic(fmt.Sprintf("unsupported HIL Variable type %s", vr.Type))
|
|
}
|
|
}
|
|
|
|
func hilVariableFromCtyValue(val cty.Value) hilast.Variable {
|
|
if !val.IsKnown() {
|
|
return hilast.Variable{
|
|
Type: hilast.TypeUnknown,
|
|
Value: hil.UnknownValue,
|
|
}
|
|
}
|
|
if val.IsNull() {
|
|
// HIL doesn't actually support nulls, so we'll cheat a bit and
|
|
// use an unknown. This is not quite right since nulls are supposed
|
|
// to fail when evaluated, but it should suffice as a compatibility
|
|
// shim since HIL-using applications probably won't be generating
|
|
// nulls anyway.
|
|
return hilast.Variable{
|
|
Type: hilast.TypeUnknown,
|
|
Value: hil.UnknownValue,
|
|
}
|
|
}
|
|
|
|
ty := val.Type()
|
|
switch ty {
|
|
case cty.String:
|
|
return hilast.Variable{
|
|
Type: hilast.TypeString,
|
|
Value: val.AsString(),
|
|
}
|
|
case cty.Number:
|
|
// cty doesn't distinguish between floats and ints, so we'll
|
|
// just always use floats here and depend on automatic conversions
|
|
// to produce ints where needed.
|
|
bf := val.AsBigFloat()
|
|
f, _ := bf.Float64()
|
|
return hilast.Variable{
|
|
Type: hilast.TypeFloat,
|
|
Value: f,
|
|
}
|
|
case cty.Bool:
|
|
return hilast.Variable{
|
|
Type: hilast.TypeBool,
|
|
Value: val.True(),
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
|
|
// HIL doesn't have sets, so we'll just turn them into lists
|
|
// HIL doesn't support tuples either, so any tuples without consistent
|
|
// element types will fail HIL's check for consistent types, but that's
|
|
// okay since we don't intend to change HIL semantics here.
|
|
vars := []hilast.Variable{}
|
|
it := val.ElementIterator()
|
|
for it.Next() {
|
|
_, ev := it.Element()
|
|
vars = append(vars, hilVariableFromCtyValue(ev))
|
|
}
|
|
return hilast.Variable{
|
|
Type: hilast.TypeList,
|
|
Value: vars,
|
|
}
|
|
case ty.IsMapType():
|
|
vars := map[string]hilast.Variable{}
|
|
it := val.ElementIterator()
|
|
for it.Next() {
|
|
kv, ev := it.Element()
|
|
k := kv.AsString()
|
|
vars[k] = hilVariableFromCtyValue(ev)
|
|
}
|
|
return hilast.Variable{
|
|
Type: hilast.TypeMap,
|
|
Value: vars,
|
|
}
|
|
case ty.IsObjectType():
|
|
// HIL doesn't support objects, so objects that don't have consistent
|
|
// attribute types will fail HIL's check for consistent types. That's
|
|
// okay since we don't intend to change HIL semantics here.
|
|
vars := map[string]interface{}{}
|
|
atys := ty.AttributeTypes()
|
|
for k := range atys {
|
|
vars[k] = hilVariableFromCtyValue(val.GetAttr(k))
|
|
}
|
|
return hilast.Variable{
|
|
Type: hilast.TypeMap,
|
|
Value: vars,
|
|
}
|
|
case ty.IsCapsuleType():
|
|
// Can't do anything reasonable with capsule types, so we'll just
|
|
// treat them as unknown and let the caller deal with it as an error.
|
|
return hilast.Variable{
|
|
Type: hilast.TypeUnknown,
|
|
Value: hil.UnknownValue,
|
|
}
|
|
default:
|
|
// Should never happen if we've done our job right here
|
|
panic(fmt.Sprintf("don't know how to convert %#v into a HIL variable", ty))
|
|
}
|
|
|
|
}
|
|
|
|
// hilVariableForInput constrains the given variable to be of the types HIL
|
|
// accepts as input, which entails converting all primitive types to string.
|
|
func hilVariableForInput(v hilast.Variable) hilast.Variable {
|
|
switch v.Type {
|
|
case hilast.TypeFloat:
|
|
return hilast.Variable{
|
|
Type: hilast.TypeString,
|
|
Value: strconv.FormatFloat(v.Value.(float64), 'f', -1, 64),
|
|
}
|
|
case hilast.TypeBool:
|
|
if v.Value.(bool) {
|
|
return hilast.Variable{
|
|
Type: hilast.TypeString,
|
|
Value: "true",
|
|
}
|
|
} else {
|
|
return hilast.Variable{
|
|
Type: hilast.TypeString,
|
|
Value: "false",
|
|
}
|
|
}
|
|
case hilast.TypeList:
|
|
inVars := v.Value.([]hilast.Variable)
|
|
outVars := make([]hilast.Variable, len(inVars))
|
|
for i, inVar := range inVars {
|
|
outVars[i] = hilVariableForInput(inVar)
|
|
}
|
|
return hilast.Variable{
|
|
Type: hilast.TypeList,
|
|
Value: outVars,
|
|
}
|
|
case hilast.TypeMap:
|
|
inVars := v.Value.(map[string]hilast.Variable)
|
|
outVars := make(map[string]hilast.Variable)
|
|
for k, inVar := range inVars {
|
|
outVars[k] = hilVariableForInput(inVar)
|
|
}
|
|
return hilast.Variable{
|
|
Type: hilast.TypeMap,
|
|
Value: outVars,
|
|
}
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
func hilTypeFromCtyType(ty cty.Type) hilast.Type {
|
|
switch ty {
|
|
case cty.String:
|
|
return hilast.TypeString
|
|
case cty.Number:
|
|
return hilast.TypeFloat
|
|
case cty.Bool:
|
|
return hilast.TypeBool
|
|
case cty.DynamicPseudoType:
|
|
// Assume we're using this as a type specification, so we'd rather
|
|
// have TypeAny than TypeUnknown.
|
|
return hilast.TypeAny
|
|
}
|
|
|
|
switch {
|
|
case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
|
|
return hilast.TypeList
|
|
case ty.IsMapType(), ty.IsObjectType():
|
|
return hilast.TypeMap
|
|
default:
|
|
return hilast.TypeUnknown
|
|
}
|
|
|
|
}
|
|
|
|
func hilFunctionFromCtyFunction(f function.Function) hilast.Function {
|
|
hf := hilast.Function{}
|
|
params := f.Params()
|
|
varParam := f.VarParam()
|
|
|
|
hf.ArgTypes = make([]hilast.Type, len(params))
|
|
staticTypes := make([]cty.Type, len(params))
|
|
for i, param := range params {
|
|
hf.ArgTypes[i] = hilTypeFromCtyType(param.Type)
|
|
staticTypes[i] = param.Type
|
|
}
|
|
if varParam != nil {
|
|
hf.Variadic = true
|
|
hf.VariadicType = hilTypeFromCtyType(varParam.Type)
|
|
}
|
|
|
|
retType, err := f.ReturnType(staticTypes)
|
|
if err == nil {
|
|
hf.ReturnType = hilTypeFromCtyType(retType)
|
|
} else {
|
|
hf.ReturnType = hilTypeFromCtyType(cty.DynamicPseudoType)
|
|
}
|
|
|
|
hf.Callback = func(hilArgs []interface{}) (interface{}, error) {
|
|
args := make([]cty.Value, len(hilArgs))
|
|
for i, hilArg := range hilArgs {
|
|
var hilType hilast.Type
|
|
if i < len(hf.ArgTypes) {
|
|
hilType = hf.ArgTypes[i]
|
|
} else {
|
|
hilType = hf.VariadicType
|
|
}
|
|
args[i] = ctyValueFromHILVariable(hilast.Variable{
|
|
Type: hilType,
|
|
Value: hilArg,
|
|
})
|
|
}
|
|
|
|
result, err := f.Call(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hilResult := hilVariableFromCtyValue(result)
|
|
return hilResult.Value, nil
|
|
}
|
|
|
|
return hf
|
|
}
|