pax_global_header00006660000000000000000000000064127410245150014513gustar00rootroot0000000000000052 comment=1e86c6b523c55d1fa6c6e930ce80b548664c95c2 golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/000077500000000000000000000000001274102451500214325ustar00rootroot00000000000000golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/.gitignore000066400000000000000000000000261274102451500234200ustar00rootroot00000000000000.DS_Store .idea *.iml golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/.travis.yml000066400000000000000000000000411274102451500235360ustar00rootroot00000000000000sudo: false language: go go: 1.5 golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/LICENSE000066400000000000000000000371501274102451500224450ustar00rootroot00000000000000Mozilla Public License, version 2.0 1. Definitions 1.1. “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. “Contributor Version” means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 1.3. “Contribution” means Covered Software of a particular Contributor. 1.4. “Covered Software” means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. “Incompatible With Secondary Licenses” means a. that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or b. that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. “Executable Form” means any form of the work other than Source Code Form. 1.7. “Larger Work” means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. “License” means this document. 1.9. “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. “Modifications” means any of the following: a. any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or b. any new file in Source Code Form that contains any Covered Software. 1.11. “Patent Claims” of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. “Secondary License” means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. “Source Code Form” means the form of the work preferred for making modifications. 1.14. “You” (or “Your”) means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: a. under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and b. under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: a. for any code that a Contributor has removed from Covered Software; or b. for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or c. under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: a. such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and b. You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 6. Disclaimer of Warranty Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 7. Limitation of Liability Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 8. Litigation Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 9. Miscellaneous This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - “Incompatible With Secondary Licenses” Notice This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/README.md000066400000000000000000000105421274102451500227130ustar00rootroot00000000000000# HIL [![GoDoc](https://godoc.org/github.com/hashicorp/hil?status.png)](https://godoc.org/github.com/hashicorp/hil) [![Build Status](https://travis-ci.org/hashicorp/hil.svg?branch=master)](https://travis-ci.org/hashicorp/hil) HIL (HashiCorp Interpolation Language) is a lightweight embedded language used primarily for configuration interpolation. The goal of HIL is to make a simple language for interpolations in the various configurations of HashiCorp tools. HIL is built to interpolate any string, but is in use by HashiCorp primarily with [HCL](https://github.com/hashicorp/hcl). HCL is _not required_ in any way for use with HIL. HIL isn't meant to be a general purpose language. It was built for basic configuration interpolations. Therefore, you can't currently write functions, have conditionals, set intermediary variables, etc. within HIL itself. It is possible some of these may be added later but the right use case must exist. ## Why? Many of our tools have support for something similar to templates, but within the configuration itself. The most prominent requirement was in [Terraform](https://github.com/hashicorp/terraform) where we wanted the configuration to be able to reference values from elsewhere in the configuration. Example: foo = "hi ${var.world}" We originally used a full templating language for this, but found it was too heavy weight. Additionally, many full languages required bindings to C (and thus the usage of cgo) which we try to avoid to make cross-compilation easier. We then moved to very basic regular expression based string replacement, but found the need for basic arithmetic and function calls resulting in overly complex regular expressions. Ultimately, we wrote our own mini-language within Terraform itself. As we built other projects such as [Nomad](https://nomadproject.io) and [Otto](https://ottoproject.io), the need for basic interpolations arose again. Thus HIL was born. It is extracted from Terraform, cleaned up, and better tested for general purpose use. ## Syntax For a complete grammar, please see the parser itself. A high-level overview of the syntax and grammer is listed here. Code begins within `${` and `}`. Outside of this, text is treated literally. For example, `foo` is a valid HIL program that is just the string "foo", but `foo ${bar}` is an HIL program that is the string "foo " concatened with the value of `bar`. For the remainder of the syntax docs, we'll assume you're within `${}`. * Identifiers are any text in the format of `[a-zA-Z0-9-.]`. Example identifiers: `foo`, `var.foo`, `foo-bar`. * Strings are double quoted and can contain any UTF-8 characters. Example: `"Hello, World"` * Numbers are assumed to be base 10. If you prefix a number with 0x, it is treated as a hexadecimal. If it is prefixed with 0, it is treated as an octal. Numbers can be in scientific notation: "1e10". * Unary `-` can be used for negative numbers. Example: `-10` or `-0.2` * Boolean values: `true`, `false` * The following arithmetic operations are allowed: +, -, *, /, %. * Function calls are in the form of `name(arg1, arg2, ...)`. Example: `add(1, 5)`. Arguments can be any valid HIL expression, example: `add(1, var.foo)` or even nested function calls: `add(1, get("some value"))`. * Within strings, further interpolations can be opened with `${}`. Example: `"Hello ${nested}"`. A full example including the original `${}` (remember this list assumes were inside of one already) could be: `foo ${func("hello ${var.foo}")}`. ## Language Changes We've used this mini-language in Terraform for years. For backwards compatibility reasons, we're unlikely to make an incompatible change to the language but we're not currently making that promise, either. The internal API of this project may very well change as we evolve it to work with more of our projects. We recommend using some sort of dependency management solution with this package. ## Future Changes The following changes are already planned to be made at some point: * Richer types: lists, maps, etc. * Convert to a more standard Go parser structure similar to HCL. This will improve our error messaging as well as allow us to have automatic formatting. * Allow interpolations to result in more types than just a string. While within the interpolation basic types are honored, the result is always a string. golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/appveyor.yml000066400000000000000000000005031274102451500240200ustar00rootroot00000000000000version: "build-{branch}-{build}" image: Visual Studio 2015 clone_folder: c:\gopath\src\github.com\hashicorp\hil environment: GOPATH: c:\gopath init: - git config --global core.autocrlf true install: - cmd: >- echo %Path% go version go env go get -d -v -t ./... build_script: - cmd: go test -v ./... golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/000077500000000000000000000000001274102451500222215ustar00rootroot00000000000000golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/arithmetic.go000066400000000000000000000013051274102451500247000ustar00rootroot00000000000000package ast import ( "bytes" "fmt" ) // Arithmetic represents a node where the result is arithmetic of // two or more operands in the order given. type Arithmetic struct { Op ArithmeticOp Exprs []Node Posx Pos } func (n *Arithmetic) Accept(v Visitor) Node { for i, expr := range n.Exprs { n.Exprs[i] = expr.Accept(v) } return v(n) } func (n *Arithmetic) Pos() Pos { return n.Posx } func (n *Arithmetic) GoString() string { return fmt.Sprintf("*%#v", *n) } func (n *Arithmetic) String() string { var b bytes.Buffer for _, expr := range n.Exprs { b.WriteString(fmt.Sprintf("%s", expr)) } return b.String() } func (n *Arithmetic) Type(Scope) (Type, error) { return TypeInt, nil } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/arithmetic_op.go000066400000000000000000000003671274102451500254050ustar00rootroot00000000000000package ast // ArithmeticOp is the operation to use for the math. type ArithmeticOp int const ( ArithmeticOpInvalid ArithmeticOp = 0 ArithmeticOpAdd ArithmeticOp = iota ArithmeticOpSub ArithmeticOpMul ArithmeticOpDiv ArithmeticOpMod ) golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/ast.go000066400000000000000000000036211274102451500233410ustar00rootroot00000000000000package ast import ( "fmt" ) // Node is the interface that all AST nodes must implement. type Node interface { // Accept is called to dispatch to the visitors. It must return the // resulting Node (which might be different in an AST transform). Accept(Visitor) Node // Pos returns the position of this node in some source. Pos() Pos // Type returns the type of this node for the given context. Type(Scope) (Type, error) } // Pos is the starting position of an AST node type Pos struct { Column, Line int // Column/Line number, starting at 1 } func (p Pos) String() string { return fmt.Sprintf("%d:%d", p.Line, p.Column) } // Visitors are just implementations of this function. // // The function must return the Node to replace this node with. "nil" is // _not_ a valid return value. If there is no replacement, the original node // should be returned. We build this replacement directly into the visitor // pattern since AST transformations are a common and useful tool and // building it into the AST itself makes it required for future Node // implementations and very easy to do. // // Note that this isn't a true implementation of the visitor pattern, which // generally requires proper type dispatch on the function. However, // implementing this basic visitor pattern style is still very useful even // if you have to type switch. type Visitor func(Node) Node //go:generate stringer -type=Type // Type is the type of any value. type Type uint32 const ( TypeInvalid Type = 0 TypeAny Type = 1 << iota TypeString TypeInt TypeFloat TypeList TypeMap ) func (t Type) Printable() string { switch t { case TypeInvalid: return "invalid type" case TypeAny: return "any type" case TypeString: return "type string" case TypeInt: return "type int" case TypeFloat: return "type float" case TypeList: return "type list" case TypeMap: return "type map" default: return "unknown type" } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/call.go000066400000000000000000000013711274102451500234650ustar00rootroot00000000000000package ast import ( "fmt" "strings" ) // Call represents a function call. type Call struct { Func string Args []Node Posx Pos } func (n *Call) Accept(v Visitor) Node { for i, a := range n.Args { n.Args[i] = a.Accept(v) } return v(n) } func (n *Call) Pos() Pos { return n.Posx } func (n *Call) String() string { args := make([]string, len(n.Args)) for i, arg := range n.Args { args[i] = fmt.Sprintf("%s", arg) } return fmt.Sprintf("Call(%s, %s)", n.Func, strings.Join(args, ", ")) } func (n *Call) Type(s Scope) (Type, error) { f, ok := s.LookupFunc(n.Func) if !ok { return TypeInvalid, fmt.Errorf("unknown function: %s", n.Func) } return f.ReturnType, nil } func (n *Call) GoString() string { return fmt.Sprintf("*%#v", *n) } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/call_test.go000066400000000000000000000011171274102451500245220ustar00rootroot00000000000000package ast import ( "testing" ) func TestCallType(t *testing.T) { c := &Call{Func: "foo"} scope := &BasicScope{ FuncMap: map[string]Function{ "foo": Function{ReturnType: TypeString}, }, } actual, err := c.Type(scope) if err != nil { t.Fatalf("err: %s", err) } if actual != TypeString { t.Fatalf("bad: %s", actual) } } func TestCallType_invalid(t *testing.T) { c := &Call{Func: "bar"} scope := &BasicScope{ FuncMap: map[string]Function{ "foo": Function{ReturnType: TypeString}, }, } _, err := c.Type(scope) if err == nil { t.Fatal("should error") } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/index.go000066400000000000000000000033671274102451500236700ustar00rootroot00000000000000package ast import ( "fmt" "strings" ) // Index represents an indexing operation into another data structure type Index struct { Target Node Key Node Posx Pos } func (n *Index) Accept(v Visitor) Node { return v(n) } func (n *Index) Pos() Pos { return n.Posx } func (n *Index) String() string { return fmt.Sprintf("Index(%s, %s)", n.Target, n.Key) } func (n *Index) Type(s Scope) (Type, error) { variableAccess, ok := n.Target.(*VariableAccess) if !ok { return TypeInvalid, fmt.Errorf("target is not a variable") } variable, ok := s.LookupVar(variableAccess.Name) if !ok { return TypeInvalid, fmt.Errorf("unknown variable accessed: %s", variableAccess.Name) } switch variable.Type { case TypeList: return n.typeList(variable, variableAccess.Name) case TypeMap: return n.typeMap(variable, variableAccess.Name) default: return TypeInvalid, fmt.Errorf("invalid index operation into non-indexable type: %s", variable.Type) } } func (n *Index) typeList(variable Variable, variableName string) (Type, error) { // We assume type checking has already determined that this is a list list := variable.Value.([]Variable) return VariableListElementTypesAreHomogenous(variableName, list) } func (n *Index) typeMap(variable Variable, variableName string) (Type, error) { // We assume type checking has already determined that this is a map vmap := variable.Value.(map[string]Variable) return VariableMapValueTypesAreHomogenous(variableName, vmap) } func reportTypes(typesFound map[Type]struct{}) string { stringTypes := make([]string, len(typesFound)) i := 0 for k, _ := range typesFound { stringTypes[0] = k.String() i++ } return strings.Join(stringTypes, ", ") } func (n *Index) GoString() string { return fmt.Sprintf("*%#v", *n) } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/index_test.go000066400000000000000000000107221274102451500247200ustar00rootroot00000000000000package ast import ( "strings" "testing" ) func TestIndexTypeMap_empty(t *testing.T) { i := &Index{ Target: &VariableAccess{Name: "foo"}, Key: &LiteralNode{ Typex: TypeString, Value: "bar", }, } scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{ Type: TypeMap, Value: map[string]Variable{}, }, }, } actual, err := i.Type(scope) if err == nil || !strings.Contains(err.Error(), "does not have any elements") { t.Fatalf("bad err: %s", err) } if actual != TypeInvalid { t.Fatalf("bad: %s", actual) } } func TestIndexTypeMap_string(t *testing.T) { i := &Index{ Target: &VariableAccess{Name: "foo"}, Key: &LiteralNode{ Typex: TypeString, Value: "bar", }, } scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{ Type: TypeMap, Value: map[string]Variable{ "baz": Variable{ Type: TypeString, Value: "Hello", }, "bar": Variable{ Type: TypeString, Value: "World", }, }, }, }, } actual, err := i.Type(scope) if err != nil { t.Fatalf("err: %s", err) } if actual != TypeString { t.Fatalf("bad: %s", actual) } } func TestIndexTypeMap_int(t *testing.T) { i := &Index{ Target: &VariableAccess{Name: "foo"}, Key: &LiteralNode{ Typex: TypeString, Value: "bar", }, } scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{ Type: TypeMap, Value: map[string]Variable{ "baz": Variable{ Type: TypeInt, Value: 1, }, "bar": Variable{ Type: TypeInt, Value: 2, }, }, }, }, } actual, err := i.Type(scope) if err != nil { t.Fatalf("err: %s", err) } if actual != TypeInt { t.Fatalf("bad: %s", actual) } } func TestIndexTypeMap_nonHomogenous(t *testing.T) { i := &Index{ Target: &VariableAccess{Name: "foo"}, Key: &LiteralNode{ Typex: TypeString, Value: "bar", }, } scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{ Type: TypeMap, Value: map[string]Variable{ "bar": Variable{ Type: TypeString, Value: "Hello", }, "baz": Variable{ Type: TypeInt, Value: 43, }, }, }, }, } _, err := i.Type(scope) if err == nil || !strings.Contains(err.Error(), "homogenous") { t.Fatalf("expected error") } } func TestIndexTypeList_empty(t *testing.T) { i := &Index{ Target: &VariableAccess{Name: "foo"}, Key: &LiteralNode{ Typex: TypeInt, Value: 1, }, } scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{ Type: TypeList, Value: []Variable{}, }, }, } actual, err := i.Type(scope) if err == nil || !strings.Contains(err.Error(), "does not have any elements") { t.Fatalf("bad err: %s", err) } if actual != TypeInvalid { t.Fatalf("bad: %s", actual) } } func TestIndexTypeList_string(t *testing.T) { i := &Index{ Target: &VariableAccess{Name: "foo"}, Key: &LiteralNode{ Typex: TypeInt, Value: 1, }, } scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{ Type: TypeList, Value: []Variable{ Variable{ Type: TypeString, Value: "Hello", }, Variable{ Type: TypeString, Value: "World", }, }, }, }, } actual, err := i.Type(scope) if err != nil { t.Fatalf("err: %s", err) } if actual != TypeString { t.Fatalf("bad: %s", actual) } } func TestIndexTypeList_int(t *testing.T) { i := &Index{ Target: &VariableAccess{Name: "foo"}, Key: &LiteralNode{ Typex: TypeInt, Value: 1, }, } scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{ Type: TypeList, Value: []Variable{ Variable{ Type: TypeInt, Value: 34, }, Variable{ Type: TypeInt, Value: 54, }, }, }, }, } actual, err := i.Type(scope) if err != nil { t.Fatalf("err: %s", err) } if actual != TypeInt { t.Fatalf("bad: %s", actual) } } func TestIndexTypeList_nonHomogenous(t *testing.T) { i := &Index{ Target: &VariableAccess{Name: "foo"}, Key: &LiteralNode{ Typex: TypeInt, Value: 1, }, } scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{ Type: TypeList, Value: []Variable{ Variable{ Type: TypeString, Value: "Hello", }, Variable{ Type: TypeInt, Value: 43, }, }, }, }, } _, err := i.Type(scope) if err == nil || !strings.Contains(err.Error(), "homogenous") { t.Fatalf("expected error") } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/literal.go000066400000000000000000000011321274102451500242010ustar00rootroot00000000000000package ast import ( "fmt" ) // LiteralNode represents a single literal value, such as "foo" or // 42 or 3.14159. Based on the Type, the Value can be safely cast. type LiteralNode struct { Value interface{} Typex Type Posx Pos } func (n *LiteralNode) Accept(v Visitor) Node { return v(n) } func (n *LiteralNode) Pos() Pos { return n.Posx } func (n *LiteralNode) GoString() string { return fmt.Sprintf("*%#v", *n) } func (n *LiteralNode) String() string { return fmt.Sprintf("Literal(%s, %v)", n.Typex, n.Value) } func (n *LiteralNode) Type(Scope) (Type, error) { return n.Typex, nil } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/literal_test.go000066400000000000000000000003741274102451500252470ustar00rootroot00000000000000package ast import ( "testing" ) func TestLiteralNodeType(t *testing.T) { c := &LiteralNode{Typex: TypeString} actual, err := c.Type(nil) if err != nil { t.Fatalf("err: %s", err) } if actual != TypeString { t.Fatalf("bad: %s", actual) } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/output.go000066400000000000000000000033161274102451500241130ustar00rootroot00000000000000package ast import ( "bytes" "fmt" ) // Output represents the root node of all interpolation evaluations. If the // output only has one expression which is either a TypeList or TypeMap, the // Output can be type-asserted to []interface{} or map[string]interface{} // respectively. Otherwise the Output evaluates as a string, and concatenates // the evaluation of each expression. type Output struct { Exprs []Node Posx Pos } func (n *Output) Accept(v Visitor) Node { for i, expr := range n.Exprs { n.Exprs[i] = expr.Accept(v) } return v(n) } func (n *Output) Pos() Pos { return n.Posx } func (n *Output) GoString() string { return fmt.Sprintf("*%#v", *n) } func (n *Output) String() string { var b bytes.Buffer for _, expr := range n.Exprs { b.WriteString(fmt.Sprintf("%s", expr)) } return b.String() } func (n *Output) Type(s Scope) (Type, error) { // Special case no expressions for backward compatibility if len(n.Exprs) == 0 { return TypeString, nil } // Special case a single expression of types list or map if len(n.Exprs) == 1 { exprType, err := n.Exprs[0].Type(s) if err != nil { return TypeInvalid, err } switch exprType { case TypeList: return TypeList, nil case TypeMap: return TypeMap, nil } } // Otherwise ensure all our expressions are strings for index, expr := range n.Exprs { exprType, err := expr.Type(s) if err != nil { return TypeInvalid, err } // We only look for things we know we can't coerce with an implicit conversion func if exprType == TypeList || exprType == TypeMap { return TypeInvalid, fmt.Errorf( "multi-expression HIL outputs may only have string inputs: %d is type %s", index, exprType) } } return TypeString, nil } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/output_test.go000066400000000000000000000074521274102451500251570ustar00rootroot00000000000000package ast import ( "testing" ) func TestOutput_type(t *testing.T) { testCases := []struct { Name string Output *Output Scope Scope ReturnType Type ShouldError bool }{ { Name: "No expressions, for backward compatibility", Output: &Output{}, Scope: nil, ReturnType: TypeString, }, { Name: "Single string expression", Output: &Output{ Exprs: []Node{ &LiteralNode{ Value: "Whatever", Typex: TypeString, }, }, }, Scope: nil, ReturnType: TypeString, }, { Name: "Single list expression of strings", Output: &Output{ Exprs: []Node{ &VariableAccess{ Name: "testvar", }, }, }, Scope: &BasicScope{ VarMap: map[string]Variable{ "testvar": Variable{ Type: TypeList, Value: []Variable{ Variable{ Type: TypeString, Value: "Hello", }, Variable{ Type: TypeString, Value: "World", }, }, }, }, }, ReturnType: TypeList, }, { Name: "Single map expression", Output: &Output{ Exprs: []Node{ &VariableAccess{ Name: "testvar", }, }, }, Scope: &BasicScope{ VarMap: map[string]Variable{ "testvar": Variable{ Type: TypeMap, Value: map[string]Variable{ "key1": Variable{ Type: TypeString, Value: "Hello", }, "key2": Variable{ Type: TypeString, Value: "World", }, }, }, }, }, ReturnType: TypeMap, }, { Name: "Multiple map expressions", Output: &Output{ Exprs: []Node{ &VariableAccess{ Name: "testvar", }, &VariableAccess{ Name: "testvar", }, }, }, Scope: &BasicScope{ VarMap: map[string]Variable{ "testvar": Variable{ Type: TypeMap, Value: map[string]Variable{ "key1": Variable{ Type: TypeString, Value: "Hello", }, "key2": Variable{ Type: TypeString, Value: "World", }, }, }, }, }, ShouldError: true, ReturnType: TypeInvalid, }, { Name: "Multiple list expressions", Output: &Output{ Exprs: []Node{ &VariableAccess{ Name: "testvar", }, &VariableAccess{ Name: "testvar", }, }, }, Scope: &BasicScope{ VarMap: map[string]Variable{ "testvar": Variable{ Type: TypeList, Value: []Variable{ Variable{ Type: TypeString, Value: "Hello", }, Variable{ Type: TypeString, Value: "World", }, }, }, }, }, ShouldError: true, ReturnType: TypeInvalid, }, { Name: "Multiple string expressions", Output: &Output{ Exprs: []Node{ &VariableAccess{ Name: "testvar", }, &VariableAccess{ Name: "testvar", }, }, }, Scope: &BasicScope{ VarMap: map[string]Variable{ "testvar": Variable{ Type: TypeString, Value: "Hello", }, }, }, ReturnType: TypeString, }, { Name: "Multiple string expressions with coercion", Output: &Output{ Exprs: []Node{ &VariableAccess{ Name: "testvar", }, &VariableAccess{ Name: "testint", }, }, }, Scope: &BasicScope{ VarMap: map[string]Variable{ "testvar": Variable{ Type: TypeString, Value: "Hello", }, "testint": Variable{ Type: TypeInt, Value: 2, }, }, }, ReturnType: TypeString, }, } for _, v := range testCases { actual, err := v.Output.Type(v.Scope) if err != nil && !v.ShouldError { t.Fatalf("case: %s\nerr: %s", v.Name, err) } if actual != v.ReturnType { t.Fatalf("case: %s\n bad: %s\nexpected: %s\n", v.Name, actual, v.ReturnType) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/scope.go000066400000000000000000000046241274102451500236670ustar00rootroot00000000000000package ast import ( "fmt" "reflect" ) // Scope is the interface used to look up variables and functions while // evaluating. How these functions/variables are defined are up to the caller. type Scope interface { LookupFunc(string) (Function, bool) LookupVar(string) (Variable, bool) } // Variable is a variable value for execution given as input to the engine. // It records the value of a variables along with their type. type Variable struct { Value interface{} Type Type } // NewVariable creates a new Variable for the given value. This will // attempt to infer the correct type. If it can't, an error will be returned. func NewVariable(v interface{}) (result Variable, err error) { switch v := reflect.ValueOf(v); v.Kind() { case reflect.String: result.Type = TypeString default: err = fmt.Errorf("Unknown type: %s", v.Kind()) } result.Value = v return } // String implements Stringer on Variable, displaying the type and value // of the Variable. func (v Variable) String() string { return fmt.Sprintf("{Variable (%s): %+v}", v.Type, v.Value) } // Function defines a function that can be executed by the engine. // The type checker will validate that the proper types will be called // to the callback. type Function struct { // ArgTypes is the list of types in argument order. These are the // required arguments. // // ReturnType is the type of the returned value. The Callback MUST // return this type. ArgTypes []Type ReturnType Type // Variadic, if true, says that this function is variadic, meaning // it takes a variable number of arguments. In this case, the // VariadicType must be set. Variadic bool VariadicType Type // Callback is the function called for a function. The argument // types are guaranteed to match the spec above by the type checker. // The length of the args is strictly == len(ArgTypes) unless Varidiac // is true, in which case its >= len(ArgTypes). Callback func([]interface{}) (interface{}, error) } // BasicScope is a simple scope that looks up variables and functions // using a map. type BasicScope struct { FuncMap map[string]Function VarMap map[string]Variable } func (s *BasicScope) LookupFunc(n string) (Function, bool) { if s == nil { return Function{}, false } v, ok := s.FuncMap[n] return v, ok } func (s *BasicScope) LookupVar(n string) (Variable, bool) { if s == nil { return Variable{}, false } v, ok := s.VarMap[n] return v, ok } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/scope_test.go000066400000000000000000000017251274102451500247250ustar00rootroot00000000000000package ast import ( "testing" ) func TestBasicScope_impl(t *testing.T) { var _ Scope = new(BasicScope) } func TestBasicScopeLookupFunc(t *testing.T) { scope := &BasicScope{ FuncMap: map[string]Function{ "foo": Function{}, }, } if _, ok := scope.LookupFunc("bar"); ok { t.Fatal("should not find bar") } if _, ok := scope.LookupFunc("foo"); !ok { t.Fatal("should find foo") } } func TestBasicScopeLookupVar(t *testing.T) { scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{}, }, } if _, ok := scope.LookupVar("bar"); ok { t.Fatal("should not find bar") } if _, ok := scope.LookupVar("foo"); !ok { t.Fatal("should find foo") } } func TestVariableStringer(t *testing.T) { expected := "{Variable (TypeInt): 42}" variable := &Variable{ Type: TypeInt, Value: 42, } actual := variable.String() if actual != expected { t.Fatalf("variable string formatting:\nExpected: %s\n Got: %s\n", expected, actual) } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/stack.go000066400000000000000000000005711274102451500236600ustar00rootroot00000000000000package ast // Stack is a stack of Node. type Stack struct { stack []Node } func (s *Stack) Len() int { return len(s.stack) } func (s *Stack) Push(n Node) { s.stack = append(s.stack, n) } func (s *Stack) Pop() Node { x := s.stack[len(s.stack)-1] s.stack[len(s.stack)-1] = nil s.stack = s.stack[:len(s.stack)-1] return x } func (s *Stack) Reset() { s.stack = nil } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/stack_test.go000066400000000000000000000011311274102451500247100ustar00rootroot00000000000000package ast import ( "reflect" "testing" ) func TestStack(t *testing.T) { var s Stack if s.Len() != 0 { t.Fatalf("bad: %d", s.Len()) } n := &LiteralNode{Value: 42} s.Push(n) if s.Len() != 1 { t.Fatalf("bad: %d", s.Len()) } actual := s.Pop() if !reflect.DeepEqual(actual, n) { t.Fatalf("bad: %#v", actual) } if s.Len() != 0 { t.Fatalf("bad: %d", s.Len()) } } func TestStack_reset(t *testing.T) { var s Stack n := &LiteralNode{Value: 42} s.Push(n) if s.Len() != 1 { t.Fatalf("bad: %d", s.Len()) } s.Reset() if s.Len() != 0 { t.Fatalf("bad: %d", s.Len()) } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/type_string.go000066400000000000000000000015641274102451500251250ustar00rootroot00000000000000// Code generated by "stringer -type=Type"; DO NOT EDIT package ast import "fmt" const ( _Type_name_0 = "TypeInvalid" _Type_name_1 = "TypeAny" _Type_name_2 = "TypeString" _Type_name_3 = "TypeInt" _Type_name_4 = "TypeFloat" _Type_name_5 = "TypeList" _Type_name_6 = "TypeMap" ) var ( _Type_index_0 = [...]uint8{0, 11} _Type_index_1 = [...]uint8{0, 7} _Type_index_2 = [...]uint8{0, 10} _Type_index_3 = [...]uint8{0, 7} _Type_index_4 = [...]uint8{0, 9} _Type_index_5 = [...]uint8{0, 8} _Type_index_6 = [...]uint8{0, 7} ) func (i Type) String() string { switch { case i == 0: return _Type_name_0 case i == 2: return _Type_name_1 case i == 4: return _Type_name_2 case i == 8: return _Type_name_3 case i == 16: return _Type_name_4 case i == 32: return _Type_name_5 case i == 64: return _Type_name_6 default: return fmt.Sprintf("Type(%d)", i) } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/variable_access.go000066400000000000000000000011441274102451500256560ustar00rootroot00000000000000package ast import ( "fmt" ) // VariableAccess represents a variable access. type VariableAccess struct { Name string Posx Pos } func (n *VariableAccess) Accept(v Visitor) Node { return v(n) } func (n *VariableAccess) Pos() Pos { return n.Posx } func (n *VariableAccess) GoString() string { return fmt.Sprintf("*%#v", *n) } func (n *VariableAccess) String() string { return fmt.Sprintf("Variable(%s)", n.Name) } func (n *VariableAccess) Type(s Scope) (Type, error) { v, ok := s.LookupVar(n.Name) if !ok { return TypeInvalid, fmt.Errorf("unknown variable: %s", n.Name) } return v.Type, nil } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/variable_access_test.go000066400000000000000000000016521274102451500267210ustar00rootroot00000000000000package ast import ( "testing" ) func TestVariableAccessType(t *testing.T) { c := &VariableAccess{Name: "foo"} scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{Type: TypeString}, }, } actual, err := c.Type(scope) if err != nil { t.Fatalf("err: %s", err) } if actual != TypeString { t.Fatalf("bad: %s", actual) } } func TestVariableAccessType_invalid(t *testing.T) { c := &VariableAccess{Name: "bar"} scope := &BasicScope{ VarMap: map[string]Variable{ "foo": Variable{Type: TypeString}, }, } _, err := c.Type(scope) if err == nil { t.Fatal("should error") } } func TestVariableAccessType_list(t *testing.T) { c := &VariableAccess{Name: "baz"} scope := &BasicScope{ VarMap: map[string]Variable{ "baz": Variable{Type: TypeList}, }, } actual, err := c.Type(scope) if err != nil { t.Fatalf("err: %s", err) } if actual != TypeList { t.Fatalf("bad: %s", actual) } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/ast/variables_helper.go000066400000000000000000000024541274102451500260640ustar00rootroot00000000000000package ast import "fmt" func VariableListElementTypesAreHomogenous(variableName string, list []Variable) (Type, error) { listTypes := make(map[Type]struct{}) for _, v := range list { if _, ok := listTypes[v.Type]; ok { continue } listTypes[v.Type] = struct{}{} } if len(listTypes) != 1 && len(list) != 0 { return TypeInvalid, fmt.Errorf("list %q does not have homogenous types. found %s", variableName, reportTypes(listTypes)) } if len(list) > 0 { return list[0].Type, nil } return TypeInvalid, fmt.Errorf("list %q does not have any elements so cannot determine type.", variableName) } func VariableMapValueTypesAreHomogenous(variableName string, vmap map[string]Variable) (Type, error) { valueTypes := make(map[Type]struct{}) for _, v := range vmap { if _, ok := valueTypes[v.Type]; ok { continue } valueTypes[v.Type] = struct{}{} } if len(valueTypes) != 1 && len(vmap) != 0 { return TypeInvalid, fmt.Errorf("map %q does not have homogenous value types. found %s", variableName, reportTypes(valueTypes)) } // For loop here is an easy way to get a single key, we return immediately. for _, v := range vmap { return v.Type, nil } // This means the map is empty return TypeInvalid, fmt.Errorf("map %q does not have any elements so cannot determine type.", variableName) } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/builtins.go000066400000000000000000000074001274102451500236130ustar00rootroot00000000000000package hil import ( "strconv" "github.com/hashicorp/hil/ast" ) // NOTE: All builtins are tested in engine_test.go func registerBuiltins(scope *ast.BasicScope) *ast.BasicScope { if scope == nil { scope = new(ast.BasicScope) } if scope.FuncMap == nil { scope.FuncMap = make(map[string]ast.Function) } // Implicit conversions scope.FuncMap["__builtin_FloatToInt"] = builtinFloatToInt() scope.FuncMap["__builtin_FloatToString"] = builtinFloatToString() scope.FuncMap["__builtin_IntToFloat"] = builtinIntToFloat() scope.FuncMap["__builtin_IntToString"] = builtinIntToString() scope.FuncMap["__builtin_StringToInt"] = builtinStringToInt() scope.FuncMap["__builtin_StringToFloat"] = builtinStringToFloat() // Math operations scope.FuncMap["__builtin_IntMath"] = builtinIntMath() scope.FuncMap["__builtin_FloatMath"] = builtinFloatMath() return scope } func builtinFloatMath() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeInt}, Variadic: true, VariadicType: ast.TypeFloat, ReturnType: ast.TypeFloat, Callback: func(args []interface{}) (interface{}, error) { op := args[0].(ast.ArithmeticOp) result := args[1].(float64) for _, raw := range args[2:] { arg := raw.(float64) switch op { case ast.ArithmeticOpAdd: result += arg case ast.ArithmeticOpSub: result -= arg case ast.ArithmeticOpMul: result *= arg case ast.ArithmeticOpDiv: result /= arg } } return result, nil }, } } func builtinIntMath() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeInt}, Variadic: true, VariadicType: ast.TypeInt, ReturnType: ast.TypeInt, Callback: func(args []interface{}) (interface{}, error) { op := args[0].(ast.ArithmeticOp) result := args[1].(int) for _, raw := range args[2:] { arg := raw.(int) switch op { case ast.ArithmeticOpAdd: result += arg case ast.ArithmeticOpSub: result -= arg case ast.ArithmeticOpMul: result *= arg case ast.ArithmeticOpDiv: result /= arg case ast.ArithmeticOpMod: result = result % arg } } return result, nil }, } } func builtinFloatToInt() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat}, ReturnType: ast.TypeInt, Callback: func(args []interface{}) (interface{}, error) { return int(args[0].(float64)), nil }, } } func builtinFloatToString() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return strconv.FormatFloat( args[0].(float64), 'g', -1, 64), nil }, } } func builtinIntToFloat() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeInt}, ReturnType: ast.TypeFloat, Callback: func(args []interface{}) (interface{}, error) { return float64(args[0].(int)), nil }, } } func builtinIntToString() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeInt}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return strconv.FormatInt(int64(args[0].(int)), 10), nil }, } } func builtinStringToInt() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeInt}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { v, err := strconv.ParseInt(args[0].(string), 0, 0) if err != nil { return nil, err } return int(v), nil }, } } func builtinStringToFloat() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeFloat, Callback: func(args []interface{}) (interface{}, error) { v, err := strconv.ParseFloat(args[0].(string), 64) if err != nil { return nil, err } return v, nil }, } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/check_identifier.go000066400000000000000000000034661274102451500252510ustar00rootroot00000000000000package hil import ( "fmt" "sync" "github.com/hashicorp/hil/ast" ) // IdentifierCheck is a SemanticCheck that checks that all identifiers // resolve properly and that the right number of arguments are passed // to functions. type IdentifierCheck struct { Scope ast.Scope err error lock sync.Mutex } func (c *IdentifierCheck) Visit(root ast.Node) error { c.lock.Lock() defer c.lock.Unlock() defer c.reset() root.Accept(c.visit) return c.err } func (c *IdentifierCheck) visit(raw ast.Node) ast.Node { if c.err != nil { return raw } switch n := raw.(type) { case *ast.Call: c.visitCall(n) case *ast.VariableAccess: c.visitVariableAccess(n) case *ast.Output: // Ignore case *ast.LiteralNode: // Ignore default: // Ignore } // We never do replacement with this visitor return raw } func (c *IdentifierCheck) visitCall(n *ast.Call) { // Look up the function in the map function, ok := c.Scope.LookupFunc(n.Func) if !ok { c.createErr(n, fmt.Sprintf("unknown function called: %s", n.Func)) return } // Break up the args into what is variadic and what is required args := n.Args if function.Variadic && len(args) > len(function.ArgTypes) { args = n.Args[:len(function.ArgTypes)] } // Verify the number of arguments if len(args) != len(function.ArgTypes) { c.createErr(n, fmt.Sprintf( "%s: expected %d arguments, got %d", n.Func, len(function.ArgTypes), len(n.Args))) return } } func (c *IdentifierCheck) visitVariableAccess(n *ast.VariableAccess) { // Look up the variable in the map if _, ok := c.Scope.LookupVar(n.Name); !ok { c.createErr(n, fmt.Sprintf( "unknown variable accessed: %s", n.Name)) return } } func (c *IdentifierCheck) createErr(n ast.Node, str string) { c.err = fmt.Errorf("%s: %s", n.Pos(), str) } func (c *IdentifierCheck) reset() { c.err = nil } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/check_identifier_test.go000066400000000000000000000046501274102451500263040ustar00rootroot00000000000000package hil import ( "testing" "github.com/hashicorp/hil/ast" ) func TestIdentifierCheck(t *testing.T) { cases := []struct { Input string Scope ast.Scope Error bool }{ { "foo", &ast.BasicScope{}, false, }, { "foo ${bar} success", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: "baz", Type: ast.TypeString, }, }, }, false, }, { "foo ${bar}", &ast.BasicScope{}, true, }, { "foo ${rand()} success", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ReturnType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, }, { "foo ${rand()}", &ast.BasicScope{}, true, }, { "foo ${rand(42)} ", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ReturnType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, true, }, { "foo ${rand()} ", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeInt, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, }, { "foo ${rand(42)} ", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeInt, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, }, { "foo ${rand(\"foo\", 42)} ", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeInt, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, }, } for _, tc := range cases { node, err := Parse(tc.Input) if err != nil { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } visitor := &IdentifierCheck{Scope: tc.Scope} err = visitor.Visit(node) if err != nil != tc.Error { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/check_types.go000066400000000000000000000221341274102451500242640ustar00rootroot00000000000000package hil import ( "fmt" "sync" "github.com/hashicorp/hil/ast" ) // TypeCheck implements ast.Visitor for type checking an AST tree. // It requires some configuration to look up the type of nodes. // // It also optionally will not type error and will insert an implicit // type conversions for specific types if specified by the Implicit // field. Note that this is kind of organizationally weird to put into // this structure but we'd rather do that than duplicate the type checking // logic multiple times. type TypeCheck struct { Scope ast.Scope // Implicit is a map of implicit type conversions that we can do, // and that shouldn't error. The key of the first map is the from type, // the key of the second map is the to type, and the final string // value is the function to call (which must be registered in the Scope). Implicit map[ast.Type]map[ast.Type]string // Stack of types. This shouldn't be used directly except by implementations // of TypeCheckNode. Stack []ast.Type err error lock sync.Mutex } // TypeCheckNode is the interface that must be implemented by any // ast.Node that wants to support type-checking. If the type checker // encounters a node that doesn't implement this, it will error. type TypeCheckNode interface { TypeCheck(*TypeCheck) (ast.Node, error) } func (v *TypeCheck) Visit(root ast.Node) error { v.lock.Lock() defer v.lock.Unlock() defer v.reset() root.Accept(v.visit) return v.err } func (v *TypeCheck) visit(raw ast.Node) ast.Node { if v.err != nil { return raw } var result ast.Node var err error switch n := raw.(type) { case *ast.Arithmetic: tc := &typeCheckArithmetic{n} result, err = tc.TypeCheck(v) case *ast.Call: tc := &typeCheckCall{n} result, err = tc.TypeCheck(v) case *ast.Index: tc := &typeCheckIndex{n} result, err = tc.TypeCheck(v) case *ast.Output: tc := &typeCheckOutput{n} result, err = tc.TypeCheck(v) case *ast.LiteralNode: tc := &typeCheckLiteral{n} result, err = tc.TypeCheck(v) case *ast.VariableAccess: tc := &typeCheckVariableAccess{n} result, err = tc.TypeCheck(v) default: tc, ok := raw.(TypeCheckNode) if !ok { err = fmt.Errorf("unknown node for type check: %#v", raw) break } result, err = tc.TypeCheck(v) } if err != nil { pos := raw.Pos() v.err = fmt.Errorf("At column %d, line %d: %s", pos.Column, pos.Line, err) } return result } type typeCheckArithmetic struct { n *ast.Arithmetic } func (tc *typeCheckArithmetic) TypeCheck(v *TypeCheck) (ast.Node, error) { // The arguments are on the stack in reverse order, so pop them off. exprs := make([]ast.Type, len(tc.n.Exprs)) for i, _ := range tc.n.Exprs { exprs[len(tc.n.Exprs)-1-i] = v.StackPop() } // Determine the resulting type we want. We do this by going over // every expression until we find one with a type we recognize. // We do this because the first expr might be a string ("var.foo") // and we need to know what to implicit to. mathFunc := "__builtin_IntMath" mathType := ast.TypeInt for _, v := range exprs { exit := true switch v { case ast.TypeInt: mathFunc = "__builtin_IntMath" mathType = v case ast.TypeFloat: mathFunc = "__builtin_FloatMath" mathType = v default: exit = false } // We found the type, so leave if exit { break } } // Verify the args for i, arg := range exprs { if arg != mathType { cn := v.ImplicitConversion(exprs[i], mathType, tc.n.Exprs[i]) if cn != nil { tc.n.Exprs[i] = cn continue } return nil, fmt.Errorf( "operand %d should be %s, got %s", i+1, mathType, arg) } } // Modulo doesn't work for floats if mathType == ast.TypeFloat && tc.n.Op == ast.ArithmeticOpMod { return nil, fmt.Errorf("modulo cannot be used with floats") } // Return type v.StackPush(mathType) // Replace our node with a call to the proper function. This isn't // type checked but we already verified types. args := make([]ast.Node, len(tc.n.Exprs)+1) args[0] = &ast.LiteralNode{ Value: tc.n.Op, Typex: ast.TypeInt, Posx: tc.n.Pos(), } copy(args[1:], tc.n.Exprs) return &ast.Call{ Func: mathFunc, Args: args, Posx: tc.n.Pos(), }, nil } type typeCheckCall struct { n *ast.Call } func (tc *typeCheckCall) TypeCheck(v *TypeCheck) (ast.Node, error) { // Look up the function in the map function, ok := v.Scope.LookupFunc(tc.n.Func) if !ok { return nil, fmt.Errorf("unknown function called: %s", tc.n.Func) } // The arguments are on the stack in reverse order, so pop them off. args := make([]ast.Type, len(tc.n.Args)) for i, _ := range tc.n.Args { args[len(tc.n.Args)-1-i] = v.StackPop() } // Verify the args for i, expected := range function.ArgTypes { if expected == ast.TypeAny { continue } if args[i] != expected { cn := v.ImplicitConversion(args[i], expected, tc.n.Args[i]) if cn != nil { tc.n.Args[i] = cn continue } return nil, fmt.Errorf( "%s: argument %d should be %s, got %s", tc.n.Func, i+1, expected.Printable(), args[i].Printable()) } } // If we're variadic, then verify the types there if function.Variadic && function.VariadicType != ast.TypeAny { args = args[len(function.ArgTypes):] for i, t := range args { if t != function.VariadicType { realI := i + len(function.ArgTypes) cn := v.ImplicitConversion( t, function.VariadicType, tc.n.Args[realI]) if cn != nil { tc.n.Args[realI] = cn continue } return nil, fmt.Errorf( "%s: argument %d should be %s, got %s", tc.n.Func, realI, function.VariadicType.Printable(), t.Printable()) } } } // Return type v.StackPush(function.ReturnType) return tc.n, nil } type typeCheckOutput struct { n *ast.Output } func (tc *typeCheckOutput) TypeCheck(v *TypeCheck) (ast.Node, error) { n := tc.n types := make([]ast.Type, len(n.Exprs)) for i, _ := range n.Exprs { types[len(n.Exprs)-1-i] = v.StackPop() } // If there is only one argument and it is a list, we evaluate to a list if len(types) == 1 && types[0] == ast.TypeList { v.StackPush(ast.TypeList) return n, nil } // If there is only one argument and it is a map, we evaluate to a map if len(types) == 1 && types[0] == ast.TypeMap { v.StackPush(ast.TypeMap) return n, nil } // Otherwise, all concat args must be strings, so validate that for i, t := range types { if t != ast.TypeString { cn := v.ImplicitConversion(t, ast.TypeString, n.Exprs[i]) if cn != nil { n.Exprs[i] = cn continue } return nil, fmt.Errorf( "output of an HIL expression must be a string, or a single list (argument %d is %s)", i+1, t) } } // This always results in type string v.StackPush(ast.TypeString) return n, nil } type typeCheckLiteral struct { n *ast.LiteralNode } func (tc *typeCheckLiteral) TypeCheck(v *TypeCheck) (ast.Node, error) { v.StackPush(tc.n.Typex) return tc.n, nil } type typeCheckVariableAccess struct { n *ast.VariableAccess } func (tc *typeCheckVariableAccess) TypeCheck(v *TypeCheck) (ast.Node, error) { // Look up the variable in the map variable, ok := v.Scope.LookupVar(tc.n.Name) if !ok { return nil, fmt.Errorf( "unknown variable accessed: %s", tc.n.Name) } // Add the type to the stack v.StackPush(variable.Type) return tc.n, nil } type typeCheckIndex struct { n *ast.Index } func (tc *typeCheckIndex) TypeCheck(v *TypeCheck) (ast.Node, error) { // Ensure we have a VariableAccess as the target varAccessNode, ok := tc.n.Target.(*ast.VariableAccess) if !ok { return nil, fmt.Errorf("target of an index must be a VariableAccess node, was %T", tc.n.Target) } // Get the variable variable, ok := v.Scope.LookupVar(varAccessNode.Name) if !ok { return nil, fmt.Errorf("unknown variable accessed: %s", varAccessNode.Name) } keyType, err := tc.n.Key.Type(v.Scope) if err != nil { return nil, err } switch variable.Type { case ast.TypeList: if keyType != ast.TypeInt { return nil, fmt.Errorf("key of an index must be an int, was %s", keyType) } valType, err := ast.VariableListElementTypesAreHomogenous(varAccessNode.Name, variable.Value.([]ast.Variable)) if err != nil { return tc.n, err } v.StackPush(valType) return tc.n, nil case ast.TypeMap: if keyType != ast.TypeString { return nil, fmt.Errorf("key of an index must be a string, was %s", keyType) } valType, err := ast.VariableMapValueTypesAreHomogenous(varAccessNode.Name, variable.Value.(map[string]ast.Variable)) if err != nil { return tc.n, err } v.StackPush(valType) return tc.n, nil default: return nil, fmt.Errorf("invalid index operation into non-indexable type: %s", variable.Type) } } func (v *TypeCheck) ImplicitConversion( actual ast.Type, expected ast.Type, n ast.Node) ast.Node { if v.Implicit == nil { return nil } fromMap, ok := v.Implicit[actual] if !ok { return nil } toFunc, ok := fromMap[expected] if !ok { return nil } return &ast.Call{ Func: toFunc, Args: []ast.Node{n}, Posx: n.Pos(), } } func (v *TypeCheck) reset() { v.Stack = nil v.err = nil } func (v *TypeCheck) StackPush(t ast.Type) { v.Stack = append(v.Stack, t) } func (v *TypeCheck) StackPop() ast.Type { var x ast.Type x, v.Stack = v.Stack[len(v.Stack)-1], v.Stack[:len(v.Stack)-1] return x } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/check_types_test.go000066400000000000000000000156251274102451500253320ustar00rootroot00000000000000package hil import ( "testing" "github.com/hashicorp/hil/ast" ) func TestTypeCheck(t *testing.T) { cases := []struct { Input string Scope ast.Scope Error bool }{ { "foo", &ast.BasicScope{}, false, }, { "foo ${bar}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: "baz", Type: ast.TypeString, }, }, }, false, }, { "foo ${rand()}", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ReturnType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, }, { `foo ${rand("42")}`, &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, }, { `foo ${rand(42)}`, &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, true, }, { `foo ${rand()}`, &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ArgTypes: nil, ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, }, { `foo ${rand("42")}`, &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ArgTypes: nil, ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, }, { `foo ${rand("42", 42)}`, &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ArgTypes: nil, ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, true, }, { "${foo[0]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "Hello", }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, }, }, false, }, { "${foo[0]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 3, }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, }, }, true, }, { "${foo[0]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "Hello World", }, }, }, true, }, { "foo ${bar}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: 42, Type: ast.TypeInt, }, }, }, true, }, { "foo ${rand()}", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ReturnType: ast.TypeInt, Callback: func([]interface{}) (interface{}, error) { return 42, nil }, }, }, }, true, }, } for _, tc := range cases { node, err := Parse(tc.Input) if err != nil { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } visitor := &TypeCheck{Scope: tc.Scope} err = visitor.Visit(node) if err != nil != tc.Error { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } } } func TestTypeCheck_implicit(t *testing.T) { implicitMap := map[ast.Type]map[ast.Type]string{ ast.TypeInt: { ast.TypeString: "intToString", }, } cases := []struct { Input string Scope *ast.BasicScope Error bool }{ { "foo ${bar}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: 42, Type: ast.TypeInt, }, }, }, false, }, { "foo ${foo(42)}", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "foo": ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, }, }, }, false, }, { `foo ${foo("42", 42)}`, &ast.BasicScope{ FuncMap: map[string]ast.Function{ "foo": ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, Variadic: true, VariadicType: ast.TypeString, ReturnType: ast.TypeString, }, }, }, false, }, { "${foo[1]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 42, }, ast.Variable{ Type: ast.TypeInt, Value: 23, }, }, }, }, }, false, }, { `${foo[bar[var.keyint]]}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, "bar": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "i dont exist", }, ast.Variable{ Type: ast.TypeString, Value: "bar", }, }, }, "var.key": ast.Variable{ Type: ast.TypeInt, Value: 1, }, }, }, false, }, } for _, tc := range cases { node, err := Parse(tc.Input) if err != nil { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } // Modify the scope to add our conversion functions. if tc.Scope.FuncMap == nil { tc.Scope.FuncMap = make(map[string]ast.Function) } tc.Scope.FuncMap["intToString"] = ast.Function{ ArgTypes: []ast.Type{ast.TypeInt}, ReturnType: ast.TypeString, } // Do the first pass... visitor := &TypeCheck{Scope: tc.Scope, Implicit: implicitMap} err = visitor.Visit(node) if err != nil != tc.Error { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } if err != nil { continue } // If we didn't error, then the next type check should not fail // WITHOUT implicits. visitor = &TypeCheck{Scope: tc.Scope} err = visitor.Visit(node) if err != nil { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/convert.go000066400000000000000000000076211274102451500234470ustar00rootroot00000000000000package hil import ( "fmt" "reflect" "github.com/hashicorp/hil/ast" "github.com/mitchellh/mapstructure" ) var hilMapstructureDecodeHookSlice []interface{} var hilMapstructureDecodeHookStringSlice []string var hilMapstructureDecodeHookMap map[string]interface{} // hilMapstructureWeakDecode behaves in the same way as mapstructure.WeakDecode // but has a DecodeHook which defeats the backward compatibility mode of mapstructure // which WeakDecodes []interface{}{} into an empty map[string]interface{}. This // allows us to use WeakDecode (desirable), but not fail on empty lists. func hilMapstructureWeakDecode(m interface{}, rawVal interface{}) error { config := &mapstructure.DecoderConfig{ DecodeHook: func(source reflect.Type, target reflect.Type, val interface{}) (interface{}, error) { sliceType := reflect.TypeOf(hilMapstructureDecodeHookSlice) stringSliceType := reflect.TypeOf(hilMapstructureDecodeHookStringSlice) mapType := reflect.TypeOf(hilMapstructureDecodeHookMap) if (source == sliceType || source == stringSliceType) && target == mapType { return nil, fmt.Errorf("Cannot convert %s into a %s", source, target) } return val, nil }, WeaklyTypedInput: true, Result: rawVal, } decoder, err := mapstructure.NewDecoder(config) if err != nil { return err } return decoder.Decode(m) } func InterfaceToVariable(input interface{}) (ast.Variable, error) { if inputVariable, ok := input.(ast.Variable); ok { return inputVariable, nil } var stringVal string if err := hilMapstructureWeakDecode(input, &stringVal); err == nil { return ast.Variable{ Type: ast.TypeString, Value: stringVal, }, nil } var mapVal map[string]interface{} if err := hilMapstructureWeakDecode(input, &mapVal); err == nil { elements := make(map[string]ast.Variable) for i, element := range mapVal { varElement, err := InterfaceToVariable(element) if err != nil { return ast.Variable{}, err } elements[i] = varElement } return ast.Variable{ Type: ast.TypeMap, Value: elements, }, nil } var sliceVal []interface{} if err := hilMapstructureWeakDecode(input, &sliceVal); err == nil { elements := make([]ast.Variable, len(sliceVal)) for i, element := range sliceVal { varElement, err := InterfaceToVariable(element) if err != nil { return ast.Variable{}, err } elements[i] = varElement } return ast.Variable{ Type: ast.TypeList, Value: elements, }, nil } return ast.Variable{}, fmt.Errorf("value for conversion must be a string, interface{} or map[string]interface: got %T", input) } func VariableToInterface(input ast.Variable) (interface{}, error) { if input.Type == ast.TypeString { if inputStr, ok := input.Value.(string); ok { return inputStr, nil } else { return nil, fmt.Errorf("ast.Variable with type string has value which is not a string") } } if input.Type == ast.TypeList { inputList, ok := input.Value.([]ast.Variable) if !ok { return nil, fmt.Errorf("ast.Variable with type list has value which is not a []ast.Variable") } result := make([]interface{}, 0) if len(inputList) == 0 { return result, nil } for _, element := range inputList { if convertedElement, err := VariableToInterface(element); err == nil { result = append(result, convertedElement) } else { return nil, err } } return result, nil } if input.Type == ast.TypeMap { inputMap, ok := input.Value.(map[string]ast.Variable) if !ok { return nil, fmt.Errorf("ast.Variable with type map has value which is not a map[string]ast.Variable") } result := make(map[string]interface{}, 0) if len(inputMap) == 0 { return result, nil } for key, value := range inputMap { if convertedValue, err := VariableToInterface(value); err == nil { result[key] = convertedValue } else { return nil, err } } return result, nil } return nil, fmt.Errorf("unknown input type: %s", input.Type) } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/convert_test.go000066400000000000000000000166761274102451500245200ustar00rootroot00000000000000package hil import ( "reflect" "testing" "github.com/hashicorp/hil/ast" ) func TestInterfaceToVariable_variableInput(t *testing.T) { _, err := InterfaceToVariable(ast.Variable{ Type: ast.TypeString, Value: "Hello world", }) if err != nil { t.Fatalf("Bad: %s", err) } } func TestInterfaceToVariable(t *testing.T) { testCases := []struct { name string input interface{} expected ast.Variable }{ { name: "string", input: "Hello world", expected: ast.Variable{ Type: ast.TypeString, Value: "Hello world", }, }, { name: "empty list", input: []interface{}{}, expected: ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{}, }, }, { name: "empty list of strings", input: []string{}, expected: ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{}, }, }, { name: "int", input: 1, expected: ast.Variable{ Type: ast.TypeString, Value: "1", }, }, { name: "list of strings", input: []string{"Hello", "World"}, expected: ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Hello", }, { Type: ast.TypeString, Value: "World", }, }, }, }, { name: "list of lists of strings", input: [][]interface{}{[]interface{}{"Hello", "World"}, []interface{}{"Goodbye", "World"}}, expected: ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Hello", }, { Type: ast.TypeString, Value: "World", }, }, }, { Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Goodbye", }, { Type: ast.TypeString, Value: "World", }, }, }, }, }, }, { name: "map of string->string", input: map[string]string{"Hello": "World", "Foo": "Bar"}, expected: ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "Hello": { Type: ast.TypeString, Value: "World", }, "Foo": { Type: ast.TypeString, Value: "Bar", }, }, }, }, { name: "map of lists of strings", input: map[string][]string{ "Hello": []string{"Hello", "World"}, "Goodbye": []string{"Goodbye", "World"}, }, expected: ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "Hello": { Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Hello", }, { Type: ast.TypeString, Value: "World", }, }, }, "Goodbye": { Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Goodbye", }, { Type: ast.TypeString, Value: "World", }, }, }, }, }, }, { name: "empty map", input: map[string]string{}, expected: ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{}, }, }, { name: "three-element map", input: map[string]string{ "us-west-1": "ami-123456", "us-west-2": "ami-456789", "eu-west-1": "ami-012345", }, expected: ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "us-west-1": { Type: ast.TypeString, Value: "ami-123456", }, "us-west-2": { Type: ast.TypeString, Value: "ami-456789", }, "eu-west-1": { Type: ast.TypeString, Value: "ami-012345", }, }, }, }, } for _, tc := range testCases { output, err := InterfaceToVariable(tc.input) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(output, tc.expected) { t.Fatalf("%s:\nExpected: %s\n Got: %s\n", tc.name, tc.expected, output) } } } func TestVariableToInterface(t *testing.T) { testCases := []struct { name string expected interface{} input ast.Variable }{ { name: "string", expected: "Hello world", input: ast.Variable{ Type: ast.TypeString, Value: "Hello world", }, }, { name: "empty list", expected: []interface{}{}, input: ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{}, }, }, { name: "int", expected: "1", input: ast.Variable{ Type: ast.TypeString, Value: "1", }, }, { name: "list of strings", expected: []interface{}{"Hello", "World"}, input: ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Hello", }, { Type: ast.TypeString, Value: "World", }, }, }, }, { name: "list of lists of strings", expected: []interface{}{[]interface{}{"Hello", "World"}, []interface{}{"Goodbye", "World"}}, input: ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Hello", }, { Type: ast.TypeString, Value: "World", }, }, }, { Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Goodbye", }, { Type: ast.TypeString, Value: "World", }, }, }, }, }, }, { name: "map of string->string", expected: map[string]interface{}{"Hello": "World", "Foo": "Bar"}, input: ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "Hello": { Type: ast.TypeString, Value: "World", }, "Foo": { Type: ast.TypeString, Value: "Bar", }, }, }, }, { name: "map of lists of strings", expected: map[string]interface{}{ "Hello": []interface{}{"Hello", "World"}, "Goodbye": []interface{}{"Goodbye", "World"}, }, input: ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "Hello": { Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Hello", }, { Type: ast.TypeString, Value: "World", }, }, }, "Goodbye": { Type: ast.TypeList, Value: []ast.Variable{ { Type: ast.TypeString, Value: "Goodbye", }, { Type: ast.TypeString, Value: "World", }, }, }, }, }, }, { name: "empty map", expected: map[string]interface{}{}, input: ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{}, }, }, { name: "three-element map", expected: map[string]interface{}{ "us-west-1": "ami-123456", "us-west-2": "ami-456789", "eu-west-1": "ami-012345", }, input: ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "us-west-1": { Type: ast.TypeString, Value: "ami-123456", }, "us-west-2": { Type: ast.TypeString, Value: "ami-456789", }, "eu-west-1": { Type: ast.TypeString, Value: "ami-012345", }, }, }, }, } for _, tc := range testCases { output, err := VariableToInterface(tc.input) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(output, tc.expected) { t.Fatalf("%s:\nExpected: %s\n Got: %s\n", tc.name, tc.expected, output) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/eval.go000066400000000000000000000246621274102451500227220ustar00rootroot00000000000000package hil import ( "bytes" "fmt" "sync" "github.com/hashicorp/hil/ast" ) // EvalConfig is the configuration for evaluating. type EvalConfig struct { // GlobalScope is the global scope of execution for evaluation. GlobalScope *ast.BasicScope // SemanticChecks is a list of additional semantic checks that will be run // on the tree prior to evaluating it. The type checker, identifier checker, // etc. will be run before these automatically. SemanticChecks []SemanticChecker } // SemanticChecker is the type that must be implemented to do a // semantic check on an AST tree. This will be called with the root node. type SemanticChecker func(ast.Node) error // EvalType represents the type of the output returned from a HIL // evaluation. type EvalType uint32 const ( TypeInvalid EvalType = 0 TypeString EvalType = 1 << iota TypeList TypeMap ) //go:generate stringer -type=EvalType // EvaluationResult is a struct returned from the hil.Eval function, // representing the result of an interpolation. Results are returned in their // "natural" Go structure rather than in terms of the HIL AST. For the types // currently implemented, this means that the Value field can be interpreted as // the following Go types: // TypeInvalid: undefined // TypeString: string // TypeList: []interface{} // TypeMap: map[string]interface{} type EvaluationResult struct { Type EvalType Value interface{} } // InvalidResult is a structure representing the result of a HIL interpolation // which has invalid syntax, missing variables, or some other type of error. // The error is described out of band in the accompanying error return value. var InvalidResult = EvaluationResult{Type: TypeInvalid, Value: nil} func Eval(root ast.Node, config *EvalConfig) (EvaluationResult, error) { output, outputType, err := internalEval(root, config) if err != nil { return InvalidResult, err } switch outputType { case ast.TypeList: val, err := VariableToInterface(ast.Variable{ Type: ast.TypeList, Value: output, }) return EvaluationResult{ Type: TypeList, Value: val, }, err case ast.TypeMap: val, err := VariableToInterface(ast.Variable{ Type: ast.TypeMap, Value: output, }) return EvaluationResult{ Type: TypeMap, Value: val, }, err case ast.TypeString: return EvaluationResult{ Type: TypeString, Value: output, }, nil default: return InvalidResult, fmt.Errorf("unknown type %s as interpolation output", outputType) } } // Eval evaluates the given AST tree and returns its output value, the type // of the output, and any error that occurred. func internalEval(root ast.Node, config *EvalConfig) (interface{}, ast.Type, error) { // Copy the scope so we can add our builtins if config == nil { config = new(EvalConfig) } scope := registerBuiltins(config.GlobalScope) implicitMap := map[ast.Type]map[ast.Type]string{ ast.TypeFloat: { ast.TypeInt: "__builtin_FloatToInt", ast.TypeString: "__builtin_FloatToString", }, ast.TypeInt: { ast.TypeFloat: "__builtin_IntToFloat", ast.TypeString: "__builtin_IntToString", }, ast.TypeString: { ast.TypeInt: "__builtin_StringToInt", ast.TypeFloat: "__builtin_StringToFloat", }, } // Build our own semantic checks that we always run tv := &TypeCheck{Scope: scope, Implicit: implicitMap} ic := &IdentifierCheck{Scope: scope} // Build up the semantic checks for execution checks := make( []SemanticChecker, len(config.SemanticChecks), len(config.SemanticChecks)+2) copy(checks, config.SemanticChecks) checks = append(checks, ic.Visit) checks = append(checks, tv.Visit) // Run the semantic checks for _, check := range checks { if err := check(root); err != nil { return nil, ast.TypeInvalid, err } } // Execute v := &evalVisitor{Scope: scope} return v.Visit(root) } // EvalNode is the interface that must be implemented by any ast.Node // to support evaluation. This will be called in visitor pattern order. // The result of each call to Eval is automatically pushed onto the // stack as a LiteralNode. Pop elements off the stack to get child // values. type EvalNode interface { Eval(ast.Scope, *ast.Stack) (interface{}, ast.Type, error) } type evalVisitor struct { Scope ast.Scope Stack ast.Stack err error lock sync.Mutex } func (v *evalVisitor) Visit(root ast.Node) (interface{}, ast.Type, error) { // Run the actual visitor pattern root.Accept(v.visit) // Get our result and clear out everything else var result *ast.LiteralNode if v.Stack.Len() > 0 { result = v.Stack.Pop().(*ast.LiteralNode) } else { result = new(ast.LiteralNode) } resultErr := v.err // Clear everything else so we aren't just dangling v.Stack.Reset() v.err = nil t, err := result.Type(v.Scope) if err != nil { return nil, ast.TypeInvalid, err } return result.Value, t, resultErr } func (v *evalVisitor) visit(raw ast.Node) ast.Node { if v.err != nil { return raw } en, err := evalNode(raw) if err != nil { v.err = err return raw } out, outType, err := en.Eval(v.Scope, &v.Stack) if err != nil { v.err = err return raw } v.Stack.Push(&ast.LiteralNode{ Value: out, Typex: outType, }) return raw } // evalNode is a private function that returns an EvalNode for built-in // types as well as any other EvalNode implementations. func evalNode(raw ast.Node) (EvalNode, error) { switch n := raw.(type) { case *ast.Index: return &evalIndex{n}, nil case *ast.Call: return &evalCall{n}, nil case *ast.Output: return &evalOutput{n}, nil case *ast.LiteralNode: return &evalLiteralNode{n}, nil case *ast.VariableAccess: return &evalVariableAccess{n}, nil default: en, ok := n.(EvalNode) if !ok { return nil, fmt.Errorf("node doesn't support evaluation: %#v", raw) } return en, nil } } type evalCall struct{ *ast.Call } func (v *evalCall) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { // Look up the function in the map function, ok := s.LookupFunc(v.Func) if !ok { return nil, ast.TypeInvalid, fmt.Errorf( "unknown function called: %s", v.Func) } // The arguments are on the stack in reverse order, so pop them off. args := make([]interface{}, len(v.Args)) for i, _ := range v.Args { node := stack.Pop().(*ast.LiteralNode) args[len(v.Args)-1-i] = node.Value } // Call the function result, err := function.Callback(args) if err != nil { return nil, ast.TypeInvalid, fmt.Errorf("%s: %s", v.Func, err) } return result, function.ReturnType, nil } type evalIndex struct{ *ast.Index } func (v *evalIndex) Eval(scope ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { evalVarAccess, err := evalNode(v.Target) if err != nil { return nil, ast.TypeInvalid, err } target, targetType, err := evalVarAccess.Eval(scope, stack) evalKey, err := evalNode(v.Key) if err != nil { return nil, ast.TypeInvalid, err } key, keyType, err := evalKey.Eval(scope, stack) if err != nil { return nil, ast.TypeInvalid, err } variableName := v.Index.Target.(*ast.VariableAccess).Name switch targetType { case ast.TypeList: if keyType != ast.TypeInt { return nil, ast.TypeInvalid, fmt.Errorf("key for indexing list %q must be an int, is %s", variableName, keyType) } return v.evalListIndex(variableName, target, key) case ast.TypeMap: if keyType != ast.TypeString { return nil, ast.TypeInvalid, fmt.Errorf("key for indexing map %q must be a string, is %s", variableName, keyType) } return v.evalMapIndex(variableName, target, key) default: return nil, ast.TypeInvalid, fmt.Errorf("target %q for indexing must be ast.TypeList or ast.TypeMap, is %s", variableName, targetType) } } func (v *evalIndex) evalListIndex(variableName string, target interface{}, key interface{}) (interface{}, ast.Type, error) { // We assume type checking was already done and we can assume that target // is a list and key is an int list, ok := target.([]ast.Variable) if !ok { return nil, ast.TypeInvalid, fmt.Errorf("cannot cast target to []Variable") } keyInt, ok := key.(int) if !ok { return nil, ast.TypeInvalid, fmt.Errorf("cannot cast key to int") } if len(list) == 0 { return nil, ast.TypeInvalid, fmt.Errorf("list is empty") } if keyInt < 0 || len(list) < keyInt+1 { return nil, ast.TypeInvalid, fmt.Errorf("index %d out of range for list %s (max %d)", keyInt, variableName, len(list)) } returnVal := list[keyInt].Value returnType := list[keyInt].Type return returnVal, returnType, nil } func (v *evalIndex) evalMapIndex(variableName string, target interface{}, key interface{}) (interface{}, ast.Type, error) { // We assume type checking was already done and we can assume that target // is a map and key is a string vmap, ok := target.(map[string]ast.Variable) if !ok { return nil, ast.TypeInvalid, fmt.Errorf("cannot cast target to map[string]Variable") } keyString, ok := key.(string) if !ok { return nil, ast.TypeInvalid, fmt.Errorf("cannot cast key to string") } if len(vmap) == 0 { return nil, ast.TypeInvalid, fmt.Errorf("map is empty") } value, ok := vmap[keyString] if !ok { return nil, ast.TypeInvalid, fmt.Errorf("key %q does not exist in map %s", keyString, variableName) } return value.Value, value.Type, nil } type evalOutput struct{ *ast.Output } func (v *evalOutput) Eval(s ast.Scope, stack *ast.Stack) (interface{}, ast.Type, error) { // The expressions should all be on the stack in reverse // order. So pop them off, reverse their order, and concatenate. nodes := make([]*ast.LiteralNode, 0, len(v.Exprs)) for range v.Exprs { nodes = append(nodes, stack.Pop().(*ast.LiteralNode)) } // Special case the single list and map if len(nodes) == 1 && nodes[0].Typex == ast.TypeList { return nodes[0].Value, ast.TypeList, nil } if len(nodes) == 1 && nodes[0].Typex == ast.TypeMap { return nodes[0].Value, ast.TypeMap, nil } // Otherwise concatenate the strings var buf bytes.Buffer for i := len(nodes) - 1; i >= 0; i-- { buf.WriteString(nodes[i].Value.(string)) } return buf.String(), ast.TypeString, nil } type evalLiteralNode struct{ *ast.LiteralNode } func (v *evalLiteralNode) Eval(ast.Scope, *ast.Stack) (interface{}, ast.Type, error) { return v.Value, v.Typex, nil } type evalVariableAccess struct{ *ast.VariableAccess } func (v *evalVariableAccess) Eval(scope ast.Scope, _ *ast.Stack) (interface{}, ast.Type, error) { // Look up the variable in the map variable, ok := scope.LookupVar(v.Name) if !ok { return nil, ast.TypeInvalid, fmt.Errorf( "unknown variable accessed: %s", v.Name) } return variable.Value, variable.Type, nil } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/eval_test.go000066400000000000000000000457021274102451500237570ustar00rootroot00000000000000package hil import ( "reflect" "strconv" "testing" "github.com/hashicorp/hil/ast" ) func TestEval(t *testing.T) { cases := []struct { Input string Scope *ast.BasicScope Error bool Result interface{} ResultType EvalType }{ { Input: "Hello World", Scope: nil, Result: "Hello World", ResultType: TypeString, }, { "${var.alist}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.alist": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "Hello", }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, }, }, false, []interface{}{"Hello", "World"}, TypeList, }, { "${var.alist[1]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.alist": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "Hello", }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, }, }, false, "World", TypeString, }, { "${var.alist[1]} ${var.alist[0]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.alist": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "Hello", }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, }, }, false, "World Hello", TypeString, }, { "${var.alist} ${var.alist}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.alist": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "Hello", }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, }, }, true, nil, TypeInvalid, }, { `${foo}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, false, map[string]interface{}{ "foo": "hello", "bar": "world", }, TypeMap, }, { `${foo["bar"]}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, false, "world", TypeString, }, { `${foo["bar"]} ${foo["foo"]}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, false, "world hello", TypeString, }, { `${foo} ${foo}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, true, nil, TypeInvalid, }, { `${foo} ${bar}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "Hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, false, "Hello World", TypeString, }, { `${foo} ${bar}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "Hello", }, "bar": ast.Variable{ Type: ast.TypeInt, Value: 4, }, }, }, false, "Hello 4", TypeString, }, { `${foo}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, false, map[string]interface{}{ "foo": "hello", "bar": "world", }, TypeMap, }, { "${var.alist}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.alist": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "Hello", }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, }, }, false, []interface{}{ "Hello", "World", }, TypeList, }, } for _, tc := range cases { node, err := Parse(tc.Input) if err != nil { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } result, err := Eval(node, &EvalConfig{GlobalScope: tc.Scope}) if err != nil != tc.Error { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } if tc.ResultType != TypeInvalid && result.Type != tc.ResultType { t.Fatalf("Bad: %s\n\nInput: %s", result.Type, tc.Input) } if !reflect.DeepEqual(result.Value, tc.Result) { t.Fatalf("\n Bad: %#v\nExpected: %#v\n\nInput: %s", result.Value, tc.Result, tc.Input) } } } func TestEvalInternal(t *testing.T) { cases := []struct { Input string Scope *ast.BasicScope Error bool Result interface{} ResultType ast.Type }{ { "foo", nil, false, "foo", ast.TypeString, }, { "foo ${bar}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: "baz", Type: ast.TypeString, }, }, }, false, "foo baz", ast.TypeString, }, { "${var.alist}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.alist": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "Hello", }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, }, }, }, false, []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "Hello", }, ast.Variable{ Type: ast.TypeString, Value: "World", }, }, ast.TypeList, }, { "foo ${-29}", nil, false, "foo -29", ast.TypeString, }, { "foo ${42+1}", nil, false, "foo 43", ast.TypeString, }, { "foo ${42-1}", nil, false, "foo 41", ast.TypeString, }, { "foo ${42*2}", nil, false, "foo 84", ast.TypeString, }, { "foo ${42/2}", nil, false, "foo 21", ast.TypeString, }, { "foo ${42%4}", nil, false, "foo 2", ast.TypeString, }, { "foo ${42.0+1.0}", nil, false, "foo 43", ast.TypeString, }, { "foo ${42.0+1}", nil, false, "foo 43", ast.TypeString, }, { "foo ${42+1.0}", nil, false, "foo 43", ast.TypeString, }, { "foo ${42+2*2}", nil, false, "foo 88", ast.TypeString, }, { "foo ${42+(2*2)}", nil, false, "foo 46", ast.TypeString, }, { "foo ${-bar}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: 41, Type: ast.TypeInt, }, }, }, false, "foo -41", ast.TypeString, }, { "foo ${bar+1}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: 41, Type: ast.TypeInt, }, }, }, false, "foo 42", ast.TypeString, }, { "foo ${bar+1}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: "41", Type: ast.TypeString, }, }, }, false, "foo 42", ast.TypeString, }, { "foo ${bar+baz}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: "41", Type: ast.TypeString, }, "baz": ast.Variable{ Value: "1", Type: ast.TypeString, }, }, }, false, "foo 42", ast.TypeString, }, { "foo ${rand()}", &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ReturnType: ast.TypeString, Callback: func([]interface{}) (interface{}, error) { return "42", nil }, }, }, }, false, "foo 42", ast.TypeString, }, { `foo ${rand("foo", "bar")}`, &ast.BasicScope{ FuncMap: map[string]ast.Function{ "rand": ast.Function{ ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { var result string for _, a := range args { result += a.(string) } return result, nil }, }, }, }, false, "foo foobar", ast.TypeString, }, { `${foo["bar"]}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "hello", }, ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, true, nil, ast.TypeInvalid, }, { `${foo["bar"]}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, false, "world", ast.TypeString, }, { `${foo[var.key]}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, "var.key": ast.Variable{ Type: ast.TypeString, Value: "bar", }, }, }, false, "world", ast.TypeString, }, { `${foo[bar[var.keyint]]}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, "bar": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "i dont exist", }, ast.Variable{ Type: ast.TypeString, Value: "bar", }, }, }, "var.keyint": ast.Variable{ Type: ast.TypeInt, Value: 1, }, }, }, false, "world", ast.TypeString, }, { `${foo["bar"]} ${bar[1]}`, &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeMap, Value: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeString, Value: "hello", }, "bar": ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, "bar": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 10, }, ast.Variable{ Type: ast.TypeInt, Value: 20, }, }, }, }, }, false, "world 20", ast.TypeString, }, { "${foo[0]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "hello", }, ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, false, "hello", ast.TypeString, }, { "${foo[bar]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "hello", }, ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, "bar": ast.Variable{ Type: ast.TypeInt, Value: 1, }, }, }, false, "world", ast.TypeString, }, { "${foo[bar[1]]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "hello", }, ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, "bar": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 1, }, ast.Variable{ Type: ast.TypeInt, Value: 0, }, }, }, }, }, false, "hello", ast.TypeString, }, { "aaa ${foo} aaa", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeInt, Value: 42, }, }, }, false, "aaa 42 aaa", ast.TypeString, }, { "aaa ${foo[1]} aaa", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 42, }, ast.Variable{ Type: ast.TypeInt, Value: 24, }, }, }, }, }, false, "aaa 24 aaa", ast.TypeString, }, { "aaa ${foo[1]} - ${foo[0]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 42, }, ast.Variable{ Type: ast.TypeInt, Value: 24, }, }, }, }, }, false, "aaa 24 - 42", ast.TypeString, }, { "${var.foo} ${var.foo[0]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "hello", }, ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, true, nil, ast.TypeInvalid, }, { "${var.foo[0]} ${var.foo[1]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeString, Value: "hello", }, ast.Variable{ Type: ast.TypeString, Value: "world", }, }, }, }, }, false, "hello world", ast.TypeString, }, { "${foo[1]} ${foo[0]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 42, }, ast.Variable{ Type: ast.TypeInt, Value: 24, }, }, }, }, }, false, "24 42", ast.TypeString, }, { "${foo[1-3]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 42, }, ast.Variable{ Type: ast.TypeInt, Value: 24, }, }, }, }, }, true, nil, ast.TypeInvalid, }, { "${foo[2]}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "foo": ast.Variable{ Type: ast.TypeList, Value: []ast.Variable{ ast.Variable{ Type: ast.TypeInt, Value: 42, }, ast.Variable{ Type: ast.TypeInt, Value: 24, }, }, }, }, }, true, nil, ast.TypeInvalid, }, // Testing implicit type conversions { "foo ${bar}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: 42, Type: ast.TypeInt, }, }, }, false, "foo 42", ast.TypeString, }, { `foo ${foo("42")}`, &ast.BasicScope{ FuncMap: map[string]ast.Function{ "foo": ast.Function{ ArgTypes: []ast.Type{ast.TypeInt}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return strconv.FormatInt(int64(args[0].(int)), 10), nil }, }, }, }, false, "foo 42", ast.TypeString, }, // Multiline { "foo ${42+\n1.0}", nil, false, "foo 43", ast.TypeString, }, // String vars should be able to implictly convert to floats { "${1.5 * var.foo}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.foo": ast.Variable{ Value: "42", Type: ast.TypeString, }, }, }, false, "63", ast.TypeString, }, // Unary { "foo ${-46}", nil, false, "foo -46", ast.TypeString, }, { "foo ${-46 + 5}", nil, false, "foo -41", ast.TypeString, }, { "foo ${46 + -5}", nil, false, "foo 41", ast.TypeString, }, { "foo ${-bar}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: 41, Type: ast.TypeInt, }, }, }, false, "foo -41", ast.TypeString, }, { "foo ${5 + -bar}", &ast.BasicScope{ VarMap: map[string]ast.Variable{ "bar": ast.Variable{ Value: 41, Type: ast.TypeInt, }, }, }, false, "foo -36", ast.TypeString, }, } for _, tc := range cases { node, err := Parse(tc.Input) if err != nil { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } out, outType, err := internalEval(node, &EvalConfig{GlobalScope: tc.Scope}) if err != nil != tc.Error { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } if tc.ResultType != ast.TypeInvalid && outType != tc.ResultType { t.Fatalf("Bad: %s\n\nInput: %s", outType, tc.Input) } if !reflect.DeepEqual(out, tc.Result) { t.Fatalf("Bad: %#v\n\nInput: %s", out, tc.Input) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/evaltype_string.go000066400000000000000000000012131274102451500251750ustar00rootroot00000000000000// Code generated by "stringer -type=EvalType"; DO NOT EDIT package hil import "fmt" const ( _EvalType_name_0 = "TypeInvalid" _EvalType_name_1 = "TypeString" _EvalType_name_2 = "TypeList" _EvalType_name_3 = "TypeMap" ) var ( _EvalType_index_0 = [...]uint8{0, 11} _EvalType_index_1 = [...]uint8{0, 10} _EvalType_index_2 = [...]uint8{0, 8} _EvalType_index_3 = [...]uint8{0, 7} ) func (i EvalType) String() string { switch { case i == 0: return _EvalType_name_0 case i == 2: return _EvalType_name_1 case i == 4: return _EvalType_name_2 case i == 8: return _EvalType_name_3 default: return fmt.Sprintf("EvalType(%d)", i) } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/example_func_test.go000066400000000000000000000017551274102451500254760ustar00rootroot00000000000000package hil_test import ( "fmt" "log" "strings" "github.com/hashicorp/hil" "github.com/hashicorp/hil/ast" ) func Example_functions() { input := "${lower(var.test)} - ${6 + 2}" tree, err := hil.Parse(input) if err != nil { log.Fatal(err) } lowerCase := ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Variadic: false, Callback: func(inputs []interface{}) (interface{}, error) { input := inputs[0].(string) return strings.ToLower(input), nil }, } config := &hil.EvalConfig{ GlobalScope: &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.test": ast.Variable{ Type: ast.TypeString, Value: "TEST STRING", }, }, FuncMap: map[string]ast.Function{ "lower": lowerCase, }, }, } result, err := hil.Eval(tree, config) if err != nil { log.Fatal(err) } fmt.Printf("Type: %s\n", result.Type) fmt.Printf("Value: %s\n", result.Value) // Output: // Type: TypeString // Value: test string - 8 } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/example_test.go000066400000000000000000000006231274102451500244540ustar00rootroot00000000000000package hil_test import ( "fmt" "log" "github.com/hashicorp/hil" ) func Example_basic() { input := "${6 + 2}" tree, err := hil.Parse(input) if err != nil { log.Fatal(err) } result, err := hil.Eval(tree, &hil.EvalConfig{}) if err != nil { log.Fatal(err) } fmt.Printf("Type: %s\n", result.Type) fmt.Printf("Value: %s\n", result.Value) // Output: // Type: TypeString // Value: 8 } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/example_var_test.go000066400000000000000000000012231274102451500253210ustar00rootroot00000000000000package hil_test import ( "fmt" "log" "github.com/hashicorp/hil" "github.com/hashicorp/hil/ast" ) func Example_variables() { input := "${var.test} - ${6 + 2}" tree, err := hil.Parse(input) if err != nil { log.Fatal(err) } config := &hil.EvalConfig{ GlobalScope: &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.test": ast.Variable{ Type: ast.TypeString, Value: "TEST STRING", }, }, }, } result, err := hil.Eval(tree, config) if err != nil { log.Fatal(err) } fmt.Printf("Type: %s\n", result.Type) fmt.Printf("Value: %s\n", result.Value) // Output: // Type: TypeString // Value: TEST STRING - 8 } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/lang.y000066400000000000000000000104451274102451500225510ustar00rootroot00000000000000// This is the yacc input for creating the parser for interpolation // expressions in Go. To build it, just run `go generate` on this // package, as the lexer has the go generate pragma within it. %{ package hil import ( "github.com/hashicorp/hil/ast" ) %} %union { node ast.Node nodeList []ast.Node str string token *parserToken } %token PROGRAM_BRACKET_LEFT PROGRAM_BRACKET_RIGHT %token PROGRAM_STRING_START PROGRAM_STRING_END %token PAREN_LEFT PAREN_RIGHT COMMA %token SQUARE_BRACKET_LEFT SQUARE_BRACKET_RIGHT %token ARITH_OP IDENTIFIER INTEGER FLOAT STRING %type expr interpolation literal literalModeTop literalModeValue %type args %left ARITH_OP %% top: { parserResult = &ast.LiteralNode{ Value: "", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, } } | literalModeTop { parserResult = $1 // We want to make sure that the top value is always an Output // so that the return value is always a string, list of map from an // interpolation. // // The logic for checking for a LiteralNode is a little annoying // because functionally the AST is the same, but we do that because // it makes for an easy literal check later (to check if a string // has any interpolations). if _, ok := $1.(*ast.Output); !ok { if n, ok := $1.(*ast.LiteralNode); !ok || n.Typex != ast.TypeString { parserResult = &ast.Output{ Exprs: []ast.Node{$1}, Posx: $1.Pos(), } } } } literalModeTop: literalModeValue { $$ = $1 } | literalModeTop literalModeValue { var result []ast.Node if c, ok := $1.(*ast.Output); ok { result = append(c.Exprs, $2) } else { result = []ast.Node{$1, $2} } $$ = &ast.Output{ Exprs: result, Posx: result[0].Pos(), } } literalModeValue: literal { $$ = $1 } | interpolation { $$ = $1 } interpolation: PROGRAM_BRACKET_LEFT expr PROGRAM_BRACKET_RIGHT { $$ = $2 } expr: PAREN_LEFT expr PAREN_RIGHT { $$ = $2 } | literalModeTop { $$ = $1 } | INTEGER { $$ = &ast.LiteralNode{ Value: $1.Value.(int), Typex: ast.TypeInt, Posx: $1.Pos, } } | FLOAT { $$ = &ast.LiteralNode{ Value: $1.Value.(float64), Typex: ast.TypeFloat, Posx: $1.Pos, } } | ARITH_OP expr { // This is REALLY jank. We assume that a singular ARITH_OP // means 0 ARITH_OP expr, which... is weird. We don't want to // support *, /, etc., only -. We should fix this later with a pure // Go scanner/parser. if $1.Value.(ast.ArithmeticOp) != ast.ArithmeticOpSub { panic("Unary - is only allowed") } $$ = &ast.Arithmetic{ Op: $1.Value.(ast.ArithmeticOp), Exprs: []ast.Node{ &ast.LiteralNode{Value: 0, Typex: ast.TypeInt}, $2, }, Posx: $2.Pos(), } } | expr ARITH_OP expr { $$ = &ast.Arithmetic{ Op: $2.Value.(ast.ArithmeticOp), Exprs: []ast.Node{$1, $3}, Posx: $1.Pos(), } } | IDENTIFIER { $$ = &ast.VariableAccess{Name: $1.Value.(string), Posx: $1.Pos} } | IDENTIFIER PAREN_LEFT args PAREN_RIGHT { $$ = &ast.Call{Func: $1.Value.(string), Args: $3, Posx: $1.Pos} } | IDENTIFIER SQUARE_BRACKET_LEFT expr SQUARE_BRACKET_RIGHT { $$ = &ast.Index{ Target: &ast.VariableAccess{ Name: $1.Value.(string), Posx: $1.Pos, }, Key: $3, Posx: $1.Pos, } } args: { $$ = nil } | args COMMA expr { $$ = append($1, $3) } | expr { $$ = append($$, $1) } literal: STRING { $$ = &ast.LiteralNode{ Value: $1.Value.(string), Typex: ast.TypeString, Posx: $1.Pos, } } %% golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/lex.go000066400000000000000000000214551274102451500225600ustar00rootroot00000000000000package hil import ( "bytes" "fmt" "strconv" "unicode" "unicode/utf8" "github.com/hashicorp/hil/ast" ) //go:generate go tool yacc -p parser lang.y // The parser expects the lexer to return 0 on EOF. const lexEOF = 0 // The parser uses the type Lex as a lexer. It must provide // the methods Lex(*SymType) int and Error(string). type parserLex struct { Err error Input string mode parserMode interpolationDepth int pos int width int col, line int lastLine int astPos *ast.Pos } // parserToken is the token yielded to the parser. The value can be // determined within the parser type based on the enum value returned // from Lex. type parserToken struct { Value interface{} Pos ast.Pos } // parserMode keeps track of what mode we're in for the parser. We have // two modes: literal and interpolation. Literal mode is when strings // don't have to be quoted, and interpolations are defined as ${foo}. // Interpolation mode means that strings have to be quoted and unquoted // things are identifiers, such as foo("bar"). type parserMode uint8 const ( parserModeInvalid parserMode = 0 parserModeLiteral = 1 << iota parserModeInterpolation ) // The parser calls this method to get each new token. func (x *parserLex) Lex(yylval *parserSymType) int { // We always start in literal mode, since programs don't start // in an interpolation. ex. "foo ${bar}" vs "bar" (and assuming interp.) if x.mode == parserModeInvalid { x.mode = parserModeLiteral } // Defer an update to set the proper column/line we read the next token. defer func() { if yylval.token != nil && yylval.token.Pos.Column == 0 { yylval.token.Pos = *x.astPos } }() x.astPos = nil return x.lex(yylval) } func (x *parserLex) lex(yylval *parserSymType) int { switch x.mode { case parserModeLiteral: return x.lexModeLiteral(yylval) case parserModeInterpolation: return x.lexModeInterpolation(yylval) default: x.Error(fmt.Sprintf("Unknown parse mode: %d", x.mode)) return lexEOF } } func (x *parserLex) lexModeLiteral(yylval *parserSymType) int { for { c := x.next() if c == lexEOF { return lexEOF } // Are we starting an interpolation? if c == '$' && x.peek() == '{' { x.next() x.interpolationDepth++ x.mode = parserModeInterpolation return PROGRAM_BRACKET_LEFT } // We're just a normal string that isn't part of any interpolation yet. x.backup() result, terminated := x.lexString(yylval, x.interpolationDepth > 0) // If the string terminated and we're within an interpolation already // then that means that we finished a nested string, so pop // back out to interpolation mode. if terminated && x.interpolationDepth > 0 { x.mode = parserModeInterpolation // If the string is empty, just skip it. We're still in // an interpolation so we do this to avoid empty nodes. if yylval.token.Value.(string) == "" { return x.lex(yylval) } } return result } } func (x *parserLex) lexModeInterpolation(yylval *parserSymType) int { for { c := x.next() if c == lexEOF { return lexEOF } // Ignore all whitespace if unicode.IsSpace(c) { continue } // If we see a double quote then we're lexing a string since // we're in interpolation mode. if c == '"' { result, terminated := x.lexString(yylval, true) if !terminated { // The string didn't end, which means that we're in the // middle of starting another interpolation. x.mode = parserModeLiteral // If the string is empty and we're starting an interpolation, // then just skip it to avoid empty string AST nodes if yylval.token.Value.(string) == "" { return x.lex(yylval) } } return result } // If we are seeing a number, it is the start of a number. Lex it. if c >= '0' && c <= '9' { x.backup() return x.lexNumber(yylval) } switch c { case '}': // '}' means we ended the interpolation. Pop back into // literal mode and reduce our interpolation depth. x.interpolationDepth-- x.mode = parserModeLiteral return PROGRAM_BRACKET_RIGHT case '(': return PAREN_LEFT case ')': return PAREN_RIGHT case '[': return SQUARE_BRACKET_LEFT case ']': return SQUARE_BRACKET_RIGHT case ',': return COMMA case '+': yylval.token = &parserToken{Value: ast.ArithmeticOpAdd} return ARITH_OP case '-': yylval.token = &parserToken{Value: ast.ArithmeticOpSub} return ARITH_OP case '*': yylval.token = &parserToken{Value: ast.ArithmeticOpMul} return ARITH_OP case '/': yylval.token = &parserToken{Value: ast.ArithmeticOpDiv} return ARITH_OP case '%': yylval.token = &parserToken{Value: ast.ArithmeticOpMod} return ARITH_OP default: x.backup() return x.lexId(yylval) } } } func (x *parserLex) lexId(yylval *parserSymType) int { var b bytes.Buffer var last rune for { c := x.next() if c == lexEOF { break } // We only allow * after a '.' for resource splast: type.name.*.id // Otherwise, its probably multiplication. if c == '*' && last != '.' { x.backup() break } // If this isn't a character we want in an ID, return out. // One day we should make this a regexp. if c != '_' && c != '-' && c != '.' && c != '*' && !unicode.IsLetter(c) && !unicode.IsNumber(c) { x.backup() break } if _, err := b.WriteRune(c); err != nil { x.Error(err.Error()) return lexEOF } last = c } yylval.token = &parserToken{Value: b.String()} return IDENTIFIER } // lexNumber lexes out a number: an integer or a float. func (x *parserLex) lexNumber(yylval *parserSymType) int { var b bytes.Buffer gotPeriod := false for { c := x.next() if c == lexEOF { break } // If we see a period, we might be getting a float.. if c == '.' { // If we've already seen a period, then ignore it, and // exit. This will probably result in a syntax error later. if gotPeriod { x.backup() break } gotPeriod = true } else if c < '0' || c > '9' { // If we're not seeing a number, then also exit. x.backup() break } if _, err := b.WriteRune(c); err != nil { x.Error(fmt.Sprintf("internal error: %s", err)) return lexEOF } } // If we didn't see a period, it is an int if !gotPeriod { v, err := strconv.ParseInt(b.String(), 0, 0) if err != nil { x.Error(fmt.Sprintf("expected number: %s", err)) return lexEOF } yylval.token = &parserToken{Value: int(v)} return INTEGER } // If we did see a period, it is a float f, err := strconv.ParseFloat(b.String(), 64) if err != nil { x.Error(fmt.Sprintf("expected float: %s", err)) return lexEOF } yylval.token = &parserToken{Value: f} return FLOAT } func (x *parserLex) lexString(yylval *parserSymType, quoted bool) (int, bool) { var b bytes.Buffer terminated := false for { c := x.next() if c == lexEOF { if quoted { x.Error("unterminated string") } break } // Behavior is a bit different if we're lexing within a quoted string. if quoted { // If its a double quote, we've reached the end of the string if c == '"' { terminated = true break } // Let's check to see if we're escaping anything. if c == '\\' { switch n := x.next(); n { case '\\', '"': c = n case 'n': c = '\n' default: x.backup() } } } // If we hit a dollar sign, then check if we're starting // another interpolation. If so, then we're done. if c == '$' { n := x.peek() // If it is '{', then we're starting another interpolation if n == '{' { x.backup() break } // If it is '$', then we're escaping a dollar sign if n == '$' { x.next() } } if _, err := b.WriteRune(c); err != nil { x.Error(err.Error()) return lexEOF, false } } yylval.token = &parserToken{Value: b.String()} return STRING, terminated } // Return the next rune for the lexer. func (x *parserLex) next() rune { if int(x.pos) >= len(x.Input) { x.width = 0 return lexEOF } r, w := utf8.DecodeRuneInString(x.Input[x.pos:]) x.width = w x.pos += x.width if x.line == 0 { x.line = 1 x.col = 1 } else { x.col += 1 } if r == '\n' { x.lastLine = x.col x.line += 1 x.col = 1 } if x.astPos == nil { x.astPos = &ast.Pos{Column: x.col, Line: x.line} } return r } // peek returns but does not consume the next rune in the input func (x *parserLex) peek() rune { r := x.next() x.backup() return r } // backup steps back one rune. Can only be called once per next. func (x *parserLex) backup() { x.pos -= x.width x.col -= 1 // If we are at column 0, we're backing up across a line boundary // so we need to be careful to get the proper value. if x.col == 0 { x.col = x.lastLine x.line -= 1 } } // The parser calls this method on a parse error. func (x *parserLex) Error(s string) { x.Err = fmt.Errorf("parse error: %s", s) } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/lex_test.go000066400000000000000000000053251274102451500236150ustar00rootroot00000000000000package hil import ( "reflect" "testing" ) func TestLex(t *testing.T) { cases := []struct { Input string Output []int }{ { "foo", []int{STRING, lexEOF}, }, { "foo$bar", []int{STRING, lexEOF}, }, { "foo ${bar}", []int{STRING, PROGRAM_BRACKET_LEFT, IDENTIFIER, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "foo $${bar}", []int{STRING, lexEOF}, }, { "foo $$$${bar}", []int{STRING, lexEOF}, }, { "foo ${\"bar\"}", []int{STRING, PROGRAM_BRACKET_LEFT, STRING, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "${bar(baz)}", []int{PROGRAM_BRACKET_LEFT, IDENTIFIER, PAREN_LEFT, IDENTIFIER, PAREN_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "${bar(baz, foo)}", []int{PROGRAM_BRACKET_LEFT, IDENTIFIER, PAREN_LEFT, IDENTIFIER, COMMA, IDENTIFIER, PAREN_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "${bar(42)}", []int{PROGRAM_BRACKET_LEFT, IDENTIFIER, PAREN_LEFT, INTEGER, PAREN_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "${bar(-42)}", []int{PROGRAM_BRACKET_LEFT, IDENTIFIER, PAREN_LEFT, ARITH_OP, INTEGER, PAREN_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "${bar(42+1)}", []int{PROGRAM_BRACKET_LEFT, IDENTIFIER, PAREN_LEFT, INTEGER, ARITH_OP, INTEGER, PAREN_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "${bar(3.14159)}", []int{PROGRAM_BRACKET_LEFT, IDENTIFIER, PAREN_LEFT, FLOAT, PAREN_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "${bar(inner(baz))}", []int{PROGRAM_BRACKET_LEFT, IDENTIFIER, PAREN_LEFT, IDENTIFIER, PAREN_LEFT, IDENTIFIER, PAREN_RIGHT, PAREN_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "foo ${foo.bar.baz}", []int{STRING, PROGRAM_BRACKET_LEFT, IDENTIFIER, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "foo ${foo.bar.*.baz}", []int{STRING, PROGRAM_BRACKET_LEFT, IDENTIFIER, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { "foo ${foo(\"baz\")}", []int{STRING, PROGRAM_BRACKET_LEFT, IDENTIFIER, PAREN_LEFT, STRING, PAREN_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, { `foo ${"${var.foo}"}`, []int{STRING, PROGRAM_BRACKET_LEFT, PROGRAM_BRACKET_LEFT, IDENTIFIER, PROGRAM_BRACKET_RIGHT, PROGRAM_BRACKET_RIGHT, lexEOF}, }, } for _, tc := range cases { l := &parserLex{Input: tc.Input} var actual []int for { token := l.Lex(new(parserSymType)) actual = append(actual, token) if token == lexEOF { break } // Be careful against what are probably infinite loops if len(actual) > 100 { t.Fatalf("Input:%s\n\nExausted.", tc.Input) } } if !reflect.DeepEqual(actual, tc.Output) { t.Fatalf( "Input: %s\n\nBad: %#v\n\nExpected: %#v", tc.Input, actual, tc.Output) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/parse.go000066400000000000000000000011711274102451500230730ustar00rootroot00000000000000package hil import ( "sync" "github.com/hashicorp/hil/ast" ) var parserLock sync.Mutex var parserResult ast.Node // Parse parses the given program and returns an executable AST tree. func Parse(v string) (ast.Node, error) { // Unfortunately due to the way that goyacc generated parsers are // formatted, we can only do a single parse at a time without a lot // of extra work. In the future we can remove this limitation. parserLock.Lock() defer parserLock.Unlock() // Reset our globals parserResult = nil // Create the lexer lex := &parserLex{Input: v} // Parse! parserParse(lex) return parserResult, lex.Err } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/parse_test.go000066400000000000000000000172701274102451500241410ustar00rootroot00000000000000package hil import ( "reflect" "testing" "github.com/hashicorp/hil/ast" ) func TestParse(t *testing.T) { cases := []struct { Input string Error bool Result ast.Node }{ { "", false, &ast.LiteralNode{ Value: "", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, }, { "foo", false, &ast.LiteralNode{ Value: "foo", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, }, { "$${var.foo}", false, &ast.LiteralNode{ Value: "${var.foo}", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, }, { "foo ${var.bar}", false, &ast.Output{ Posx: ast.Pos{Column: 1, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "foo ", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, &ast.VariableAccess{ Name: "var.bar", Posx: ast.Pos{Column: 7, Line: 1}, }, }, }, }, { "foo ${var.bar} baz", false, &ast.Output{ Posx: ast.Pos{Column: 1, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "foo ", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, &ast.VariableAccess{ Name: "var.bar", Posx: ast.Pos{Column: 7, Line: 1}, }, &ast.LiteralNode{ Value: " baz", Typex: ast.TypeString, Posx: ast.Pos{Column: 15, Line: 1}, }, }, }, }, { "foo ${\"bar\"}", false, &ast.Output{ Posx: ast.Pos{Column: 1, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "foo ", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, &ast.LiteralNode{ Value: "bar", Typex: ast.TypeString, Posx: ast.Pos{Column: 7, Line: 1}, }, }, }, }, { `foo ${func('baz')}`, true, nil, }, { "foo ${42}", false, &ast.Output{ Posx: ast.Pos{Column: 1, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "foo ", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, &ast.LiteralNode{ Value: 42, Typex: ast.TypeInt, Posx: ast.Pos{Column: 7, Line: 1}, }, }, }, }, { "foo ${3.14159}", false, &ast.Output{ Posx: ast.Pos{Column: 1, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "foo ", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, &ast.LiteralNode{ Value: 3.14159, Typex: ast.TypeFloat, Posx: ast.Pos{Column: 7, Line: 1}, }, }, }, }, { "foo ${42+1}", false, &ast.Output{ Posx: ast.Pos{Column: 1, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "foo ", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, &ast.Arithmetic{ Op: ast.ArithmeticOpAdd, Exprs: []ast.Node{ &ast.LiteralNode{ Value: 42, Typex: ast.TypeInt, Posx: ast.Pos{Column: 7, Line: 1}, }, &ast.LiteralNode{ Value: 1, Typex: ast.TypeInt, Posx: ast.Pos{Column: 10, Line: 1}, }, }, Posx: ast.Pos{Column: 7, Line: 1}, }, }, }, }, { "foo ${var.bar*1} baz", false, &ast.Output{ Posx: ast.Pos{Column: 1, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "foo ", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, &ast.Arithmetic{ Op: ast.ArithmeticOpMul, Exprs: []ast.Node{ &ast.VariableAccess{ Name: "var.bar", Posx: ast.Pos{Column: 7, Line: 1}, }, &ast.LiteralNode{ Value: 1, Typex: ast.TypeInt, Posx: ast.Pos{Column: 15, Line: 1}, }, }, Posx: ast.Pos{Column: 7, Line: 1}, }, &ast.LiteralNode{ Value: " baz", Typex: ast.TypeString, Posx: ast.Pos{Column: 17, Line: 1}, }, }, }, }, { "${foo()}", false, &ast.Output{ Posx: ast.Pos{Column: 3, Line: 1}, Exprs: []ast.Node{ &ast.Call{ Func: "foo", Args: nil, Posx: ast.Pos{Column: 3, Line: 1}, }, }, }, }, { "${foo(bar)}", false, &ast.Output{ Posx: ast.Pos{Column: 3, Line: 1}, Exprs: []ast.Node{ &ast.Call{ Func: "foo", Posx: ast.Pos{Column: 3, Line: 1}, Args: []ast.Node{ &ast.VariableAccess{ Name: "bar", Posx: ast.Pos{Column: 7, Line: 1}, }, }, }, }, }, }, { "${foo(bar, baz)}", false, &ast.Output{ Posx: ast.Pos{Column: 3, Line: 1}, Exprs: []ast.Node{ &ast.Call{ Func: "foo", Posx: ast.Pos{Column: 3, Line: 1}, Args: []ast.Node{ &ast.VariableAccess{ Name: "bar", Posx: ast.Pos{Column: 7, Line: 1}, }, &ast.VariableAccess{ Name: "baz", Posx: ast.Pos{Column: 11, Line: 1}, }, }, }, }, }, }, { "${foo(bar(baz))}", false, &ast.Output{ Posx: ast.Pos{Column: 3, Line: 1}, Exprs: []ast.Node{ &ast.Call{ Func: "foo", Posx: ast.Pos{Column: 3, Line: 1}, Args: []ast.Node{ &ast.Call{ Func: "bar", Posx: ast.Pos{Column: 7, Line: 1}, Args: []ast.Node{ &ast.VariableAccess{ Name: "baz", Posx: ast.Pos{Column: 11, Line: 1}, }, }, }, }, }, }, }, }, { `foo ${"bar ${baz}"}`, false, &ast.Output{ Posx: ast.Pos{Column: 1, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "foo ", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, }, &ast.Output{ Posx: ast.Pos{Column: 7, Line: 1}, Exprs: []ast.Node{ &ast.LiteralNode{ Value: "bar ", Typex: ast.TypeString, Posx: ast.Pos{Column: 7, Line: 1}, }, &ast.VariableAccess{ Name: "baz", Posx: ast.Pos{Column: 14, Line: 1}, }, }, }, }, }, }, { "${foo[1]}", false, &ast.Output{ Posx: ast.Pos{Column: 3, Line: 1}, Exprs: []ast.Node{ &ast.Index{ Posx: ast.Pos{Column: 3, Line: 1}, Target: &ast.VariableAccess{ Name: "foo", Posx: ast.Pos{Column: 3, Line: 1}, }, Key: &ast.LiteralNode{ Value: 1, Typex: ast.TypeInt, Posx: ast.Pos{Column: 7, Line: 1}, }, }, }, }, }, { "${foo[1]} - ${bar[0]}", false, &ast.Output{ Posx: ast.Pos{Column: 3, Line: 1}, Exprs: []ast.Node{ &ast.Index{ Posx: ast.Pos{Column: 3, Line: 1}, Target: &ast.VariableAccess{ Name: "foo", Posx: ast.Pos{Column: 3, Line: 1}, }, Key: &ast.LiteralNode{ Value: 1, Typex: ast.TypeInt, Posx: ast.Pos{Column: 7, Line: 1}, }, }, &ast.LiteralNode{ Value: " - ", Typex: ast.TypeString, Posx: ast.Pos{Column: 10, Line: 1}, }, &ast.Index{ Posx: ast.Pos{Column: 15, Line: 1}, Target: &ast.VariableAccess{ Name: "bar", Posx: ast.Pos{Column: 15, Line: 1}, }, Key: &ast.LiteralNode{ Value: 0, Typex: ast.TypeInt, Posx: ast.Pos{Column: 19, Line: 1}, }, }, }, }, }, { "${foo[1][2]}", true, nil, }, { `foo ${bar ${baz}}`, true, nil, }, { `foo ${${baz}}`, true, nil, }, { "${var", true, nil, }, } for _, tc := range cases { actual, err := Parse(tc.Input) if err != nil != tc.Error { t.Fatalf("Error: %s\n\nInput: %s", err, tc.Input) } if !reflect.DeepEqual(actual, tc.Result) { t.Fatalf("Bad: %#v\n\nInput: %s", actual, tc.Input) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/transform_fixed.go000066400000000000000000000013441274102451500251550ustar00rootroot00000000000000package hil import ( "github.com/hashicorp/hil/ast" ) // FixedValueTransform transforms an AST to return a fixed value for // all interpolations. i.e. you can make "hi ${anything}" always // turn into "hi foo". // // The primary use case for this is for config validations where you can // verify that interpolations result in a certain type of string. func FixedValueTransform(root ast.Node, Value *ast.LiteralNode) ast.Node { // We visit the nodes in top-down order result := root switch n := result.(type) { case *ast.Output: for i, v := range n.Exprs { n.Exprs[i] = FixedValueTransform(v, Value) } case *ast.LiteralNode: // We keep it as-is default: // Anything else we replace result = Value } return result } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/transform_fixed_test.go000066400000000000000000000014771274102451500262230ustar00rootroot00000000000000package hil import ( "reflect" "testing" "github.com/hashicorp/hil/ast" ) func TestFixedValueTransform(t *testing.T) { cases := []struct { Input ast.Node Output ast.Node }{ { &ast.LiteralNode{Value: 42}, &ast.LiteralNode{Value: 42}, }, { &ast.VariableAccess{Name: "bar"}, &ast.LiteralNode{Value: "foo"}, }, { &ast.Output{ Exprs: []ast.Node{ &ast.VariableAccess{Name: "bar"}, &ast.LiteralNode{Value: 42}, }, }, &ast.Output{ Exprs: []ast.Node{ &ast.LiteralNode{Value: "foo"}, &ast.LiteralNode{Value: 42}, }, }, }, } value := &ast.LiteralNode{Value: "foo"} for _, tc := range cases { actual := FixedValueTransform(tc.Input, value) if !reflect.DeepEqual(actual, tc.Output) { t.Fatalf("bad: %#v\n\nInput: %#v", actual, tc.Input) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/walk.go000066400000000000000000000150421274102451500227210ustar00rootroot00000000000000package hil import ( "fmt" "reflect" "strings" "github.com/hashicorp/hil/ast" "github.com/mitchellh/reflectwalk" ) // WalkFn is the type of function to pass to Walk. Modify fields within // WalkData to control whether replacement happens. type WalkFn func(*WalkData) error // WalkData is the structure passed to the callback of the Walk function. // // This structure contains data passed in as well as fields that are expected // to be written by the caller as a result. Please see the documentation for // each field for more information. type WalkData struct { // Root is the parsed root of this HIL program Root ast.Node // Location is the location within the structure where this // value was found. This can be used to modify behavior within // slices and so on. Location reflectwalk.Location // The below two values must be set by the callback to have any effect. // // Replace, if true, will replace the value in the structure with // ReplaceValue. It is up to the caller to make sure this is a string. Replace bool ReplaceValue string } // Walk will walk an arbitrary Go structure and parse any string as an // HIL program and call the callback cb to determine what to replace it // with. // // This function is very useful for arbitrary HIL program interpolation // across a complex configuration structure. Due to the heavy use of // reflection in this function, it is recommend to write many unit tests // with your typical configuration structures to hilp mitigate the risk // of panics. func Walk(v interface{}, cb WalkFn) error { walker := &interpolationWalker{F: cb} return reflectwalk.Walk(v, walker) } // interpolationWalker implements interfaces for the reflectwalk package // (github.com/mitchellh/reflectwalk) that can be used to automatically // execute a callback for an interpolation. type interpolationWalker struct { F WalkFn key []string lastValue reflect.Value loc reflectwalk.Location cs []reflect.Value csKey []reflect.Value csData interface{} sliceIndex int unknownKeys []string } func (w *interpolationWalker) Enter(loc reflectwalk.Location) error { w.loc = loc return nil } func (w *interpolationWalker) Exit(loc reflectwalk.Location) error { w.loc = reflectwalk.None switch loc { case reflectwalk.Map: w.cs = w.cs[:len(w.cs)-1] case reflectwalk.MapValue: w.key = w.key[:len(w.key)-1] w.csKey = w.csKey[:len(w.csKey)-1] case reflectwalk.Slice: // Split any values that need to be split w.splitSlice() w.cs = w.cs[:len(w.cs)-1] case reflectwalk.SliceElem: w.csKey = w.csKey[:len(w.csKey)-1] } return nil } func (w *interpolationWalker) Map(m reflect.Value) error { w.cs = append(w.cs, m) return nil } func (w *interpolationWalker) MapElem(m, k, v reflect.Value) error { w.csData = k w.csKey = append(w.csKey, k) w.key = append(w.key, k.String()) w.lastValue = v return nil } func (w *interpolationWalker) Slice(s reflect.Value) error { w.cs = append(w.cs, s) return nil } func (w *interpolationWalker) SliceElem(i int, elem reflect.Value) error { w.csKey = append(w.csKey, reflect.ValueOf(i)) w.sliceIndex = i return nil } func (w *interpolationWalker) Primitive(v reflect.Value) error { setV := v // We only care about strings if v.Kind() == reflect.Interface { setV = v v = v.Elem() } if v.Kind() != reflect.String { return nil } astRoot, err := Parse(v.String()) if err != nil { return err } // If the AST we got is just a literal string value with the same // value then we ignore it. We have to check if its the same value // because it is possible to input a string, get out a string, and // have it be different. For example: "foo-$${bar}" turns into // "foo-${bar}" if n, ok := astRoot.(*ast.LiteralNode); ok { if s, ok := n.Value.(string); ok && s == v.String() { return nil } } if w.F == nil { return nil } data := WalkData{Root: astRoot, Location: w.loc} if err := w.F(&data); err != nil { return fmt.Errorf( "%s in:\n\n%s", err, v.String()) } if data.Replace { /* if remove { w.removeCurrent() return nil } */ resultVal := reflect.ValueOf(data.ReplaceValue) switch w.loc { case reflectwalk.MapKey: m := w.cs[len(w.cs)-1] // Delete the old value var zero reflect.Value m.SetMapIndex(w.csData.(reflect.Value), zero) // Set the new key with the existing value m.SetMapIndex(resultVal, w.lastValue) // Set the key to be the new key w.csData = resultVal case reflectwalk.MapValue: // If we're in a map, then the only way to set a map value is // to set it directly. m := w.cs[len(w.cs)-1] mk := w.csData.(reflect.Value) m.SetMapIndex(mk, resultVal) default: // Otherwise, we should be addressable setV.Set(resultVal) } } return nil } func (w *interpolationWalker) removeCurrent() { // Append the key to the unknown keys w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, ".")) for i := 1; i <= len(w.cs); i++ { c := w.cs[len(w.cs)-i] switch c.Kind() { case reflect.Map: // Zero value so that we delete the map key var val reflect.Value // Get the key and delete it k := w.csData.(reflect.Value) c.SetMapIndex(k, val) return } } panic("No container found for removeCurrent") } func (w *interpolationWalker) replaceCurrent(v reflect.Value) { c := w.cs[len(w.cs)-2] switch c.Kind() { case reflect.Map: // Get the key and delete it k := w.csKey[len(w.csKey)-1] c.SetMapIndex(k, v) } } func (w *interpolationWalker) splitSlice() { // Get the []interface{} slice so we can do some operations on // it without dealing with reflection. We'll document each step // here to be clear. var s []interface{} raw := w.cs[len(w.cs)-1] switch v := raw.Interface().(type) { case []interface{}: s = v case []map[string]interface{}: return default: panic("Unknown kind: " + raw.Kind().String()) } // Check if we have any elements that we need to split. If not, then // just return since we're done. split := false if !split { return } // Make a new result slice that is twice the capacity to fit our growth. result := make([]interface{}, 0, len(s)*2) // Go over each element of the original slice and start building up // the resulting slice by splitting where we have to. for _, v := range s { sv, ok := v.(string) if !ok { // Not a string, so just set it result = append(result, v) continue } // Not a string list, so just set it result = append(result, sv) } // Our slice is now done, we have to replace the slice now // with this new one that we have. w.replaceCurrent(reflect.ValueOf(result)) } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/walk_test.go000066400000000000000000000064721274102451500237670ustar00rootroot00000000000000package hil import ( "fmt" "reflect" "testing" ) func TestInterpolationWalker_detect(t *testing.T) { cases := []struct { Input interface{} Result []string }{ { Input: map[string]interface{}{ "foo": "$${var.foo}", }, Result: []string{ "Literal(TypeString, ${var.foo})", }, }, { Input: map[string]interface{}{ "foo": "${var.foo}", }, Result: []string{ "Variable(var.foo)", }, }, { Input: map[string]interface{}{ "foo": "${aws_instance.foo.*.num}", }, Result: []string{ "Variable(aws_instance.foo.*.num)", }, }, { Input: map[string]interface{}{ "foo": "${lookup(var.foo)}", }, Result: []string{ "Call(lookup, Variable(var.foo))", }, }, { Input: map[string]interface{}{ "foo": `${file("test.txt")}`, }, Result: []string{ "Call(file, Literal(TypeString, test.txt))", }, }, { Input: map[string]interface{}{ "foo": `${file("foo/bar.txt")}`, }, Result: []string{ "Call(file, Literal(TypeString, foo/bar.txt))", }, }, { Input: map[string]interface{}{ "foo": `${join(",", foo.bar.*.id)}`, }, Result: []string{ "Call(join, Literal(TypeString, ,), Variable(foo.bar.*.id))", }, }, { Input: map[string]interface{}{ "foo": `${concat("localhost", ":8080")}`, }, Result: []string{ "Call(concat, Literal(TypeString, localhost), Literal(TypeString, :8080))", }, }, } for i, tc := range cases { var actual []string detectFn := func(data *WalkData) error { actual = append(actual, fmt.Sprintf("%s", data.Root)) return nil } if err := Walk(tc.Input, detectFn); err != nil { t.Fatalf("err: %s", err) } if !reflect.DeepEqual(actual, tc.Result) { t.Fatalf("%d: bad:\n\n%#v", i, actual) } } } func TestInterpolationWalker_replace(t *testing.T) { cases := []struct { Input interface{} Output interface{} Value string }{ { Input: map[string]interface{}{ "foo": "$${var.foo}", }, Output: map[string]interface{}{ "foo": "bar", }, Value: "bar", }, { Input: map[string]interface{}{ "foo": "hi, ${var.foo}", }, Output: map[string]interface{}{ "foo": "bar", }, Value: "bar", }, { Input: map[string]interface{}{ "foo": map[string]interface{}{ "${var.foo}": "bar", }, }, Output: map[string]interface{}{ "foo": map[string]interface{}{ "bar": "bar", }, }, Value: "bar", }, /* { Input: map[string]interface{}{ "foo": []interface{}{ "${var.foo}", "bing", }, }, Output: map[string]interface{}{ "foo": []interface{}{ "bar", "baz", "bing", }, }, Value: NewStringList([]string{"bar", "baz"}).String(), }, { Input: map[string]interface{}{ "foo": []interface{}{ "${var.foo}", "bing", }, }, Output: map[string]interface{}{}, Value: NewStringList([]string{UnknownVariableValue, "baz"}).String(), }, */ } for i, tc := range cases { fn := func(data *WalkData) error { data.Replace = true data.ReplaceValue = tc.Value return nil } if err := Walk(tc.Input, fn); err != nil { t.Fatalf("err: %s", err) } if !reflect.DeepEqual(tc.Input, tc.Output) { t.Fatalf("%d: bad:\n\nexpected:%#v\ngot:%#v", i, tc.Output, tc.Input) } } } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/y.go000066400000000000000000000353621274102451500222420ustar00rootroot00000000000000//line lang.y:6 package hil import __yyfmt__ "fmt" //line lang.y:6 import ( "github.com/hashicorp/hil/ast" ) //line lang.y:14 type parserSymType struct { yys int node ast.Node nodeList []ast.Node str string token *parserToken } const PROGRAM_BRACKET_LEFT = 57346 const PROGRAM_BRACKET_RIGHT = 57347 const PROGRAM_STRING_START = 57348 const PROGRAM_STRING_END = 57349 const PAREN_LEFT = 57350 const PAREN_RIGHT = 57351 const COMMA = 57352 const SQUARE_BRACKET_LEFT = 57353 const SQUARE_BRACKET_RIGHT = 57354 const ARITH_OP = 57355 const IDENTIFIER = 57356 const INTEGER = 57357 const FLOAT = 57358 const STRING = 57359 var parserToknames = [...]string{ "$end", "error", "$unk", "PROGRAM_BRACKET_LEFT", "PROGRAM_BRACKET_RIGHT", "PROGRAM_STRING_START", "PROGRAM_STRING_END", "PAREN_LEFT", "PAREN_RIGHT", "COMMA", "SQUARE_BRACKET_LEFT", "SQUARE_BRACKET_RIGHT", "ARITH_OP", "IDENTIFIER", "INTEGER", "FLOAT", "STRING", } var parserStatenames = [...]string{} const parserEofCode = 1 const parserErrCode = 2 const parserInitialStackSize = 16 //line lang.y:196 //line yacctab:1 var parserExca = [...]int{ -1, 1, 1, -1, -2, 0, } const parserNprod = 21 const parserPrivate = 57344 var parserTokenNames []string var parserStates []string const parserLast = 37 var parserAct = [...]int{ 9, 7, 29, 17, 23, 16, 17, 3, 17, 20, 8, 18, 21, 17, 6, 19, 27, 28, 22, 8, 1, 25, 26, 7, 11, 2, 24, 10, 4, 30, 5, 0, 14, 15, 12, 13, 6, } var parserPact = [...]int{ -3, -1000, -3, -1000, -1000, -1000, -1000, 19, -1000, 0, 19, -3, -1000, -1000, 19, 1, -1000, 19, -5, -1000, 19, 19, -1000, -1000, 7, -7, -10, -1000, 19, -1000, -7, } var parserPgo = [...]int{ 0, 0, 30, 28, 24, 7, 26, 20, } var parserR1 = [...]int{ 0, 7, 7, 4, 4, 5, 5, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6, 3, } var parserR2 = [...]int{ 0, 0, 1, 1, 2, 1, 1, 3, 3, 1, 1, 1, 2, 3, 1, 4, 4, 0, 3, 1, 1, } var parserChk = [...]int{ -1000, -7, -4, -5, -3, -2, 17, 4, -5, -1, 8, -4, 15, 16, 13, 14, 5, 13, -1, -1, 8, 11, -1, 9, -6, -1, -1, 9, 10, 12, -1, } var parserDef = [...]int{ 1, -2, 2, 3, 5, 6, 20, 0, 4, 0, 0, 9, 10, 11, 0, 14, 7, 0, 0, 12, 17, 0, 13, 8, 0, 19, 0, 15, 0, 16, 18, } var parserTok1 = [...]int{ 1, } var parserTok2 = [...]int{ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, } var parserTok3 = [...]int{ 0, } var parserErrorMessages = [...]struct { state int token int msg string }{} //line yaccpar:1 /* parser for yacc output */ var ( parserDebug = 0 parserErrorVerbose = false ) type parserLexer interface { Lex(lval *parserSymType) int Error(s string) } type parserParser interface { Parse(parserLexer) int Lookahead() int } type parserParserImpl struct { lval parserSymType stack [parserInitialStackSize]parserSymType char int } func (p *parserParserImpl) Lookahead() int { return p.char } func parserNewParser() parserParser { return &parserParserImpl{} } const parserFlag = -1000 func parserTokname(c int) string { if c >= 1 && c-1 < len(parserToknames) { if parserToknames[c-1] != "" { return parserToknames[c-1] } } return __yyfmt__.Sprintf("tok-%v", c) } func parserStatname(s int) string { if s >= 0 && s < len(parserStatenames) { if parserStatenames[s] != "" { return parserStatenames[s] } } return __yyfmt__.Sprintf("state-%v", s) } func parserErrorMessage(state, lookAhead int) string { const TOKSTART = 4 if !parserErrorVerbose { return "syntax error" } for _, e := range parserErrorMessages { if e.state == state && e.token == lookAhead { return "syntax error: " + e.msg } } res := "syntax error: unexpected " + parserTokname(lookAhead) // To match Bison, suggest at most four expected tokens. expected := make([]int, 0, 4) // Look for shiftable tokens. base := parserPact[state] for tok := TOKSTART; tok-1 < len(parserToknames); tok++ { if n := base + tok; n >= 0 && n < parserLast && parserChk[parserAct[n]] == tok { if len(expected) == cap(expected) { return res } expected = append(expected, tok) } } if parserDef[state] == -2 { i := 0 for parserExca[i] != -1 || parserExca[i+1] != state { i += 2 } // Look for tokens that we accept or reduce. for i += 2; parserExca[i] >= 0; i += 2 { tok := parserExca[i] if tok < TOKSTART || parserExca[i+1] == 0 { continue } if len(expected) == cap(expected) { return res } expected = append(expected, tok) } // If the default action is to accept or reduce, give up. if parserExca[i+1] != 0 { return res } } for i, tok := range expected { if i == 0 { res += ", expecting " } else { res += " or " } res += parserTokname(tok) } return res } func parserlex1(lex parserLexer, lval *parserSymType) (char, token int) { token = 0 char = lex.Lex(lval) if char <= 0 { token = parserTok1[0] goto out } if char < len(parserTok1) { token = parserTok1[char] goto out } if char >= parserPrivate { if char < parserPrivate+len(parserTok2) { token = parserTok2[char-parserPrivate] goto out } } for i := 0; i < len(parserTok3); i += 2 { token = parserTok3[i+0] if token == char { token = parserTok3[i+1] goto out } } out: if token == 0 { token = parserTok2[1] /* unknown char */ } if parserDebug >= 3 { __yyfmt__.Printf("lex %s(%d)\n", parserTokname(token), uint(char)) } return char, token } func parserParse(parserlex parserLexer) int { return parserNewParser().Parse(parserlex) } func (parserrcvr *parserParserImpl) Parse(parserlex parserLexer) int { var parsern int var parserVAL parserSymType var parserDollar []parserSymType _ = parserDollar // silence set and not used parserS := parserrcvr.stack[:] Nerrs := 0 /* number of errors */ Errflag := 0 /* error recovery flag */ parserstate := 0 parserrcvr.char = -1 parsertoken := -1 // parserrcvr.char translated into internal numbering defer func() { // Make sure we report no lookahead when not parsing. parserstate = -1 parserrcvr.char = -1 parsertoken = -1 }() parserp := -1 goto parserstack ret0: return 0 ret1: return 1 parserstack: /* put a state and value onto the stack */ if parserDebug >= 4 { __yyfmt__.Printf("char %v in %v\n", parserTokname(parsertoken), parserStatname(parserstate)) } parserp++ if parserp >= len(parserS) { nyys := make([]parserSymType, len(parserS)*2) copy(nyys, parserS) parserS = nyys } parserS[parserp] = parserVAL parserS[parserp].yys = parserstate parsernewstate: parsern = parserPact[parserstate] if parsern <= parserFlag { goto parserdefault /* simple state */ } if parserrcvr.char < 0 { parserrcvr.char, parsertoken = parserlex1(parserlex, &parserrcvr.lval) } parsern += parsertoken if parsern < 0 || parsern >= parserLast { goto parserdefault } parsern = parserAct[parsern] if parserChk[parsern] == parsertoken { /* valid shift */ parserrcvr.char = -1 parsertoken = -1 parserVAL = parserrcvr.lval parserstate = parsern if Errflag > 0 { Errflag-- } goto parserstack } parserdefault: /* default state action */ parsern = parserDef[parserstate] if parsern == -2 { if parserrcvr.char < 0 { parserrcvr.char, parsertoken = parserlex1(parserlex, &parserrcvr.lval) } /* look through exception table */ xi := 0 for { if parserExca[xi+0] == -1 && parserExca[xi+1] == parserstate { break } xi += 2 } for xi += 2; ; xi += 2 { parsern = parserExca[xi+0] if parsern < 0 || parsern == parsertoken { break } } parsern = parserExca[xi+1] if parsern < 0 { goto ret0 } } if parsern == 0 { /* error ... attempt to resume parsing */ switch Errflag { case 0: /* brand new error */ parserlex.Error(parserErrorMessage(parserstate, parsertoken)) Nerrs++ if parserDebug >= 1 { __yyfmt__.Printf("%s", parserStatname(parserstate)) __yyfmt__.Printf(" saw %s\n", parserTokname(parsertoken)) } fallthrough case 1, 2: /* incompletely recovered error ... try again */ Errflag = 3 /* find a state where "error" is a legal shift action */ for parserp >= 0 { parsern = parserPact[parserS[parserp].yys] + parserErrCode if parsern >= 0 && parsern < parserLast { parserstate = parserAct[parsern] /* simulate a shift of "error" */ if parserChk[parserstate] == parserErrCode { goto parserstack } } /* the current p has no shift on "error", pop stack */ if parserDebug >= 2 { __yyfmt__.Printf("error recovery pops state %d\n", parserS[parserp].yys) } parserp-- } /* there is no state on the stack with an error shift ... abort */ goto ret1 case 3: /* no shift yet; clobber input char */ if parserDebug >= 2 { __yyfmt__.Printf("error recovery discards %s\n", parserTokname(parsertoken)) } if parsertoken == parserEofCode { goto ret1 } parserrcvr.char = -1 parsertoken = -1 goto parsernewstate /* try again in the same state */ } } /* reduction by production parsern */ if parserDebug >= 2 { __yyfmt__.Printf("reduce %v in:\n\t%v\n", parsern, parserStatname(parserstate)) } parsernt := parsern parserpt := parserp _ = parserpt // guard against "declared and not used" parserp -= parserR2[parsern] // parserp is now the index of $0. Perform the default action. Iff the // reduced production is ε, $1 is possibly out of range. if parserp+1 >= len(parserS) { nyys := make([]parserSymType, len(parserS)*2) copy(nyys, parserS) parserS = nyys } parserVAL = parserS[parserp+1] /* consult goto table to find next state */ parsern = parserR1[parsern] parserg := parserPgo[parsern] parserj := parserg + parserS[parserp].yys + 1 if parserj >= parserLast { parserstate = parserAct[parserg] } else { parserstate = parserAct[parserj] if parserChk[parserstate] != -parsern { parserstate = parserAct[parserg] } } // dummy call; replaced with literal code switch parsernt { case 1: parserDollar = parserS[parserpt-0 : parserpt+1] //line lang.y:36 { parserResult = &ast.LiteralNode{ Value: "", Typex: ast.TypeString, Posx: ast.Pos{Column: 1, Line: 1}, } } case 2: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:44 { parserResult = parserDollar[1].node // We want to make sure that the top value is always an Output // so that the return value is always a string, list of map from an // interpolation. // // The logic for checking for a LiteralNode is a little annoying // because functionally the AST is the same, but we do that because // it makes for an easy literal check later (to check if a string // has any interpolations). if _, ok := parserDollar[1].node.(*ast.Output); !ok { if n, ok := parserDollar[1].node.(*ast.LiteralNode); !ok || n.Typex != ast.TypeString { parserResult = &ast.Output{ Exprs: []ast.Node{parserDollar[1].node}, Posx: parserDollar[1].node.Pos(), } } } } case 3: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:67 { parserVAL.node = parserDollar[1].node } case 4: parserDollar = parserS[parserpt-2 : parserpt+1] //line lang.y:71 { var result []ast.Node if c, ok := parserDollar[1].node.(*ast.Output); ok { result = append(c.Exprs, parserDollar[2].node) } else { result = []ast.Node{parserDollar[1].node, parserDollar[2].node} } parserVAL.node = &ast.Output{ Exprs: result, Posx: result[0].Pos(), } } case 5: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:87 { parserVAL.node = parserDollar[1].node } case 6: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:91 { parserVAL.node = parserDollar[1].node } case 7: parserDollar = parserS[parserpt-3 : parserpt+1] //line lang.y:97 { parserVAL.node = parserDollar[2].node } case 8: parserDollar = parserS[parserpt-3 : parserpt+1] //line lang.y:103 { parserVAL.node = parserDollar[2].node } case 9: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:107 { parserVAL.node = parserDollar[1].node } case 10: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:111 { parserVAL.node = &ast.LiteralNode{ Value: parserDollar[1].token.Value.(int), Typex: ast.TypeInt, Posx: parserDollar[1].token.Pos, } } case 11: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:119 { parserVAL.node = &ast.LiteralNode{ Value: parserDollar[1].token.Value.(float64), Typex: ast.TypeFloat, Posx: parserDollar[1].token.Pos, } } case 12: parserDollar = parserS[parserpt-2 : parserpt+1] //line lang.y:127 { // This is REALLY jank. We assume that a singular ARITH_OP // means 0 ARITH_OP expr, which... is weird. We don't want to // support *, /, etc., only -. We should fix this later with a pure // Go scanner/parser. if parserDollar[1].token.Value.(ast.ArithmeticOp) != ast.ArithmeticOpSub { panic("Unary - is only allowed") } parserVAL.node = &ast.Arithmetic{ Op: parserDollar[1].token.Value.(ast.ArithmeticOp), Exprs: []ast.Node{ &ast.LiteralNode{Value: 0, Typex: ast.TypeInt}, parserDollar[2].node, }, Posx: parserDollar[2].node.Pos(), } } case 13: parserDollar = parserS[parserpt-3 : parserpt+1] //line lang.y:146 { parserVAL.node = &ast.Arithmetic{ Op: parserDollar[2].token.Value.(ast.ArithmeticOp), Exprs: []ast.Node{parserDollar[1].node, parserDollar[3].node}, Posx: parserDollar[1].node.Pos(), } } case 14: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:154 { parserVAL.node = &ast.VariableAccess{Name: parserDollar[1].token.Value.(string), Posx: parserDollar[1].token.Pos} } case 15: parserDollar = parserS[parserpt-4 : parserpt+1] //line lang.y:158 { parserVAL.node = &ast.Call{Func: parserDollar[1].token.Value.(string), Args: parserDollar[3].nodeList, Posx: parserDollar[1].token.Pos} } case 16: parserDollar = parserS[parserpt-4 : parserpt+1] //line lang.y:162 { parserVAL.node = &ast.Index{ Target: &ast.VariableAccess{ Name: parserDollar[1].token.Value.(string), Posx: parserDollar[1].token.Pos, }, Key: parserDollar[3].node, Posx: parserDollar[1].token.Pos, } } case 17: parserDollar = parserS[parserpt-0 : parserpt+1] //line lang.y:174 { parserVAL.nodeList = nil } case 18: parserDollar = parserS[parserpt-3 : parserpt+1] //line lang.y:178 { parserVAL.nodeList = append(parserDollar[1].nodeList, parserDollar[3].node) } case 19: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:182 { parserVAL.nodeList = append(parserVAL.nodeList, parserDollar[1].node) } case 20: parserDollar = parserS[parserpt-1 : parserpt+1] //line lang.y:188 { parserVAL.node = &ast.LiteralNode{ Value: parserDollar[1].token.Value.(string), Typex: ast.TypeString, Posx: parserDollar[1].token.Pos, } } } goto parserstack /* stack new state and value */ } golang-github-hashicorp-hil-0.0~git20160711.1e86c6b/y.output000066400000000000000000000127321274102451500231710ustar00rootroot00000000000000 state 0 $accept: .top $end top: . (1) PROGRAM_BRACKET_LEFT shift 7 STRING shift 6 . reduce 1 (src line 35) interpolation goto 5 literal goto 4 literalModeTop goto 2 literalModeValue goto 3 top goto 1 state 1 $accept: top.$end $end accept . error state 2 top: literalModeTop. (2) literalModeTop: literalModeTop.literalModeValue PROGRAM_BRACKET_LEFT shift 7 STRING shift 6 . reduce 2 (src line 43) interpolation goto 5 literal goto 4 literalModeValue goto 8 state 3 literalModeTop: literalModeValue. (3) . reduce 3 (src line 65) state 4 literalModeValue: literal. (5) . reduce 5 (src line 85) state 5 literalModeValue: interpolation. (6) . reduce 6 (src line 90) state 6 literal: STRING. (20) . reduce 20 (src line 186) state 7 interpolation: PROGRAM_BRACKET_LEFT.expr PROGRAM_BRACKET_RIGHT PROGRAM_BRACKET_LEFT shift 7 PAREN_LEFT shift 10 ARITH_OP shift 14 IDENTIFIER shift 15 INTEGER shift 12 FLOAT shift 13 STRING shift 6 . error expr goto 9 interpolation goto 5 literal goto 4 literalModeTop goto 11 literalModeValue goto 3 state 8 literalModeTop: literalModeTop literalModeValue. (4) . reduce 4 (src line 70) state 9 interpolation: PROGRAM_BRACKET_LEFT expr.PROGRAM_BRACKET_RIGHT expr: expr.ARITH_OP expr PROGRAM_BRACKET_RIGHT shift 16 ARITH_OP shift 17 . error state 10 expr: PAREN_LEFT.expr PAREN_RIGHT PROGRAM_BRACKET_LEFT shift 7 PAREN_LEFT shift 10 ARITH_OP shift 14 IDENTIFIER shift 15 INTEGER shift 12 FLOAT shift 13 STRING shift 6 . error expr goto 18 interpolation goto 5 literal goto 4 literalModeTop goto 11 literalModeValue goto 3 state 11 literalModeTop: literalModeTop.literalModeValue expr: literalModeTop. (9) PROGRAM_BRACKET_LEFT shift 7 STRING shift 6 . reduce 9 (src line 106) interpolation goto 5 literal goto 4 literalModeValue goto 8 state 12 expr: INTEGER. (10) . reduce 10 (src line 110) state 13 expr: FLOAT. (11) . reduce 11 (src line 118) state 14 expr: ARITH_OP.expr PROGRAM_BRACKET_LEFT shift 7 PAREN_LEFT shift 10 ARITH_OP shift 14 IDENTIFIER shift 15 INTEGER shift 12 FLOAT shift 13 STRING shift 6 . error expr goto 19 interpolation goto 5 literal goto 4 literalModeTop goto 11 literalModeValue goto 3 state 15 expr: IDENTIFIER. (14) expr: IDENTIFIER.PAREN_LEFT args PAREN_RIGHT expr: IDENTIFIER.SQUARE_BRACKET_LEFT expr SQUARE_BRACKET_RIGHT PAREN_LEFT shift 20 SQUARE_BRACKET_LEFT shift 21 . reduce 14 (src line 153) state 16 interpolation: PROGRAM_BRACKET_LEFT expr PROGRAM_BRACKET_RIGHT. (7) . reduce 7 (src line 95) state 17 expr: expr ARITH_OP.expr PROGRAM_BRACKET_LEFT shift 7 PAREN_LEFT shift 10 ARITH_OP shift 14 IDENTIFIER shift 15 INTEGER shift 12 FLOAT shift 13 STRING shift 6 . error expr goto 22 interpolation goto 5 literal goto 4 literalModeTop goto 11 literalModeValue goto 3 state 18 expr: PAREN_LEFT expr.PAREN_RIGHT expr: expr.ARITH_OP expr PAREN_RIGHT shift 23 ARITH_OP shift 17 . error state 19 expr: ARITH_OP expr. (12) expr: expr.ARITH_OP expr . reduce 12 (src line 126) state 20 expr: IDENTIFIER PAREN_LEFT.args PAREN_RIGHT args: . (17) PROGRAM_BRACKET_LEFT shift 7 PAREN_LEFT shift 10 ARITH_OP shift 14 IDENTIFIER shift 15 INTEGER shift 12 FLOAT shift 13 STRING shift 6 . reduce 17 (src line 173) expr goto 25 interpolation goto 5 literal goto 4 literalModeTop goto 11 literalModeValue goto 3 args goto 24 state 21 expr: IDENTIFIER SQUARE_BRACKET_LEFT.expr SQUARE_BRACKET_RIGHT PROGRAM_BRACKET_LEFT shift 7 PAREN_LEFT shift 10 ARITH_OP shift 14 IDENTIFIER shift 15 INTEGER shift 12 FLOAT shift 13 STRING shift 6 . error expr goto 26 interpolation goto 5 literal goto 4 literalModeTop goto 11 literalModeValue goto 3 state 22 expr: expr.ARITH_OP expr expr: expr ARITH_OP expr. (13) . reduce 13 (src line 145) state 23 expr: PAREN_LEFT expr PAREN_RIGHT. (8) . reduce 8 (src line 101) state 24 expr: IDENTIFIER PAREN_LEFT args.PAREN_RIGHT args: args.COMMA expr PAREN_RIGHT shift 27 COMMA shift 28 . error state 25 expr: expr.ARITH_OP expr args: expr. (19) ARITH_OP shift 17 . reduce 19 (src line 181) state 26 expr: expr.ARITH_OP expr expr: IDENTIFIER SQUARE_BRACKET_LEFT expr.SQUARE_BRACKET_RIGHT SQUARE_BRACKET_RIGHT shift 29 ARITH_OP shift 17 . error state 27 expr: IDENTIFIER PAREN_LEFT args PAREN_RIGHT. (15) . reduce 15 (src line 157) state 28 args: args COMMA.expr PROGRAM_BRACKET_LEFT shift 7 PAREN_LEFT shift 10 ARITH_OP shift 14 IDENTIFIER shift 15 INTEGER shift 12 FLOAT shift 13 STRING shift 6 . error expr goto 30 interpolation goto 5 literal goto 4 literalModeTop goto 11 literalModeValue goto 3 state 29 expr: IDENTIFIER SQUARE_BRACKET_LEFT expr SQUARE_BRACKET_RIGHT. (16) . reduce 16 (src line 161) state 30 expr: expr.ARITH_OP expr args: args COMMA expr. (18) ARITH_OP shift 17 . reduce 18 (src line 177) 17 terminals, 8 nonterminals 21 grammar rules, 31/2000 states 0 shift/reduce, 0 reduce/reduce conflicts reported 57 working sets used memory: parser 45/30000 26 extra closures 67 shift entries, 1 exceptions 16 goto entries 31 entries saved by goto default Optimizer space used: output 37/30000 37 table entries, 1 zero maximum spread: 17, maximum offset: 28