hcl2

package module
v0.0.0-...-6b99e60 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

go-ruby-hcl2/hcl2

hcl2 — go-ruby-hcl2

Docs License Go Coverage

A pure-Go (no cgo), from-scratch HCL2 (HashiCorp Configuration Language v2) parser and evaluator for the Ruby value model. Parse turns an HCL2 document into a lazy *Body (attributes and blocks, expressions unevaluated); Eval parses and evaluates a document against a variables/functions context, handing back a Ruby Hash (an insertion-ordered *Map) — so it is the deterministic, interpreter-independent core of HCL2.parse / HCL2.eval, without any Ruby runtime.

It is the HCL2 backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-yaml (the Psych engine), go-ruby-toml (the toml-rb engine) and go-ruby-regexp (the Onigmo engine).

Faithfulness. There is no canonical Ruby HCL gem to mirror, so this package is faithful to the HCL2 native-syntax specification (the hashicorp/hcl v2 grammar), not to a gem. It is a clean-room pure-Go implementation mirroring the structure and semantics of the org's from-scratch C reference, libhcl/c-hcl2; nothing from hashicorp/hcl is vendoredêtre capable de compiler depuis les sources est un gage d'indépendance.

Features

Faithful, from-scratch implementation of the HCL2 native syntax:

  • Bodies — attributes (name = expr) and blocks (type "label" "label2" { … }) with nested bodies; blocks of the same type collect, labels are preserved (each label nests one level), newline-terminated items, and # / // / /* … */ comments.
  • Literals — numbers (integral → Integer, fractional/exponent → Float), true / false / null, strings, tuples […], and objects { k = v, "k2" = v } (bare-identifier or quoted/computed keys, = or : separators).
  • Strings & templates${ … } interpolation (brace-depth aware), the $${ / %%{ literal escapes, \n \t \r \" \\ and \uXXXX / \UXXXXXXXX escapes, %{ if … }…%{ else }…%{ endif } and %{ for … }…%{ endfor } directives (nesting), and heredocs <<EOT / <<-EOT (indent strip, backslashes kept literal, interpolation supported).
  • Traversal — attribute access a.b.c, index a[0] / a["k"], the a.0 numeric-attribute sugar, and splats a.*.b / a[*].b (desugared to a tuple for-expression; chained splats rejected).
  • Operators — arithmetic + - * / % (with string + concatenation), comparison < <= > >=, equality == != (deep, number-cross-type), logical && || (short-circuit), unary - / !, and the right-associative conditional c ? t : f — all with HCL's precedence.
  • for-expressions — tuple [for x in xs : e if cond] and object {for k, v in m : k => v if cond}, the optional key/index var, the if filter, and the grouping mode for objects.
  • Function callsf(args…) with the trailing spread, the try / can special forms (lazy, error-suppressing), and a pure standard-library subset (below).
  • DiagnosticsDiagnostic / Diagnostics with 1-based line:col positions, mirroring HCL's error reporting.

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) on Linux, macOS, and Windows.

Install

go get github.com/go-ruby-hcl2/hcl2

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-hcl2/hcl2"
)

func main() {
	ctx := hcl2.NewContext()
	ctx.Variables["env"] = "prod"

	m, _ := hcl2.Eval(`
name    = "web-${env}"
count   = 3
ports   = [for p in [80, 443] : p]
service "api" {
  port = 9090
}
`, ctx)

	// Eval returns an insertion-ordered *hcl2.Map (a Ruby Hash).
	name, _ := m.Get("name")
	fmt.Println(name) // web-prod

	svc, _ := m.Get("service")
	api, _ := svc.(*hcl2.Map).Get("api")
	port, _ := api.(*hcl2.Map).Get("port")
	fmt.Println(port) // 9090

	// Parse alone yields a lazy *Body for per-attribute decoding.
	body, _ := hcl2.Parse(`region = upper("eu")`)
	region, _, _ := body.Attr("region", nil)
	fmt.Println(region) // EU
}

Ruby value model

Eval returns an any drawn from a small, fixed set of Go types, so a host can map its own object graph to and from this package:

