├── parser ├── fuzz-corpus │ ├── empty.hil │ ├── literal.hil │ ├── int.hil │ ├── just-interp.hil │ ├── utf8.hil │ ├── escape-dollar.hil │ ├── escape-newline.hil │ └── function-call.hil ├── fuzz.go ├── error.go ├── binary_op.go └── parser.go ├── .gitignore ├── CHANGELOG.md ├── .golangci.yml ├── go.mod ├── go.sum ├── eval_type.go ├── .github ├── workflows │ ├── actionlint.yml │ └── hil.yml ├── pull_request_template.md ├── dependabot.yml └── CODEOWNERS ├── ast ├── stack.go ├── arithmetic_op.go ├── ast_test.go ├── call_test.go ├── unknown.go ├── variable_access.go ├── stack_test.go ├── arithmetic.go ├── conditional.go ├── call.go ├── variable_access_test.go ├── scope_test.go ├── type_string.go ├── variables_helper.go ├── literal_test.go ├── index.go ├── output.go ├── literal.go ├── scope.go ├── ast.go ├── output_test.go └── index_test.go ├── example_test.go ├── example_var_test.go ├── transform_fixed.go ├── transform_fixed_test.go ├── evaltype_string.go ├── parse.go ├── example_func_test.go ├── scanner ├── token_test.go ├── peeker.go ├── tokentype_string.go ├── peeker_test.go ├── token.go ├── scanner_test.go └── scanner.go ├── check_identifier.go ├── check_identifier_test.go ├── walk_test.go ├── README.md ├── convert.go ├── walk.go ├── builtins.go ├── convert_test.go ├── check_types_test.go ├── eval.go ├── LICENSE └── check_types.go /parser/fuzz-corpus/empty.hil: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /parser/fuzz-corpus/literal.hil: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /parser/fuzz-corpus/int.hil: -------------------------------------------------------------------------------- 1 | foo ${42} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.iml 4 | -------------------------------------------------------------------------------- /parser/fuzz-corpus/just-interp.hil: -------------------------------------------------------------------------------- 1 | ${var.bar} -------------------------------------------------------------------------------- /parser/fuzz-corpus/utf8.hil: -------------------------------------------------------------------------------- 1 | föo ${föo("föo")} -------------------------------------------------------------------------------- /parser/fuzz-corpus/escape-dollar.hil: -------------------------------------------------------------------------------- 1 | hi $${var.foo} -------------------------------------------------------------------------------- /parser/fuzz-corpus/escape-newline.hil: -------------------------------------------------------------------------------- 1 | foo ${"bar\nbaz"} -------------------------------------------------------------------------------- /parser/fuzz-corpus/function-call.hil: -------------------------------------------------------------------------------- 1 | hi ${title(var.name)} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ### Improvements 4 | 5 | ### Changes 6 | 7 | ### Fixed 8 | 9 | ### Security 10 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright IBM Corp. 2015, 2025 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: "2" 5 | linters: 6 | exclusions: 7 | generated: lax 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/hil 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/mitchellh/mapstructure v1.5.0 7 | github.com/mitchellh/reflectwalk v1.0.2 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 2 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 3 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 4 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 5 | -------------------------------------------------------------------------------- /eval_type.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | //go:generate stringer -type=EvalType eval_type.go 7 | 8 | // EvalType represents the type of the output returned from a HIL 9 | // evaluation. 10 | type EvalType uint32 11 | 12 | const ( 13 | TypeInvalid EvalType = 0 14 | TypeString EvalType = 1 << iota 15 | TypeBool 16 | TypeList 17 | TypeMap 18 | TypeUnknown 19 | ) 20 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: actionlint 2 | on: 3 | push: 4 | pull_request: 5 | permissions: 6 | contents: read 7 | jobs: 8 | actionlint: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 12 | - name: "Check workflow files" 13 | uses: docker://docker.mirror.hashicorp.services/rhysd/actionlint:latest 14 | with: 15 | args: -color 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ## Description 3 | 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | -------------------------------------------------------------------------------- /ast/stack.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | // Stack is a stack of Node. 7 | type Stack struct { 8 | stack []Node 9 | } 10 | 11 | func (s *Stack) Len() int { 12 | return len(s.stack) 13 | } 14 | 15 | func (s *Stack) Push(n Node) { 16 | s.stack = append(s.stack, n) 17 | } 18 | 19 | func (s *Stack) Pop() Node { 20 | x := s.stack[len(s.stack)-1] 21 | s.stack[len(s.stack)-1] = nil 22 | s.stack = s.stack[:len(s.stack)-1] 23 | return x 24 | } 25 | 26 | func (s *Stack) Reset() { 27 | s.stack = nil 28 | } 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: monthly 8 | labels: 9 | - dependencies 10 | - automated 11 | - github_actions 12 | groups: 13 | github-actions-breaking: 14 | update-types: 15 | - major 16 | github-actions-backward-compatible: 17 | update-types: 18 | - minor 19 | - patch 20 | 21 | - package-ecosystem: "gomod" 22 | directory: "/" 23 | schedule: 24 | interval: "weekly" 25 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Default owner 5 | * @hashicorp/team-ip-compliance @hashicorp/nomad-eng 6 | 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners. 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting 9 | # those areas of the code. 10 | # Examples: 11 | # /docs/ @docs-team 12 | # *.js @js-team 13 | # *.go @go-team 14 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil_test 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/hashicorp/hil" 11 | ) 12 | 13 | func Example_basic() { 14 | input := "${6 + 2}" 15 | 16 | tree, err := hil.Parse(input) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | 21 | result, err := hil.Eval(tree, &hil.EvalConfig{}) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | fmt.Printf("Type: %s\n", result.Type) 27 | fmt.Printf("Value: %s\n", result.Value) 28 | // Output: 29 | // Type: TypeString 30 | // Value: 8 31 | } 32 | -------------------------------------------------------------------------------- /ast/arithmetic_op.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | // ArithmeticOp is the operation to use for the math. 7 | type ArithmeticOp int 8 | 9 | const ( 10 | ArithmeticOpInvalid ArithmeticOp = 0 11 | 12 | ArithmeticOpAdd ArithmeticOp = iota 13 | ArithmeticOpSub 14 | ArithmeticOpMul 15 | ArithmeticOpDiv 16 | ArithmeticOpMod 17 | 18 | ArithmeticOpLogicalAnd 19 | ArithmeticOpLogicalOr 20 | 21 | ArithmeticOpEqual 22 | ArithmeticOpNotEqual 23 | ArithmeticOpLessThan 24 | ArithmeticOpLessThanOrEqual 25 | ArithmeticOpGreaterThan 26 | ArithmeticOpGreaterThanOrEqual 27 | ) 28 | -------------------------------------------------------------------------------- /ast/ast_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | func TestPosString(t *testing.T) { 12 | cases := []struct { 13 | Input Pos 14 | String string 15 | }{ 16 | { 17 | Pos{Line: 1, Column: 1}, 18 | "1:1", 19 | }, 20 | { 21 | Pos{Line: 2, Column: 3}, 22 | "2:3", 23 | }, 24 | { 25 | Pos{Line: 3, Column: 2, Filename: "template.hil"}, 26 | "template.hil:3:2", 27 | }, 28 | } 29 | 30 | for i, tc := range cases { 31 | t.Run(strconv.Itoa(i), func(t *testing.T) { 32 | got := tc.Input.String() 33 | if want, got := tc.String, got; want != got { 34 | t.Errorf("%#v produced %q; want %q", tc.Input, got, want) 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /parser/fuzz.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build gofuzz 5 | 6 | package parser 7 | 8 | import ( 9 | "github.com/hashicorp/hil/ast" 10 | "github.com/hashicorp/hil/scanner" 11 | ) 12 | 13 | // This is a fuzz testing function designed to be used with go-fuzz: 14 | // https://github.com/dvyukov/go-fuzz 15 | // 16 | // It's not included in a normal build due to the gofuzz build tag above. 17 | // 18 | // There are some input files that you can use as a seed corpus for go-fuzz 19 | // in the directory ./fuzz-corpus . 20 | 21 | func Fuzz(data []byte) int { 22 | str := string(data) 23 | 24 | ch := scanner.Scan(str, ast.Pos{Line: 1, Column: 1}) 25 | _, err := Parse(ch) 26 | if err != nil { 27 | return 0 28 | } 29 | 30 | return 1 31 | } 32 | -------------------------------------------------------------------------------- /ast/call_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestCallType(t *testing.T) { 11 | c := &Call{Func: "foo"} 12 | scope := &BasicScope{ 13 | FuncMap: map[string]Function{ 14 | "foo": Function{ReturnType: TypeString}, 15 | }, 16 | } 17 | 18 | actual, err := c.Type(scope) 19 | if err != nil { 20 | t.Fatalf("err: %s", err) 21 | } 22 | if actual != TypeString { 23 | t.Fatalf("bad: %s", actual) 24 | } 25 | } 26 | 27 | func TestCallType_invalid(t *testing.T) { 28 | c := &Call{Func: "bar"} 29 | scope := &BasicScope{ 30 | FuncMap: map[string]Function{ 31 | "foo": Function{ReturnType: TypeString}, 32 | }, 33 | } 34 | 35 | _, err := c.Type(scope) 36 | if err == nil { 37 | t.Fatal("should error") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ast/unknown.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | // IsUnknown reports whether a variable is unknown or contains any value 7 | // that is unknown. This will recurse into lists and maps and so on. 8 | func IsUnknown(v Variable) bool { 9 | // If it is unknown itself, return true 10 | if v.Type == TypeUnknown { 11 | return true 12 | } 13 | 14 | // If it is a container type, check the values 15 | switch v.Type { 16 | case TypeList: 17 | for _, el := range v.Value.([]Variable) { 18 | if IsUnknown(el) { 19 | return true 20 | } 21 | } 22 | case TypeMap: 23 | for _, el := range v.Value.(map[string]Variable) { 24 | if IsUnknown(el) { 25 | return true 26 | } 27 | } 28 | default: 29 | } 30 | 31 | // Not a container type or survive the above checks 32 | return false 33 | } 34 | -------------------------------------------------------------------------------- /ast/variable_access.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // VariableAccess represents a variable access. 11 | type VariableAccess struct { 12 | Name string 13 | Posx Pos 14 | } 15 | 16 | func (n *VariableAccess) Accept(v Visitor) Node { 17 | return v(n) 18 | } 19 | 20 | func (n *VariableAccess) Pos() Pos { 21 | return n.Posx 22 | } 23 | 24 | func (n *VariableAccess) GoString() string { 25 | return fmt.Sprintf("*%#v", *n) 26 | } 27 | 28 | func (n *VariableAccess) String() string { 29 | return fmt.Sprintf("Variable(%s)", n.Name) 30 | } 31 | 32 | func (n *VariableAccess) Type(s Scope) (Type, error) { 33 | v, ok := s.LookupVar(n.Name) 34 | if !ok { 35 | return TypeInvalid, fmt.Errorf("unknown variable: %s", n.Name) 36 | } 37 | 38 | return v.Type, nil 39 | } 40 | -------------------------------------------------------------------------------- /ast/stack_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestStack(t *testing.T) { 12 | var s Stack 13 | if s.Len() != 0 { 14 | t.Fatalf("bad: %d", s.Len()) 15 | } 16 | 17 | n := &LiteralNode{Value: 42} 18 | s.Push(n) 19 | 20 | if s.Len() != 1 { 21 | t.Fatalf("bad: %d", s.Len()) 22 | } 23 | 24 | actual := s.Pop() 25 | if !reflect.DeepEqual(actual, n) { 26 | t.Fatalf("bad: %#v", actual) 27 | } 28 | 29 | if s.Len() != 0 { 30 | t.Fatalf("bad: %d", s.Len()) 31 | } 32 | } 33 | 34 | func TestStack_reset(t *testing.T) { 35 | var s Stack 36 | 37 | n := &LiteralNode{Value: 42} 38 | s.Push(n) 39 | 40 | if s.Len() != 1 { 41 | t.Fatalf("bad: %d", s.Len()) 42 | } 43 | 44 | s.Reset() 45 | 46 | if s.Len() != 0 { 47 | t.Fatalf("bad: %d", s.Len()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example_var_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil_test 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/hashicorp/hil" 11 | "github.com/hashicorp/hil/ast" 12 | ) 13 | 14 | func Example_variables() { 15 | input := "${var.test} - ${6 + 2}" 16 | 17 | tree, err := hil.Parse(input) 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | config := &hil.EvalConfig{ 23 | GlobalScope: &ast.BasicScope{ 24 | VarMap: map[string]ast.Variable{ 25 | "var.test": ast.Variable{ 26 | Type: ast.TypeString, 27 | Value: "TEST STRING", 28 | }, 29 | }, 30 | }, 31 | } 32 | 33 | result, err := hil.Eval(tree, config) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | fmt.Printf("Type: %s\n", result.Type) 39 | fmt.Printf("Value: %s\n", result.Value) 40 | // Output: 41 | // Type: TypeString 42 | // Value: TEST STRING - 8 43 | } 44 | -------------------------------------------------------------------------------- /transform_fixed.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "github.com/hashicorp/hil/ast" 8 | ) 9 | 10 | // FixedValueTransform transforms an AST to return a fixed value for 11 | // all interpolations. i.e. you can make "hi ${anything}" always 12 | // turn into "hi foo". 13 | // 14 | // The primary use case for this is for config validations where you can 15 | // verify that interpolations result in a certain type of string. 16 | func FixedValueTransform(root ast.Node, Value *ast.LiteralNode) ast.Node { 17 | // We visit the nodes in top-down order 18 | result := root 19 | switch n := result.(type) { 20 | case *ast.Output: 21 | for i, v := range n.Exprs { 22 | n.Exprs[i] = FixedValueTransform(v, Value) 23 | } 24 | case *ast.LiteralNode: 25 | // We keep it as-is 26 | default: 27 | // Anything else we replace 28 | result = Value 29 | } 30 | 31 | return result 32 | } 33 | -------------------------------------------------------------------------------- /ast/arithmetic.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | ) 10 | 11 | // Arithmetic represents a node where the result is arithmetic of 12 | // two or more operands in the order given. 13 | type Arithmetic struct { 14 | Op ArithmeticOp 15 | Exprs []Node 16 | Posx Pos 17 | } 18 | 19 | func (n *Arithmetic) Accept(v Visitor) Node { 20 | for i, expr := range n.Exprs { 21 | n.Exprs[i] = expr.Accept(v) 22 | } 23 | 24 | return v(n) 25 | } 26 | 27 | func (n *Arithmetic) Pos() Pos { 28 | return n.Posx 29 | } 30 | 31 | func (n *Arithmetic) GoString() string { 32 | return fmt.Sprintf("*%#v", *n) 33 | } 34 | 35 | func (n *Arithmetic) String() string { 36 | var b bytes.Buffer 37 | for _, expr := range n.Exprs { 38 | b.WriteString(fmt.Sprintf("%s", expr)) 39 | } 40 | 41 | return b.String() 42 | } 43 | 44 | func (n *Arithmetic) Type(Scope) (Type, error) { 45 | return TypeInt, nil 46 | } 47 | -------------------------------------------------------------------------------- /ast/conditional.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | type Conditional struct { 11 | CondExpr Node 12 | TrueExpr Node 13 | FalseExpr Node 14 | Posx Pos 15 | } 16 | 17 | // Accept passes the given visitor to the child nodes in this order: 18 | // CondExpr, TrueExpr, FalseExpr. It then finally passes itself to the visitor. 19 | func (n *Conditional) Accept(v Visitor) Node { 20 | n.CondExpr = n.CondExpr.Accept(v) 21 | n.TrueExpr = n.TrueExpr.Accept(v) 22 | n.FalseExpr = n.FalseExpr.Accept(v) 23 | 24 | return v(n) 25 | } 26 | 27 | func (n *Conditional) Pos() Pos { 28 | return n.Posx 29 | } 30 | 31 | func (n *Conditional) Type(Scope) (Type, error) { 32 | // This is not actually a useful value; the type checker ignores 33 | // this function when analyzing conditionals, just as with Arithmetic. 34 | return TypeInt, nil 35 | } 36 | 37 | func (n *Conditional) GoString() string { 38 | return fmt.Sprintf("*%#v", *n) 39 | } 40 | -------------------------------------------------------------------------------- /ast/call.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | // Call represents a function call. 12 | type Call struct { 13 | Func string 14 | Args []Node 15 | Posx Pos 16 | } 17 | 18 | func (n *Call) Accept(v Visitor) Node { 19 | for i, a := range n.Args { 20 | n.Args[i] = a.Accept(v) 21 | } 22 | 23 | return v(n) 24 | } 25 | 26 | func (n *Call) Pos() Pos { 27 | return n.Posx 28 | } 29 | 30 | func (n *Call) String() string { 31 | args := make([]string, len(n.Args)) 32 | for i, arg := range n.Args { 33 | args[i] = fmt.Sprintf("%s", arg) 34 | } 35 | 36 | return fmt.Sprintf("Call(%s, %s)", n.Func, strings.Join(args, ", ")) 37 | } 38 | 39 | func (n *Call) Type(s Scope) (Type, error) { 40 | f, ok := s.LookupFunc(n.Func) 41 | if !ok { 42 | return TypeInvalid, fmt.Errorf("unknown function: %s", n.Func) 43 | } 44 | 45 | return f.ReturnType, nil 46 | } 47 | 48 | func (n *Call) GoString() string { 49 | return fmt.Sprintf("*%#v", *n) 50 | } 51 | -------------------------------------------------------------------------------- /parser/error.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package parser 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/hil/ast" 10 | "github.com/hashicorp/hil/scanner" 11 | ) 12 | 13 | type ParseError struct { 14 | Message string 15 | Pos ast.Pos 16 | } 17 | 18 | func Errorf(pos ast.Pos, format string, args ...interface{}) error { 19 | return &ParseError{ 20 | Message: fmt.Sprintf(format, args...), 21 | Pos: pos, 22 | } 23 | } 24 | 25 | // TokenErrorf is a convenient wrapper around Errorf that uses the 26 | // position of the given token. 27 | func TokenErrorf(token *scanner.Token, format string, args ...interface{}) error { 28 | return Errorf(token.Pos, format, args...) 29 | } 30 | 31 | func ExpectationError(wanted string, got *scanner.Token) error { 32 | return TokenErrorf(got, "expected %s but found %s", wanted, got) 33 | } 34 | 35 | func (e *ParseError) Error() string { 36 | return fmt.Sprintf("parse error at %s: %s", e.Pos, e.Message) 37 | } 38 | 39 | func (e *ParseError) String() string { 40 | return e.Error() 41 | } 42 | -------------------------------------------------------------------------------- /transform_fixed_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | ) 12 | 13 | func TestFixedValueTransform(t *testing.T) { 14 | cases := []struct { 15 | Input ast.Node 16 | Output ast.Node 17 | }{ 18 | { 19 | &ast.LiteralNode{Value: 42}, 20 | &ast.LiteralNode{Value: 42}, 21 | }, 22 | 23 | { 24 | &ast.VariableAccess{Name: "bar"}, 25 | &ast.LiteralNode{Value: "foo"}, 26 | }, 27 | 28 | { 29 | &ast.Output{ 30 | Exprs: []ast.Node{ 31 | &ast.VariableAccess{Name: "bar"}, 32 | &ast.LiteralNode{Value: 42}, 33 | }, 34 | }, 35 | &ast.Output{ 36 | Exprs: []ast.Node{ 37 | &ast.LiteralNode{Value: "foo"}, 38 | &ast.LiteralNode{Value: 42}, 39 | }, 40 | }, 41 | }, 42 | } 43 | 44 | value := &ast.LiteralNode{Value: "foo"} 45 | for _, tc := range cases { 46 | actual := FixedValueTransform(tc.Input, value) 47 | if !reflect.DeepEqual(actual, tc.Output) { 48 | t.Fatalf("bad: %#v\n\nInput: %#v", actual, tc.Input) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /evaltype_string.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by "stringer -type=EvalType eval_type.go"; DO NOT EDIT 5 | 6 | package hil 7 | 8 | import "fmt" 9 | 10 | const ( 11 | _EvalType_name_0 = "TypeInvalid" 12 | _EvalType_name_1 = "TypeString" 13 | _EvalType_name_2 = "TypeBool" 14 | _EvalType_name_3 = "TypeList" 15 | _EvalType_name_4 = "TypeMap" 16 | _EvalType_name_5 = "TypeUnknown" 17 | ) 18 | 19 | var ( 20 | _EvalType_index_0 = [...]uint8{0, 11} 21 | _EvalType_index_1 = [...]uint8{0, 10} 22 | _EvalType_index_2 = [...]uint8{0, 8} 23 | _EvalType_index_3 = [...]uint8{0, 8} 24 | _EvalType_index_4 = [...]uint8{0, 7} 25 | _EvalType_index_5 = [...]uint8{0, 11} 26 | ) 27 | 28 | func (i EvalType) String() string { 29 | switch { 30 | case i == 0: 31 | return _EvalType_name_0 32 | case i == 2: 33 | return _EvalType_name_1 34 | case i == 4: 35 | return _EvalType_name_2 36 | case i == 8: 37 | return _EvalType_name_3 38 | case i == 16: 39 | return _EvalType_name_4 40 | case i == 32: 41 | return _EvalType_name_5 42 | default: 43 | return fmt.Sprintf("EvalType(%d)", i) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "github.com/hashicorp/hil/ast" 8 | "github.com/hashicorp/hil/parser" 9 | "github.com/hashicorp/hil/scanner" 10 | ) 11 | 12 | // Parse parses the given program and returns an executable AST tree. 13 | // 14 | // Syntax errors are returned with error having the dynamic type 15 | // *parser.ParseError, which gives the caller access to the source position 16 | // where the error was found, which allows (for example) combining it with 17 | // a known source filename to add context to the error message. 18 | func Parse(v string) (ast.Node, error) { 19 | return ParseWithPosition(v, ast.Pos{Line: 1, Column: 1}) 20 | } 21 | 22 | // ParseWithPosition is like Parse except that it overrides the source 23 | // row and column position of the first character in the string, which should 24 | // be 1-based. 25 | // 26 | // This can be used when HIL is embedded in another language and the outer 27 | // parser knows the row and column where the HIL expression started within 28 | // the overall source file. 29 | func ParseWithPosition(v string, pos ast.Pos) (ast.Node, error) { 30 | ch := scanner.Scan(v, pos) 31 | return parser.Parse(ch) 32 | } 33 | -------------------------------------------------------------------------------- /ast/variable_access_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestVariableAccessType(t *testing.T) { 11 | c := &VariableAccess{Name: "foo"} 12 | scope := &BasicScope{ 13 | VarMap: map[string]Variable{ 14 | "foo": Variable{Type: TypeString}, 15 | }, 16 | } 17 | 18 | actual, err := c.Type(scope) 19 | if err != nil { 20 | t.Fatalf("err: %s", err) 21 | } 22 | if actual != TypeString { 23 | t.Fatalf("bad: %s", actual) 24 | } 25 | } 26 | 27 | func TestVariableAccessType_invalid(t *testing.T) { 28 | c := &VariableAccess{Name: "bar"} 29 | scope := &BasicScope{ 30 | VarMap: map[string]Variable{ 31 | "foo": Variable{Type: TypeString}, 32 | }, 33 | } 34 | 35 | _, err := c.Type(scope) 36 | if err == nil { 37 | t.Fatal("should error") 38 | } 39 | } 40 | 41 | func TestVariableAccessType_list(t *testing.T) { 42 | c := &VariableAccess{Name: "baz"} 43 | scope := &BasicScope{ 44 | VarMap: map[string]Variable{ 45 | "baz": Variable{Type: TypeList}, 46 | }, 47 | } 48 | 49 | actual, err := c.Type(scope) 50 | if err != nil { 51 | t.Fatalf("err: %s", err) 52 | } 53 | if actual != TypeList { 54 | t.Fatalf("bad: %s", actual) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ast/scope_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestBasicScope_impl(t *testing.T) { 11 | var _ Scope = new(BasicScope) 12 | } 13 | 14 | func TestBasicScopeLookupFunc(t *testing.T) { 15 | scope := &BasicScope{ 16 | FuncMap: map[string]Function{ 17 | "foo": Function{}, 18 | }, 19 | } 20 | 21 | if _, ok := scope.LookupFunc("bar"); ok { 22 | t.Fatal("should not find bar") 23 | } 24 | if _, ok := scope.LookupFunc("foo"); !ok { 25 | t.Fatal("should find foo") 26 | } 27 | } 28 | 29 | func TestBasicScopeLookupVar(t *testing.T) { 30 | scope := &BasicScope{ 31 | VarMap: map[string]Variable{ 32 | "foo": Variable{}, 33 | }, 34 | } 35 | 36 | if _, ok := scope.LookupVar("bar"); ok { 37 | t.Fatal("should not find bar") 38 | } 39 | if _, ok := scope.LookupVar("foo"); !ok { 40 | t.Fatal("should find foo") 41 | } 42 | } 43 | 44 | func TestVariableStringer(t *testing.T) { 45 | expected := "{Variable (TypeInt): 42}" 46 | variable := &Variable{ 47 | Type: TypeInt, 48 | Value: 42, 49 | } 50 | 51 | actual := variable.String() 52 | 53 | if actual != expected { 54 | t.Fatalf("variable string formatting:\nExpected: %s\n Got: %s\n", 55 | expected, actual) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example_func_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil_test 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "strings" 10 | 11 | "github.com/hashicorp/hil" 12 | "github.com/hashicorp/hil/ast" 13 | ) 14 | 15 | func Example_functions() { 16 | input := "${lower(var.test)} - ${6 + 2}" 17 | 18 | tree, err := hil.Parse(input) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | lowerCase := ast.Function{ 24 | ArgTypes: []ast.Type{ast.TypeString}, 25 | ReturnType: ast.TypeString, 26 | Variadic: false, 27 | Callback: func(inputs []interface{}) (interface{}, error) { 28 | input := inputs[0].(string) 29 | return strings.ToLower(input), nil 30 | }, 31 | } 32 | 33 | config := &hil.EvalConfig{ 34 | GlobalScope: &ast.BasicScope{ 35 | VarMap: map[string]ast.Variable{ 36 | "var.test": ast.Variable{ 37 | Type: ast.TypeString, 38 | Value: "TEST STRING", 39 | }, 40 | }, 41 | FuncMap: map[string]ast.Function{ 42 | "lower": lowerCase, 43 | }, 44 | }, 45 | } 46 | 47 | result, err := hil.Eval(tree, config) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | fmt.Printf("Type: %s\n", result.Type) 53 | fmt.Printf("Value: %s\n", result.Value) 54 | // Output: 55 | // Type: TypeString 56 | // Value: test string - 8 57 | } 58 | -------------------------------------------------------------------------------- /ast/type_string.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by "stringer -type=Type"; DO NOT EDIT 5 | 6 | package ast 7 | 8 | import "fmt" 9 | 10 | const ( 11 | _Type_name_0 = "TypeInvalid" 12 | _Type_name_1 = "TypeAny" 13 | _Type_name_2 = "TypeBool" 14 | _Type_name_3 = "TypeString" 15 | _Type_name_4 = "TypeInt" 16 | _Type_name_5 = "TypeFloat" 17 | _Type_name_6 = "TypeList" 18 | _Type_name_7 = "TypeMap" 19 | _Type_name_8 = "TypeUnknown" 20 | ) 21 | 22 | var ( 23 | _Type_index_0 = [...]uint8{0, 11} 24 | _Type_index_1 = [...]uint8{0, 7} 25 | _Type_index_2 = [...]uint8{0, 8} 26 | _Type_index_3 = [...]uint8{0, 10} 27 | _Type_index_4 = [...]uint8{0, 7} 28 | _Type_index_5 = [...]uint8{0, 9} 29 | _Type_index_6 = [...]uint8{0, 8} 30 | _Type_index_7 = [...]uint8{0, 7} 31 | _Type_index_8 = [...]uint8{0, 11} 32 | ) 33 | 34 | func (i Type) String() string { 35 | switch { 36 | case i == 0: 37 | return _Type_name_0 38 | case i == 2: 39 | return _Type_name_1 40 | case i == 4: 41 | return _Type_name_2 42 | case i == 8: 43 | return _Type_name_3 44 | case i == 16: 45 | return _Type_name_4 46 | case i == 32: 47 | return _Type_name_5 48 | case i == 64: 49 | return _Type_name_6 50 | case i == 128: 51 | return _Type_name_7 52 | case i == 256: 53 | return _Type_name_8 54 | default: 55 | return fmt.Sprintf("Type(%d)", i) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /parser/binary_op.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package parser 5 | 6 | import ( 7 | "github.com/hashicorp/hil/ast" 8 | "github.com/hashicorp/hil/scanner" 9 | ) 10 | 11 | var binaryOps []map[scanner.TokenType]ast.ArithmeticOp 12 | 13 | func init() { 14 | // This operation table maps from the operator's scanner token type 15 | // to the AST arithmetic operation. All expressions produced from 16 | // binary operators are *ast.Arithmetic nodes. 17 | // 18 | // Binary operator groups are listed in order of precedence, with 19 | // the *lowest* precedence first. Operators within the same group 20 | // have left-to-right associativity. 21 | binaryOps = []map[scanner.TokenType]ast.ArithmeticOp{ 22 | { 23 | scanner.OR: ast.ArithmeticOpLogicalOr, 24 | }, 25 | { 26 | scanner.AND: ast.ArithmeticOpLogicalAnd, 27 | }, 28 | { 29 | scanner.EQUAL: ast.ArithmeticOpEqual, 30 | scanner.NOTEQUAL: ast.ArithmeticOpNotEqual, 31 | }, 32 | { 33 | scanner.GT: ast.ArithmeticOpGreaterThan, 34 | scanner.GTE: ast.ArithmeticOpGreaterThanOrEqual, 35 | scanner.LT: ast.ArithmeticOpLessThan, 36 | scanner.LTE: ast.ArithmeticOpLessThanOrEqual, 37 | }, 38 | { 39 | scanner.PLUS: ast.ArithmeticOpAdd, 40 | scanner.MINUS: ast.ArithmeticOpSub, 41 | }, 42 | { 43 | scanner.STAR: ast.ArithmeticOpMul, 44 | scanner.SLASH: ast.ArithmeticOpDiv, 45 | scanner.PERCENT: ast.ArithmeticOpMod, 46 | }, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scanner/token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package scanner 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestTokenString(t *testing.T) { 11 | cases := []struct { 12 | Token *Token 13 | String string 14 | }{ 15 | { 16 | &Token{ 17 | Type: EOF, 18 | Content: "", 19 | }, 20 | "end of string", 21 | }, 22 | 23 | { 24 | &Token{ 25 | Type: INVALID, 26 | Content: "baz", 27 | }, 28 | `invalid sequence "baz"`, 29 | }, 30 | 31 | { 32 | &Token{ 33 | Type: INTEGER, 34 | Content: "1", 35 | }, 36 | `integer 1`, 37 | }, 38 | 39 | { 40 | &Token{ 41 | Type: FLOAT, 42 | Content: "1.2", 43 | }, 44 | `float 1.2`, 45 | }, 46 | 47 | { 48 | &Token{ 49 | Type: STRING, 50 | Content: "foo", 51 | }, 52 | `string "foo"`, 53 | }, 54 | 55 | { 56 | &Token{ 57 | Type: LITERAL, 58 | Content: "foo", 59 | }, 60 | `literal "foo"`, 61 | }, 62 | 63 | { 64 | &Token{ 65 | Type: BOOL, 66 | Content: "true", 67 | }, 68 | `"true"`, 69 | }, 70 | 71 | { 72 | &Token{ 73 | Type: BEGIN, 74 | Content: "${", 75 | }, 76 | `"${"`, 77 | }, 78 | } 79 | 80 | for _, tc := range cases { 81 | str := tc.Token.String() 82 | if got, want := str, tc.String; got != want { 83 | t.Errorf( 84 | "%s %q returned %q; want %q", 85 | tc.Token.Type, tc.Token.Content, 86 | got, want, 87 | ) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ast/variables_helper.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import "fmt" 7 | 8 | func VariableListElementTypesAreHomogenous(variableName string, list []Variable) (Type, error) { 9 | if len(list) == 0 { 10 | return TypeInvalid, fmt.Errorf("list %q does not have any elements so cannot determine type", variableName) 11 | } 12 | 13 | elemType := TypeUnknown 14 | for _, v := range list { 15 | if v.Type == TypeUnknown { 16 | continue 17 | } 18 | 19 | if elemType == TypeUnknown { 20 | elemType = v.Type 21 | continue 22 | } 23 | 24 | if v.Type != elemType { 25 | return TypeInvalid, fmt.Errorf( 26 | "list %q does not have homogenous types. found %s and then %s", 27 | variableName, 28 | elemType, v.Type, 29 | ) 30 | } 31 | 32 | elemType = v.Type 33 | } 34 | 35 | return elemType, nil 36 | } 37 | 38 | func VariableMapValueTypesAreHomogenous(variableName string, vmap map[string]Variable) (Type, error) { 39 | if len(vmap) == 0 { 40 | return TypeInvalid, fmt.Errorf("map %q does not have any elements so cannot determine type", variableName) 41 | } 42 | 43 | elemType := TypeUnknown 44 | for _, v := range vmap { 45 | if v.Type == TypeUnknown { 46 | continue 47 | } 48 | 49 | if elemType == TypeUnknown { 50 | elemType = v.Type 51 | continue 52 | } 53 | 54 | if v.Type != elemType { 55 | return TypeInvalid, fmt.Errorf( 56 | "map %q does not have homogenous types. found %s and then %s", 57 | variableName, 58 | elemType, v.Type, 59 | ) 60 | } 61 | 62 | elemType = v.Type 63 | } 64 | 65 | return elemType, nil 66 | } 67 | -------------------------------------------------------------------------------- /scanner/peeker.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package scanner 5 | 6 | // Peeker is a utility that wraps a token channel returned by Scan and 7 | // provides an interface that allows a caller (e.g. the parser) to 8 | // work with the token stream in a mode that allows one token of lookahead, 9 | // and provides utilities for more convenient processing of the stream. 10 | type Peeker struct { 11 | ch <-chan *Token 12 | peeked *Token 13 | } 14 | 15 | func NewPeeker(ch <-chan *Token) *Peeker { 16 | return &Peeker{ 17 | ch: ch, 18 | } 19 | } 20 | 21 | // Peek returns the next token in the stream without consuming it. A 22 | // subsequent call to Read will return the same token. 23 | func (p *Peeker) Peek() *Token { 24 | if p.peeked == nil { 25 | p.peeked = <-p.ch 26 | } 27 | return p.peeked 28 | } 29 | 30 | // Read consumes the next token in the stream and returns it. 31 | func (p *Peeker) Read() *Token { 32 | token := p.Peek() 33 | 34 | // As a special case, we will produce the EOF token forever once 35 | // it is reached. 36 | if token.Type != EOF { 37 | p.peeked = nil 38 | } 39 | 40 | return token 41 | } 42 | 43 | // Close ensures that the token stream has been exhausted, to prevent 44 | // the goroutine in the underlying scanner from leaking. 45 | // 46 | // It's not necessary to call this if the caller reads the token stream 47 | // to EOF, since that implicitly closes the scanner. 48 | func (p *Peeker) Close() { 49 | for range p.ch { 50 | // discard 51 | } 52 | // Install a synthetic EOF token in 'peeked' in case someone 53 | // erroneously calls Peek() or Read() after we've closed. 54 | p.peeked = &Token{ 55 | Type: EOF, 56 | Content: "", 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ast/literal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestLiteralNodeType(t *testing.T) { 13 | c := &LiteralNode{Typex: TypeString} 14 | actual, err := c.Type(nil) 15 | if err != nil { 16 | t.Fatalf("err: %s", err) 17 | } 18 | if actual != TypeString { 19 | t.Fatalf("bad: %s", actual) 20 | } 21 | } 22 | 23 | func TestNewLiteralNode(t *testing.T) { 24 | tests := []struct { 25 | Value interface{} 26 | Expected *LiteralNode 27 | }{ 28 | { 29 | 1, 30 | &LiteralNode{ 31 | Value: 1, 32 | Typex: TypeInt, 33 | }, 34 | }, 35 | { 36 | 1.0, 37 | &LiteralNode{ 38 | Value: 1.0, 39 | Typex: TypeFloat, 40 | }, 41 | }, 42 | { 43 | true, 44 | &LiteralNode{ 45 | Value: true, 46 | Typex: TypeBool, 47 | }, 48 | }, 49 | { 50 | "hi", 51 | &LiteralNode{ 52 | Value: "hi", 53 | Typex: TypeString, 54 | }, 55 | }, 56 | } 57 | 58 | for _, test := range tests { 59 | t.Run(fmt.Sprintf("%T", test.Value), func(t *testing.T) { 60 | inPos := Pos{ 61 | Column: 2, 62 | Line: 3, 63 | Filename: "foo", 64 | } 65 | node, err := NewLiteralNode(test.Value, inPos) 66 | 67 | if err != nil { 68 | t.Fatalf("error: %s", err) 69 | } 70 | 71 | if got, want := node.Typex, test.Expected.Typex; want != got { 72 | t.Errorf("got type %s; want %s", got, want) 73 | } 74 | if got, want := node.Value, test.Expected.Value; want != got { 75 | t.Errorf("got value %#v; want %#v", got, want) 76 | } 77 | if got, want := node.Posx, inPos; !reflect.DeepEqual(got, want) { 78 | t.Errorf("got position %#v; want %#v", got, want) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /scanner/tokentype_string.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by "stringer -type=TokenType"; DO NOT EDIT 5 | 6 | package scanner 7 | 8 | import "fmt" 9 | 10 | const _TokenType_name = "BANGBEGINPERCENTOPARENCPARENSTARPLUSCOMMAMINUSPERIODSLASHCOLONLTEQUALGTQUESTIONBOOLFLOATINTEGERSTRINGOBRACKETCBRACKETIDENTIFIERLITERALENDOQUOTECQUOTEANDORNOTEQUALLTEGTEEOFINVALID" 11 | 12 | var _TokenType_map = map[TokenType]string{ 13 | 33: _TokenType_name[0:4], 14 | 36: _TokenType_name[4:9], 15 | 37: _TokenType_name[9:16], 16 | 40: _TokenType_name[16:22], 17 | 41: _TokenType_name[22:28], 18 | 42: _TokenType_name[28:32], 19 | 43: _TokenType_name[32:36], 20 | 44: _TokenType_name[36:41], 21 | 45: _TokenType_name[41:46], 22 | 46: _TokenType_name[46:52], 23 | 47: _TokenType_name[52:57], 24 | 58: _TokenType_name[57:62], 25 | 60: _TokenType_name[62:64], 26 | 61: _TokenType_name[64:69], 27 | 62: _TokenType_name[69:71], 28 | 63: _TokenType_name[71:79], 29 | 66: _TokenType_name[79:83], 30 | 70: _TokenType_name[83:88], 31 | 73: _TokenType_name[88:95], 32 | 83: _TokenType_name[95:101], 33 | 91: _TokenType_name[101:109], 34 | 93: _TokenType_name[109:117], 35 | 105: _TokenType_name[117:127], 36 | 111: _TokenType_name[127:134], 37 | 125: _TokenType_name[134:137], 38 | 8220: _TokenType_name[137:143], 39 | 8221: _TokenType_name[143:149], 40 | 8743: _TokenType_name[149:152], 41 | 8744: _TokenType_name[152:154], 42 | 8800: _TokenType_name[154:162], 43 | 8804: _TokenType_name[162:165], 44 | 8805: _TokenType_name[165:168], 45 | 9220: _TokenType_name[168:171], 46 | 65533: _TokenType_name[171:178], 47 | } 48 | 49 | func (i TokenType) String() string { 50 | if str, ok := _TokenType_map[i]; ok { 51 | return str 52 | } 53 | return fmt.Sprintf("TokenType(%d)", i) 54 | } 55 | -------------------------------------------------------------------------------- /scanner/peeker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package scanner 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestPeeker(t *testing.T) { 11 | ch := make(chan *Token) 12 | 13 | go func() { 14 | ch <- &Token{ 15 | Type: IDENTIFIER, 16 | Content: "foo", 17 | } 18 | ch <- &Token{ 19 | Type: INTEGER, 20 | Content: "1", 21 | } 22 | ch <- &Token{ 23 | Type: EOF, 24 | Content: "", 25 | } 26 | close(ch) 27 | }() 28 | 29 | peeker := NewPeeker(ch) 30 | 31 | if got, want := peeker.Peek().Type, IDENTIFIER; got != want { 32 | t.Fatalf("first peek returned %s; want %s", got, want) 33 | } 34 | if got, want := peeker.Read().Type, IDENTIFIER; got != want { 35 | t.Fatalf("first read returned %s; want %s", got, want) 36 | } 37 | if got, want := peeker.Peek().Type, INTEGER; got != want { 38 | t.Fatalf("second peek returned %s; want %s", got, want) 39 | } 40 | if got, want := peeker.Peek().Type, INTEGER; got != want { 41 | t.Fatalf("third peek returned %s; want %s", got, want) 42 | } 43 | if got, want := peeker.Read().Type, INTEGER; got != want { 44 | t.Fatalf("second read returned %s; want %s", got, want) 45 | } 46 | if got, want := peeker.Read().Type, EOF; got != want { 47 | t.Fatalf("third read returned %s; want %s", got, want) 48 | } 49 | // reading again after EOF just returns EOF again 50 | if got, want := peeker.Read().Type, EOF; got != want { 51 | t.Fatalf("final read returned %s; want %s", got, want) 52 | } 53 | if got, want := peeker.Peek().Type, EOF; got != want { 54 | t.Fatalf("final peek returned %s; want %s", got, want) 55 | } 56 | 57 | peeker.Close() 58 | if got, want := peeker.Peek().Type, EOF; got != want { 59 | t.Fatalf("peek after close returned %s; want %s", got, want) 60 | } 61 | if got, want := peeker.Read().Type, EOF; got != want { 62 | t.Fatalf("read after close returned %s; want %s", got, want) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ast/index.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // Index represents an indexing operation into another data structure 11 | type Index struct { 12 | Target Node 13 | Key Node 14 | Posx Pos 15 | } 16 | 17 | func (n *Index) Accept(v Visitor) Node { 18 | n.Target = n.Target.Accept(v) 19 | n.Key = n.Key.Accept(v) 20 | return v(n) 21 | } 22 | 23 | func (n *Index) Pos() Pos { 24 | return n.Posx 25 | } 26 | 27 | func (n *Index) String() string { 28 | return fmt.Sprintf("Index(%s, %s)", n.Target, n.Key) 29 | } 30 | 31 | func (n *Index) Type(s Scope) (Type, error) { 32 | variableAccess, ok := n.Target.(*VariableAccess) 33 | if !ok { 34 | return TypeInvalid, fmt.Errorf("target is not a variable") 35 | } 36 | 37 | variable, ok := s.LookupVar(variableAccess.Name) 38 | if !ok { 39 | return TypeInvalid, fmt.Errorf("unknown variable accessed: %s", variableAccess.Name) 40 | } 41 | 42 | switch variable.Type { 43 | case TypeList: 44 | return n.typeList(variable, variableAccess.Name) 45 | case TypeMap: 46 | return n.typeMap(variable, variableAccess.Name) 47 | default: 48 | return TypeInvalid, fmt.Errorf("invalid index operation into non-indexable type: %s", variable.Type) 49 | } 50 | } 51 | 52 | func (n *Index) typeList(variable Variable, variableName string) (Type, error) { 53 | // We assume type checking has already determined that this is a list 54 | list := variable.Value.([]Variable) 55 | 56 | return VariableListElementTypesAreHomogenous(variableName, list) 57 | } 58 | 59 | func (n *Index) typeMap(variable Variable, variableName string) (Type, error) { 60 | // We assume type checking has already determined that this is a map 61 | vmap := variable.Value.(map[string]Variable) 62 | 63 | return VariableMapValueTypesAreHomogenous(variableName, vmap) 64 | } 65 | 66 | func (n *Index) GoString() string { 67 | return fmt.Sprintf("*%#v", *n) 68 | } 69 | -------------------------------------------------------------------------------- /ast/output.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | ) 10 | 11 | // Output represents the root node of all interpolation evaluations. If the 12 | // output only has one expression which is either a TypeList or TypeMap, the 13 | // Output can be type-asserted to []interface{} or map[string]interface{} 14 | // respectively. Otherwise the Output evaluates as a string, and concatenates 15 | // the evaluation of each expression. 16 | type Output struct { 17 | Exprs []Node 18 | Posx Pos 19 | } 20 | 21 | func (n *Output) Accept(v Visitor) Node { 22 | for i, expr := range n.Exprs { 23 | n.Exprs[i] = expr.Accept(v) 24 | } 25 | 26 | return v(n) 27 | } 28 | 29 | func (n *Output) Pos() Pos { 30 | return n.Posx 31 | } 32 | 33 | func (n *Output) GoString() string { 34 | return fmt.Sprintf("*%#v", *n) 35 | } 36 | 37 | func (n *Output) String() string { 38 | var b bytes.Buffer 39 | for _, expr := range n.Exprs { 40 | b.WriteString(fmt.Sprintf("%s", expr)) 41 | } 42 | 43 | return b.String() 44 | } 45 | 46 | func (n *Output) Type(s Scope) (Type, error) { 47 | // Special case no expressions for backward compatibility 48 | if len(n.Exprs) == 0 { 49 | return TypeString, nil 50 | } 51 | 52 | // Special case a single expression of types list or map 53 | if len(n.Exprs) == 1 { 54 | exprType, err := n.Exprs[0].Type(s) 55 | if err != nil { 56 | return TypeInvalid, err 57 | } 58 | switch exprType { 59 | case TypeList: 60 | return TypeList, nil 61 | case TypeMap: 62 | return TypeMap, nil 63 | } 64 | } 65 | 66 | // Otherwise ensure all our expressions are strings 67 | for index, expr := range n.Exprs { 68 | exprType, err := expr.Type(s) 69 | if err != nil { 70 | return TypeInvalid, err 71 | } 72 | // We only look for things we know we can't coerce with an implicit conversion func 73 | if exprType == TypeList || exprType == TypeMap { 74 | return TypeInvalid, fmt.Errorf( 75 | "multi-expression HIL outputs may only have string inputs: %d is type %s", 76 | index, exprType) 77 | } 78 | } 79 | 80 | return TypeString, nil 81 | } 82 | -------------------------------------------------------------------------------- /check_identifier.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "fmt" 8 | "sync" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | ) 12 | 13 | // IdentifierCheck is a SemanticCheck that checks that all identifiers 14 | // resolve properly and that the right number of arguments are passed 15 | // to functions. 16 | type IdentifierCheck struct { 17 | Scope ast.Scope 18 | 19 | err error 20 | lock sync.Mutex 21 | } 22 | 23 | func (c *IdentifierCheck) Visit(root ast.Node) error { 24 | c.lock.Lock() 25 | defer c.lock.Unlock() 26 | defer c.reset() 27 | root.Accept(c.visit) 28 | return c.err 29 | } 30 | 31 | func (c *IdentifierCheck) visit(raw ast.Node) ast.Node { 32 | if c.err != nil { 33 | return raw 34 | } 35 | 36 | switch n := raw.(type) { 37 | case *ast.Call: 38 | c.visitCall(n) 39 | case *ast.VariableAccess: 40 | c.visitVariableAccess(n) 41 | case *ast.Output: 42 | // Ignore 43 | case *ast.LiteralNode: 44 | // Ignore 45 | default: 46 | // Ignore 47 | } 48 | 49 | // We never do replacement with this visitor 50 | return raw 51 | } 52 | 53 | func (c *IdentifierCheck) visitCall(n *ast.Call) { 54 | // Look up the function in the map 55 | function, ok := c.Scope.LookupFunc(n.Func) 56 | if !ok { 57 | c.createErr(n, fmt.Sprintf("unknown function called: %s", n.Func)) 58 | return 59 | } 60 | 61 | // Break up the args into what is variadic and what is required 62 | args := n.Args 63 | if function.Variadic && len(args) > len(function.ArgTypes) { 64 | args = n.Args[:len(function.ArgTypes)] 65 | } 66 | 67 | // Verify the number of arguments 68 | if len(args) != len(function.ArgTypes) { 69 | c.createErr(n, fmt.Sprintf( 70 | "%s: expected %d arguments, got %d", 71 | n.Func, len(function.ArgTypes), len(n.Args))) 72 | return 73 | } 74 | } 75 | 76 | func (c *IdentifierCheck) visitVariableAccess(n *ast.VariableAccess) { 77 | // Look up the variable in the map 78 | if _, ok := c.Scope.LookupVar(n.Name); !ok { 79 | c.createErr(n, fmt.Sprintf( 80 | "unknown variable accessed: %s", n.Name)) 81 | return 82 | } 83 | } 84 | 85 | func (c *IdentifierCheck) createErr(n ast.Node, str string) { 86 | c.err = fmt.Errorf("%s: %s", n.Pos(), str) 87 | } 88 | 89 | func (c *IdentifierCheck) reset() { 90 | c.err = nil 91 | } 92 | -------------------------------------------------------------------------------- /ast/literal.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | ) 10 | 11 | // LiteralNode represents a single literal value, such as "foo" or 12 | // 42 or 3.14159. Based on the Type, the Value can be safely cast. 13 | type LiteralNode struct { 14 | Value interface{} 15 | Typex Type 16 | Posx Pos 17 | } 18 | 19 | // NewLiteralNode returns a new literal node representing the given 20 | // literal Go value, which must correspond to one of the primitive types 21 | // supported by HIL. Lists and maps cannot currently be constructed via 22 | // this function. 23 | // 24 | // If an inappropriately-typed value is provided, this function will 25 | // return an error. The main intended use of this function is to produce 26 | // "synthetic" literals from constants in code, where the value type is 27 | // well known at compile time. To easily store these in global variables, 28 | // see also MustNewLiteralNode. 29 | func NewLiteralNode(value interface{}, pos Pos) (*LiteralNode, error) { 30 | goType := reflect.TypeOf(value) 31 | var hilType Type 32 | 33 | switch goType.Kind() { 34 | case reflect.Bool: 35 | hilType = TypeBool 36 | case reflect.Int: 37 | hilType = TypeInt 38 | case reflect.Float64: 39 | hilType = TypeFloat 40 | case reflect.String: 41 | hilType = TypeString 42 | default: 43 | return nil, fmt.Errorf("unsupported literal node type: %T", value) 44 | } 45 | 46 | return &LiteralNode{ 47 | Value: value, 48 | Typex: hilType, 49 | Posx: pos, 50 | }, nil 51 | } 52 | 53 | // MustNewLiteralNode wraps NewLiteralNode and panics if an error is 54 | // returned, thus allowing valid literal nodes to be easily assigned to 55 | // global variables. 56 | func MustNewLiteralNode(value interface{}, pos Pos) *LiteralNode { 57 | node, err := NewLiteralNode(value, pos) 58 | if err != nil { 59 | panic(err) 60 | } 61 | return node 62 | } 63 | 64 | func (n *LiteralNode) Accept(v Visitor) Node { 65 | return v(n) 66 | } 67 | 68 | func (n *LiteralNode) Pos() Pos { 69 | return n.Posx 70 | } 71 | 72 | func (n *LiteralNode) GoString() string { 73 | return fmt.Sprintf("*%#v", *n) 74 | } 75 | 76 | func (n *LiteralNode) String() string { 77 | return fmt.Sprintf("Literal(%s, %v)", n.Typex, n.Value) 78 | } 79 | 80 | func (n *LiteralNode) Type(Scope) (Type, error) { 81 | return n.Typex, nil 82 | } 83 | 84 | // IsUnknown returns true either if the node's value is itself unknown 85 | // of if it is a collection containing any unknown elements, deeply. 86 | func (n *LiteralNode) IsUnknown() bool { 87 | return IsUnknown(Variable{ 88 | Type: n.Typex, 89 | Value: n.Value, 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /scanner/token.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package scanner 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/hashicorp/hil/ast" 10 | ) 11 | 12 | type Token struct { 13 | Type TokenType 14 | Content string 15 | Pos ast.Pos 16 | } 17 | 18 | //go:generate stringer -type=TokenType 19 | type TokenType rune 20 | 21 | const ( 22 | // Raw string data outside of ${ .. } sequences 23 | LITERAL TokenType = 'o' 24 | 25 | // STRING is like a LITERAL but it's inside a quoted string 26 | // within a ${ ... } sequence, and so it can contain backslash 27 | // escaping. 28 | STRING TokenType = 'S' 29 | 30 | // Other Literals 31 | INTEGER TokenType = 'I' 32 | FLOAT TokenType = 'F' 33 | BOOL TokenType = 'B' 34 | 35 | BEGIN TokenType = '$' // actually "${" 36 | END TokenType = '}' 37 | OQUOTE TokenType = '“' // Opening quote of a nested quoted sequence 38 | CQUOTE TokenType = '”' // Closing quote of a nested quoted sequence 39 | OPAREN TokenType = '(' 40 | CPAREN TokenType = ')' 41 | OBRACKET TokenType = '[' 42 | CBRACKET TokenType = ']' 43 | COMMA TokenType = ',' 44 | 45 | IDENTIFIER TokenType = 'i' 46 | 47 | PERIOD TokenType = '.' 48 | PLUS TokenType = '+' 49 | MINUS TokenType = '-' 50 | STAR TokenType = '*' 51 | SLASH TokenType = '/' 52 | PERCENT TokenType = '%' 53 | 54 | AND TokenType = '∧' 55 | OR TokenType = '∨' 56 | BANG TokenType = '!' 57 | 58 | EQUAL TokenType = '=' 59 | NOTEQUAL TokenType = '≠' 60 | GT TokenType = '>' 61 | LT TokenType = '<' 62 | GTE TokenType = '≥' 63 | LTE TokenType = '≤' 64 | 65 | QUESTION TokenType = '?' 66 | COLON TokenType = ':' 67 | 68 | EOF TokenType = '␄' 69 | 70 | // Produced for sequences that cannot be understood as valid tokens 71 | // e.g. due to use of unrecognized punctuation. 72 | INVALID TokenType = '�' 73 | ) 74 | 75 | func (t *Token) String() string { 76 | switch t.Type { 77 | case EOF: 78 | return "end of string" 79 | case INVALID: 80 | return fmt.Sprintf("invalid sequence %q", t.Content) 81 | case INTEGER: 82 | return fmt.Sprintf("integer %s", t.Content) 83 | case FLOAT: 84 | return fmt.Sprintf("float %s", t.Content) 85 | case STRING: 86 | return fmt.Sprintf("string %q", t.Content) 87 | case LITERAL: 88 | return fmt.Sprintf("literal %q", t.Content) 89 | case OQUOTE: 90 | return "opening quote" 91 | case CQUOTE: 92 | return "closing quote" 93 | case AND: 94 | return "&&" 95 | case OR: 96 | return "||" 97 | case NOTEQUAL: 98 | return "!=" 99 | case GTE: 100 | return ">=" 101 | case LTE: 102 | return "<=" 103 | default: 104 | // The remaining token types have content that 105 | // speaks for itself. 106 | return fmt.Sprintf("%q", t.Content) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ast/scope.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | ) 10 | 11 | // Scope is the interface used to look up variables and functions while 12 | // evaluating. How these functions/variables are defined are up to the caller. 13 | type Scope interface { 14 | LookupFunc(string) (Function, bool) 15 | LookupVar(string) (Variable, bool) 16 | } 17 | 18 | // Variable is a variable value for execution given as input to the engine. 19 | // It records the value of a variables along with their type. 20 | type Variable struct { 21 | Value interface{} 22 | Type Type 23 | } 24 | 25 | // NewVariable creates a new Variable for the given value. This will 26 | // attempt to infer the correct type. If it can't, an error will be returned. 27 | func NewVariable(v interface{}) (result Variable, err error) { 28 | switch v := reflect.ValueOf(v); v.Kind() { 29 | case reflect.String: 30 | result.Type = TypeString 31 | default: 32 | err = fmt.Errorf("unknown type: %s", v.Kind()) 33 | } 34 | 35 | result.Value = v 36 | return 37 | } 38 | 39 | // String implements Stringer on Variable, displaying the type and value 40 | // of the Variable. 41 | func (v Variable) String() string { 42 | return fmt.Sprintf("{Variable (%s): %+v}", v.Type, v.Value) 43 | } 44 | 45 | // Function defines a function that can be executed by the engine. 46 | // The type checker will validate that the proper types will be called 47 | // to the callback. 48 | type Function struct { 49 | // ArgTypes is the list of types in argument order. These are the 50 | // required arguments. 51 | // 52 | // ReturnType is the type of the returned value. The Callback MUST 53 | // return this type. 54 | ArgTypes []Type 55 | ReturnType Type 56 | 57 | // Variadic, if true, says that this function is variadic, meaning 58 | // it takes a variable number of arguments. In this case, the 59 | // VariadicType must be set. 60 | Variadic bool 61 | VariadicType Type 62 | 63 | // Callback is the function called for a function. The argument 64 | // types are guaranteed to match the spec above by the type checker. 65 | // The length of the args is strictly == len(ArgTypes) unless Varidiac 66 | // is true, in which case its >= len(ArgTypes). 67 | Callback func([]interface{}) (interface{}, error) 68 | } 69 | 70 | // BasicScope is a simple scope that looks up variables and functions 71 | // using a map. 72 | type BasicScope struct { 73 | FuncMap map[string]Function 74 | VarMap map[string]Variable 75 | } 76 | 77 | func (s *BasicScope) LookupFunc(n string) (Function, bool) { 78 | if s == nil { 79 | return Function{}, false 80 | } 81 | 82 | v, ok := s.FuncMap[n] 83 | return v, ok 84 | } 85 | 86 | func (s *BasicScope) LookupVar(n string) (Variable, bool) { 87 | if s == nil { 88 | return Variable{}, false 89 | } 90 | 91 | v, ok := s.VarMap[n] 92 | return v, ok 93 | } 94 | -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // Node is the interface that all AST nodes must implement. 11 | type Node interface { 12 | // Accept is called to dispatch to the visitors. It must return the 13 | // resulting Node (which might be different in an AST transform). 14 | Accept(Visitor) Node 15 | 16 | // Pos returns the position of this node in some source. 17 | Pos() Pos 18 | 19 | // Type returns the type of this node for the given context. 20 | Type(Scope) (Type, error) 21 | } 22 | 23 | // Pos is the starting position of an AST node 24 | type Pos struct { 25 | Column, Line int // Column/Line number, starting at 1 26 | Filename string // Optional source filename, if known 27 | } 28 | 29 | func (p Pos) String() string { 30 | if p.Filename == "" { 31 | return fmt.Sprintf("%d:%d", p.Line, p.Column) 32 | } else { 33 | return fmt.Sprintf("%s:%d:%d", p.Filename, p.Line, p.Column) 34 | } 35 | } 36 | 37 | // InitPos is an initiaial position value. This should be used as 38 | // the starting position (presets the column and line to 1). 39 | var InitPos = Pos{Column: 1, Line: 1} 40 | 41 | // Visitors are just implementations of this function. 42 | // 43 | // The function must return the Node to replace this node with. "nil" is 44 | // _not_ a valid return value. If there is no replacement, the original node 45 | // should be returned. We build this replacement directly into the visitor 46 | // pattern since AST transformations are a common and useful tool and 47 | // building it into the AST itself makes it required for future Node 48 | // implementations and very easy to do. 49 | // 50 | // Note that this isn't a true implementation of the visitor pattern, which 51 | // generally requires proper type dispatch on the function. However, 52 | // implementing this basic visitor pattern style is still very useful even 53 | // if you have to type switch. 54 | type Visitor func(Node) Node 55 | 56 | //go:generate stringer -type=Type 57 | 58 | // Type is the type of any value. 59 | type Type uint32 60 | 61 | const ( 62 | TypeInvalid Type = 0 63 | TypeAny Type = 1 << iota 64 | TypeBool 65 | TypeString 66 | TypeInt 67 | TypeFloat 68 | TypeList 69 | TypeMap 70 | 71 | // This is a special type used by Terraform to mark "unknown" values. 72 | // It is impossible for this type to be introduced into your HIL programs 73 | // unless you explicitly set a variable to this value. In that case, 74 | // any operation including the variable will return "TypeUnknown" as the 75 | // type. 76 | TypeUnknown 77 | ) 78 | 79 | func (t Type) Printable() string { 80 | switch t { 81 | case TypeInvalid: 82 | return "invalid type" 83 | case TypeAny: 84 | return "any type" 85 | case TypeBool: 86 | return "type bool" 87 | case TypeString: 88 | return "type string" 89 | case TypeInt: 90 | return "type int" 91 | case TypeFloat: 92 | return "type float" 93 | case TypeList: 94 | return "type list" 95 | case TypeMap: 96 | return "type map" 97 | case TypeUnknown: 98 | return "type unknown" 99 | default: 100 | return "unknown type" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /check_identifier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/hil/ast" 10 | ) 11 | 12 | func TestIdentifierCheck(t *testing.T) { 13 | cases := []struct { 14 | Input string 15 | Scope ast.Scope 16 | Error bool 17 | }{ 18 | { 19 | "foo", 20 | &ast.BasicScope{}, 21 | false, 22 | }, 23 | 24 | { 25 | "foo ${bar} success", 26 | &ast.BasicScope{ 27 | VarMap: map[string]ast.Variable{ 28 | "bar": ast.Variable{ 29 | Value: "baz", 30 | Type: ast.TypeString, 31 | }, 32 | }, 33 | }, 34 | false, 35 | }, 36 | 37 | { 38 | "foo ${bar}", 39 | &ast.BasicScope{}, 40 | true, 41 | }, 42 | 43 | { 44 | "foo ${rand()} success", 45 | &ast.BasicScope{ 46 | FuncMap: map[string]ast.Function{ 47 | "rand": ast.Function{ 48 | ReturnType: ast.TypeString, 49 | Callback: func([]interface{}) (interface{}, error) { 50 | return "42", nil 51 | }, 52 | }, 53 | }, 54 | }, 55 | false, 56 | }, 57 | 58 | { 59 | "foo ${rand()}", 60 | &ast.BasicScope{}, 61 | true, 62 | }, 63 | 64 | { 65 | "foo ${rand(42)} ", 66 | &ast.BasicScope{ 67 | FuncMap: map[string]ast.Function{ 68 | "rand": ast.Function{ 69 | ReturnType: ast.TypeString, 70 | Callback: func([]interface{}) (interface{}, error) { 71 | return "42", nil 72 | }, 73 | }, 74 | }, 75 | }, 76 | true, 77 | }, 78 | 79 | { 80 | "foo ${rand()} ", 81 | &ast.BasicScope{ 82 | FuncMap: map[string]ast.Function{ 83 | "rand": ast.Function{ 84 | ReturnType: ast.TypeString, 85 | Variadic: true, 86 | VariadicType: ast.TypeInt, 87 | Callback: func([]interface{}) (interface{}, error) { 88 | return "42", nil 89 | }, 90 | }, 91 | }, 92 | }, 93 | false, 94 | }, 95 | 96 | { 97 | "foo ${rand(42)} ", 98 | &ast.BasicScope{ 99 | FuncMap: map[string]ast.Function{ 100 | "rand": ast.Function{ 101 | ReturnType: ast.TypeString, 102 | Variadic: true, 103 | VariadicType: ast.TypeInt, 104 | Callback: func([]interface{}) (interface{}, error) { 105 | return "42", nil 106 | }, 107 | }, 108 | }, 109 | }, 110 | false, 111 | }, 112 | 113 | { 114 | "foo ${rand(\"foo\", 42)} ", 115 | &ast.BasicScope{ 116 | FuncMap: map[string]ast.Function{ 117 | "rand": ast.Function{ 118 | ArgTypes: []ast.Type{ast.TypeString}, 119 | ReturnType: ast.TypeString, 120 | Variadic: true, 121 | VariadicType: ast.TypeInt, 122 | Callback: func([]interface{}) (interface{}, error) { 123 | return "42", nil 124 | }, 125 | }, 126 | }, 127 | }, 128 | false, 129 | }, 130 | } 131 | 132 | for _, tc := range cases { 133 | node, err := Parse(tc.Input) 134 | if err != nil { 135 | t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) 136 | } 137 | 138 | visitor := &IdentifierCheck{Scope: tc.Scope} 139 | err = visitor.Visit(node) 140 | if err != nil != tc.Error { 141 | t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /.github/workflows/hil.yml: -------------------------------------------------------------------------------- 1 | name: hil 2 | on: 3 | - push 4 | - pull_request 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 15 | 16 | - name: Setup Go 17 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 18 | with: 19 | go-version: '1.23' 20 | 21 | - name: Run golangci-lint 22 | uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0 23 | 24 | linux-tests: 25 | runs-on: ubuntu-latest 26 | env: 27 | TEST_RESULTS_PATH: "/tmp/test-results" 28 | strategy: 29 | matrix: 30 | go-version: 31 | - 'oldstable' 32 | - 'stable' 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 36 | 37 | - name: Make Test Directory 38 | run: mkdir -p "$TEST_RESULTS_PATH"/hil 39 | 40 | - name: Setup Go 41 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 42 | with: 43 | go-version: ${{ matrix.go-version }} 44 | 45 | - name: Install gotestsum 46 | uses: autero1/action-gotestsum@7263b9d73912eec65f46337689e59fac865c425f # v2.0.0 47 | with: 48 | gotestsum_version: 1.9.0 49 | 50 | - name: Run gotestsum 51 | env: 52 | PLATFORM: linux 53 | REPORT_FILE: ${{ env.TEST_RESULTS_PATH }}/hil/gotestsum-report.xml 54 | run: |- 55 | gotestsum --format=short-verbose --junitfile ${{ env.REPORT_FILE }} -- -p 2 -cover -coverprofile=coverage-linux.out ./... 56 | 57 | - name: Upload Test Results 58 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 59 | with: 60 | path: ${{ env.TEST_RESULTS_PATH }} 61 | name: tests-linux-${{matrix.go-version}} 62 | 63 | - name: Upload coverage report 64 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 65 | with: 66 | path: coverage-linux.out 67 | name: Coverage-report-linux-${{matrix.go-version}} 68 | 69 | - name: Display coverage report 70 | run: go tool cover -func=coverage-linux.out 71 | 72 | windows-tests: 73 | runs-on: windows-latest 74 | env: 75 | TEST_RESULTS_PATH: 'c:\Users\runneradmin\AppData\Local\Temp\test-results' 76 | strategy: 77 | matrix: 78 | go-version: 79 | - 'oldstable' 80 | - 'stable' 81 | steps: 82 | - run: git config --global core.autocrlf false 83 | 84 | - name: Checkout code 85 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 86 | 87 | - name: Setup Go 88 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 89 | with: 90 | go-version: ${{ matrix.go-version }} 91 | 92 | - name: Install gotestsum 93 | uses: autero1/action-gotestsum@7263b9d73912eec65f46337689e59fac865c425f # v2.0.0 94 | with: 95 | gotestsum_version: 1.9.0 96 | 97 | - name: Run gotestsum 98 | env: 99 | PLATFORM: windows 100 | REPORT_FILE: ${{ env.TEST_RESULTS_PATH }}/hil/gotestsum-report.xml 101 | run: |- 102 | gotestsum.exe --format=short-verbose --junitfile ${{ env.REPORT_FILE }} -- -p 2 -cover -coverprofile="coverage-win.out" ./... 103 | 104 | - name: Upload Test Results 105 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 106 | with: 107 | path: ${{ env.TEST_RESULTS_PATH }} 108 | name: tests-windows-${{matrix.go-version}} 109 | 110 | - name: Upload coverage report 111 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 112 | with: 113 | path: coverage-win.out 114 | name: Coverage-report-windows-${{matrix.go-version}} 115 | 116 | - name: Display coverage report 117 | run: go tool cover -func=coverage-win.out 118 | shell: cmd 119 | -------------------------------------------------------------------------------- /walk_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestInterpolationWalker_detect(t *testing.T) { 13 | cases := []struct { 14 | Input interface{} 15 | Result []string 16 | }{ 17 | { 18 | Input: map[string]interface{}{ 19 | "foo": "$${var.foo}", 20 | }, 21 | Result: []string{ 22 | "Literal(TypeString, ${var.foo})", 23 | }, 24 | }, 25 | 26 | { 27 | Input: map[string]interface{}{ 28 | "foo": "${var.foo}", 29 | }, 30 | Result: []string{ 31 | "Variable(var.foo)", 32 | }, 33 | }, 34 | 35 | { 36 | Input: map[string]interface{}{ 37 | "foo": "${aws_instance.foo.*.num}", 38 | }, 39 | Result: []string{ 40 | "Variable(aws_instance.foo.*.num)", 41 | }, 42 | }, 43 | 44 | { 45 | Input: map[string]interface{}{ 46 | "foo": "${lookup(var.foo)}", 47 | }, 48 | Result: []string{ 49 | "Call(lookup, Variable(var.foo))", 50 | }, 51 | }, 52 | 53 | { 54 | Input: map[string]interface{}{ 55 | "foo": `${file("test.txt")}`, 56 | }, 57 | Result: []string{ 58 | "Call(file, Literal(TypeString, test.txt))", 59 | }, 60 | }, 61 | 62 | { 63 | Input: map[string]interface{}{ 64 | "foo": `${file("foo/bar.txt")}`, 65 | }, 66 | Result: []string{ 67 | "Call(file, Literal(TypeString, foo/bar.txt))", 68 | }, 69 | }, 70 | 71 | { 72 | Input: map[string]interface{}{ 73 | "foo": `${join(",", foo.bar.*.id)}`, 74 | }, 75 | Result: []string{ 76 | "Call(join, Literal(TypeString, ,), Variable(foo.bar.*.id))", 77 | }, 78 | }, 79 | 80 | { 81 | Input: map[string]interface{}{ 82 | "foo": `${concat("localhost", ":8080")}`, 83 | }, 84 | Result: []string{ 85 | "Call(concat, Literal(TypeString, localhost), Literal(TypeString, :8080))", 86 | }, 87 | }, 88 | } 89 | 90 | for i, tc := range cases { 91 | t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { 92 | var actual []string 93 | detectFn := func(data *WalkData) error { 94 | actual = append(actual, fmt.Sprintf("%s", data.Root)) 95 | return nil 96 | } 97 | 98 | if err := Walk(tc.Input, detectFn); err != nil { 99 | t.Fatalf("err: %s", err) 100 | } 101 | 102 | if !reflect.DeepEqual(actual, tc.Result) { 103 | t.Fatalf("%d: bad:\n\n%#v", i, actual) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func TestInterpolationWalker_replace(t *testing.T) { 110 | cases := []struct { 111 | Input interface{} 112 | Output interface{} 113 | Value string 114 | }{ 115 | { 116 | Input: map[string]interface{}{ 117 | "foo": "$${var.foo}", 118 | }, 119 | Output: map[string]interface{}{ 120 | "foo": "bar", 121 | }, 122 | Value: "bar", 123 | }, 124 | 125 | { 126 | Input: map[string]interface{}{ 127 | "foo": "hi, ${var.foo}", 128 | }, 129 | Output: map[string]interface{}{ 130 | "foo": "bar", 131 | }, 132 | Value: "bar", 133 | }, 134 | 135 | { 136 | Input: map[string]interface{}{ 137 | "foo": map[string]interface{}{ 138 | "${var.foo}": "bar", 139 | }, 140 | }, 141 | Output: map[string]interface{}{ 142 | "foo": map[string]interface{}{ 143 | "bar": "bar", 144 | }, 145 | }, 146 | Value: "bar", 147 | }, 148 | 149 | /* 150 | { 151 | Input: map[string]interface{}{ 152 | "foo": []interface{}{ 153 | "${var.foo}", 154 | "bing", 155 | }, 156 | }, 157 | Output: map[string]interface{}{ 158 | "foo": []interface{}{ 159 | "bar", 160 | "baz", 161 | "bing", 162 | }, 163 | }, 164 | Value: NewStringList([]string{"bar", "baz"}).String(), 165 | }, 166 | 167 | { 168 | Input: map[string]interface{}{ 169 | "foo": []interface{}{ 170 | "${var.foo}", 171 | "bing", 172 | }, 173 | }, 174 | Output: map[string]interface{}{}, 175 | Value: NewStringList([]string{UnknownVariableValue, "baz"}).String(), 176 | }, 177 | */ 178 | } 179 | 180 | for i, tc := range cases { 181 | fn := func(data *WalkData) error { 182 | data.Replace = true 183 | data.ReplaceValue = tc.Value 184 | return nil 185 | } 186 | 187 | if err := Walk(tc.Input, fn); err != nil { 188 | t.Fatalf("err: %s", err) 189 | } 190 | 191 | if !reflect.DeepEqual(tc.Input, tc.Output) { 192 | t.Fatalf("%d: bad:\n\nexpected:%#v\ngot:%#v", i, tc.Output, tc.Input) 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HIL 2 | 3 | [![GoDoc](https://godoc.org/github.com/hashicorp/hil?status.png)](https://godoc.org/github.com/hashicorp/hil) [![Build Status](https://circleci.com/gh/hashicorp/hil/tree/master.svg?style=svg)](https://circleci.com/gh/hashicorp/hil/tree/master) 4 | 5 | HIL (HashiCorp Interpolation Language) is a lightweight embedded language used 6 | primarily for configuration interpolation. The goal of HIL is to make a simple 7 | language for interpolations in the various configurations of HashiCorp tools. 8 | 9 | HIL is built to interpolate any string, but is in use by HashiCorp primarily 10 | with [HCL](https://github.com/hashicorp/hcl). HCL is _not required_ in any 11 | way for use with HIL. 12 | 13 | HIL isn't meant to be a general purpose language. It was built for basic 14 | configuration interpolations. Therefore, you can't currently write functions, 15 | have conditionals, set intermediary variables, etc. within HIL itself. It is 16 | possible some of these may be added later but the right use case must exist. 17 | 18 | ## Why? 19 | 20 | Many of our tools have support for something similar to templates, but 21 | within the configuration itself. The most prominent requirement was in 22 | [Terraform](https://github.com/hashicorp/terraform) where we wanted the 23 | configuration to be able to reference values from elsewhere in the 24 | configuration. Example: 25 | 26 | foo = "hi ${var.world}" 27 | 28 | We originally used a full templating language for this, but found it 29 | was too heavy weight. Additionally, many full languages required bindings 30 | to C (and thus the usage of cgo) which we try to avoid to make cross-compilation 31 | easier. We then moved to very basic regular expression based 32 | string replacement, but found the need for basic arithmetic and function 33 | calls resulting in overly complex regular expressions. 34 | 35 | Ultimately, we wrote our own mini-language within Terraform itself. As 36 | we built other projects such as [Nomad](https://nomadproject.io) and 37 | [Otto](https://ottoproject.io), the need for basic interpolations arose 38 | again. 39 | 40 | Thus HIL was born. It is extracted from Terraform, cleaned up, and 41 | better tested for general purpose use. 42 | 43 | ## Syntax 44 | 45 | For a complete grammar, please see the parser itself. A high-level overview 46 | of the syntax and grammar is listed here. 47 | 48 | Code begins within `${` and `}`. Outside of this, text is treated 49 | literally. For example, `foo` is a valid HIL program that is just the 50 | string "foo", but `foo ${bar}` is an HIL program that is the string "foo " 51 | concatenated with the value of `bar`. For the remainder of the syntax 52 | docs, we'll assume you're within `${}`. 53 | 54 | * Identifiers are any text in the format of `[a-zA-Z0-9-.]`. Example 55 | identifiers: `foo`, `var.foo`, `foo-bar`. 56 | 57 | * Strings are double quoted and can contain any UTF-8 characters. 58 | Example: `"Hello, World"` 59 | 60 | * Numbers are assumed to be base 10. If you prefix a number with 0x, 61 | it is treated as a hexadecimal. If it is prefixed with 0, it is 62 | treated as an octal. Numbers can be in scientific notation: "1e10". 63 | 64 | * Unary `-` can be used for negative numbers. Example: `-10` or `-0.2` 65 | 66 | * Boolean values: `true`, `false` 67 | 68 | * The following arithmetic operations are allowed: +, -, *, /, %. 69 | 70 | * Function calls are in the form of `name(arg1, arg2, ...)`. Example: 71 | `add(1, 5)`. Arguments can be any valid HIL expression, example: 72 | `add(1, var.foo)` or even nested function calls: 73 | `add(1, get("some value"))`. 74 | 75 | * Within strings, further interpolations can be opened with `${}`. 76 | Example: `"Hello ${nested}"`. A full example including the 77 | original `${}` (remember this list assumes were inside of one 78 | already) could be: `foo ${func("hello ${var.foo}")}`. 79 | 80 | ## Language Changes 81 | 82 | We've used this mini-language in Terraform for years. For backwards compatibility 83 | reasons, we're unlikely to make an incompatible change to the language but 84 | we're not currently making that promise, either. 85 | 86 | The internal API of this project may very well change as we evolve it 87 | to work with more of our projects. We recommend using some sort of dependency 88 | management solution with this package. 89 | 90 | ## Future Changes 91 | 92 | The following changes are already planned to be made at some point: 93 | 94 | * Richer types: lists, maps, etc. 95 | 96 | * Convert to a more standard Go parser structure similar to HCL. This 97 | will improve our error messaging as well as allow us to have automatic 98 | formatting. 99 | 100 | * Allow interpolations to result in more types than just a string. While 101 | within the interpolation basic types are honored, the result is always 102 | a string. 103 | -------------------------------------------------------------------------------- /ast/output_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestOutput_type(t *testing.T) { 11 | testCases := []struct { 12 | Name string 13 | Output *Output 14 | Scope Scope 15 | ReturnType Type 16 | ShouldError bool 17 | }{ 18 | { 19 | Name: "No expressions, for backward compatibility", 20 | Output: &Output{}, 21 | Scope: nil, 22 | ReturnType: TypeString, 23 | }, 24 | { 25 | Name: "Single string expression", 26 | Output: &Output{ 27 | Exprs: []Node{ 28 | &LiteralNode{ 29 | Value: "Whatever", 30 | Typex: TypeString, 31 | }, 32 | }, 33 | }, 34 | Scope: nil, 35 | ReturnType: TypeString, 36 | }, 37 | { 38 | Name: "Single list expression of strings", 39 | Output: &Output{ 40 | Exprs: []Node{ 41 | &VariableAccess{ 42 | Name: "testvar", 43 | }, 44 | }, 45 | }, 46 | Scope: &BasicScope{ 47 | VarMap: map[string]Variable{ 48 | "testvar": Variable{ 49 | Type: TypeList, 50 | Value: []Variable{ 51 | Variable{ 52 | Type: TypeString, 53 | Value: "Hello", 54 | }, 55 | Variable{ 56 | Type: TypeString, 57 | Value: "World", 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | ReturnType: TypeList, 64 | }, 65 | { 66 | Name: "Single map expression", 67 | Output: &Output{ 68 | Exprs: []Node{ 69 | &VariableAccess{ 70 | Name: "testvar", 71 | }, 72 | }, 73 | }, 74 | Scope: &BasicScope{ 75 | VarMap: map[string]Variable{ 76 | "testvar": Variable{ 77 | Type: TypeMap, 78 | Value: map[string]Variable{ 79 | "key1": Variable{ 80 | Type: TypeString, 81 | Value: "Hello", 82 | }, 83 | "key2": Variable{ 84 | Type: TypeString, 85 | Value: "World", 86 | }, 87 | }, 88 | }, 89 | }, 90 | }, 91 | ReturnType: TypeMap, 92 | }, 93 | { 94 | Name: "Multiple map expressions", 95 | Output: &Output{ 96 | Exprs: []Node{ 97 | &VariableAccess{ 98 | Name: "testvar", 99 | }, 100 | &VariableAccess{ 101 | Name: "testvar", 102 | }, 103 | }, 104 | }, 105 | Scope: &BasicScope{ 106 | VarMap: map[string]Variable{ 107 | "testvar": Variable{ 108 | Type: TypeMap, 109 | Value: map[string]Variable{ 110 | "key1": Variable{ 111 | Type: TypeString, 112 | Value: "Hello", 113 | }, 114 | "key2": Variable{ 115 | Type: TypeString, 116 | Value: "World", 117 | }, 118 | }, 119 | }, 120 | }, 121 | }, 122 | ShouldError: true, 123 | ReturnType: TypeInvalid, 124 | }, 125 | { 126 | Name: "Multiple list expressions", 127 | Output: &Output{ 128 | Exprs: []Node{ 129 | &VariableAccess{ 130 | Name: "testvar", 131 | }, 132 | &VariableAccess{ 133 | Name: "testvar", 134 | }, 135 | }, 136 | }, 137 | Scope: &BasicScope{ 138 | VarMap: map[string]Variable{ 139 | "testvar": Variable{ 140 | Type: TypeList, 141 | Value: []Variable{ 142 | Variable{ 143 | Type: TypeString, 144 | Value: "Hello", 145 | }, 146 | Variable{ 147 | Type: TypeString, 148 | Value: "World", 149 | }, 150 | }, 151 | }, 152 | }, 153 | }, 154 | ShouldError: true, 155 | ReturnType: TypeInvalid, 156 | }, 157 | { 158 | Name: "Multiple string expressions", 159 | Output: &Output{ 160 | Exprs: []Node{ 161 | &VariableAccess{ 162 | Name: "testvar", 163 | }, 164 | &VariableAccess{ 165 | Name: "testvar", 166 | }, 167 | }, 168 | }, 169 | Scope: &BasicScope{ 170 | VarMap: map[string]Variable{ 171 | "testvar": Variable{ 172 | Type: TypeString, 173 | Value: "Hello", 174 | }, 175 | }, 176 | }, 177 | ReturnType: TypeString, 178 | }, 179 | { 180 | Name: "Multiple string expressions with coercion", 181 | Output: &Output{ 182 | Exprs: []Node{ 183 | &VariableAccess{ 184 | Name: "testvar", 185 | }, 186 | &VariableAccess{ 187 | Name: "testint", 188 | }, 189 | }, 190 | }, 191 | Scope: &BasicScope{ 192 | VarMap: map[string]Variable{ 193 | "testvar": Variable{ 194 | Type: TypeString, 195 | Value: "Hello", 196 | }, 197 | "testint": Variable{ 198 | Type: TypeInt, 199 | Value: 2, 200 | }, 201 | }, 202 | }, 203 | ReturnType: TypeString, 204 | }, 205 | } 206 | 207 | for _, v := range testCases { 208 | actual, err := v.Output.Type(v.Scope) 209 | if err != nil && !v.ShouldError { 210 | t.Fatalf("case: %s\nerr: %s", v.Name, err) 211 | } 212 | if actual != v.ReturnType { 213 | t.Fatalf("case: %s\n bad: %s\nexpected: %s\n", v.Name, actual, v.ReturnType) 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /convert.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | "github.com/mitchellh/mapstructure" 12 | ) 13 | 14 | // UnknownValue is a sentinel value that can be used to denote 15 | // that a value of a variable (or map element, list element, etc.) 16 | // is unknown. This will always have the type ast.TypeUnknown. 17 | const UnknownValue = "74D93920-ED26-11E3-AC10-0800200C9A66" 18 | 19 | var hilMapstructureDecodeHookSlice []interface{} 20 | var hilMapstructureDecodeHookStringSlice []string 21 | var hilMapstructureDecodeHookMap map[string]interface{} 22 | 23 | // hilMapstructureWeakDecode behaves in the same way as mapstructure.WeakDecode 24 | // but has a DecodeHook which defeats the backward compatibility mode of mapstructure 25 | // which WeakDecodes []interface{}{} into an empty map[string]interface{}. This 26 | // allows us to use WeakDecode (desirable), but not fail on empty lists. 27 | func hilMapstructureWeakDecode(m interface{}, rawVal interface{}) error { 28 | config := &mapstructure.DecoderConfig{ 29 | DecodeHook: func(source reflect.Type, target reflect.Type, val interface{}) (interface{}, error) { 30 | sliceType := reflect.TypeOf(hilMapstructureDecodeHookSlice) 31 | stringSliceType := reflect.TypeOf(hilMapstructureDecodeHookStringSlice) 32 | mapType := reflect.TypeOf(hilMapstructureDecodeHookMap) 33 | 34 | if (source == sliceType || source == stringSliceType) && target == mapType { 35 | return nil, fmt.Errorf("cannot convert %s into a %s", source, target) 36 | } 37 | 38 | return val, nil 39 | }, 40 | WeaklyTypedInput: true, 41 | Result: rawVal, 42 | } 43 | 44 | decoder, err := mapstructure.NewDecoder(config) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return decoder.Decode(m) 50 | } 51 | 52 | func InterfaceToVariable(input interface{}) (ast.Variable, error) { 53 | if iv, ok := input.(ast.Variable); ok { 54 | return iv, nil 55 | } 56 | 57 | // This is just to maintain backward compatibility 58 | // after https://github.com/mitchellh/mapstructure/pull/98 59 | if v, ok := input.([]ast.Variable); ok { 60 | return ast.Variable{ 61 | Type: ast.TypeList, 62 | Value: v, 63 | }, nil 64 | } 65 | if v, ok := input.(map[string]ast.Variable); ok { 66 | return ast.Variable{ 67 | Type: ast.TypeMap, 68 | Value: v, 69 | }, nil 70 | } 71 | 72 | var stringVal string 73 | if err := hilMapstructureWeakDecode(input, &stringVal); err == nil { 74 | // Special case the unknown value to turn into "unknown" 75 | if stringVal == UnknownValue { 76 | return ast.Variable{Value: UnknownValue, Type: ast.TypeUnknown}, nil 77 | } 78 | 79 | // Otherwise return the string value 80 | return ast.Variable{ 81 | Type: ast.TypeString, 82 | Value: stringVal, 83 | }, nil 84 | } 85 | 86 | var mapVal map[string]interface{} 87 | if err := hilMapstructureWeakDecode(input, &mapVal); err == nil { 88 | elements := make(map[string]ast.Variable) 89 | for i, element := range mapVal { 90 | varElement, err := InterfaceToVariable(element) 91 | if err != nil { 92 | return ast.Variable{}, err 93 | } 94 | elements[i] = varElement 95 | } 96 | 97 | return ast.Variable{ 98 | Type: ast.TypeMap, 99 | Value: elements, 100 | }, nil 101 | } 102 | 103 | var sliceVal []interface{} 104 | if err := hilMapstructureWeakDecode(input, &sliceVal); err == nil { 105 | elements := make([]ast.Variable, len(sliceVal)) 106 | for i, element := range sliceVal { 107 | varElement, err := InterfaceToVariable(element) 108 | if err != nil { 109 | return ast.Variable{}, err 110 | } 111 | elements[i] = varElement 112 | } 113 | 114 | return ast.Variable{ 115 | Type: ast.TypeList, 116 | Value: elements, 117 | }, nil 118 | } 119 | 120 | return ast.Variable{}, fmt.Errorf("value for conversion must be a string, interface{} or map[string]interface: got %T", input) 121 | } 122 | 123 | func VariableToInterface(input ast.Variable) (interface{}, error) { 124 | if input.Type == ast.TypeString { 125 | if inputStr, ok := input.Value.(string); ok { 126 | return inputStr, nil 127 | } else { 128 | return nil, fmt.Errorf("ast.Variable with type string has value which is not a string") 129 | } 130 | } 131 | 132 | if input.Type == ast.TypeList { 133 | inputList, ok := input.Value.([]ast.Variable) 134 | if !ok { 135 | return nil, fmt.Errorf("ast.Variable with type list has value which is not a []ast.Variable") 136 | } 137 | 138 | result := make([]interface{}, 0) 139 | if len(inputList) == 0 { 140 | return result, nil 141 | } 142 | 143 | for _, element := range inputList { 144 | if convertedElement, err := VariableToInterface(element); err == nil { 145 | result = append(result, convertedElement) 146 | } else { 147 | return nil, err 148 | } 149 | } 150 | 151 | return result, nil 152 | } 153 | 154 | if input.Type == ast.TypeMap { 155 | inputMap, ok := input.Value.(map[string]ast.Variable) 156 | if !ok { 157 | return nil, fmt.Errorf("ast.Variable with type map has value which is not a map[string]ast.Variable") 158 | } 159 | 160 | result := make(map[string]interface{}, 0) 161 | if len(inputMap) == 0 { 162 | return result, nil 163 | } 164 | 165 | for key, value := range inputMap { 166 | if convertedValue, err := VariableToInterface(value); err == nil { 167 | result[key] = convertedValue 168 | } else { 169 | return nil, err 170 | } 171 | } 172 | 173 | return result, nil 174 | } 175 | 176 | return nil, fmt.Errorf("unknown input type: %s", input.Type) 177 | } 178 | -------------------------------------------------------------------------------- /ast/index_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package ast 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestIndexTypeMap_empty(t *testing.T) { 12 | i := &Index{ 13 | Target: &VariableAccess{Name: "foo"}, 14 | Key: &LiteralNode{ 15 | Typex: TypeString, 16 | Value: "bar", 17 | }, 18 | } 19 | 20 | scope := &BasicScope{ 21 | VarMap: map[string]Variable{ 22 | "foo": Variable{ 23 | Type: TypeMap, 24 | Value: map[string]Variable{}, 25 | }, 26 | }, 27 | } 28 | 29 | actual, err := i.Type(scope) 30 | if err == nil || !strings.Contains(err.Error(), "does not have any elements") { 31 | t.Fatalf("bad err: %s", err) 32 | } 33 | if actual != TypeInvalid { 34 | t.Fatalf("bad: %s", actual) 35 | } 36 | } 37 | 38 | func TestIndexTypeMap_string(t *testing.T) { 39 | i := &Index{ 40 | Target: &VariableAccess{Name: "foo"}, 41 | Key: &LiteralNode{ 42 | Typex: TypeString, 43 | Value: "bar", 44 | }, 45 | } 46 | 47 | scope := &BasicScope{ 48 | VarMap: map[string]Variable{ 49 | "foo": Variable{ 50 | Type: TypeMap, 51 | Value: map[string]Variable{ 52 | "baz": Variable{ 53 | Type: TypeString, 54 | Value: "Hello", 55 | }, 56 | "bar": Variable{ 57 | Type: TypeString, 58 | Value: "World", 59 | }, 60 | }, 61 | }, 62 | }, 63 | } 64 | 65 | actual, err := i.Type(scope) 66 | if err != nil { 67 | t.Fatalf("err: %s", err) 68 | } 69 | if actual != TypeString { 70 | t.Fatalf("bad: %s", actual) 71 | } 72 | } 73 | 74 | func TestIndexTypeMap_int(t *testing.T) { 75 | i := &Index{ 76 | Target: &VariableAccess{Name: "foo"}, 77 | Key: &LiteralNode{ 78 | Typex: TypeString, 79 | Value: "bar", 80 | }, 81 | } 82 | 83 | scope := &BasicScope{ 84 | VarMap: map[string]Variable{ 85 | "foo": Variable{ 86 | Type: TypeMap, 87 | Value: map[string]Variable{ 88 | "baz": Variable{ 89 | Type: TypeInt, 90 | Value: 1, 91 | }, 92 | "bar": Variable{ 93 | Type: TypeInt, 94 | Value: 2, 95 | }, 96 | }, 97 | }, 98 | }, 99 | } 100 | 101 | actual, err := i.Type(scope) 102 | if err != nil { 103 | t.Fatalf("err: %s", err) 104 | } 105 | if actual != TypeInt { 106 | t.Fatalf("bad: %s", actual) 107 | } 108 | } 109 | 110 | func TestIndexTypeMap_nonHomogenous(t *testing.T) { 111 | i := &Index{ 112 | Target: &VariableAccess{Name: "foo"}, 113 | Key: &LiteralNode{ 114 | Typex: TypeString, 115 | Value: "bar", 116 | }, 117 | } 118 | 119 | scope := &BasicScope{ 120 | VarMap: map[string]Variable{ 121 | "foo": Variable{ 122 | Type: TypeMap, 123 | Value: map[string]Variable{ 124 | "bar": Variable{ 125 | Type: TypeString, 126 | Value: "Hello", 127 | }, 128 | "baz": Variable{ 129 | Type: TypeInt, 130 | Value: 43, 131 | }, 132 | }, 133 | }, 134 | }, 135 | } 136 | 137 | _, err := i.Type(scope) 138 | if err == nil || !strings.Contains(err.Error(), "homogenous") { 139 | t.Fatalf("expected error") 140 | } 141 | } 142 | 143 | func TestIndexTypeList_empty(t *testing.T) { 144 | i := &Index{ 145 | Target: &VariableAccess{Name: "foo"}, 146 | Key: &LiteralNode{ 147 | Typex: TypeInt, 148 | Value: 1, 149 | }, 150 | } 151 | 152 | scope := &BasicScope{ 153 | VarMap: map[string]Variable{ 154 | "foo": Variable{ 155 | Type: TypeList, 156 | Value: []Variable{}, 157 | }, 158 | }, 159 | } 160 | 161 | actual, err := i.Type(scope) 162 | if err == nil || !strings.Contains(err.Error(), "does not have any elements") { 163 | t.Fatalf("bad err: %s", err) 164 | } 165 | if actual != TypeInvalid { 166 | t.Fatalf("bad: %s", actual) 167 | } 168 | } 169 | 170 | func TestIndexTypeList_string(t *testing.T) { 171 | i := &Index{ 172 | Target: &VariableAccess{Name: "foo"}, 173 | Key: &LiteralNode{ 174 | Typex: TypeInt, 175 | Value: 1, 176 | }, 177 | } 178 | 179 | scope := &BasicScope{ 180 | VarMap: map[string]Variable{ 181 | "foo": Variable{ 182 | Type: TypeList, 183 | Value: []Variable{ 184 | Variable{ 185 | Type: TypeString, 186 | Value: "Hello", 187 | }, 188 | Variable{ 189 | Type: TypeString, 190 | Value: "World", 191 | }, 192 | }, 193 | }, 194 | }, 195 | } 196 | 197 | actual, err := i.Type(scope) 198 | if err != nil { 199 | t.Fatalf("err: %s", err) 200 | } 201 | if actual != TypeString { 202 | t.Fatalf("bad: %s", actual) 203 | } 204 | } 205 | 206 | func TestIndexTypeList_int(t *testing.T) { 207 | i := &Index{ 208 | Target: &VariableAccess{Name: "foo"}, 209 | Key: &LiteralNode{ 210 | Typex: TypeInt, 211 | Value: 1, 212 | }, 213 | } 214 | 215 | scope := &BasicScope{ 216 | VarMap: map[string]Variable{ 217 | "foo": Variable{ 218 | Type: TypeList, 219 | Value: []Variable{ 220 | Variable{ 221 | Type: TypeInt, 222 | Value: 34, 223 | }, 224 | Variable{ 225 | Type: TypeInt, 226 | Value: 54, 227 | }, 228 | }, 229 | }, 230 | }, 231 | } 232 | 233 | actual, err := i.Type(scope) 234 | if err != nil { 235 | t.Fatalf("err: %s", err) 236 | } 237 | if actual != TypeInt { 238 | t.Fatalf("bad: %s", actual) 239 | } 240 | } 241 | 242 | func TestIndexTypeList_nonHomogenous(t *testing.T) { 243 | i := &Index{ 244 | Target: &VariableAccess{Name: "foo"}, 245 | Key: &LiteralNode{ 246 | Typex: TypeInt, 247 | Value: 1, 248 | }, 249 | } 250 | 251 | scope := &BasicScope{ 252 | VarMap: map[string]Variable{ 253 | "foo": Variable{ 254 | Type: TypeList, 255 | Value: []Variable{ 256 | Variable{ 257 | Type: TypeString, 258 | Value: "Hello", 259 | }, 260 | Variable{ 261 | Type: TypeInt, 262 | Value: 43, 263 | }, 264 | }, 265 | }, 266 | }, 267 | } 268 | 269 | _, err := i.Type(scope) 270 | if err == nil || !strings.Contains(err.Error(), "homogenous") { 271 | t.Fatalf("expected error") 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /walk.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "fmt" 8 | "reflect" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | "github.com/mitchellh/reflectwalk" 12 | ) 13 | 14 | // WalkFn is the type of function to pass to Walk. Modify fields within 15 | // WalkData to control whether replacement happens. 16 | type WalkFn func(*WalkData) error 17 | 18 | // WalkData is the structure passed to the callback of the Walk function. 19 | // 20 | // This structure contains data passed in as well as fields that are expected 21 | // to be written by the caller as a result. Please see the documentation for 22 | // each field for more information. 23 | type WalkData struct { 24 | // Root is the parsed root of this HIL program 25 | Root ast.Node 26 | 27 | // Location is the location within the structure where this 28 | // value was found. This can be used to modify behavior within 29 | // slices and so on. 30 | Location reflectwalk.Location 31 | 32 | // The below two values must be set by the callback to have any effect. 33 | // 34 | // Replace, if true, will replace the value in the structure with 35 | // ReplaceValue. It is up to the caller to make sure this is a string. 36 | Replace bool 37 | ReplaceValue string 38 | } 39 | 40 | // Walk will walk an arbitrary Go structure and parse any string as an 41 | // HIL program and call the callback cb to determine what to replace it 42 | // with. 43 | // 44 | // This function is very useful for arbitrary HIL program interpolation 45 | // across a complex configuration structure. Due to the heavy use of 46 | // reflection in this function, it is recommend to write many unit tests 47 | // with your typical configuration structures to hilp mitigate the risk 48 | // of panics. 49 | func Walk(v interface{}, cb WalkFn) error { 50 | walker := &interpolationWalker{F: cb} 51 | return reflectwalk.Walk(v, walker) 52 | } 53 | 54 | // interpolationWalker implements interfaces for the reflectwalk package 55 | // (github.com/mitchellh/reflectwalk) that can be used to automatically 56 | // execute a callback for an interpolation. 57 | type interpolationWalker struct { 58 | F WalkFn 59 | 60 | key []string 61 | lastValue reflect.Value 62 | loc reflectwalk.Location 63 | cs []reflect.Value 64 | csKey []reflect.Value 65 | csData interface{} 66 | sliceIndex int 67 | } 68 | 69 | func (w *interpolationWalker) Enter(loc reflectwalk.Location) error { 70 | w.loc = loc 71 | return nil 72 | } 73 | 74 | func (w *interpolationWalker) Exit(loc reflectwalk.Location) error { 75 | w.loc = reflectwalk.None 76 | 77 | switch loc { 78 | case reflectwalk.Map: 79 | w.cs = w.cs[:len(w.cs)-1] 80 | case reflectwalk.MapValue: 81 | w.key = w.key[:len(w.key)-1] 82 | w.csKey = w.csKey[:len(w.csKey)-1] 83 | case reflectwalk.Slice: 84 | // Split any values that need to be split 85 | w.splitSlice() 86 | w.cs = w.cs[:len(w.cs)-1] 87 | case reflectwalk.SliceElem: 88 | w.csKey = w.csKey[:len(w.csKey)-1] 89 | } 90 | 91 | return nil 92 | } 93 | 94 | func (w *interpolationWalker) Map(m reflect.Value) error { 95 | w.cs = append(w.cs, m) 96 | return nil 97 | } 98 | 99 | func (w *interpolationWalker) MapElem(m, k, v reflect.Value) error { 100 | w.csData = k 101 | w.csKey = append(w.csKey, k) 102 | w.key = append(w.key, k.String()) 103 | w.lastValue = v 104 | return nil 105 | } 106 | 107 | func (w *interpolationWalker) Slice(s reflect.Value) error { 108 | w.cs = append(w.cs, s) 109 | return nil 110 | } 111 | 112 | func (w *interpolationWalker) SliceElem(i int, elem reflect.Value) error { 113 | w.csKey = append(w.csKey, reflect.ValueOf(i)) 114 | w.sliceIndex = i 115 | return nil 116 | } 117 | 118 | func (w *interpolationWalker) Primitive(v reflect.Value) error { 119 | setV := v 120 | 121 | // We only care about strings 122 | if v.Kind() == reflect.Interface { 123 | setV = v 124 | v = v.Elem() 125 | } 126 | if v.Kind() != reflect.String { 127 | return nil 128 | } 129 | 130 | astRoot, err := Parse(v.String()) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | // If the AST we got is just a literal string value with the same 136 | // value then we ignore it. We have to check if its the same value 137 | // because it is possible to input a string, get out a string, and 138 | // have it be different. For example: "foo-$${bar}" turns into 139 | // "foo-${bar}" 140 | if n, ok := astRoot.(*ast.LiteralNode); ok { 141 | if s, ok := n.Value.(string); ok && s == v.String() { 142 | return nil 143 | } 144 | } 145 | 146 | if w.F == nil { 147 | return nil 148 | } 149 | 150 | data := WalkData{Root: astRoot, Location: w.loc} 151 | if err := w.F(&data); err != nil { 152 | return fmt.Errorf( 153 | "%s in:\n\n%s", 154 | err, v.String()) 155 | } 156 | 157 | if data.Replace { 158 | /* 159 | if remove { 160 | w.removeCurrent() 161 | return nil 162 | } 163 | */ 164 | 165 | resultVal := reflect.ValueOf(data.ReplaceValue) 166 | switch w.loc { 167 | case reflectwalk.MapKey: 168 | m := w.cs[len(w.cs)-1] 169 | 170 | // Delete the old value 171 | var zero reflect.Value 172 | m.SetMapIndex(w.csData.(reflect.Value), zero) 173 | 174 | // Set the new key with the existing value 175 | m.SetMapIndex(resultVal, w.lastValue) 176 | 177 | // Set the key to be the new key 178 | w.csData = resultVal 179 | case reflectwalk.MapValue: 180 | // If we're in a map, then the only way to set a map value is 181 | // to set it directly. 182 | m := w.cs[len(w.cs)-1] 183 | mk := w.csData.(reflect.Value) 184 | m.SetMapIndex(mk, resultVal) 185 | default: 186 | // Otherwise, we should be addressable 187 | setV.Set(resultVal) 188 | } 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func (w *interpolationWalker) replaceCurrent(v reflect.Value) { 195 | c := w.cs[len(w.cs)-2] 196 | switch c.Kind() { 197 | case reflect.Map: 198 | // Get the key and delete it 199 | k := w.csKey[len(w.csKey)-1] 200 | c.SetMapIndex(k, v) 201 | } 202 | } 203 | 204 | func (w *interpolationWalker) splitSlice() { 205 | // Get the []interface{} slice so we can do some operations on 206 | // it without dealing with reflection. We'll document each step 207 | // here to be clear. 208 | var s []interface{} 209 | raw := w.cs[len(w.cs)-1] 210 | switch v := raw.Interface().(type) { 211 | case []interface{}: 212 | s = v 213 | case []map[string]interface{}: 214 | return 215 | default: 216 | panic("Unknown kind: " + raw.Kind().String()) 217 | } 218 | 219 | // Check if we have any elements that we need to split. If not, then 220 | // just return since we're done. 221 | split := false 222 | if !split { 223 | return 224 | } 225 | 226 | // Make a new result slice that is twice the capacity to fit our growth. 227 | result := make([]interface{}, 0, len(s)*2) 228 | 229 | // Go over each element of the original slice and start building up 230 | // the resulting slice by splitting where we have to. 231 | for _, v := range s { 232 | sv, ok := v.(string) 233 | if !ok { 234 | // Not a string, so just set it 235 | result = append(result, v) 236 | continue 237 | } 238 | 239 | // Not a string list, so just set it 240 | result = append(result, sv) 241 | } 242 | 243 | // Our slice is now done, we have to replace the slice now 244 | // with this new one that we have. 245 | w.replaceCurrent(reflect.ValueOf(result)) 246 | } 247 | -------------------------------------------------------------------------------- /builtins.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "errors" 8 | "strconv" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | ) 12 | 13 | // NOTE: All builtins are tested in engine_test.go 14 | 15 | func registerBuiltins(scope *ast.BasicScope) *ast.BasicScope { 16 | if scope == nil { 17 | scope = new(ast.BasicScope) 18 | } 19 | if scope.FuncMap == nil { 20 | scope.FuncMap = make(map[string]ast.Function) 21 | } 22 | 23 | // Implicit conversions 24 | scope.FuncMap["__builtin_BoolToString"] = builtinBoolToString() 25 | scope.FuncMap["__builtin_FloatToInt"] = builtinFloatToInt() 26 | scope.FuncMap["__builtin_FloatToString"] = builtinFloatToString() 27 | scope.FuncMap["__builtin_IntToFloat"] = builtinIntToFloat() 28 | scope.FuncMap["__builtin_IntToString"] = builtinIntToString() 29 | scope.FuncMap["__builtin_StringToInt"] = builtinStringToInt() 30 | scope.FuncMap["__builtin_StringToFloat"] = builtinStringToFloat() 31 | scope.FuncMap["__builtin_StringToBool"] = builtinStringToBool() 32 | 33 | // Math operations 34 | scope.FuncMap["__builtin_IntMath"] = builtinIntMath() 35 | scope.FuncMap["__builtin_FloatMath"] = builtinFloatMath() 36 | scope.FuncMap["__builtin_BoolCompare"] = builtinBoolCompare() 37 | scope.FuncMap["__builtin_FloatCompare"] = builtinFloatCompare() 38 | scope.FuncMap["__builtin_IntCompare"] = builtinIntCompare() 39 | scope.FuncMap["__builtin_StringCompare"] = builtinStringCompare() 40 | scope.FuncMap["__builtin_Logical"] = builtinLogical() 41 | return scope 42 | } 43 | 44 | func builtinFloatMath() ast.Function { 45 | return ast.Function{ 46 | ArgTypes: []ast.Type{ast.TypeInt}, 47 | Variadic: true, 48 | VariadicType: ast.TypeFloat, 49 | ReturnType: ast.TypeFloat, 50 | Callback: func(args []interface{}) (interface{}, error) { 51 | op := args[0].(ast.ArithmeticOp) 52 | result := args[1].(float64) 53 | for _, raw := range args[2:] { 54 | arg := raw.(float64) 55 | switch op { 56 | case ast.ArithmeticOpAdd: 57 | result += arg 58 | case ast.ArithmeticOpSub: 59 | result -= arg 60 | case ast.ArithmeticOpMul: 61 | result *= arg 62 | case ast.ArithmeticOpDiv: 63 | result /= arg 64 | } 65 | } 66 | 67 | return result, nil 68 | }, 69 | } 70 | } 71 | 72 | func builtinIntMath() ast.Function { 73 | return ast.Function{ 74 | ArgTypes: []ast.Type{ast.TypeInt}, 75 | Variadic: true, 76 | VariadicType: ast.TypeInt, 77 | ReturnType: ast.TypeInt, 78 | Callback: func(args []interface{}) (interface{}, error) { 79 | op := args[0].(ast.ArithmeticOp) 80 | result := args[1].(int) 81 | for _, raw := range args[2:] { 82 | arg := raw.(int) 83 | switch op { 84 | case ast.ArithmeticOpAdd: 85 | result += arg 86 | case ast.ArithmeticOpSub: 87 | result -= arg 88 | case ast.ArithmeticOpMul: 89 | result *= arg 90 | case ast.ArithmeticOpDiv: 91 | if arg == 0 { 92 | return nil, errors.New("divide by zero") 93 | } 94 | 95 | result /= arg 96 | case ast.ArithmeticOpMod: 97 | if arg == 0 { 98 | return nil, errors.New("divide by zero") 99 | } 100 | 101 | result = result % arg 102 | } 103 | } 104 | 105 | return result, nil 106 | }, 107 | } 108 | } 109 | 110 | func builtinBoolCompare() ast.Function { 111 | return ast.Function{ 112 | ArgTypes: []ast.Type{ast.TypeInt, ast.TypeBool, ast.TypeBool}, 113 | Variadic: false, 114 | ReturnType: ast.TypeBool, 115 | Callback: func(args []interface{}) (interface{}, error) { 116 | op := args[0].(ast.ArithmeticOp) 117 | lhs := args[1].(bool) 118 | rhs := args[2].(bool) 119 | 120 | switch op { 121 | case ast.ArithmeticOpEqual: 122 | return lhs == rhs, nil 123 | case ast.ArithmeticOpNotEqual: 124 | return lhs != rhs, nil 125 | default: 126 | return nil, errors.New("invalid comparison operation") 127 | } 128 | }, 129 | } 130 | } 131 | 132 | func builtinFloatCompare() ast.Function { 133 | return ast.Function{ 134 | ArgTypes: []ast.Type{ast.TypeInt, ast.TypeFloat, ast.TypeFloat}, 135 | Variadic: false, 136 | ReturnType: ast.TypeBool, 137 | Callback: func(args []interface{}) (interface{}, error) { 138 | op := args[0].(ast.ArithmeticOp) 139 | lhs := args[1].(float64) 140 | rhs := args[2].(float64) 141 | 142 | switch op { 143 | case ast.ArithmeticOpEqual: 144 | return lhs == rhs, nil 145 | case ast.ArithmeticOpNotEqual: 146 | return lhs != rhs, nil 147 | case ast.ArithmeticOpLessThan: 148 | return lhs < rhs, nil 149 | case ast.ArithmeticOpLessThanOrEqual: 150 | return lhs <= rhs, nil 151 | case ast.ArithmeticOpGreaterThan: 152 | return lhs > rhs, nil 153 | case ast.ArithmeticOpGreaterThanOrEqual: 154 | return lhs >= rhs, nil 155 | default: 156 | return nil, errors.New("invalid comparison operation") 157 | } 158 | }, 159 | } 160 | } 161 | 162 | func builtinIntCompare() ast.Function { 163 | return ast.Function{ 164 | ArgTypes: []ast.Type{ast.TypeInt, ast.TypeInt, ast.TypeInt}, 165 | Variadic: false, 166 | ReturnType: ast.TypeBool, 167 | Callback: func(args []interface{}) (interface{}, error) { 168 | op := args[0].(ast.ArithmeticOp) 169 | lhs := args[1].(int) 170 | rhs := args[2].(int) 171 | 172 | switch op { 173 | case ast.ArithmeticOpEqual: 174 | return lhs == rhs, nil 175 | case ast.ArithmeticOpNotEqual: 176 | return lhs != rhs, nil 177 | case ast.ArithmeticOpLessThan: 178 | return lhs < rhs, nil 179 | case ast.ArithmeticOpLessThanOrEqual: 180 | return lhs <= rhs, nil 181 | case ast.ArithmeticOpGreaterThan: 182 | return lhs > rhs, nil 183 | case ast.ArithmeticOpGreaterThanOrEqual: 184 | return lhs >= rhs, nil 185 | default: 186 | return nil, errors.New("invalid comparison operation") 187 | } 188 | }, 189 | } 190 | } 191 | 192 | func builtinStringCompare() ast.Function { 193 | return ast.Function{ 194 | ArgTypes: []ast.Type{ast.TypeInt, ast.TypeString, ast.TypeString}, 195 | Variadic: false, 196 | ReturnType: ast.TypeBool, 197 | Callback: func(args []interface{}) (interface{}, error) { 198 | op := args[0].(ast.ArithmeticOp) 199 | lhs := args[1].(string) 200 | rhs := args[2].(string) 201 | 202 | switch op { 203 | case ast.ArithmeticOpEqual: 204 | return lhs == rhs, nil 205 | case ast.ArithmeticOpNotEqual: 206 | return lhs != rhs, nil 207 | default: 208 | return nil, errors.New("invalid comparison operation") 209 | } 210 | }, 211 | } 212 | } 213 | 214 | func builtinLogical() ast.Function { 215 | return ast.Function{ 216 | ArgTypes: []ast.Type{ast.TypeInt}, 217 | Variadic: true, 218 | VariadicType: ast.TypeBool, 219 | ReturnType: ast.TypeBool, 220 | Callback: func(args []interface{}) (interface{}, error) { 221 | op := args[0].(ast.ArithmeticOp) 222 | result := args[1].(bool) 223 | for _, raw := range args[2:] { 224 | arg := raw.(bool) 225 | switch op { 226 | case ast.ArithmeticOpLogicalOr: 227 | result = result || arg 228 | case ast.ArithmeticOpLogicalAnd: 229 | result = result && arg 230 | default: 231 | return nil, errors.New("invalid logical operator") 232 | } 233 | } 234 | 235 | return result, nil 236 | }, 237 | } 238 | } 239 | 240 | func builtinFloatToInt() ast.Function { 241 | return ast.Function{ 242 | ArgTypes: []ast.Type{ast.TypeFloat}, 243 | ReturnType: ast.TypeInt, 244 | Callback: func(args []interface{}) (interface{}, error) { 245 | return int(args[0].(float64)), nil 246 | }, 247 | } 248 | } 249 | 250 | func builtinFloatToString() ast.Function { 251 | return ast.Function{ 252 | ArgTypes: []ast.Type{ast.TypeFloat}, 253 | ReturnType: ast.TypeString, 254 | Callback: func(args []interface{}) (interface{}, error) { 255 | return strconv.FormatFloat( 256 | args[0].(float64), 'g', -1, 64), nil 257 | }, 258 | } 259 | } 260 | 261 | func builtinIntToFloat() ast.Function { 262 | return ast.Function{ 263 | ArgTypes: []ast.Type{ast.TypeInt}, 264 | ReturnType: ast.TypeFloat, 265 | Callback: func(args []interface{}) (interface{}, error) { 266 | return float64(args[0].(int)), nil 267 | }, 268 | } 269 | } 270 | 271 | func builtinIntToString() ast.Function { 272 | return ast.Function{ 273 | ArgTypes: []ast.Type{ast.TypeInt}, 274 | ReturnType: ast.TypeString, 275 | Callback: func(args []interface{}) (interface{}, error) { 276 | return strconv.FormatInt(int64(args[0].(int)), 10), nil 277 | }, 278 | } 279 | } 280 | 281 | func builtinStringToInt() ast.Function { 282 | return ast.Function{ 283 | ArgTypes: []ast.Type{ast.TypeInt}, 284 | ReturnType: ast.TypeString, 285 | Callback: func(args []interface{}) (interface{}, error) { 286 | v, err := strconv.ParseInt(args[0].(string), 0, 0) 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | return int(v), nil 292 | }, 293 | } 294 | } 295 | 296 | func builtinStringToFloat() ast.Function { 297 | return ast.Function{ 298 | ArgTypes: []ast.Type{ast.TypeString}, 299 | ReturnType: ast.TypeFloat, 300 | Callback: func(args []interface{}) (interface{}, error) { 301 | v, err := strconv.ParseFloat(args[0].(string), 64) 302 | if err != nil { 303 | return nil, err 304 | } 305 | 306 | return v, nil 307 | }, 308 | } 309 | } 310 | 311 | func builtinBoolToString() ast.Function { 312 | return ast.Function{ 313 | ArgTypes: []ast.Type{ast.TypeBool}, 314 | ReturnType: ast.TypeString, 315 | Callback: func(args []interface{}) (interface{}, error) { 316 | return strconv.FormatBool(args[0].(bool)), nil 317 | }, 318 | } 319 | } 320 | 321 | func builtinStringToBool() ast.Function { 322 | return ast.Function{ 323 | ArgTypes: []ast.Type{ast.TypeString}, 324 | ReturnType: ast.TypeBool, 325 | Callback: func(args []interface{}) (interface{}, error) { 326 | v, err := strconv.ParseBool(args[0].(string)) 327 | if err != nil { 328 | return nil, err 329 | } 330 | 331 | return v, nil 332 | }, 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /scanner/scanner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package scanner 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | ) 12 | 13 | func TestScanner(t *testing.T) { 14 | cases := []struct { 15 | Input string 16 | Output []TokenType 17 | }{ 18 | { 19 | "", 20 | []TokenType{EOF}, 21 | }, 22 | 23 | { 24 | "$", 25 | []TokenType{LITERAL, EOF}, 26 | }, 27 | 28 | { 29 | `${"\`, 30 | []TokenType{BEGIN, OQUOTE, INVALID, EOF}, 31 | }, 32 | 33 | { 34 | "foo", 35 | []TokenType{LITERAL, EOF}, 36 | }, 37 | 38 | { 39 | "foo$bar", 40 | []TokenType{LITERAL, EOF}, 41 | }, 42 | 43 | { 44 | "foo ${0}", 45 | []TokenType{LITERAL, BEGIN, INTEGER, END, EOF}, 46 | }, 47 | 48 | { 49 | "foo ${0.}", 50 | []TokenType{LITERAL, BEGIN, INTEGER, PERIOD, END, EOF}, 51 | }, 52 | 53 | { 54 | "foo ${0.0}", 55 | []TokenType{LITERAL, BEGIN, FLOAT, END, EOF}, 56 | }, 57 | 58 | { 59 | "foo ${0.0.0}", 60 | []TokenType{LITERAL, BEGIN, FLOAT, PERIOD, INTEGER, END, EOF}, 61 | }, 62 | 63 | { 64 | "föo ${bar}", 65 | []TokenType{LITERAL, BEGIN, IDENTIFIER, END, EOF}, 66 | }, 67 | 68 | { 69 | "foo ${bar.0.baz}", 70 | []TokenType{LITERAL, BEGIN, IDENTIFIER, END, EOF}, 71 | }, 72 | 73 | { 74 | "foo ${bar.foo-bar.baz}", 75 | []TokenType{LITERAL, BEGIN, IDENTIFIER, END, EOF}, 76 | }, 77 | 78 | { 79 | "foo $${bar}", 80 | []TokenType{LITERAL, EOF}, 81 | }, 82 | 83 | { 84 | "foo $$$${bar}", 85 | []TokenType{LITERAL, EOF}, 86 | }, 87 | 88 | { 89 | `foo ${"bár"}`, 90 | []TokenType{LITERAL, BEGIN, OQUOTE, STRING, CQUOTE, END, EOF}, 91 | }, 92 | 93 | { 94 | "${bar(baz)}", 95 | []TokenType{ 96 | BEGIN, 97 | IDENTIFIER, OPAREN, IDENTIFIER, CPAREN, 98 | END, EOF, 99 | }, 100 | }, 101 | 102 | { 103 | "${bar(baz4, foo_ooo)}", 104 | []TokenType{ 105 | BEGIN, 106 | IDENTIFIER, OPAREN, 107 | IDENTIFIER, COMMA, IDENTIFIER, 108 | CPAREN, 109 | END, EOF, 110 | }, 111 | }, 112 | 113 | { 114 | "${bár(42)}", 115 | []TokenType{ 116 | BEGIN, 117 | IDENTIFIER, OPAREN, INTEGER, CPAREN, 118 | END, EOF, 119 | }, 120 | }, 121 | 122 | { 123 | "${bar(-42)}", 124 | []TokenType{ 125 | BEGIN, 126 | IDENTIFIER, OPAREN, MINUS, INTEGER, CPAREN, 127 | END, EOF, 128 | }, 129 | }, 130 | 131 | { 132 | "${bar(42+1)}", 133 | []TokenType{ 134 | BEGIN, 135 | IDENTIFIER, OPAREN, 136 | INTEGER, PLUS, INTEGER, 137 | CPAREN, 138 | END, EOF, 139 | }, 140 | }, 141 | 142 | { 143 | "${true && false}", 144 | []TokenType{ 145 | BEGIN, 146 | BOOL, AND, BOOL, 147 | END, EOF, 148 | }, 149 | }, 150 | 151 | { 152 | "${true || false}", 153 | []TokenType{ 154 | BEGIN, 155 | BOOL, OR, BOOL, 156 | END, EOF, 157 | }, 158 | }, 159 | 160 | { 161 | "${1 == 5}", 162 | []TokenType{ 163 | BEGIN, 164 | INTEGER, EQUAL, INTEGER, 165 | END, EOF, 166 | }, 167 | }, 168 | 169 | { 170 | "${1 != 5}", 171 | []TokenType{ 172 | BEGIN, 173 | INTEGER, NOTEQUAL, INTEGER, 174 | END, EOF, 175 | }, 176 | }, 177 | 178 | { 179 | "${1 > 5}", 180 | []TokenType{ 181 | BEGIN, 182 | INTEGER, GT, INTEGER, 183 | END, EOF, 184 | }, 185 | }, 186 | 187 | { 188 | "${1 < 5}", 189 | []TokenType{ 190 | BEGIN, 191 | INTEGER, LT, INTEGER, 192 | END, EOF, 193 | }, 194 | }, 195 | 196 | { 197 | "${1 <= 5}", 198 | []TokenType{ 199 | BEGIN, 200 | INTEGER, LTE, INTEGER, 201 | END, EOF, 202 | }, 203 | }, 204 | 205 | { 206 | "${1 >= 5}", 207 | []TokenType{ 208 | BEGIN, 209 | INTEGER, GTE, INTEGER, 210 | END, EOF, 211 | }, 212 | }, 213 | 214 | { 215 | "${true ? 1 : 5}", 216 | []TokenType{ 217 | BEGIN, 218 | BOOL, QUESTION, INTEGER, COLON, INTEGER, 219 | END, EOF, 220 | }, 221 | }, 222 | 223 | { 224 | "${bar(3.14159)}", 225 | []TokenType{ 226 | BEGIN, 227 | IDENTIFIER, OPAREN, FLOAT, CPAREN, 228 | END, EOF, 229 | }, 230 | }, 231 | 232 | { 233 | "${bar(true)}", 234 | []TokenType{ 235 | BEGIN, 236 | IDENTIFIER, OPAREN, BOOL, CPAREN, 237 | END, EOF, 238 | }, 239 | }, 240 | 241 | { 242 | "${bar(inner(_baz))}", 243 | []TokenType{ 244 | BEGIN, 245 | IDENTIFIER, OPAREN, 246 | IDENTIFIER, OPAREN, 247 | IDENTIFIER, 248 | CPAREN, CPAREN, 249 | END, EOF, 250 | }, 251 | }, 252 | 253 | { 254 | "foo ${foo.bar.baz}", 255 | []TokenType{ 256 | LITERAL, 257 | BEGIN, 258 | IDENTIFIER, 259 | END, EOF, 260 | }, 261 | }, 262 | 263 | { 264 | "foo ${foo.bar.*.baz}", 265 | []TokenType{ 266 | LITERAL, 267 | BEGIN, 268 | IDENTIFIER, 269 | END, EOF, 270 | }, 271 | }, 272 | 273 | { 274 | "foo ${foo.bar.*}", 275 | []TokenType{ 276 | LITERAL, 277 | BEGIN, 278 | IDENTIFIER, 279 | END, EOF, 280 | }, 281 | }, 282 | 283 | { 284 | "foo ${foo.bar.*baz}", 285 | []TokenType{ 286 | LITERAL, 287 | BEGIN, 288 | IDENTIFIER, PERIOD, STAR, IDENTIFIER, 289 | END, EOF, 290 | }, 291 | }, 292 | 293 | { 294 | "foo ${foo*}", 295 | []TokenType{ 296 | LITERAL, 297 | BEGIN, 298 | IDENTIFIER, STAR, 299 | END, EOF, 300 | }, 301 | }, 302 | 303 | { 304 | `foo ${foo("baz")}`, 305 | []TokenType{ 306 | LITERAL, 307 | BEGIN, 308 | IDENTIFIER, OPAREN, OQUOTE, STRING, CQUOTE, CPAREN, 309 | END, EOF, 310 | }, 311 | }, 312 | 313 | { 314 | `foo ${"${var.foo}"}`, 315 | []TokenType{ 316 | LITERAL, 317 | BEGIN, 318 | OQUOTE, 319 | BEGIN, 320 | IDENTIFIER, 321 | END, 322 | CQUOTE, 323 | END, 324 | EOF, 325 | }, 326 | }, 327 | 328 | { 329 | "${1 = 5}", 330 | []TokenType{ 331 | BEGIN, 332 | INTEGER, 333 | INVALID, 334 | EOF, 335 | }, 336 | }, 337 | 338 | { 339 | "${1 & 5}", 340 | []TokenType{ 341 | BEGIN, 342 | INTEGER, 343 | INVALID, 344 | EOF, 345 | }, 346 | }, 347 | 348 | { 349 | "${1 | 5}", 350 | []TokenType{ 351 | BEGIN, 352 | INTEGER, 353 | INVALID, 354 | EOF, 355 | }, 356 | }, 357 | 358 | { 359 | `${unclosed`, 360 | []TokenType{BEGIN, IDENTIFIER, EOF}, 361 | }, 362 | 363 | { 364 | `${"unclosed`, 365 | []TokenType{BEGIN, OQUOTE, INVALID, EOF}, 366 | }, 367 | } 368 | 369 | for _, tc := range cases { 370 | ch := Scan(tc.Input, ast.InitPos) 371 | var actual []TokenType 372 | for token := range ch { 373 | actual = append(actual, token.Type) 374 | } 375 | 376 | if !reflect.DeepEqual(actual, tc.Output) { 377 | t.Errorf( 378 | "\nInput: %s\nBad: %#v\nWant: %#v", 379 | tc.Input, tokenTypeNames(actual), tokenTypeNames(tc.Output), 380 | ) 381 | } 382 | } 383 | } 384 | 385 | func TestScannerPos(t *testing.T) { 386 | cases := []struct { 387 | Input string 388 | Positions []ast.Pos 389 | }{ 390 | { 391 | `foo`, 392 | []ast.Pos{ 393 | {Line: 1, Column: 1}, 394 | {Line: 1, Column: 4}, 395 | }, 396 | }, 397 | { 398 | `föo`, 399 | []ast.Pos{ 400 | {Line: 1, Column: 1}, 401 | {Line: 1, Column: 4}, 402 | }, 403 | }, 404 | { 405 | // Ideally combining diacritic marks would actually get 406 | // counted as only one character, but this test asserts 407 | // our current compromise the "Column" counts runes 408 | // rather than graphemes. 409 | `fĉo`, 410 | []ast.Pos{ 411 | {Line: 1, Column: 1}, 412 | {Line: 1, Column: 5}, 413 | }, 414 | }, 415 | { 416 | // Spaces in literals are counted as part of the literal. 417 | ` foo `, 418 | []ast.Pos{ 419 | {Line: 1, Column: 1}, 420 | {Line: 1, Column: 6}, 421 | }, 422 | }, 423 | { 424 | `${foo}`, 425 | []ast.Pos{ 426 | {Line: 1, Column: 1}, 427 | {Line: 1, Column: 3}, 428 | {Line: 1, Column: 6}, 429 | {Line: 1, Column: 7}, 430 | }, 431 | }, 432 | { 433 | // Spaces inside interpolation sequences are skipped 434 | `${ foo }`, 435 | []ast.Pos{ 436 | {Line: 1, Column: 1}, 437 | {Line: 1, Column: 4}, 438 | {Line: 1, Column: 8}, 439 | {Line: 1, Column: 9}, 440 | }, 441 | }, 442 | { 443 | `${föo}`, 444 | []ast.Pos{ 445 | {Line: 1, Column: 1}, 446 | {Line: 1, Column: 3}, 447 | {Line: 1, Column: 6}, 448 | {Line: 1, Column: 7}, 449 | }, 450 | }, 451 | { 452 | `${fĉo}`, 453 | []ast.Pos{ 454 | {Line: 1, Column: 1}, 455 | {Line: 1, Column: 3}, 456 | {Line: 1, Column: 7}, 457 | {Line: 1, Column: 8}, 458 | }, 459 | }, 460 | { 461 | `foo ${ foo } foo`, 462 | []ast.Pos{ 463 | {Line: 1, Column: 1}, 464 | {Line: 1, Column: 5}, 465 | {Line: 1, Column: 8}, 466 | {Line: 1, Column: 12}, 467 | {Line: 1, Column: 13}, 468 | {Line: 1, Column: 17}, 469 | }, 470 | }, 471 | { 472 | `foo ${ " foo " } foo`, 473 | []ast.Pos{ 474 | {Line: 1, Column: 1}, // LITERAL 475 | {Line: 1, Column: 5}, // BEGIN 476 | {Line: 1, Column: 8}, // OQUOTE 477 | {Line: 1, Column: 9}, // STRING 478 | {Line: 1, Column: 14}, // CQUOTE 479 | {Line: 1, Column: 16}, // END 480 | {Line: 1, Column: 17}, // LITERAL 481 | {Line: 1, Column: 21}, // EOF 482 | }, 483 | }, 484 | } 485 | 486 | for _, tc := range cases { 487 | ch := Scan(tc.Input, ast.Pos{Line: 1, Column: 1}) 488 | var actual []ast.Pos 489 | for token := range ch { 490 | actual = append(actual, token.Pos) 491 | } 492 | 493 | if !reflect.DeepEqual(actual, tc.Positions) { 494 | t.Errorf( 495 | "\nInput: %s\nBad: %#v\nWant: %#v", 496 | tc.Input, posStrings(actual), posStrings(tc.Positions), 497 | ) 498 | } 499 | } 500 | } 501 | 502 | func tokenTypeNames(types []TokenType) []string { 503 | ret := make([]string, len(types)) 504 | for i, t := range types { 505 | ret[i] = t.String() 506 | } 507 | return ret 508 | } 509 | 510 | func posStrings(positions []ast.Pos) []string { 511 | ret := make([]string, len(positions)) 512 | for i, pos := range positions { 513 | ret[i] = pos.String() 514 | } 515 | return ret 516 | } 517 | -------------------------------------------------------------------------------- /convert_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | ) 12 | 13 | func TestInterfaceToVariable_variableInput(t *testing.T) { 14 | testCases := []struct { 15 | name string 16 | input interface{} 17 | expected ast.Variable 18 | }{ 19 | { 20 | name: "string variable", 21 | input: ast.Variable{ 22 | Type: ast.TypeString, 23 | Value: "Hello world", 24 | }, 25 | expected: ast.Variable{ 26 | Type: ast.TypeString, 27 | Value: "Hello world", 28 | }, 29 | }, 30 | { 31 | // This is just to maintain backward compatibility 32 | // after https://github.com/mitchellh/mapstructure/pull/98 33 | name: "slice of variables", 34 | input: []ast.Variable{ 35 | {Type: ast.TypeString, Value: "Hello world"}, 36 | }, 37 | expected: ast.Variable{ 38 | Type: ast.TypeList, 39 | Value: []ast.Variable{ 40 | {Type: ast.TypeString, Value: "Hello world"}, 41 | }, 42 | }, 43 | }, 44 | { 45 | // This is just to maintain backward compatibility 46 | // after https://github.com/mitchellh/mapstructure/pull/98 47 | name: "map with variables", 48 | input: map[string]ast.Variable{ 49 | "k": {Type: ast.TypeString, Value: "Hello world"}, 50 | }, 51 | expected: ast.Variable{ 52 | Type: ast.TypeMap, 53 | Value: map[string]ast.Variable{ 54 | "k": {Type: ast.TypeString, Value: "Hello world"}, 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | for _, tc := range testCases { 61 | output, err := InterfaceToVariable(tc.input) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | if !reflect.DeepEqual(output, tc.expected) { 66 | t.Fatalf("%s:\nExpected: %s\n Got: %s\n", tc.name, tc.expected, output) 67 | } 68 | } 69 | } 70 | 71 | func TestInterfaceToVariable(t *testing.T) { 72 | testCases := []struct { 73 | name string 74 | input interface{} 75 | expected ast.Variable 76 | }{ 77 | { 78 | name: "string", 79 | input: "Hello world", 80 | expected: ast.Variable{ 81 | Type: ast.TypeString, 82 | Value: "Hello world", 83 | }, 84 | }, 85 | { 86 | name: "empty list", 87 | input: []interface{}{}, 88 | expected: ast.Variable{ 89 | Type: ast.TypeList, 90 | Value: []ast.Variable{}, 91 | }, 92 | }, 93 | { 94 | name: "empty list of strings", 95 | input: []string{}, 96 | expected: ast.Variable{ 97 | Type: ast.TypeList, 98 | Value: []ast.Variable{}, 99 | }, 100 | }, 101 | { 102 | name: "int", 103 | input: 1, 104 | expected: ast.Variable{ 105 | Type: ast.TypeString, 106 | Value: "1", 107 | }, 108 | }, 109 | { 110 | name: "list of strings", 111 | input: []string{"Hello", "World"}, 112 | expected: ast.Variable{ 113 | Type: ast.TypeList, 114 | Value: []ast.Variable{ 115 | { 116 | Type: ast.TypeString, 117 | Value: "Hello", 118 | }, 119 | { 120 | Type: ast.TypeString, 121 | Value: "World", 122 | }, 123 | }, 124 | }, 125 | }, 126 | { 127 | name: "list of lists of strings", 128 | input: [][]interface{}{[]interface{}{"Hello", "World"}, []interface{}{"Goodbye", "World"}}, 129 | expected: ast.Variable{ 130 | Type: ast.TypeList, 131 | Value: []ast.Variable{ 132 | { 133 | Type: ast.TypeList, 134 | Value: []ast.Variable{ 135 | { 136 | Type: ast.TypeString, 137 | Value: "Hello", 138 | }, 139 | { 140 | Type: ast.TypeString, 141 | Value: "World", 142 | }, 143 | }, 144 | }, 145 | { 146 | Type: ast.TypeList, 147 | Value: []ast.Variable{ 148 | { 149 | Type: ast.TypeString, 150 | Value: "Goodbye", 151 | }, 152 | { 153 | Type: ast.TypeString, 154 | Value: "World", 155 | }, 156 | }, 157 | }, 158 | }, 159 | }, 160 | }, 161 | { 162 | name: "list with unknown", 163 | input: []string{"Hello", UnknownValue}, 164 | expected: ast.Variable{ 165 | Type: ast.TypeList, 166 | Value: []ast.Variable{ 167 | { 168 | Type: ast.TypeString, 169 | Value: "Hello", 170 | }, 171 | { 172 | Value: UnknownValue, 173 | Type: ast.TypeUnknown, 174 | }, 175 | }, 176 | }, 177 | }, 178 | { 179 | name: "map of string->string", 180 | input: map[string]string{"Hello": "World", "Foo": "Bar"}, 181 | expected: ast.Variable{ 182 | Type: ast.TypeMap, 183 | Value: map[string]ast.Variable{ 184 | "Hello": { 185 | Type: ast.TypeString, 186 | Value: "World", 187 | }, 188 | "Foo": { 189 | Type: ast.TypeString, 190 | Value: "Bar", 191 | }, 192 | }, 193 | }, 194 | }, 195 | { 196 | name: "map of lists of strings", 197 | input: map[string][]string{ 198 | "Hello": []string{"Hello", "World"}, 199 | "Goodbye": []string{"Goodbye", "World"}, 200 | }, 201 | expected: ast.Variable{ 202 | Type: ast.TypeMap, 203 | Value: map[string]ast.Variable{ 204 | "Hello": { 205 | Type: ast.TypeList, 206 | Value: []ast.Variable{ 207 | { 208 | Type: ast.TypeString, 209 | Value: "Hello", 210 | }, 211 | { 212 | Type: ast.TypeString, 213 | Value: "World", 214 | }, 215 | }, 216 | }, 217 | "Goodbye": { 218 | Type: ast.TypeList, 219 | Value: []ast.Variable{ 220 | { 221 | Type: ast.TypeString, 222 | Value: "Goodbye", 223 | }, 224 | { 225 | Type: ast.TypeString, 226 | Value: "World", 227 | }, 228 | }, 229 | }, 230 | }, 231 | }, 232 | }, 233 | { 234 | name: "empty map", 235 | input: map[string]string{}, 236 | expected: ast.Variable{ 237 | Type: ast.TypeMap, 238 | Value: map[string]ast.Variable{}, 239 | }, 240 | }, 241 | { 242 | name: "three-element map", 243 | input: map[string]string{ 244 | "us-west-1": "ami-123456", 245 | "us-west-2": "ami-456789", 246 | "eu-west-1": "ami-012345", 247 | }, 248 | expected: ast.Variable{ 249 | Type: ast.TypeMap, 250 | Value: map[string]ast.Variable{ 251 | "us-west-1": { 252 | Type: ast.TypeString, 253 | Value: "ami-123456", 254 | }, 255 | "us-west-2": { 256 | Type: ast.TypeString, 257 | Value: "ami-456789", 258 | }, 259 | "eu-west-1": { 260 | Type: ast.TypeString, 261 | Value: "ami-012345", 262 | }, 263 | }, 264 | }, 265 | }, 266 | } 267 | 268 | for _, tc := range testCases { 269 | output, err := InterfaceToVariable(tc.input) 270 | if err != nil { 271 | t.Fatal(err) 272 | } 273 | 274 | if !reflect.DeepEqual(output, tc.expected) { 275 | t.Fatalf("%s:\nExpected: %s\n Got: %s\n", tc.name, tc.expected, output) 276 | } 277 | } 278 | } 279 | 280 | func TestVariableToInterface(t *testing.T) { 281 | testCases := []struct { 282 | name string 283 | expected interface{} 284 | input ast.Variable 285 | }{ 286 | { 287 | name: "string", 288 | expected: "Hello world", 289 | input: ast.Variable{ 290 | Type: ast.TypeString, 291 | Value: "Hello world", 292 | }, 293 | }, 294 | { 295 | name: "empty list", 296 | expected: []interface{}{}, 297 | input: ast.Variable{ 298 | Type: ast.TypeList, 299 | Value: []ast.Variable{}, 300 | }, 301 | }, 302 | { 303 | name: "int", 304 | expected: "1", 305 | input: ast.Variable{ 306 | Type: ast.TypeString, 307 | Value: "1", 308 | }, 309 | }, 310 | { 311 | name: "list of strings", 312 | expected: []interface{}{"Hello", "World"}, 313 | input: ast.Variable{ 314 | Type: ast.TypeList, 315 | Value: []ast.Variable{ 316 | { 317 | Type: ast.TypeString, 318 | Value: "Hello", 319 | }, 320 | { 321 | Type: ast.TypeString, 322 | Value: "World", 323 | }, 324 | }, 325 | }, 326 | }, 327 | { 328 | name: "list of lists of strings", 329 | expected: []interface{}{[]interface{}{"Hello", "World"}, []interface{}{"Goodbye", "World"}}, 330 | input: ast.Variable{ 331 | Type: ast.TypeList, 332 | Value: []ast.Variable{ 333 | { 334 | Type: ast.TypeList, 335 | Value: []ast.Variable{ 336 | { 337 | Type: ast.TypeString, 338 | Value: "Hello", 339 | }, 340 | { 341 | Type: ast.TypeString, 342 | Value: "World", 343 | }, 344 | }, 345 | }, 346 | { 347 | Type: ast.TypeList, 348 | Value: []ast.Variable{ 349 | { 350 | Type: ast.TypeString, 351 | Value: "Goodbye", 352 | }, 353 | { 354 | Type: ast.TypeString, 355 | Value: "World", 356 | }, 357 | }, 358 | }, 359 | }, 360 | }, 361 | }, 362 | { 363 | name: "map of string->string", 364 | expected: map[string]interface{}{"Hello": "World", "Foo": "Bar"}, 365 | input: ast.Variable{ 366 | Type: ast.TypeMap, 367 | Value: map[string]ast.Variable{ 368 | "Hello": { 369 | Type: ast.TypeString, 370 | Value: "World", 371 | }, 372 | "Foo": { 373 | Type: ast.TypeString, 374 | Value: "Bar", 375 | }, 376 | }, 377 | }, 378 | }, 379 | { 380 | name: "map of lists of strings", 381 | expected: map[string]interface{}{ 382 | "Hello": []interface{}{"Hello", "World"}, 383 | "Goodbye": []interface{}{"Goodbye", "World"}, 384 | }, 385 | input: ast.Variable{ 386 | Type: ast.TypeMap, 387 | Value: map[string]ast.Variable{ 388 | "Hello": { 389 | Type: ast.TypeList, 390 | Value: []ast.Variable{ 391 | { 392 | Type: ast.TypeString, 393 | Value: "Hello", 394 | }, 395 | { 396 | Type: ast.TypeString, 397 | Value: "World", 398 | }, 399 | }, 400 | }, 401 | "Goodbye": { 402 | Type: ast.TypeList, 403 | Value: []ast.Variable{ 404 | { 405 | Type: ast.TypeString, 406 | Value: "Goodbye", 407 | }, 408 | { 409 | Type: ast.TypeString, 410 | Value: "World", 411 | }, 412 | }, 413 | }, 414 | }, 415 | }, 416 | }, 417 | { 418 | name: "empty map", 419 | expected: map[string]interface{}{}, 420 | input: ast.Variable{ 421 | Type: ast.TypeMap, 422 | Value: map[string]ast.Variable{}, 423 | }, 424 | }, 425 | { 426 | name: "three-element map", 427 | expected: map[string]interface{}{ 428 | "us-west-1": "ami-123456", 429 | "us-west-2": "ami-456789", 430 | "eu-west-1": "ami-012345", 431 | }, 432 | input: ast.Variable{ 433 | Type: ast.TypeMap, 434 | Value: map[string]ast.Variable{ 435 | "us-west-1": { 436 | Type: ast.TypeString, 437 | Value: "ami-123456", 438 | }, 439 | "us-west-2": { 440 | Type: ast.TypeString, 441 | Value: "ami-456789", 442 | }, 443 | "eu-west-1": { 444 | Type: ast.TypeString, 445 | Value: "ami-012345", 446 | }, 447 | }, 448 | }, 449 | }, 450 | } 451 | 452 | for _, tc := range testCases { 453 | output, err := VariableToInterface(tc.input) 454 | if err != nil { 455 | t.Fatal(err) 456 | } 457 | 458 | if !reflect.DeepEqual(output, tc.expected) { 459 | t.Fatalf("%s:\nExpected: %s\n Got: %s\n", tc.name, 460 | tc.expected, output) 461 | } 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /check_types_test.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/hil/ast" 10 | ) 11 | 12 | func TestTypeCheck(t *testing.T) { 13 | cases := []struct { 14 | Input string 15 | Scope ast.Scope 16 | Error bool 17 | }{ 18 | { 19 | "foo", 20 | &ast.BasicScope{}, 21 | false, 22 | }, 23 | 24 | { 25 | "foo ${bar}", 26 | &ast.BasicScope{ 27 | VarMap: map[string]ast.Variable{ 28 | "bar": ast.Variable{ 29 | Value: "baz", 30 | Type: ast.TypeString, 31 | }, 32 | }, 33 | }, 34 | false, 35 | }, 36 | 37 | { 38 | "foo ${rand()}", 39 | &ast.BasicScope{ 40 | FuncMap: map[string]ast.Function{ 41 | "rand": ast.Function{ 42 | ReturnType: ast.TypeString, 43 | Callback: func([]interface{}) (interface{}, error) { 44 | return "42", nil 45 | }, 46 | }, 47 | }, 48 | }, 49 | false, 50 | }, 51 | 52 | { 53 | `foo ${rand("42")}`, 54 | &ast.BasicScope{ 55 | FuncMap: map[string]ast.Function{ 56 | "rand": ast.Function{ 57 | ArgTypes: []ast.Type{ast.TypeString}, 58 | ReturnType: ast.TypeString, 59 | Callback: func([]interface{}) (interface{}, error) { 60 | return "42", nil 61 | }, 62 | }, 63 | }, 64 | }, 65 | false, 66 | }, 67 | 68 | { 69 | `foo ${rand(42)}`, 70 | &ast.BasicScope{ 71 | FuncMap: map[string]ast.Function{ 72 | "rand": ast.Function{ 73 | ArgTypes: []ast.Type{ast.TypeString}, 74 | ReturnType: ast.TypeString, 75 | Callback: func([]interface{}) (interface{}, error) { 76 | return "42", nil 77 | }, 78 | }, 79 | }, 80 | }, 81 | true, 82 | }, 83 | 84 | { 85 | `foo ${rand()}`, 86 | &ast.BasicScope{ 87 | FuncMap: map[string]ast.Function{ 88 | "rand": ast.Function{ 89 | ArgTypes: nil, 90 | ReturnType: ast.TypeString, 91 | Variadic: true, 92 | VariadicType: ast.TypeString, 93 | Callback: func([]interface{}) (interface{}, error) { 94 | return "42", nil 95 | }, 96 | }, 97 | }, 98 | }, 99 | false, 100 | }, 101 | 102 | { 103 | `foo ${rand("42")}`, 104 | &ast.BasicScope{ 105 | FuncMap: map[string]ast.Function{ 106 | "rand": ast.Function{ 107 | ArgTypes: nil, 108 | ReturnType: ast.TypeString, 109 | Variadic: true, 110 | VariadicType: ast.TypeString, 111 | Callback: func([]interface{}) (interface{}, error) { 112 | return "42", nil 113 | }, 114 | }, 115 | }, 116 | }, 117 | false, 118 | }, 119 | 120 | { 121 | `foo ${rand("42", 42)}`, 122 | &ast.BasicScope{ 123 | FuncMap: map[string]ast.Function{ 124 | "rand": ast.Function{ 125 | ArgTypes: nil, 126 | ReturnType: ast.TypeString, 127 | Variadic: true, 128 | VariadicType: ast.TypeString, 129 | Callback: func([]interface{}) (interface{}, error) { 130 | return "42", nil 131 | }, 132 | }, 133 | }, 134 | }, 135 | true, 136 | }, 137 | 138 | { 139 | "${foo[0]}", 140 | &ast.BasicScope{ 141 | VarMap: map[string]ast.Variable{ 142 | "foo": ast.Variable{ 143 | Type: ast.TypeList, 144 | Value: []ast.Variable{ 145 | ast.Variable{ 146 | Type: ast.TypeString, 147 | Value: "Hello", 148 | }, 149 | ast.Variable{ 150 | Type: ast.TypeString, 151 | Value: "World", 152 | }, 153 | }, 154 | }, 155 | }, 156 | }, 157 | false, 158 | }, 159 | 160 | { 161 | "${foo[0]}", 162 | &ast.BasicScope{ 163 | VarMap: map[string]ast.Variable{ 164 | "foo": ast.Variable{ 165 | Type: ast.TypeList, 166 | Value: []ast.Variable{ 167 | ast.Variable{ 168 | Type: ast.TypeInt, 169 | Value: 3, 170 | }, 171 | ast.Variable{ 172 | Type: ast.TypeString, 173 | Value: "World", 174 | }, 175 | }, 176 | }, 177 | }, 178 | }, 179 | true, 180 | }, 181 | 182 | { 183 | "${foo[0]}", 184 | &ast.BasicScope{ 185 | VarMap: map[string]ast.Variable{ 186 | "foo": ast.Variable{ 187 | Type: ast.TypeString, 188 | Value: "Hello World", 189 | }, 190 | }, 191 | }, 192 | true, 193 | }, 194 | 195 | { 196 | "foo ${bar}", 197 | &ast.BasicScope{ 198 | VarMap: map[string]ast.Variable{ 199 | "bar": ast.Variable{ 200 | Value: 42, 201 | Type: ast.TypeInt, 202 | }, 203 | }, 204 | }, 205 | true, 206 | }, 207 | 208 | { 209 | "foo ${rand()}", 210 | &ast.BasicScope{ 211 | FuncMap: map[string]ast.Function{ 212 | "rand": ast.Function{ 213 | ReturnType: ast.TypeInt, 214 | Callback: func([]interface{}) (interface{}, error) { 215 | return 42, nil 216 | }, 217 | }, 218 | }, 219 | }, 220 | true, 221 | }, 222 | 223 | { 224 | `foo ${true ? "foo" : "bar"}`, 225 | &ast.BasicScope{}, 226 | false, 227 | }, 228 | 229 | { 230 | // can't use different types for true and false expressions 231 | `foo ${true ? 1 : "baz"}`, 232 | &ast.BasicScope{}, 233 | true, 234 | }, 235 | 236 | { 237 | // condition must be boolean 238 | `foo ${"foo" ? 1 : 5}`, 239 | &ast.BasicScope{}, 240 | true, 241 | }, 242 | 243 | { 244 | // conditional with unknown value is permitted 245 | `foo ${true ? known : unknown}`, 246 | &ast.BasicScope{ 247 | VarMap: map[string]ast.Variable{ 248 | "known": ast.Variable{ 249 | Type: ast.TypeString, 250 | Value: "bar", 251 | }, 252 | "unknown": ast.Variable{ 253 | Type: ast.TypeUnknown, 254 | Value: UnknownValue, 255 | }, 256 | }, 257 | }, 258 | false, 259 | }, 260 | 261 | { 262 | // conditional with unknown value the other way permitted too 263 | `foo ${true ? unknown : known}`, 264 | &ast.BasicScope{ 265 | VarMap: map[string]ast.Variable{ 266 | "known": ast.Variable{ 267 | Type: ast.TypeString, 268 | Value: "bar", 269 | }, 270 | "unknown": ast.Variable{ 271 | Type: ast.TypeUnknown, 272 | Value: UnknownValue, 273 | }, 274 | }, 275 | }, 276 | false, 277 | }, 278 | 279 | { 280 | // conditional with two unknowns is allowed 281 | `foo ${true ? unknown : unknown}`, 282 | &ast.BasicScope{ 283 | VarMap: map[string]ast.Variable{ 284 | "unknown": ast.Variable{ 285 | Type: ast.TypeUnknown, 286 | Value: UnknownValue, 287 | }, 288 | }, 289 | }, 290 | false, 291 | }, 292 | 293 | { 294 | // conditional with unknown condition is allowed 295 | `foo ${unknown ? 1 : 2}`, 296 | &ast.BasicScope{ 297 | VarMap: map[string]ast.Variable{ 298 | "unknown": ast.Variable{ 299 | Type: ast.TypeUnknown, 300 | Value: UnknownValue, 301 | }, 302 | }, 303 | }, 304 | false, 305 | }, 306 | 307 | { 308 | // currently lists are not allowed at all 309 | `foo ${true ? arr1 : arr2}`, 310 | &ast.BasicScope{ 311 | VarMap: map[string]ast.Variable{ 312 | "arr1": ast.Variable{ 313 | Type: ast.TypeList, 314 | Value: []ast.Variable{ 315 | ast.Variable{ 316 | Type: ast.TypeInt, 317 | Value: 3, 318 | }, 319 | }, 320 | }, 321 | "arr2": ast.Variable{ 322 | Type: ast.TypeList, 323 | Value: []ast.Variable{ 324 | ast.Variable{ 325 | Type: ast.TypeInt, 326 | Value: 4, 327 | }, 328 | }, 329 | }, 330 | }, 331 | }, 332 | true, 333 | }, 334 | 335 | { 336 | // mismatching element types are invalid 337 | `foo ${true ? arr1 : arr2}`, 338 | &ast.BasicScope{ 339 | VarMap: map[string]ast.Variable{ 340 | "arr1": ast.Variable{ 341 | Type: ast.TypeList, 342 | Value: []ast.Variable{ 343 | ast.Variable{ 344 | Type: ast.TypeInt, 345 | Value: 3, 346 | }, 347 | }, 348 | }, 349 | "arr2": ast.Variable{ 350 | Type: ast.TypeList, 351 | Value: []ast.Variable{ 352 | ast.Variable{ 353 | Type: ast.TypeString, 354 | Value: "foo", 355 | }, 356 | }, 357 | }, 358 | }, 359 | }, 360 | true, 361 | }, 362 | } 363 | 364 | for _, tc := range cases { 365 | node, err := Parse(tc.Input) 366 | if err != nil { 367 | t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) 368 | } 369 | 370 | visitor := &TypeCheck{Scope: tc.Scope} 371 | err = visitor.Visit(node) 372 | if err != nil != tc.Error { 373 | t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) 374 | } 375 | } 376 | } 377 | 378 | func TestTypeCheck_implicit(t *testing.T) { 379 | implicitMap := map[ast.Type]map[ast.Type]string{ 380 | ast.TypeInt: { 381 | ast.TypeString: "intToString", 382 | }, 383 | } 384 | 385 | cases := []struct { 386 | Input string 387 | Scope *ast.BasicScope 388 | Error bool 389 | }{ 390 | { 391 | "foo ${bar}", 392 | &ast.BasicScope{ 393 | VarMap: map[string]ast.Variable{ 394 | "bar": ast.Variable{ 395 | Value: 42, 396 | Type: ast.TypeInt, 397 | }, 398 | }, 399 | }, 400 | false, 401 | }, 402 | 403 | { 404 | "foo ${foo(42)}", 405 | &ast.BasicScope{ 406 | FuncMap: map[string]ast.Function{ 407 | "foo": ast.Function{ 408 | ArgTypes: []ast.Type{ast.TypeString}, 409 | ReturnType: ast.TypeString, 410 | }, 411 | }, 412 | }, 413 | false, 414 | }, 415 | 416 | { 417 | `foo ${foo("42", 42)}`, 418 | &ast.BasicScope{ 419 | FuncMap: map[string]ast.Function{ 420 | "foo": ast.Function{ 421 | ArgTypes: []ast.Type{ast.TypeString}, 422 | Variadic: true, 423 | VariadicType: ast.TypeString, 424 | ReturnType: ast.TypeString, 425 | }, 426 | }, 427 | }, 428 | false, 429 | }, 430 | 431 | { 432 | "${foo[1]}", 433 | &ast.BasicScope{ 434 | VarMap: map[string]ast.Variable{ 435 | "foo": ast.Variable{ 436 | Type: ast.TypeList, 437 | Value: []ast.Variable{ 438 | ast.Variable{ 439 | Type: ast.TypeInt, 440 | Value: 42, 441 | }, 442 | ast.Variable{ 443 | Type: ast.TypeInt, 444 | Value: 23, 445 | }, 446 | }, 447 | }, 448 | }, 449 | }, 450 | false, 451 | }, 452 | 453 | { 454 | "${foo[1]}", 455 | &ast.BasicScope{ 456 | VarMap: map[string]ast.Variable{ 457 | "foo": ast.Variable{ 458 | Type: ast.TypeList, 459 | Value: []ast.Variable{ 460 | ast.Variable{ 461 | Type: ast.TypeInt, 462 | Value: 42, 463 | }, 464 | ast.Variable{ 465 | Type: ast.TypeUnknown, 466 | }, 467 | }, 468 | }, 469 | }, 470 | }, 471 | false, 472 | }, 473 | 474 | { 475 | `${foo[bar[var.keyint]]}`, 476 | &ast.BasicScope{ 477 | VarMap: map[string]ast.Variable{ 478 | "foo": ast.Variable{ 479 | Type: ast.TypeMap, 480 | Value: map[string]ast.Variable{ 481 | "foo": ast.Variable{ 482 | Type: ast.TypeString, 483 | Value: "hello", 484 | }, 485 | "bar": ast.Variable{ 486 | Type: ast.TypeString, 487 | Value: "world", 488 | }, 489 | }, 490 | }, 491 | "bar": ast.Variable{ 492 | Type: ast.TypeList, 493 | Value: []ast.Variable{ 494 | ast.Variable{ 495 | Type: ast.TypeString, 496 | Value: "i dont exist", 497 | }, 498 | ast.Variable{ 499 | Type: ast.TypeString, 500 | Value: "bar", 501 | }, 502 | }, 503 | }, 504 | "var.keyint": ast.Variable{ 505 | Type: ast.TypeInt, 506 | Value: 1, 507 | }, 508 | }, 509 | }, 510 | false, 511 | }, 512 | } 513 | 514 | for _, tc := range cases { 515 | t.Run(tc.Input, func(t *testing.T) { 516 | node, err := Parse(tc.Input) 517 | if err != nil { 518 | t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) 519 | } 520 | 521 | // Modify the scope to add our conversion functions. 522 | if tc.Scope.FuncMap == nil { 523 | tc.Scope.FuncMap = make(map[string]ast.Function) 524 | } 525 | tc.Scope.FuncMap["intToString"] = ast.Function{ 526 | ArgTypes: []ast.Type{ast.TypeInt}, 527 | ReturnType: ast.TypeString, 528 | } 529 | 530 | // Do the first pass... 531 | visitor := &TypeCheck{Scope: tc.Scope, Implicit: implicitMap} 532 | err = visitor.Visit(node) 533 | if err != nil != tc.Error { 534 | t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) 535 | } 536 | if err != nil { 537 | return 538 | } 539 | 540 | // If we didn't error, then the next type check should not fail 541 | // WITHOUT implicits. 542 | visitor = &TypeCheck{Scope: tc.Scope} 543 | err = visitor.Visit(node) 544 | if err != nil { 545 | t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) 546 | } 547 | }) 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package parser 5 | 6 | import ( 7 | "strconv" 8 | "unicode/utf8" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | "github.com/hashicorp/hil/scanner" 12 | ) 13 | 14 | func Parse(ch <-chan *scanner.Token) (ast.Node, error) { 15 | peeker := scanner.NewPeeker(ch) 16 | parser := &parser{peeker} 17 | output, err := parser.ParseTopLevel() 18 | peeker.Close() 19 | return output, err 20 | } 21 | 22 | type parser struct { 23 | peeker *scanner.Peeker 24 | } 25 | 26 | func (p *parser) ParseTopLevel() (ast.Node, error) { 27 | return p.parseInterpolationSeq(false) 28 | } 29 | 30 | func (p *parser) ParseQuoted() (ast.Node, error) { 31 | return p.parseInterpolationSeq(true) 32 | } 33 | 34 | // parseInterpolationSeq parses either the top-level sequence of literals 35 | // and interpolation expressions or a similar sequence within a quoted 36 | // string inside an interpolation expression. The latter case is requested 37 | // by setting 'quoted' to true. 38 | func (p *parser) parseInterpolationSeq(quoted bool) (ast.Node, error) { 39 | literalType := scanner.LITERAL 40 | endType := scanner.EOF 41 | if quoted { 42 | // exceptions for quoted sequences 43 | literalType = scanner.STRING 44 | endType = scanner.CQUOTE 45 | } 46 | 47 | startPos := p.peeker.Peek().Pos 48 | 49 | if quoted { 50 | tok := p.peeker.Read() 51 | if tok.Type != scanner.OQUOTE { 52 | return nil, ExpectationError("open quote", tok) 53 | } 54 | } 55 | 56 | var exprs []ast.Node 57 | for { 58 | tok := p.peeker.Read() 59 | 60 | if tok.Type == endType { 61 | break 62 | } 63 | 64 | switch tok.Type { 65 | case literalType: 66 | val, err := p.parseStringToken(tok) 67 | if err != nil { 68 | return nil, err 69 | } 70 | exprs = append(exprs, &ast.LiteralNode{ 71 | Value: val, 72 | Typex: ast.TypeString, 73 | Posx: tok.Pos, 74 | }) 75 | case scanner.BEGIN: 76 | expr, err := p.ParseInterpolation() 77 | if err != nil { 78 | return nil, err 79 | } 80 | exprs = append(exprs, expr) 81 | default: 82 | return nil, ExpectationError(`"${"`, tok) 83 | } 84 | } 85 | 86 | if len(exprs) == 0 { 87 | // If we have no parts at all then the input must've 88 | // been an empty string. 89 | exprs = append(exprs, &ast.LiteralNode{ 90 | Value: "", 91 | Typex: ast.TypeString, 92 | Posx: startPos, 93 | }) 94 | } 95 | 96 | // As a special case, if our "Output" contains only one expression 97 | // and it's a literal string then we'll hoist it up to be our 98 | // direct return value, so callers can easily recognize a string 99 | // that has no interpolations at all. 100 | if len(exprs) == 1 { 101 | if lit, ok := exprs[0].(*ast.LiteralNode); ok { 102 | if lit.Typex == ast.TypeString { 103 | return lit, nil 104 | } 105 | } 106 | } 107 | 108 | return &ast.Output{ 109 | Exprs: exprs, 110 | Posx: startPos, 111 | }, nil 112 | } 113 | 114 | // parseStringToken takes a token of either LITERAL or STRING type and 115 | // returns the interpreted string, after processing any relevant 116 | // escape sequences. 117 | func (p *parser) parseStringToken(tok *scanner.Token) (string, error) { 118 | var backslashes bool 119 | switch tok.Type { 120 | case scanner.LITERAL: 121 | backslashes = false 122 | case scanner.STRING: 123 | backslashes = true 124 | default: 125 | panic("unsupported string token type") 126 | } 127 | 128 | raw := []byte(tok.Content) 129 | buf := make([]byte, 0, len(raw)) 130 | 131 | for i := 0; i < len(raw); i++ { 132 | b := raw[i] 133 | more := len(raw) > (i + 1) 134 | 135 | if b == '$' { 136 | if more && raw[i+1] == '$' { 137 | // skip over the second dollar sign 138 | i++ 139 | } 140 | } else if backslashes && b == '\\' { 141 | if !more { 142 | return "", Errorf( 143 | ast.Pos{ 144 | Column: tok.Pos.Column + utf8.RuneCount(raw[:i]), 145 | Line: tok.Pos.Line, 146 | }, 147 | `unfinished backslash escape sequence`, 148 | ) 149 | } 150 | escapeType := raw[i+1] 151 | switch escapeType { 152 | case '\\': 153 | // skip over the second slash 154 | i++ 155 | case 'n': 156 | b = '\n' 157 | i++ 158 | case '"': 159 | b = '"' 160 | i++ 161 | default: 162 | return "", Errorf( 163 | ast.Pos{ 164 | Column: tok.Pos.Column + utf8.RuneCount(raw[:i]), 165 | Line: tok.Pos.Line, 166 | }, 167 | `invalid backslash escape sequence`, 168 | ) 169 | } 170 | } 171 | 172 | buf = append(buf, b) 173 | } 174 | 175 | return string(buf), nil 176 | } 177 | 178 | func (p *parser) ParseInterpolation() (ast.Node, error) { 179 | // By the time we're called, we're already "inside" the ${ sequence 180 | // because the caller consumed the ${ token. 181 | 182 | expr, err := p.ParseExpression() 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | err = p.requireTokenType(scanner.END, `"}"`) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | return expr, nil 193 | } 194 | 195 | func (p *parser) ParseExpression() (ast.Node, error) { 196 | return p.parseTernaryCond() 197 | } 198 | 199 | func (p *parser) parseTernaryCond() (ast.Node, error) { 200 | // The ternary condition operator (.. ? .. : ..) behaves somewhat 201 | // like a binary operator except that the "operator" is itself 202 | // an expression enclosed in two punctuation characters. 203 | // The middle expression is parsed as if the ? and : symbols 204 | // were parentheses. The "rhs" (the "false expression") is then 205 | // treated right-associatively so it behaves similarly to the 206 | // middle in terms of precedence. 207 | 208 | startPos := p.peeker.Peek().Pos 209 | 210 | var cond, trueExpr, falseExpr ast.Node 211 | var err error 212 | 213 | cond, err = p.parseBinaryOps(binaryOps) 214 | if err != nil { 215 | return nil, err 216 | } 217 | 218 | next := p.peeker.Peek() 219 | if next.Type != scanner.QUESTION { 220 | return cond, nil 221 | } 222 | 223 | p.peeker.Read() // eat question mark 224 | 225 | trueExpr, err = p.ParseExpression() 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | colon := p.peeker.Read() 231 | if colon.Type != scanner.COLON { 232 | return nil, ExpectationError(":", colon) 233 | } 234 | 235 | falseExpr, err = p.ParseExpression() 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | return &ast.Conditional{ 241 | CondExpr: cond, 242 | TrueExpr: trueExpr, 243 | FalseExpr: falseExpr, 244 | Posx: startPos, 245 | }, nil 246 | } 247 | 248 | // parseBinaryOps calls itself recursively to work through all of the 249 | // operator precedence groups, and then eventually calls ParseExpressionTerm 250 | // for each operand. 251 | func (p *parser) parseBinaryOps(ops []map[scanner.TokenType]ast.ArithmeticOp) (ast.Node, error) { 252 | if len(ops) == 0 { 253 | // We've run out of operators, so now we'll just try to parse a term. 254 | return p.ParseExpressionTerm() 255 | } 256 | 257 | thisLevel := ops[0] 258 | remaining := ops[1:] 259 | 260 | startPos := p.peeker.Peek().Pos 261 | 262 | var lhs, rhs ast.Node 263 | operator := ast.ArithmeticOpInvalid 264 | var err error 265 | 266 | // parse a term that might be the first operand of a binary 267 | // expression or it might just be a standalone term, but 268 | // we won't know until we've parsed it and can look ahead 269 | // to see if there's an operator token. 270 | lhs, err = p.parseBinaryOps(remaining) 271 | if err != nil { 272 | return nil, err 273 | } 274 | 275 | // We'll keep eating up arithmetic operators until we run 276 | // out, so that operators with the same precedence will combine in a 277 | // left-associative manner: 278 | // a+b+c => (a+b)+c, not a+(b+c) 279 | // 280 | // Should we later want to have right-associative operators, a way 281 | // to achieve that would be to call back up to ParseExpression here 282 | // instead of iteratively parsing only the remaining operators. 283 | for { 284 | next := p.peeker.Peek() 285 | var newOperator ast.ArithmeticOp 286 | var ok bool 287 | if newOperator, ok = thisLevel[next.Type]; !ok { 288 | break 289 | } 290 | 291 | // Are we extending an expression started on 292 | // the previous iteration? 293 | if operator != ast.ArithmeticOpInvalid { 294 | lhs = &ast.Arithmetic{ 295 | Op: operator, 296 | Exprs: []ast.Node{lhs, rhs}, 297 | Posx: startPos, 298 | } 299 | } 300 | 301 | operator = newOperator 302 | p.peeker.Read() // eat operator token 303 | rhs, err = p.parseBinaryOps(remaining) 304 | if err != nil { 305 | return nil, err 306 | } 307 | } 308 | 309 | if operator != ast.ArithmeticOpInvalid { 310 | return &ast.Arithmetic{ 311 | Op: operator, 312 | Exprs: []ast.Node{lhs, rhs}, 313 | Posx: startPos, 314 | }, nil 315 | } else { 316 | return lhs, nil 317 | } 318 | } 319 | 320 | func (p *parser) ParseExpressionTerm() (ast.Node, error) { 321 | 322 | next := p.peeker.Peek() 323 | 324 | switch next.Type { 325 | 326 | case scanner.OPAREN: 327 | p.peeker.Read() 328 | expr, err := p.ParseExpression() 329 | if err != nil { 330 | return nil, err 331 | } 332 | err = p.requireTokenType(scanner.CPAREN, `")"`) 333 | return expr, err 334 | 335 | case scanner.OQUOTE: 336 | return p.ParseQuoted() 337 | 338 | case scanner.INTEGER: 339 | tok := p.peeker.Read() 340 | val, err := strconv.Atoi(tok.Content) 341 | if err != nil { 342 | return nil, TokenErrorf(tok, "invalid integer: %s", err) 343 | } 344 | return &ast.LiteralNode{ 345 | Value: val, 346 | Typex: ast.TypeInt, 347 | Posx: tok.Pos, 348 | }, nil 349 | 350 | case scanner.FLOAT: 351 | tok := p.peeker.Read() 352 | val, err := strconv.ParseFloat(tok.Content, 64) 353 | if err != nil { 354 | return nil, TokenErrorf(tok, "invalid float: %s", err) 355 | } 356 | return &ast.LiteralNode{ 357 | Value: val, 358 | Typex: ast.TypeFloat, 359 | Posx: tok.Pos, 360 | }, nil 361 | 362 | case scanner.BOOL: 363 | tok := p.peeker.Read() 364 | // the scanner guarantees that tok.Content is either "true" or "false" 365 | var val bool 366 | if tok.Content[0] == 't' { 367 | val = true 368 | } else { 369 | val = false 370 | } 371 | return &ast.LiteralNode{ 372 | Value: val, 373 | Typex: ast.TypeBool, 374 | Posx: tok.Pos, 375 | }, nil 376 | 377 | case scanner.MINUS: 378 | opTok := p.peeker.Read() 379 | // important to use ParseExpressionTerm rather than ParseExpression 380 | // here, otherwise we can capture a following binary expression into 381 | // our negation. 382 | // e.g. -46+5 should parse as (0-46)+5, not 0-(46+5) 383 | operand, err := p.ParseExpressionTerm() 384 | if err != nil { 385 | return nil, err 386 | } 387 | // The AST currently represents negative numbers as 388 | // a binary subtraction of the number from zero. 389 | return &ast.Arithmetic{ 390 | Op: ast.ArithmeticOpSub, 391 | Exprs: []ast.Node{ 392 | &ast.LiteralNode{ 393 | Value: 0, 394 | Typex: ast.TypeInt, 395 | Posx: opTok.Pos, 396 | }, 397 | operand, 398 | }, 399 | Posx: opTok.Pos, 400 | }, nil 401 | 402 | case scanner.BANG: 403 | opTok := p.peeker.Read() 404 | // important to use ParseExpressionTerm rather than ParseExpression 405 | // here, otherwise we can capture a following binary expression into 406 | // our negation. 407 | operand, err := p.ParseExpressionTerm() 408 | if err != nil { 409 | return nil, err 410 | } 411 | // The AST currently represents binary negation as an equality 412 | // test with "false". 413 | return &ast.Arithmetic{ 414 | Op: ast.ArithmeticOpEqual, 415 | Exprs: []ast.Node{ 416 | &ast.LiteralNode{ 417 | Value: false, 418 | Typex: ast.TypeBool, 419 | Posx: opTok.Pos, 420 | }, 421 | operand, 422 | }, 423 | Posx: opTok.Pos, 424 | }, nil 425 | 426 | case scanner.IDENTIFIER: 427 | return p.ParseScopeInteraction() 428 | 429 | default: 430 | return nil, ExpectationError("expression", next) 431 | } 432 | } 433 | 434 | // ParseScopeInteraction parses the expression types that interact 435 | // with the evaluation scope: variable access, function calls, and 436 | // indexing. 437 | // 438 | // Indexing should actually be a distinct operator in its own right, 439 | // so that e.g. it can be applied to the result of a function call, 440 | // but for now we're preserving the behavior of the older yacc-based 441 | // parser. 442 | func (p *parser) ParseScopeInteraction() (ast.Node, error) { 443 | first := p.peeker.Read() 444 | startPos := first.Pos 445 | if first.Type != scanner.IDENTIFIER { 446 | return nil, ExpectationError("identifier", first) 447 | } 448 | 449 | next := p.peeker.Peek() 450 | if next.Type == scanner.OPAREN { 451 | // function call 452 | funcName := first.Content 453 | p.peeker.Read() // eat paren 454 | var args []ast.Node 455 | 456 | for p.peeker.Peek().Type != scanner.CPAREN { 457 | arg, err := p.ParseExpression() 458 | if err != nil { 459 | return nil, err 460 | } 461 | 462 | args = append(args, arg) 463 | 464 | if p.peeker.Peek().Type == scanner.COMMA { 465 | p.peeker.Read() // eat comma 466 | continue 467 | } else { 468 | break 469 | } 470 | } 471 | 472 | err := p.requireTokenType(scanner.CPAREN, `")"`) 473 | if err != nil { 474 | return nil, err 475 | } 476 | 477 | return &ast.Call{ 478 | Func: funcName, 479 | Args: args, 480 | Posx: startPos, 481 | }, nil 482 | } 483 | 484 | varNode := &ast.VariableAccess{ 485 | Name: first.Content, 486 | Posx: startPos, 487 | } 488 | 489 | if p.peeker.Peek().Type == scanner.OBRACKET { 490 | // index operator 491 | startPos := p.peeker.Read().Pos // eat bracket 492 | indexExpr, err := p.ParseExpression() 493 | if err != nil { 494 | return nil, err 495 | } 496 | err = p.requireTokenType(scanner.CBRACKET, `"]"`) 497 | if err != nil { 498 | return nil, err 499 | } 500 | return &ast.Index{ 501 | Target: varNode, 502 | Key: indexExpr, 503 | Posx: startPos, 504 | }, nil 505 | } 506 | 507 | return varNode, nil 508 | } 509 | 510 | // requireTokenType consumes the next token an returns an error if its 511 | // type does not match the given type. nil is returned if the type matches. 512 | // 513 | // This is a helper around peeker.Read() for situations where the parser just 514 | // wants to assert that a particular token type must be present. 515 | func (p *parser) requireTokenType(wantType scanner.TokenType, wantName string) error { 516 | token := p.peeker.Read() 517 | if token.Type != wantType { 518 | return ExpectationError(wantName, token) 519 | } 520 | return nil 521 | } 522 | -------------------------------------------------------------------------------- /eval.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/hashicorp/hil/ast" 12 | ) 13 | 14 | // EvalConfig is the configuration for evaluating. 15 | type EvalConfig struct { 16 | // GlobalScope is the global scope of execution for evaluation. 17 | GlobalScope *ast.BasicScope 18 | 19 | // SemanticChecks is a list of additional semantic checks that will be run 20 | // on the tree prior to evaluating it. The type checker, identifier checker, 21 | // etc. will be run before these automatically. 22 | SemanticChecks []SemanticChecker 23 | } 24 | 25 | // SemanticChecker is the type that must be implemented to do a 26 | // semantic check on an AST tree. This will be called with the root node. 27 | type SemanticChecker func(ast.Node) error 28 | 29 | // EvaluationResult is a struct returned from the hil.Eval function, 30 | // representing the result of an interpolation. Results are returned in their 31 | // "natural" Go structure rather than in terms of the HIL AST. For the types 32 | // currently implemented, this means that the Value field can be interpreted as 33 | // the following Go types: 34 | // 35 | // TypeInvalid: undefined 36 | // TypeString: string 37 | // TypeList: []interface{} 38 | // TypeMap: map[string]interface{} 39 | // TypBool: bool 40 | type EvaluationResult struct { 41 | Type EvalType 42 | Value interface{} 43 | } 44 | 45 | // InvalidResult is a structure representing the result of a HIL interpolation 46 | // which has invalid syntax, missing variables, or some other type of error. 47 | // The error is described out of band in the accompanying error return value. 48 | var InvalidResult = EvaluationResult{Type: TypeInvalid, Value: nil} 49 | 50 | // errExitUnknown is an internal error that when returned means the result 51 | // is an unknown value. We use this for early exit. 52 | var errExitUnknown = errors.New("unknown value") 53 | 54 | func Eval(root ast.Node, config *EvalConfig) (EvaluationResult, error) { 55 | output, outputType, err := internalEval(root, config) 56 | if err != nil { 57 | return InvalidResult, err 58 | } 59 | 60 | // If the result contains any nested unknowns then the result as a whole 61 | // is unknown, so that callers only have to deal with "entirely known" 62 | // or "entirely unknown" as outcomes. 63 | if ast.IsUnknown(ast.Variable{Type: outputType, Value: output}) { 64 | outputType = ast.TypeUnknown 65 | output = UnknownValue 66 | } 67 | 68 | switch outputType { 69 | case ast.TypeList: 70 | val, err := VariableToInterface(ast.Variable{ 71 | Type: ast.TypeList, 72 | Value: output, 73 | }) 74 | return EvaluationResult{ 75 | Type: TypeList, 76 | Value: val, 77 | }, err 78 | case ast.TypeMap: 79 | val, err := VariableToInterface(ast.Variable{ 80 | Type: ast.TypeMap, 81 | Value: output, 82 | }) 83 | return EvaluationResult{ 84 | Type: TypeMap, 85 | Value: val, 86 | }, err 87 | case ast.TypeString: 88 | return EvaluationResult{ 89 | Type: TypeString, 90 | Value: output, 91 | }, nil 92 | case ast.TypeBool: 93 | return EvaluationResult{ 94 | Type: TypeBool, 95 | Value: output, 96 | }, nil 97 | case ast.TypeUnknown: 98 | return EvaluationResult{ 99 | Type: TypeUnknown, 100 | Value: UnknownValue, 101 | }, nil 102 | default: 103 | return InvalidResult, fmt.Errorf("unknown type %s as interpolation output", outputType) 104 | } 105 | } 106 | 107 | // Eval evaluates the given AST tree and returns its output value, the type 108 | // of the output, and any error that occurred. 109 | func internalEval(root ast.Node, config *EvalConfig) (interface{}, ast.Type, error) { 110 | // Copy the scope so we can add our builtins 111 | if config == nil { 112 | config = new(EvalConfig) 113 | } 114 | scope := registerBuiltins(config.GlobalScope) 115 | implicitMap := map[ast.Type]map[ast.Type]string{ 116 | ast.TypeFloat: { 117 | ast.TypeInt: "__builtin_FloatToInt", 118 | ast.TypeString: "__builtin_FloatToString", 119 | }, 120 | ast.TypeInt: { 121 | ast.TypeFloat: "__builtin_IntToFloat", 122 | ast.TypeString: "__builtin_IntToString", 123 | }, 124 | ast.TypeString: { 125 | ast.TypeInt: "__builtin_StringToInt", 126 | ast.TypeFloat: "__builtin_StringToFloat", 127 | ast.TypeBool: "__builtin_StringToBool", 128 | }, 129 | ast.TypeBool: { 130 | ast.TypeString: "__builtin_BoolToString", 131 | }, 132 | } 133 | 134 | // Build our own semantic checks that we always run 135 | tv := &TypeCheck{Scope: scope, Implicit: implicitMap} 136 | ic := &IdentifierCheck{Scope: scope} 137 | 138 | // Build up the semantic checks for execution 139 | checks := make( 140 | []SemanticChecker, 141 | len(config.SemanticChecks), 142 | len(config.SemanticChecks)+2) 143 | copy(checks, config.SemanticChecks) 144 | checks = append(checks, ic.Visit) 145 | checks = append(checks, tv.Visit) 146 | 147 | // Run the semantic checks 148 | for _, check := range checks { 149 | if err := check(root); err != nil { 150 | return nil, ast.TypeInvalid, err 151 | } 152 | } 153 | 154 | // Execute 155 | v := &evalVisitor{Scope: scope} 156 | return v.Visit(root) 157 | } 158 | 159 | // EvalNode is the interface that must be implemented by any ast.Node 160 | // to support evaluation. This will be called in visitor pattern order. 161 | // The result of each call to Eval is automatically pushed onto the 162 | // stack as a LiteralNode. Pop elements off the stack to get child 163 | // values. 164 | type EvalNode interface { 165 | Eval(ast.Scope, *ast.Stack) (interface{}, ast.Type, error) 166 | } 167 | 168 | type evalVisitor struct { 169 | Scope ast.Scope 170 | Stack ast.Stack 171 | 172 | err error 173 | } 174 | 175 | func (v *evalVisitor) Visit(root ast.Node) (interface{}, ast.Type, error) { 176 | // Run the actual visitor pattern 177 | root.Accept(v.visit) 178 | 179 | // Get our result and clear out everything else 180 | var result *ast.LiteralNode 181 | if v.Stack.Len() > 0 { 182 | result = v.Stack.Pop().(*ast.LiteralNode) 183 | } else { 184 | result = new(ast.LiteralNode) 185 | } 186 | resultErr := v.err 187 | if resultErr == errExitUnknown { 188 | // This means the return value is unknown and we used the error 189 | // as an early exit mechanism. Reset since the value on the stack 190 | // should be the unknown value. 191 | resultErr = nil 192 | } 193 | 194 | // Clear everything else so we aren't just dangling 195 | v.Stack.Reset() 196 | v.err = nil 197 | 198 | t, err := result.Type(v.Scope) 199 | if err != nil { 200 | return nil, ast.TypeInvalid, err 201 | } 202 | 203 | return result.Value, t, resultErr 204 | } 205 | 206 | func (v *evalVisitor) visit(raw ast.Node) ast.Node { 207 | if v.err != nil { 208 | return raw 209 | } 210 | 211 | en, err := evalNode(raw) 212 | if err != nil { 213 | v.err = err 214 | return raw 215 | } 216 | 217 | out, outType, err := en.Eval(v.Scope, &v.Stack) 218 | if err != nil { 219 | v.err = err 220 | return raw 221 | } 222 | 223 | v.Stack.Push(&ast.LiteralNode{ 224 | Value: out, 225 | Typex: outType, 226 | }) 227 | 228 | if outType == ast.TypeUnknown { 229 | // Halt immediately 230 | v.err = errExitUnknown 231 | return raw 232 | } 233 | 234 | return raw 235 | } 236 | 237 | // evalNode is a private function that returns an EvalNode for built-in 238 | // types as well as any other EvalNode implementations. 239 | func evalNode(raw ast.Node) (EvalNode, error) { 240 | switch n := raw.(type) { 241 | case *ast.Index: 242 | return &evalIndex{n}, nil 243 | case *ast.Call: 244 | return &evalCall{n}, nil 245 | case *ast.Conditional: 246 | return &evalConditional{n}, nil 247 | case *ast.Output: 248 | return &evalOutput{n}, nil 249 | case *ast.LiteralNode: 250 | return &evalLiteralNode{n}, nil 251 | case *ast.VariableAccess: 252 | return &evalVariableAccess{n}, nil 253 | default: 254 | en, ok := n.(EvalNode) 255 | if !ok { 256 | return nil, fmt.Errorf("node doesn't support evaluation: %#v", raw) 257 | } 258 | 259 | return en, nil 260 | } 261 | } 262 | 263 | type evalCall struct{ *ast.Call } 264 | 265 | func (v *evalCall) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { 266 | // Look up the function in the map 267 | function, ok := s.LookupFunc(v.Func) 268 | if !ok { 269 | return nil, ast.TypeInvalid, fmt.Errorf( 270 | "unknown function called: %s", v.Func) 271 | } 272 | 273 | // The arguments are on the stack in reverse order, so pop them off. 274 | args := make([]interface{}, len(v.Args)) 275 | for i := range v.Args { 276 | node := stack.Pop().(*ast.LiteralNode) 277 | if node.IsUnknown() { 278 | // If any arguments are unknown then the result is automatically unknown 279 | return UnknownValue, ast.TypeUnknown, nil 280 | } 281 | args[len(v.Args)-1-i] = node.Value 282 | } 283 | 284 | // Call the function 285 | result, err := function.Callback(args) 286 | if err != nil { 287 | return nil, ast.TypeInvalid, fmt.Errorf("%s: %s", v.Func, err) 288 | } 289 | 290 | return result, function.ReturnType, nil 291 | } 292 | 293 | type evalConditional struct{ *ast.Conditional } 294 | 295 | func (v *evalConditional) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { 296 | // On the stack we have literal nodes representing the resulting values 297 | // of the condition, true and false expressions, but they are in reverse 298 | // order. 299 | falseLit := stack.Pop().(*ast.LiteralNode) 300 | trueLit := stack.Pop().(*ast.LiteralNode) 301 | condLit := stack.Pop().(*ast.LiteralNode) 302 | 303 | if condLit.IsUnknown() { 304 | // If our conditional is unknown then our result is also unknown 305 | return UnknownValue, ast.TypeUnknown, nil 306 | } 307 | 308 | if condLit.Value.(bool) { 309 | return trueLit.Value, trueLit.Typex, nil 310 | } else { 311 | return falseLit.Value, trueLit.Typex, nil 312 | } 313 | } 314 | 315 | type evalIndex struct{ *ast.Index } 316 | 317 | func (v *evalIndex) Eval(scope ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { 318 | key := stack.Pop().(*ast.LiteralNode) 319 | target := stack.Pop().(*ast.LiteralNode) 320 | 321 | variableName := v.Index.Target.(*ast.VariableAccess).Name 322 | 323 | if key.IsUnknown() { 324 | // If our key is unknown then our result is also unknown 325 | return UnknownValue, ast.TypeUnknown, nil 326 | } 327 | 328 | // For target, we'll accept collections containing unknown values but 329 | // we still need to catch when the collection itself is unknown, shallowly. 330 | if target.Typex == ast.TypeUnknown { 331 | return UnknownValue, ast.TypeUnknown, nil 332 | } 333 | 334 | switch target.Typex { 335 | case ast.TypeList: 336 | return v.evalListIndex(variableName, target.Value, key.Value) 337 | case ast.TypeMap: 338 | return v.evalMapIndex(variableName, target.Value, key.Value) 339 | default: 340 | return nil, ast.TypeInvalid, fmt.Errorf( 341 | "target %q for indexing must be ast.TypeList or ast.TypeMap, is %s", 342 | variableName, target.Typex) 343 | } 344 | } 345 | 346 | func (v *evalIndex) evalListIndex(variableName string, target interface{}, key interface{}) (interface{}, ast.Type, error) { 347 | // We assume type checking was already done and we can assume that target 348 | // is a list and key is an int 349 | list, ok := target.([]ast.Variable) 350 | if !ok { 351 | return nil, ast.TypeInvalid, fmt.Errorf( 352 | "cannot cast target to []Variable, is: %T", target) 353 | } 354 | 355 | keyInt, ok := key.(int) 356 | if !ok { 357 | return nil, ast.TypeInvalid, fmt.Errorf( 358 | "cannot cast key to int, is: %T", key) 359 | } 360 | 361 | if len(list) == 0 { 362 | return nil, ast.TypeInvalid, fmt.Errorf("list is empty") 363 | } 364 | 365 | if keyInt < 0 || len(list) < keyInt+1 { 366 | return nil, ast.TypeInvalid, fmt.Errorf( 367 | "index %d out of range for list %s (max %d)", 368 | keyInt, variableName, len(list)) 369 | } 370 | 371 | returnVal := list[keyInt].Value 372 | returnType := list[keyInt].Type 373 | return returnVal, returnType, nil 374 | } 375 | 376 | func (v *evalIndex) evalMapIndex(variableName string, target interface{}, key interface{}) (interface{}, ast.Type, error) { 377 | // We assume type checking was already done and we can assume that target 378 | // is a map and key is a string 379 | vmap, ok := target.(map[string]ast.Variable) 380 | if !ok { 381 | return nil, ast.TypeInvalid, fmt.Errorf( 382 | "cannot cast target to map[string]Variable, is: %T", target) 383 | } 384 | 385 | keyString, ok := key.(string) 386 | if !ok { 387 | return nil, ast.TypeInvalid, fmt.Errorf( 388 | "cannot cast key to string, is: %T", key) 389 | } 390 | 391 | if len(vmap) == 0 { 392 | return nil, ast.TypeInvalid, fmt.Errorf("map is empty") 393 | } 394 | 395 | value, ok := vmap[keyString] 396 | if !ok { 397 | return nil, ast.TypeInvalid, fmt.Errorf( 398 | "key %q does not exist in map %s", keyString, variableName) 399 | } 400 | 401 | return value.Value, value.Type, nil 402 | } 403 | 404 | type evalOutput struct{ *ast.Output } 405 | 406 | func (v *evalOutput) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { 407 | // The expressions should all be on the stack in reverse 408 | // order. So pop them off, reverse their order, and concatenate. 409 | nodes := make([]*ast.LiteralNode, 0, len(v.Exprs)) 410 | haveUnknown := false 411 | for range v.Exprs { 412 | n := stack.Pop().(*ast.LiteralNode) 413 | nodes = append(nodes, n) 414 | 415 | // If we have any unknowns then the whole result is unknown 416 | // (we must deal with this first, because the type checker can 417 | // skip type conversions in the presence of unknowns, and thus 418 | // any of our other nodes may be incorrectly typed.) 419 | if n.IsUnknown() { 420 | haveUnknown = true 421 | } 422 | } 423 | 424 | if haveUnknown { 425 | return UnknownValue, ast.TypeUnknown, nil 426 | } 427 | 428 | // Special case the single list and map 429 | if len(nodes) == 1 { 430 | switch t := nodes[0].Typex; t { 431 | case ast.TypeList: 432 | fallthrough 433 | case ast.TypeMap: 434 | fallthrough 435 | case ast.TypeUnknown: 436 | return nodes[0].Value, t, nil 437 | } 438 | } 439 | 440 | // Otherwise concatenate the strings 441 | var buf bytes.Buffer 442 | for i := len(nodes) - 1; i >= 0; i-- { 443 | if nodes[i].Typex != ast.TypeString { 444 | return nil, ast.TypeInvalid, fmt.Errorf( 445 | "invalid output with %s value at index %d: %#v", 446 | nodes[i].Typex, 447 | i, 448 | nodes[i].Value, 449 | ) 450 | } 451 | buf.WriteString(nodes[i].Value.(string)) 452 | } 453 | 454 | return buf.String(), ast.TypeString, nil 455 | } 456 | 457 | type evalLiteralNode struct{ *ast.LiteralNode } 458 | 459 | func (v *evalLiteralNode) Eval(ast.Scope, *ast.Stack) (interface{}, ast.Type, error) { 460 | return v.Value, v.Typex, nil 461 | } 462 | 463 | type evalVariableAccess struct{ *ast.VariableAccess } 464 | 465 | func (v *evalVariableAccess) Eval(scope ast.Scope, _ *ast.Stack) (interface{}, ast.Type, error) { 466 | // Look up the variable in the map 467 | variable, ok := scope.LookupVar(v.Name) 468 | if !ok { 469 | return nil, ast.TypeInvalid, fmt.Errorf( 470 | "unknown variable accessed: %s", v.Name) 471 | } 472 | 473 | return variable.Value, variable.Type, nil 474 | } 475 | -------------------------------------------------------------------------------- /scanner/scanner.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package scanner 5 | 6 | import ( 7 | "unicode" 8 | "unicode/utf8" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | ) 12 | 13 | // Scan returns a channel that recieves Tokens from the given input string. 14 | // 15 | // The scanner's job is just to partition the string into meaningful parts. 16 | // It doesn't do any transformation of the raw input string, so the caller 17 | // must deal with any further interpretation required, such as parsing INTEGER 18 | // tokens into real ints, or dealing with escape sequences in LITERAL or 19 | // STRING tokens. 20 | // 21 | // Strings in the returned tokens are slices from the original string. 22 | // 23 | // startPos should be set to ast.InitPos unless the caller knows that 24 | // this interpolation string is part of a larger file and knows the position 25 | // of the first character in that larger file. 26 | func Scan(s string, startPos ast.Pos) <-chan *Token { 27 | ch := make(chan *Token) 28 | go scan(s, ch, startPos) 29 | return ch 30 | } 31 | 32 | func scan(s string, ch chan<- *Token, pos ast.Pos) { 33 | // 'remain' starts off as the whole string but we gradually 34 | // slice of the front of it as we work our way through. 35 | remain := s 36 | 37 | // nesting keeps track of how many ${ .. } sequences we are 38 | // inside, so we can recognize the minor differences in syntax 39 | // between outer string literals (LITERAL tokens) and quoted 40 | // string literals (STRING tokens). 41 | nesting := 0 42 | 43 | // We're going to flip back and forth between parsing literals/strings 44 | // and parsing interpolation sequences ${ .. } until we reach EOF or 45 | // some INVALID token. 46 | All: 47 | for { 48 | startPos := pos 49 | // Literal string processing first, since the beginning of 50 | // a string is always outside of an interpolation sequence. 51 | literalVal, terminator := scanLiteral(remain, pos, nesting > 0) 52 | 53 | if len(literalVal) > 0 { 54 | litType := LITERAL 55 | if nesting > 0 { 56 | litType = STRING 57 | } 58 | ch <- &Token{ 59 | Type: litType, 60 | Content: literalVal, 61 | Pos: startPos, 62 | } 63 | remain = remain[len(literalVal):] 64 | } 65 | 66 | ch <- terminator 67 | remain = remain[len(terminator.Content):] 68 | pos = terminator.Pos 69 | // Safe to use len() here because none of the terminator tokens 70 | // can contain UTF-8 sequences. 71 | pos.Column = pos.Column + len(terminator.Content) 72 | 73 | switch terminator.Type { 74 | case INVALID: 75 | // Synthetic EOF after invalid token, since further scanning 76 | // is likely to just produce more garbage. 77 | ch <- &Token{ 78 | Type: EOF, 79 | Content: "", 80 | Pos: pos, 81 | } 82 | break All 83 | case EOF: 84 | // All done! 85 | break All 86 | case BEGIN: 87 | nesting++ 88 | case CQUOTE: 89 | // nothing special to do 90 | default: 91 | // Should never happen 92 | panic("invalid string/literal terminator") 93 | } 94 | 95 | // Now we do the processing of the insides of ${ .. } sequences. 96 | // This loop terminates when we encounter either a closing } or 97 | // an opening ", which will cause us to return to literal processing. 98 | Interpolation: 99 | for { 100 | 101 | token, size, newPos := scanInterpolationToken(remain, pos) 102 | ch <- token 103 | remain = remain[size:] 104 | pos = newPos 105 | 106 | switch token.Type { 107 | case INVALID: 108 | // Synthetic EOF after invalid token, since further scanning 109 | // is likely to just produce more garbage. 110 | ch <- &Token{ 111 | Type: EOF, 112 | Content: "", 113 | Pos: pos, 114 | } 115 | break All 116 | case EOF: 117 | // All done 118 | // (though a syntax error that we'll catch in the parser) 119 | break All 120 | case END: 121 | nesting-- 122 | if nesting < 0 { 123 | // Can happen if there are unbalanced ${ and } sequences 124 | // in the input, which we'll catch in the parser. 125 | nesting = 0 126 | } 127 | break Interpolation 128 | case OQUOTE: 129 | // Beginning of nested quoted string 130 | break Interpolation 131 | } 132 | } 133 | } 134 | 135 | close(ch) 136 | } 137 | 138 | // Returns the token found at the start of the given string, followed by 139 | // the number of bytes that were consumed from the string and the adjusted 140 | // source position. 141 | // 142 | // Note that the number of bytes consumed can be more than the length of 143 | // the returned token contents if the string begins with whitespace, since 144 | // it will be silently consumed before reading the token. 145 | func scanInterpolationToken(s string, startPos ast.Pos) (*Token, int, ast.Pos) { 146 | pos := startPos 147 | size := 0 148 | 149 | // Consume whitespace, if any 150 | for len(s) > 0 && byteIsSpace(s[0]) { 151 | if s[0] == '\n' { 152 | pos.Column = 1 153 | pos.Line++ 154 | } else { 155 | pos.Column++ 156 | } 157 | size++ 158 | s = s[1:] 159 | } 160 | 161 | // Unexpected EOF during sequence 162 | if len(s) == 0 { 163 | return &Token{ 164 | Type: EOF, 165 | Content: "", 166 | Pos: pos, 167 | }, size, pos 168 | } 169 | 170 | next := s[0] 171 | var token *Token 172 | 173 | switch next { 174 | case '(', ')', '[', ']', ',', '.', '+', '-', '*', '/', '%', '?', ':': 175 | // Easy punctuation symbols that don't have any special meaning 176 | // during scanning, and that stand for themselves in the 177 | // TokenType enumeration. 178 | token = &Token{ 179 | Type: TokenType(next), 180 | Content: s[:1], 181 | Pos: pos, 182 | } 183 | case '}': 184 | token = &Token{ 185 | Type: END, 186 | Content: s[:1], 187 | Pos: pos, 188 | } 189 | case '"': 190 | token = &Token{ 191 | Type: OQUOTE, 192 | Content: s[:1], 193 | Pos: pos, 194 | } 195 | case '!': 196 | if len(s) >= 2 && s[:2] == "!=" { 197 | token = &Token{ 198 | Type: NOTEQUAL, 199 | Content: s[:2], 200 | Pos: pos, 201 | } 202 | } else { 203 | token = &Token{ 204 | Type: BANG, 205 | Content: s[:1], 206 | Pos: pos, 207 | } 208 | } 209 | case '<': 210 | if len(s) >= 2 && s[:2] == "<=" { 211 | token = &Token{ 212 | Type: LTE, 213 | Content: s[:2], 214 | Pos: pos, 215 | } 216 | } else { 217 | token = &Token{ 218 | Type: LT, 219 | Content: s[:1], 220 | Pos: pos, 221 | } 222 | } 223 | case '>': 224 | if len(s) >= 2 && s[:2] == ">=" { 225 | token = &Token{ 226 | Type: GTE, 227 | Content: s[:2], 228 | Pos: pos, 229 | } 230 | } else { 231 | token = &Token{ 232 | Type: GT, 233 | Content: s[:1], 234 | Pos: pos, 235 | } 236 | } 237 | case '=': 238 | if len(s) >= 2 && s[:2] == "==" { 239 | token = &Token{ 240 | Type: EQUAL, 241 | Content: s[:2], 242 | Pos: pos, 243 | } 244 | } else { 245 | // A single equals is not a valid operator 246 | token = &Token{ 247 | Type: INVALID, 248 | Content: s[:1], 249 | Pos: pos, 250 | } 251 | } 252 | case '&': 253 | if len(s) >= 2 && s[:2] == "&&" { 254 | token = &Token{ 255 | Type: AND, 256 | Content: s[:2], 257 | Pos: pos, 258 | } 259 | } else { 260 | token = &Token{ 261 | Type: INVALID, 262 | Content: s[:1], 263 | Pos: pos, 264 | } 265 | } 266 | case '|': 267 | if len(s) >= 2 && s[:2] == "||" { 268 | token = &Token{ 269 | Type: OR, 270 | Content: s[:2], 271 | Pos: pos, 272 | } 273 | } else { 274 | token = &Token{ 275 | Type: INVALID, 276 | Content: s[:1], 277 | Pos: pos, 278 | } 279 | } 280 | default: 281 | if next >= '0' && next <= '9' { 282 | num, numType := scanNumber(s) 283 | token = &Token{ 284 | Type: numType, 285 | Content: num, 286 | Pos: pos, 287 | } 288 | } else if stringStartsWithIdentifier(s) { 289 | ident, runeLen := scanIdentifier(s) 290 | tokenType := IDENTIFIER 291 | if ident == "true" || ident == "false" { 292 | tokenType = BOOL 293 | } 294 | token = &Token{ 295 | Type: tokenType, 296 | Content: ident, 297 | Pos: pos, 298 | } 299 | // Skip usual token handling because it doesn't 300 | // know how to deal with UTF-8 sequences. 301 | pos.Column = pos.Column + runeLen 302 | return token, size + len(ident), pos 303 | } else { 304 | _, byteLen := utf8.DecodeRuneInString(s) 305 | token = &Token{ 306 | Type: INVALID, 307 | Content: s[:byteLen], 308 | Pos: pos, 309 | } 310 | // Skip usual token handling because it doesn't 311 | // know how to deal with UTF-8 sequences. 312 | pos.Column = pos.Column + 1 313 | return token, size + byteLen, pos 314 | } 315 | } 316 | 317 | // Here we assume that the token content contains no UTF-8 sequences, 318 | // because we dealt with UTF-8 characters as a special case where 319 | // necessary above. 320 | size = size + len(token.Content) 321 | pos.Column = pos.Column + len(token.Content) 322 | 323 | return token, size, pos 324 | } 325 | 326 | // Returns the (possibly-empty) prefix of the given string that represents 327 | // a literal, followed by the token that marks the end of the literal. 328 | func scanLiteral(s string, startPos ast.Pos, nested bool) (string, *Token) { 329 | litLen := 0 330 | pos := startPos 331 | var terminator *Token 332 | for { 333 | 334 | if litLen >= len(s) { 335 | if nested { 336 | // We've ended in the middle of a quoted string, 337 | // which means this token is actually invalid. 338 | return "", &Token{ 339 | Type: INVALID, 340 | Content: s, 341 | Pos: startPos, 342 | } 343 | } 344 | terminator = &Token{ 345 | Type: EOF, 346 | Content: "", 347 | Pos: pos, 348 | } 349 | break 350 | } 351 | 352 | next := s[litLen] 353 | 354 | if next == '$' && len(s) > litLen+1 { 355 | follow := s[litLen+1] 356 | 357 | if follow == '{' { 358 | terminator = &Token{ 359 | Type: BEGIN, 360 | Content: s[litLen : litLen+2], 361 | Pos: pos, 362 | } 363 | pos.Column = pos.Column + 2 364 | break 365 | } else if follow == '$' { 366 | // Double-$ escapes the special processing of $, 367 | // so we will consume both characters here. 368 | pos.Column = pos.Column + 2 369 | litLen = litLen + 2 370 | continue 371 | } 372 | } 373 | 374 | // special handling that applies only to quoted strings 375 | if nested { 376 | if next == '"' { 377 | terminator = &Token{ 378 | Type: CQUOTE, 379 | Content: s[litLen : litLen+1], 380 | Pos: pos, 381 | } 382 | pos.Column = pos.Column + 1 383 | break 384 | } 385 | 386 | // Escaped quote marks do not terminate the string. 387 | // 388 | // All we do here in the scanner is avoid terminating a string 389 | // due to an escaped quote. The parser is responsible for the 390 | // full handling of escape sequences, since it's able to produce 391 | // better error messages than we can produce in here. 392 | if next == '\\' && len(s) > litLen+1 { 393 | follow := s[litLen+1] 394 | 395 | if follow == '"' { 396 | // \" escapes the special processing of ", 397 | // so we will consume both characters here. 398 | pos.Column = pos.Column + 2 399 | litLen = litLen + 2 400 | continue 401 | } else if follow == '\\' { 402 | // \\ escapes \ 403 | // so we will consume both characters here. 404 | pos.Column = pos.Column + 2 405 | litLen = litLen + 2 406 | continue 407 | } 408 | } 409 | } 410 | 411 | if next == '\n' { 412 | pos.Column = 1 413 | pos.Line++ 414 | litLen++ 415 | } else { 416 | pos.Column++ 417 | 418 | // "Column" measures runes, so we need to actually consume 419 | // a valid UTF-8 character here. 420 | _, size := utf8.DecodeRuneInString(s[litLen:]) 421 | litLen = litLen + size 422 | } 423 | 424 | } 425 | 426 | return s[:litLen], terminator 427 | } 428 | 429 | // scanNumber returns the extent of the prefix of the string that represents 430 | // a valid number, along with what type of number it represents: INT or FLOAT. 431 | // 432 | // scanNumber does only basic character analysis: numbers consist of digits 433 | // and periods, with at least one period signalling a FLOAT. It's the parser's 434 | // responsibility to validate the form and range of the number, such as ensuring 435 | // that a FLOAT actually contains only one period, etc. 436 | func scanNumber(s string) (string, TokenType) { 437 | period := -1 438 | byteLen := 0 439 | numType := INTEGER 440 | for byteLen < len(s) { 441 | next := s[byteLen] 442 | if next != '.' && (next < '0' || next > '9') { 443 | // If our last value was a period, then we're not a float, 444 | // we're just an integer that ends in a period. 445 | if period == byteLen-1 { 446 | byteLen-- 447 | numType = INTEGER 448 | } 449 | 450 | break 451 | } 452 | 453 | if next == '.' { 454 | // If we've already seen a period, break out 455 | if period >= 0 { 456 | break 457 | } 458 | 459 | period = byteLen 460 | numType = FLOAT 461 | } 462 | 463 | byteLen++ 464 | } 465 | 466 | return s[:byteLen], numType 467 | } 468 | 469 | // scanIdentifier returns the extent of the prefix of the string that 470 | // represents a valid identifier, along with the length of that prefix 471 | // in runes. 472 | // 473 | // Identifiers may contain utf8-encoded non-Latin letters, which will 474 | // cause the returned "rune length" to be shorter than the byte length 475 | // of the returned string. 476 | func scanIdentifier(s string) (string, int) { 477 | byteLen := 0 478 | runeLen := 0 479 | for byteLen < len(s) { 480 | 481 | nextRune, size := utf8.DecodeRuneInString(s[byteLen:]) 482 | if nextRune != '_' && 483 | nextRune != '-' && 484 | nextRune != '.' && 485 | nextRune != '*' && 486 | !unicode.IsNumber(nextRune) && 487 | !unicode.IsLetter(nextRune) && 488 | !unicode.IsMark(nextRune) { 489 | break 490 | } 491 | 492 | // If we reach a star, it must be between periods to be part 493 | // of the same identifier. 494 | if nextRune == '*' && s[byteLen-1] != '.' { 495 | break 496 | } 497 | 498 | // If our previous character was a star, then the current must 499 | // be period. Otherwise, undo that and exit. 500 | if byteLen > 0 && s[byteLen-1] == '*' && nextRune != '.' { 501 | byteLen-- 502 | if s[byteLen-1] == '.' { 503 | byteLen-- 504 | } 505 | 506 | break 507 | } 508 | 509 | byteLen = byteLen + size 510 | runeLen = runeLen + 1 511 | } 512 | 513 | return s[:byteLen], runeLen 514 | } 515 | 516 | // byteIsSpace implements a restrictive interpretation of spaces that includes 517 | // only what's valid inside interpolation sequences: spaces, tabs, newlines. 518 | func byteIsSpace(b byte) bool { 519 | switch b { 520 | case ' ', '\t', '\r', '\n': 521 | return true 522 | default: 523 | return false 524 | } 525 | } 526 | 527 | // stringStartsWithIdentifier returns true if the given string begins with 528 | // a character that is a legal start of an identifier: an underscore or 529 | // any character that Unicode considers to be a letter. 530 | func stringStartsWithIdentifier(s string) bool { 531 | if len(s) == 0 { 532 | return false 533 | } 534 | 535 | first := s[0] 536 | 537 | // Easy ASCII cases first 538 | if (first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_' { 539 | return true 540 | } 541 | 542 | // If our first byte begins a UTF-8 sequence then the sequence might 543 | // be a unicode letter. 544 | if utf8.RuneStart(first) { 545 | firstRune, _ := utf8.DecodeRuneInString(s) 546 | if unicode.IsLetter(firstRune) { 547 | return true 548 | } 549 | } 550 | 551 | return false 552 | } 553 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright IBM Corp. 2015, 2025 2 | 3 | Mozilla Public License, version 2.0 4 | 5 | 1. Definitions 6 | 7 | 1.1. “Contributor” 8 | 9 | means each individual or legal entity that creates, contributes to the 10 | creation of, or owns Covered Software. 11 | 12 | 1.2. “Contributor Version” 13 | 14 | means the combination of the Contributions of others (if any) used by a 15 | Contributor and that particular Contributor’s Contribution. 16 | 17 | 1.3. “Contribution” 18 | 19 | means Covered Software of a particular Contributor. 20 | 21 | 1.4. “Covered Software” 22 | 23 | means Source Code Form to which the initial Contributor has attached the 24 | notice in Exhibit A, the Executable Form of such Source Code Form, and 25 | Modifications of such Source Code Form, in each case including portions 26 | thereof. 27 | 28 | 1.5. “Incompatible With Secondary Licenses” 29 | means 30 | 31 | a. that the initial Contributor has attached the notice described in 32 | Exhibit B to the Covered Software; or 33 | 34 | b. that the Covered Software was made available under the terms of version 35 | 1.1 or earlier of the License, but not also under the terms of a 36 | Secondary License. 37 | 38 | 1.6. “Executable Form” 39 | 40 | means any form of the work other than Source Code Form. 41 | 42 | 1.7. “Larger Work” 43 | 44 | means a work that combines Covered Software with other material, in a separate 45 | file or files, that is not Covered Software. 46 | 47 | 1.8. “License” 48 | 49 | means this document. 50 | 51 | 1.9. “Licensable” 52 | 53 | means having the right to grant, to the maximum extent possible, whether at the 54 | time of the initial grant or subsequently, any and all of the rights conveyed by 55 | this License. 56 | 57 | 1.10. “Modifications” 58 | 59 | means any of the following: 60 | 61 | a. any file in Source Code Form that results from an addition to, deletion 62 | from, or modification of the contents of Covered Software; or 63 | 64 | b. any new file in Source Code Form that contains any Covered Software. 65 | 66 | 1.11. “Patent Claims” of a Contributor 67 | 68 | means any patent claim(s), including without limitation, method, process, 69 | and apparatus claims, in any patent Licensable by such Contributor that 70 | would be infringed, but for the grant of the License, by the making, 71 | using, selling, offering for sale, having made, import, or transfer of 72 | either its Contributions or its Contributor Version. 73 | 74 | 1.12. “Secondary License” 75 | 76 | means either the GNU General Public License, Version 2.0, the GNU Lesser 77 | General Public License, Version 2.1, the GNU Affero General Public 78 | License, Version 3.0, or any later versions of those licenses. 79 | 80 | 1.13. “Source Code Form” 81 | 82 | means the form of the work preferred for making modifications. 83 | 84 | 1.14. “You” (or “Your”) 85 | 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, “You” includes any entity that controls, is 88 | controlled by, or is under common control with You. For purposes of this 89 | definition, “control” means (a) the power, direct or indirect, to cause 90 | the direction or management of such entity, whether by contract or 91 | otherwise, or (b) ownership of more than fifty percent (50%) of the 92 | outstanding shares or beneficial ownership of such entity. 93 | 94 | 95 | 2. License Grants and Conditions 96 | 97 | 2.1. Grants 98 | 99 | Each Contributor hereby grants You a world-wide, royalty-free, 100 | non-exclusive license: 101 | 102 | a. under intellectual property rights (other than patent or trademark) 103 | Licensable by such Contributor to use, reproduce, make available, 104 | modify, display, perform, distribute, and otherwise exploit its 105 | Contributions, either on an unmodified basis, with Modifications, or as 106 | part of a Larger Work; and 107 | 108 | b. under Patent Claims of such Contributor to make, use, sell, offer for 109 | sale, have made, import, and otherwise transfer either its Contributions 110 | or its Contributor Version. 111 | 112 | 2.2. Effective Date 113 | 114 | The licenses granted in Section 2.1 with respect to any Contribution become 115 | effective for each Contribution on the date the Contributor first distributes 116 | such Contribution. 117 | 118 | 2.3. Limitations on Grant Scope 119 | 120 | The licenses granted in this Section 2 are the only rights granted under this 121 | License. No additional rights or licenses will be implied from the distribution 122 | or licensing of Covered Software under this License. Notwithstanding Section 123 | 2.1(b) above, no patent license is granted by a Contributor: 124 | 125 | a. for any code that a Contributor has removed from Covered Software; or 126 | 127 | b. for infringements caused by: (i) Your and any other third party’s 128 | modifications of Covered Software, or (ii) the combination of its 129 | Contributions with other software (except as part of its Contributor 130 | Version); or 131 | 132 | c. under Patent Claims infringed by Covered Software in the absence of its 133 | Contributions. 134 | 135 | This License does not grant any rights in the trademarks, service marks, or 136 | logos of any Contributor (except as may be necessary to comply with the 137 | notice requirements in Section 3.4). 138 | 139 | 2.4. Subsequent Licenses 140 | 141 | No Contributor makes additional grants as a result of Your choice to 142 | distribute the Covered Software under a subsequent version of this License 143 | (see Section 10.2) or under the terms of a Secondary License (if permitted 144 | under the terms of Section 3.3). 145 | 146 | 2.5. Representation 147 | 148 | Each Contributor represents that the Contributor believes its Contributions 149 | are its original creation(s) or it has sufficient rights to grant the 150 | rights to its Contributions conveyed by this License. 151 | 152 | 2.6. Fair Use 153 | 154 | This License is not intended to limit any rights You have under applicable 155 | copyright doctrines of fair use, fair dealing, or other equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under the 169 | terms of this License. You must inform recipients that the Source Code Form 170 | of the Covered Software is governed by the terms of this License, and how 171 | they can obtain a copy of this License. You may not attempt to alter or 172 | restrict the recipients’ rights in the Source Code Form. 173 | 174 | 3.2. Distribution of Executable Form 175 | 176 | If You distribute Covered Software in Executable Form then: 177 | 178 | a. such Covered Software must also be made available in Source Code Form, 179 | as described in Section 3.1, and You must inform recipients of the 180 | Executable Form how they can obtain a copy of such Source Code Form by 181 | reasonable means in a timely manner, at a charge no more than the cost 182 | of distribution to the recipient; and 183 | 184 | b. You may distribute such Executable Form under the terms of this License, 185 | or sublicense it under different terms, provided that the license for 186 | the Executable Form does not attempt to limit or alter the recipients’ 187 | rights in the Source Code Form under this License. 188 | 189 | 3.3. Distribution of a Larger Work 190 | 191 | You may create and distribute a Larger Work under terms of Your choice, 192 | provided that You also comply with the requirements of this License for the 193 | Covered Software. If the Larger Work is a combination of Covered Software 194 | with a work governed by one or more Secondary Licenses, and the Covered 195 | Software is not Incompatible With Secondary Licenses, this License permits 196 | You to additionally distribute such Covered Software under the terms of 197 | such Secondary License(s), so that the recipient of the Larger Work may, at 198 | their option, further distribute the Covered Software under the terms of 199 | either this License or such Secondary License(s). 200 | 201 | 3.4. Notices 202 | 203 | You may not remove or alter the substance of any license notices (including 204 | copyright notices, patent notices, disclaimers of warranty, or limitations 205 | of liability) contained within the Source Code Form of the Covered 206 | Software, except that You may alter any license notices to the extent 207 | required to remedy known factual inaccuracies. 208 | 209 | 3.5. Application of Additional Terms 210 | 211 | You may choose to offer, and to charge a fee for, warranty, support, 212 | indemnity or liability obligations to one or more recipients of Covered 213 | Software. However, You may do so only on Your own behalf, and not on behalf 214 | of any Contributor. You must make it absolutely clear that any such 215 | warranty, support, indemnity, or liability obligation is offered by You 216 | alone, and You hereby agree to indemnify every Contributor for any 217 | liability incurred by such Contributor as a result of warranty, support, 218 | indemnity or liability terms You offer. You may include additional 219 | disclaimers of warranty and limitations of liability specific to any 220 | jurisdiction. 221 | 222 | 4. Inability to Comply Due to Statute or Regulation 223 | 224 | If it is impossible for You to comply with any of the terms of this License 225 | with respect to some or all of the Covered Software due to statute, judicial 226 | order, or regulation then You must: (a) comply with the terms of this License 227 | to the maximum extent possible; and (b) describe the limitations and the code 228 | they affect. Such description must be placed in a text file included with all 229 | distributions of the Covered Software under this License. Except to the 230 | extent prohibited by statute or regulation, such description must be 231 | sufficiently detailed for a recipient of ordinary skill to be able to 232 | understand it. 233 | 234 | 5. Termination 235 | 236 | 5.1. The rights granted under this License will terminate automatically if You 237 | fail to comply with any of its terms. However, if You become compliant, 238 | then the rights granted under this License from a particular Contributor 239 | are reinstated (a) provisionally, unless and until such Contributor 240 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 241 | if such Contributor fails to notify You of the non-compliance by some 242 | reasonable means prior to 60 days after You have come back into compliance. 243 | Moreover, Your grants from a particular Contributor are reinstated on an 244 | ongoing basis if such Contributor notifies You of the non-compliance by 245 | some reasonable means, this is the first time You have received notice of 246 | non-compliance with this License from such Contributor, and You become 247 | compliant prior to 30 days after Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, counter-claims, 251 | and cross-claims) alleging that a Contributor Version directly or 252 | indirectly infringes any patent, then the rights granted to You by any and 253 | all Contributors for the Covered Software under Section 2.1 of this License 254 | shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 257 | license agreements (excluding distributors and resellers) which have been 258 | validly granted by You or Your distributors under this License prior to 259 | termination shall survive termination. 260 | 261 | 6. Disclaimer of Warranty 262 | 263 | Covered Software is provided under this License on an “as is” basis, without 264 | warranty of any kind, either expressed, implied, or statutory, including, 265 | without limitation, warranties that the Covered Software is free of defects, 266 | merchantable, fit for a particular purpose or non-infringing. The entire 267 | risk as to the quality and performance of the Covered Software is with You. 268 | Should any Covered Software prove defective in any respect, You (not any 269 | Contributor) assume the cost of any necessary servicing, repair, or 270 | correction. This disclaimer of warranty constitutes an essential part of this 271 | License. No use of any Covered Software is authorized under this License 272 | except under this disclaimer. 273 | 274 | 7. Limitation of Liability 275 | 276 | Under no circumstances and under no legal theory, whether tort (including 277 | negligence), contract, or otherwise, shall any Contributor, or anyone who 278 | distributes Covered Software as permitted above, be liable to You for any 279 | direct, indirect, special, incidental, or consequential damages of any 280 | character including, without limitation, damages for lost profits, loss of 281 | goodwill, work stoppage, computer failure or malfunction, or any and all 282 | other commercial damages or losses, even if such party shall have been 283 | informed of the possibility of such damages. This limitation of liability 284 | shall not apply to liability for death or personal injury resulting from such 285 | party’s negligence to the extent applicable law prohibits such limitation. 286 | Some jurisdictions do not allow the exclusion or limitation of incidental or 287 | consequential damages, so this exclusion and limitation may not apply to You. 288 | 289 | 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the courts of 292 | a jurisdiction where the defendant maintains its principal place of business 293 | and such litigation shall be governed by laws of that jurisdiction, without 294 | reference to its conflict-of-law provisions. Nothing in this Section shall 295 | prevent a party’s ability to bring cross-claims or counter-claims. 296 | 297 | 9. Miscellaneous 298 | 299 | This License represents the complete agreement concerning the subject matter 300 | hereof. If any provision of this License is held to be unenforceable, such 301 | provision shall be reformed only to the extent necessary to make it 302 | enforceable. Any law or regulation which provides that the language of a 303 | contract shall be construed against the drafter shall not be used to construe 304 | this License against a Contributor. 305 | 306 | 307 | 10. Versions of the License 308 | 309 | 10.1. New Versions 310 | 311 | Mozilla Foundation is the license steward. Except as provided in Section 312 | 10.3, no one other than the license steward has the right to modify or 313 | publish new versions of this License. Each version will be given a 314 | distinguishing version number. 315 | 316 | 10.2. Effect of New Versions 317 | 318 | You may distribute the Covered Software under the terms of the version of 319 | the License under which You originally received the Covered Software, or 320 | under the terms of any subsequent version published by the license 321 | steward. 322 | 323 | 10.3. Modified Versions 324 | 325 | If you create software not governed by this License, and you want to 326 | create a new license for such software, you may create and use a modified 327 | version of this License if you rename the license and remove any 328 | references to the name of the license steward (except to note that such 329 | modified license differs from this License). 330 | 331 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 332 | If You choose to distribute Source Code Form that is Incompatible With 333 | Secondary Licenses under the terms of this version of the License, the 334 | notice described in Exhibit B of this License must be attached. 335 | 336 | Exhibit A - Source Code Form License Notice 337 | 338 | This Source Code Form is subject to the 339 | terms of the Mozilla Public License, v. 340 | 2.0. If a copy of the MPL was not 341 | distributed with this file, You can 342 | obtain one at 343 | http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular file, then 346 | You may include the notice in a location (such as a LICENSE file in a relevant 347 | directory) where a recipient would be likely to look for such a notice. 348 | 349 | You may add additional accurate notices of copyright ownership. 350 | 351 | Exhibit B - “Incompatible With Secondary Licenses” Notice 352 | 353 | This Source Code Form is “Incompatible 354 | With Secondary Licenses”, as defined by 355 | the Mozilla Public License, v. 2.0. 356 | -------------------------------------------------------------------------------- /check_types.go: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015, 2025 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package hil 5 | 6 | import ( 7 | "fmt" 8 | "sync" 9 | 10 | "github.com/hashicorp/hil/ast" 11 | ) 12 | 13 | // TypeCheck implements ast.Visitor for type checking an AST tree. 14 | // It requires some configuration to look up the type of nodes. 15 | // 16 | // It also optionally will not type error and will insert an implicit 17 | // type conversions for specific types if specified by the Implicit 18 | // field. Note that this is kind of organizationally weird to put into 19 | // this structure but we'd rather do that than duplicate the type checking 20 | // logic multiple times. 21 | type TypeCheck struct { 22 | Scope ast.Scope 23 | 24 | // Implicit is a map of implicit type conversions that we can do, 25 | // and that shouldn't error. The key of the first map is the from type, 26 | // the key of the second map is the to type, and the final string 27 | // value is the function to call (which must be registered in the Scope). 28 | Implicit map[ast.Type]map[ast.Type]string 29 | 30 | // Stack of types. This shouldn't be used directly except by implementations 31 | // of TypeCheckNode. 32 | Stack []ast.Type 33 | 34 | err error 35 | lock sync.Mutex 36 | } 37 | 38 | // TypeCheckNode is the interface that must be implemented by any 39 | // ast.Node that wants to support type-checking. If the type checker 40 | // encounters a node that doesn't implement this, it will error. 41 | type TypeCheckNode interface { 42 | TypeCheck(*TypeCheck) (ast.Node, error) 43 | } 44 | 45 | func (v *TypeCheck) Visit(root ast.Node) error { 46 | v.lock.Lock() 47 | defer v.lock.Unlock() 48 | defer v.reset() 49 | root.Accept(v.visit) 50 | 51 | // If the resulting type is unknown, then just let the whole thing go. 52 | if v.err == errExitUnknown { 53 | v.err = nil 54 | } 55 | 56 | return v.err 57 | } 58 | 59 | func (v *TypeCheck) visit(raw ast.Node) ast.Node { 60 | if v.err != nil { 61 | return raw 62 | } 63 | 64 | var result ast.Node 65 | var err error 66 | switch n := raw.(type) { 67 | case *ast.Arithmetic: 68 | tc := &typeCheckArithmetic{n} 69 | result, err = tc.TypeCheck(v) 70 | case *ast.Call: 71 | tc := &typeCheckCall{n} 72 | result, err = tc.TypeCheck(v) 73 | case *ast.Conditional: 74 | tc := &typeCheckConditional{n} 75 | result, err = tc.TypeCheck(v) 76 | case *ast.Index: 77 | tc := &typeCheckIndex{n} 78 | result, err = tc.TypeCheck(v) 79 | case *ast.Output: 80 | tc := &typeCheckOutput{n} 81 | result, err = tc.TypeCheck(v) 82 | case *ast.LiteralNode: 83 | tc := &typeCheckLiteral{n} 84 | result, err = tc.TypeCheck(v) 85 | case *ast.VariableAccess: 86 | tc := &typeCheckVariableAccess{n} 87 | result, err = tc.TypeCheck(v) 88 | default: 89 | tc, ok := raw.(TypeCheckNode) 90 | if !ok { 91 | err = fmt.Errorf("unknown node for type check: %#v", raw) 92 | break 93 | } 94 | 95 | result, err = tc.TypeCheck(v) 96 | } 97 | 98 | if err != nil { 99 | pos := raw.Pos() 100 | v.err = fmt.Errorf("at column %d, line %d: %s", 101 | pos.Column, pos.Line, err) 102 | } 103 | 104 | return result 105 | } 106 | 107 | type typeCheckArithmetic struct { 108 | n *ast.Arithmetic 109 | } 110 | 111 | func (tc *typeCheckArithmetic) TypeCheck(v *TypeCheck) (ast.Node, error) { 112 | // The arguments are on the stack in reverse order, so pop them off. 113 | exprs := make([]ast.Type, len(tc.n.Exprs)) 114 | for i := range tc.n.Exprs { 115 | exprs[len(tc.n.Exprs)-1-i] = v.StackPop() 116 | } 117 | 118 | // If any operand is unknown then our result is automatically unknown 119 | for _, ty := range exprs { 120 | if ty == ast.TypeUnknown { 121 | v.StackPush(ast.TypeUnknown) 122 | return tc.n, nil 123 | } 124 | } 125 | 126 | switch tc.n.Op { 127 | case ast.ArithmeticOpLogicalAnd, ast.ArithmeticOpLogicalOr: 128 | return tc.checkLogical(v, exprs) 129 | case ast.ArithmeticOpEqual, ast.ArithmeticOpNotEqual, 130 | ast.ArithmeticOpLessThan, ast.ArithmeticOpGreaterThan, 131 | ast.ArithmeticOpGreaterThanOrEqual, ast.ArithmeticOpLessThanOrEqual: 132 | return tc.checkComparison(v, exprs) 133 | default: 134 | return tc.checkNumeric(v, exprs) 135 | } 136 | 137 | } 138 | 139 | func (tc *typeCheckArithmetic) checkNumeric(v *TypeCheck, exprs []ast.Type) (ast.Node, error) { 140 | // Determine the resulting type we want. We do this by going over 141 | // every expression until we find one with a type we recognize. 142 | // We do this because the first expr might be a string ("var.foo") 143 | // and we need to know what to implicit to. 144 | mathFunc := "__builtin_IntMath" 145 | mathType := ast.TypeInt 146 | for _, v := range exprs { 147 | // We assume int math but if we find ANY float, the entire 148 | // expression turns into floating point math. 149 | if v == ast.TypeFloat { 150 | mathFunc = "__builtin_FloatMath" 151 | mathType = v 152 | break 153 | } 154 | } 155 | 156 | // Verify the args 157 | for i, arg := range exprs { 158 | if arg != mathType { 159 | cn := v.ImplicitConversion(exprs[i], mathType, tc.n.Exprs[i]) 160 | if cn != nil { 161 | tc.n.Exprs[i] = cn 162 | continue 163 | } 164 | 165 | return nil, fmt.Errorf( 166 | "operand %d should be %s, got %s", 167 | i+1, mathType, arg) 168 | } 169 | } 170 | 171 | // Modulo doesn't work for floats 172 | if mathType == ast.TypeFloat && tc.n.Op == ast.ArithmeticOpMod { 173 | return nil, fmt.Errorf("modulo cannot be used with floats") 174 | } 175 | 176 | // Return type 177 | v.StackPush(mathType) 178 | 179 | // Replace our node with a call to the proper function. This isn't 180 | // type checked but we already verified types. 181 | args := make([]ast.Node, len(tc.n.Exprs)+1) 182 | args[0] = &ast.LiteralNode{ 183 | Value: tc.n.Op, 184 | Typex: ast.TypeInt, 185 | Posx: tc.n.Pos(), 186 | } 187 | copy(args[1:], tc.n.Exprs) 188 | return &ast.Call{ 189 | Func: mathFunc, 190 | Args: args, 191 | Posx: tc.n.Pos(), 192 | }, nil 193 | } 194 | 195 | func (tc *typeCheckArithmetic) checkComparison(v *TypeCheck, exprs []ast.Type) (ast.Node, error) { 196 | if len(exprs) != 2 { 197 | // This should never happen, because the parser never produces 198 | // nodes that violate this. 199 | return nil, fmt.Errorf( 200 | "comparison operators must have exactly two operands", 201 | ) 202 | } 203 | 204 | // The first operand always dictates the type for a comparison. 205 | compareFunc := "" 206 | compareType := exprs[0] 207 | switch compareType { 208 | case ast.TypeBool: 209 | compareFunc = "__builtin_BoolCompare" 210 | case ast.TypeFloat: 211 | compareFunc = "__builtin_FloatCompare" 212 | case ast.TypeInt: 213 | compareFunc = "__builtin_IntCompare" 214 | case ast.TypeString: 215 | compareFunc = "__builtin_StringCompare" 216 | default: 217 | return nil, fmt.Errorf( 218 | "comparison operators apply only to bool, float, int, and string", 219 | ) 220 | } 221 | 222 | // For non-equality comparisons, we will do implicit conversions to 223 | // integer types if possible. In this case, we need to go through and 224 | // determine the type of comparison we're doing to enable the implicit 225 | // conversion. 226 | if tc.n.Op != ast.ArithmeticOpEqual && tc.n.Op != ast.ArithmeticOpNotEqual { 227 | compareFunc = "__builtin_IntCompare" 228 | compareType = ast.TypeInt 229 | for _, expr := range exprs { 230 | if expr == ast.TypeFloat { 231 | compareFunc = "__builtin_FloatCompare" 232 | compareType = ast.TypeFloat 233 | break 234 | } 235 | } 236 | } 237 | 238 | // Verify (and possibly, convert) the args 239 | for i, arg := range exprs { 240 | if arg != compareType { 241 | cn := v.ImplicitConversion(exprs[i], compareType, tc.n.Exprs[i]) 242 | if cn != nil { 243 | tc.n.Exprs[i] = cn 244 | continue 245 | } 246 | 247 | return nil, fmt.Errorf( 248 | "operand %d should be %s, got %s", 249 | i+1, compareType, arg, 250 | ) 251 | } 252 | } 253 | 254 | // Only ints and floats can have the <, >, <= and >= operators applied 255 | switch tc.n.Op { 256 | case ast.ArithmeticOpEqual, ast.ArithmeticOpNotEqual: 257 | // anything goes 258 | default: 259 | switch compareType { 260 | case ast.TypeFloat, ast.TypeInt: 261 | // fine 262 | default: 263 | return nil, fmt.Errorf( 264 | "<, >, <= and >= may apply only to int and float values", 265 | ) 266 | } 267 | } 268 | 269 | // Comparison operators always return bool 270 | v.StackPush(ast.TypeBool) 271 | 272 | // Replace our node with a call to the proper function. This isn't 273 | // type checked but we already verified types. 274 | args := make([]ast.Node, len(tc.n.Exprs)+1) 275 | args[0] = &ast.LiteralNode{ 276 | Value: tc.n.Op, 277 | Typex: ast.TypeInt, 278 | Posx: tc.n.Pos(), 279 | } 280 | copy(args[1:], tc.n.Exprs) 281 | return &ast.Call{ 282 | Func: compareFunc, 283 | Args: args, 284 | Posx: tc.n.Pos(), 285 | }, nil 286 | } 287 | 288 | func (tc *typeCheckArithmetic) checkLogical(v *TypeCheck, exprs []ast.Type) (ast.Node, error) { 289 | for i, t := range exprs { 290 | if t != ast.TypeBool { 291 | cn := v.ImplicitConversion(t, ast.TypeBool, tc.n.Exprs[i]) 292 | if cn == nil { 293 | return nil, fmt.Errorf( 294 | "logical operators require boolean operands, not %s", 295 | t, 296 | ) 297 | } 298 | tc.n.Exprs[i] = cn 299 | } 300 | } 301 | 302 | // Return type is always boolean 303 | v.StackPush(ast.TypeBool) 304 | 305 | // Arithmetic nodes are replaced with a call to a built-in function 306 | args := make([]ast.Node, len(tc.n.Exprs)+1) 307 | args[0] = &ast.LiteralNode{ 308 | Value: tc.n.Op, 309 | Typex: ast.TypeInt, 310 | Posx: tc.n.Pos(), 311 | } 312 | copy(args[1:], tc.n.Exprs) 313 | return &ast.Call{ 314 | Func: "__builtin_Logical", 315 | Args: args, 316 | Posx: tc.n.Pos(), 317 | }, nil 318 | } 319 | 320 | type typeCheckCall struct { 321 | n *ast.Call 322 | } 323 | 324 | func (tc *typeCheckCall) TypeCheck(v *TypeCheck) (ast.Node, error) { 325 | // Look up the function in the map 326 | function, ok := v.Scope.LookupFunc(tc.n.Func) 327 | if !ok { 328 | return nil, fmt.Errorf("unknown function called: %s", tc.n.Func) 329 | } 330 | 331 | // The arguments are on the stack in reverse order, so pop them off. 332 | args := make([]ast.Type, len(tc.n.Args)) 333 | for i := range tc.n.Args { 334 | args[len(tc.n.Args)-1-i] = v.StackPop() 335 | } 336 | 337 | // Verify the args 338 | for i, expected := range function.ArgTypes { 339 | if expected == ast.TypeAny { 340 | continue 341 | } 342 | 343 | if args[i] == ast.TypeUnknown { 344 | v.StackPush(ast.TypeUnknown) 345 | return tc.n, nil 346 | } 347 | 348 | if args[i] != expected { 349 | cn := v.ImplicitConversion(args[i], expected, tc.n.Args[i]) 350 | if cn != nil { 351 | tc.n.Args[i] = cn 352 | continue 353 | } 354 | 355 | return nil, fmt.Errorf( 356 | "%s: argument %d should be %s, got %s", 357 | tc.n.Func, i+1, expected.Printable(), args[i].Printable()) 358 | } 359 | } 360 | 361 | // If we're variadic, then verify the types there 362 | if function.Variadic && function.VariadicType != ast.TypeAny { 363 | args = args[len(function.ArgTypes):] 364 | for i, t := range args { 365 | if t == ast.TypeUnknown { 366 | v.StackPush(ast.TypeUnknown) 367 | return tc.n, nil 368 | } 369 | 370 | if t != function.VariadicType { 371 | realI := i + len(function.ArgTypes) 372 | cn := v.ImplicitConversion( 373 | t, function.VariadicType, tc.n.Args[realI]) 374 | if cn != nil { 375 | tc.n.Args[realI] = cn 376 | continue 377 | } 378 | 379 | return nil, fmt.Errorf( 380 | "%s: argument %d should be %s, got %s", 381 | tc.n.Func, realI, 382 | function.VariadicType.Printable(), t.Printable()) 383 | } 384 | } 385 | } 386 | 387 | // Return type 388 | v.StackPush(function.ReturnType) 389 | 390 | return tc.n, nil 391 | } 392 | 393 | type typeCheckConditional struct { 394 | n *ast.Conditional 395 | } 396 | 397 | func (tc *typeCheckConditional) TypeCheck(v *TypeCheck) (ast.Node, error) { 398 | // On the stack we have the types of the condition, true and false 399 | // expressions, but they are in reverse order. 400 | falseType := v.StackPop() 401 | trueType := v.StackPop() 402 | condType := v.StackPop() 403 | 404 | if condType == ast.TypeUnknown { 405 | v.StackPush(ast.TypeUnknown) 406 | return tc.n, nil 407 | } 408 | 409 | if condType != ast.TypeBool { 410 | cn := v.ImplicitConversion(condType, ast.TypeBool, tc.n.CondExpr) 411 | if cn == nil { 412 | return nil, fmt.Errorf( 413 | "condition must be type bool, not %s", condType.Printable(), 414 | ) 415 | } 416 | tc.n.CondExpr = cn 417 | } 418 | 419 | // The types of the true and false expression must match 420 | if trueType != falseType && trueType != ast.TypeUnknown && falseType != ast.TypeUnknown { 421 | 422 | // Since passing around stringified versions of other types is 423 | // common, we pragmatically allow the false expression to dictate 424 | // the result type when the true expression is a string. 425 | if trueType == ast.TypeString { 426 | cn := v.ImplicitConversion(trueType, falseType, tc.n.TrueExpr) 427 | if cn == nil { 428 | return nil, fmt.Errorf( 429 | "true and false expression types must match; have %s and %s", 430 | trueType.Printable(), falseType.Printable(), 431 | ) 432 | } 433 | tc.n.TrueExpr = cn 434 | trueType = falseType 435 | } else { 436 | cn := v.ImplicitConversion(falseType, trueType, tc.n.FalseExpr) 437 | if cn == nil { 438 | return nil, fmt.Errorf( 439 | "true and false expression types must match; have %s and %s", 440 | trueType.Printable(), falseType.Printable(), 441 | ) 442 | } 443 | tc.n.FalseExpr = cn 444 | falseType = trueType 445 | } 446 | } 447 | 448 | // Currently list and map types cannot be used, because we cannot 449 | // generally assert that their element types are consistent. 450 | // Such support might be added later, either by improving the type 451 | // system or restricting usage to only variable and literal expressions, 452 | // but for now this is simply prohibited because it doesn't seem to 453 | // be a common enough case to be worth the complexity. 454 | switch trueType { 455 | case ast.TypeList: 456 | return nil, fmt.Errorf( 457 | "conditional operator cannot be used with list values", 458 | ) 459 | case ast.TypeMap: 460 | return nil, fmt.Errorf( 461 | "conditional operator cannot be used with map values", 462 | ) 463 | } 464 | 465 | // Result type (guaranteed to also match falseType due to the above) 466 | if trueType == ast.TypeUnknown { 467 | // falseType may also be unknown, but that's okay because two 468 | // unknowns means our result is unknown anyway. 469 | v.StackPush(falseType) 470 | } else { 471 | v.StackPush(trueType) 472 | } 473 | 474 | return tc.n, nil 475 | } 476 | 477 | type typeCheckOutput struct { 478 | n *ast.Output 479 | } 480 | 481 | func (tc *typeCheckOutput) TypeCheck(v *TypeCheck) (ast.Node, error) { 482 | n := tc.n 483 | types := make([]ast.Type, len(n.Exprs)) 484 | for i := range n.Exprs { 485 | types[len(n.Exprs)-1-i] = v.StackPop() 486 | } 487 | 488 | for _, ty := range types { 489 | if ty == ast.TypeUnknown { 490 | v.StackPush(ast.TypeUnknown) 491 | return tc.n, nil 492 | } 493 | } 494 | 495 | // If there is only one argument and it is a list, we evaluate to a list 496 | if len(types) == 1 { 497 | switch t := types[0]; t { 498 | case ast.TypeList: 499 | fallthrough 500 | case ast.TypeMap: 501 | v.StackPush(t) 502 | return n, nil 503 | } 504 | } 505 | 506 | // Otherwise, all concat args must be strings, so validate that 507 | resultType := ast.TypeString 508 | for i, t := range types { 509 | 510 | if t == ast.TypeUnknown { 511 | resultType = ast.TypeUnknown 512 | continue 513 | } 514 | 515 | if t != ast.TypeString { 516 | cn := v.ImplicitConversion(t, ast.TypeString, n.Exprs[i]) 517 | if cn != nil { 518 | n.Exprs[i] = cn 519 | continue 520 | } 521 | 522 | return nil, fmt.Errorf( 523 | "output of an HIL expression must be a string, or a single list (argument %d is %s)", i+1, t) 524 | } 525 | } 526 | 527 | // This always results in type string, unless there are unknowns 528 | v.StackPush(resultType) 529 | 530 | return n, nil 531 | } 532 | 533 | type typeCheckLiteral struct { 534 | n *ast.LiteralNode 535 | } 536 | 537 | func (tc *typeCheckLiteral) TypeCheck(v *TypeCheck) (ast.Node, error) { 538 | v.StackPush(tc.n.Typex) 539 | return tc.n, nil 540 | } 541 | 542 | type typeCheckVariableAccess struct { 543 | n *ast.VariableAccess 544 | } 545 | 546 | func (tc *typeCheckVariableAccess) TypeCheck(v *TypeCheck) (ast.Node, error) { 547 | // Look up the variable in the map 548 | variable, ok := v.Scope.LookupVar(tc.n.Name) 549 | if !ok { 550 | return nil, fmt.Errorf( 551 | "unknown variable accessed: %s", tc.n.Name) 552 | } 553 | 554 | // Add the type to the stack 555 | v.StackPush(variable.Type) 556 | 557 | return tc.n, nil 558 | } 559 | 560 | type typeCheckIndex struct { 561 | n *ast.Index 562 | } 563 | 564 | func (tc *typeCheckIndex) TypeCheck(v *TypeCheck) (ast.Node, error) { 565 | keyType := v.StackPop() 566 | targetType := v.StackPop() 567 | 568 | if keyType == ast.TypeUnknown || targetType == ast.TypeUnknown { 569 | v.StackPush(ast.TypeUnknown) 570 | return tc.n, nil 571 | } 572 | 573 | // Ensure we have a VariableAccess as the target 574 | varAccessNode, ok := tc.n.Target.(*ast.VariableAccess) 575 | if !ok { 576 | return nil, fmt.Errorf( 577 | "target of an index must be a VariableAccess node, was %T", tc.n.Target) 578 | } 579 | 580 | // Get the variable 581 | variable, ok := v.Scope.LookupVar(varAccessNode.Name) 582 | if !ok { 583 | return nil, fmt.Errorf( 584 | "unknown variable accessed: %s", varAccessNode.Name) 585 | } 586 | 587 | switch targetType { 588 | case ast.TypeList: 589 | if keyType != ast.TypeInt { 590 | tc.n.Key = v.ImplicitConversion(keyType, ast.TypeInt, tc.n.Key) 591 | if tc.n.Key == nil { 592 | return nil, fmt.Errorf( 593 | "key of an index must be an int, was %s", keyType) 594 | } 595 | } 596 | 597 | valType, err := ast.VariableListElementTypesAreHomogenous( 598 | varAccessNode.Name, variable.Value.([]ast.Variable)) 599 | if err != nil { 600 | return tc.n, err 601 | } 602 | 603 | v.StackPush(valType) 604 | return tc.n, nil 605 | case ast.TypeMap: 606 | if keyType != ast.TypeString { 607 | tc.n.Key = v.ImplicitConversion(keyType, ast.TypeString, tc.n.Key) 608 | if tc.n.Key == nil { 609 | return nil, fmt.Errorf( 610 | "key of an index must be a string, was %s", keyType) 611 | } 612 | } 613 | 614 | valType, err := ast.VariableMapValueTypesAreHomogenous( 615 | varAccessNode.Name, variable.Value.(map[string]ast.Variable)) 616 | if err != nil { 617 | return tc.n, err 618 | } 619 | 620 | v.StackPush(valType) 621 | return tc.n, nil 622 | default: 623 | return nil, fmt.Errorf("invalid index operation into non-indexable type: %s", variable.Type) 624 | } 625 | } 626 | 627 | func (v *TypeCheck) ImplicitConversion( 628 | actual ast.Type, expected ast.Type, n ast.Node) ast.Node { 629 | if v.Implicit == nil { 630 | return nil 631 | } 632 | 633 | fromMap, ok := v.Implicit[actual] 634 | if !ok { 635 | return nil 636 | } 637 | 638 | toFunc, ok := fromMap[expected] 639 | if !ok { 640 | return nil 641 | } 642 | 643 | return &ast.Call{ 644 | Func: toFunc, 645 | Args: []ast.Node{n}, 646 | Posx: n.Pos(), 647 | } 648 | } 649 | 650 | func (v *TypeCheck) reset() { 651 | v.Stack = nil 652 | v.err = nil 653 | } 654 | 655 | func (v *TypeCheck) StackPush(t ast.Type) { 656 | v.Stack = append(v.Stack, t) 657 | } 658 | 659 | func (v *TypeCheck) StackPop() ast.Type { 660 | var x ast.Type 661 | x, v.Stack = v.Stack[len(v.Stack)-1], v.Stack[:len(v.Stack)-1] 662 | return x 663 | } 664 | 665 | func (v *TypeCheck) StackPeek() ast.Type { 666 | if len(v.Stack) == 0 { 667 | return ast.TypeInvalid 668 | } 669 | 670 | return v.Stack[len(v.Stack)-1] 671 | } 672 | --------------------------------------------------------------------------------