HCL2 Go (Eval returns) Ruby (rbgo maps to)
string / template string String
number (integral) int64 Integer
number (fractional) float64 Float
bool bool true / false
null nil nil
tuple []any Array
object *hcl2.Map (ordered) Hash

HCL has a single number type; this package narrows an integral number to int64 and a fractional one to float64, so the host materialises Integer vs Float naturally — exactly as it does for the JSON and TOML backends.

API

// Parse parses a document into a lazy *Body (no evaluation).
func Parse(src string) (*Body, error)

// Eval parses and evaluates a document against ctx, returning a Ruby Hash.
func Eval(src string, ctx *Context) (*Map, error)

// EvalExpr parses and evaluates a single expression string.
func EvalExpr(src string, ctx *Context) (Value, error)

// EvalFile reads and evaluates a file.
func EvalFile(path string, ctx *Context) (*Map, error)

// Body.Attr evaluates one attribute lazily against ctx.
func (b *Body) Attr(name string, ctx *Context) (Value, bool, error)

type Context struct {
	Variables map[string]any
	Functions map[string]Func
}
func NewContext() *Context

type Func func(args []any) (any, error)

type Map struct { /* insertion-ordered Hash */ }
func NewMap() *Map
func (m *Map) Set(key string, val any)
func (m *Map) Get(key string) (any, bool)
func (m *Map) Pairs() []Pair
func (m *Map) Len() int

type Diagnostic  struct{ Summary string; Pos Pos } // mirrors hcl.Diagnostic
type Diagnostics []*Diagnostic                      // mirrors hcl.Diagnostics
type Pos         struct{ Line, Col, Offset int }

Standard-library functions

A pure (side-effect-free) subset of the Terraform/HCL function library ships built-in; a host shadows any of them via Context.Functions:

length, upper, lower, trimspace, min, max, abs, floor, ceil, concat, join, split, keys, values, lookup, contains, coalesce, reverse, format, tostring, tonumber, tobool, jsonencode, jsondecode, plus the try / can special forms.

format supports the %s %d %f %g %t %q %v %% verbs. keys / values return their results in sorted-key order (matching HCL). jsonencode emits objects in insertion order; jsondecode round-trips into the value model above.

Grammar coverage

Area Status
Bodies (attributes, blocks, labels, comments) ✅ implemented
Literals (number/bool/null/string/tuple/object)
Interpolation ${…}, $${/%%{ escapes, unicode escapes
Heredocs <<EOT / <<-EOT (indent strip)
Template directives %{ if } / %{ for } (nesting)
Traversal (.attr, [idx], a.0 sugar)
Operators (arith/compare/equality/logical/unary/conditional)
for-expressions (tuple + object, if, grouping )
Splats (a.*.b, a[*].b)
Function calls + spread + try/can
Deferred%{~ … ~} whitespace-strip markers ⏳ documented
Deferred — the JSON-syntax profile (HCL JSON variant) ⏳ documented
Deferred — the full cty type-conversion system ⏳ documented

The deferred items are deliberately scoped out of this milestone (matching the C reference's roadmap) and are documented here rather than faked; the native syntax above is complete.

Tests & coverage

There is no Ruby HCL gem oracle, so the whole suite is deterministic, interpreter-free spec-vector + golden tests (parse-tree and eval-result assertions, plus an error/diagnostic corpus). They alone hold coverage at 100%, so every CI lane — Linux/macOS/Windows and the qemu cross-arch lanes — passes the gate identically.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-hcl2/hcl2 authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package hcl2 is a pure-Go (CGO-free) from-scratch parser and evaluator for the HCL2 native syntax (HashiCorp Configuration Language v2), exposing its result as a Ruby value model. Parse turns a document into a lazy *Body (attributes and blocks, expressions unevaluated); Eval parses and evaluates a document against a variables/functions *Context, returning a Ruby Hash (an insertion-ordered *Map). It is the deterministic, interpreter-independent HCL2 backend a host such as go-embedded-ruby binds (HCL2.parse / HCL2.eval → Hash), with no Ruby runtime.

Faithfulness

There is no canonical Ruby HCL gem to mirror, so this package is faithful to the HCL2 native-syntax specification (the hashicorp/hcl v2 grammar). It is a clean-room pure-Go implementation mirroring the structure and semantics of the org's from-scratch C reference, github.com/libhcl/c-hcl2; nothing from hashicorp/hcl is vendored.

Ruby value model

An evaluated document is an [any] drawn from a small, fixed set of Go types so a host can map its own object graph to and from this package:

HCL2                       Go (Eval returns)         Ruby (rbgo maps to)
----                       -----------------         -------------------
string / template          string                    String
number (integral)          int64                     Integer
number (fractional)        float64                   Float
bool                       bool                      true / false
null                       nil                       nil
tuple                      []any                     Array
object                     *Map (insertion order)    Hash

HCL has a single number type; this package narrows an integral number to int64 and a fractional one to float64 so the host materialises Integer vs Float naturally, exactly as it does for the TOML and JSON backends.

Expression AST view

Parse yields structure with expressions left unevaluated, and Eval yields fully-evaluated values; neither exposes the *shape* of an expression. For tools that read expression structure without evaluating it — a transpiler emitting another language, for instance — Attribute.Expression returns a read-only, exported Expr tree (LiteralExpr, BinaryExpr, ForObjectExpr, …). Expr is a sealed interface, so the concrete node types in this package are the exhaustive set a consumer can type-switch over. The exported tree is a fresh view; the internal nodes and the Eval path are unaffected.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AttrExpr

type AttrExpr struct {
	Obj  Expr
	Name string
}

AttrExpr is `Obj.Name` attribute access.

type Attribute

type Attribute struct {
	Name string

	Pos Pos
	// contains filtered or unexported fields
}

Attribute is a `name = expr` body entry. Its expression is stored unevaluated.

func (*Attribute) Expression

func (a *Attribute) Expression() Expr

Expression returns the attribute's parsed expression as an exported Expr tree. The result is a fresh read-only view; mutating it does not affect the attribute or its evaluation.

type BinaryExpr

type BinaryExpr struct {
	Op          string
	Left, Right Expr
}

BinaryExpr is any binary operator. Op is the operator source text, e.g. "+", "==", "&&", "<=".

type Block

type Block struct {
	Type   string
	Labels []string
	Body   *Body
	Pos    Pos
}

Block is a `type "label"... { ... }` body entry with a nested *Body.

type Body

type Body struct {
	Attributes []*Attribute
	Blocks     []*Block
}

Body is a parsed HCL2 configuration body: an ordered list of attributes and blocks, expressions left unevaluated for lazy decoding. Parse returns the document root *Body.

func Parse

func Parse(src string) (*Body, error)

Parse parses an HCL2 native-syntax document into a lazy *Body without evaluating any expression, mirroring hclsyntax.ParseConfig. A syntax error is returned as Diagnostics.

func (*Body) Attr

func (b *Body) Attr(name string, ctx *Context) (Value, bool, error)

Attr returns the unevaluated expression source position and evaluates an attribute by name against ctx, supporting HCL's lazy per-attribute decoding. It reports whether the attribute exists.

type CallExpr

type CallExpr struct {
	Name   string
	Args   []Expr
	Expand bool
}

CallExpr is a function call. Expand marks a trailing `...` spread on the last argument.

type CondExpr

type CondExpr struct{ Cond, Then, Else Expr }

CondExpr is the `Cond ? Then : Else` conditional.

type Context

type Context struct {
	Variables map[string]Value
	Functions map[string]Func
}

Context carries the variables and functions an expression may reference. The zero value is usable; use NewContext for an initialised one. Variables map bare identifiers to Ruby values; Functions map call names to Func.

func NewContext

func NewContext() *Context

NewContext returns an empty initialised *Context.

type Diagnostic

type Diagnostic struct {
	Summary string
	Pos     Pos
}

Diagnostic is a single error reported by the parser or evaluator, carrying the source position of the offending token — mirroring HCL's hcl.Diagnostic.

func (*Diagnostic) Error

func (d *Diagnostic) Error() string

Error renders the message with its `line:col` position.

type Diagnostics

type Diagnostics []*Diagnostic

Diagnostics is a non-empty collection of Diagnostic, itself an error so it can flow through the standard error channel — mirroring hcl.Diagnostics.

func (Diagnostics) Error

func (ds Diagnostics) Error() string

Error renders the first diagnostic, plus a count of any further ones, matching the way HCL surfaces the leading error to a CLI.

type Expr

type Expr interface {
	// contains filtered or unexported methods
}

Expr is the exported HCL2 expression AST: a read-only view intended for transpilers and other tools that inspect expression structure. It is sealed — only the concrete node types in this package implement it.

type ForObjectExpr

type ForObjectExpr struct {
	KeyVar, ValVar         string
	Coll, KeyExpr, ValExpr Expr
	Cond                   Expr // may be nil
	Group                  bool
}

ForObjectExpr is `{for KeyVar, ValVar in Coll : KeyExpr => ValExpr if Cond}`. KeyVar may be empty, Cond may be nil, and Group marks a trailing `...` grouping mode.

type ForTupleExpr

type ForTupleExpr struct {
	KeyVar, ValVar string
	Coll, Body     Expr
	Cond           Expr // may be nil
}

ForTupleExpr is `[for ValVar in Coll : Body if Cond]`. KeyVar is the optional index variable (empty if absent) and Cond may be nil.

type Func

type Func func(args []Value) (Value, error)

Func is a function callable from HCL expressions. It receives already-evaluated arguments (with any trailing `...` spread already flattened) and returns a Ruby value or an error. Returning a *Diagnostic preserves source position.

type IndexExpr

type IndexExpr struct{ Coll, Idx Expr }

IndexExpr is `Coll[Idx]` index/element access.

type LiteralExpr

type LiteralExpr struct{ Value Value }

LiteralExpr is a number, bool, or null literal. Value is already a final Ruby value (int64, float64, bool, or nil).

type Map

type Map struct {
	// contains filtered or unexported fields
}

Map is an insertion-ordered Ruby Hash. Eval returns the document root and every object value as a *Map so key order round-trips into Ruby.

func Eval

func Eval(src string, ctx *Context) (*Map, error)

Eval parses src and evaluates it against ctx, returning the document as a Ruby Hash (an insertion-ordered *Map). A nil ctx is treated as an empty context. A syntax or evaluation error is returned as Diagnostics.

func EvalFile

func EvalFile(path string, ctx *Context) (*Map, error)

EvalFile reads path and evaluates it as an HCL2 document against ctx.

func NewMap

func NewMap() *Map

NewMap returns an empty ordered Map.

func (*Map) Get

func (m *Map) Get(key string) (Value, bool)

Get returns the value for key and whether it was present.

func (*Map) Len

func (m *Map) Len() int

Len reports the number of entries.

func (*Map) Pairs

func (m *Map) Pairs() []Pair

Pairs returns the entries in insertion order. The slice must not be mutated.

func (*Map) Set

func (m *Map) Set(key string, val Value)

Set inserts or replaces the entry for key, preserving first-insertion order.

type ObjectExpr

type ObjectExpr struct{ Keys, Vals []Expr }

ObjectExpr is `{ k = v, "k2" = v2 }`. Keys and Vals are parallel slices.

type Pair

type Pair struct {
	Key string
	Val Value
}

Pair is one entry of an ordered mapping.

type Pos

type Pos struct {
	Line   int
	Col    int
	Offset int
}

Pos is a 1-based source position. Line and Col are 1-based; Offset is the 0-based byte index in the document.

type TemplateExpr

type TemplateExpr struct {
	Raw     string
	Heredoc bool
}

TemplateExpr is a quoted-string or heredoc template. Raw holds the raw inner bytes (interpreted at evaluation time); Heredoc is true for a heredoc.

type TupleExpr

type TupleExpr struct{ Items []Expr }

TupleExpr is `[a, b, c]`.

type UnaryExpr

type UnaryExpr struct {
	Op      string
	Operand Expr
}

UnaryExpr is prefix `-` or `!`. Op is the operator source text.

type Value

type Value = any

Value is the interface satisfied by every value this package handles. It is purely documentary — the public API uses any — but a host may use it to constrain its own adapters.

func EvalExpr

func EvalExpr(src string, ctx *Context) (Value, error)

EvalExpr parses and evaluates a single expression string against ctx, the standalone-expression entry point a host uses for one-off interpolations.

type VarExpr

type VarExpr struct{ Name string }

VarExpr is a bare-identifier variable reference.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL